| 1 | export type T3McpToolLogo = "t3-code"; |
| 2 | |
| 3 | export interface T3McpToolPresentation { |
| 4 | readonly displayName: string; |
| 5 | readonly logo: T3McpToolLogo; |
| 6 | } |
| 7 | |
| 8 | export 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 | | "thread-configuration" |
| 26 | | "thread-configure" |
| 27 | | "thread-fork" |
| 28 | | "thread-merge" |
| 29 | | "thread-search" |
| 30 | | "thread-transfers" |
| 31 | | "thread-organize" |
| 32 | | "thread-update" |
| 33 | | "queue-list" |
| 34 | | "queue-read" |
| 35 | | "queue-edit" |
| 36 | | "queue-cancel" |
| 37 | | "queue-reorder" |
| 38 | | "queue-steer" |
| 39 | | "question-list" |
| 40 | | "question-read" |
| 41 | | "question-respond" |
| 42 | | "secret-request" |
| 43 | | "worktree-handoff" |
| 44 | | "worktree-list" |
| 45 | | "worktree-status" |
| 46 | | "project-list" |
| 47 | | "project-read" |
| 48 | | "project-create" |
| 49 | | "project-update" |
| 50 | | "project-delete" |
| 51 | | "project-clone" |
| 52 | | "environment-read" |
| 53 | | "environment-update" |
| 54 | | "attachment-prepare" |
| 55 | | "attachment-discard" |
| 56 | | "attachment-send" |
| 57 | | "link-pr" |
| 58 | | "unlink-pr" |
| 59 | | "list-prs" |
| 60 | | "watch-pr" |
| 61 | | "unwatch-pr" |
| 62 | | "write-pr-guides" |
| 63 | | "read-pr-guide" |
| 64 | | "browser" |
| 65 | | "device" |
| 66 | | "html-preview" |
| 67 | | "html-render"; |
| 68 | |
| 69 | export interface T3McpToolDefinition { |
| 70 | readonly displayName: string; |
| 71 | readonly labels: readonly [action: string, running: string, completed: string, detail: string]; |
| 72 | readonly icon: "t3-code" | "browser" | "device" | "pull-request"; |
| 73 | readonly summaryAction: T3McpToolSummaryAction; |
| 74 | } |
| 75 | |
| 76 | function tool( |
| 77 | labels: T3McpToolDefinition["labels"], |
| 78 | summaryAction: T3McpToolSummaryAction, |
| 79 | icon: T3McpToolDefinition["icon"] = "t3-code", |
| 80 | displayName = `${labels[0]} ${labels[3]}`, |
| 81 | ): T3McpToolDefinition { |
| 82 | return { displayName, labels, icon, summaryAction }; |
| 83 | } |
| 84 | |
| 85 | const T3_MCP_SERVER_ALIASES = new Set(["t3-code", "t3_code", "t3code"]); |
| 86 | |
| 87 | // Cards, activity rows, summaries, and provider identity recovery share this inventory. |
| 88 | const T3_MCP_TOOLS: Readonly<Record<string, T3McpToolDefinition>> = { |
| 89 | link_pull_request: tool( |
| 90 | ["Link", "Linking", "Linked", "a pull request"], |
| 91 | "link-pr", |
| 92 | "pull-request", |
| 93 | ), |
| 94 | unlink_pull_request: tool( |
| 95 | ["Unlink", "Unlinking", "Unlinked", "a pull request"], |
| 96 | "unlink-pr", |
| 97 | "pull-request", |
| 98 | ), |
| 99 | list_thread_pull_requests: tool( |
| 100 | ["Check", "Checking", "Checked", "linked pull requests"], |
| 101 | "list-prs", |
| 102 | "pull-request", |
| 103 | ), |
| 104 | watch_pull_request: tool( |
| 105 | ["Watch", "Watching", "Watching", "a pull request"], |
| 106 | "watch-pr", |
| 107 | "pull-request", |
| 108 | ), |
| 109 | unwatch_pull_request: tool( |
| 110 | ["Stop watching", "Stopping watching", "Stopped watching", "a pull request"], |
| 111 | "unwatch-pr", |
| 112 | "pull-request", |
| 113 | ), |
| 114 | write_pull_request_guides: tool( |
| 115 | ["Write", "Writing", "Started", "review guides"], |
| 116 | "write-pr-guides", |
| 117 | "pull-request", |
| 118 | ), |
| 119 | read_pull_request_guide: tool( |
| 120 | ["Read", "Reading", "Read", "a review guide"], |
| 121 | "read-pr-guide", |
| 122 | "pull-request", |
| 123 | ), |
| 124 | orchestrator_capabilities: tool( |
| 125 | ["Get", "Getting", "Got", "orchestration capabilities"], |
| 126 | "capabilities", |
| 127 | ), |
| 128 | delegate_task: tool(["Delegate", "Delegating", "Delegated", "a child task"], "delegate"), |
| 129 | task_status: tool(["Get", "Getting", "Got", "delegated task status"], "task-status"), |
| 130 | task_cancel: tool( |
| 131 | ["Cancel", "Canceling", "Requested cancellation of", "delegated task"], |
| 132 | "task-cancel", |
| 133 | ), |
| 134 | schedule_task: tool( |
| 135 | ["Schedule", "Scheduling", "Scheduled", "a recurring task"], |
| 136 | "schedule-create", |
| 137 | ), |
| 138 | list_scheduled_tasks: tool(["List", "Listing", "Listed", "scheduled tasks"], "schedule-list"), |
| 139 | update_scheduled_task: tool( |
| 140 | ["Update", "Updating", "Updated", "a scheduled task"], |
| 141 | "schedule-update", |
| 142 | ), |
| 143 | delete_scheduled_task: tool( |
| 144 | ["Delete", "Deleting", "Requested deletion of", "a scheduled task"], |
| 145 | "schedule-delete", |
| 146 | ), |
| 147 | request_secret: tool(["Ask for", "Asking for", "Asked for", "a secret"], "secret-request"), |
| 148 | create_threads: tool(["Create", "Creating", "Created", "T3 threads"], "thread-create"), |
| 149 | t3_thread_start: tool(["Start", "Starting", "Started", "a T3 thread"], "thread-create"), |
| 150 | t3_thread_list: tool(["List", "Listing", "Listed", "T3 threads"], "thread-list"), |
| 151 | t3_thread_read: tool(["Read", "Reading", "Read", "a T3 thread"], "thread-read"), |
| 152 | t3_thread_send: tool(["Send", "Sending", "Sent", "to a T3 thread"], "thread-send"), |
| 153 | t3_thread_wait: tool(["Wait", "Waiting", "Waited", "for a T3 thread"], "thread-wait"), |
| 154 | t3_workflow_stop: tool( |
| 155 | ["Stop", "Stopping", "Requested a stop for", "a workflow"], |
| 156 | "workflow-stop", |
| 157 | ), |
| 158 | t3_thread_interrupt: tool( |
| 159 | ["Interrupt", "Interrupting", "Requested an interrupt of", "a T3 thread"], |
| 160 | "thread-interrupt", |
| 161 | ), |
| 162 | t3_worktree_handoff: tool( |
| 163 | ["Hand off", "Handing off", "Handed off", "thread to a git worktree"], |
| 164 | "worktree-handoff", |
| 165 | ), |
| 166 | t3_worktree_status: tool(["Get", "Getting", "Got", "thread worktree status"], "worktree-status"), |
| 167 | preview_status: tool(["Get", "Getting", "Got", "preview browser status"], "browser", "browser"), |
| 168 | preview_open: tool( |
| 169 | ["Open", "Opening", "Opened", "a page in the preview browser"], |
| 170 | "browser", |
| 171 | "browser", |
| 172 | ), |
| 173 | preview_navigate: tool( |
| 174 | ["Navigate", "Navigating", "Navigated", "the preview browser"], |
| 175 | "browser", |
| 176 | "browser", |
| 177 | ), |
| 178 | preview_dialog: tool( |
| 179 | ["Respond", "Responding", "Responded", "to a preview browser dialog"], |
| 180 | "browser", |
| 181 | "browser", |
| 182 | ), |
| 183 | preview_snapshot: tool( |
| 184 | ["Take a snapshot of", "Taking a snapshot of", "Took a snapshot of", "the preview page"], |
| 185 | "browser", |
| 186 | "browser", |
| 187 | "Snapshot the preview page", |
| 188 | ), |
| 189 | preview_click: tool( |
| 190 | ["Click", "Clicking", "Clicked", "in the preview browser"], |
| 191 | "browser", |
| 192 | "browser", |
| 193 | ), |
| 194 | preview_press: tool( |
| 195 | ["Press", "Pressing", "Pressed", "a key in the preview browser"], |
| 196 | "browser", |
| 197 | "browser", |
| 198 | ), |
| 199 | preview_type: tool(["Type", "Typing", "Typed", "in the preview browser"], "browser", "browser"), |
| 200 | preview_hover: tool( |
| 201 | ["Hover", "Hovering", "Hovered", "in the preview browser"], |
| 202 | "browser", |
| 203 | "browser", |
| 204 | ), |
| 205 | preview_select: tool( |
| 206 | ["Choose", "Choosing", "Chose", "an option in the preview browser"], |
| 207 | "browser", |
| 208 | "browser", |
| 209 | ), |
| 210 | preview_drag: tool( |
| 211 | ["Drag", "Dragging", "Dragged", "in the preview browser"], |
| 212 | "browser", |
| 213 | "browser", |
| 214 | ), |
| 215 | preview_upload: tool( |
| 216 | ["Upload", "Uploading", "Uploaded", "files to the preview browser"], |
| 217 | "browser", |
| 218 | "browser", |
| 219 | ), |
| 220 | preview_scroll: tool( |
| 221 | ["Scroll", "Scrolling", "Scrolled", "the preview browser"], |
| 222 | "browser", |
| 223 | "browser", |
| 224 | ), |
| 225 | preview_resize: tool( |
| 226 | ["Resize", "Resizing", "Resized", "the preview browser"], |
| 227 | "browser", |
| 228 | "browser", |
| 229 | ), |
| 230 | preview_evaluate: tool( |
| 231 | ["Evaluate", "Evaluating", "Evaluated", "script in the preview browser"], |
| 232 | "browser", |
| 233 | "browser", |
| 234 | ), |
| 235 | preview_wait_for: tool( |
| 236 | ["Wait", "Waiting", "Waited", "for the preview page"], |
| 237 | "browser", |
| 238 | "browser", |
| 239 | ), |
| 240 | preview_set_appearance: tool( |
| 241 | ["Set", "Setting", "Set", "preview browser appearance"], |
| 242 | "browser", |
| 243 | "browser", |
| 244 | ), |
| 245 | preview_recording_start: tool( |
| 246 | ["Start", "Starting", "Started", "recording the preview browser"], |
| 247 | "browser", |
| 248 | "browser", |
| 249 | ), |
| 250 | preview_recording_stop: tool( |
| 251 | ["Stop", "Stopping", "Stopped", "recording the preview browser"], |
| 252 | "browser", |
| 253 | "browser", |
| 254 | ), |
| 255 | device_list: tool(["List", "Listing", "Listed", "simulators and emulators"], "device", "device"), |
| 256 | device_open: tool( |
| 257 | ["Open", "Opening", "Opened", "a device in the Device panel"], |
| 258 | "device", |
| 259 | "device", |
| 260 | ), |
| 261 | device_screenshot: tool( |
| 262 | ["Take a screenshot of", "Taking a screenshot of", "Took a screenshot of", "the device"], |
| 263 | "device", |
| 264 | "device", |
| 265 | ), |
| 266 | device_close: tool(["Close", "Closing", "Closed", "a device"], "device", "device"), |
| 267 | run_scheduled_task_now: tool( |
| 268 | ["Run", "Running", "Requested a run of", "a scheduled task"], |
| 269 | "schedule-run", |
| 270 | ), |
| 271 | t3_queue_list: tool(["List", "Listing", "Listed", "queued messages"], "queue-list"), |
| 272 | t3_queue_read: tool(["Read", "Reading", "Read", "a queued message"], "queue-read"), |
| 273 | t3_queue_edit: tool(["Edit", "Editing", "Edited", "a queued message"], "queue-edit"), |
| 274 | t3_queue_cancel: tool( |
| 275 | ["Cancel", "Canceling", "Requested cancellation of", "a queued run"], |
| 276 | "queue-cancel", |
| 277 | ), |
| 278 | t3_queue_reorder: tool(["Reorder", "Reordering", "Reordered", "a queued run"], "queue-reorder"), |
| 279 | t3_queue_promote_to_steer: tool( |
| 280 | ["Steer with", "Steering with", "Requested steering with", "a queued message"], |
| 281 | "queue-steer", |
| 282 | ), |
| 283 | t3_pending_request_list: tool( |
| 284 | ["List", "Listing", "Listed", "pending questions"], |
| 285 | "question-list", |
| 286 | ), |
| 287 | t3_pending_request_read: tool(["Read", "Reading", "Read", "pending questions"], "question-read"), |
| 288 | t3_pending_request_respond: tool( |
| 289 | ["Answer", "Answering", "Answered", "pending questions"], |
| 290 | "question-respond", |
| 291 | ), |
| 292 | t3_thread_configuration: tool( |
| 293 | ["Read", "Reading", "Read", "thread configuration"], |
| 294 | "thread-configuration", |
| 295 | ), |
| 296 | t3_thread_configure: tool(["Set", "Setting", "Set", "thread model"], "thread-configure"), |
| 297 | t3_thread_fork: tool(["Fork", "Forking", "Requested a fork of", "this thread"], "thread-fork"), |
| 298 | t3_thread_merge_back: tool( |
| 299 | ["Merge", "Merging", "Requested a merge of", "thread context"], |
| 300 | "thread-merge", |
| 301 | ), |
| 302 | t3_thread_search: tool(["Search", "Searching", "Searched", "thread content"], "thread-search"), |
| 303 | t3_thread_transfers: tool(["Read", "Reading", "Read", "thread transfers"], "thread-transfers"), |
| 304 | t3_thread_organize: tool(["Organize", "Organizing", "Organized", "a thread"], "thread-organize"), |
| 305 | t3_thread_update: tool(["Update", "Updating", "Updated", "T3 thread metadata"], "thread-update"), |
| 306 | t3_worktree_list: tool(["List", "Listing", "Listed", "workspace branches"], "worktree-list"), |
| 307 | t3_preview_list: tool(["List", "Listing", "Listed", "preview tabs"], "browser", "browser"), |
| 308 | t3_preview_close: tool(["Close", "Closing", "Closed", "a preview tab"], "browser", "browser"), |
| 309 | t3_environment_read: tool( |
| 310 | ["Read", "Reading", "Read", "environment preferences"], |
| 311 | "environment-read", |
| 312 | ), |
| 313 | t3_environment_preferences_update: tool( |
| 314 | ["Update", "Updating", "Updated", "environment preferences"], |
| 315 | "environment-update", |
| 316 | ), |
| 317 | t3_thread_launch: tool(["Launch", "Launching", "Launched", "a project thread"], "thread-create"), |
| 318 | t3_project_list: tool(["List", "Listing", "Listed", "projects"], "project-list"), |
| 319 | t3_project_read: tool(["Read", "Reading", "Read", "a project"], "project-read"), |
| 320 | t3_project_create: tool(["Register", "Registering", "Registered", "a project"], "project-create"), |
| 321 | t3_project_update: tool(["Update", "Updating", "Updated", "a project"], "project-update"), |
| 322 | t3_project_delete: tool(["Delete", "Deleting", "Deleted", "a project"], "project-delete"), |
| 323 | t3_project_clone: tool(["Clone", "Cloning", "Cloned", "a repository"], "project-clone"), |
| 324 | t3_attachment_prepare_upload: tool( |
| 325 | ["Prepare", "Preparing", "Prepared", "an attachment upload"], |
| 326 | "attachment-prepare", |
| 327 | ), |
| 328 | t3_attachment_discard: tool( |
| 329 | ["Discard", "Discarding", "Discarded", "a pending attachment"], |
| 330 | "attachment-discard", |
| 331 | ), |
| 332 | t3_thread_send_attachments: tool(["Send", "Sending", "Sent", "attachments"], "attachment-send"), |
| 333 | html_preview: tool(["Preview", "Previewing", "Previewed", "an HTML page"], "html-preview"), |
| 334 | html_render: tool(["Render", "Rendering", "Rendered", "an HTML page"], "html-render"), |
| 335 | }; |
| 336 | |
| 337 | /** |
| 338 | * The T3 orchestration tool inventory, used to gate loose name matching on |
| 339 | * both the server (ACP MCP identity recovery) and the client (logo branding). |
| 340 | */ |
| 341 | export const T3_MCP_TOOL_NAMES: ReadonlySet<string> = new Set(Object.keys(T3_MCP_TOOLS)); |
| 342 | |
| 343 | function normalizeT3McpToolLabel(value: string): string { |
| 344 | return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim(); |
| 345 | } |
| 346 | |
| 347 | /** |
| 348 | * ACP agents disagree on how the injected T3 server prefixes its tools: |
| 349 | * `mcp__t3-code__x` (Claude/Cursor), `t3-code.x` (Codex), plus single |
| 350 | * underscore, colon, slash, dash, and space separators seen from registry |
| 351 | * agents. The prefix match is deliberately loose because the display-name |
| 352 | * inventory is the real gate; unknown tools stay on the generic renderer. |
| 353 | */ |
| 354 | function resolveT3McpToolName(value: string): string | null { |
| 355 | const label = normalizeT3McpToolLabel(value); |
| 356 | const mcpMatch = /^mcp__(?<server>.+?)__(?<tool>.+)$/i.exec(label); |
| 357 | if (mcpMatch?.groups) { |
| 358 | const { server, tool } = mcpMatch.groups; |
| 359 | return server !== undefined && |
| 360 | tool !== undefined && |
| 361 | T3_MCP_SERVER_ALIASES.has(server.toLowerCase()) |
| 362 | ? tool |
| 363 | : null; |
| 364 | } |
| 365 | |
| 366 | const namespaceMatch = /^(?<server>t3-code|t3_code|t3code)(?:[.:/]|\s*·\s*)(?<tool>.+)$/i.exec( |
| 367 | label, |
| 368 | ); |
| 369 | if (namespaceMatch?.groups) { |
| 370 | return namespaceMatch.groups.tool ?? null; |
| 371 | } |
| 372 | |
| 373 | const prefixed = /^(?:mcp[-_]{1,2})?t3[-_ ]?code(?:__|[-_.:/ ])(?<tool>.+)$/i.exec(label); |
| 374 | const candidate = prefixed?.groups?.tool ?? label; |
| 375 | if (Object.hasOwn(T3_MCP_TOOLS, candidate)) return candidate; |
| 376 | // OpenCode 2 registers one server per thread, `t3-code-<thread>`, and joins |
| 377 | // it to the tool with `_`. Thread ids can hold `_` too, so take the longest |
| 378 | // known tool name that ends the label. |
| 379 | if (!/^t3-code-/i.test(label)) return null; |
| 380 | let longest: string | null = null; |
| 381 | for (const tool of Object.keys(T3_MCP_TOOLS)) { |
| 382 | if (label.endsWith(`_${tool}`) && tool.length > (longest?.length ?? 0)) longest = tool; |
| 383 | } |
| 384 | return longest; |
| 385 | } |
| 386 | |
| 387 | /** The bare T3 tool name (`html_render`) for any provider's spelling of it. */ |
| 388 | export function resolveT3McpToolId(toolName: string | null | undefined): string | null { |
| 389 | const name = toolName == null ? null : resolveT3McpToolName(toolName); |
| 390 | return name !== null && Object.hasOwn(T3_MCP_TOOLS, name) ? name : null; |
| 391 | } |
| 392 | |
| 393 | export function resolveT3McpToolDefinition( |
| 394 | toolName: string | null | undefined, |
| 395 | ): T3McpToolDefinition | null { |
| 396 | const name = toolName == null ? null : resolveT3McpToolName(toolName); |
| 397 | return name !== null && Object.hasOwn(T3_MCP_TOOLS, name) ? T3_MCP_TOOLS[name]! : null; |
| 398 | } |
| 399 | |
| 400 | export function resolveT3McpToolPresentation( |
| 401 | toolName: string | null | undefined, |
| 402 | ): T3McpToolPresentation | null { |
| 403 | const definition = resolveT3McpToolDefinition(toolName); |
| 404 | return definition === null ? null : { displayName: definition.displayName, logo: "t3-code" }; |
| 405 | } |
| 406 | |
| 407 | export function resolveT3McpToolSummaryAction( |
| 408 | toolName: string | null | undefined, |
| 409 | ): T3McpToolSummaryAction | null { |
| 410 | return resolveT3McpToolDefinition(toolName)?.summaryAction ?? null; |
| 411 | } |