用 TypeScript 開發你的第一個 MCP Server

Model Context Protocol(MCP)是 Anthropic 推出的開放標準,讓 AI 模型能夠與外部工具和資料來源互動。透過開發自己的 MCP Server,你可以為 Claude Code 擴充無限可能:從查詢資料庫、操作 API、到整合內部系統,一切皆可實現。本篇文章將帶你從零開始,使用 TypeScript 與官方 @modelcontextprotocol/sdk 套件,打造你的第一個 MCP Server,並完成測試與部署。

什麼是 MCP Server?

MCP Server 是 Model Context Protocol 架構中的服務提供者角色。在 MCP 的架構中,Client(如 Claude Code)向 Server 發送請求,Server 負責提供三種核心能力:Tools(工具)、Resources(資源)和 Prompts(提示範本)。透過這個標準化的協議,AI 應用可以安全、一致地存取外部功能。

元件角色說明
MCP Host宿主應用如 Claude Desktop、IDE 外掛,負責管理 Client 連線
MCP Client協議客戶端與 Server 建立一對一連線,傳遞請求與回應
MCP Server服務提供者暴露 Tools、Resources、Prompts 給 Client 使用

環境準備與套件安裝

在開始開發之前,請確保你的開發環境已準備就緒。MCP TypeScript SDK 支援 Node.js、Bun 和 Deno 三種執行環境,本文以 Node.js 為主要範例。

系統需求

項目最低版本建議版本
Node.js18.0+20 LTS 或 22 LTS
TypeScript5.0+5.5+
npm9.0+10+

建立專案與安裝套件

首先建立一個新的 TypeScript 專案,並安裝 MCP SDK 與必要的開發工具:

# 建立專案資料夾
mkdir my-mcp-server
cd my-mcp-server

# 初始化專案
npm init -y

# 安裝 MCP SDK 與相關套件
npm install @modelcontextprotocol/sdk zod

# 安裝 TypeScript 開發工具
npm install -D typescript @types/node tsx

# 初始化 TypeScript 設定
npx tsc --init

其中 zod 是用於定義工具參數驗證的 Schema 庫,MCP SDK 內建整合了 Zod,讓你可以輕鬆定義型別安全的工具輸入參數。

TypeScript 設定檔

修改 tsconfig.json,確保編譯設定適合 MCP Server 開發:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

package.json 設定

package.json 中加入必要的設定,包括 ES Module 支援與執行腳本:

{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js",
    "inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
  }
}

建構你的第一個 MCP Server

MCP SDK 提供了高階的 McpServer 類別,讓你可以快速建構 Server。以下我們會逐步介紹如何建立 Server 實例、註冊 Tools、Resources 和 Prompts。

Server 基本架構

建立 src/index.ts 檔案,撰寫 Server 的基本架構:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// 建立 MCP Server 實例
const server = new McpServer({
  name: "my-mcp-server",
  version: "1.0.0",
});

// 建立 stdio 傳輸層
const transport = new StdioServerTransport();

// 連接 Server 與 Transport
await server.connect(transport);

// 注意:使用 console.error() 輸出除錯訊息
// 絕對不要使用 console.log(),因為 stdout 是 MCP 通訊通道
console.error("MCP Server is running...");

這裡有一個非常重要的觀念:MCP 使用 stdout 作為 JSON-RPC 通訊管道,因此你的 Server 中所有的除錯或日誌輸出都必須使用 console.error() 而非 console.log(),否則會破壞通訊協議。

定義 Tools:讓 AI 執行動作

Tools 是 MCP 中最常用的能力,讓 AI 模型可以呼叫你定義的函式來執行特定動作。每個 Tool 需要定義名稱、描述、輸入參數的 Schema,以及實際執行的處理函式。

基本 Tool 定義

使用 server.tool() 方法註冊一個工具。以下範例定義了一個簡單的加法計算工具:

import { z } from "zod";

// 定義一個加法計算工具
server.tool(
  "add",
  "計算兩個數字的加總",
  {
    a: z.number().describe("第一個數字"),
    b: z.number().describe("第二個數字"),
  },
  async ({ a, b }) => {
    const result = a + b;
    return {
      content: [
        {
          type: "text",
          text: `計算結果:${a} + ${b} = ${result}`,
        },
      ],
    };
  }
);

server.tool() 方法接受四個參數:工具名稱、描述文字、使用 Zod 定義的輸入參數 Schema,以及非同步的處理函式。處理函式的回傳格式固定為包含 content 陣列的物件。

進階 Tool:API 查詢範例

以下是一個更實用的範例,透過 Tool 查詢外部 API 取得天氣資訊:

