ai agent---mcp知识汇总

18 阅读3分钟

一.agent定义自己的工具,并绑定他,执行他。

//定义大模型
const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL
  }
})
//引入工具
const tools = [
  readFileTool,
  writeTool,
  executeCommandTool,
  listDirectoryTool
]
//大模型绑定工具
const modelWithTool = model.bindTools(tools);

const messages=[
    new SystemMessage(`
    你是一个项目管理助手,使用工具完成任务,。。。
    `),
    new HumanMessage('你帮我干个啥呢')
]

//绑定工具后的大模型执行message

const response = await modelWithTool.invoke(messages);

//看一下需要执行哪个工具

for(const toolCall of response.tool_calls){
      const foundTool = tools.find(t=>t.name===toolCall.name);
      if(foundTool){
       // 这是‌调用大语言模型(LLM)‌,对应Agent的「思考/决策」环节:
        const toolResult = await foundTool.invoke(toolCall.args);
        messages.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolCall.id
        }))
      }
    }
 
return messages[messages.length-1].content;

代码逻辑:

  1. 定义工具,使用import {tool } from '@langchain/core/tools';
  2. 定义大模型,import {ChatOpenAI} from '@langchain/openai';
  3. 大模型和工具相互绑定
  4. 执行invoke()执行提示
  5. 大模型通过response.tool_calls他需要的工具,工具执行invoke()获取结果。

定义工具的办法:

import {tool } from '@langchain/core/tools';
import fs from 'node:fs/promises';//node之后的规范写法
import path from 'node:path';
import {spawn} from 'node:child_process';
import {z} from 'zod';

//1.读取文件
const readFileTool = tool(
  async({filePath})=>{
    const content = await fs.readFile(filePath, 'utf-8');
    return `文件内容: ${content}`
  },{
    name: 'read_file',
    description: '读取文件',
    schema: z.object({
      filePath: z.string().describe('文件路径')
    })
  
  }
)

二.利用MCP定义远程工具,然后使用他

服务端代码

#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import { resolve } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const dataBase = {
  users: {
    '001': { id: '001', name: '张三', age: 30 },
    '002': { id: '002', name: '李四', age: 28 },
    '003': { id: '003', name: '王五', age: 35 },
    '004': { id: '004', name: '赵六', age: 32 },
    '005': { id: '005', name: '孙七', age: 29 },
    '006': { id: '006', name: '周八', age: 27 },
    '007': { id: '007', name: '吴九', age: 31 },
    '008': { id: '008', name: '郑十', age: 33 },
  }
};
// ✅ 1. 创建 MCP Server
const server = new McpServer({
  name: 'my-mcp-server',
  version: '1.0.0',
});

server.tool(
  "Query_user",
  "查询数据库中用户信息,输入用户ID,返回该用户的详细信息(id,name,age)",
  {
    userId: z.string().describe('用户 id,例如 "001"')
  },
  async ({ userId }) => {
    const user = dataBase.users[userId];
    if (!user) {
      // ✅ 严格标准格式:外层是对象,content是数组,元素必须是{type:"text", text:字符串}
      return {
        content: [
          {
            type: "text",
            text: `查询失败:用户 ${userId} 不存在`
          }
        ],
        // 可选:标记为错误结果,不影响返回格式
        isError: true
      };
    }
    return {
      content: [
        {
          type: "text",
          text: `查询成功,用户信息如下:\n${JSON.stringify(user, null, 2)}`
        }
      ]
    };
  }
);

// ✅ 2. 注册 Resource(重点)
server.resource(
  "使用指南", // resource name
  "docs://guide", // 这个是读取资源的名字,在client的时候使用
  async (uri) => {
    const filePath = resolve(__dirname, 'docs', 'guide.md');// 读取真实的本地guide.md文件(路径:当前脚本目录下的docs/guide.md)
    let content = '';
    try {
      content = await fs.readFile(filePath, 'utf-8');
    } catch (err) {
      content = err.message;
    }
    
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "text/plain",
          text: content
        }
      ]
    };
  }
);



// ✅ 4. 启动 Server
const transport = new StdioServerTransport();
await server.connect(transport);

console.error("✅ MCP Resource Server is running...");

