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.js | 18.0+ | 20 LTS 或 22 LTS |
| TypeScript | 5.0+ | 5.5+ |
| npm | 9.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 比較
| 特性 | Tools | Resources | Prompts |
|---|---|---|---|
| 用途 | 執行動作、呼叫 API | 提供資料給 AI 讀取 | 預定義對話範本 |
| 觸發方式 | AI 模型主動呼叫 | Client 讀取 | 使用者選取 |
| 類比 | POST / PUT API | GET API | 範本系統 |
| 副作用 | 可能有(修改資料) | 無(唯讀) | 無 |
| 參數定義 | Zod Schema | URI Template | Zod 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 開發技巧,包括認證機制、錯誤處理策略、以及效能最佳化。