Node搭建MCP

什么是MCP?

官方介绍:https://www.anthropic.com/news/model-context-protocol
MCP是一种开放协议,通过标准化的服务器实现,使AI模型能够安全地与本地和远程资源进行交互。
个人理解:MCP只是提供一种规范,将服务器搭建格式本地客户端连接服务形式做了约束(你必须按照我说的做,要不你就连不了我)。因为其它服务器符合MCP规范定义,所以客户端client(claude)就在mcp规范基础上连接服务器。双方达成约定,实现共赢
举个例子:当你给电脑插入USB设备时,无论是鼠标、键盘还是U盘,只要遵循 USB 标准接口,就能被系统识别和使用。MCP 就是这样一个为 LLM(大型语言模型)提供的“通用标准接口”,只不过它是用在 LLM 与外部数据源、工具或应用之间的信息沟通中,而不是给电脑插东西。

工作原理

在这里插入图片描述

[图片]

MCP 协议采用了一种独特的架构设计,它将 LLM 与资源之间的通信划分为三个主要部分:客户端、服务器和资源。
客户端负责发送请求给 MCP 服务器,服务器则将这些请求转发给相应的资源。这种分层的设计使得 MCP 协议能够更好地控制访问权限,确保只有经过授权的用户才能访问特定的资源。

MCP 核心架构

MCP 遵循客户端-服务器架构(client-server),其中包含以下几个核心概念:

  • MCP 主机(MCP Hosts):发起请求的 LLM 应用程序(例如 Claude Desktop、IDE 或 AI 工具)。
  • MCP 客户端(MCP Clients):在主机程序内部,与 MCP server 保持 1:1 的连接。
  • MCP 服务器(MCP Servers):为 MCP client 提供上下文、工具和 prompt 信息。
  • 本地资源(Local Resources):本地计算机中可供 MCP server 安全访问的资源(例如文件、数据库)。
  • 远程资源(Remote Resources):MCP server 可以连接到的远程资源(例如通过 API)。
MCP Client

MCP client 充当 LLM 和 MCP server 之间的桥梁,MCP client 的工作流程如下:

  • MCP client 首先从 MCP server 获取可用的工具列表。
  • 将用户的输入+工具(通过 function calling) 一起发送给 LLM。
  • LLM 决定是否需要使用工具以及使用哪些工具。
  • 如果需要使用工具,MCP client 会通过 MCP server 执行相应的工具调用。
  • 工具调用的结果会被发送回 LLM。
  • LLM 基于所有信息生成自然语言响应。
  • 最后将响应展示给用户。
    Claude Desktop 和Cursor都支持了MCP Server接入能力,它们就是作为 MCP client来连接某个MCP Server感知和实现调用。
MCP Server

MCP server 是 MCP 架构中的关键组件,它可以提供 3 种主要类型的功能:

  • 资源(Resources):类似文件的数据,可以被客户端读取,如 API 响应或文件内容。
  • 工具(Tools):可以被 LLM 调用的函数(需要用户批准)。
  • 提示(Prompts):预先编写的模板,帮助用户完成特定任务。
    使用 TypeScript 编写的 MCP server 可以通过 npx 命令来运行,使用 Python 编写的 MCP server 可以通过 uvx 命令来运行。
    MCP server社区: MCP Servers Repository 和 Awesome MCP Servers 。

MCP 的基本工作流程

以下是 MCP 的基本工作流程:

  • 初始化连接:客户端向服务器发送连接请求,建立通信通道。
  • 发送请求:客户端根据需求构建请求消息,并发送给服务器。
  • 处理请求:服务器接收到请求后,解析请求内容,执行相应的操作(如查询数据库、读取文件等)。
  • 返回结果:服务器将处理结果封装成响应消息,发送回客户端。
  • 断开连接:任务完成后,客户端可以主动关闭连接或等待服务器超时关闭。

三、MCP、FunctionCalling 、AI AGent区别

Function Calling

  • Function Calling 指的是 AI 模型根据上下文自动执行函数的机制。
  • Function Calling 充当了 AI 模型与外部系统之间的桥梁,不同的模型有不同的 Function Calling 实现,代码集成的方式也不一样。由不同的 AI 模型平台来定义和实现。
    如果我们使用 Function Calling,那么需要通过代码给 LLM 提供一组 functions,并且提供清晰的函数描述、函数输入和输出,那么 LLM 就可以根据清晰的结构化数据进行推理,提取需要的执行函数,将其返回。
    Function Calling 的缺点在于处理不好多轮对话和复杂需求,适合边界清晰、描述明确的任务。如果需要处理很多的任务,那么 Function Calling 的代码比较难维护。

