再谈MCP协议-工具

本次我们再一次聊聊MCP中的工具,这是结合之前我们说了MCP中的协议层之后,再一次查看MCP中的工具实现。

在实际讲述之前,我们需要先说下计算1.0和计算2.0这两个模型发展时代的区别,在之前计算1.0的时代,往往输出结果是或与非,也即0或者1的输出方式,而计算2.0时代,则两者运算逻辑高度相似,结合概率计算思维,实现了机器和人的自然交互。

在2.0时代,交互方式发生了变化,但并非是万能的,大模型可以进行计算,进行思考,但由于自身限制,并不能直接执行任务,而为了让大模型可以执行任务,因此发展出了LLM+工具调用体系。

具体的就是MCP的协议实现,其让服务器可以自己通过统一的协议调用工具。最终让大模型和外部系统进行交互,比如查询数据库,调用API。

更加具体的就是服务器端声明了自己支持tools调用

{

“capabilities”: {

“tools”: {

“listChanged”: true

}

}

}

而在一个工具内部,包含着name,description,inputSchema,annotations这些元数据字段,表明着工具用于做什么的,下面就是一个示例

{

“name”: “get_weather”,

“description”: “Get current weather information for a location”,

“inputSchema”: {

“type”: “object”,

“properties”: {

“location”: {

“type”: “string”,

“description”: “City name or zip code”

}

},

“required”: [“location”]

}

}

而对应到更加细粒度的代码层面来说,交互流程如下

首先获取到工具列表

{

“jsonrpc”: “2.0”,

“id”: 1,

“method”: “tools/list”,

“params”: {

“cursor”: “optional-cursor-value”

}

}

服务器端的响应如下

{

“jsonrpc”: “2.0”,

“id”: 1,

“result”: {

“tools”: [

{

“name”: “get_weather”,

“description”: “Get current weather information for a location”,

“inputSchema”: {

“type”: “object”,

“properties”: {

“location”: {

“type”: “string”,

“description”: “City name or zip code”

}

},

“required”: [“location”]

}

}

],

“nextCursor”: “next-page-cursor”

}

}

获取到了数组形式的tool的metadata

之后是客户端进行工具的调用,发起tools/call请求

{

“jsonrpc”: “2.0”,

“id”: 2,

“method”: “tools/call”,

“params”: {

“name”: “get_weather”,

“arguments”: {

“location”: “New York”

}

}

}

将要求的参数传入给服务器端,并期待得到响应。

{

“jsonrpc”: “2.0”,

“id”: 2,

“result”: {

“content”: [

{

“type”: “text”,

“text”: “Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy”

}

],

“isError”: false

}

}

在其中,需要注意的是返回的信息有不同的类型

诸如文本内容

{

“type”: “text”,

“text”: “Tool result text”

}

或者图片内容

{

“type”: “image”,

“data”: “base64-encoded-data”,

“mimeType”: “image/png”

}

音频内容

{

“type”: “audio”,

“data”: “base64-encoded-audio-data”,

“mimeType”: “audio/wav”

}

以及其他的内嵌资源。

而且MCP 还有着工具调用的错误机制。

一个是协议错误,常见的适用于 无效参数和服务器错误这类情况。

{

“jsonrpc”: “2.0”,

“id”: 3,

“error”: {

“code”: -32602,

“message”: “Unknown tool: invalid_tool_name”

}

}

一个是工具执行错误,常见的包含 无效数据输入,业务逻辑错误。

{

“jsonrpc”: “2.0”,

“id”: 4,

“result”: {

“content”: [

{

“type”: “text”,

“text”: “Failed to fetch weather data: API rate limit exceeded”

}

],

“isError”: true

}

}

对于工具的调用,在设计的时候,一般都需要注意一点,客户端需要在设计的时候考虑,如果交互涉及了敏感操作,需要在服务器端项用户展示工具输入,或者提供验证机制,交由客户来进行实际的调用。

在这里,我们就以一个简单的示例来展示工具的实战,代码中我们分为服务器端和客户端两者来展示实现。并且分别展示通过高纬度FastMCP和低纬度原生MCP Server两种方式来展示代码。

首先是高纬度,也就是通过FastMCP框架实现的方式。

其支持通过装饰器和注解的方式来暴露相关Tool供客户端调用。

比如@mcp.tool

import asyncio

from mcp.server.fastmcp import FastMCP

# 初始化 FastMCP 服务器

mcp = FastMCP(“tools-server”)

@mcp.tool()

async def calculator(operation: str, a: float, b: float) -> str:

“””执行基本的数学运算

Args:

operation: 运算类型 (add, subtract, multiply, divide)

a: 第一个数字

b: 第二个数字

“””

if operation == “add”:

return f”计算结果: {a + b}”

elif operation == “subtract”:

return f”计算结果: {a – b}”

elif operation == “multiply”:

return f”计算结果: {a * b}”

elif operation == “divide”:

if b == 0:

