Studio Notebook

Claude Code Atlas

MCP Tool Wrapper And Client Bridge

Learn how the generic MCPTool wrapper becomes real server-specific tools through the MCP client layer.

Why this matters

MCPTool.ts is intentionally generic. If you only read that file, you do not yet know what a real per-server MCP tool looks like.

Big picture first

The MCP story has two layers:

  1. tools/MCPTool/MCPTool.ts defines a reusable shell with generic prompt, schema, permission, and rendering behavior.
  2. services/mcp/client.ts clones that shell and overrides it with real server names, schemas, and call behavior.

The client bridge starts in fetchToolsForClient(...).

The generic wrapper

// Actual prompt and description are overridden in mcpClient.ts
export const PROMPT = ''
export const DESCRIPTION = ''
export const MCPTool = buildTool({
  isMcp: true,
  name: 'mcp',
  maxResultSizeChars: 100_000,
  async description() {
    return DESCRIPTION
  },
  async prompt() {
    return PROMPT
  },
  async checkPermissions(): Promise<PermissionResult> {
    return {
      behavior: 'passthrough',
      message: 'MCPTool requires permission.',
    }
  },

This is the wrapper pattern in one glance. The generic tool exists, but it is waiting for real server-specific data.

The client layer injects the real tool

return toolsToProcess
  .map((tool): Tool => {
    const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
    return {
      ...MCPTool,
      name: skipPrefix ? tool.name : fullyQualifiedName,
      mcpInfo: { serverName: client.name, toolName: tool.name },
      isMcp: true,
      searchHint:
        typeof tool._meta?.['anthropic/searchHint'] === 'string'
          ? tool._meta['anthropic/searchHint']
              .replace(/\\s+/g, ' ')
              .trim() || undefined
          : undefined,
      alwaysLoad: tool._meta?.['anthropic/alwaysLoad'] === true,
      async description() {
        return tool.description ?? ''
      },
      async prompt() {
        const desc = tool.description ?? ''
        return desc.length > MAX_MCP_DESCRIPTION_LENGTH
          ? desc.slice(0, MAX_MCP_DESCRIPTION_LENGTH) + '… [truncated]'
          : desc
      },

This is the real bridge. services/mcp/client.ts takes the generic shell and fills in:

  1. the fully qualified tool name via buildMcpToolName(...)
  2. the server/tool identity in mcpInfo
  3. the real prompt and description
  4. schema metadata like alwaysLoad
  5. the real call(...) behavior that eventually invokes the MCP server

That is why MCPTool.ts is useful but incomplete on its own.

Takeaways

  • MCPTool is a generic shell, not the full per-server tool definition.
  • The real MCP tool shape is assembled in services/mcp/client.ts.
  • Names, prompts, schemas, and call behavior are all injected at the client layer.