server.tool(
  "get-weather",
  "查詢指定城市的天氣資訊",
  {
    city: z.string().describe("城市名稱,例如 Taipei"),
    unit: z.enum(["celsius", "fahrenheit"])
      .default("celsius")
      .describe("溫度單位"),
  },
  async ({ city, unit }) => {
    try {
      const response = await fetch(
        `https://api.weather.example/v1/current?city=${city}&unit=${unit}`
      );
      const data = await response.json();

      return {
        content: [
          {
            type: "text",
            text: `${city} 目前天氣:${data.condition},溫度 ${data.temperature}°${unit === "celsius" ? "C" : "F"}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `查詢天氣失敗:${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);

注意錯誤處理的部分:當工具執行失敗時,回傳物件中加入 isError: true 可以讓 Client 知道這是一個錯誤回應,AI 模型會據此調整後續行為。

Zod Schema 常用型別

Zod 型別說明範例
z.string()字串型別z.string().min(1).max(100)
z.number()數字型別z.number().int().positive()
z.boolean()布林型別z.boolean().default(false)
z.enum()列舉型別z.enum(["a", "b", "c"])
z.array()陣列型別z.array(z.string())
z.object()物件型別z.object({ key: z.string() })
z.optional()可選參數z.string().optional()

定義 Resources:提供資料給 AI

Resources 讓你的 MCP Server 可以向 AI 模型提供結構化的資料。與 Tools 不同的是,Resources 是被動讀取的資料來源,類似於 REST API 的 GET 端點。每個 Resource 透過 URI 識別,Client 可以列出並讀取可用的 Resources。

靜態 Resource

靜態 Resource 是最簡單的形式,提供固定 URI 的資料。使用 server.resource() 方法註冊:

// 靜態 Resource:提供系統設定檔
server.resource(
  "config",
  "config://app/settings",
  async (uri) => {
    const config = {
      appName: "My Application",
      version: "2.0.0",
      environment: "production",
      features: ["auth", "logging", "cache"],
    };

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(config, null, 2),
        },
      ],
    };
  }
);

動態 Resource(ResourceTemplate)

當你需要根據參數提供不同資料時,可以使用 ResourceTemplate 定義動態 Resource。URI 中的 {參數名} 會被自動解析:

import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

// 動態 Resource:根據使用者 ID 提供使用者資料
server.resource(
  "user-profile",
  new ResourceTemplate("users://{userId}/profile", { list: undefined }),
  async (uri, { userId }) => {
    // 從資料庫或 API 查詢使用者資料
    const user = await getUserById(userId);

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify({
            id: user.id,
            name: user.name,
            email: user.email,
            role: user.role,
          }, null, 2),
        },
      ],
    };
  }
);

ResourceTemplate 的第二個參數可以傳入 { list: undefined },表示這個動態 Resource 不支援列出所有可用的 URI。如果你想支援列出功能,可以傳入一個回傳 URI 陣列的函式。

定義 Prompts:建立可重用的提示範本

Prompts 讓你預先定義好結構化的對話範本,使用者或 Client 可以直接取用這些範本來進行特定類型的互動。這對於建立標準化的工作流程特別有用。

// 定義 Code Review Prompt 範本
server.prompt(
  "code-review",
  "進行程式碼審查的提示範本",
  {
    language: z.string().describe("程式語言"),
    code: z.string().describe("要審查的程式碼"),
    focus: z.enum(["security", "performance", "readability", "all"])
      .default("all")
      .describe("審查重點"),
  },
  async ({ language, code, focus }) => {
    const focusText = focus === "all"
      ? "安全性、效能與可讀性"
      : focus === "security" ? "安全性"
      : focus === "performance" ? "效能" : "可讀性";

    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `請審查以下 ${language} 程式碼,重點關注${focusText}:\n\n\`\`\`${language}\n${code}\n\`\`\`\n\n請提供具體的改善建議,包含修改後的程式碼範例。`,
          },
        },
      ],
    };
  }
);

Prompts 的回傳格式是一個包含 messages 陣列的物件,每個 message 包含 role(user 或 assistant)和 content。這讓你可以建立多輪對話的範本。

Tools、Resources、Prompts 比較

特性ToolsResourcesPrompts
用途執行動作、呼叫 API提供資料給 AI 讀取預定義對話範本
觸發方式AI 模型主動呼叫Client 讀取使用者選取
類比POST / PUT APIGET API範本系統
副作用可能有(修改資料)無(唯讀)
參數定義Zod SchemaURI TemplateZod Schema

完整範例專案:待辦事項 MCP Server

以下是一個完整的待辦事項(Todo)MCP Server 範例,整合了 Tools、Resources 和 Prompts 三大功能,展示如何在實際專案中組織程式碼:

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 型別定義
interface Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: string;
  priority: "low" | "medium" | "high";
}

// 模擬資料庫
const todos: Map = new Map();
let nextId = 1;

