最近MCP风头正盛,技术圈都在讨论这个玩意儿。笔者所在的项目组也跟风想要搞MCP
,作为资深牛马,这个任务自然而然又落到自己身上。关于什么是MCP
,以及如何构建一个MCP Server
已经有很多文章写了,本文就重点聊一聊MCP Client
。
MCP Client

先看官方文档的MCP协议架构图,MCP Client
通过 MCP Protocol
实现跟MCP Server
的通信。好的,那这个MCP Client
实现了跟MCP Server
的通信后会做些什么呢?请接着往下看
1. 跟MCP Server建立连接
根据最新的官方typescript-sdk仓库信息,目前Client可以使用四种通信方法建立跟Server的连接。
sse
方式,需要node.js >= 18,因为使用了原生的fetch
API;stdio
方式,让Server作为一个本地进程运行在MCP Client所在的Host里,然后里进程间I/O方法通信;websocket
方式,通过websocket协议建立双向通信http
方式,还未正式发布(截止到2025.4.26)
上面几种方式各有优势,开发者可以自行决定使用哪种方式(当然,也跟Server端的支持紧密相关,如果Server只支持其中一种,那你也只能乖乖配合了)。
2. 获取tools列表
tools是什么?
Enable LLMs to perform actions through your server
Tools are a powerful primitive in the Model Context Protocol (MCP) that enable servers to expose executable functionality to clients. Through tools, LLMs can interact with external systems, perform computations, and take actions in the real world.
Form Google 翻译:
tools使 LLM 能够通过服务器执行操作
tools是MCP中一个强大的原语,它使服务器能够向客户端公开可执行功能。通过工具,LLM 可以与外部系统交互、执行计算并在现实世界中采取行动。
准确来讲,这句话有一定的误导性,单从字面意思上看很容易让人以为大模型会"亲自"使用tool。其实不是这样的,所以这句话更准确的理解是LLM
可以通过Server
知道外界有哪些tools
,然后对于一个复杂task,LLM
可以告诉你使用哪些tools
来解决这个问题。LLM不会自己下场去调用tools!!!
3. 携带tools信息跟LLM通信
为什么要携带tools
信息跟LLM通信?
大哥,你不携带tools
信息LLM
怎么知道你手里有哪些工具,不知道有哪些工具LLM
又怎么知道你使用工具呢?
做个简单的比喻,LLM
是一个大脑,脖子以下都没有(让我想起《黑侠II》里被大反派只保留头部的摩洛克博士)。所以ta只能告诉你下一步的动作,自己是无法直接去执行这个动作的。
所以,调用tools
的任务只能 MCP Client
来执行。
4. 调用tools
MCP Client在得到LLM的指引后,仍然会通过跟MCP Serve的连接去调用tool。没错,这里又回到了 Client 跟 Server 的交互。
5. 让LLM整理tools结果
这一步是可选的,一些操作类的工具没有返回结果,也就不需要LLM进行二次加工。一些结果类、文书类的结果可以扔给LLM,让LLM二次加工,返回最终整理的内容。
顺着上面的思路,整理得到 MCP Client 跟LLM和MCP Server调用时序图:
实践
上面给出MCP Client跟MCP Serve和LLM交互的主要流程,接下来详细看看两个实际的产品是如何实现 MCP Client的能力的。
Cline
cline是一个开源 AI 编码助手,具有双重计划/行动模式、终端执行和用于 VS Code 的模型上下文协议 (MCP)。在 VS Code 里安装了cline 插件后,就可以看到cline的聊天界面:
这里不细究cline的具体功能,而是直接去看ta的源码,重点关注ta是如何集成MCP的。
去github上clonecline的源码,可以很快找到 McpHub.ts这个文件,没错,这个文件就实现了 MCP Client
的所有能力:

