Host、Client、Server 与首次握手
协议连接不等于模型调用
Host 是承载用户与模型的应用,Client 为一个 Server 管理协议会话,Server 提供工具、资源与提示。一个 Host 可持有多个 Client。工具调用可以完全不经过模型:本章客户端直接发 tools/list 与 tools/call,证明协议与模型决策是分离的。
本书主实验固定 mcp==1.12.4(Python SDK v1)。main 分支或 v2 文档可能使用 MCPServer 和不同返回签名,升级时不要逐行混抄。Python 3.12 空目录执行:
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install 'mcp==1.12.4'
python -m pip freeze > requirements.lock保存 server.py:
from mcp.server.fastmcp import FastMCP
mcp=FastMCP('book-inventory')
@mcp.tool()
def stock(sku: str) -> str:
"""读取图书库存,不修改数据。"""
inventory={'A':3,'B':0}
return str(inventory[sku]) if sku in inventory else 'not found'
if __name__=='__main__':mcp.run(transport='stdio')保存 client.py,与 server.py 同目录运行 python client.py,它会启动并关闭服务进程:
import asyncio,sys
from pathlib import Path
from mcp import ClientSession,StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params=StdioServerParameters(command=sys.executable,
args=[str(Path(__file__).with_name('server.py'))])
async with stdio_client(params) as (read,write):
async with ClientSession(read,write) as session:
init=await session.initialize()
print('protocol',init.protocolVersion)
tools=await session.list_tools()
assert [x.name for x in tools.tools]==['stock']
result=await session.call_tool('stock',{'sku':'B'})
assert not result.isError
assert result.content[0].text=='0'
print('stock B',result.content[0].text)
asyncio.run(main())stdio 的 stdout 属于协议,调试日志写 stderr。向 stdout 打印“server ready”可能破坏 JSON-RPC 帧解析。进程启动不是会话就绪;initialize 交换版本与能力,客户端随后按协商结果使用功能。
JSON-RPC request 有 id 并期待 response;notification 没有 id,不期待 response。关联 id 不是工具幂等键。连接关闭后新会话不能假定旧的待处理请求仍有效。
工具失败和协议失败
方法不存在、请求结构错误等属于协议层错误;某工具找到但业务执行失败可以体现在 CallToolResult 的 isError。客户端必须检查这两层,不能仅因拿到 content 就认定成功。readOnlyHint 等注解帮助展示与策略判断,但不是服务端授权证明。
参阅 Python SDK v1.12.4 与 MCP 生命周期。
练习
在 client 中分别调用 A、B、X,验证 3、0、not found;不要用字符串是否为真来判断是否有库存。
先记录自己的输出与解释,再核对本章答案。