// 建立 Server
const server = new McpServer({
  name: "todo-mcp-server",
  version: "1.0.0",
});

// === Tools ===

// 新增待辦事項
server.tool(
  "add-todo",
  "新增一個待辦事項",
  {
    title: z.string().min(1).describe("待辦事項標題"),
    priority: z.enum(["low", "medium", "high"])
      .default("medium")
      .describe("優先順序"),
  },
  async ({ title, priority }) => {
    const id = String(nextId++);
    const todo: Todo = {
      id,
      title,
      completed: false,
      createdAt: new Date().toISOString(),
      priority,
    };
    todos.set(id, todo);

    return {
      content: [
        {
          type: "text",
          text: `已新增待辦事項 #${id}:${title}(優先度:${priority})`,
        },
      ],
    };
  }
);

// 完成待辦事項
server.tool(
  "complete-todo",
  "將待辦事項標記為完成",
  {
    id: z.string().describe("待辦事項 ID"),
  },
  async ({ id }) => {
    const todo = todos.get(id);
    if (!todo) {
      return {
        content: [{ type: "text", text: `找不到 ID 為 ${id} 的待辦事項` }],
        isError: true,
      };
    }
    todo.completed = true;

    return {
      content: [
        {
          type: "text",
          text: `已完成待辦事項 #${id}:${todo.title}`,
        },
      ],
    };
  }
);

// === Resources ===

// 列出所有待辦事項
server.resource(
  "all-todos",
  "todo://list",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify([...todos.values()], null, 2),
      },
    ],
  })
);

// 查詢單一待辦事項
server.resource(
  "single-todo",
  new ResourceTemplate("todo://{id}", { list: undefined }),
  async (uri, { id }) => {
    const todo = todos.get(String(id));
    if (!todo) throw new Error(`Todo ${id} not found`);

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(todo, null, 2),
        },
      ],
    };
  }
);

// === Prompts ===

server.prompt(
  "daily-summary",
  "產生每日待辦事項摘要",
  {},
  async () => {
    const allTodos = [...todos.values()];
    const pending = allTodos.filter((t) => !t.completed);
    const completed = allTodos.filter((t) => t.completed);

    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `請根據以下待辦事項資料,產生今日工作摘要:\n\n待完成(${pending.length} 項):\n${pending.map((t) => `- [${t.priority}] ${t.title}`).join("\n")}\n\n已完成(${completed.length} 項):\n${completed.map((t) => `- ${t.title}`).join("\n")}\n\n請提供進度分析與建議。`,
          },
        },
      ],
    };
  }
);

// 啟動 Server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Todo MCP Server started!");

測試與除錯

完成 Server 開發後,測試是確保一切正常運作的關鍵步驟。MCP 生態系提供了多種測試工具,讓你可以在不同層級驗證 Server 的行為。

使用 MCP Inspector 測試

MCP Inspector 是官方提供的互動式測試工具,可以讓你在瀏覽器中直接測試 Server 的各項功能。它提供了視覺化的介面來列出和呼叫 Tools、讀取 Resources、以及測試 Prompts。

# 使用 Inspector 測試你的 Server
npx @modelcontextprotocol/inspector node dist/index.js

# 如果使用 tsx 開發模式
npx @modelcontextprotocol/inspector npx tsx src/index.ts

執行後會在瀏覽器開啟 Inspector 介面,你可以在左側看到 Server 提供的所有 Tools、Resources 和 Prompts,點選任一項目即可進行互動測試。

撰寫單元測試

對於正式專案,建議使用測試框架撰寫自動化測試。以下範例使用 Vitest 測試我們的 Todo Server:

// tests/server.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../src/server.js";

describe("Todo MCP Server", () => {
  let client: Client;

  beforeEach(async () => {
    const server = createServer();
    const [clientTransport, serverTransport] =
      InMemoryTransport.createLinkedPair();

    await server.connect(serverTransport);
    client = new Client({ name: "test-client", version: "1.0.0" });
    await client.connect(clientTransport);
  });

  it("should add a todo", async () => {
    const result = await client.callTool({
      name: "add-todo",
      arguments: { title: "寫測試", priority: "high" },
    });

    expect(result.content[0].text).toContain("已新增待辦事項");
  });

  it("should list todos via resource", async () => {
    await client.callTool({
      name: "add-todo",
      arguments: { title: "測試資源", priority: "low" },
    });

    const resource = await client.readResource({
      uri: "todo://list",
    });

    const todos = JSON.parse(resource.contents[0].text);
    expect(todos).toHaveLength(1);
    expect(todos[0].title).toBe("測試資源");
  });
});

使用 InMemoryTransport 可以建立記憶體內的 Client-Server 連線對,不需要真正啟動 stdio 程序,非常適合單元測試場景。