客户端代码

import 'dotenv/config'
import { MultiServerMCPClient } from'@langchain/mcp-adapters';
import { ChatOpenAI } from'@langchain/openai';
import chalk from'chalk';
import { HumanMessage, ToolMessage } from'@langchain/core/messages'

const model = new ChatOpenAI({
  modelName:"qwen-plus",
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
  baseURL: process.env.OPENAI_BASE_URL,
}
});

const mcpClient = new MultiServerMCPClient({
    "my-mcp-server": {
      command: "node",
      args: ["src/mcp-test/my-mcp-server.mjs"],
      cwd: process.cwd(), // 强制用当前终端的工作目录作为基准,避免路径错位
      // 关键新增:显式指定stdio传输协议
      transport: "stdio"
    }

});

const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);

async function runAgentWithTools(query,maxIterations = 30) {
  const messages = [
    new HumanMessage(query)
  ]; 
  
  for (let i= 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`正在等待 AI思考...`));
    const response = await modelWithTools.invoke(messages);
    messages.push(response);
    
    // 检查是否有工具调用
  if (!response.tool_calls || response.tool_calls.length === 0) {
    console.log(` AI 最终回复:${response.content}`); 
    return response.content;
  }
  
  
  console.log(chalk.bgBlue( `检测到 ${response.tool_calls.length} 个工具调用~`));
  
  // 执行工具调用
  for (const toolCall of response.tool_calls) {
    const foundTool = tools.find(t => t.name ===toolCall.name);
    
    if (foundTool) {
       const toolResult = await foundTool.invoke(toolCall.args);
      messages.push(new ToolMessage({
        content: toolResult,
        tool_call_id: toolCall.id,
      }));
    }else{
      console.log(chalk.bgRed(`找不到名为 ${toolCall.name} 的工具`));
      return `找不到名为 ${toolCall.name} 的工具`;
    }
  }
}
  return messages[messages.length - 1].content;
}   
console.log( 12121212);
//await runAgentWithTools("查一下用户 002 的信息?");
//const res = await mcpClient.listResources("my-mcp-server");
const res = await mcpClient.readResource(
  "my-mcp-server",
  "docs://guide"//服务端有很多个读取静态资源的代码,你要说清楚是哪个资源,我这里用的是guide.md文件,如果不写,client就不知道你要读取的是谁。所以在教程里面不写是不行的
);

console.log(res)

await mcpClient.close();

你自己的本地的tool,就用model.bindTools绑定就好了,现在如果是远程的tools呢?就要用到MCP了,MCP是什么就参考这个文章:cloud.tencent.com/developer/a…

他就是个协议,很像https,在互联网上的tools都可以通过MCP引进来,自己用。

1.静态资源获取

在上面代码里面

server.resource(
  "使用指南", // resource name
  "docs://guide", // 这个是读取资源的名字,在client的时候使用
  async (uri) => {
    const filePath = resolve(__dirname, 'docs', 'guide.md');// 读取真实的本地guide.md文件(路径:当前脚本目录下的docs/guide.md)
    let content = '';
    try {
      content = await fs.readFile(filePath, 'utf-8');
    } catch (err) {
      content = err.message;
    }
    
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "text/plain",
          text: content
        }
      ]
    };
  }
);

是获取静态资源的,前端调用的时候,就需要指定资源的名字

image.png

如果不指定,就不知道你想要的获取的是哪个静态资源了。

2.工具使用

image.png

server定义了工具,还命名了工具名,但是在使用的时候,却没有指定工具名,那大模型具体使用哪个?

image.png

回复如下:

image.png

image.png

言外之意就是,工具是我开发的,工具的description很重要,大模型就是靠着description来决定到底要用哪个工具,开发者不需要手动定义。

image.png

三.使用别人的tool

import 'dotenv/config';
import { MultiServerMCPClient } from'@langchain/mcp-adapters';
import {ChatOpenAI} from'@langchain/openai';
import {HumanMessage, SystemMessage,ToolMessage} from'@langchain/core/messages';
import chalk from 'chalk'

const model = new ChatOpenAI({
  modelName:'qwen-plus',
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  }
});

