Skip to content

refactor(mcp): add registerTool wrapper to deduplicate tracing boilerplate#2559

Merged
kodiakhq[bot] merged 3 commits into
mainfrom
brandon/brandon-mcp-register-tool-util
Jul 1, 2026
Merged

refactor(mcp): add registerTool wrapper to deduplicate tracing boilerplate#2559
kodiakhq[bot] merged 3 commits into
mainfrom
brandon/brandon-mcp-register-tool-util

Conversation

@brandon-pereira

@brandon-pereira brandon-pereira commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

MCP tool registration required every tool file to independently import McpServer, withToolTracing, and McpContext, then pass the tool name string twice — once to server.registerTool() and again to withToolTracing(). This was error-prone and noisy across 24 tool files.

A new ToolRegistrar pattern ({ server, context, registerTool }) centralizes tracing into a createRegisterTool factory. Tool files now destructure { context, registerTool } and call registerTool(name, config, handler) directly — tracing is applied automatically.

What changed

  • New: registerTool.tscreateRegisterTool(server, context) factory that returns a RegisterToolFn. Wraps every handler with withToolTracing internally, binding the tool name once.
  • New: ToolRegistrar type in types.ts — replaces the old (server, context) function signature with ({ server, context, registerTool }).
  • Updated: tracing.tsToolResult is now CallToolResult & { content: ... } so it's assignable to the SDK's return type without casts. withToolTracing return signature accepts the SDK's extra parameter.
  • Updated: All 24 tool files and 6 group index files — migrated to the new pattern. Each tool drops 2-3 imports and removes the withToolTracing wrapper call.

Design decisions

The SDK's server.registerTool infers its own InputArgs generic from config.inputSchema, producing a ToolCallback whose args type is computed via a conditional BaseToolCallback. Our wrapper's generic (TSchema) can't unify with the SDK's InputArgs across the call boundary — they resolve identically at every call site but TypeScript can't prove it at the definition site. Rather than using a type assertion, the wrapper explicitly binds the SDK generic to AnyZodObject so both sides resolve through the same AnySchema branch of BaseToolCallback, making the assignment structurally sound with no casts.

Prompt-related tools (server.registerPrompt) keep their existing (server, context) signature since they don't use withToolTracing.

Changeset

No changeset — internal plumbing refactor with no user-facing changes.

…plate

Introduce a ToolRegistrar pattern that wraps server.registerTool with
automatic withToolTracing, eliminating the need for each tool file to:
- Pass the tool name twice (registerTool + withToolTracing)
- Import McpServer, withToolTracing, and McpContext separately
- Manually wire up tracing in every handler

All 24 tool files now receive { context, registerTool } and call
registerTool(name, config, handler) directly.
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 1, 2026 4:48pm
hyperdx-storybook Ready Ready Preview, Comment Jul 1, 2026 4:48pm

Request Review

@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 29b588c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Jun 30, 2026
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Large diff: 3175 production lines changed (threshold: 1000)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 34
  • Production lines changed: 3175
  • Branch: brandon/brandon-mcp-register-tool-util
  • Author: brandon-pereira

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR centralizes MCP tool registration boilerplate into a createRegisterTool factory, removing the need to pass the tool name string twice and eliminating 2–3 imports per tool file. The migration is mechanically consistent across all 34 changed files with no behavioral changes.

  • New registerTool.ts: createRegisterTool(server, context) returns a RegisterToolFn that automatically wraps every handler with withToolTracing, binding the tool name once.
  • New ToolRegistrar type: replaces the (server, context) two-argument function signature for all ToolDefinition functions; prompt definitions intentionally retain the old signature.
  • tracing.ts update: ToolResult is now CallToolResult & { content: ... } (subtype, no cast needed), and withToolTracing's return type accepts the SDK's extra parameter to satisfy ToolCallback.

Confidence Score: 5/5

Pure mechanical refactor — no logic, routing, or data-handling changes; tracing is applied identically to all 24 tool handlers via the new factory.

Every tool file follows the exact same migration pattern: drop two imports, destructure the registrar, remove the withToolTracing wrapper call. The factory in registerTool.ts passes name and context to withToolTracing correctly, and the AnyZodObject binding is a sound workaround for the SDK generic constraint. No behavioral changes were introduced.

No files require special attention.

Important Files Changed

