Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@
| `mobile_display_fps_close` | Stop FPS collection and release the tracing connection |

When reactivated, the `agent-device` MCP server exposes the pinned runtime's official operations through compact, validated schemas, including `open`, `snapshot`, `press`, `fill`, `type`, `scroll`, `wait`, `find`, `get`, `is`, `close`, and debugging tools. Their input schemas describe each command. The source [control skill](../skills/agent-device/SKILL.md), currently excluded from the package, explains session ordering and links to the version-matched guide.


## TraceSift annotations

Analysis writes compact results and replaces the latest reference for the input
profile. Remember writes/replaces the reference for a host view. Both declare
`readOnlyHint: false` and `destructiveHint: true` because a previous association
can be overwritten, even though the input profile is not edited.

Explore, Explore payload, and investigation handoff declare `readOnlyHint: false`
and `destructiveHint: false`: they can start a local background service, and
browser Explore also launches the explorer. The two restore tools only read local
files and remain read-only. All seven use `openWorldHint: false`; operations are
confined to local files and the authenticated loopback TraceSift service.
24 changes: 14 additions & 10 deletions src/server/tracesift-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { TraceSiftError } from "./tracesift-client.ts";
// every other tool call sharing this process.
const gzip = promisify(gzipCallback);
const read = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
// Analysis replaces the latest-input reference; remember replaces the host-view reference.
const persist = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
// Service-backed reads can start a local background process; Explore also opens a browser.
const localService = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
const visibility: ("app" | "model")[] = ["app", "model"];
const analysisOutput = {
apiVersion: z.literal(1), analysisId: z.string(), profileType: z.enum(["cpu", "react"]), title: z.string(),
Expand All @@ -30,9 +34,9 @@ function failure(error: unknown, operation: "analyze" | "remember" | "restore" |
export function registerTraceSiftTools(server: McpServer, client: TraceSiftClient) {
registerAppTool(server, "mobile_analyze_profile", {
title: "Analyze a CPU or React profile with TraceSift",
description: "Upload a local CPU profile or React DevTools profiling export to the local TraceSift service and render its measured findings as inline cards, each dividing its task between the features it entered, with an Explore view for the call tree and timeline. Use when the user provides a profile path and asks to investigate performance or find a root cause. Set profileType only when the format is known; auto detects supported JSON formats. After rendering, wait for the user to choose Investigate before changing code.",
description: "Upload a local CPU profile or React DevTools profiling export to the local TraceSift service and render its measured findings as inline cards, each dividing its task between the features it entered, with an Explore view for the call tree and timeline. Use when the user provides a profile path and asks to investigate performance or find a root cause. Set profileType only when the format is known; auto detects supported JSON formats. May start the local TraceSift service, saves analysis files, and replaces the latest analysis reference for this profile input. After rendering, wait for the user to choose Investigate before changing code.",
inputSchema: traceSiftProfileInputSchema.shape,
outputSchema: analysisOutput, annotations: read,
outputSchema: analysisOutput, annotations: persist,
_meta: { ui: { resourceUri: TRACESIFT_URI, visibility } },
}, async ({ path, profileType }, context) => {
const startedAt = performance.now();
Expand All @@ -46,9 +50,9 @@ export function registerTraceSiftTools(server: McpServer, client: TraceSiftClien

registerAppTool(server, "mobile_remember_tracesift_analysis", {
title: "Remember a TraceSift analysis for this card",
description: "Associate an inline TraceSift card's stable host view with its persisted compact analysis so the card can be restored after Codex restarts.",
description: "Associate an inline TraceSift card's stable host view with its persisted compact analysis so the card can be restored after Codex restarts. Writes a local reference file, replacing the previous association for this host view.",
inputSchema: { viewId: z.union([z.string(), z.number()]), analysisId: z.string().min(1).max(256) },
outputSchema: { remembered: z.literal(true) }, annotations: read,
outputSchema: { remembered: z.literal(true) }, annotations: persist,
_meta: { ui: { visibility: ["app"] } },
}, async ({ viewId, analysisId }, context) => {
const startedAt = performance.now();
Expand Down Expand Up @@ -93,13 +97,13 @@ export function registerTraceSiftTools(server: McpServer, client: TraceSiftClien

registerAppTool(server, "mobile_open_tracesift_explore", {
title: "Open a TraceSift finding in its web explorer",
description: "Open one CPU task or React commit through `tracesift explore`. This app-only action is called after the user presses Explore; the persisted compact analysis carries the selection while full Explore data stays in TraceSift instead of crossing MCP.",
description: "Open one CPU task or React commit through `tracesift explore`. May start the local service and opens a browser. This app-only action is called after the user presses Explore; the persisted compact analysis carries the selection while full Explore data stays in TraceSift instead of crossing MCP.",
inputSchema: {
analysisId: z.string().min(1).max(256), taskIndex: z.number().int().nonnegative().optional(),
rootId: z.number().int().nonnegative().optional(), commitIndex: z.number().int().nonnegative().optional(),
focus: z.string().max(16_384).optional(),
},
outputSchema: { opened: z.literal(true) }, annotations: read,
outputSchema: { opened: z.literal(true) }, annotations: localService,
_meta: { ui: { visibility: ["app"] } },
}, async ({ analysisId, taskIndex, rootId, commitIndex, focus }, context) => {
const startedAt = performance.now();
Expand All @@ -118,14 +122,14 @@ export function registerTraceSiftTools(server: McpServer, client: TraceSiftClien

registerAppTool(server, "mobile_tracesift_explore_payload", {
title: "Load the full data one TraceSift finding explores",
description: "Return the complete call tree and timeline for one CPU task, or every commit timeline for one React analysis, so the inline card can render the Explore view itself. This app-only action is called when the user presses Explore; the payload is large, travels compressed as one text item, and is never read back by the model.",
description: "Return the complete call tree and timeline for one CPU task, or every commit timeline for one React analysis, so the inline card can render the Explore view itself. May start the local TraceSift service. This app-only action is called when the user presses Explore; the payload is large, travels compressed as one text item, and is never read back by the model.",
inputSchema: {
analysisId: z.string().min(1).max(256), taskIndex: z.number().int().nonnegative().optional(),
},
// Only the envelope is schema-checked: the tree inside is megabytes the host
// would walk node by node to prove a shape the explorer checks as it draws.
outputSchema: traceSiftExploreEnvelopeSchema.shape,
annotations: read,
annotations: localService,
_meta: { ui: { visibility: ["app"] } },
}, async ({ analysisId, taskIndex }, context) => {
const startedAt = performance.now();
Expand Down Expand Up @@ -163,9 +167,9 @@ export function registerTraceSiftTools(server: McpServer, client: TraceSiftClien

registerAppTool(server, "mobile_get_tracesift_handoff", {
title: "Get a TraceSift card handoff",
description: "Return the authoritative TraceSift investigation handoff for a selected card. This app-only action is called by the inline card after the user presses Investigate.",
description: "Return the authoritative TraceSift investigation handoff for a selected card. May start the local TraceSift service. This app-only action is called by the inline card after the user presses Investigate.",
inputSchema: { analysisId: z.string().min(1).max(256), cardId: z.string().min(1).max(256) },
outputSchema: traceSiftHandoffSchema.shape, annotations: read,
outputSchema: traceSiftHandoffSchema.shape, annotations: localService,
_meta: { ui: { visibility: ["app"] } },
}, async ({ analysisId, cardId }, context) => {
const startedAt = performance.now();
Expand Down
22 changes: 22 additions & 0 deletions tests/tracesift-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,25 @@ test("the Explore payload tool refuses a tree too deep to serialize", async () =
assert.match((result.content as { text: string }[])[0]!.text, /larger than the panel can open/);
await close();
});


test("published TraceSift annotations distinguish saved references, service startup, and file-only restores", async t => {
const { mcp, close } = await connect({});
t.after(close);
const { tools } = await mcp.listTools();
const expected = {
mobile_analyze_profile: [false, true],
mobile_remember_tracesift_analysis: [false, true],
mobile_restore_tracesift_analysis: [true, false],
mobile_restore_tracesift_analysis_by_input: [true, false],
mobile_open_tracesift_explore: [false, false],
mobile_tracesift_explore_payload: [false, false],
mobile_get_tracesift_handoff: [false, false],
};
assert.equal(tools.length, Object.keys(expected).length);
for (const [name, [readOnlyHint, destructiveHint]] of Object.entries(expected)) {
const tool = tools.find(tool => tool.name === name);
assert.ok(tool, `Missing ${name}`);
assert.deepEqual(tool.annotations, { readOnlyHint, destructiveHint, openWorldHint: false }, name);
}
});
Loading