部署你的 MCP Server

MCP Server 支援兩種主要的傳輸方式:stdio(標準輸入輸出)和 Streamable HTTP。根據你的使用場景選擇適合的部署方式。

傳輸方式適用場景優點限制
stdio本地工具、CLI 整合簡單、安全、無需網路只能本地使用
Streamable HTTP遠端服務、多人共用可遠端存取、支援多 Client需處理認證與安全

stdio 部署(本地使用)

stdio 是最簡單的部署方式,適合個人本地使用。只需要編譯 TypeScript 並確保執行檔可被呼叫:

# 編譯專案
npm run build

# 確保入口檔案有執行權限(Linux/macOS)
chmod +x dist/index.js

# 在 dist/index.js 最上方加入 shebang
# #!/usr/bin/env node

Streamable HTTP 部署(遠端服務)

如果你需要將 MCP Server 部署為遠端服務,可以使用 Streamable HTTP 傳輸層搭配 Express 等 Web 框架:

import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = express();

app.post("/mcp", async (req, res) => {
  const server = new McpServer({
    name: "remote-mcp-server",
    version: "1.0.0",
  });

  // 註冊你的 Tools、Resources、Prompts...

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });

  res.on("close", () => {
    transport.close();
    server.close();
  });

  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000, () => {
  console.error("MCP HTTP Server listening on port 3000");
});

與 Claude Code 整合測試

開發完成後,最重要的一步是將你的 MCP Server 與 Claude Code 整合。Claude Code 透過設定檔來管理 MCP Server 連線,你可以在專案層級或全域層級進行設定。

設定 Claude Code 連線

在你的專案根目錄建立 .mcp.json 設定檔,告訴 Claude Code 如何啟動你的 MCP Server:

{
  "mcpServers": {
    "todo-server": {
      "command": "node",
      "args": ["./dist/index.js"],
      "cwd": "/path/to/my-mcp-server"
    }
  }
}

如果你想在所有專案中都能使用這個 Server,可以將設定加到全域設定檔 ~/.claude/settings.json 中。

驗證 Server 連線

設定完成後,在 Claude Code 中使用以下指令確認 Server 已正確連線:

# 查看已連線的 MCP Server 列表
/mcp

# 確認你的 Server 狀態為 "connected"
# 如果顯示 "failed",檢查 Server 的 stderr 輸出找出錯誤原因

連線成功後,你就可以在 Claude Code 對話中直接使用你定義的 Tools 了。例如輸入「幫我新增一個高優先度的待辦事項:完成 MCP Server 文件」,Claude 會自動呼叫你的 add-todo Tool。

專案結構建議

隨著 MCP Server 功能的增長,良好的專案結構可以幫助你維護和擴展程式碼。以下是建議的目錄結構:

my-mcp-server/
├── src/
│   ├── index.ts                               # 入口檔案,啟動 Server
│   ├── server.ts                              # Server 建構與設定
│   ├── tools/
│   │   ├── index.ts                         # 匯出所有 Tools
│   │   ├── add-todo.ts                 # 個別 Tool 定義
│   │   └── complete-todo.ts
│   ├── resources/
│   │   ├── index.ts                         # 匯出所有 Resources
│   │   └── todos.ts
│   └── prompts/
│       ├── index.ts                          # 匯出所有 Prompts
│       └── daily-summary.ts
├── tests/
│   └── server.test.ts
├── package.json
├── tsconfig.json
└── README.md

常見問題與最佳實踐

問題原因解決方案
Server 無法連線使用了 console.log() 輸出訊息改用 console.error(),保持 stdout 乾淨
Tool 參數驗證失敗Zod Schema 不符合傳入資料使用 .describe() 提供清楚的參數說明
Inspector 無法啟動編譯錯誤或路徑不正確先執行 npm run build 確認編譯成功
Resource 回傳空資料URI 格式不匹配確認 ResourceTemplate URI 的參數名稱一致
TypeScript 型別錯誤SDK 版本不相容確認安裝最新版 SDK 並更新 tsconfig 設定

總結

透過本篇教學,你已經學會了如何使用 TypeScript 與 @modelcontextprotocol/sdk 開發一個完整的 MCP Server。從環境建置、定義 Tools、Resources 和 Prompts、撰寫測試、到部署與 Claude Code 整合,你已經掌握了 MCP Server 開發的完整流程。

MCP 的生態系正在快速發展,越來越多的工具和服務都開始支援 MCP 協議。建議你持續關注官方 TypeScript SDK 的更新,並嘗試開發更多實用的 MCP Server 來擴充 Claude Code 的能力。在下一篇文章中,我們將探討更進階的 MCP 開發技巧,包括認證機制、錯誤處理策略、以及效能最佳化。