const mcpClient = new MultiServerMCPClient({
  //这里可以引入很多个mcp服务,这里只引入高德地图的mcp服务
  "amap-maps-streamableHttp":{
    "url": "https://mcp.amap.com/mcp?key="+ process.env.GAODE_MAP_API_KEY,
    "transport": "http",
 },
 //这里我要引入mcp自己定义的文件处理tools服务
  "file-system": {
    command: "npx",//这里用的npx是因为我本地安装了@modelcontextprotocol/server-filesystem,如果要全局安装则可以不用npx
    args: [
       "-y",
      "@modelcontextprotocol/server-filesystem",
      "E:/AITool/tool-test",   // ← 必填:允许访问的根目录
    ],
  },
  //要大模型帮我们打开浏览器,然后自动处理网页的工具,这里用的是chrome-devtools-mcp工具包
  "chrome-devtools":{
    "command":"npx",
    "args": [
      "-y",
      "chrome-devtools-mcp@latest"
    ]
  }
}) 

const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);

async function runAgentWithTools(query,maxIterations = 30) {
  const messages = [
    new HumanMessage(query)
  ]; 
  
  for (let i= 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`正在等待 AI思考...`));
    const response = await modelWithTools.invoke(messages);
    messages.push(response);
    
    // 检查是否有工具调用
  if (!response.tool_calls || response.tool_calls.length === 0) {
    console.log(` AI 最终回复:${response.content}`); 
    return response.content;
  }
  
  // 执行工具调用
  for (const toolCall of response.tool_calls) {
    const foundTool = tools.find(t => t.name ===toolCall.name);
    
    if (foundTool) {
      const toolResult = await foundTool.invoke(toolCall.args);
      let res = '';
      if(typeof toolResult === 'string'){
        res= toolResult;
      }else if(toolResult  && toolResult.text){
        res= toolResult.text;
      }
      
      messages.push(new ToolMessage({
        content: res,
        tool_call_id: toolCall.id,
      }));
    }else{
      console.log(chalk.bgRed(`找不到名为 ${toolCall.name} 的工具`));
      return `找不到名为 ${toolCall.name} 的工具`;
    }
  }
}
  return messages[messages.length - 1].content;
}   
console.log( 12121212);
//await runAgentWithTools("北京南站附近的酒店,以及过去的路线");
//await runAgentWithTools("北京南站附近的5个酒店,以及去的路线,路线规划生成文档,保存到 E:/AITool/tool-test/test.md 文件里,并返回保存的路径。");
await runAgentWithTools("北京南站附近的酒店,最近的 3 个酒店,拿到酒店图片,然后用浏览器打开,一个图片对应一个浏览器标签页,并且把对应页面的标题改成酒店名字");
await mcpClient.close();

image.png

我们用高德地图的mcp拿到酒店地址,用file-system的文件处理工具在本地建了一个文件,然后把酒店信息存储在文件里,又利用chrome-devtools工具将酒店图片展示到浏览器上。 上述代码的关系形式:image.png

思考问题1:

下图,为什么有的工具用stdio协议,有的使用http协议?他到底是谁来决定的?

image.png

在 MCP(Model Context Protocol)架构中,stdiohttp(通常指 Streamable HTTP 或早期的 SSE)是两种不同的传输层协议(Transport) ,负责客户端(MCP Adapter)与服务端(MCP Server)之间传递消息。

决定使用哪种协议的,是服务端的部署形态、运行位置以及你在客户端代码中的配置

本地通信用stdio 远程通信用http

开发者在定义tool的时候是可以规定的,比如:

// 配置本地 stdio 服务

const fileSystemServer = {

command: "node",

args: ["path/to/filesystem-server.js"],

transport: "stdio" // 👈 决定用 stdio

};
// 配置远程 http 服务

const amapServer = {

url: "https://mcp.amap.com/xxx",

transport: "http" // 👈 决定用 http

};

总结

  • 本地工具(跑在自己电脑上的脚本、本地文件、本地浏览器):用 stdio,简单、快、安全。
  • 远程服务(云端的 API、第三方开放平台如高德):用 http,跨网络、可共享。

一句话概括:谁决定的?是业务场景和你的客户端配置决定的。本地起子进程就用 stdio,调用云端网址就用 http。