Filename Overview
packages/api/src/mcp/utils/registerTool.ts New factory that creates a RegisterToolFn bound to server/context; uses AnyZodObject cast to satisfy SDK generic constraints without type assertions.
packages/api/src/mcp/utils/tracing.ts ToolResult narrowed to CallToolResult subtype and withToolTracing return type extended with _extra? parameter to satisfy SDK ToolCallback signature.
packages/api/src/mcp/tools/types.ts Adds RegisterToolFn, ToolRegistrar types and updates ToolDefinition signature; PromptDefinition retains the old (server, context) shape intentionally.
packages/api/src/mcp/mcpServer.ts Constructs the registrar object and passes it to all tool groups; dashboardPrompts still takes (server, context) directly.
packages/api/src/mcp/tools/alerts/index.ts Migrated to ToolDefinition / ToolRegistrar pattern; removes McpServer and McpContext imports.
packages/api/src/mcp/tools/dashboards/deleteDashboard.ts Migrated cleanly; indentation de-nesting is a minor readability improvement alongside the tracing wrapper removal.
packages/api/src/mcp/tools/query/search.ts Complex tool with denoising post-processing; migrated correctly with no logic changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[createServer] --> B["createRegisterTool(server, context)"]
    B --> C["registerTool fn bound to server + context"]
    A --> D["registrar = { server, context, registerTool }"]
    D --> E[sourcesTools]
    D --> F[alertsTools]
    D --> G[dashboardsTools]
    D --> H[queryTools]
    D --> I[savedSearchesTools]
    D --> J[traceTools]
    A --> K["dashboardPrompts(server, context) — no tracing wrapper"]

    subgraph ToolFile["Inside each tool file"]
        L["registerTool(name, config, handler)"]
        L --> M["withToolTracing(name, context, handler)"]
        M --> N["server.registerTool(name, config, traced)"]
    end

    C --> L
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[createServer] --> B["createRegisterTool(server, context)"]
    B --> C["registerTool fn bound to server + context"]
    A --> D["registrar = { server, context, registerTool }"]
    D --> E[sourcesTools]
    D --> F[alertsTools]
    D --> G[dashboardsTools]
    D --> H[queryTools]
    D --> I[savedSearchesTools]
    D --> J[traceTools]
    A --> K["dashboardPrompts(server, context) — no tracing wrapper"]

    subgraph ToolFile["Inside each tool file"]
        L["registerTool(name, config, handler)"]
        L --> M["withToolTracing(name, context, handler)"]
        M --> N["server.registerTool(name, config, traced)"]
    end

    C --> L
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' into brandon/brandon..." | Re-trigger Greptile

Comment thread packages/api/src/mcp/tools/types.ts Outdated
Comment thread packages/api/src/mcp/utils/registerTool.ts Outdated
@brandon-pereira brandon-pereira requested a review from pulpdrew June 30, 2026 23:06
@brandon-pereira

Copy link
Copy Markdown
Member Author

@pulpdrew PR for you - its actually fairly small, turn off whitespace diffing to view this one

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 225 passed • 3 skipped • 1450s

Status Count
✅ Passed 225
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

Move RegisterToolFn and ToolResult into tools/types.ts so the
dependency flows one way (registerTool.ts → types.ts, tracing.ts →
types.ts) with no cycle. Drop unused annotations field and
ToolAnnotations import.

@pulpdrew pulpdrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. This is a behavior-preserving mechanical refactor: tracing coverage is identical before and after (every tool was and still is wrapped by withToolTracing), result.isError discrimination in tracing.ts is unaffected by the ToolResult type change, the prompt-registration path is untouched, and the large per-file diffs are confirmed to be pure de-indentation from removing the inline wrapper. Two non-blocking items are worth addressing.

🟡 P2 -- recommended

  • packages/api/src/mcp/utils/registerTool.ts:17 -- The createRegisterTool factory is the only new runtime logic in the refactor and has no dedicated test; existing tool tests run through createServer and assert result content but never span attributes or the error counter, so a name- or context-binding regression in the traced wrapper would pass every current test.
    • Fix: Add a unit test that mocks McpServer.registerTool and asserts the returned function wraps the handler with withToolTracing bound to the exact tool name and context.
    • testing, kieran-typescript, correctness
🔵 P3 nitpicks (1)
  • packages/api/src/mcp/tools/types.ts:38 -- ToolRegistrar exposes a server field, but every migrated tool file destructures only { context, registerTool }, leaving server as dead surface area handed to tool files that never read it.
    • Fix: Drop server from ToolRegistrar (and from the object built in mcpServer.ts), or annotate it as a deliberate escape hatch for future tools needing raw server access.

Reviewers (7): correctness, adversarial, kieran-typescript, api-contract, testing, maintainability, project-standards.

Testing gaps:

  • No direct unit test for createRegisterTool; its tracing wiring is covered only incidentally by end-to-end tool tests that assert result content, not span/metadata correctness.
  • The type soundness of server.registerTool<AnyZodObject, AnyZodObject> accepting the traced callback without a cast is load-bearing and could not be verified without a build — confirm tsc --noEmit for the api package is green in CI for this diff.

@kodiakhq kodiakhq Bot merged commit ebffee5 into main Jul 1, 2026
20 checks passed
@kodiakhq kodiakhq Bot deleted the brandon/brandon-mcp-register-tool-util branch July 1, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants