什么是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);
});