AI Agent

  • AI Agent 是一个智能系统,它可以自主运行以实现特定目标。传统的 AI 聊天仅提供建议或者需要手动执行任务,AI Agent 则可以分析具体情况,做出决策,并自行采取行动。
  • AI Agent 可以利用 MCP 提供的功能描述来理解更多的上下文,并在各种平台/服务自动执行任务。
    在这里插入图片描述

Model Context Protocol (MCP)

  • MCP 是一个标准协议,如同电子设备的 Type C 协议(可以充电也可以传输数据),使 AI 模型能够与不同的 API 和数据源无缝交互。
  • MCP 旨在替换碎片化的 Agent 代码集成,从而使 AI 系统更可靠,更有效。通过建立通用标准,服务商可以基于协议来推出它们自己服务的 AI 能力,从而支持开发者更快的构建更强大的 AI 应用。开发者也不需要重复造轮子,通过开源项目可以建立强大的 AI Agent 生态。
  • MCP 可以在不同的应用/服务之间保持上下文,从而增强整体自主执行任务的能力。
    可以理解为 MCP 是将不同任务进行分层处理,每一层都提供特定的能力、描述和限制。而 MCP Client 端根据不同的任务判断,选择是否需要调用某个能力,然后通过每层的输入和输出,构建一个可以处理复杂、多步对话和统一上下文的 Agent。

如何搭建一个MCP?

在这里插入图片描述

config.ts
const config = [
{
    name: "github-server",
    type: "command",
    command: "node ~/Desktop/test/MCP/github-server.js",
    isOpen: true,
  },
  {
    name: "demo-stdio",
    type: "command",
    command: "npx -y @executeautomation/playwright-mcp-server",
    isOpen: true,
  },
  {
    name: "chrome-stdio",
    type: "command",
    command: "node ~/Desktop/test/MCP/mcp-server.js",
    isOpen: true,
  },
  {
    name: "demo-sse",
    type: "sse",
    command: "node ~/code-open/cursor-toolkits/mcp/build/demo-stdios.js",
    url: "http://localhost:3000/demo-sse",
    isOpen: false,
  },
];
export default config;

client.ts
// 1.读取配置文件,运行所有Server,获取可用的Tools
// 2.用户与LLM对话(附带所有Tools名称描述,参数定义)
// 3.LLM识别到要执行某个Tool,返回名称和参数
// 4.找到对应Server的Tool,调用执行,返回结果
// 5.把工具执行结果提交给LLM
// 6.LLM返回分析结果给用户
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
  StdioClientTransport,
  StdioServerParameters,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import OpenAI from "openai";
