#!/usr/bin/env vpython3
# Copyright 2026 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""TraceProcessor MCP server."""
import contextlib
import pathlib
import sys
# Add third_party/perfetto/python to sys.path
SRC_DIR = pathlib.Path(__file__).resolve().parents[3]
PERFETTO_PY_DIR = SRC_DIR / 'third_party/perfetto/python'
if not PERFETTO_PY_DIR.exists():
print(
f"Error: Perfetto Python module not found at {PERFETTO_PY_DIR}",
file=sys.stderr)
sys.exit(1)
if str(PERFETTO_PY_DIR) not in sys.path:
sys.path.insert(0, str(PERFETTO_PY_DIR))
from perfetto.trace_processor import TraceProcessor
from mcp.server import fastmcp
from mcp.server.fastmcp.exceptions import ToolError
# TODO: replace this with the native perfetto skill and helpers once they
# are more mature.
mcp = fastmcp.FastMCP('trace-processor')
_tp_instance: TraceProcessor | None = None
_current_trace_path: pathlib.Path | None = None
def _assert_active_session() -> TraceProcessor:
if not _tp_instance:
raise ToolError(
"No active TraceProcessor session. Please call 'launch' first "
"with a valid trace path.")
return _tp_instance
@mcp.tool()
def launch(trace_path: str) -> dict:
"""Launch or reuse a long-running Perfetto TraceProcessor session on an
existing trace file. Must be called first before running any queries.
Args:
trace_path: Path to the Perfetto trace file to analyze.
"""
global _tp_instance, _current_trace_path
abs_path = pathlib.Path(trace_path).expanduser().resolve()
if not abs_path.exists():
raise ToolError(f"Trace file does not exist at {abs_path}")
if _tp_instance:
if _current_trace_path == abs_path:
return {
"status": "reused",
"trace_path": str(abs_path),
"message": f"Reusing existing active session for {abs_path}",
}
else:
with contextlib.suppress(Exception):
_tp_instance.close()
_tp_instance = None
_current_trace_path = None
try:
_tp_instance = TraceProcessor(trace=str(abs_path))
_current_trace_path = abs_path
return {
"status": "launched",
"trace_path": str(abs_path),
"message": f"Successfully launched new session for {abs_path}",
}
except Exception as e:
raise ToolError(f"Failed to launch TraceProcessor session: {str(e)}") from e
@mcp.tool()
def query(sql: str, limit: int = 100) -> dict:
"""Execute a PerfettoSQL query against the active trace session. Returns a
list of result rows up to the specified limit.
Args:
sql: PerfettoSQL query string to execute against the active session.
limit: Maximum number of rows to return (default: 100).
"""
try:
tp = _assert_active_session()
res = tp.query(sql.strip())
df = res.as_pandas_dataframe()
total_rows = len(df)
truncated = total_rows > limit
df = df.head(limit)
rows = []
if not df.empty:
rows = df.to_dict(orient="records")
return {
"rows": rows,
"truncated": truncated,
"count": total_rows,
"limit": limit,
}
except ToolError:
raise
except Exception as e:
raise ToolError(f"Error executing query: {str(e)}") from e
if __name__ == '__main__':
mcp.run()