上面类图列举了 McpHub所有的方法,需要重点关注的就这三个方法:connectToServer
,fetchToolsList
,callTool
。
connectToServer
这个方法主要作用是建立起跟 MCP Server
的通信链路
typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
private async connectToServer(
name: string,
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
): Promise<void> {
// ...
const client = new Client(
{
name: "Cline",
version: this.clientVersion,
},
{
capabilities: {},
},
)
let transport: StdioClientTransport | SSEClientTransport
if (config.transportType === "sse") {
transport = new SSEClientTransport(new URL(config.url), {})
} else {
transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: {
...config.env,
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
},
stderr: "pipe", // necessary for stderr to be available
})
}
}
// ...
核心逻辑是上面这段,根据配置决定是使用sse
方法还是stdio
方式跟MCP Server
进行通信。
fetchToolsList
该方法是在建立跟Server端的通信后,去获取当前MCP Server支持的tools列表:
typescript
private async fetchToolsList(serverName: string): Promise<McpTool[]> {
try {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
const response = await connection.client.request({ method: "tools/list" }, ListToolsResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
})
// Get autoApprove settings
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
const autoApproveConfig = config.mcpServers[serverName]?.autoApprove || []
// Mark tools as always allowed based on settings
const tools = (response?.tools || []).map((tool) => ({
...tool,
autoApprove: autoApproveConfig.includes(tool.name),
}))
// console.log(`[MCP] Fetched tools for ${serverName}:`, tools)
return tools
} catch (error) {
// console.error(`Failed to fetch tools for ${serverName}:`, error)
return []
}
}
callTool
callTool方法是最关键的一个方法,通过该方法调用具体的tool,完成某一项功能:
ts
async callTool(serverName: string, toolName: string, toolArguments?: Record<string, unknown>): Promise<McpToolCallResponse> {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
)
}
if (connection.server.disabled) {
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
}
let timeout = secondsToMs(DEFAULT_MCP_TIMEOUT_SECONDS) // sdk expects ms
try {
const config = JSON.parse(connection.server.config)
const parsedConfig = ServerConfigSchema.parse(config)
timeout = secondsToMs(parsedConfig.timeout)
} catch (error) {
console.error(`Failed to parse timeout configuration for server ${serverName}: ${error}`)
}
return await connection.client.request(
{
method: "tools/call",
params: {
name: toolName,
arguments: toolArguments,
},
},
CallToolResultSchema,
{
timeout,
},
)
}
基本上来讲,所有的 MCP Client都有类似的三个方法来建立连接、获取tools列表、调用tool。可能实现有稍许差别,但基本的方法结构跟上面的方法是一致的。
如何使用
那么最关键的一个问题:Cline如何使用具体的tool呢?
答案是:Prompts!!! Cline的驱动大模型完成任务的核心便是一系列内置的Prompts
,系统 Prompts 主入口是这个文件 system.ts,从中可以看到这段提示词:
markdown
====
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
${
mcpHub.getServers().length > 0
? `${mcpHub
.getServers()
.filter((server) => server.status === "connected")
.map((server) => {
const tools = server.tools
?.map((tool) => {
const schemaStr = tool.inputSchema
? ` Input Schema:
${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`
: ""
return `- ${tool.name}: ${tool.description}\n${schemaStr}`
})
.join("\n\n")
const templates = server.resourceTemplates
?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
.join("\n")
const resources = server.resources
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
.join("\n")
const config = JSON.parse(server.config)
return (
`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
(tools ? `\n\n### Available Tools\n${tools}` : "") +
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
(resources ? `\n\n### Direct Resources\n${resources}` : "")
)
})
.join("\n\n")}`
: "(No MCP servers currently connected)"
}
====
是的,就是这么简单粗暴,Cline通过提示词将当前MCP Server
提供的tools
丢给LLM,让LLM自动决策使用哪些tools,并且约定当需要执行MCP tools时,通过 use_mcp_tool
指令返回具体的工具信息,然后调用tools并将结果重新添加到对话上下文:
ts
// now execute the tool
await this.say("mcp_server_request_started") // same as browser_action_result
const toolResult = await this.mcpHub.callTool(server_name, tool_name, parsedArguments)
// TODO: add progress indicator and ability to parse images and non-text responses
const toolResultPretty =
(toolResult?.isError ? "Error:\n" : "") +
toolResult?.content
.map((item) => {
if (item.type === "text") {
return item.text
}
if (item.type === "resource") {
const { blob, ...rest } = item.resource
return JSON.stringify(rest, null, 2)
}
return ""
})
.filter(Boolean)
.join("\n\n") || "(No response)"
await this.say("mcp_server_response", toolResultPretty)
pushToolResult(formatResponse.toolResult(toolResultPretty))
CherryStudio
Cherry Studio AI 是一款支持多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。可以快速切换多个先进的 LLM 模型,提升LLM使用效率。
Cherry Studio
也支持MCP调用,那么ta是如何做到的呢?
这里需要做一个版本划分,因为通过笔者的分析,其使用 MCP Server的方式可以按版本分成两个阶段,在 v1.2.2及之前使用的是 Function Calling,在 v1.2.2之后则切换为了 Prompts!
版本 <= v1.2.2
依旧是看源码,跟Cline类似,Cherry Studio
把对MCP所有的操作方法集中到了 MCPService里。
ts
class McpService {
// ...
constructor() {
this.initClient = this.initClient.bind(this)
this.listTools = this.listTools.bind(this)
this.callTool = this.callTool.bind(this)
this.closeClient = this.closeClient.bind(this)
this.removeServer = this.removeServer.bind(this)
this.restartServer = this.restartServer.bind(this)
this.stopServer = this.stopServer.bind(this)
this.cleanup = this.cleanup.bind(this)
}
// ...
}
具体的方法实现大家有兴趣可以去github
上看源码,本文不再列举(其实跟Cline
的实现很类似,也是 建立通信连接-获取tools列表-调用tool 这三步)。
那如何告知LLM有哪些tools可以调用以让其进行决策呢?
答案是:Function Calling!!!
这里省去中间过程,直接看Cherry Studio
是如何把 mcpTools
转换为 Function Calling
的:
ts
// 适用于Anthropic大模型的mcpTools转换方法
export function mcpToolsToAnthropicTools(mcpTools: MCPTool[]): Array<ToolUnion> {
return mcpTools.map((tool) => {
const t: Tool = {
name: tool.id,
description: tool.description,
// @ts-ignore no check
input_schema: tool.inputSchema
}
return t
})
}
// 适用于Gemini大模型的mcpTools转换方法
export function geminiFunctionCallToMcpTool(
mcpTools: MCPTool[] | undefined,
fcall: FunctionCall | undefined
): MCPTool | undefined {
if (!fcall) return undefined
if (!mcpTools) return undefined
const tool = mcpTools.find((tool) => tool.id === fcall.name)
if (!tool) {
return undefined
}
// @ts-ignore schema is not a valid property
tool.inputSchema = fcall.args
return tool
}
// 适用于OpenAI大模型的mcpTools转换方法
export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletionTool> {
return mcpTools.map((tool) => ({
type: 'function',
name: tool.name,
function: {
name: tool.id,
description: tool.description,
parameters: {
type: 'object',
properties: filterPropertieAttributes(tool)
}
}
}))
}
上面的三个转换方法会把 tools 通过Function Calling让大模型感知到,然后进行决策调用。请注意,上面的大模型不是一个,而是一类。
PS: 国内流程的 DeepSeek,Qwen,Doubao 都属于OpenAI类大模型
所以之前有言论称MCP的出现会替代 Function Calling,完全是胡扯,这两者本来就不是一个维度的东西,根本没有谁替代谁这一说。
版本 > v1.2.2
截止到 2025-4-27
,Cherry Studio
最新版本已经来到 v1.2.9
。除了能力上的升级,其对 MCP 的支持兼容度也有较大的提升。其中一个重点改进便是用 Prompts 代替 Function Calling 实现对 MCP tools的调用。
ts
public async completions({
messages,
assistant,
mcpTools,
onChunk,
onFilterMessages
}: CompletionsParams): Promise<void> {
// ...
let systemInstruction = assistant.prompt
if (mcpTools && mcpTools.length > 0) {
systemInstruction = buildSystemPrompt(assistant.prompt || '', mcpTools)
}
// ...
}
/**
* 核心的组装 Prompt 方法
*/
export const buildSystemPrompt = (userSystemPrompt: string, tools: MCPTool[]): string => {
if (tools && tools.length > 0) {
return SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt)
.replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples)
.replace('{{ AVAILABLE_TOOLS }}', AvailableTools(tools))
}
return userSystemPrompt
}
export const AvailableTools = (tools: MCPTool[]) => {
const availableTools = tools
.map((tool) => {
return `
<tool>
<name>${tool.id}</name>
<description>${tool.description}</description>
<arguments>
${tool.inputSchema ? JSON.stringify(tool.inputSchema) : ''}
</arguments>
</tool>
`
})
.join('\n')
return `<tools>
${availableTools}
</tools>`
}
之前针对不同类LLM分别转换的问题不存在了,统统收敛到 buildSystemPrompt
方法,将 mcpTools
的调用方法跟规则注入到 Prompts
里传递给 LLM ,让其决策是否调用,以及调用哪些 tools。 以上就是关于Cline
跟Cherry Studio
如何实现 MCP 的完整分析。
总结
- 在MCP协议出来后,尤其是通用AI Agent的兴起,各类AI客户端工具都开始支持 MCP,将其客户端打造成一个
MCP Client
实现对更广泛工具的调用能力; LLM
在MCP协议中充当大脑角色,不会"亲自"去调用mcpTools
,这些脏活累活都是在MCP Client
完成的;MCP Client
充当MCP Server
跟LLM
之间的翻译者,将MCP Server
提供的tools翻译成LLM可以识别的内容,让其决策。MCP Client
翻译的方式有两种,一种是"翻译"成Function Calling
,第二种是"翻译"成格式化的Prompts
;- 从目前来看,"翻译"成
Function Calling
的方式存在一定局限性(部分大模型不支持Function Calling
或对Function Calling
的支持度不是很好)。通过Cherry Studio
的实现演进可以发现"翻译"成格式化的Prompts
似乎是个更好的选择; - MCP的优势在于:开放标准利于服务商开发API,避免开发者重复造轮子,可利用现有MCP服务增强Agent。所以还是那句话------Open Source Forever!