前幾篇我們談到 AI Agent 在 LangGraph 中的決策、協作與記憶管理能力,但如果 Agent 永遠只能在封閉環境內工作,它的價值就會受到限制。真正在應用中有用的 Agent,往往需要具備與外部世界互通的能力,例如查詢 API、操作資料庫、存取企業內部系統,甚至透過安全協議來交換資料。
今天要介紹的 MCP,就是為了解決如何讓模型與外部世界安全、標準化地交換訊息而設計的協定。我們將示範如何在 LangGraph 中整合 MCP Tools,打造能真正與外部系統互動的 AI Agent。
MCP(Model Context Protocol) 是由 Anthropic 於 2024 年底推出的開放標準協定,目的是為大型語言模型提供一個標準化的介面,以便能夠輕鬆地連接和互動外部數據來源和工具。
在 MCP 的架構中,有幾個核心概念:
MCP 的價值在於把具體功能轉換成統一的抽象工具介面。舉例來說,如果有一個 getWeather(city)
的工具,底層實作可能是呼叫外部 API,也可能是讀取本地 JSON 檔,甚至連到企業內部資料庫。對 LLM 而言,這些差異完全透明,它看到的都是一個統一格式的工具介面,只需要輸入 city
,就能得到天氣資訊。
透過這樣的設計,Agent 不需要了解背後的細節,只要會呼叫 MCP Tool,就能與外部世界互動。
在沒有 MCP 之前,開發者往往必須自行設計 LLM 與外部工具的整合方式。常見的做法包括直接呼叫 REST API、透過 RPC 進行遠端呼叫,或是撰寫一段本地腳本交給 LLM 執行。雖然這些方法都能達到目的,但缺乏統一規範,導致整體發展呈現各自為政的狀態,並帶來幾個明顯的問題:
MCP 的出現,提供的不只是工具封裝的方法,而是一個標準化的協議層,讓工具與模型之間有一致的介面與規範:
換句話說,MCP 就像是 LLM 世界的 USB 協議。只要符合這個標準,不同功能模組就能「隨插即用」,而不必擔心相容性問題,這讓 MCP 成為 AI Agent 生態系發展的重要基礎。
要體驗 MCP 的開發流程,最好的方式就是從一個簡單的 MCP Server 開始。我們這裡示範一個「四則運算服務」,提供 add
、subtract
、multiply
、divide
四個工具,讓 Agent 可以直接呼叫計算。
在進入程式碼之前,先簡單說明一下 MCP 的傳輸類型。目前 MCP 官方定義了兩種標準 Transport:
在本篇示範中,我們選擇使用 Streamable HTTP 作為傳輸方式。原因是它能自然結合 Express 這類 Web 框架,並且特別適合未來要與 LangGraph Agent 等分散式系統整合的情境。如果你有興趣嘗試 stdi,也可以參考 MCP SDK 提供的範例。
在開始之前,請先初始化一個新的專案,命名為 calculator-mcp-server
,我們將在這個專案中完成 MCP Server 的實作。
Note:如果你對 Node.js 專案初始化流程還不熟悉,可以先回顧 Day 01 中「建立 Node.js 專案與 TypeScript 開發環境」的內容。
專案建立好之後,接著安裝以下套件:
npm install @modelcontextprotocol/sdk zod express
這些套件的用途如下:
@modelcontextprotocol/sdk
:MCP 的官方 SDK,提供 McpServer
以及傳輸介面與工具註冊的功能。zod
:Schema 驗證工具,用來定義與驗證工具的輸入參數,確保 LLM 呼叫時能提供正確的格式。express
:Node.js 的 HTTP 框架,這裡用來建立 API 端點並承載 MCP 的傳輸。以下是一個完整的 src/index.ts
範例程式,示範如何用 express
與 StreamableHTTPServerTransport
建立 MCP Server,並實作四則運算工具:
// src/index.ts
import express from 'express';
import { z } from 'zod';
import { randomUUID } from 'crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
const PORT = process.env.PORT || 3000;
const app = express();
app.use(express.json());
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transports[sessionId] = transport;
},
});
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
const server = new McpServer({
name: 'calculator-mcp-server',
version: '1.0.0',
});
const asText = (text: string) => {
return { content: [{ type: 'text' as const, text }] }
};
server.registerTool(
'add',
{ title: 'Addition', description: 'Add two numbers', inputSchema: { a: z.number(), b: z.number() } },
async ({ a, b }) => asText(String(a + b))
);
server.registerTool(
'subtract',
{ title: 'Subtraction', description: 'Subtract b from a', inputSchema: { a: z.number(), b: z.number() } },
async ({ a, b }) => asText(String(a - b))
);
server.registerTool(
'multiply',
{ title: 'Multiplication', description: 'Multiply two numbers', inputSchema: { a: z.number(), b: z.number() } },
async ({ a, b }) => asText(String(a * b))
);
server.registerTool(
'divide',
{ title: 'Division', description: 'Divide a by b', inputSchema: { a: z.number(), b: z.number() } },
async ({ a, b }) => {
if (b === 0) return { ...asText('Error: Division by zero'), isError: true as const };
return asText(String(a / b));
}
);
await server.connect(transport);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
await transport.handleRequest(req, res, req.body);
});
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
app.get('/mcp', handleSessionRequest);
app.delete('/mcp', handleSessionRequest);
app.listen(PORT, () => {
console.log(`[calculator-mcp-server] listening on port ${PORT}`);
});
這段程式碼主要完成了以下功能:
/mcp
路由處理所有與 MCP 相關的請求。mcp-session-id
header 區分不同的 Client,每個連線都能維持獨立的會話狀態。McpServer
,並註冊四則運算工具。每個工具的輸入結構由 zod
驗證,確保輸入正確。transport.handleRequest()
將請求交由 SDK 處理,Express 不必自行解析 JSON-RPC 協議。app.listen(PORT)
在指定的埠口啟動,並在 console 印出服務啟動訊息。透過以上實作,我們就完成了一個最基本的 MCP Server,能夠接受請求並提供四則運算的工具服務。
完成程式碼後,就可以啟動專案,驗證 MCP Server 是否能正常運作。
由於我們已在 package.json
中設定了 dev
指令,在開發階段可以直接使用以下命令啟動:
npm run dev
啟動後,你應該會看到輸出:
[calculator-mcp-server] listening on port 3000
代表 MCP Server 已經成功啟動,可以開始接受來自 MCP Client 的請求。
在完成 MCP Server 之後,接下來我們要建立一個 MCP Client 來測試剛剛註冊的四則運算工具。Client 的角色就像是使用者代理人,透過 MCP 協議去呼叫 Server 提供的工具,並取得結果。
在開始之前,請先初始化一個新的專案,命名為 calculator-mcp-client
,我們將在這個專案中完成 MCP Client 的實作。
Note:如果你對 Node.js 專案初始化流程還不熟悉,可以先回顧 Day 01 中「建立 Node.js 專案與 TypeScript 開發環境」的內容。
MCP Client 端只需要安裝一個核心套件即可:
npm install @modelcontextprotocol/sdk
@modelcontextprotocol/sdk
:提供 MCP Client 所需的 API,包括建立連線、管理工具清單,以及呼叫工具的功能。以下程式碼示範如何透過 Streamable HTTP Client transport 連線到剛剛建立的 MCP Server,並測試四則運算工具。
// src/index.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const MCP_SERVER_URL = process.env.MCP_SERVER_URL || 'http://localhost:3000/mcp';
async function main() {
const transport = new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL));
const client = new Client({ name: 'calculator-mcp-client', version: '1.0.0' });
await client.connect(transport);
console.log('Connected to MCP server at', MCP_SERVER_URL);
const tools = await client.listTools();
console.log('Available tools:', tools.tools.map((t) => t.name));
console.log('Running Calculations:');
const add = await client.callTool({ name: 'add', arguments: { a: 12, b: 8 } });
const sub = await client.callTool({ name: 'subtract', arguments: { a: 10, b: 4 } });
const mul = await client.callTool({ name: 'multiply', arguments: { a: 6, b: 7 } });
const div = await client.callTool({ name: 'divide', arguments: { a: 20, b: 4 } });
const getText = (r: any) => r.content?.[0]?.text ?? JSON.stringify(r);
console.log('12 + 8 =', getText(add));
console.log('10 - 4 =', getText(sub));
console.log('6 × 7 =', getText(mul));
console.log('20 ÷ 4 =', getText(div));
await client.close();
transport.close();
console.log('Disconnected from MCP server');
}
main();
這段程式碼主要完成了以下功能:
StreamableHTTPClientTransport
與 MCP Server 建立連線,並初始化一個 Client
。client.listTools()
取得 Server 提供的工具清單。client.callTool()
分別測試 add
、subtract
、multiply
、divide
四個工具,並將結果印出。這裡實作一個 getText
輔助函式把結果轉成字串輸出。client.close()
與 transport.close()
,確保連線乾淨關閉。透過以上實作,我們就完成了一個可測試的 MCP Client,確認 MCP Server 是否能正常運作。
當所有程式碼完成後,就可以啟動 Client,驗證是否能順利呼叫 MCP Server 的工具。
由於我們已在 package.json
中設定了 dev
指令,在開發階段可以直接使用以下命令啟動:
npm run dev
如果一切正常,你應該會在終端機看到如下輸出:
Connected to MCP server at http://localhost:3000/mcp
Available tools: [ 'add', 'subtract', 'multiply', 'divide' ]
Running Calculations:
12 + 8 = 20
10 - 4 = 6
6 × 7 = 42
20 ÷ 4 = 5
Disconnected from MCP server
這代表你的 Client 已經成功連上 MCP Server,並且正確調用了四則運算工具。
到目前為止,我們已經完成了 MCP Server 與 MCP Client,並且驗證了工具能正常運作。但光靠 Client 測試還不夠,實務應用上更常見的需求是讓 AI Agent 自己決定何時要呼叫 MCP Tools,而不是由我們人工下指令。這時候,就可以透過 LangGraph 來把 MCP Tools 整合進 Agent 的推理流程。
LangGraph 已經提供了 @langchain/mcp-adapters
套件,能自動將 MCP 服務轉換成 LangChain Tool
物件。這些工具會成為 Agent 的「外掛功能」,讓 LLM 在推理過程中自行判斷該不該使用工具,以及如何串連使用。換句話說,MCP 負責提供功能,LangGraph 則負責幫助 Agent 決策與編排工具的使用時機。
首先,我們使用官方工具 create-langgraph
來建立一個全新的專案:
create-langgraph calculator-agent
當工具詢問模板類型時,選擇 New LangGraph Project 即可。這個模板會自動幫你建立一個包含 src/agent
目錄的專案骨架。
接著進入專案資料夾並安裝相依套件:
cd calculator-agent
yarn install
初始化專案後,接著安裝以下套件:
yarn add @langchain/openai @langchain/mcp-adapters
這些套件的用途如下:
@langchain/openai
:提供 OpenAI LLM 的封裝,方便在 Agent 中使用。@langchain/mcp-adapters
:用來將 MCP Server 提供的工具自動轉換為 LangChain 的 Tool
。在專案根目錄建立 .env
檔案,填入 OpenAI API 金鑰:
LANGSMITH_KEY=llsv2...
OPENAI_API_KEY=sk-...
LANGSMITH_KEY
:啟用 LangSmith 觀察功能(可選,但建議填入,方便後續在 Studio UI 追蹤流程)。OPENAI_API_KEY
:OpenAI 模型的金鑰。專案的 src/agent
目錄下已經有兩個檔案:
state.ts
:定義狀態結構,預設已經包含訊息處理,我們直接沿用即可。graph.ts
:主要的流程定義檔,我們會在這裡實作 Calculator Agent。開啟 src/agent/graph.ts
,加入以下程式碼:
import { StateGraph, START, END } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { AIMessage } from '@langchain/core/messages';
import { ChatOpenAI } from '@langchain/openai';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { StateAnnotation } from './state.js';
const MCP_SERVER_URL = process.env.MCP_SERVER_URL || 'http://localhost:3000/mcp';
const client = new MultiServerMCPClient({
mcpServers: {
'calculator': {
url: MCP_SERVER_URL,
transport: 'http',
}
}
});
const tools = await client.getTools();
const toolNode = new ToolNode(tools);
const model = new ChatOpenAI({
model: 'gpt-4o-mini',
temperature: 0,
}).bindTools(tools);
function shouldContinue({ messages }: typeof StateAnnotation.State) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
return 'tools';
}
return END;
}
async function callModel(state: typeof StateAnnotation.State) {
const response = await model.invoke(state.messages);
return { messages: [response] };
}
const builder = new StateGraph(StateAnnotation)
.addNode('agent', callModel)
.addEdge(START, 'agent')
.addNode('tools', toolNode)
.addEdge('tools', 'agent')
.addConditionalEdges('agent', shouldContinue);
export const graph = builder.compile();
graph.name = 'Calculator Agent';
完成程式碼後,可以啟動 LangGraph Server,並透過 LangGraph Studio 觀察流程:
npx @langchain/langgraph-cli dev
啟動後,終端機會顯示本地伺服器 URL,例如:
http://127.0.0.1:2024
將它帶入 Studio UI 的網址:
https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
在 Studio UI 的 Chat 分頁輸入計算問題,例如:
what's (3 + 5) x 12?
我們可以從 Studio UI 中觀察:
The result of (3 + 5) multiplied by 12 is 96.
這代表 Agent 已經成功透過 LangGraph 整合 MCP Server 的工具,並在推理過程中自動選擇正確的運算步驟。
除了自己撰寫 MCP Server,你也可以直接利用 官方與社群提供的 Server,快速擴充 Agent 的能力。這些 Server 將各種外部 API 或系統功能包裝成標準化工具,你只需要透過 @langchain/mcp-adapters
連線,就能即時使用。
modelcontextprotocol/servers
最主要的官方來源是 GitHub 的 modelcontextprotocol/servers 倉庫。這裡收錄了:
由於這些伺服器都遵循 MCP 協議,因此幾乎可以「即插即用」,開發者可以在專案中快速導入並測試不同的外部服務。
除了官方倉庫,社群也逐漸建立起 MCP Server 索引平台,幫助開發者快速找到可用的服務。這些平台通常會整理一份清單,列出各式各樣由社群貢獻或第三方提供的 MCP Server,例如:
這些平台讓你能更快找到適合的工具,但在正式導入時,仍建議回到專案原始碼確認更新狀態與安全性。
在選用第三方 MCP Server 時,可以注意以下幾點:
透過這些官方與社群資源,MCP 生態系逐步形成一個「可插拔」的工具市場。你的 Agent 不再受限於本地功能,而是能隨時接入外部服務,打造更靈活且強大的智慧助理。
今天我們實作了如何透過 MCP(Model Context Protocol) 讓 AI Agent 與外部世界互通,並將 MCP Tools 整合進 LangGraph 中,讓 Agent 真正能在推理過程中自動使用外部服務:
MCP 讓 AI Agent 的能力邊界不再受限於模型本身,而能透過標準化工具協定即時接入外部世界,這也為 AI 生態奠定了可擴充的基礎。