return “错误:除数不能为零”

return f”计算结果: {a / b}”

@mcp.tool()

async def text_analyzer(text: str) -> str:

“””分析文本,统计字符数和单词数

Args:

text: 要分析的文本

“””

char_count = len(text)

word_count = len(text.split())

return f”字符数: {char_count}\n单词数: {word_count}”

if __name__ == “__main__”:

mcp.run(transport=”stdio”)

如果直接使用MCP的Server实现,则是如下代码

import asyncio

import mcp.types as types

from mcp.server import Server

from mcp.server.stdio import stdio_server

app = Server(“tools-server”)

@app.list_tools()

async def list_tools() -> list[types.Tool]:

“””

返回服务器提供的工具列表

“””

return [

types.Tool(

name=”calculator”,

description=”执行基本的数学运算(加、减、乘、除)”,

inputSchema={

“type”: “object”,

“properties”: {

“operation”: {

“type”: “string”,

“enum”: [“add”, “subtract”, “multiply”, “divide”]

},

“a”: {“type”: “number”},

“b”: {“type”: “number”}

},

“required”: [“operation”, “a”, “b”]

}

),

types.Tool(

name=”text_analyzer”,

description=”分析文本,统计字符数和单词数”,

inputSchema={

“type”: “object”,

“properties”: {

“text”: {“type”: “string”}

},

“required”: [“text”]

}

)

]

@app.call_tool()

async def call_tool(

name: str,

arguments: dict

) -> list[types.TextContent]:

“””

处理工具调用请求

“””

if name == “calculator”:

operation = arguments[“operation”]

a = arguments[“a”]

b = arguments[“b”]

if operation == “add”:

result = a + b

elif operation == “subtract”:

result = a – b

elif operation == “multiply”:

result = a * b

elif operation == “divide”:

if b == 0:

return [types.TextContent(type=”text”, text=”错误:除数不能为零”)]

result = a / b

return [types.TextContent(type=”text”, text=f”计算结果: {result}”)]

elif name == “text_analyzer”:

text = arguments[“text”]

char_count = len(text)

word_count = len(text.split())

return [types.TextContent(

type=”text”,

text=f”字符数: {char_count}\n单词数: {word_count}”

)]

return [types.TextContent(type=”text”, text=f”未知工具: {name}”)]

async def main():

async with stdio_server() as streams:

await app.run(

streams[0],

streams[1],

app.create_initialization_options()

)

if __name__ == “__main__”:

asyncio.run(main())

也提供了对应的装饰器,但是使用起来的粒度更为细,适合有更多客制化需求的场景。

之后是相关的client端的实现。

def call_llm_with_tools(messages, tools):

response = client.chat.completions.create(

model=”deepseek-chat”,

messages=messages,

tools=tools,

tool_choice=”auto”

)

return response.choices[0].message

async with stdio_client(params) as (reader, writer):

async with ClientSession(reader, writer) as session:

await session.initialize()

notification = Notification(

method=”notifications/initialized”,

params={}

)

await session.send_notification(notification)

# 获取工具列表

response = await session.list_tools()

tools = response.tools

print(“欢迎使用工具调用系统!\n可用工具列表已加载。\n请输入您的需求(输入’退出’结束):”)

# 构造 tools 列表

tools_list = [{

“type”: “function”,

“function”: {

“name”: tool.name,

“description”: tool.description,

“parameters”: tool.inputSchema

}

} for tool in tools]

# 交互主循环

messages = [

{“role”: “system”, “content”: “你是一个智能助手,请根据用户输入选择合适的工具并构造参数,或直接回复用户。”}

]

while True:

user_input = input(“> “)

if user_input.lower() == “退出”:

break

messages.append({“role”: “user”, “content”: user_input})

message = call_llm_with_tools(messages, tools_list)

messages.append(message)

if not message.tool_calls:

print(message.content)

continue

# 工具调用

for tool_call in message.tool_calls:

args = json.loads(tool_call.function.arguments)

result = await session.call_tool(tool_call.function.name, args)

messages.append({

“role”: “tool”,

“content”: str(result),

“tool_call_id”: tool_call.id

})

# 再次让 LLM 总结最终回复(这时 messages 里有 “答案” 的内容了)

message = call_llm_with_tools(messages, tools_list)

print(message.content)

messages.append(message)

通过session获取到tools,然后tools列表和message列表交给Deepseek,剩下的LLM自动完成。

在交互过程中,如果返回的信息要求进行工具调用,则交给session进行相关的工具调用。

在不断你的交互过程中,直到得到最后的答案,那么这就是在MCP中Tool的具体实现。

这里我们总结一下,工具调用原语的关键流程。

MCP协议把每一次列工具和调工具都转换为了标准的JSON-RPC请求

客户端读取 list_tools 返回的工具定义,以及call_tool的返回结果。

并且利用了统一的封装交互方式,避免了原本的Function Calling需要深入细节了解相关工具具体实现的问题。