在上一篇文章中我們將 MCP Server 已經實作完了,接下來我們就要來實作 MCP Client 與實際上的串接到我們的工具人上。
事實上在 LangChain 要串接 MCP Server 還算蠻簡單的,只要使用官方的 LangChain.js MCP Adapters 就好了。
https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-mcp-adapters
npm install @langchain/mcp-adapters
然後你現在應該也會部到這個錯誤,因為他還沒支援 1.0.0 版本的 LangChain。
$ npm install @langchain/mcp-adapters
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: 2025-ai-mark@1.0.0
npm error Found: @langchain/core@1.0.0-alpha.6
npm error node_modules/@langchain/core
npm error @langchain/core@"^1.0.0-alpha.5" from the root project
npm error
npm error Could not resolve dependency:
npm error peer @langchain/core@"^0.3.66" from @langchain/mcp-adapters@0.6.0
npm error node_modules/@langchain/mcp-adapters
npm error @langchain/mcp-adapters@"*" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
所以你可以先使用以下的方法強行安裝,目前試了一下是沒什麼問題,不過還是要提醒,建議一切以官方 stable 版本為主,這個還是以實驗為主
。
"overrides": {
"@langchain/mcp-adapters": {
"@langchain/core": "1.0.0-alpha.5"
}
}
接下來我們程式碼大概就是如下,這裡就只貼部份的程式碼,反正使用起來就是透過 MCP Client 取得 tools,接下來再丟給 createAgent 裡的 tools 中,用法和 function calling 差不多。
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
const notionMcpClient = new MultiServerMCPClient({
notion: {
url: "http://localhost:3002/notion/mcp",
},
});
const notionMcpTools = await notionMcpClient.getTools();
export class SummaryAgent {
private checkpointSaver: BaseCheckpointSaver;
private configurable: Configurable;
private agent: any;
constructor(
checkpointSaver: BaseCheckpointSaver,
configurable: Configurable,
notionMcpTools: DynamicStructuredTool[]
) {
this.checkpointSaver = checkpointSaver;
this.configurable = configurable;
this.agent = createAgent({
model: "openai:gpt-5",
tools: [getLearningRecords, ...notionMcpTools],
checkpointer: this.checkpointSaver,
// ref: https://blog.langchain.com/agent-middleware/
middleware: [cleanMessageMiddleware],
responseFormat: ResponseFormatSchema,
});
}
我這裡有進行以下的修改,來當成不同的情境使用時,回傳的格式,如下所示。
const SummaryResponseSchema = z
.object({
task: z.literal(TaskEnum.SUMMARY).describe("只要回傳 summary"),
response: z.object({
youLearned: z.string().describe("你今天學了什麼(總結版)"),
yourOutput: z.string().describe("你今天的產出"),
feedback: z.string().describe("自我回饋"),
afterThoughtQuestions: z.string().describe("課後思考"),
createdAt: z.string().describe("這份總結生成時間(ISO)"),
}),
})
.describe("總結學習的回應格式,當沒有用工具時,只進行總結時,回傳這個格式");
const GetLearningRecordsResponseSchema = z
.object({
task: z.literal(TaskEnum.GET_LEARNING_RECORDS).describe("只要回傳 get-learning-records"),
response: z.object({
youLearned: z.string().describe("學了什麼(總結版)"),
yourOutput: z.string().describe("產出"),
feedback: z.string().describe("自我回饋"),
afterThoughtQuestions: z.string().describe("課後思考"),
createdAt: z.string().describe("這份總結生成時間(ISO)"),
}),
})
.describe(
"取得學習記錄的回應格式,有用到 getLearningRecords 工具時,回傳這個格式"
);
const CreateNotionPageResponseSchema = z
.object({
task: z.literal(TaskEnum.CREATE_NOTION_PAGE).describe("只要回傳 create-notion-page"),
response: z.object({
success: z.boolean().describe("是否成功建立 Notion 筆記"),
message: z.string().describe("回傳 create_page 工具的結果"),
}),
})
.describe(
"建立 Notion 筆記的回應格式,有用到 createNotionPage MCP 工具時,回傳這個格式"
);
然後我的 Agent 使用的方式如下 :
export class SummaryAgent {
private checkpointSaver: BaseCheckpointSaver;
private configurable: Configurable;
private agent: any;
constructor(
checkpointSaver: BaseCheckpointSaver,
configurable: Configurable,
notionMcpTools: DynamicStructuredTool[]
) {
this.checkpointSaver = checkpointSaver;
this.configurable = configurable;
this.agent = createAgent({
model: "openai:gpt-5-mini",
tools: [getLearningRecords, ...notionMcpTools],
checkpointer: this.checkpointSaver,
// ref: https://blog.langchain.com/agent-middleware/
middleware: [cleanMessageMiddleware],
responseFormat: toolStrategy([
SummaryResponseSchema,
GetLearningRecordsResponseSchema,
CreateNotionPageResponseSchema,
]),
});
}
然後有幾個小提醒 :
] Chat error: StructuredOutputParsingError: Failed to parse structured output for tool 'extract-2': [0] - Property "task" does not match schema. [0] - Instance does not match "get-learning-records".. [0] at ToolStrategy.parse (/Users/marklin/Documents/github/2025-AI-Mark/node_modules/langchain/src/agents/responses.ts:141:13) [0] at #handleSingleStructuredOutput (/Users/marklin/Documents/github/2025-AI-Mark/node_modules/langchain/src/agents/middlewareAgent/nodes/AgentNode.ts:346:39) [0] at AgentNode.#invokeModel (/Users/marklin/Documents/github/2025-AI-Mark/node_modules/langchain/src/agents/middlewareAgent/nodes/AgentNode.ts:296:17) [0] at process.processTicksAndRejections
然後這個是我們的溝通過程,你看最後他還有提到 notion 連結給你。
以下我們的的 Notion 結果。
大概就是如下,這個是請 AI 寫的,但如果真的要自幹一個,那事實上大概會長的如下,然後再想辦法讓 LangChain 的 Agent 或是 LangGraph Workflow 當判斷到某種情況下呼叫 callTool 之類的。
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { CallToolResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types";
interface Tool {
name: string;
description?: string;
inputSchema?: any;
}
class MCPTestClient {
private client: Client;
private transport: StreamableHTTPClientTransport | null = null;
private serverUrl: string;
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
this.client = new Client(
{
name: "mcp-test-client",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
}
async connect(): Promise<void> {
try {
console.log(`🔗 正在連接到 MCP server: ${this.serverUrl}`);
console.log('new SSEClientTransport',new URL(this.serverUrl));
this.transport = new StreamableHTTPClientTransport(new URL(this.serverUrl));
await this.client.connect(this.transport);
console.log("✅ 成功連接到 MCP server");
} catch (error) {
console.error("❌ 連接失敗:", error);
throw error;
}
}
async listTools(): Promise<Tool[]> {
try {
console.log("tools/list request");
const response = await this.client.request(
{ method: "tools/list" },
ListToolsResultSchema
);
console.log("tools/list response",response);
console.log("🔧 可用工具:");
response.tools.forEach((tool: Tool, index: number) => {
console.log(` ${index + 1}. ${tool.name}: ${tool.description || "無描述"}`);
});
return response.tools;
} catch (error) {
console.error("❌ 獲取工具列表失敗:", error);
return [];
}
}
async callTool(name: string, arguments_?: any): Promise<any> {
const startTime = Date.now();
try {
console.log(`🚀 調用工具: ${name}`, arguments_ ? `參數: ${JSON.stringify(arguments_, null, 2)}` : "");
const response = await this.client.request(
{
method: "tools/call",
params: {
name,
arguments: arguments_ || {}
}
},
CallToolResultSchema
);
const duration = Date.now() - startTime;
console.log(`✅ 工具 ${name} 執行成功 (${duration}ms)`);
console.log("📄 結果:", JSON.stringify(response, null, 2));
return {
tool: name,
success: true,
result: response,
duration
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ 工具 ${name} 執行失敗 (${duration}ms):`, error);
return {
tool: name,
success: false,
error: error instanceof Error ? error.message : String(error),
duration
};
}
}
}
export { MCPTestClient };
事實上這篇文章看似簡單,但事實上也不太像我想的那麼簡單,我覺得比較大的問題在於,以下兩個 :
這裡又調整了好幾次 tool 與 structuredResponse 的 Prompt 最後才比較能成功完成,真的後來發現開發到現在,讓 AI 乖乖跟這你腦袋做事情才是最難的。