[FastMCP设计、原理与应用-01] Hello, MCP

张开发
2026/4/11 10:32:28 15 分钟阅读

分享文章

[FastMCP设计、原理与应用-01] Hello, MCP
我觉得目前Agent技术圈存在一个很不好的风气很多人为了博眼球总喜欢抛出一下“语不惊人死不休”的言论动不动就说什么“XXX已死XXX才是唯一的未来”。既然LLM采用深度神经网络来实现意味着它永远就只是一个黑盒基于LLM的理论具有一个确定的特性那就是不确定性。对某种AI技术如此“笃定”不是因为他们了解这项范围是因为“一知半解”。这两个月来我们已经看到太多人把MCP判了死刑有些人的理由是MCP将所有的工具绑定给LLM选择过多反而让LLM“无从选择”同时让上下文极具膨胀导致推理质量下降。MCP是AI界的“USB-C接口”它是一种标准化协议让不同的大模型能够通过统一的方式轻松连接并调用各种外部数据源和工具。MCP这个协议可没有规定我们应该这样将MCP提供的所有工具一股脑倾倒进上下文来使用工具连一点暗示都没有。不仅没有很多MCP框架都提供了扩展来支持“渐进式披露”比如FastMCP提供扩展支持基于自然语言的工具筛选还支持基于当前上下文的工具可见性控制在工具执行过程中针对客户端的Elicitation也是“渐进式披露”的一种手段。这应该是一个很长的系列我希望通过这一一系列文章尽量涵盖MCP协议的方方面面。但单纯从协议层面介绍会很枯燥的必需结合一种MCP框架鉴于使用的广泛性和框架自身设计的优劣FastMCP无疑是最好的选择。我希望这个系列的文章能给读者带来如下的收获全面的了解MCP规范其实MCP不仅仅用于提供工具、资源和提示词并在此基础上提供工具调用、资源读取和提示词渲染的能力其实它还支持后台任务的调度从消息交换模式来将MCP不仅仅支持从客户端到服务端的请求/响应模式还支持从服务端向客户端的反向请求和通知通过后者我们可以实时推送进度报告、日志和组件变化还可以动态客户端的输入Elicitation甚至调用客户端的LLM生成文本LLM Sampling熟练使用FastMCP服务器和客户端SDK作为一个最受欢迎的MCP框架如果你采用MCP解决方案你大概率会选择它所以这一系列得文章会涵盖FastMCP包括搭建服务器和使用客户端大部分编程模式并且提供充足的演示实例结合官方提供的开发手册希望带你“从入门到精通”深刻理解FastMCP的架构设计和实现原理“了解框架的设计才能更好地使用框架”这是我一致秉承的理念。从架构设计来审视FastMCP可谓可圈可点它采用MiddlewareProviderTransform是整个框架极具扩展性很多问题即使默认的编程模型不能解决我们也可以利用它提供的扩展来解决。MCP不仅不会死其MCP规范还会不断迭代下去FastMCP这样设计可以保证其主体架构保持不变通过自定义Middleware、Provider或者Transform来实现新特性和新功能。作为一个计算器从业者我们已经习惯了从“Hello World”开始去学习一门技术所以我们也采用这惯例构建一个极简的“HelloMCP”程序让大家对MCP协议以及FastMCP的编程模式有一个大致的了解。1. 构建MCP服务器在通过pip install fastmcp[tasks]安装了fastmcptasks这个特性提供后台任务调度我们会在后面频繁使用到后我们利用如下这段代码构建一个MCP服务器。fromfastmcpimportFastMCP mcpFastMCP(Greeting)mcp.tool()asyncdefgreet(name:str)-str:Get a greeting message for the given namereturnfHi,{name}!mcp.resource(greeting://everyone)asyncdefgreet_everyone()-str:Get a greeting message for everyonereturnfHey, everyone!mcp.resource(greeting://{name})asyncdefgreet_to_name(name:str)-str:Get a greeting message for the given namereturnfHello,{name}!mcp.prompt()asyncdefgreet_prompt(name:str)-str:Get a greeting message for the given namereturnfHi,{name}!mcp.run(transporthttp,host0.0.0.0,port3721,stateless_httpTrue)在根据名称Greeting创建了代表MCP服务器的FastMCP对象之后我们利用该对象的三种装饰器方法注册了四种典型的组件greet利用mcp.tool装饰器注册的工具greet_everyone: 利用mcp.resource装饰器注册的静态文本资源表示绑定于固定URI的单个资源greet_to_name利用mcp.resource装饰器注册的动态资源模板表示绑定于URI模板的一组资源greet_prompt利用mcp.prompt装饰器注册的提示词模板然后我们调用FastMCP的run方法启动服务器。为了方便后面的演示我们设置了如下四个参数transport将传输协议设置为HTTP那么我们就可以单纯地发送HTTP请求的方式与MCP服务交互了host: 设置成0.0.0.0那么我们可以利用localhost、127.0.0.1以及绑定的主机名和真实IP地址访问MCP服务器port显式指定的端口号默认为8000stateless_http客户端默认采用Session保存客户端与服务器会话上下文Session ID会在初始握手阶段生成并要求后续请求携带此ID。我们利用此参数将其设置成无状态的服务器就是为了手工发送请求的时候可以不用指定Session ID我们构建的MCP服务器本质上就是一个单纯的Web服务器这意味着我们可以利用Fiddler、Postman和curl这种工具向监听URL发送HTTP请求的方式与MCP服务器进行交互。MCP采用JSON-RPC协议除了会话开始和结束的握手请求采用的HTTP方法都是POST。接下来我们就以HTTP请求-响应对的形式来演示读取工具列表和调用某个工具读取资源列表和指定某个资源的内容读取提示词模板列表和渲染提示词FastMCP把利用指定的参数填充提示词模板的占位符生成完整提示词文本的操作称为Render2. 获取工具列表和调用工具我们可以发送如下的POST请求读取MCP服务器提供的工具列表由于采用JSON-RPC协议所以请求的Content-Type和Accept报头指定为application/json由于MCP服务器采用SSE (Server-Sent Events) 协议向客户端推送通知所以Accept报头还需加上text/event-stream。按照JSON-RPC的约定不包含id字段的请求将被视为一个单向通知并且会得到不含主体内容的202 Accept响应所以我们会为每个请求随机指定一个数字作为id。method字段可是视为MCP的操作名称tools/list表示读取工具列表的操作。请求POST http://127.0.0.1:3721/mcp HTTP/1.1 Content-Type: application/json Accept: application/json,text/event-stream Host: 127.0.0.1:3721 Content-Length: 63 { jsonrpc: 2.0, id: 1, method: tools/list }上述请求对应的响应内容如下可以看到Content-Type报头的值正是text/event-stream如果请求的Accept报头不显式包含此MIME类型将会收到一个406 Not Acceptable响应。返回的内容是对一个事件Event的描述包含事件的名称message和事件携带的数据data后者承载的内容正是工具列表。从响应的server报头还可以看出我们的MCP服务是底层是通过Uvicorn构建的。响应HTTP/1.1 200 OK date: Sat, 28 Mar 2026 12:36:01 GMT server: uvicorn cache-control: no-cache, no-transform connection: keep-alive content-type: text/event-stream x-accel-buffering: no Content-Length: 425 event: message data: { jsonrpc: 2.0, id: 1, result: { tools: [ { name: greet, description: Get a greeting message for the given name, inputSchema: { additionalProperties: false, properties: { name: { type: string } }, required: [ name ], type: object }, outputSchema: { properties: { result: { type: string } }, required: [ result ], type: object, x-fastmcp-wrap-result: true }, _meta: { fastmcp: { tags: [] } } } ] } }响应事件的数据(data)是一个JSON包含我们唯一定义的工具工具通过如下的字段来描述name默认为工具函数的名称greetdescription默认将工具函数的docstring作为描述inputSchema解析函数的输入参数的注解并生成代表输入的SchemaoutputSchema解析函数返回类型的注解生成代表输出的Schema由于获取的工具列表中包含对工具的基本描述所以我们可以根据这些数据直接发起对某个工具的调用。如下这个就是调用greet工具发送的请求以下的演示中忽略均请求的报头部分对于响应我们也只给出事件携带的JSON数据。我们使用工具调用专属的方法tools/call并指定name参数MCP。请求{jsonrpc:2.0,id:1,method:tools/call,params:{name:greet,arguments:{name:MCP}}}响应:{jsonrpc:2.0,id:1,result:{_meta:{fastmcp:{wrap_result:true}},content:[{type:text,text:Hi, MCP!}],structuredContent:{result:Hi, MCP!},isError:false}}工具执行的结果被封装在响应JSON的result字段中,具体包含如下的字段_meta元数据。由于MCP的响应只能是一个结构完整的JSON所谓greet工具返回的字符串需要被包装称一个JSON。这个元数据“wrap_result”: true旨在说明这一点coententMCP协议定义的多媒体内容这里表示基于文本类型“type”: “text”的内容“text”: “Hi, MCP!”structuredContent根据输出Schema生成的结构化输出。对于像greet函数返回的基元类型比如str、int和float等都会返回具有这种JSON结构isError表示工具调用的是否发生异常3. 读取资源MCP中的资源分两种固定URI标识的静态资源和采用URI模板定义的动态资源根据请求URI动态决定返回的资源内容。我们可以利用如下的请求将method设置为resources/list读取静态资源列表。{jsonrpc:2.0,id:1,method:resources/list}响应{jsonrpc:2.0,id:1,result:{resources:[{name:greet_everyone,uri:greeting://everyone,description:Get a greeting message for everyone,mimeType:text/plain,_meta:{fastmcp:{tags:[]}}}]}}result/resources节点返回静态资源列表每个资源通过如下的字段来描述name资源名称默认采用资源函数名称greet_everyoneuri由mcp.resource装饰器函数的第一个参数uri指定description默认来源于资源函数的docstringmimeType资源内容的MIME类型text/plain源于资源函数返回类型的注解str_meta元数据。FASTMCP的元数据会一并包含进来这里没有指定如果希望提取资源模板的列表可以按照如下的方式将method设置为resources/templates/list。这样就会在响应的资源列表中得到我们定义的名为greet_to_name的资源了。{jsonrpc:2.0,id:3,method:resources/templates/list}响应:{jsonrpc:2.0,id:3,result:{resourceTemplates:[{name:greet_to_name,uriTemplate:greeting://{name},description:Get a greeting message for the given name,mimeType:text/plain,_meta:{fastmcp:{tags:[]}}}]}}除了uri字段被替换成uriTemplate之外描述资源模板和资源的数据结构基本相同。虽然资源具有两种定义方式但是其内容的读取方式都一样将method设置为resources/read并提供唯一标识资源的URI。如下这个请求用于读取greet_everyone函数定义的静态资源。{jsonrpc:2.0,id:1,method:resources/read,params:{uri:greeting://everyone}}响应{jsonrpc:2.0,id:1,result:{contents:[{uri:greeting://everyone,mimeType:text/plain,text:Hey, everyone!}]}}如下这个请求则用于读取greet_to_name函数定义的动态资源模板参数name被设置为MCP。读取到的资源不仅仅包含其承载内容还包含资源的URI和MIME类型。{jsonrpc:2.0,id:1,method:resources/read,params:{uri:greeting://MCP}}响应{jsonrpc:2.0,id:1,result:{contents:[{uri:greeting://MCP,mimeType:text/plain,text:Hello, MCP!}]}}4. 读取提示词按照前面的规律我们自然会想到用于读取提示词(模板)列表的JSON-RPC的方法为prompts/list所以如下这个请求可以读取到我们在MCP服务器中提供的提示词。{jsonrpc:2.0,id:1,method:prompts/list}响应{jsonrpc:2.0,id:1,result:{prompts:[{name:greet_prompt,description:Get a greeting message for the given name,arguments:[{name:name,required:true}],_meta:{fastmcp:{tags:[]}}}]}}提示词模板的描述包括name默认采用提示词函数名称greet_promptdescription默认来源提示词函数的docstringarguments解析提示词函数的参数注解得到。参数是否必需required取决于对应参数是否定义了默认值_meta: 元数据在确定提示词名称之后客户端利用指定的参数对提示词模板进行填充将其格式化成一个完整的提示词文本FastMCP将个过程称为提示词的“渲染Render”。此操作对应的JSON-RPC方法为prompts/get,并利用name和arguments提供提示词模板名称和填充的参数。如下这个请求可以得到greet_prompt这个提示词MCP作为填充的参数name的值。{jsonrpc:2.0,id:1,method:prompts/get,params:{name:greet_prompt,arguments:{name:MCP}}}响应{jsonrpc:2.0,id:1,result:{description:Get a greeting message for the given name,messages:[{role:user,content:{type:text,text:Hi, MCP!}}]}}

更多文章