// import { Tool } from "@modelcontextprotocol/SharedWorker.types.js";
import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
import { createInterface } from "readline";
import { homedir } from "os";
import config from './config.ts';
console.log(123)
//初始化环境变量
const baseAI = {
  api_key: "sk-790d3c8f37a84f68aee857eb9116ae6c",
  base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
}
// const OPENAI_API_KEY = process.env.OPENAI_API_KEY || config.OPENAI_API_KEY;
const OPENAI_API_KEY = "123"
if (!OPENAI_API_KEY) {
  throw new Error("OPENAI_API_KEY environment variable is required")
}
interface MCPToolResult {
  content: string;
}
interface ServerConfig {
  name: string;
  type: 'command' | 'sse';
  command?: string;
  url?: string;
  isOpen?: boolean
}
class MCPClient {
  static getOpenServers(): string[] {
    return config.filter((cfg: ServerConfig) => cfg.isOpen).map((cfg: ServerConfig) => cfg.name);
  }
  private sessions: Map<string, Client> = new Map();
  private transports: Map<string, StdioClientTransport | SSEClientTransport> = new Map()
  private openai: OpenAI;
  constructor() {
    this.openai = new OpenAI({
      apiKey: OPENAI_API_KEY
    })
  }
  //服务器连接
  async connectToServer(serverName: string): Promise<void> {
    const serverConfig = config.find((cfg: ServerConfig) => cfg.name === serverName) as ServerConfig
    if (!serverConfig) {
      throw new Error(`Server configuration not found for :${serverName}`);
    }
    let transport: StdioClientTransport | SSEClientTransport;
    if (serverConfig.type === 'command' && serverConfig.command) {
      transport = await this.createCommandTransport(serverConfig.command)
    } else if (serverConfig.type === 'sse' && serverConfig.url) {
      transport = await this.createSSETransport(serverConfig.url)
    } else {
      throw new Error(`Invalid server configuration for:${serverName}`)
    }
    //创建一个客户端
    const client = new Client({
      name: "mcp-client",
      version: "1.0.0"
    }, {
      capabilities: {
        prompts: {},
        resources: {},
        tools: {}
      }
    })
    //客户端连接服务器

    await client.connect(transport);
    //缓存所有客户端和服务器连接
    this.sessions.set(serverName, client)
    this.transports.set(serverName, transport);
    //列出可用工具
    const response = await client.listTools();
    console.log(`\nConnected to server ${serverName} with tools:${response.tools.map(tool => tool.name)}`)
  }
  private async createCommandTransport(shell: string): Promise<StdioClientTransport> {
    const [command, ...shellArgs] = shell.split(' ');
    if (!command) {
      throw new Error("Invalid shell command");
    }
    //处理参数中的波浪号路径
    const args = shellArgs.map(arg => {
      if (arg.startsWith('~/')) {
        return arg.replace('~', homedir());
      }
      //转成绝对路径
      return arg
    })
    const serverParams: StdioServerParameters = {
      command,
      args,
      env: Object.fromEntries(Object.entries(process.env).filter(([_, v]) => v !== undefined)) as Record<string, string>
    }
    console.log(serverParams)
    //按照mcpAPI启动服务器
    return new StdioClientTransport(serverParams)
  }
  private async createSSETransport(url: string): Promise<SSEClientTransport> {
    return new SSEClientTransport(new URL(url));
  }
  async processQuery(query: string): Promise<string> {
    if (this.sessions.size === 0) {
      throw new Error("Not connected to any server");
    }
    const messages: ChatCompletionMessageParam[] = [
      { role: "user", content: query }
    ];
    //获取所有服务器的工具列表
    const availableTools: any[] = [];
    for (const [serverName, session] of this.sessions) {
      const response = await session.listTools();
      const tools = response.tools.map((tool: any) => ({
        type: "function" as const,
        function: {
          name: `${serverName}_${tool.name}`,
          description: `[${serverName}] ${tool.description}`,
          parameters: tool.inputSchema
        }
      }))
      availableTools.push(...tools)
    }

    //调用OpenAI API
    const completion = await this.openai.chat.completions.create({
      model: "gpt-4-turbo-preview",
      messages,
      tools: availableTools,
      tool_choice: "auto"
    })
    const finalText: string[] = []
    //处理OpenAI的响应
    for (const choice of completion.choices) {
      const message = choice.message;
      if (message.content) {
        finalText.push(message.content);
      }
      if (message.tool_calls) {
        for (const toolCall of message.tool_calls) {
          const [serverName, toolName] = toolCall.function.name.split('_')
          const session = this.sessions.get(serverName);
          if (!session) {
            finalText.push(`[Error:Server ${serverName} not found]`);
            continue;
          }
          const toolsArgs = JSON.parse(toolCall.function.arguments)
          //执行工具调用
          const result = await session.callTool({
            name: toolName,
            arguments: toolsArgs
          })
          const toolResult = result as unknown as MCPToolResult;
          finalText.push(`[Calling tool ${toolName} on server ${serverName} with args ${JSON.stringify(toolsArgs)}]`)
          console.log(toolResult.content);
          finalText.push(toolResult.content);
          //继续与工具结果的圣诞
          messages.push({
            role: "assistant",
            content: "",
            tool_calls: [toolCall]
          })
          messages.push({
            role: "tool",
            tool_call_id: toolCall.id,
            content: toolResult.content
          })
          //获取下一个响应
          const nextCompletion = await this.openai.chat.completions.create({
            model: "gpt-4-turbo-preview",
            messages,
            tools: availableTools,
            tool_choice: "auto"
          })
          if (nextCompletion.choices[0].message.content) {
            finalText.push(nextCompletion.choices[0].message.content)
          }
        }
      }
    }
    return finalText.join('\n')
  }
  async chatLoop(): Promise<void> {
    console.log("\nMCP Client Started!")
    console.log("Type your queries or 'quit' to exit.");
    const readline = createInterface({
      input: process.stdin,
      output: process.stdout
    })
    const askQuestion = () => {
      return new Promise<string>((resolve) => {
        readline.question("\nQuery:", resolve);
      })
    };
    try {
      while (true) {
        const query = ((await askQuestion()).trim())
        if (query.toLowerCase() === 'quit') {
          break;
        }
        try {
          const response = await this.processQuery(query);
          console.log("\nError:", response);
        } catch (error) {
          console.error("\nError:", error)
        }
      }
    } finally {
      readline.close()
    }
  }
  async cleanup(): Promise<void> {
    for (const transport of this.transports.values()) {
      await transport.close();
    }
    this.transports.clear()
  }
  hasActiveSessions(): boolean {
    return this.sessions.size > 0
  }
}
async function main() {
  console.log(123)
  //获取所有可用的MCP服务器
  const openServers = MCPClient.getOpenServers();
  console.log("Connecting to servers:", openServers.join(","));
  const client = new MCPClient();
  try {
    //连接所有开启的服务器
    for (const serverName of openServers) {
      try {
        //一个MCP的Client连接一个MCP的Server
        await client.connectToServer(serverName);
      } catch (error) {
        console.error(`Failed to connect to server ${serverName}`)
      }
    }
    if (!client.hasActiveSessions()) {
      throw new Error("Failed to connect to any server")
    }
    //进入聊天循环
    await client.chatLoop()
  } finally {
    await client.cleanup()
  }
}
main().catch(console.error)
github-server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Octokit } from "@octokit/rest";

