iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
生成式 AI

用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰系列 第 26

Day 26 - 整合 MCP Tools:建構與外部世界互通的 AI Agent

  • 分享至 

  • xImage
  •  

前幾篇我們談到 AI Agent 在 LangGraph 中的決策、協作與記憶管理能力,但如果 Agent 永遠只能在封閉環境內工作,它的價值就會受到限制。真正在應用中有用的 Agent,往往需要具備與外部世界互通的能力,例如查詢 API、操作資料庫、存取企業內部系統,甚至透過安全協議來交換資料。

今天要介紹的 MCP,就是為了解決如何讓模型與外部世界安全、標準化地交換訊息而設計的協定。我們將示範如何在 LangGraph 中整合 MCP Tools,打造能真正與外部系統互動的 AI Agent。

什麼是 MCP?

MCP(Model Context Protocol) 是由 Anthropic 於 2024 年底推出的開放標準協定,目的是為大型語言模型提供一個標準化的介面,以便能夠輕鬆地連接和互動外部數據來源和工具。

在 MCP 的架構中,有幾個核心概念:

  • MCP Host:MCP Host 可以是一個 LLM 應用程式或 AI Agent,主要負責載入並管理 MCP 工具,並將它們提供給模型使用。例如,一個聊天機器人平台就能作為 Host,透過 MCP Client 對接不同的外部功能,讓模型在對話中靈活調用。
  • MCP Client:MCP Client 扮演溝通橋樑的角色。它會與 MCP Server 建立連線,並把 Server 定義的工具描述轉換成 LLM 可直接理解與調用的格式。對 AI Agent 而言,它不需要知道底層實作細節,只要透過 Client,就能像使用內建函式一樣呼叫外部工具。
  • MCP Server:MCP Server 是功能的提供者,可以是任何類型的服務,例如查詢天氣的 API、讀寫檔案的工具、資料庫查詢代理,甚至是你自己撰寫的小型程式。只要符合 MCP 的規範,就能被 AI Agent 調用。

https://ithelp.ithome.com.tw/upload/images/20250926/20150150fM8SrOUm2P.png

圖片來源:Model Context Protocol

MCP 的價值在於把具體功能轉換成統一的抽象工具介面。舉例來說,如果有一個 getWeather(city) 的工具,底層實作可能是呼叫外部 API,也可能是讀取本地 JSON 檔,甚至連到企業內部資料庫。對 LLM 而言,這些差異完全透明,它看到的都是一個統一格式的工具介面,只需要輸入 city,就能得到天氣資訊。

透過這樣的設計,Agent 不需要了解背後的細節,只要會呼叫 MCP Tool,就能與外部世界互動。

為什麼 MCP 重要?

在沒有 MCP 之前,開發者往往必須自行設計 LLM 與外部工具的整合方式。常見的做法包括直接呼叫 REST API、透過 RPC 進行遠端呼叫,或是撰寫一段本地腳本交給 LLM 執行。雖然這些方法都能達到目的,但缺乏統一規範,導致整體發展呈現各自為政的狀態,並帶來幾個明顯的問題:

  • 缺乏標準:每個專案都定義自己的呼叫協議,導致工具無法重複使用,也難以在不同應用之間共享。
  • 開發與維護成本高:每新增一個工具,就要額外撰寫一層整合程式碼,專案規模一大,維護工作就變得非常沉重。
  • 安全性與一致性不足:LLM 如果能直接操作底層 API 或執行程式碼,容易造成不可控的副作用;而且沒有統一的存取規範,安全性很難保障。

MCP 的出現,提供的不只是工具封裝的方法,而是一個標準化的協議層,讓工具與模型之間有一致的介面與規範:

  • 對開發者而言:不必從零撰寫橋接程式碼,只要 MCP Server 遵循協議,就能被任何支援 MCP 的 Agent 使用,大幅降低整合成本。
  • 對生態系而言:MCP Server 可以由社群共享,形成可互通的工具市場,這讓開發者不再受限於自己寫的功能,而能快速接入全球社群提供的能力。
  • 對安全性而言:MCP 規範了工具的輸入與輸出格式,並由 MCP Client 管理調用過程,避免 LLM 直接接觸高風險 API,讓權限控制與安全防護更容易設計與落實。

