1export type T3McpToolLogo = "t3-code";
2
3export interface T3McpToolPresentation {
4 readonly displayName: string;
5 readonly logo: T3McpToolLogo;
6}
7
8export type T3McpToolSummaryAction =
9 | "capabilities"
10 | "delegate"
11 | "task-status"
12 | "task-cancel"
13 | "schedule-run"
14 | "schedule-create"
15 | "schedule-list"
16 | "schedule-update"
17 | "schedule-delete"
18 | "thread-create"
19 | "thread-list"
20 | "thread-read"
21 | "thread-send"
22 | "thread-wait"
23 | "thread-interrupt"
24 | "workflow-stop"
25 | "background-wait"
26 | "thread-configuration"
27 | "thread-configure"
28 | "thread-fork"
29 | "thread-merge"
30 | "thread-search"
31 | "thread-transfers"
32 | "thread-organize"
33 | "thread-update"
34 | "queue-list"
35 | "queue-read"
36 | "queue-edit"
37 | "queue-cancel"
38 | "queue-reorder"
39 | "queue-steer"
40 | "question-list"
41 | "question-read"
42 | "question-respond"
43 | "secret-request"
44 | "worktree-handoff"
45 | "worktree-list"
46 | "worktree-status"
47 | "project-list"
48 | "project-read"
49 | "project-create"
50 | "project-update"
51 | "project-delete"
52 | "project-clone"
53 | "environment-read"
54 | "environment-links"
55 | "environment-link"
56 | "environment-unlink"
57 | "environment-update"
58 | "attachment-prepare"
59 | "attachment-discard"
60 | "attachment-send"
61 | "link-pr"
62 | "unlink-pr"
63 | "list-prs"
64 | "watch-pr"
65 | "unwatch-pr"
66 | "write-pr-guides"
67 | "read-pr-guide"
68 | "browser"
69 | "device"
70 | "html-preview"
71 | "html-render";
72
73export interface T3McpToolDefinition {
74 readonly displayName: string;
75 readonly labels: readonly [action: string, running: string, completed: string, detail: string];
76 readonly icon: "t3-code" | "browser" | "device" | "pull-request";
77 readonly summaryAction: T3McpToolSummaryAction;
78}
79
80function tool(
81 labels: T3McpToolDefinition["labels"],
82 summaryAction: T3McpToolSummaryAction,
83 icon: T3McpToolDefinition["icon"] = "t3-code",
84 displayName = `${labels[0]} ${labels[3]}`,
85): T3McpToolDefinition {
86 return { displayName, labels, icon, summaryAction };
87}
88
89// Codex registers "t3-code-direct" for T3 tools kept out of Code Mode.
90const T3_MCP_SERVER_ALIASES = new Set([
91 "t3-code",
92 "t3_code",
93 "t3code",
94 "t3-code-direct",
95 "t3_code_direct",
96]);
97
98const T3_MCP_TOOLS: Readonly<Record<string, T3McpToolDefinition>> = {
99 link_pull_request: tool(
100 ["Link", "Linking", "Linked", "a pull request"],
101 "link-pr",
102 "pull-request",
103 ),
104 link_pull_requests: tool(
105 ["Link", "Linking", "Linked", "pull requests"],
106 "link-pr",
107 "pull-request",
108 ),
109 unlink_pull_request: tool(
110 ["Unlink", "Unlinking", "Unlinked", "a pull request"],
111 "unlink-pr",
112 "pull-request",
113 ),
114 unlink_pull_requests: tool(
115 ["Unlink", "Unlinking", "Unlinked", "pull requests by state"],
116 "unlink-pr",
117 "pull-request",
118 ),
119 list_thread_pull_requests: tool(
120 ["Check", "Checking", "Checked", "linked pull requests"],
121 "list-prs",
122 "pull-request",
123 ),
124 watch_pull_request: tool(
125 ["Watch", "Watching", "Watching", "a pull request"],
126 "watch-pr",
127 "pull-request",
128 ),
129 unwatch_pull_request: tool(
130 ["Stop watching", "Unwatching", "Stopped watching", "a pull request"],
131 "unwatch-pr",
132 "pull-request",
133 ),
134 write_pull_request_guides: tool(
135 ["Write", "Writing", "Started writing", "review guides"],
136 "write-pr-guides",
137 "pull-request",
138 ),
139 read_pull_request_guide: tool(
140 ["Read", "Reading", "Read", "a review guide"],
141 "read-pr-guide",
142 "pull-request",
143 ),
144 orchestrator_capabilities: tool(
145 ["Get", "Getting", "Got", "orchestration capabilities"],
146 "capabilities",
147 ),
148 delegate_task: tool(["Delegate", "Delegating", "Delegated", "a child task"], "delegate"),
149 task_status: tool(["Get", "Getting", "Got", "delegated task status"], "task-status"),
150 task_cancel: tool(
151 ["Cancel", "Canceling", "Requested cancellation of", "a delegated task"],
152 "task-cancel",
153 ),
154 schedule_task: tool(["Schedule", "Scheduling", "Scheduled", "a task"], "schedule-create"),
155 list_scheduled_tasks: tool(["List", "Listing", "Listed", "scheduled tasks"], "schedule-list"),
156 update_scheduled_task: tool(
157 ["Update", "Updating", "Updated", "a scheduled task"],
158 "schedule-update",
159 ),
160 delete_scheduled_task: tool(
161 ["Delete", "Deleting", "Requested deletion of", "a scheduled task"],
162 "schedule-delete",
163 ),
164 request_secret: tool(["Ask for", "Asking for", "Asked for", "a secret"], "secret-request"),
165 create_threads: tool(["Create", "Creating", "Created", "T3 threads"], "thread-create"),
166 t3_thread_start: tool(["Start", "Starting", "Started", "a T3 thread"], "thread-create"),
167 t3_thread_list: tool(["List", "Listing", "Listed", "T3 threads"], "thread-list"),
168 t3_thread_read: tool(["Read", "Reading", "Read", "a T3 thread"], "thread-read"),
169 t3_thread_send: tool(["Send", "Sending", "Sent", "to a T3 thread"], "thread-send"),
170 t3_thread_wait: tool(["Wait", "Waiting", "Waited", "for a T3 thread"], "thread-wait"),
171 t3_workflow_stop: tool(
172 ["Stop", "Stopping", "Requested a stop for", "a workflow"],
173 "workflow-stop",
174 ),
175 t3_background_wait: tool(
176 ["Update", "Updating", "Updated", "background waits"],
177 "background-wait",
178 ),
179 t3_thread_interrupt: tool(
180 ["Interrupt", "Interrupting", "Requested an interrupt of", "a T3 thread"],
181 "thread-interrupt",
182 ),
183 t3_worktree_handoff: tool(
184 ["Hand off", "Handing off", "Handed off", "thread to a git worktree"],
185 "worktree-handoff",
186 ),
187 t3_worktree_status: tool(["Get", "Getting", "Got", "thread worktree status"], "worktree-status"),
188 preview_status: tool(["Get", "Getting", "Got", "preview browser status"], "browser", "browser"),
189 preview_open: tool(
190 ["Open", "Opening", "Opened", "a page in the preview browser"],
191 "browser",
192 "browser",
193 ),
194 preview_navigate: tool(
195 ["Navigate", "Navigating", "Navigated", "the preview browser"],
196 "browser",
197 "browser",
198 ),
199 preview_dialog: tool(
200 ["Respond", "Responding", "Responded", "to a preview browser dialog"],
201 "browser",
202 "browser",
203 ),
204 preview_snapshot: tool(
205 ["Take a snapshot of", "Taking a snapshot of", "Took a snapshot of", "the preview page"],
206 "browser",
207 "browser",
208 "Snapshot the preview page",
209 ),
210 preview_act: tool(["Use", "Using", "Used", "the preview browser"], "browser", "browser"),
211 preview_click: tool(
212 ["Click", "Clicking", "Clicked", "in the preview browser"],
213 "browser",
214 "browser",
215 ),
216 preview_press: tool(
217 ["Press", "Pressing", "Pressed", "a key in the preview browser"],
218 "browser",
219 "browser",
220 ),
221 preview_type: tool(["Type", "Typing", "Typed", "in the preview browser"], "browser", "browser"),
222 preview_hover: tool(
223 ["Hover", "Hovering", "Hovered", "in the preview browser"],
224 "browser",
225 "browser",
226 ),
227 preview_select: tool(
228 ["Choose", "Choosing", "Chose", "an option in the preview browser"],
229 "browser",
230 "browser",
231 ),
232 preview_drag: tool(
233 ["Drag", "Dragging", "Dragged", "in the preview browser"],
234 "browser",
235 "browser",
236 ),
237 preview_upload: tool(
238 ["Upload", "Uploading", "Uploaded", "files to the preview browser"],
239 "browser",
240 "browser",
241 ),
242 preview_scroll: tool(
243 ["Scroll", "Scrolling", "Scrolled", "the preview browser"],
244 "browser",
245 "browser",
246 ),
247 preview_resize: tool(
248 ["Resize", "Resizing", "Resized", "the preview browser"],
249 "browser",
250 "browser",
251 ),
252 preview_evaluate: tool(
253 ["Evaluate", "Evaluating", "Evaluated", "script in the preview browser"],
254 "browser",
255 "browser",
256 ),
257 preview_wait_for: tool(
258 ["Wait", "Waiting", "Waited", "for the preview page"],
259 "browser",
260 "browser",
261 ),
262 preview_set_appearance: tool(
263 ["Set", "Setting", "Set", "preview browser appearance"],
264 "browser",
265 "browser",
266 ),
267 preview_recording_start: tool(
268 ["Start", "Starting", "Started", "recording the preview browser"],
269 "browser",
270 "browser",
271 ),
272 preview_recording_stop: tool(
273 ["Stop", "Stopping", "Stopped", "recording the preview browser"],
274 "browser",
275 "browser",
276 ),
277 device_list: tool(["List", "Listing", "Listed", "simulators and emulators"], "device", "device"),
278 device_open: tool(
279 ["Open", "Opening", "Opened", "a device in the Device panel"],
280 "device",
281 "device",
282 ),
283 device_screenshot: tool(
284 ["Take a screenshot of", "Taking a screenshot of", "Took a screenshot of", "the device"],
285 "device",
286 "device",
287 ),
288 device_close: tool(["Close", "Closing", "Closed", "a device"], "device", "device"),
289 run_scheduled_task_now: tool(
290 ["Run", "Running", "Requested a run of", "a scheduled task"],
291 "schedule-run",
292 ),
293 t3_queue: tool(["Manage", "Managing", "Managed", "queued messages"], "queue-edit"),
294 t3_pending_request: tool(
295 ["Handle", "Handling", "Handled", "pending questions"],
296 "question-respond",
297 ),
298 t3_queue_list: tool(["List", "Listing", "Listed", "queued messages"], "queue-list"),
299 t3_queue_read: tool(["Read", "Reading", "Read", "a queued message"], "queue-read"),
300 t3_queue_edit: tool(["Edit", "Editing", "Edited", "a queued message"], "queue-edit"),
301 t3_queue_cancel: tool(
302 ["Cancel", "Canceling", "Requested cancellation of", "a queued run"],
303 "queue-cancel",
304 ),
305 t3_queue_reorder: tool(["Reorder", "Reordering", "Reordered", "a queued run"], "queue-reorder"),
306 t3_queue_promote_to_steer: tool(
307 ["Steer with", "Steering with", "Requested steering with", "a queued message"],
308 "queue-steer",
309 ),
310 t3_pending_request_list: tool(
311 ["List", "Listing", "Listed", "pending questions"],
312 "question-list",
313 ),
314 t3_pending_request_read: tool(["Read", "Reading", "Read", "pending questions"], "question-read"),
315 t3_pending_request_respond: tool(
316 ["Answer", "Answering", "Answered", "pending questions"],
317 "question-respond",
318 ),
319 t3_thread_configuration: tool(
320 ["Read", "Reading", "Read", "thread configuration"],
321 "thread-configuration",
322 ),
323 t3_thread_configure: tool(["Use", "Using", "Used", "thread model settings"], "thread-configure"),
324 t3_thread_transfer: tool(["Use", "Using", "Used", "thread transfers"], "thread-fork"),
325 t3_thread_fork: tool(["Fork", "Forking", "Requested a fork of", "this thread"], "thread-fork"),
326 t3_thread_merge_back: tool(
327 ["Merge", "Merging", "Requested a merge of", "thread context"],
328 "thread-merge",
329 ),
330 t3_thread_search: tool(["Search", "Searching", "Searched", "thread content"], "thread-search"),
331 t3_thread_transfers: tool(["Read", "Reading", "Read", "thread transfers"], "thread-transfers"),
332 t3_thread_organize: tool(["Organize", "Organizing", "Organized", "a thread"], "thread-organize"),
333 t3_thread_update: tool(["Update", "Updating", "Updated", "T3 thread metadata"], "thread-update"),
334 t3_worktree_list: tool(["List", "Listing", "Listed", "workspace branches"], "worktree-list"),
335 t3_preview_list: tool(["List", "Listing", "Listed", "preview tabs"], "browser", "browser"),
336 t3_preview_close: tool(["Close", "Closing", "Closed", "a preview tab"], "browser", "browser"),
337 t3_environment_read: tool(
338 ["Read", "Reading", "Read", "environment preferences"],
339 "environment-read",
340 ),
341 t3_environment_links: tool(
342 ["List", "Listing", "Listed", "linked environments"],
343 "environment-links",
344 ),
345 t3_environment_link: tool(
346 ["Ask to link", "Asking to link", "Asked to link", "an environment"],
347 "environment-link",
348 ),
349 t3_environment_unlink: tool(
350 ["Forget", "Forgetting", "Forgot", "a linked environment"],
351 "environment-unlink",
352 ),
353 t3_environment_preferences_update: tool(
354 ["Update", "Updating", "Updated", "environment preferences"],
355 "environment-update",
356 ),
357 t3_thread_launch: tool(["Launch", "Launching", "Launched", "a project thread"], "thread-create"),
358 t3_project_list: tool(["List", "Listing", "Listed", "projects"], "project-list"),
359 t3_project_read: tool(["Read", "Reading", "Read", "a project"], "project-read"),
360 t3_project_create: tool(["Register", "Registering", "Registered", "a project"], "project-create"),
361 t3_project_update: tool(["Update", "Updating", "Updated", "a project"], "project-update"),
362 t3_project_delete: tool(["Delete", "Deleting", "Deleted", "a project"], "project-delete"),
363 t3_project_clone: tool(["Clone", "Cloning", "Cloned", "a repository"], "project-clone"),
364 t3_attachment_prepare_upload: tool(
365 ["Prepare", "Preparing", "Prepared", "an attachment upload"],
366 "attachment-prepare",
367 ),
368 t3_attachment_discard: tool(
369 ["Discard", "Discarding", "Discarded", "a pending attachment"],
370 "attachment-discard",
371 ),
372 t3_thread_send_attachments: tool(["Send", "Sending", "Sent", "attachments"], "attachment-send"),
373 html_preview: tool(["Preview", "Previewing", "Previewed", "an HTML page"], "html-preview"),
374 html_render: tool(["Render", "Rendering", "Rendered", "an HTML page"], "html-render"),
375};
376
377export const T3_MCP_TOOL_NAMES: ReadonlySet<string> = new Set(Object.keys(T3_MCP_TOOLS));
378
379function normalizeT3McpToolLabel(value: string): string {
380 return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();
381}
382
383// Timelines resolve every work row's tool name on each update; names repeat across updates.
384// ACP titles can carry whole commands, so long labels are parsed but not kept.
385const toolNameCache = new Map<string, string | null>();
386const TOOL_NAME_CACHE_LIMIT = 10_000;
387const TOOL_NAME_CACHE_MAX_KEY_LENGTH = 256;
388
389function resolveT3McpToolName(value: string): string | null {
390 if (value.length > TOOL_NAME_CACHE_MAX_KEY_LENGTH) return parseT3McpToolName(value);
391 const cached = toolNameCache.get(value);
392 if (cached !== undefined) return cached;
393 const name = parseT3McpToolName(value);
394 if (toolNameCache.size >= TOOL_NAME_CACHE_LIMIT) toolNameCache.clear();
395 toolNameCache.set(value, name);
396 return name;
397}
398
399// ACP tool prefixes vary; match loosely but require a known display-name entry.
400function parseT3McpToolName(value: string): string | null {
401 const label = normalizeT3McpToolLabel(value);
402 const mcpMatch = /^mcp__(?<server>.+?)__(?<tool>.+)$/i.exec(label);
403 if (mcpMatch?.groups) {
404 const { server, tool } = mcpMatch.groups;
405 return server !== undefined &&
406 tool !== undefined &&
407 T3_MCP_SERVER_ALIASES.has(server.toLowerCase())
408 ? tool
409 : null;
410 }
411
412 const namespaceMatch =
413 /^(?<server>t3[-_]code[-_]direct|t3-code|t3_code|t3code)(?:[.:/]|\s*·\s*)(?<tool>.+)$/i.exec(
414 label,
415 );
416 if (namespaceMatch?.groups) {
417 return namespaceMatch.groups.tool ?? null;
418 }
419
420 const prefixed = /^(?:mcp[-_]{1,2})?t3[-_ ]?code(?:__|[-_.:/ ])(?<tool>.+)$/i.exec(label);
421 const candidate = prefixed?.groups?.tool ?? label;
422 if (Object.hasOwn(T3_MCP_TOOLS, candidate)) return candidate;
423 // OpenCode 2 thread prefixes can contain underscores; choose the longest known tool-name suffix.
424 if (!/^t3-code-/i.test(label)) return null;
425 let longest: string | null = null;
426 for (const tool of Object.keys(T3_MCP_TOOLS)) {
427 if (label.endsWith(`_${tool}`) && tool.length > (longest?.length ?? 0)) longest = tool;
428 }
429 return longest;
430}
431
432export function resolveT3McpToolId(toolName: string | null | undefined): string | null {
433 const name = toolName == null ? null : resolveT3McpToolName(toolName);
434 return name !== null && Object.hasOwn(T3_MCP_TOOLS, name) ? name : null;
435}
436
437type SummaryInput = Readonly<Record<string, unknown>>;
438
439const asSummaryInput = (input: unknown): SummaryInput | undefined =>
440 typeof input === "object" && input !== null ? (input as SummaryInput) : undefined;
441
442/** Action tools present each call as the single-purpose tool its action replaced. */
443const ACTION_TOOLS: Readonly<Record<string, (action: string) => string | undefined>> = {
444 preview_act: (action) => `preview_${action}`,
445 t3_queue: (action) => `t3_queue_${action}`,
446 t3_pending_request: (action) => `t3_pending_request_${action}`,
447 t3_thread_transfer: (action) =>
448 ({ fork: "t3_thread_fork", merge_back: "t3_thread_merge_back", status: "t3_thread_transfers" })[
449 action
450 ],
451};
452
453/** Pass the call's input so an action tool resolves to the labels and summary of its action. */
454export function resolveT3McpToolDefinition(
455 toolName: string | null | undefined,
456 input?: unknown,
457): T3McpToolDefinition | null {
458 const name = toolName == null ? null : resolveT3McpToolName(toolName);
459 if (name === null || !Object.hasOwn(T3_MCP_TOOLS, name)) return null;
460 const action = asSummaryInput(input)?.action;
461 const actionTool = typeof action === "string" ? ACTION_TOOLS[name]?.(action) : undefined;
462 return actionTool !== undefined && Object.hasOwn(T3_MCP_TOOLS, actionTool)
463 ? T3_MCP_TOOLS[actionTool]!
464 : T3_MCP_TOOLS[name]!;
465}
466
467/** Tools whose summary depends on which fields a call sets. */
468const ACTION_SUMMARIES: Readonly<
469 Record<string, (input: SummaryInput) => T3McpToolSummaryAction | undefined>
470> = {
471 t3_thread_configure: ({ modelSelection }) =>
472 modelSelection === undefined ? "thread-configuration" : "thread-configure",
473 schedule_task: ({ scheduledTaskId }) =>
474 scheduledTaskId === undefined ? "schedule-create" : "schedule-update",
475};
476
477export function resolveT3McpSummaryAction(
478 toolName: string | null | undefined,
479 input: unknown,
480): T3McpToolSummaryAction | null {
481 const definition = resolveT3McpToolDefinition(toolName, input);
482 if (definition === null) return null;
483 const summarize = ACTION_SUMMARIES[resolveT3McpToolName(toolName!) ?? ""];
484 const fields = asSummaryInput(input);
485 return (fields && summarize?.(fields)) ?? definition.summaryAction;
486}
487
488export function resolveT3McpToolPresentation(
489 toolName: string | null | undefined,
490 input?: unknown,
491): T3McpToolPresentation | null {
492 const definition = resolveT3McpToolDefinition(toolName, input);
493 return definition === null ? null : { displayName: definition.displayName, logo: "t3-code" };
494}