Advanced · about 3-5 hours

Connect Claude to External Tools with MCP

Connect Claude Code to an existing hosted MCP server, share that setup with your team without committing secrets, then build a minimal MCP server of your own with the official TypeScript SDK and register it in Claude Code and Claude Desktop.

You'll end up with: Claude can look up your agency's service catalogue on request through a small, read-only tool you wrote and control.

Claude CodeClaude DesktopMCP TypeScript SDK v2ZodNode.jsMCP Inspector

Before you start

  • □Claude Code installed and signed in (it's included on every paid Claude plan)
  • □Node.js 20 or later
  • □An account on a service that offers a hosted MCP server, such as Notion
  • □Basic TypeScript and JSON

Architecture

  1. Host

    Claude Code or Claude Desktop holds the conversation and runs an MCP client for each configured server.

  2. Hosted server

    A vendor's remote MCP server, reached over Streamable HTTP and authorised with OAuth from the /mcp panel.

  3. Your server

    A local stdio process the host launches. It registers tools with a name, a description and a Zod input schema.

  4. Tool call

    Claude picks a tool from its description, the SDK validates the arguments, and your handler runs.

  5. Your data

    The handler reads a local JSON price list. Later this becomes a real API call with a scoped token from an env var.

  6. Result

    The handler returns text content, or isError, which Claude reads and uses in its answer.

Build it, step by step

Tick steps off as you build.

0/8
  1. Decide what Claude should be able to do

    Write down the one job you want done, and start read-only. Every tool you add is something the model can trigger, sometimes from text it read in a document or web page. A read-only lookup can leak data at worst. A write tool can change records. Add write tools only after the read path works, and keep the permission prompt on for them.

  2. Connect an existing hosted server in Claude Code

    Remote servers are added with the http transport and a URL. Run /mcp inside a session to complete the OAuth sign-in, then check health with claude mcp list, which shows a status such as Connected, Needs authentication or Failed to connect next to each server.

    Ask Claude something that needs the tool, for example to list your recent pages, and approve the tool call when prompted.

    bash
    # Add a hosted MCP server (local scope: this project, only you)
    claude mcp add --transport http notion https://mcp.notion.com/mcp
    
    # Inside a Claude Code session: run /mcp and sign in
    
    # Check every configured server
    claude mcp list
  3. Share the setup with your team without sharing secrets

    Project scope writes the server to .mcp.json in the repo root so teammates get it from version control. Claude Code asks each person to approve project servers before using them. Reference secrets with ${VAR} expansion so the file can be committed while each person keeps the token in their own environment. Claude Code warns when a referenced variable isn't set.

    json
    {
      "mcpServers": {
        "internal-api": {
          "type": "http",
          "url": "https://mcp.example.com/mcp",
          "headers": { "Authorization": "Bearer ${INTERNAL_MCP_TOKEN}" }
        }
      }
    }
  4. Scaffold your own server

    Version 2 of the official TypeScript SDK is the stable line and implements the 2026-07-28 spec. It ships as split packages; servers use @modelcontextprotocol/server. The SDK is ES modules only, so set type to module. tsx runs TypeScript directly, so there's no build step while you learn.

    bash
    mkdir service-catalog && cd service-catalog
    npm init -y
    npm pkg set type=module
    npm install @modelcontextprotocol/server zod tsx
    mkdir src data
  5. Register one tool

    registerTool takes a name, a config and an async handler. The Zod inputSchema is the only schema you write: the SDK derives the JSON Schema the model sees from it and rejects bad arguments before your handler runs. The description is the most important string here. It's what the model reads when deciding whether to call the tool, so say what it returns and that it's read-only.

    stdout is the protocol channel. Log with console.error, because a single console.log corrupts the JSON-RPC stream.

    typescript
    // src/index.ts
    import { readFile } from "node:fs/promises";
    import { McpServer } from "@modelcontextprotocol/server";
    import { serveStdio } from "@modelcontextprotocol/server/stdio";
    import * as z from "zod/v4";
    
    interface Service { sku: string; name: string; priceUsd: number; turnaroundDays: number }
    
    const CATALOG = new URL("../data/services.json", import.meta.url);
    
    function createServer(): McpServer {
      const server = new McpServer({ name: "service-catalog", version: "1.0.0" });
    
      server.registerTool(
        "find-service",
        {
          description:
            "Search the agency service catalogue by keyword. Returns name, SKU, price in USD " +
            "and turnaround in days. Read-only.",
          inputSchema: z.object({
            keyword: z.string().trim().min(2).max(60).describe("Word to match, e.g. funnel or SEO"),
          }),
        },
        async ({ keyword }) => {
          let catalog: Service[];
          try {
            catalog = JSON.parse(await readFile(CATALOG, "utf8")) as Service[];
          } catch {
            return { content: [{ type: "text", text: "The catalogue file could not be read." }], isError: true };
          }
          const needle = keyword.toLowerCase();
          const matches = catalog.filter((service) => service.name.toLowerCase().includes(needle));
          const text = matches.length > 0 ? JSON.stringify(matches, null, 2) : "No service matches that keyword.";
          return { content: [{ type: "text", text }] };
        },
      );
    
      return server;
    }
    
    void serveStdio(createServer);
    console.error("service-catalog MCP server running on stdio");
  6. Test it with the MCP Inspector before any host

    Add a few services to data/services.json, then launch the Inspector with your server command. Click Connect, open the Tools tab, run find-service with a keyword, and try a one-letter keyword to see the validation error the model would get. Debugging here is much faster than through a chat.

    bash
    npx @modelcontextprotocol/inspector npx tsx src/index.ts
  7. Register the server in Claude Code and Claude Desktop

    In Claude Code, everything after the double dash is the launch command, run from the project root. Run /mcp to confirm the server is connected and find-service is listed.

    Claude Desktop reads claude_desktop_config.json (on Windows under %AppData%\Claude, on macOS under ~/Library/Application Support/Claude). Use an absolute path to the server file and restart the app after saving.

    bash
    # Claude Code, from the service-catalog folder
    claude mcp add service-catalog -- npx tsx src/index.ts
    
    # Claude Desktop: claude_desktop_config.json
    {
      "mcpServers": {
        "service-catalog": {
          "command": "npx",
          "args": ["tsx", "/ABSOLUTE/PATH/service-catalog/src/index.ts"]
        }
      }
    }
  8. Harden it before you connect real systems

    When you swap the JSON file for a real API, read the token from an environment variable passed with --env, and give it the narrowest scope the API offers. Keep inputs bounded with Zod limits. Return isError with a short message instead of throwing a stack trace at the model. Treat tool output as untrusted: text from a CRM note or email can contain instructions aimed at the model, so never give the same session a read tool for untrusted content and an unprompted write tool.

    When you need one server for many users, serve the same createServer factory over Streamable HTTP. HTTP+SSE is deprecated, and the 2026-07-28 spec made the protocol stateless, so don't design around server-side sessions.

Example run

  1. data/services.json holds three entries: Funnel build (SKU FUN-01, 1500 USD, 10 days), SEO audit (SKU SEO-02, 600 USD, 5 days) and GHL snapshot setup (SKU GHL-03, 900 USD, 7 days).
  2. In Claude Code you ask: "A lead wants a funnel and an SEO audit. What would that cost and how long would it take?"
  3. Claude calls find-service with {"keyword": "funnel"}, then with {"keyword": "SEO"}. Claude Code shows each call and you approve it.
  4. The handler returns the Funnel build entry, then the SEO audit entry, as JSON text.
  5. Claude answers that the two services total 2100 USD and that the longest turnaround is 10 days, and names both SKUs.
  6. You ask for a "website" service. The tool returns "No service matches that keyword." and Claude says the catalogue has no such service instead of inventing one.

Test it

  • ✓In the Inspector, call find-service with keyword "a". The SDK must reject it with an input validation error before the handler runs.
  • ✓Failure case: rename data/services.json and call the tool. It must return isError with the short message, and the host must stay connected.
  • ✓Failure case: add a console.log to the handler and call the tool. The host drops the connection, which proves why logs must go to stderr. Remove it afterwards.
  • ✓Run claude mcp list and confirm service-catalog shows as connected. For a hosted server, revoke its access in the vendor's settings and confirm the status changes to needing authentication.
  • ✓Ask a question the catalogue can't answer and check that Claude reports no match rather than making up a price.

Watch out for

  • !Vague tool descriptions. The model chooses tools from their names and descriptions, so "lookup" gets called at the wrong times while a precise description gets called at the right ones.
  • !Writing to stdout in a stdio server. One stray console.log breaks the protocol stream and the host drops the server.
  • !Committing tokens in .mcp.json. Use ${VAR} references and keep the values in each person's environment.
  • !Relative paths in Claude Desktop config. Desktop doesn't launch from your project folder, so a relative path fails silently. Use the absolute path.
  • !Following old tutorials. Many still use the v1 package @modelcontextprotocol/sdk, the deprecated HTTP+SSE transport or session-based patterns that the 2026-07-28 spec removed.
  • !Granting broad tokens. A tool that only needs to read one object type shouldn't hold a token that can delete contacts.

Concepts used