換句話說,MCP 就像是 LLM 世界的 USB 協議。只要符合這個標準,不同功能模組就能「隨插即用」,而不必擔心相容性問題,這讓 MCP 成為 AI Agent 生態系發展的重要基礎。

實作:建立一個 MCP Server

要體驗 MCP 的開發流程,最好的方式就是從一個簡單的 MCP Server 開始。我們這裡示範一個「四則運算服務」,提供 addsubtractmultiplydivide 四個工具,讓 Agent 可以直接呼叫計算。

在進入程式碼之前,先簡單說明一下 MCP 的傳輸類型。目前 MCP 官方定義了兩種標準 Transport

  • stdio:透過標準輸入/輸出進行通訊,適合用於本地 CLI 工具、IDE 插件或桌面應用程式。這種方式通常與 LLM runtime(例如 VS Code 插件或本地 Agent)直接整合。
  • Streamable HTTP:以 HTTP 為基礎的通訊方式,支援跨語言與跨網路的應用場景。只要有 HTTP 介面,就能讓外部 Agent 或服務輕鬆連線,非常適合分散式或雲端場景。

在本篇示範中,我們選擇使用 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 的傳輸。

建立 MCP Server

以下是一個完整的 src/index.ts 範例程式,示範如何用 expressStreamableHTTPServerTransport 建立 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}`);
});

這段程式碼主要完成了以下功能:

  1. 建立 Express 應用:啟動一個 HTTP 伺服器,並在 /mcp 路由處理所有與 MCP 相關的請求。
  2. 管理 Session:使用 mcp-session-id header 區分不同的 Client,每個連線都能維持獨立的會話狀態。
  3. 初始化 MCP Server:在第一次請求時建立 McpServer,並註冊四則運算工具。每個工具的輸入結構由 zod 驗證,確保輸入正確。
  4. 處理 MCP 協議請求:透過 transport.handleRequest() 將請求交由 SDK 處理,Express 不必自行解析 JSON-RPC 協議。
  5. 啟動伺服器:呼叫 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 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,包括建立連線、管理工具清單,以及呼叫工具的功能。

建立 MCP Client

以下程式碼示範如何透過 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();

這段程式碼主要完成了以下功能:

  1. 建立連線:使用 StreamableHTTPClientTransport 與 MCP Server 建立連線,並初始化一個 Client
  2. 列出可用工具:透過 client.listTools() 取得 Server 提供的工具清單。
  3. 呼叫工具並取得結果:使用 client.callTool() 分別測試 addsubtractmultiplydivide 四個工具,並將結果印出。這裡實作一個 getText 輔助函式把結果轉成字串輸出。
  4. 關閉連線:測試完成後,呼叫 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,並且正確調用了四則運算工具。

實作:在 LangGraph 中整合 MCP Tools

到目前為止,我們已經完成了 MCP ServerMCP Client,並且驗證了工具能正常運作。但光靠 Client 測試還不夠,實務應用上更常見的需求是讓 AI Agent 自己決定何時要呼叫 MCP Tools,而不是由我們人工下指令。這時候,就可以透過 LangGraph 來把 MCP Tools 整合進 Agent 的推理流程。

LangGraph 已經提供了 @langchain/mcp-adapters 套件,能自動將 MCP 服務轉換成 LangChain Tool 物件。這些工具會成為 Agent 的「外掛功能」,讓 LLM 在推理過程中自行判斷該不該使用工具,以及如何串連使用。換句話說,MCP 負責提供功能,LangGraph 則負責幫助 Agent 決策與編排工具的使用時機。

建立 LangGraph 專案

首先,我們使用官方工具 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 模型的金鑰。

撰寫 Calculator Agent 流程

專案的 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';

測試 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?

https://ithelp.ithome.com.tw/upload/images/20250926/20150150a8YGg80IqA.png

我們可以從 Studio UI 中觀察:

  1. 使用者的問題先進到 agent 節點,由 LLM 判斷是否需要呼叫 MCP 工具。
  2. LLM 呼叫我們綁定的 MCP 工具,計算問題。
  3. 工具回傳觀察結果,再送回 LLM。
  4. LLM 根據計算結果,給出最終答案。
The result of (3 + 5) multiplied by 12 is 96.

這代表 Agent 已經成功透過 LangGraph 整合 MCP Server 的工具,並在推理過程中自動選擇正確的運算步驟。

整合外部服務:連接第三方 MCP Server

除了自己撰寫 MCP Server,你也可以直接利用 官方與社群提供的 Server,快速擴充 Agent 的能力。這些 Server 將各種外部 API 或系統功能包裝成標準化工具,你只需要透過 @langchain/mcp-adapters 連線,就能即時使用。

官方清單:modelcontextprotocol/servers

最主要的官方來源是 GitHub 的 modelcontextprotocol/servers 倉庫。這裡收錄了:

  • Reference Servers:由官方維護的參考實作,通常用來展示 MCP 規範的最佳實踐。例如提供檔案存取、命令列工具、或 API 呼叫的基礎範例,方便開發者理解如何撰寫自己的 MCP Server。
  • Third-Party Servers:由社群或合作夥伴提供的外部服務封裝,例如天氣查詢、資料庫操作或雲端服務 API。這些工具通常能直接整合到 Agent 當中,方便開發者快速擴充系統能力。

由於這些伺服器都遵循 MCP 協議,因此幾乎可以「即插即用」,開發者可以在專案中快速導入並測試不同的外部服務。

社群提供的 MCP Server 索引平台

除了官方倉庫,社群也逐漸建立起 MCP Server 索引平台,幫助開發者快速找到可用的服務。這些平台通常會整理一份清單,列出各式各樣由社群貢獻或第三方提供的 MCP Server,例如:

  • cursor.directory:由 Cursor 社群整理的 MCP 相關資源清單。
  • MCP.so:目前較知名的 MCP Server 索引平台,提供分類清單與搜尋功能。
  • Glama:收錄多種 AI 工具與 MCP Server,並提供整合範例。

這些平台讓你能更快找到適合的工具,但在正式導入時,仍建議回到專案原始碼確認更新狀態與安全性。

在選用第三方 MCP Server 時,可以注意以下幾點:

  • 來源可信度:優先選擇官方或社群活躍維護的專案。
  • 更新頻率:檢查最近更新時間,確保與最新協定版本相容。
  • 安全性:若需要 API Key 或系統存取權限,應先在隔離環境測試,再導入正式系統。

透過這些官方與社群資源,MCP 生態系逐步形成一個「可插拔」的工具市場。你的 Agent 不再受限於本地功能,而是能隨時接入外部服務,打造更靈活且強大的智慧助理。

小結

今天我們實作了如何透過 MCP(Model Context Protocol) 讓 AI Agent 與外部世界互通,並將 MCP Tools 整合進 LangGraph 中,讓 Agent 真正能在推理過程中自動使用外部服務:

  • MCP 架構分成 Host、Client、Server,分別負責管理工具、轉換協議與提供功能,讓 LLM 能像使用內建函式一樣調用外部資源。
  • MCP 的核心價值在於統一標準,降低開發與維護成本、改善安全性,並促進工具在生態系間的互通,就像 LLM 世界的 USB 協議。
  • 我們實作了一個提供四則運算的 MCP Server,並用 MCP Client 驗證可用工具與呼叫結果。
  • 接著透過 LangGraph,把 MCP Tools 包進 Agent 的決策流程,讓 LLM 能自動判斷何時使用工具並回傳正確答案。
  • 除了自建 Server,還能透過官方與社群提供的 MCP Servers,快速擴充 Agent 功能。

MCP 讓 AI Agent 的能力邊界不再受限於模型本身,而能透過標準化工具協定即時接入外部世界,這也為 AI 生態奠定了可擴充的基礎。


上一篇
Day 25 - AI Agent 記憶管理:打造能延續對話與持久化的智慧助理
下一篇
Day 27 - LangGraph 整合實戰:打造具網路搜尋與人機互動能力的 AI 驅動寫作代理
系列文
用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言