let githubToken: string | null = null;
let octokit: Octokit | null = null;

const server = new Server(
  {
    name: "github-mapper-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "set-github-token",//设置GitHub个人访问令牌
        description: "Set the GitHub Personal Access Token for authentication",
        inputSchema: {
          type: "object",
          properties: {
            token: {
              type: "string",
              description: "GitHub Personal Access Token",
            },
          },
          required: ["token"],
        },
      },
      {
        name: "map-github-repo",//分析github仓库结构并给出总结
        description: "Map a GitHub repository structure and provide summary information",
        inputSchema: {
          type: "object",
          properties: {
            repoUrl: {
              type: "string",
              description: "URL of the GitHub repository (e.g., https://github.com/username/repo)",
            },
          },
          required: ["repoUrl"],
        },
      },
    ],
  };
});

// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "set-github-token") {
    const { token } = args as { token: string };
    githubToken = token;
    octokit = new Octokit({ auth: githubToken });
    return {
      content: [
        {
          type: "text",
          text: "GitHub Personal Access Token has been set successfully.",
        },
      ],
    };
  } else if (name === "map-github-repo") {
    if (!githubToken || !octokit) {
      throw new Error("GitHub token not set. Please use the set-github-token tool first.");
    }

    const { repoUrl } = args as { repoUrl: string };

    try {
      const { owner, repo } = parseGitHubUrl(repoUrl);
      const repoInfo = await getRepoInfo(owner, repo);
      const repoStructure = await getRepoStructure(owner, repo);
      const formattedOutput = formatOutput(repoInfo, repoStructure);

      return {
        content: [
          {
            type: "text",
            text: formattedOutput,
          },
        ],
      };
    } catch (error: unknown) {
      console.error("Error mapping repository:", error);
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
      return {
        content: [
          {
            type: "text",
            text: `Error mapping repository: ${errorMessage}`,
          },
        ],
      };
    }
  } else {
    throw new Error(`Unknown tool: ${name}`);
  }
});

function parseGitHubUrl(url: string): { owner: string; repo: string } {
  const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/);
  if (!match) {
    throw new Error("Invalid GitHub URL format");
  }
  return { owner: match[1], repo: match[2] };
}

async function getRepoInfo(owner: string, repo: string) {
  if (!octokit) {
    throw new Error("GitHub client not initialized");
  }
  const { data } = await octokit.repos.get({ owner, repo });
  return {
    name: data.name,
    description: data.description,
    stars: data.stargazers_count,
    forks: data.forks_count,
    language: data.language,
    createdAt: data.created_at,
    updatedAt: data.updated_at,
  };
}

async function getRepoStructure(owner: string, repo: string, path = "") {
  if (!octokit) {
    throw new Error("GitHub client not initialized");
  }
  const { data } = await octokit.repos.getContent({ owner, repo, path });

  if (!Array.isArray(data)) {
    throw new Error("Unable to retrieve repository structure");
  }

  const structure: { [key: string]: any } = {};

  for (const item of data) {
    if (item.type === "file") {
      structure[item.name] = null;
    } else if (item.type === "dir") {
      structure[item.name] = await getRepoStructure(owner, repo, item.path);
    }
  }

  return structure;
}

function formatOutput(repoInfo: any, repoStructure: any): string {
  const structureString = JSON.stringify(repoStructure, null, 2);

  return `
Repository Analysis Summary:

Name: ${repoInfo.name}
Description: ${repoInfo.description || "No description provided"}
Stars: ${repoInfo.stars}
Forks: ${repoInfo.forks}
Primary Language: ${repoInfo.language}
Created: ${new Date(repoInfo.createdAt).toLocaleDateString()}
Last Updated: ${new Date(repoInfo.updatedAt).toLocaleDateString()}

Repository Structure:

${structureString}
  `.trim();
}

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  //服务器连接MCP通道
  await server.connect(transport);
  console.error("GitHub Mapper MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值