Python-MCP-学习指南-全-
Python MCP 学习指南(全)
原文:Learn Model Context Protocol with Python
译者:飞龙
前言
模型上下文协议(MCP)代表了一种革命性的方法,用于构建能够高效分配资源、标准化能力和促进复杂系统中不同组件之间无缝通信的人工智能(AI)应用。随着人工智能继续发展和融入我们数字生活的各个方面,标准化、可扩展和安全的协议需求变得越来越关键。
MCP 解决了开发者在构建人工智能应用时面临的根本挑战:资源分配瓶颈、不同组件之间缺乏标准化、构建和测试分布式系统的复杂性,以及开发能够有效与服务器和大型语言模型(LLMs)交互的客户端的复杂过程。通过提供一个结构化框架,MCP 使开发者能够创建更高效、可维护和可扩展的人工智能应用。
在所有可用于人工智能应用开发的协议和框架中,MCP 脱颖而出,因为它提供了几个关键优势:
-
它提供了一种标准化的方式来描述和在不同系统组件之间进行能力沟通
-
它使得在多个服务器之间高效地分配资源成为可能,从而提高性能和可扩展性
-
它为构建、测试和部署服务器和客户端提供了全面的指南
-
它支持多种通信方法,包括标准输入/输出(STDIO)和服务器发送事件(SSE)
-
它促进了与现代开发工具和平台的集成
在这本综合性的书中,我们将首先探索 MCP 的基础概念,了解其架构、组件以及它在现代人工智能应用开发中解决的问题。
一旦您理解了核心协议概念,我们将深入到实际实施中,涵盖如何使用各种方法构建和测试 MCP 服务器,包括基于 STDIO 和 SSE 的服务器。我们还将探索高级服务器开发技术和模式,这些将帮助您创建健壮、生产就绪的应用。
随后,本书将进入客户端开发,向您展示如何构建有效的客户端,无论是带有还是不带大型语言模型(LLM)集成,以及如何使用 Claude 桌面和虚拟工作室代码(VS Code)代理模式等流行工具来消费 MCP 服务器。我们还将涵盖采样和诱导技术等高级主题,这些技术可以增强您的 AI 应用。
最后,我们将解决关键的生产问题,包括安全最佳实践、部署策略和扩展考虑因素,这些都是运行 MCP 应用在现实世界环境中必不可少的。
本书将引导你通过众多实际示例和练习,展示构建 MCP 应用程序的最佳实践,并提供在真实场景中的实际操作经验。示例和代码示例旨在立即应用于你的项目,无论你是构建简单的概念验证还是复杂的企业应用程序。
本书贯穿始终,包括全面的解决方案和 Python 代码示例。附录还提供了针对需要提高 Python 技能的读者的全面 Python 入门指南。
作者承认使用了尖端的人工智能技术,在本例中为 GitHub Copilot,其唯一目的是提高本书的语言和清晰度,从而确保读者有顺畅的阅读体验。重要的是要注意,内容本身是由作者创作的,并由专业出版团队编辑。
本书面向的对象
本书面向开发者、人工智能工程师和软件架构师,他们希望使用 MCP 构建复杂的人工智能应用程序。无论你是希望创建高效人工智能服务器的后端开发者,还是希望将人工智能功能集成到应用程序中的前端开发者,或者是一位探索人工智能系统新结构的 AI 研究者,本书都提供了你需要的知识和实际技能。
推荐具备 Python 基础编程经验,并熟悉网络开发概念、API 设计以及基本人工智能/机器学习概念。熟悉现代开发工具和实践将有助于你充分利用本书。
本书涵盖的内容
第一章,模型上下文协议简介,介绍了模型上下文协议、其历史背景以及它在人工智能应用开发中解决的 fundamental problems。你将了解为什么 MCP 对现代人工智能系统至关重要。
第二章,解释模型上下文协议,对 MCP 协议本身进行了全面的深入探讨,涵盖了其架构、关键组件(主机、客户端、服务器)、通信方法(STDIO 和 SSE)以及标准能力框架。
第三章,构建和测试服务器,专注于使用 STDIO 通信的实际服务器开发,涵盖了服务器架构、资源和工具实现,以及使用检查工具的综合测试策略。
第四章,构建 SSE 服务器,探讨了基于 SSE 的服务器开发,展示了如何构建用于更动态应用程序的实时、流式 MCP 服务器。
第五章,可流式 HTTP,涵盖了针对 MCP 服务器的先进 HTTP 流式传输技术,使你能够构建高度可扩展和高效的服务器实现。
第六章,高级服务器,深入探讨了复杂的服务器模式、高级资源管理、复杂工具实现和现成服务器架构。
第七章,构建客户端,教您如何开发 MCP 客户端,包括独立应用程序和与 LLMs 集成的应用程序,涵盖客户端架构和最佳实践。
第八章,使用服务器,演示了如何通过各种客户端和工具,包括 Claude 桌面版、VS Code 代理模式以及自定义客户端实现,有效地使用 MCP 服务器。
第九章,采样,探讨了 AI 应用的先进采样技术,展示了如何利用 MCP 的能力进行复杂的 AI 交互和内容生成。
第十章,信息提取,涵盖了在 AI 应用中进行有效信息提取的技术,展示了如何设计能够智能收集和处理信息的系统。
第十一章,确保您的应用程序安全,讨论了 MCP 应用程序的综合安全考虑,包括身份验证、授权、数据保护和安全通信模式。
第十二章,将 MCP 应用投入生产,提供了将 MCP 应用程序部署到生产环境的实用指南,包括扩展策略、监控和维护最佳实践。
附录,使用现代 Python 构建 Web 应用,提供了关于适用于 MCP 开发的 Python 高级特性的全面入门指南,包括异步编程、上下文管理器、类型提示和现代 Python 模式。
要充分利用本书
要跟随本书中的示例和练习,您需要以下内容:
-
在您的系统上安装 Python 3.8 或更高版本
-
代码编辑器或 IDE(推荐使用 VS Code)
-
熟悉命令行界面
-
理解 HTTP、JSON 和基本网络概念
-
访问现代 AI 工具,如 Claude 或 ChatGPT,以测试示例
所有代码示例都设计在 Windows、macOS 和 Linux 上运行。本书包括针对每个主要平台的特定安装说明和设置指南。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python。如果代码有更新,它将在 GitHub 仓库中更新。我们还提供了全面的示例、练习题的解决方案和额外的资源,以帮助您掌握 MCP 开发。
我们还提供其他代码包,这些代码包来自我们丰富的图书和视频目录,可在 github.com/PacktPublishing/ 获取。查看它们吧!
下载彩色图像
我们还提供包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/gbp/9781806103232。
使用的约定
本书中使用了多种文本约定。
Code In Text:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和协议名称。例如:“语法是 uvicorn <filename>:<name of app instance>。在这种情况下,文件名是 main.py,应用程序实例是 app。”
代码块应如下设置:
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def homepage(request):
return JSONResponse({'hello': 'world'})
app = Starlette(debug=True, routes=[
Route('/', homepage),
])
任何命令行输入或输出都应如下编写:
npx @modelcontextprotocol/inspector --cli http:localhost:8000/sse --method tools/list
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“注意传输类型设置为 SSE,URL 设置为 http://localhost:8000/sse。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问或有任何一般性反馈,请通过电子邮件发送至 customercare@packt.com,并在邮件主题中提及本书的标题。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问 www.packt.com/submit-errata,点击 提交勘误 并填写表格。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感激您能提供位置地址或网站名称。请通过电子邮件发送至 copyright@packt.com 并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问 authors.packt.com/。
分享您的想法
一旦您阅读了《使用 Python 学习模型上下文协议》,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
第一章:模型上下文协议简介
生成式 AI 迅速成为当今技术景观中的一股力量,重塑行业并重新定义我们解决问题的方法。从自然语言处理到图像生成,生成式 AI 在各个领域的集成为创新和效率开辟了新的可能性。
对于我们开发者来说,将生成式 AI 集成到应用程序开发工作流程中并非没有复杂性。我们必须仔细评估模型准确性、伦理考虑和计算效率等因素。
在构建应用程序的过程中,我们需要考虑如何标准化我们构建 AI 应用程序的方式。标准化意味着一切看起来都一样,这意味着跨不同团队和工具的集成和协作应该更加容易。
正是模型上下文协议(MCP)在这里发挥作用,以标准化我们确保 AI 应用程序能够轻松从工具、内容和提示中找到所需内容的方式;更多内容将在稍后介绍。
本章涵盖了以下主题:
-
我们是如何从 SOAP 到 REST 再到 GraphQL 到 gRPC 到 MCP 的
-
标准化的需求
-
无尽的可能性:了解如何提示,MCP 服务器世界尽在掌握
-
MCP 是什么?
充分利用本书 – 了解您的免费福利
解锁购买时附带的所有免费福利,这些福利经过精心设计,旨在加速您的学习之旅,并帮助您无限制地学习。
这本书为您提供了以下快速概述:
下一代阅读器
|
图 1.1:下一代 Packt 阅读器功能的说明 | 我们基于网络的阅读器,旨在帮助您有效地学习,具有以下功能:
多设备进度同步:在任何设备上学习,无缝同步进度。
高亮和笔记:将您的阅读转化为持久的知识。
书签:随时回顾您最重要的学习成果。
深色模式:切换到深色或棕褐色模式,以最小化眼睛疲劳。 |
| --- | --- |
互动式 AI 助手(测试版)
|
图 1.2:Packt 的 AI 助手的说明 | 我们互动的 AI 助手已经接受了本书内容的训练,以最大化您的学习体验。它具有以下功能:
总结:总结关键部分或整章内容。
AI 代码解释器:在下一代 Packt 阅读器中,点击每个代码块上方的解释按钮,获取 AI 驱动的代码解释。注意:AI 助手是下一代 Packt 阅读器的一部分,目前仍处于测试阶段。 |
| --- | --- |
无 DRM 的 PDF 或 ePub 版本
|
图 1.3:免费的 PDF 和 ePub | 购买后,以下优惠将包含在内,无限制地学习:
使用这本书的无 DRM PDF 副本在任何地方学习。
使用您喜欢的电子阅读器,通过这本书的无 DRM ePub 版本学习。 |
| --- | --- |
|
现在解锁这本书的独家优惠
扫描此二维码或访问packtpub.com/unlock,然后按书名搜索。确保是正确的版本。
|
| 注意:在开始之前,请准备好您的购买发票。* |
| --- |
我们是如何从 SOAP 到 REST 到 GraphQL 到 gRPC 再到 MCP 的
在我们深入探讨 MCP 的细节之前,让我们退一步看看我们是如何到达这里的。
我早期使用 Web 请求的记忆之一是使用 XML 发送和接收数据。那是在简单对象访问协议(SOAP)的时代,这是一种在实现 Web 服务中交换结构化信息的协议。它很棒,但也非常复杂,感觉很重。
然后出现了表示状态转移(REST),这是一种构建 Web 服务的更简单方法。它使用了 HTTP 和 JSON,这使得它更容易处理。REST 曾经是,现在仍然是伟大的。
REST 本身并没有什么固有的错误,但你可以争论,如果你有一个后端团队和一个前端团队,前端团队通常会等待后端团队完成工作后才能开始构建前端。这就是GraphQL出现的地方,它允许你查询你需要的仅有的数据,并使与 API 一起工作变得更加容易。当然,这也会产生其他问题,如过度获取和不足获取数据以及所谓的N+1 问题。N+1 问题是在 GraphQL API 中常见的性能问题,其中会发出多个请求来获取相关数据,导致效率低下和延迟增加。
还有Google 远程过程调用(gRPC),这是一个高性能的 RPC 框架,它使用 HTTP/2 和 Protocol Buffers。gRPC 非常适合微服务,允许你以更结构化的方式定义你的 API,但设置和使用它可能很复杂。
需要一个标准
所有这些格式本身都很出色,但它们都有自己的问题。此外,问题通常不是这种性质,而是一系列我们在构建应用程序时需要问自己的问题:
-
这个应用/API 做什么?我们如何轻松地以易于理解和使用的方式展示我们应用程序的功能?当然,到目前为止,还没有人真正就这一点达成一致。
-
如果提示是新的交互方式,我们如何构建应用程序?再加上用户越来越习惯于使用提示与应用程序交互,你开始思考生成 AI 的部分是什么,应用程序本身的能力又是什么?
-
大型语言模型(LLM)和其他能力应该保持独立吗?还有,我真的需要将生成式 AI 部分和应用程序的能力都放在同一个地方吗?
-
如果它们保持独立,我们又能得到什么?如果我们能够在客户端和服务器部分将这两者分开,那么我们可能可以轻松地消费其他人构建的服务器——“你好,能力时代”。
这些是一些值得自问的好问题。但这并没有回答我们为什么需要一个标准。让我们进一步探讨这个问题:
-
我们作为开发者,编程能力太强了:这里的问题是:作为开发者,我们几乎太擅长编程了,这意味着我们已经习惯了将不同的事物粘合在一起。我们可以构建使用多个 AI 模型的应用程序,我们也可以让使用不同协议和格式的应用程序相互通信。这并不总是容易,但我们能做到。
-
我们可以做到,但代价是什么?如前所述,仅仅因为我们几乎可以将任何事物粘合在一起,并不意味着我们应该这样做。是的,我们可以将任何事物包装成 REST API,并使其与其他任何事物通信。但这需要多少时间和精力?
-
解决方案,一个标准:现在,你看到了对标准的需要,希望如此。好消息是,有一个正在开发中的标准来解决这个问题。它被称为 MCP。这使你不仅能够以标准化的方式描述你的资源和能力,还能够描述如何与之交互。
这意味着你实际上可以将 MCP 放在任何应用程序之上,突然之间,任何能够使用 MCP 进行通信的客户端都可以与之交互。想象以下场景:你有一个客户端,这个客户端可以与运行在本地和远程的多个 MCP 服务器通信。所有这一切都是因为你将这些服务器列在了mcp.json文件中。
突然之间,你有了访问任何你能想象到的工具的能力,从数据库到云服务提供商到任何其他公开 MCP 服务器的服务。你几乎不费吹灰之力就变得更有能力了。想象一下这些可能性!
让我们讨论一下 MCP 为我们开启的一些可能性。
无尽的可能性:掌握如何提示,MCP 服务器世界就是你的珍珠
无尽是一个很大的词,那么我们的意思是什么?想象一下:你可能今天没有这些技能。在 MCP 服务器存在的世界里,这不再是问题,因为有了 MCP 包装的 API,一个代理将允许你提示以获取你需要完成的事情。
以 3D 模型的设计和管理为例。Blender是创建 3D 模型的常用工具。要使用它,你需要 3D 建模技能。因此,你将花费数小时学习如何使用这个应用程序,我相信这是一项值得拥有的技能。
由于 Blender 的 MCP 服务器,对 3D 建模的知识需求不再那么大。你可以通过提示来表述你想完成的事情。这就像电影《黑客帝国》,其中主角尼奥在信息直接上传到大脑后说,“我会功夫”。未来已经到来。如果你知道如何使用提示,你就是尼奥。
这是 Blender MCP 服务器和公开的功能链接:github.com/ahujasid/blender-mcp。
任何客户端现在,只要拥有自己的 LLM 和 MCP,就能调用任何 MCP 服务器,因为 Blender 并不是唯一的例子;其他主要公司也在转向 MCP。以下是一些例子:
-
GitHub MCP
-
Playwright MCP
-
Google Maps
要获取 MCP 服务器的列表,请查看以下链接:github.com/modelcontextprotocol/servers。
现在有更多服务器,每天都有新的服务器加入,所以卷起袖子开始构建你自己的 MCP 服务器,同时也要利用现有的资源!
你可能正在想我正在想的事情:我们都将拥有自己的贾维斯,即电影《钢铁侠》中的 AI 助手,能够做任何事情。我们需要的只是利用现有的 MCP 服务器和构建缺失的部分;只需使用 MCP。
想象一下拥有一个能够帮助你处理任何需要的个人助理,从安排约会到管理你的财务。有了 MCP,这现在成为可能。
未来正在敲你的门,声音清晰响亮。你准备好了吗?
MCP 是什么?
这是官方 MCP 网站对它的说法:
模型上下文协议(MCP)是一个开放的协议,旨在标准化应用程序如何向大型语言模型(LLMs)提供上下文。把它想象成 AI 应用程序的 USB-C 端口,为连接 AI 模型到各种数据源和工具提供了一种标准化的方式。
这对应用程序开发者意味着什么?
这意味着我们构建应用程序的方式正在改变,变得更加标准化。通过学习和使用 MCP,所有你的应用程序都将能够以标准化的方式相互通信和共享数据。这意味着你将花费更少的时间担心如何连接或粘合你的应用程序到其他应用程序,而可以有更多的时间构建重要的功能。
我们将在下一章中更深入地探讨这些概念的具体细节,但就目前而言,我们以宏观的角度理解了 MCP 是什么以及它是如何工作的,以及涉及的主要组件。
摘要和下一步:现在怎么办?
我们理解了问题、解决方案和可能性,这些可能性是无限的,我们甚至了解 MCP 的一些核心概念。但在我们开始构建自己的 MCP 服务器之前,我们可能需要对其了解更多。我们将在下一章中探讨这一点。
|
现在解锁这本书的独家优惠
扫描此二维码或访问packtpub.com/unlock,然后通过书名搜索此书。 | 
|
| 注意:在开始之前准备好您的购买发票。* |
| --- |
第二章:解释模型上下文协议
MCP 由许多部分组成。简而言之,JavaScript 对象表示法-远程过程调用(JSON-RPC)消息在客户端和服务器之间交换。JSON-RPC 消息遵循 JSON-RPC 规范的消息,这意味着它有jsonrpc、id、method和params字段,数据类型是 JSON。一个例子可能如下所示:
{
"jsonrpc": "2.0",
"id": 1,
"method": "doSomething",
"params": {
"foo": "bar"
}
}
快速提示:使用AI 代码解释器和快速复制功能增强您的编码体验。在下一代 Packt Reader 中打开此书。点击复制按钮
(1)快速将代码复制到您的编码环境中,或点击解释按钮
(2)让 AI 助手为您解释一段代码。

新一代 Packt Reader随本书免费赠送。扫描二维码或访问packtpub.com/unlock,然后使用搜索栏通过书名查找本书。仔细核对显示的版本,以确保您获得正确的版本。

为了更容易理解并更有趣,本章尝试通过带您了解协议的实现来解释该协议。因此,我们希望本章更容易阅读(不仅仅是架构图)并一窥事物是如何工作的。如果您想立即开始构建 MCP 服务器,请直接跳转到第三章,但如果您想更深入地了解 MCP 协议,请继续阅读。您也可以稍后返回本章。
在本章中,您将学习以下内容:
-
MCP 中最常见的消息流及其消息类型
-
基础 SDK 实现的大致工作原理
本章涵盖了以下主题:
-
通过实现来了解协议
-
MCP 中的传输
通过实现来了解协议
我们不打算将本章写成关于协议及其不同消息的枯燥章节,而是通过实际讨论实现过程中的过程和消息来使其变得有趣。作为解释过程及其流程的一部分,您将看到流程图以及作为代码实现的流程。让我们开始吧。
MCP 中的传输
MCP 中传输的理念是定义客户端和服务器如何通信。MCP 是传输无关的,这意味着它可以通过 HTTP、WebSockets、STDIO 等方式工作。传输是处理底层消息交换的层。它交换类型为 JSON-RPC 的消息。
MCP 支持一系列传输,从 STDIO(用于本地运行的服务器)到可流式传输的 WebSockets 和服务器发送事件(SSEs),最后是请求/响应传输,如 HTTP。
对于指定的每个传输,它们都有一个共同点,即它们都在流上操作。所有传输都有一个方法来暴露这些流,如下所示:
async with anyio.create_task_group() as tg
...
yield (read_stream, write_stream)
此外,还有一个名为 BaseSession 的类,所有传输都使用它来发送原始 JSON-RPC 消息,其格式如下。
class BaseSession(
Generic[
SendRequestT,
SendNotificationT,
SendResultT,
ReceiveRequestT,
ReceiveNotificationT,
],
):
...
def send_request():
...
def send_notification():
...
def response():
...
def _send_response():
...
这个类定义了 send_request、send_notification 和 response 等方法,这些方法用于发送 JSON-RPC 消息。
STDIO 传输
好吧,让我们开始了解和实现 MCP 中的消息流。我们将使用 STDIO 传输作为这个练习的例子。让我们开始吧!
我相信你对在控制台看到消息甚至键入控制台很熟悉——那就是 标准输入和输出,或简称 STDIO。但我们是如何利用这些 流 来为程序服务的呢?好吧,让我们从服务器和客户端的角度来思考。客户端会向服务器发送消息,服务器会做出响应。让我们看看 STDIO 的一个非常简单的服务器实现:
服务器需要监听 标准输入(stdin)以接收传入的消息。在 Python 中,你可以使用 sys.stdin 并迭代它以获取下一个消息,如下所示:
import sys
for line in sys.stdin:
message = line.strip()
我们还希望区分简单文本消息和 JSON-RPC 消息。简单文本消息可以直接处理,而 JSON-RPC 消息则需要根据 JSON-RPC 规范进行解析并做出响应。让我们通过以下代码来识别 JSON-RPC 消息的结构:
if line.startswith('{"jsonrpc":'):
json_message = json.loads(line)
# do something with message
此外,我们还需要向调用客户端发送消息。我们可以使用 print 和 sys.stdout.flush() 来实现,如下所示:
print("message")
sys.stdout.flush()
现在我们已经确定了要实现的关键元素,让我们创建第一个服务器:
import sys
import json
while True:
for line in sys.stdin:
message = line.strip()
if message == "hello":
# send message to client
print("hello there")
sys.stdout.flush() # Ensure output is sent immediately
elif message.startswith('{"jsonrpc":'):
# parse it as a JSON message
json_message = json.loads(message)
# identify what type of JSON-RPC message it is and
# respond accordingly
match json_message['method']:
case "tools/list":
response = {
"jsonrpc": "2.0",
"id": json_message["id"],
"result": ["tool1", "tool2"]
}
print(json.dumps(response))
sys.stdout.flush()
break
case _:
print(f"Unknown method: {json_message['method']}")
sys.stdout.flush()
break
elif message == "exit":
print("Exiting server.")
sys.stdout.flush()
sys.exit(0)
else:
print(f"Unknown message: {message}")
在前面的代码中,我们做了以下操作:
-
创建了监听
sys.stdin的代码 -
如果发送的是
hello,则响应hello there;如果给定 JSON 消息,则解析它并检查其method属性,然后根据其值做出不同的响应 -
添加了代码,如果输入文本为
exit,则使程序关闭
注意我们如何打印和刷新代码如下:
print(json.dumps(response))
sys.stdout.flush()
这可以重写为一个 send_response 函数,如下所示:
def send_response(response):
print(json.dumps(response))
sys.stdout.flush()
这意味着我们的服务器代码现在将看起来像这样:
#server.py
import sys
import json
def send_response(response):
print(json.dumps(response))
sys.stdout.flush()
while True:
for line in sys.stdin:
message = line.strip()
if message == "hello":
send_response("hello there")
elif message.startswith('{"jsonrpc":'):
json_message = json.loads(message)
match json_message['method']:
case "tools/list":
response = {
"jsonrpc": "2.0",
"id": json_message["id"],
"result": ["tool1", "tool2"]
}
send_response(response)
break
case _:
send_response(f"Unknown method:
{json_message['method']}")
break
elif message == "exit":
send_response("Exiting server.")
sys.exit(0)
else:
send_response(f"Unknown message: {message}")
创建客户端
那么,我们如何为这个创建一个客户端呢?好吧,客户端应该发送消息并能够接收服务器消息。实现这一点的办法是将服务器作为一个子进程创建,客户端作为父进程向服务器发送消息。然后,我们可以通过标准输入和输出与之通信。
下面是一个实现示例:
作为客户端向服务器发送消息的方式是通过写入其 stdin 并从其 stdout 读取。以下是一个简单的示例:
proc.stdin.write(message)
proc.stdin.flush()
这段代码可以被重构为一个 send_message 函数,如下所示:
def send_message(proc, message):
proc.stdin.write(message)
proc.stdin.flush()
当我们创建客户端时,让我们在客户端代码中使用 send_message 函数。
#client.py
import subprocess
import json
# Start the child process
proc = subprocess.Popen(
['python3', 'server.py'], # Replace with your child script
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True
)
list_tools_message = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
};
message = 'hello\n'
def send_message(message):
"""Send a message to the child process."""
print(f'[CLIENT] Sending message to server...
Message: {message.strip()}')
proc.stdin.write(message)
proc.stdin.flush()
def serialize_message(message):
"""Serialize a message to JSON format."""
return json.dumps(message) + '\n'
# Send a message to the child
send_message(message)
# Read response from child
response = proc.stdout.readline()
print('[SERVER]:', response.strip())
# send a JSON-RPC message
send_message(serialize_message(list_tools_message))
response = proc.stdout.readline()
print('[SERVER]:', response.strip())
# this closes down the child process aka server
send_message('exit\n')
proc.stdin.close()
exit_code = proc.wait()
print(f"Child exited with code {exit_code}")
在这里,你可以看到以下情况发生:
-
客户端创建一个服务器作为子进程,并通过使用
send_message方法写入stdin来向这些进程写入出站消息 -
相反,它监听
stdout以接收子进程(服务器)通过proc.stdout.readline()代码发送的响应
这段代码是构建 MCP 和 STDIO 传输的绝佳起点。实际上,这正是发生的事情,只不过 MCP 是通过 JSON-RPC 消息进行通信的,所以让我们让它更像 MCP。
MCP 和 STDIO 传输
我们在上一节中的代码基本上就像 STDIO 传输对 MCP 所做的那样。然而,为了完全真实,客户端和服务器需要交换 JSON-RPC 消息。那么,这些是什么?
让我们看看一个示例jsonrpc消息:
const listTools = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
};
在这里,我们有一个 JSON-RPC 消息,它之所以是一个,首先是因为它是以 JSON 格式编写的,其次是因为它有一个名为jsonrpc的属性。此外,它应该有一个id属性、method和params。前面的tools/list消息是客户端发送给服务器的内容,服务器应该响应其可用的工具,因为这个命令用于确定服务器有什么工具。
那么,构建 MCP 服务器时的第一步是什么?嗯,它是初始化过程,也称为握手,所以让我们接下来处理它。
在解决方案文件夹中查看此代码的运行示例——检查github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter02/Solutions/README.md。
MCP 中的初始化过程
接下来,现在我们有了简单的客户端和服务器代码,我们应该专注于 MCP 中的初始化过程,有时也称为握手。
在这个高度上,初始化看起来是这样的:

图 2.1 – 初始化过程
在前面的过程中发生的事情是这样的:
-
首先,客户端发送一个initialize请求,这意味着它想从服务器了解其能力。
-
然后,服务器响应其能力——即它拥有的功能。
-
最后,客户端通过发送一个initialized通知来让服务器知道它已准备好执行操作。在提到的通知之前,任何其他类型的消息,如列出或运行工具,都应该产生一个错误响应,因为握手尚未完全完成。
让我们看看每一步的消息:
-
客户端发送 initialize 请求:这是客户端发送给服务器的:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } }
这是一个initialize消息,你可以从method的值initialize中看到。客户端还必须发送其capabilities,在这个例子中包括roots和sampling。让我们更仔细地看看客户端的能力:
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
}
-
服务器初始化响应:另一方面,服务器必须以类似的消息回答其功能——即它是否支持工具、资源、提示、通知等。以下是一个典型的服务器响应示例:
{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "logging": {}, "prompts": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "tools": { "listChanged": true } }, "serverInfo": { "name": "ExampleServer", "version": "1.0.0" } } }
观察响应中的capabilities属性,其中包含诸如日志记录、提示、资源和工具等内容。日志记录是服务器向客户端发送日志消息的能力。我们将在第五章中展示日志通知的示例。其他功能——提示、资源和工具——是服务器可以拥有的基本功能——更多关于这些内容将在第三章中介绍。
这个答案将帮助客户端确定它可以和不可以使用什么。它还提供了有关协议版本和服务器信息。
让我们来看看客户端发送的initialized消息,然后我们将尝试实现前面的消息流程。
-
完成握手:作为最后一步,客户端发送一个
initialized消息。这是握手的最终消息。一旦收到,服务器就可以准备处理客户端关于工具、资源或提示的任何类型的 JSON-RPC 消息。客户端的这条消息不应产生服务器的响应。然而,服务器应该记住它已经被初始化。一旦初始化完成,正常操作就可以进行,例如调用工具、提示等。以下是详细的initialized消息——如您所见,它携带的信息不多,但对于客户端和服务器正常操作至关重要。在发送此通知之前,您无法做很多事情。{ "jsonrpc": "2.0", "method": "notifications/initialized" }
我们如何实现这一点?让我们接下来看看。
实现初始化
双方交换了一些信息。实际上,客户端只需向服务器发送initialized即可完成,但通过initialize首先交换功能被认为是一种良好的实践。
之前小节中的客户端代码只是发送文本消息和 JSON-RPC 消息,但并没有真正遵循初始化过程的流程。为了解决这个问题,我们需要修改客户端,使其能够正确处理初始化序列。初始化序列完成后,客户端可以发送其他类型的消息,例如列出工具等。
让我们在客户端添加一个connect函数,并确保它首先请求服务器功能,然后通知服务器初始化已完成。
# client.py
def connect():
print("Connecting to the server...")
# 1\. Ask for capabilities
send_message(serialize_message(initialize_message))
# Read response from child/server
response = proc.stdout.readline()
print_response(response, prefix='[SERVER]: \n')
# 2\. Send initialized notification, handshake is done
send_message(serialize_message(initialized_message))
让我们来解释一下实现方法:
-
它创建了一个
connect函数,请求功能并发送了initialized通知。 -
它设置了一个调用链,从通过派发
initialize消息请求功能开始,然后派发initialized,从而结束握手过程。
让我们通过一个main函数进一步改进事情,如下所示:
def list_tools():
# 3\. send a message to list tools
# send a JSON-RPC message
send_message(serialize_message(list_tools_message))
response = proc.stdout.readline()
print_response(response, prefix='[SERVER]: \n')
def close_server():
send_message('exit\n')
exit_code = proc.wait()
print(f"Child exited with code {exit_code}")
def main():
connect()
list_tools()
close_server()
main()
这是我们的创建结果:
-
我们创建了一个
main函数,该函数连接到服务器,列出工具,并在完成后关闭服务器连接 -
我们定义了
list_tools,它发送一个特定的 JSON-RPC 消息,请求服务器列出其工具 -
此外,我们还创建了
close_server方法,它向服务器发送一个退出消息。
让我们专注于服务器部分:
对于服务器,我们需要确保它做出相应的反应。这意味着服务器只需要接受带有initialize或notifications/initialized方法的消息。初始化之前的所有其他消息应引发错误。初始化之后,我们支持的所有其他消息都应该允许客户端发送。
为了处理客户端尝试执行初始化之外的操作的情况,这里有一些代码来处理这种情况:
if method != "initialize" and method != "notifications/initialized":
print(f"Server not initialized. Please send an 'initialized'
notification first. You sent {method}")
sys.stdout.flush()
continue
此代码将停止对消息的进一步处理,并等待下一个传入的消息。
对于initialize和notifications/initialized消息,我们可以这样处理:
match method:
case "notifications/initialized":
# print("Server initialized successfully.")
sys.stdout.flush()
initialized = True
break
case "initialize":
print(json.dumps(initializeResponse))
sys.stdout.flush()
# initialized = True
break
# should return capabilities
让我们把代码放在一起,看看完整的服务器是什么样的:
# server.py
# code omitted for brevity
elif message.startswith('{"jsonrpc":'):
json_message = json.loads(message)
method = json_message.get('method', '')
if not initialized:
if method != "initialize" and method != "notifications/initialized":
print(f"Server not initialized. Please send an 'initialized'
notification first. You sent {method}")
sys.stdout.flush()
continue
match method:
case "notifications/initialized":
# print("Server initialized successfully.")
sys.stdout.flush()
initialized = True
break
case "initialize":
print(json.dumps(initializeResponse))
sys.stdout.flush()
# initialized = True
break
# should return capabilities
case "tools/list":
response = {
"jsonrpc": "2.0",
"id": json_message["id"],
"result": ["tool1", "tool2"]
}
print(json.dumps(response))
sys.stdout.flush()
break
case _:
print(f"Unknown method: {json_message['method']}")
sys.stdout.flush()
break
作为练习,建议你根据需要改进前面的解决方案。你可以将此代码重写为send_response方法,如下所示:
def send_response(response):
print(json.dumps(response))
sys.stdout.flush()
在前面的代码中,我们做了以下操作:
-
定义了一个
elif,表示如果我们收到一个 JSON-RPC 消息,我们将尝试正确路由该消息 -
添加了一个检查,表示如果我们尚未初始化,并且消息不是请求功能或客户端发送的
"notifications/initialized"通知,那么我们发送一条消息回显这不是一个合适的消息类型。 -
初始化之后,我们可以接受列出工具等消息,在这种情况下,我们以我们的工具作为响应。
在解决方案文件夹中查看此代码的运行示例。检查初始化(github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter02/Solutions/README.md)。
支持的功能
现在我们有了更健壮的代码,让我们支持工具、资源和提示等功能。
你已经看到了connect是一个我们用来与服务器握手的函数。之后,我们想要调用一个工具、一个资源和一个提示,并在继续下一步操作之前等待它们的响应。因此,为了使这种行为成为可能,我们需要做以下操作:
-
将消息放置在流中。
-
等待响应到达。
-
如果我们得到一个正常的响应,显示它;如果是通知,则忽略它。服务器通知通常是特殊消息或进度更新,我们将在下一节中实现对该功能的支持。
太好了,我们有一个计划。让我们回顾一下main方法,看看我们有哪些内容。
现在,我们需要考虑如何处理list_tools()调用返回的响应,如下面的代码所示:
def main():
connect()
list_tools()
close_server()
main()
理想情况下,我们希望捕获响应并对其进行处理,例如,将这些工具存储起来,以便我们稍后可以通过例如call_tool(我们还没有但计划创建的方法)来调用它们。
因此,我们希望以下代码能够被编写:
def main():
connect()
tools = list_tools()
call_tool(tools[0], args) # should specify args to call_tool
close_server()
main()
在这一点上,我们需要捕获list_tools的响应,并且我们需要打印它。为了实现这一点,我们需要确保服务器和客户端都进行了更改。客户端需要将响应存储在tools变量中,服务器需要识别正在发送list tools命令,并响应适当的 JSON-RPC 消息。
让我们先看看服务器。在这里,我们需要添加一个新的情况,tools/list,列出所有工具:
# server.py
# code omitted for brevity
case "tools/list":
response = {
"jsonrpc": "2.0",
"id": json_message["id"],
"result": {
"tools": [
{
"name": "example_tool",
"description": "An example tool that does something.",
"inputSchema": {
"type": "object",
"properties": {
"arg1": {
"type": "string",
"description": "An example argument."
}
},
"required": ["arg1"]
}
}
]
}
}
print(json.dumps(response))
sys.stdout.flush()
break
在前面的代码中,值得指出的是以下内容:
-
tools属性:这应该指向工具列表。 -
inputSchema:这个模式应该描述这个工具接受的参数以及它们是否是必须发送的。对于这个特定的工具,它被称为example_tool,它有一个参数arg1,这是必需的。
现在我们已经完成了服务器端,让我们接下来关注客户端。首先,让我们定义list_tools方法:
# client.py
def list_tools():
# 3\. send a message to list tools
# send a JSON-RPC message
send_message(serialize_message(list_tools_message))
response = proc.stdout.readline()
return json.loads(response)['result']['tools']
接下来,让我们使用main方法中的list_tools:
# client.py
tools = []
def main():
connect()
tool_response = list_tools()
tools.extend(tool_response)
print("Tools available:", tools)
在main方法中,我们调用list_tools,保存tool_response响应,并将其添加到我们将要稍后用于尝试调用服务器上工具的tools列表中。
我们还需要支持如何调用工具。就像之前一样,我们先添加服务器部分:
# server.py
case "tools/call":
tool_name = json_message['params']['name']
args = json_message['params']['args']
# todo create a response for the tool call, i.e call the right tool
response = {
"jsonrpc": "2.0",
"id": json_message["id"],
"result": {
"properties": {
"content": {
"description": "description of the content",
"items": [
{ "type": "text", "text": f"Called tool
{tool_name} with arguments {args}" }
]
}
}
}
}
print(json.dumps(response))
sys.stdout.flush()
break
在前面的代码中,我们做了以下操作:
-
构建了一个 JSON-RPC 消息。
-
添加了具有
properties属性的result,它本身有一个属性,即content。该属性有一个包含描述调用工具结果的多个文本块的items数组。在更实际的实现中,应该调用相关的工具,并将结果列在这里。
接下来,让我们关注客户端部分。我们需要一个call_tool方法:
# omitting code for brevity
def call_tool(tool_name, args):
# 4\. call a tool
# send a JSON-RPC message
tool_message = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": tool_name,
"args": args
},
"id": 1
}
send_message(serialize_message(tool_message))
response = proc.stdout.readline()
return
json.loads(response)["result"]["properties"]["content"]["items"]
在这里,我们做以下操作:
-
构建一个 JSON-RPC 消息,并将
tool_name和args作为消息的一部分传递。 -
通过将响应加载为 JSON 并深入响应以获取包含相关响应部分的
items来处理响应。
在main方法中,我们需要添加以下代码:
def main():
connect()
tool_response = list_tools()
tools.extend(tool_response)
print("Tools available:", tools)
tool = tools[0]
tool_call_response = call_tool(tool["name"],{"args1": "hello"})
for content in tool_call_response:
print_response(content['text'], prefix='[SERVER] tool response: \n')
# print_response(tool_call_response['result'], prefix='[SERVER]: \n')
# call tool, we need a name and arguments
close_server()
在前面的代码中,我们做了以下操作:
-
使用工具的名称调用
call_tool,并且我们也传递了参数。我们硬编码了参数。 -
遍历响应以获取我们在服务器上定义的文本块响应。
在所有这些部分就绪后,运行程序现在也应该将其作为输出的一部分列出:
[CLIENT]: {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "example_tool",
"args": {
"arg1": {
"type": "string",
"description": "An example argument."
}
}
},
"id": 1
}
[SERVER] tool response:
Called tool example_tool with arguments {'args1': 'hello'}
太好了,现在我们有了列出服务器上的工具和调用工具所需的所有功能。不过,应该指出的是,调用工具也应该执行计算。目前,我们只是列出调用了哪些工具以及传递了什么参数,所以这是一个好的开始,但我们应该进一步改进。
让我们继续讨论通知。通知可以从服务器发送到客户端,也可以从客户端发送到服务器。
在解决方案文件夹中查看此代码的运行示例——检查功能 (github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter02/Solutions/README.md)。
通知、报告进度和重要更新
通知是客户端和服务器都可以相互发送的东西。通常,他们想要告诉对方发生了重要的事情——例如,一个长时间运行的工具响应可能会报告进度,或者服务器可能会发送消息来报告其功能的变化。那么,我们如何支持通知呢?好吧,它有两个方面:
-
从客户端或服务器发送通知类型消息。这看起来是这样的:
{ "jsonrpc": "2.0", "method": "notifications/[type]", "params": {} }
[type]通常是cancelled或progress。有关类型的完整规范,请查看github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts。
- 通知的作用。对于客户端来说,通知被视为一个额外的事物,你应该在用户界面中展示出来,以改善用户体验。然而,客户端向服务器发送的通知则有所不同。例如,客户端向服务器发送通知以将状态设置为
"initialized"。
那么,我们如何实现这一点呢?好吧,我们大部分已经准备好了。尽管如此,我们将在我们的代码中添加以下内容:
-
从服务器向客户端发送通知,在这种情况下,报告等待调用工具的结果时的进度
-
在我们的
list_tools和call_tool函数中添加逻辑以支持处理到达的通知。
首先,让我们看看我们如何支持客户端上的通知:
# client.py
# code omitted for brevity
def list_tools():
# 3\. send a message to list tools
# send a JSON-RPC message
send_message(serialize_message(list_tools_message))
has_result = False
while not has_result:
response = proc.stdout.readline()
# check if message has result attribute, if so break out of loop
parsed_response = json.loads(response)
if 'result' in parsed_response:
has_result = True
return parsed_response['result']['tools']
else:
# this is a notification, we can print it
print_response(response, prefix='[SERVER] notification: \n')
在这里,我们重写了list_tools并为其添加了一个循环。只要我们接收到的不是最终结果,我们就将其打印出来(因为它是一个通知)。然后,一旦我们得到最终结果——也就是说,它包含一个result属性——我们就从函数中返回它。
我们如何在服务器端处理这个问题呢?我们需要几件事情:
-
需要创建一个通知消息。我们可以将其放置在我们的
utils文件夹中的messages.py文件里。# utils/messages.py progress_notification = { "jsonrpc": "2.0", "method": "notifications/progress", "params": { "message": "Working on it..." } }; -
我们需要从我们希望它发生的地方发送实际的通知。为了展示它是如何工作的,让我们将其添加到
tools/list案例中,如下所示:# server.py # code omitted for brevity case "tools/list": # send notification about progress first, then later the #response print(json.dumps(progress_notification)) sys.stdout.flush()
应该指出的是,进度通知,尤其是当调用工具时,应该使用,而不是列出你拥有的工具,因为调用工具在某些情况下可能是一个耗时的操作,所以让我们添加这一点。首先,让我们以与客户端上的list_tools相同的方式重做call_tool:
# client.py
# code omitted for brevity
def call_tool(tool_name, args):
# 4\. call a tool
# send a JSON-RPC message
tool_message = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": tool_name,
"args": args
},
"id": 1
}
has_result = False
send_message(serialize_message(tool_message))
while not has_result:
response = proc.stdout.readline()
parsed_response = json.loads(response)
if 'result' in parsed_response:
has_result = True
return
parsed_response["result"]["properties"]
["content"]["items"]
else:
# this is a notification, we can print it
print_response(response, prefix='[SERVER]
notification: \n')
就像在 list_tools 中一样,我们添加了一个循环,只打印通知,除非最终结果显示出来。这里确实有一些代码重复,所以我们可能想在某个时候将其拆分成一个实用函数。让我们看看服务器上需要添加什么:
# server.py
# code omitted for brevity
case "tools/call":
tool_name = json_message['params']['name']
args = json_message['params']['args']
print(json.dumps(progress_notification))
sys.stdout.flush()
print(json.dumps(progress_notification))
sys.stdout.flush()
好的,所以现在我们已经添加了代码,向处理类型为 tools/call 的传入消息的案例发送两条通知。
当我们再次尝试运行它时,我们应该在输出末尾看到以下内容:
[SERVER] notification:
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"message": "Working on it..."
}
}
[SERVER] notification:
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"message": "Working on it..."
}
}
[SERVER] tool response:
Called tool example_tool with arguments {'args1': 'hello'}
显然,两条通知在调用工具响应之前到达。
应该说,从性能的角度来看,如果我们将其视为 SDK 实现,我们可能会更改并添加asyncio库以确保非阻塞。为了演示消息如何来回流动,它已经起到了作用,但可以稍作改进。
太好了——我们已经成功实现了通知并进行了一些很好的重构。让我们看看下一个采样。
在解决方案文件夹中查看此代码的运行示例——检查通知(github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter02/Solutions/README.md)。
采样 – 帮助服务器完成请求
采样是一个有趣的功能。它的意思就是服务器在告诉客户端,“我不知道如何做这件事”,或者“你做得更好——你能帮我完成这个请求吗?”更具体地说,服务器要求客户端使用客户端的大型语言模型(LLM)来完成请求。
好的,所以我们已经确定服务器有时需要客户端帮助它完成请求。客户端帮助的方式是通过向其 LLM 请求响应,然后将响应传递回服务器。
那么,起点是什么呢?好吧,它可能是一个调用服务器上工具的客户端,然后该工具生成一个采样请求。然后流程可能看起来是这样的:

图 2.2 – 采样流程,场景 1
快速提示:需要查看此图像的高分辨率版本吗?在下一代 Packt Reader 中打开此书或在其 PDF/ePub 副本中查看。
下一代 Packt Reader以及本书的免费 PDF/ePub 副本包含在您的购买中。扫描二维码或访问packtpub.com/unlock,然后使用搜索栏通过名称查找本书。请仔细检查显示的版本,以确保您获得正确的版本。

或者,它可能是一个生成事件的外部服务,服务器会监听这个事件。以下是该场景的流程:

图 2.3 – 采样流程,场景 2
从消息的角度来看,以下是服务器发送给客户端的内容:
{
messages: [
{
role: "user" | "assistant",
content: {
type: "text" | "image",
// For text:
text?: string,
// For images:
data?: string, // base64 encoded
mimeType?: string
}
}
],
modelPreferences?: {
hints?: [{
name?: string // Suggested model name/family
}],
costPriority?: number, // 0-1, importance of minimizing cost
speedPriority?: number, // 0-1, importance of low latency
intelligencePriority?: number // 0-1, importance of capabilities
},
systemPrompt?: string,
includeContext?: "none" | "thisServer" | "allServers",
temperature?: number,
maxTokens: number,
stopSequences?: string[],
metadata?: Record<string, unknown>
}
在先前的请求中包含了一些信息:
-
messages是助手和用户之间的聊天对话,并为请求提供了所需上下文。 -
modelPreferences:在这里,服务器可以指定诸如首选模型名称以及决定成本、速度和能力优先级的事情
此外,还有关于模型配置的数据正在发送,包括温度、要使用的标记数量等。值得注意的是,这些是客户可以选择接受或修改的建议。
客户随后应该以完成消息的形式响应,如下所示:
{
model: string, // Name of the model used
stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string,
role: "user" | "assistant",
content: {
type: "text" | "image",
text?: string,
data?: string,
mimeType?: string
}
}
在先前的响应中,以下信息被涵盖:
-
model是使用的模型。它不必与服务器请求的相同。 -
stopReason– 了解你是否得到了完整的响应,或者是否因为其他原因而停止,这是很好的。 -
content是内容响应。
介绍一个场景:电子商务
然而,这发生在什么时候呢?好吧,服务器可能会监听它应该做出反应的事件。例如,假设你有一个电子商务场景,某个系统正在注册新产品。然而,在这些产品可以销售之前,它们需要一个合适的描述。这个描述是客户及其 LLM 可以帮助完成的事情。

图 2.4 – 电子商务场景流程
在最后一步,服务器记录、存储并可能缓存响应。
让我们看看这个上下文中的一个请求。以下是一个来自服务器的示例请求:
{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Create a selling product description for this sweater,
keywords autumn, cozy, knitted"
}
}
],
"systemPrompt": "You are a helpful assistant assisting with
product descriptions",
"includeContext": "thisServer",
"maxTokens": 300
}
}
如您所见,我们决定只提供上下文,并省略了关于使用哪种模型或配置的建议,但如果我们想的话,我们可以添加这些。
现在取决于客户端如何解释这个请求并做出响应。
实现采样
让我们在代码中实现采样,我们将使用产品描述场景,这样你可以看到它是如何被使用的。以下是我们需要的内容:
-
外部服务:此服务应生成一个需要产品描述的新产品。这个产品已被另一个系统中的某人注册,并作为监听事件的结果出现在这个服务器上作为消息有效负载。这是特定场景的代码。
-
服务器 – 样本请求:我们只需要发送
json-rpc请求的能力。 -
客户响应:我们需要监听这种特定的消息类型,使用消息的有效负载调用 LLM,并将响应返回给服务器。
让我们从外部服务开始。这实际上不是 MCP 的一部分,但它可能会让你对如何集成一个生成你感兴趣消息的外部组件有一个概念:
在这里,我们正在添加一个外部服务,它最终将注册新产品。它将在随机的时间间隔内生成新产品并发送给任何监听者。
# server.py
# code omitted for brevity
class ProductStore:
def __init__(self):
self.started = False
self.listeners = {}
# create timer that adds a product to the queue every 5 seconds
def add_product(self):
"""Add a product to the store and notify listeners."""
product = {
"id": str(random.randint(10000, 99999)),
"name": f"Product {random.randint(1, 100)}",
"price": round(random.uniform(10.0, 100.0), 2),
"keywords": [f"keyword{random.randint(1, 5)}" for _ in
range(random.randint(1, 3))]
}
self.dispatch_message("new_product", product)
def start_product_queue_timer(self):
"""Start a timer that adds a product to the queue
every 5 seconds."""
def schedule_next():
delay = random.uniform(1, 2)
self.product_timer = threading.Timer(delay, self.add_product)
self.product_timer.start()
def add_twice():
schedule_next()
schedule_next()
add_twice()
def add_listener(self, message, callback):
if not self.started:
self.started = True
self.start_product_queue_timer()
"""Add a listener for product updates."""
# In a real application, this would register the callback
#to be called when a new product is added
callbacks = self.listeners.get(message, [])
callbacks.append(callback)
self.listeners[message] = callbacks
def dispatch_message(self, message, payload):
"""Dispatch a message to all registered listeners."""
callbacks = self.listeners.get(message, [])
for callback in callbacks:
callback(payload)
首先,我们创建一个能够创建和分发我们希望客户帮助的产品类。在之前提到的场景中,我们需要客户及其 LLM 为我们生成描述。
下面是产品存储库代码的组成部分:
-
ProductStore类:这个存储库应该模拟一个外部源,该源可以随时发送需要增强产品描述的新产品。将这个存储库视为一个队列,例如,它可以长时间轮询 API 或监听消息队列。关键是它只是一个占位符,只有你知道这个结构是什么,因为它针对特定的解决方案。 -
几个辅助方法,例如
add_listener、dispatch_message和start_product_queue_timer:后者负责在特定时间间隔调度产品。
现在我们将通过首先创建产品存储库,然后向其添加监听器来与产品存储库进行交互。每当添加新产品时,监听器应向客户发送消息。
def create_sampling_message(product):
sampling_message = {
"jsonrpc": "2.0",
"id": 1,
"method": "sampling/createMessage",
"params": {
"messages": [{
"role": "system",
"content": {
"type": "text",
"text": f"New product available:
{product['name']} (ID: {product['id']},
Price: {product['price']}). Keywords:
{', '.join(product['keywords'])}"
}
}],
"systemPrompt": "You are a helpful assistant assisting
with product descriptions",
"includeContext": "thisServer",
"maxTokens": 300
}
}
return sampling_message
store = ProductStore()
store.add_listener("new_product", lambda product:
print(json.dumps(create_sampling_message(product))))
我们还需要一种方式在服务器上接收来自客户端的采样响应。让我们看看我们如何处理这个问题。我们目前与客户端的主要问题是,我们并没有真正设置好来处理可以随时到达的消息。我们现在所拥有的是,我们明确地从客户端发送调用到服务器,并期望得到一个响应,然后我们处理这个响应。为了解决这个问题,我们需要稍微改变我们的架构,使用一个后台线程。它的工作方式如下:
-
线程监听
stdout上的消息。 -
如果收到消息(来自服务器),则将其放入队列以供后续处理。
-
对于样本消息,我们不会将它们放入队列,而是直接处理,对于通知和常规响应,我们将它们放入队列,并让它们在相应的方法(例如
list_tools、call_tool等)中处理。
让我们看看我们如何设置线程:
# client.py
def listen_to_stdout():
"""Listen to the stdout of the child process and handle messages."""
while True:
response = proc.stdout.readline()
if not response:
break # Exit if no more output
try:
parsed_response = json.loads(response)
if is_sampling_message(parsed_response):
handle_sampling_message(parsed_response)
# consume message if it is a sampling message
else:
# put message in the queue for further processing
message_queue.put(response.strip())
except json.JSONDecodeError:
# If the response is not JSON, just print it
print("[THREAD] Non-JSON response received:",
response.strip())
# print_response(response, prefix='[THREAD]: \n')
# create thread and start it
listener_thread = threading.Thread(target=listen_to_stdout, daemon=True)
listener_thread.start()
上述代码执行以下操作:
-
从
stdout读取。 -
检查是否有采样消息。如果有,它将尝试通过调用
handle_sampling_message来处理它。如果不是采样消息,则将其放入队列。
这里还有一些我们也需要的辅助方法:
def is_sampling_message(message):
"""Check if a message is a sampling message."""
return message.get('method', '').startswith('sampling')
def create_sampling_message(llm_response):
"""Create a sampling message for a product."""
sampling_message = {
"jsonrpc": "2.0",
"result": {
"content": {
"text": llm_response
}
}
}
return sampling_message
def call_llm(message):
return "LLM: " + message
def handle_sampling_message(message):
"""Handle a sampling message."""
print("[CLIENT] Calling LLM to complete request", message)
# get content info from message, send that to LLM
content = message['params']['messages'][0]['content']['text']
llm_response = call_llm(content)
message = create_sampling_message(llm_response)
send_message(serialize_message(message))
# should call LLM to complete request
尤其值得注意的是handle_sampling_message,它从服务器接收消息并调用call_llm(我们已模拟此方法;你应该确保在实际实现中它调用实际的 LLM)。然后构建响应并将其发送回服务器进行缓存、记录或服务器需要对其进行的其他操作。
由于我们现在依赖于这个队列来接收消息,我们需要修改list_tools和call_tool以从该队列读取,而不是从流中读取,如下所示:
while not has_result:
# response = proc.stdout.readline()
response = message_queue.get()
在这里,我们注释了如何请求最新的消息,调用readline,而现在我们调用message_queue.get。
就这样 – 这就是采样的工作方式。它起源于服务器,作为事件的结果,然后它请求客户端的帮助,客户端做出响应。应该强调的是,客户端不必完全按照服务器的要求去做 – 那就是使用特定的模式或配置。你应该在平等用户面前展示服务器请求,这样他们就有机会做出响应并配置客户端发送的内容 – 那就是有人工干预。
在解决方案文件夹中查看此代码的运行示例 – 检查 采样 (github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter02/Solutions/README.md)。
SSE 传输
到目前为止,我们已经走过了 MCP 的很大一部分。那么,SSE 和 STDIO 之间的区别是什么?主要区别在于消息的流动方式。在 STDIO 中,消息在本地机器的 stdin 和 stdout 流之间传递;对于 SSE,消息通过 HTTP 在网络上流动。这意味着所有主要动作,如握手、初始化、工具调用等,都需要重新考虑为客户端和服务器之间的网络请求。
从概念上讲,SSE 传输被实现为一个具有 /messages 和 /sse 路由的网络服务器。前者处理传入的 MCP 消息,而后者用于建立流式传输事件的连接。
这是从宏观层面看的样子:

图 2.5 – SSE 传输流程
我们将在 第四章 中更详细地介绍 SSE,但现在我们已经在宏观层面上很好地理解了与 STDIO 相比的不同之处。
Streamable HTTP
在某种程度上,Streamable HTTP 与 SSE 类似,因为使用 Streamable HTTP 的 MCP 服务器可以通过网络上的 URL 访达。有一些差异和相似之处。让我们使这一点更加清晰:
-
SSE 和 Streamable HTTP 都需要客户端接受
text/event-stream。对于 Streamable HTTP,客户端还需要接受application/json,因为服务器可以选择是流式传输响应还是以 JSON 格式发送它们。对于 SSE,服务器始终以text/event-stream格式发送内容。 -
SSE 连接通常是长生命周期的
GET请求,而 Streamable HTTP 通常为POST。
从实现的角度来看,它与 SSE 非常相似 – 即,它被实现为一个网络服务器。然而,对于 Streamable HTTP,在 MCP 的上下文中,你应该设置一个路由,/mcp,该路由处理连接和消息,并且它也应该设置为 POST。

图 2.6 – Streamable HTTP 流程
因此,实现 Streamable HTTP 比 SSE 简单一些,因为你只需要跟踪一个端点,/mcp。关于 Streamable HTTP 的更多内容请参阅 第五章。
摘要
在本章中,我们涵盖了大量的信息。最重要的收获是,客户端和服务器通信需要在进一步操作之前进行初始化。幸运的是,大多数 SDK 都负责初始化部分,您通常可以通过调用和列出工具等方式开始客户端-服务器通信。希望这对那些认为图表有助于理解的人和喜欢看到代码样子的人都有所帮助。代码 是有效的,但当然可以为了性能、可维护性等方面进行改进。请尝试运行代码 – 检查解决方案文件夹。
在下一章中,我们将学习如何构建和测试我们的第一个服务器。本章将作为 MCP 动手实践的良好介绍。
作业
尝试运行提供的代码github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter02/Solutions/README.md,以查看事物是如何工作的。代码应该执行以下操作:
-
初始化连接
-
发送初始化通知
-
列出工具
-
调用一个工具并生成通知
-
生成示例消息
代码是分步骤构建的,因此值得查看您选择的运行时所有子文件夹。
解决方案
您可以通过github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter02/Solutions/README.md访问解决方案。
测验
在服务器和客户端之间的 握手 被视为完成之前,需要发生什么?
-
A: 客户端需要首先发送 initialize,等待服务器响应,然后发送 initialized。
-
B: 客户端和服务器可以立即调用,例如,列出工具。
-
C: 客户端需要向服务器发送 initialized。
|
现在解锁这本书的独家优惠
扫描此二维码或访问packtpub.com/unlock,然后通过名称搜索此书。 | 
|
| 注意:在开始之前准备好您的购买发票。* |
| --- |
第三章:构建和测试服务器
在本章中,我们将介绍构建和测试服务器的基础知识。我们将从一个使用 STDIO 传输的简单服务器开始。STDIO 传输是一种通过标准输入和输出流与服务器通信的简单方式。构建 STDIO 服务器是开始使用 模型上下文协议(MCP)并了解其工作原理的好方法。还应补充说明,STDIO 传输是与服务器通信的最常见方式,它适用于运行在您的机器上的服务器。
作为本章的一部分,我们还将介绍不同的服务器测试方法。我们构建的内容能够按预期工作是很重要的。我们将介绍可以用于视觉、CLI 和代码中的不同工具。
在本章中,你将学习以下内容:
-
使用 STDIO 传输构建一个简单的服务器
-
描述服务器的核心概念
-
使用不同的工具测试服务器
本章涵盖了以下主题:
-
一个 STDIO 服务器
-
概念
-
运行时
-
测试服务器
-
第一个服务器
一个 STDIO 服务器
使用 STDIO 传输意味着服务器通过标准输入和输出流与客户端通信。这意味着服务器可以通过 标准输入(stdin)从客户端读取输入,并通过 标准输出(stdout)将输出发送回客户端。好吧,听起来很简单,但它是如何工作的?
MCP 使用 JSON-RPC 2.0 作为其线格式。因此,它需要将流消息转换为 JSON-RPC 格式进行传输,并将接收到的 JSON-RPC 消息转换回流消息。想象以下在终端中的示例:
>> hello world
要在 STDIO 服务器中使用,前面的消息将被转换为 JSON-RPC 格式,看起来像这样:
{
"jsonrpc": "2.0",
"method": "sendMessage",
"params": {
"message": "hello world"
},
"id": 1
}
这是一个有趣的例子,但作为开发者,真正对你来说重要的是两件事:
-
如何构建一个 STDIO 服务器。我们将在接下来的部分中简要展示。
-
为什么这很重要,以及你的服务器是 STDIO 服务器意味着什么。这意味着你的服务器正在你的本地机器上运行,并通过标准输入和输出流与客户端通信。在第四章和第五章中,我们将向你展示如何构建可以与互联网上的服务器通信的服务器。
概念
让我们看看服务器可以提供的一些核心概念(如果你愿意称之为功能)。
资源
这些是服务器可以提供给客户端的数据和上下文。MCP 的使用方式是客户端在与服务器通信时拥有一个 LLM。在这个用例中,资源作为上下文,可以在提示时添加到 LLM 中。想象以下场景:

图 3.1 – 资源场景
在这种情况下,上下文确保最终用户获得更好的结果,因为服务器的上下文与用户的提示配对,就像简化的检索增强生成(RAG)模式。也就是说,你将用户的提示与你的数据配对以获得更好的响应。
一个具体实现这种思维方式的方法示例是用户请求产品如下:
提示
User: I'm looking for a new laptop

图 3.2 – 资源交互示例
在这个特定的产品查询中,我们首先调用资源来了解要查询哪个表,然后我们让 LLM 确定要调用哪个工具。多亏了调用资源,我们获得了关于选择哪些工具以及使用哪些参数的额外知识。
资源也可以在不使用 LLM 的情况下使用,但你应该这样思考你的服务器:它是为了赋予客户端的 LLM 能力。这意味着你提供的工具、资源和提示应该是有帮助的。
现在我们已经了解了何时使用资源,让我们更多地讨论它们的本质。资源是静态的,可以是服务器可以访问并与客户端共享的任何内容。重要的是要知道,你可以使用或不用模板来请求资源。如果只有一个文件或一个应用程序设置配置,使用一个设置名称,如 config 是有意义的:
server.resource(
"config",
"config://app",
async (uri) => ({
contents: [{
uri: uri.href,
text: "App configuration here"
}]
})
);
在这里,我们每次都返回相同类型的信息。
然而,如果有许多类型的设置,例如用户设置、日历设置等,创建一个更模板化的版本,如 settings://{type} 可能是有意义的:
@mcp.resource("settings://{type}")
def get_setting(type: str) -> str:
"""Get a specific setting"""
return f"Setting from file {type}!"
设置仍然是不可变的,但有很多,所以我们选择将它们分组在一个命名空间下。
在这种情况下,资源被命名为 settings 并接受 type 作为参数。例如,settings://hello 将匹配此模式。
工具
工具是服务器可以执行的功能或能力。工具的例子包括数据处理函数、调用 API 以获取数据的工具等。对于工具,我们需要通过提供模式来定义输入和输出。具体如何操作取决于所使用的运行时,但理念是应该让工具的消费者清楚输入和输出是什么。例如,如果我们有一个接受两个数字并返回其乘积的工具,我们可以这样定义它:
# Add a multiplication tool
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers"""
return a * b
在这个例子中,我们定义了一个名为 multiply 的工具,它接受两个数字作为输入并返回它们的乘积。输入和输出使用 Python 类型提示定义。
提示
提示是模板化的消息或工作流程,用于指导客户端和服务器之间的交互。一个很好的提示例子是帮助客户端在电子商务应用程序的上下文中编写产品描述或口号的模板。就像资源和工具一样,提示也可以接受输入。以下是一个接受产品名称并返回产品描述的提示示例:
@mcp.prompt()
def describe_product(product: str) -> str:
return f"Write a product description for {product}"
在这里,我们定义了一个名为 describe_product 的提示,它接受一个产品名称作为输入并返回产品描述。输入是通过 Python 类型提示定义的。
既然我们已经知道了我们的服务器可以包含什么,我们还需要了解什么?
运行时
目前官方支持 MCP 的运行时包括 TypeScript、Python、.NET、Java 和 Kotlin、Rust 和 Go。更多运行时正在不断增加。有关运行时的最新列表,请参阅此处:modelcontextprotocol.io/docs/sdk。每个运行时都有自己的 SDK 可以使用。运行时的实现类似,但也有一些差异。
激动吗?让我们开始吧!
测试服务器
你有很多工具可以使用来测试服务器。我们为什么要测试呢?因为我们想确保服务器按预期工作。在本章中我们将涵盖的工具如下。
检查员
这是一个 CLI 工具,可以提供 UI 和 CLI 界面。后者用于脚本化和自动化。
检查员工具通过 npx 运行一个 Node.js 包,所以请确保你已经安装了 Node.js 运行时。即使你通过 Python 命令运行检查员工具,这也同样适用,因为 Python 会包装对底层 Node.js 进程的调用。
UI 旨在进行手动测试和调试。在这个示例命令中,我们运行了检查员工具。确保你在运行以下命令时站在服务器文件相同的目录中。
Python SDK 安装了一个名为 mcp 的可执行文件,它有助于运行服务器:
mcp dev server.py
通过运行此工具,我们可以在视觉模式下测试服务器。以下是检查员工具的截图:

图 3.3 – 检查员工具
确保视觉工具指定以下字段:
-
传输类型:STDIO
-
命令:
mcp -
参数:
run server.py
你也可以在 CLI 模式下运行检查员工具。这对于脚本化和自动化很有用。你输入的命令几乎和之前一样,但你需要添加 --cli 标志:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method tools/list
这里,我们添加了 --method 命令,后面跟着 tools/list 参数,表示我们想要列出服务器上的所有工具。
注意,Python 中的 mcp dev 包裹了一个 Node.js 工具,即检查员。如果你想完全访问检查员的所有功能,可以直接在 Node.js 和 Python 中运行它,我们推荐这样做(即,像这样运行:npx @modelcontextprotocol/inspector)。
cURL
可以使用标准命令行工具如 cURL 向服务器发送请求。这通常用于测试使用 SSE 或可流式传输 HTTP 作为传输的服务器。也可以使用其他能够发送网络请求的工具。只有当服务器在互联网上运行时,curl 命令才会起作用,这对于 SSE 服务器来说是这种情况。对于 STDIO 服务器,你需要使用检查员或自定义客户端来向服务器发送请求。让我们看看一个典型的 curl 命令:
curl -X POST -H "Content-Type: application/json" -d '{"method": "tools/list", "params": {}, "id": 1}' http://localhost:3000/sse
在前面的命令中,我们向服务器发送了一个带有tools/list方法的POST请求。服务器将响应服务器上可用的工具列表。这是使用 CLI 模式的检查器的一个很好的替代方案。
测试
可以为服务器编写单元测试。除了使用检查器之外,这是一个很好的实践。测试可以在 CI/CD 管道中运行,并且有助于确保服务器按预期工作。你可以使用你喜欢的任何测试框架,并且你可以向服务器添加资源、工具和提示,然后进行测试。让我们看看一个测试的例子:
@pytest.mark.anyio
async def test_add_tool_decorator(self):
mcp = FastMCP()
@mcp.tool()
def add(x: int, y: int) -> int:
return x + y
assert len(mcp._tool_manager.list_tools()) == 1
在前面的例子中,我们做了以下操作:
-
使用
pytest框架测试服务器 -
创建一个新的服务器实例并添加一个名为
add的工具 -
测试工具是否已添加到服务器,并且工具的数量等于
1
这是一个简单的测试,但它展示了你可以如何使用pytest来测试服务器。
现在,我们对服务器的概念和功能有了很好的理解;让我们记录下我们将如何构建我们的第一个服务器的计划。
第一个服务器
要构建我们的第一个服务器,让我们首先通过构建简单服务器的步骤来了解:
-
创建一个新项目:我们将创建一个包含所需依赖项的新项目,并设置任何环境特定的设置。强烈建议你创建一个虚拟环境,以确保你不会全局安装库。通过使用虚拟环境,每个项目与其他项目隔离,你不必担心可能发生的库版本冲突。
-
安装依赖项:在这里,我们将列出并安装项目的依赖项,这些依赖项当然取决于你使用的运行时。
-
添加服务器代码:这是我们添加服务器功能的地方。作为这部分,我们将讨论功能、输入和输出以及模式。
-
使用检查器测试服务器:检查器是一个帮助你确保新功能正常工作的工具。该工具允许你在 CLI 模式和可视化模式中运行它,其中它显示一个网页浏览器 UI:
-
CLI 模式适用于 CI/CD 场景,因为它在终端中响应 JSON
-
当你作为开发者尝试确保服务器按预期工作的时候,可视化模式更为合适
-
让我们编写一些代码!
第 1 步:创建一个新项目
在我们编写任何代码之前,我们需要一个项目。这将为我们成功搭建基础。项目将包含我们编写服务器和定义测试和脚本的所需一切:
-
创建以下文件夹结构:
├── src/ |---- server.py -
创建虚拟环境:
python -m venv venv -
激活虚拟环境:
venv\Scripts\activate
你应该在终端提示符中看到虚拟环境的名称。这意味着虚拟环境已激活,你安装的任何包都将安装在这个环境中。
太好了,现在我们有了虚拟环境和文件夹结构。接下来,我们需要安装依赖项。
第 2 步:安装依赖项
在终端中运行以下命令:
pip install "mcp[cli]"
这将安装 MCP SDK 和 CLI 工具。
第 3 步:添加服务器代码
将以下代码添加到server.py中:
# server.py
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Demo")
# Add a multiply tool
@mcp.tool()
def multiply(first: int, second: int) -> int:
"""Multiply two numbers"""
return first * second
# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
@mcp.prompt()
def review_code(code: str) -> str:
return f"Please review this code:\n\n{code}"
上述代码执行以下操作:
-
创建一个名为
Demo的 MCP 服务器实例 -
添加一个名为
multiply的工具,它接受两个数字作为输入并返回它们的和 -
添加一个名为
greeting的资源,它接受一个名称作为输入并返回个性化的问候 -
创建一个名为
review_code的提示,它接受代码片段作为输入并返回代码的审查
第 4 步:使用检查器测试服务器
在这里,我们将使用检查器来测试服务器。检查器是一个 CLI 工具,可以提供 UI 和 CLI 界面。后者用于脚本和自动化。UI 用于手动测试和调试:
-
在终端中运行以下命令:
mcp dev server.py
这将在端口6274上启动检查器工具。
-
在您的网络浏览器中导航到
http://localhost:6274。这应该启动一个带有可视化界面的 Web 服务器,允许您测试示例。 -
在可视化界面中,确保您填写如下字段:
-
命令:
"mcp" -
参数:
"run server.py"
-
点击连接。
-
选择工具选项卡和列出工具,multiply应该被列出。点击multiply并填写如下字段:
-
第一个:
2 -
第二个:
4
-
您应该在工具结果字段中看到结果8。
太好了,现在我们有一个正在运行的服务器,我们可以使用检查器来测试它。
第 5 步:使用 CLI 模式下的检查器测试服务器
在本小节中,我们将直接在 CLI 模式下运行检查器。检查器是一个 Node.js 应用程序,而mcp dev是它的包装器。
在撰写本文时,mcp dev不支持检查器提供的所有功能。因此,我们将展示如何直接作为 Node.js 应用程序运行检查器,因为它是用这种方式编写的。
让我们展示一些您可以在检查器中运行的实用命令:
-
列出工具:在终端中运行以下命令:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method tools/list
这将列出服务器上可用的所有工具,您应该看到以下输出:
{
"tools": [
{
"name": "multiply",
"description": "Multiply two numbers",
"inputSchema": {
"type": "object",
"properties": {
"first": {
"title": "First",
"type": "integer"
},
"second": {
"title": "Second",
"type": "integer"
}
},
"required": [
"first",
"second"
],
"title": "multiplyArguments"
},
"outputSchema": {
"type": "object",
"properties": {
"result": {
"title": "Result",
"type": "integer"
}
},
"required": [
"result"
],
"title": "multiplyOutput"
}
}
]
}
在这里,您可以看到服务器上的所有工具以 JSON 格式。我们只有一个工具multiply,但我们可以看到它有一个inputSchema,包含first和second参数。
-
调用工具:在终端中运行以下命令:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method tools/call --tool-name multiply --tool-arg first=2 --tool-arg second=4
您应该看到如下响应:
{
"content": [
{
"type": "text",
"text": "8"
}
],
"structuredContent": {
"result": "8"
},
"isError": false
}
-
列出资源:在终端中运行以下命令:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/list
这将列出服务器中可用的所有资源,您应该看到以下输出:
{
"resources": []
}
空的?原因是资源和模板资源之间有一个区别。以下是列出模板资源的方法:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/templates/list
您应该得到如下响应:
{
"resourceTemplates": [
{
"uriTemplate": "greeting://{name}",
"name": "get_greeting",
"description": "Get a personalized greeting"
}
]
}
-
让我们调用我们的模板资源:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/read --uri greeting://chris
您应该看到如下响应:
{
"contents": [
{
"uri": "greeting://chris",
"mimeType": "text/plain",
"text": "Hello, chris!"
}
]
}
-
让我们使用以下命令列出提示:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method prompts/list
您应该看到类似以下输出:
{
"prompts": [
{
"name": "review_code",
"description": "",
"arguments": [
{
"name": "code",
"required": true
}
]
}
]
}
-
要调用提示,我们将输入提示名称和
prompts/get,如下所示:npx @modelcontextprotocol/inspector --cli mcp run server.py --method prompts/get --prompt-name review_code --prompt-args code="print('Hello World')"
您应该看到如下响应:
{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this code:\n\nprint('Hello World')"
}
}
]
}
可选地,您可以在server.py中添加一个资源,如下所示:
@mcp.resource("command://ping")
def get_echo() -> str:
"""Send pong"""
return "Pong"
请这样阅读:
npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/read --uri command://ping
您应该看到如下响应:
{
"contents": [
{
"uri": "command://ping",
"mimeType": "text/plain",
"text": "Pong"
}
]
}
注意您如何使用uri参数以相同的方式调用资源和资源模板。
摘要
在本章中,我们介绍了如何构建您的第一个服务器。对于我们的第一个服务器,我们使用了 STDIO 传输来创建一个旨在在您的机器上运行的服务器。我们还探讨了使用一个名为检查器的工具测试服务器功能的各种方法。检查器工具有两种不同的模式,CLI 模式和视觉模式。前者模式用于 CI/CD 场景,后者用于您作为开发者快速尝试功能。
在我们即将到来的章节中,我们将描述 SSE 传输,如果您希望服务器通过 URL 地址被消费,可以使用它。
作业 - 电子商务 STDIO 服务器
对于这个 MCP 服务器,我们将添加可以在电子商务应用程序上下文中使用的功能。因此,服务器需要以下功能。
它将需要以下工具:
-
get-orders: 此工具将返回订单列表。可选输入是客户 ID,输出是订单列表。每个订单应包含以下字段:ID、客户 ID、数量、总价和状态。 -
get-order: 此工具将返回特定订单。输入是订单 ID,输出是订单。订单应包含以下字段:ID、客户 ID、数量、总价和状态。 -
place-order: 此工具将下订单。输入是客户 ID 和购物车 ID。 -
get-cart: 此工具将返回一个购物车。 -
get-cart-items: 此工具将返回购物车中的项目列表。输入是购物车 ID,输出是项目列表。每个项目应包含以下字段:ID、名称、描述、价格、数量和产品 ID。 -
add-to-cart: 此工具将项目添加到购物车。输入是购物车 ID、产品 ID 和数量。输出是成功消息。如果未提供购物车 ID,则工具应创建一个新的购物车并将项目添加到新购物车。输出应包含成功消息和购物车 ID。 -
products: 此工具将返回产品列表。可选输入是类别,输出是产品列表。产品应包含以下字段:ID、名称、描述、价格和类别。输出应以 JSON 格式。 -
product: 此工具应返回特定产品。输入是产品 ID,输出是产品。产品应包含以下字段:名称、描述、价格、类别和 ID。输出应以 JSON 格式。 -
categories: 此工具将返回一个类别列表。输出应以 JSON 格式。类别应包含以下字段:ID、名称和描述。输出应以 JSON 格式。 -
get-customers: 此工具将返回客户列表。输出应为 JSON 格式。它应包含以下字段:ID、姓名和电子邮件。
它将需要此资源:
product_catalog: 此资源将返回目录中的产品列表。可选输入是类别,输出是产品列表。每个产品应包含以下字段:名称、描述和类别。目的是,如果潜在客户想查看它们,资源将返回目录中的产品列表。
如你所见,这将支持一个简单的电子商务应用和一个试图将商品放入购物车或下订单的客户。
你可以在内存数据结构中保持状态。
解决方案
你可以在 github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter03/solutions/README.md 找到解决方案。
问答
以下哪项是服务器可以公开的内容?
-
A: 工具、提示和服务
-
B: 工具和提示
-
C: 提示、工具和资源
你也可以在 github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter03/solutions/solution-quiz.md 找到解决方案。
参考资料
-
模型上下文协议:
github.com/modelcontextprotocol/ -
Python SDK:
github.com/modelcontextprotocol/python-sdk
|
现在解锁本书的独家优惠
扫描此二维码或访问 packtpub.com/unlock,然后通过名称搜索此书。 | 
|
| 注意:在开始之前,请准备好您的购买发票。* |
| --- |
第四章:构建 SSE 服务器
到目前为止,你已经看到了如何使用 STDIO 作为传输构建 MCP 服务器。这是一个为本地运行的服务器而选择的优秀传输方式。然而,如果你想要通过 HTTP 远程连接到服务器,或者如果你想要从 LLM 流式传输响应,那么还有一种称为 Server-Sent Events (SSE)的传输方式更适合这种情况。
在本章中,我们将专注于使用 SSE 作为传输构建和测试 MCP 服务器。
本章涵盖了以下主题:
-
SSE 概念
-
创建一个作为 web 应用的 SSE 服务器
-
使用 SSE 进行测试
-
创建 SSE 服务器
让我们深入了解 SSE 的细节以及如何使用它构建服务器。
SSE 概念
在我们开始构建服务器之前,有一些概念我们需要理解。首先,使用 SSE 作为传输的服务器是一个可以通过 HTTP 访问的服务器。这意味着,即使它可以在本地运行,也可以远程访问。这一点的含义是,这是一个我们需要通过 web 服务器公开的服务器。
SSE 是一个标准,用于在单个长连接上从服务器到客户端的单向通信。它允许服务器在不要求客户端轮询更改的情况下向客户端推送实时更新。以下是一些更详细的说明:
-
协议:SSE 使用标准的 HTTP,MIME 类型为 text/event-stream
-
客户端 API:浏览器使用 EventSource API 接收事件
-
格式:消息以纯文本形式发送,字段如
event、data和id,每个字段由两个换行符终止
它通常用于需要实时更新的仪表板类应用程序。
在 MCP 中,SSE 被分为两部分:我们连接到服务器并执行初始化(例如,握手)的部分,以及一个消息部分,我们将消息应用到服务器,最终读取或写入数据。因此,我们需要实现以下端点:
-
SSE 端点:这个端点用作客户端和服务器之间建立连接握手的方式。通过向这个端点发送请求,客户端将收到一个响应,这将保持连接打开。
-
消息端点:这个端点用于将消息路由到 MCP 服务器及其功能。
应该指出的是,根据选择的运行时和 SDK,你可能需要自己实现这些端点,但对于某些运行时,这些操作是在幕后完成的。无论如何,了解它是如何工作的以及端点在做什么是很好的。
创建一个作为 web 应用的 SSE 服务器
与使用 STDIO 传输相比,最大的不同之处在于我们需要将 SSE 服务器公开为 web 应用程序。根据我们是否使用 Python 或 TypeScript,这意味着我们需要实现必要的 HTTP 端点。
特别对于 Python 来说,我们需要利用支持 异步服务器网关接口(ASGI)的框架。ASGI 是一个规范,它允许 Python 网络框架处理异步和同步代码,使其非常适合现代网络应用程序。让我们看看 Starlette。
Starlette
Starlette 是一个轻量级的 ASGI 框架,我们将用它来构建我们的 SSE 服务器。之前我们提到了需要实现端点,Starlette 将帮助我们实现这些。它提供了一个简单的方法来创建 ASGI 应用程序并处理路由、中间件和其他功能。
让我们看看 Starlette 的工作原理,然后更详细地解释如何使用它构建 SSE 服务器。
使用 Starlette 的典型应用程序将看起来像这样:
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def homepage(request):
return JSONResponse({'hello': 'world'})
app = Starlette(debug=True, routes=[
Route('/', homepage),
])
在前面的代码中,我们做了以下操作:
-
从 Starlette 导入了必要的模块
-
定义了一个简单的
homepage函数,它返回一个 JSON 响应
下一步是运行它。我们可以使用 uvicorn 这样的服务器来运行我们的 Starlette 应用程序:
uvicorn main:app
在这里,我们使用 uvicorn 来运行应用程序。语法是 uvicorn <filename>:<name of app instance>。在这种情况下,文件名是 main.py,应用程序实例是 app。
Starlette 和 MCP
要使用 Starlette 与 MCP,我们可以创建以下应用程序:
from starlette.applications import Starlette
from starlette.routing import Mount, Host
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")
# Mount the SSE server to the existing ASGI server
app = Starlette(
routes=[
Mount('/', app=mcp.sse_app()),
]
)
# or dynamically mount as host
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))
在前面的代码中,我们正在做以下操作:
-
从 Starlette 和 MCP 导入必要的模块。
-
使用
FastMCP创建应用程序的实例。请注意mcp.sse_app()方法。这是创建 SSE 服务器并将其挂载到现有 ASGI 服务器的方法。幕后发生的事情是,它为您创建了 SSE 端点和消息端点。
如果您想深入了解 Python SDK 来查看这是如何实现的,您可以查看 github.com/modelcontextprotocol/python-sdk/blob/e80c0150e1c2e45f66195d3cf7d209be31ce6e5d/src/mcp/server/fastmcp/server.py#L747,您将看到以下代码作为 sse_app() 方法的一部分:
routes.append(
Route(
self.settings.sse_path,
endpoint=sse_endpoint,
methods=["GET"],
)
)
routes.append(
Mount(
self.settings.message_path,
app=sse.handle_post_message,
)
)
如您所见,通过调用 sse_app() 方法,为您创建了 SSE 端点和消息端点。
那么,使用 SSE 的功能是否与 STDIO 的工作方式相同?是的,它们是相同的。您可以使用相同的装饰器和方法来创建功能。唯一的区别是您需要使用 mcp.sse_app() 方法来创建 SSE 服务器并将其挂载到现有的 ASGI 服务器。
使用 SSE 进行测试
然而,当涉及到使用 SSE 的测试工具时,它们之间还是存在一些差异。让我们列出这些差异:
- 检查工具:检查工具是一个命令行工具,允许您使用视觉界面和命令行界面测试您的服务器。STDIO 和 SSE 之间的区别在于您需要将传输类型指定为SSE,将URL指定为
http://<address>:<port>/sse。这是在使用视觉界面时需要做的事情。
对于 CLI 模式,您需要指定一个 URL 而不是运行服务器的方式。因此,以下命令将有效,前提是您的服务器正在localhost:8000上运行:
npx @modelcontextprotocol/inspector --cli http:localhost:8000/sse --method tools/list
让我们看看视觉界面中的区别:

图 4.1 – 检查工具,视觉模式,SSE
快速提示:需要查看此图像的高分辨率版本吗?在下一代 Packt Reader 中打开此书或在其 PDF/ePub 副本中查看。
下一代 Packt Reader以及本书的免费 PDF/ePub 副本包含在您的购买中。扫描二维码或访问packtpub.com/unlock,然后使用搜索栏通过名称查找此书。请仔细核对显示的版本,以确保您获得正确的版本。

注意传输类型设置为SSE,URL设置为http://localhost:8000/sse。记住,在使用 STDIO 时我们没有URL字段,而是有一个指定如何运行服务器的字段?这是 STDIO 和 SSE 在检查工具中的区别。
-
Web 客户端:因为 SSE 服务器运行在 HTTP 上,您可以使用任何 HTTP 客户端来测试它。这包括 Postman、cURL 或甚至您的网络浏览器。要使用 cURL,您可以使用以下命令:
-
获取会话 ID:
export MCP_SERVER="http://0.0.0.0:8000" curl "${MCP_SERVER}/sse"
这将产生如下响应:
```文本 event: endpoint data: http://localhost:5001/messages?session_id=<my session id> ```py -
- 使用会话 ID 向消息端点的服务器发送消息。
确保您在另一个终端实例中发送此命令。此请求的答案将出现在第一个终端中。
export MCP_ENDPOINT="http://localhost:8000/messages?session_id=<my session id>"
-
在发送初始化请求的不同终端中发送消息到服务器,例如列出工具。
curl -X POST "${MCP_ENDPOINT}" -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'
因此,是的,使用 cURL 可以做到这一点,但操作略显繁琐,这使得检查工具成为测试服务器的绝佳选择——同样也适用于 SSE。
创建 SSE 服务器
好的,所以我们理解了 SSE 的概念以及如何构建服务器,甚至测试工具的工作原理以及与 STDIO 的不同之处。现在是我们构建自己的 SSE 服务器的时候了。我们将学习如何做以下事情:
-
设置项目
-
添加服务器代码
-
测试服务器
创建项目
让我们创建一个新项目,如下所示:
-
创建虚拟环境:
python -m venv venv -
激活虚拟环境:
source venv/bin/activate -
安装依赖项:
pip install "mcp[cli]"
在那里,您应该已经准备好开始构建您的 SSE 服务器。
添加服务器代码
现在将以下代码添加到您的项目中:
在 server.py 中添加以下代码:
from starlette.applications import Starlette
from starlette.routing import Mount, Host
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")
# Mount the SSE server to the existing ASGI server
app = Starlette(
routes=[
Mount('/', app=mcp.sse_app()),
]
)
在前面的代码中,我们做了以下操作:
-
从 Starlette 和 MCP 导入了必要的模块。
-
使用应用程序的名称创建了一个
FastMCP实例。特别注意mcp.sse_app()方法帮助我们挂载 SSE 握手和消息路由的端点。
添加功能
下一步是向你的服务器添加功能。如前所述,当涉及到功能时,STDIO 和 SSE 之间没有区别。你可以使用相同的装饰器和方法来创建功能。
在同一文件中添加以下代码:
@mcp.tool()
def add(a: int, b: int) -> int:
"""calc"""
return a + b
你将有机会在作业的后期添加更多功能,但到目前为止,你有一个具有添加两个数字功能的工作服务器。
运行它
下一步是运行服务器。
-
使用命令行启动服务器:
uvicorn main:app --port 3000
语法是 uvicorn <filename>:<app>,其中 <filename> 是你的 Python 文件名,而 <app> 是你的 ASGI 应用程序名。
-
使用命令行启动 检查 工具:
mcp dev server.py
在 UI 中设置以下字段:
-
传输类型:SSE
-
URL:
http://localhost:3000/sse
按照常规尝试你的功能,但现在使用 SSE 服务器。
测试它
我们将以三种不同的方式测试我们的服务器。
-
检查工具,作为可视化界面:这是一种测试服务器并查看其工作方式的好方法
-
带有 CLI 选项的检查工具:CLI 选项直接在命令行中提供响应。这是一种在例如 CI/CD 管道中测试服务器的好方法。
-
使用客户端:在这里,我们将使用 cURL 来测试我们的服务器是否能够响应请求。这是一种快速测试服务器的好方法。
检查工具
让我们尝试使用可视化界面使用检查工具。使用命令行启动检查工具:
在运行服务器的新的终端窗口中运行以下命令。
npx @modelcontextprotocol/inspector
在 UI 中设置以下字段:
-
传输类型:SSE
-
URL:
http://localhost:3000/sse
在 工具 部分中选择 添加 并输入 a 和 b 的值:
5
10
检查工具作为 CLI 选项
对于这个选项,我们将像之前一样使用检查工具,但这次添加了 --cli 选项以在 CLI 模式下运行。记住,我们将直接在命令行中获取响应,而不是在 UI 中:
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:3000/sse --method tools/call --tool-name add --tool-arg a=5 --tool-arg b=10
你应该会看到以下输出:
{
"content": [
{
"type": "text",
"text": "15"
}
],
"structuredContent": {
"result": 15
},
"isError": false
}
cURL 命令
要使用 cURL 进行测试,我们需要进行三个调用:
-
对
/sse的调用。这应该会返回一个会话 ID:curl http://127.0.0.1:3000/sse
你应该会看到类似于以下输出的内容:
event: endpoint
data: /messages/?session_id=53ddee76d5ec4b4aaa9420f24462210a
- 对
/messages的调用,其中包含初始化的 MCP 消息和会话 ID。
应在运行服务器的单独终端中运行以下命令。
curl -X POST "http://127.0.0.1:3000/messages/?session_id=53ddee76d5ec4b4aaa9420f24462210a" -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}'
这将告诉服务器我们已准备好进行通信。
- 功能请求:以下
curl命令是请求列出 MCP 服务器上的工具。
以下命令应在与上一个命令相同的终端中运行。
curl -X POST "http://127.0.0.1:3000/messages/?sessionId=53ddee76d5ec4b4aaa9420f24462210a" -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'
现在,你应该会在初始化连接时使用的第一个终端中看到响应。它应该看起来类似于以下内容:
event:message
data: {"result":{"tools":[{"name":"products","description":"get products by category","inputSchema":{"type":"object","properties":{"category":{"type":"string"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"cart-list","description":"get products in cart","inputSchema":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"cart-add","description":"Adding products to cart","inputSchema":{"type":"object","properties":{"title":{"type":"string"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"add","inputSchema":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]},"jsonrpc":"2.0","id":1}
作为建议,我更喜欢使用检查器工具,因为它提供了更好的体验并且更容易使用。curl 命令更像是低级方法,可以用来获取初始会话 ID。它确实告诉你底层协议是如何工作的,这对于调试可能很有帮助。
摘要
在本章中,我们学习了 SSE 以及如何使用它构建服务器。
我们还学习了在测试工具方面 STDIO 和 SSE 的区别,以及如何使用检查器工具与 SSE 一起使用。区别在于 STDIO 监听 stdin 和 stdout,而 SSE 监听 HTTP 请求。SSE 还可以用来从 LLMs 流式传输响应。
最后,我们构建了自己的 SSE 服务器,并使用检查器工具和 cURL 进行了测试。
在下一章中,我们将探讨另一种名为可流式 HTTP 的传输方式,这是通过 URL 公开服务器时首选的传输方式。
任务 – SSE 服务器
在这个任务中,你将构建一个具有一些功能的 SSE 服务器,以支持以下用例:
-
按类别列出产品
-
将产品添加到购物车
-
列出购物车中的产品
您可以使用本章提供的代码作为起点。
解决方案
你可以在 github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter04/solutions/README.md 访问解决方案。
测验
SSE 传输用于什么?
-
A: 通过 HTTP 公开服务器
-
B: 通过 STDIO 公开服务器
-
C: 启用从 LLMs 的响应流
哪些路由用于 SSE?
-
A:
/mcp -
B:
/sse -
C:
/messages
你可以在 github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter04/solutions/solution-quiz.md 访问解决方案。
参考文献
|
现在解锁此书的独家优惠
扫描此二维码或访问 packtpub.com/unlock,然后通过名称搜索此书。 | 
|
| 注意:在开始之前准备好您的购买发票。 |
| --- |
第五章:可流式 HTTP
在 第四章 中,我们讨论了使用服务器发送事件(SSE)传输构建 MCP 服务器。在那个章节中,您了解到如果您想用户通过 Web 访问您的 MCP 服务器,您不能使用 STDIO,而需要使用 SSE 或,如本章所述,可流式 HTTP。
因此,在本章中,您将学习以下内容:
-
可流式 HTTP 传输
-
为什么应该使用这种传输而不是 SSE
-
如何处理诸如通知和可恢复性等概念
本章涵盖了以下主题:
-
可流式 HTTP 与 SSE 的比较,以及为什么它是新标准
-
可流式 HTTP 在 MCP 中的应用
-
可恢复性
-
通知
-
创建和测试使用可流式 HTTP 的服务器
-
测试服务器
-
测试可恢复性
可流式 HTTP 与 SSE 的比较,以及为什么它是新标准
在选择适合您应用程序的正确技术时,理解 SSE 和可流式 HTTP 之间的差异非常重要。
有一些关键的区别。第一个原因是 MCP 中的 SSE 被认为是已弃用的;您应该使用可流式 HTTP。
那么,为什么这本书中有一个叫做 SSE 的章节呢?原因在于这本书是为您编写的,您既是 MCP 服务器的开发者,也是服务器的消费者,您可能正在编写一个客户端,该客户端针对可能使用 SSE 的现有服务器。简而言之,您应该知道如何处理这两种类型的传输,因为您可能需要与遗留代码一起工作。实际上,github.com/modelcontextprotocol/modelcontextprotocol/discussions/308 上的文章指出,当宣布 SSE 被弃用时,有 20 个参考服务器、超过 50 个官方集成和 186 个社区开发的服务器和客户端正在使用 SSE。这意味着当您为 MCP 开发时,确保您记住您需要处理 SSE 和可流式 HTTP,即使您在可流式 HTTP 中开发新服务器。
好吧,但为什么会有弃用决定呢?好吧,有几个原因说明为什么可流式 HTTP 是一个更好的选择:
-
单端点简单性:客户端和服务器通过单个端点(例如,
/mcp)进行通信,支持POST和GET方法。这简化了实现并减少了连接开销。 -
可恢复性支持:可流式 HTTP 支持使用
Last-Event-ID和Mcp-Session-ID等头部进行可恢复会话,允许客户端重新连接并可靠地恢复流。这是一个强大的功能,当客户端丢失连接时,它们可以从断开前的位置恢复连接并开始接收数据,而不是从头开始。 -
更好的兼容性:它与现代 HTTP 基础设施(如负载均衡器、代理和 API 网关)无缝工作,而 SSE 通常失败或需要解决方案。
-
双向通信:虽然 SSE 是单向的,但可流式 HTTP 可以升级以支持双向流,使其在代理到代理或客户端到服务器交互中更加灵活。
-
未来兼容性:可流式 HTTP 与不断发展的 MCP 标准和社区最佳实践保持一致。它是模块化的、可扩展的,并设计用于无状态或基于会话的模型。无状态服务器更轻量级且更容易构建,能够根据不同场景选择合适的模型是一个有说服力的论点。
MCP 中的可流式 HTTP
好的,通常当我们谈论流式传输时,有些人可能会想到如何将文件分成块,或者 AI 模型如何以更小的部分返回其响应。然而,在 MCP 的上下文中,流式传输更多地关乎我们在遵循可流式标准的同时如何通过 HTTP 传输数据,这意味着使用可流式 HTTP 的客户端通常发送以下Accept头信息:Accept: application/json, text/event-stream。
这告诉服务器客户端可以处理批量 JSON 响应和流式事件(通过 SSE)。服务器可以根据请求类型和上下文选择适当响应模式。
这就是全部的流式传输,只是发送简单的响应吗?其实还有更多,特别是可恢复性。
可恢复性
可恢复性是一个概念,意味着如果客户端在数据传输过程中与服务器断开连接,在重新连接到服务器后,它可以从上次断开的地方恢复数据交换,而不是从头开始。对于长时间运行的操作,这可能会带来变革。技术上,可恢复性可以通过 SSE 和可流式 HTTP 实现,但在 MCP 协议的上下文中,它仅支持可流式 HTTP。
让我们用一个图例来说明:

图 5.1 – 可恢复性
如前图所示,客户端不必从头开始,而可以从中断的地方恢复。这是因为客户端在重新连接时发送mcp-session-id和last-event-id头信息。需要注意的是,在断开连接时,客户端需要优雅地断开,因此它存储这两个头信息以备后用。
那么服务器端需要做些什么来支持这一点呢?嗯,为了使可恢复性工作,服务器需要执行以下操作:
-
创建一个会话存储,这是放置生成消息的地方。
-
设置一个中间件来处理传入请求和传出响应,以确保消息被正确存储并且可以用于恢复会话。具体如何实现这取决于不同的框架:
# 1\. Creates an in-memory store, you should use a persistent store in a production scenario event_store = InMemoryEventStore() # 2\. Create the session manager with our app and event store session_manager = StreamableHTTPSessionManager( app=app, event_store=event_store, # Enable resumability json_response=json_response, ) # 3\. Starts a session manager @contextlib.asynccontextmanager async def lifespan(app: Starlette) -> AsyncIterator[None]: """Context manager for managing session manager lifecycle.""" async with session_manager.run(): logger.info("Application started with StreamableHTTP session manager!") try: yield finally: logger.info("Application shutting down…") # 4\. Creating the web server and assigning a lifespan handler starlette_app = Starlette( debug=True, routes=[ Mount("/mcp", app=handle_streamable_http), ], lifespan=lifespan, )
小贴士:使用AI 代码解释器和快速复制功能增强您的编码体验。在下一代 Packt Reader 中打开此书。点击复制按钮
(1)快速将代码复制到您的编码环境,或点击解释按钮
(2)让 AI 助手为你解释一段代码。



让我们分解一下这段代码:
-
我们首先创建一个会话存储,这将有助于存储和检索消息。
-
然后,我们创建一个会话管理器,它控制对
StreamableHTTPSessionManager类型存储的访问。 -
我们启动会话管理器并创建具有适当生命周期处理器的网络服务器。
-
最后,我们创建网络服务器并分配生命周期处理器。
到这里,一切都已经设置好了,任何客户端现在都可以连接到服务器并开始会话。
好的,现在我们更了解了一些关于可恢复性如何真正改善用户体验的情况,因为客户端可以在断开连接的地方接收到消息,让我们谈谈另一个概念,即通知。
通知
通知并非 Streamable HTTP 的独特概念,也可以用于 SSE。然而,与可恢复性结合使用时,它们突然变得非常强大。让我们首先描述一下它们是什么,然后讨论它们与可恢复性的关系。
通知以多种不同的形式出现,以传达发生了重要的事情。它们是实时更新,并且为了 SDK 的便利,被视为一个独立的事物。这意味着 SDK 通过实现一个处理器的特殊方式来监听通知,正如你很快就会看到的。
这里有一些场景,在这些场景中使用通知是有意义的:
-
状态更新
-
进度通知
-
错误消息
-
信息性消息
例如,客户端发送给服务器的最后一条消息是一个名为notifications/initialized的通知,表示客户端和服务器可以交换非握手消息以及更正常的操作,如列出工具、读取资源等。以下是对notifications/initialized消息的 JSON-RPC 形状:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
生成通知
我们如何生成一个通知?嗯,通知不过是一个 JSON-RPC 消息,而你的 SDK 通常有一个专门的方法来简化发送通知的过程。由于存在不同类型的通知,这更多是关于使用正确的方法和相应的参数。
要生成通知,我们需要对上下文对象进行引用,例如,我们可以将其作为输入参数添加到我们的工具中。一旦我们有了这个引用,我们就可以调用特定的通知方法,如 debug、info、warning 和 error。每种方法都有自己的用途,例如发送调试信息、信息性消息、警告消息和错误消息:
from mcp.server.session import ServerSession
# 1\. Get a hold of the context object
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str,
ctx: ctx: Context[ServerSession, None]) -> str:
# 2\. Select the appropriate method for sending the correct notification type
await ctx.debug(f"Debug: Processing '{data}'")
await ctx.info("Info: Starting processing")
await ctx.warning("Warning: This is experimental")
await ctx.error("Error: (This is just a demo)")
return "Final result"
在此代码中,echo 工具将生成四种不同的通知和一个最终结果。
处理通知
当你作为客户端消费通知时,它们出现在你通常预期的地方之外。它们不是常规消息流的一部分,而是通过它们自己的回调单独处理:
# 1\. Define the message handler
def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,) -> None:
print("Received message:", message)
if isinstance(message, Exception):
raise message
else:
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
elif isinstance(message, RequestResponder):
print("REQUEST_RESPONDER:", message)
else:
print("SERVER_REQUEST:", message)
# 2\. Create the client session and assign message handler to message_handler property
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
await session.initialize()
print("Session initialized, ready to call tools.")
# Call a tool
tool_result = await session.call_tool("echo", {"message": "hello"})
在此客户端中,我们执行以下操作:
-
定义消息处理器;处理器能够接收不同类型的消息并相应地处理它们。
-
创建客户端会话并将消息处理器分配给
message_handler属性。
我们还观察到,正常的特性响应,如调用工具或读取资源,是以正常方式处理的,而通知则是通过消息处理器 message_handler 处理的。
检查器工具中的通知
太好了,现在我们已经对如何设置发送通知和接收通知有了感觉,让我们看看通知在我们的检查器工具中是如何出现的,因为我们需要学会在正确的位置寻找它们。
如果你以视觉模式启动检查器工具,你会看到一个像这样的屏幕:

图 5.2 – 检查器工具
快速提示:需要查看此图像的高分辨率版本?在下一代 Packt Reader 中打开此书或在其 PDF/ePub 版本中查看。快速提示:需要查看此图像的高分辨率版本?在下一代 Packt Reader 中打开此书或在其 PDF/ePub 版本中查看。
下一代 Packt Reader 和此书的免费 PDF/ePub 版本包含在您的购买中。扫描二维码或访问 packtpub.com/unlock,然后使用搜索栏通过名称查找此书。请仔细检查显示的版本,以确保您获得正确的版本。

现在,如果你运行一个工具,你会看到工具结果,你会在其下方看到一个显示通知的区域,如下所示:

图 5.3 – 通知
可恢复的通知
正如我们之前所说的,通知可以通过 SSE 和 Streamable HTTP 发送,但后者尤为重要。想象一下,由于网络连接不稳定,客户端多次断开连接,但多亏了客户端中执行的重新连接逻辑和服务器中的可恢复性支持,最终用户仍然拥有良好的体验,因为他们不会错过通知或正常消息,例如工具响应等。
现在,让我们继续创建服务器。
使用 Streamable HTTP 创建和测试服务器
让我们创建一个服务器,在这个过程中,我们还将集成通知。服务器构建完成后,在下一节中,我们将尝试使用我们可用的不同工具对其进行测试,例如编写我们自己的客户端,使用检查器工具,以及使用 cURL 进行测试。
要创建服务器代码,我们需要做一些事情:
-
将传输设置为可流式传输 HTTP。
-
添加功能。
-
添加在调用工具时发送通知的代码。
太好了,现在我们已经制定了计划,让我们开始实施。以下是我们计划前两点的尝试性实施:
# server.py
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any, List, AsyncGenerator
from mcp.types import (
LoggingMessageNotificationParams,
TextContent
)
# Create an MCP server
mcp = FastMCP("Streamable DEMO")
# 2\. Adding features
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
return f"Here's the file content: {message}"
# 1\. Set up the transport as streamable HTTP.
mcp.run(transport="streamable-http")
要添加发送通知的功能,我们需要将Context对象添加到我们的方法签名中,如下所示:
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
最后,让我们在工具中添加使用Context对象生成通知的部分:
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
# 3\. Send a notification
await ctx.info(f"Processing file 1/3:")
await ctx.info(f"Processing file 2/3:")
await ctx.info(f"Processing file 3/3:")
return f"Here's the file content: {message}"
我们完整的代码现在看起来是这样的:
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any, List, AsyncGenerator
from mcp.types import (
LoggingMessageNotificationParams,
TextContent
)
# Create an MCP server
mcp = FastMCP("Streamable DEMO")
# 2\. Adding features
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
# 3\. Send a notification
await ctx.info(f"Processing file 1/3:")
await ctx.info(f"Processing file 2/3:")
await ctx.info(f"Processing file 3/3:")
return f"Here's the file content: {message}"
# 1\. Set up the transport as streamable HTTP.
mcp.run(transport="streamable-http")
让我们看看如何测试我们的服务器。
测试服务器
和往常一样,我们有不同的选择来测试服务器功能:
-
创建客户端
-
使用检查器工具
-
使用 cURL 进行测试
让我们尝试每个选项。
使用检查器工具
我们在本章前面介绍了检查器工具,但让我们快速回顾一下它是如何与我们的新创建的服务器一起工作的。我们可以像这样调用检查器工具来启动一个 Web 服务器。选择以下字段:
-
传输类型:HTTP
-
服务器 URL:
http://localhost:8000/mcp(如有必要,调整端口号)
点击连接按钮连接到服务器,以便使用其功能,然后输入以下内容:
npx @modelcontextprotocol/inspector
如您所见,它与测试 SSE 服务器非常相似;只需更改传输类型,并确保您的 URL 以/mcp结尾而不是/sse。
使用 cURL 进行测试
我们在上一章介绍了 cURL;我们也可以在这里使用它。要获取会话 ID,您需要发送一个initialize消息。消息应包含您支持的功能,例如tools。
这里是您需要发送的消息:
curl -X POST "http://127.0.0.1:8000/mcp" -H "Accept: text/event-stream, application/json" -H "Content-Type: application/json" -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {} }, "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } }
}'
注意您需要发送Accept头部和内容类型。记下返回的会话 ID,因为您将在所有剩余的调用中使用它。
创建第二个终端窗口,然后运行以下命令,但将mcp-session-id值替换为上一步中获取的会话 ID:
curl -X POST "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "`mcp-session-id`: 39a0b504364140ce97d8eded79b1c244" -d '{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}'
注意到会话 ID 不再是名为session_id的查询参数,而是一个名为mcp-session-id的头部值。您发送的消息是notifications/initialized类型的通知,这意味着它是握手过程的最后一条消息。在这条消息之后,我们现在可以做一些更正常的事情,比如列出工具、调用它们等等,所以让我们继续这样做。
替换mcp-session-id的值,并继续使用第二个终端窗口,然后运行以下命令:
curl -X POST "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "mcp-session-id: 39a0b504364140ce97d8eded79b1c244" -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": { "message": "chris" }
}
}'
在这个tools/call类型的消息中,我们使用chris参数调用特定的工具echo,我们应该看到类似于以下工具响应:
event: message
data: {"method":"notifications/message","params":{"level":"info","data":"Processing file 1/3:"},"jsonrpc":"2.0"}
event: message
data: {"method":"notifications/message","params":{"level":"info","data":"Processing file 2/3:"},"jsonrpc":"2.0"}
event: message
data: {"method":"notifications/message","params":{"level":"info","data":"Processing file 3/3:"},"jsonrpc":"2.0"}
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Here's the file content: chris"}],"structuredContent":{"result":"Here's the file content: chris"},"isError":false}}
第二个终端窗口中的响应表明我们收到了三个通知和一个最终的工具响应,这表明我们的 MCP 服务器按预期工作。
如您所见,Inspector 和 cURL 都是用于测试服务器的好工具。然而,构建客户端可能是我们将 MCP 服务器集成到工作解决方案中的方法,所以让我们在下一节中看看这一点。
创建一个处理通知的客户端
让我们谈谈客户端。客户端通常需要被指示也处理通知。这被视为除了正常功能之外额外需要的东西。让我们制定一个实施计划。让我们首先定义我们的计划:
-
创建一个可流式传输的 HTTP 传输和客户端。
-
设置通知处理器。
-
调用一个工具。
解决计划中的第一个问题,我们的代码如下所示:
# 1\. Create a streamable HTTP transport and client
async with streamablehttp_client(f"http://localhost:{port}/mcp") as (
read_stream,
write_stream,
session_callback,
):
# Create a session using the client streams
async with ClientSession(
read_stream,
write_stream
) as session:
在这里,我们使用streamablehttp_client函数创建一个可流式传输的 HTTP 客户端。然后,我们通过初始化ClientSession创建一个客户端实例。
为了支持传入的通知,我们需要通过将一个函数分配给一个名为message_handler的属性来配置客户端会话:
# 2\. Set up a notification handler
async def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
) -> None:
print("Received message:", message)
if isinstance(message, Exception):
raise message
else:
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
elif isinstance(message, RequestResponder):
print("REQUEST_RESPONDER:", message)
else:
print("SERVER_REQUEST:", message)
# omitted code for brevity
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
注意message_handler是如何分配给ClientSession的,以及它指向一个名为message_handler的函数。该函数处理传入的消息并将它们打印出来。
就这样,这就是我们创建可流式传输的 HTTP 客户端并支持传入通知所需的所有内容。让我们看看完整的代码:
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
import asyncio
from typing import Optional, Dict, Any, List
import mcp.types as types
from mcp.types import (
LoggingMessageNotificationParams,
TextContent,
)
from mcp.shared.session import RequestResponder
port = 8000
# I get normal messages, notifications, and exceptions
# 2\. Set up a notification handler
async def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
) -> None:
print("Received message:", message)
if isinstance(message, Exception):
raise message
async def main():
print("Starting client...")
# 1\. Create a streamable HTTP transport and client
async with streamablehttp_client(f"http://localhost:{port}/mcp") as (
read_stream,
write_stream,
session_callback,
):
# 2\. Set up a notification handler
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
# Initialize the connection
await session.initialize()
# 3\. Call a tool
results = []
tool_result = await session.call_tool("echo",
{"message": "hello"})
print("Tool result:", tool_result)
asyncio.run(main())
现在,如果我们运行客户端,它会告诉我们通知确实是通知:
NOTIFICATION: root=LoggingMessageNotification(method='notifications/message', params=LoggingMessageNotificationParams(meta=None, level='info', logger=None, data='Processing file 3/3:'), jsonrpc='2.0')
结论是,通过使用message_handler方法,我们可以捕获从服务器发送的所有消息、通知和异常。这使我们能够适当地处理它们并向用户提供反馈。
测试可恢复性
SDK 中实际上有一个关于可恢复性的示例实现。让我们尝试这段代码并看看它有何不同。您可以在github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter05/solutions/resumability/README.md找到它的简化版本。
代码所做的是使用一个工具process-files定义一个服务器。在调用该工具时,你会收到三个通知和一个最终响应。测试服务器的最简单方法是通过使用 cURL。使用 cURL,我们可以执行握手过程,调用工具,甚至进行所需的定制请求,以便重放事件。让我们一步一步来:
-
首先启动服务器。
-
在第二个终端中启动并使用以下有效载荷调用
curl,以交换客户端和服务器都有的功能:curl -X POST "http://127.0.0.1:8000/mcp" -H "Accept: text/event-stream, application/json" -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {}, "logging": {} }, "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } }'
检查第一个终端窗口中的终端响应。您应该看到服务器显示会话 ID;复制该字段以供以后使用。
-
通过发送一个
initialized通知来结束服务器-客户端握手;将mcp-session-id的值替换为上一步中复制的值:curl -X POST "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "mcp-session-id: 957f11af-4766-4c1c-a1f2-5bd6776cca6a" -d '{ "jsonrpc": "2.0", "method": "notifications/initialized" }' -
通过在第二个终端窗口粘贴以下命令来调用工具,确保首先替换
"mcp-session-id"的值:curl -X POST "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "mcp-session-id: 957f11af-4766-4c1c-a1f2-5bd6776cca6a" -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "process-files", "arguments": { "message": "chris" } } }'
到目前为止,你应该在第二个终端窗口中看到一系列通知和最终结果,如下所示:
event: message
id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976937_z6m5xbyc
data: {"method":"notifications/message","params":{"level":"info","data":"sales1.csv processed"},"jsonrpc":"2.0"}
event: message
id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976940_meh2n52f
data: {"method":"notifications/message","params":{"level":"info","data":"sales2.csv processed"},"jsonrpc":"2.0"}
event: message
id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976943_e3v55tmn
data: {"method":"notifications/message","params":{"level":"info","data":"sales3.csv processed"},"jsonrpc":"2.0"}
event: message
id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976946_sgpvardt
data: {"result":{"content":[{"type":"text","text":"Files processed: 3"}]},"jsonrpc":"2.0","id":1}
让我们关注以下消息,一个表示我们处于文件 2/3 的通知。想象一下,我们进入了一个隧道并失去了网络连接。注意 ID,因为这个 ID 是你在失去连接之前看到的最后一个 ID:
event: message
id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976940_meh2n52f
data: {"method":"notifications/message","params":{"level":"info","data":"sales2.csv processed"},"jsonrpc":"2.0"}
-
在我们的最后一步,我们需要向
/mcp端点发送一个GET请求,并在头部传递会话 ID 和最后事件 ID。这应该会导致服务器重新播放我们错过的所有消息,这些消息应该是sales3.csv文件通知和最终工具结果。记住在将以下内容粘贴到第二个终端窗口之前,替换掉mcp-session-id和last-event-id:curl "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "mcp-session-id: 957f11af-4766-4c1c-a1f2-5bd6776cca6a" -H "last-event-id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976940_meh2n52f"
将此结果粘贴意味着你应该在第二个终端窗口中看到以下内容:
event: message
id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976943_e3v55tmn
data: {"method":"notifications/message","params":{"level":"info","data":"sales3.csv processed"},"jsonrpc":"2.0"}
event: message
id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976946_sgpvardt
data: {"result":{"content":[{"type":"text","text":"Files processed: 3"}]},"jsonrpc":"2.0","id":1}
我们缺失的一个通知和工具结果!这不是很棒吗?我们没有丢失任何消息。
应该说,尽管如此,如果你编写的代码可以利用可重放性,那么当失去网络连接时,你应该监听浏览器事件,这样你就有机会保存会话 ID 和最后事件 ID,并记得用 GET /mcp 而不是 POST /mcp 调用服务器,因为后者会启动一个新的会话。
此外,如果你使用我使用的事件存储,请记住它不适合生产,并且它需要在数据库或类似的地方持久化消息,才能被认为是生产就绪的。
摘要
在本章中,我们探讨了可流式 HTTP 的概念以及它与 SSE 的区别。我们了解到流式传输允许实时数据传输,这对于需要立即访问数据的程序很有益,例如现场活动或大文件。
此外,我们还实现了一个支持可流式 HTTP 的 MCP 服务器,并演示了如何使用 MCP SDK 消费流式数据。我们还讨论了通知在向客户端提供实时更新中的重要性以及如何有效地处理它们。
在下一章中,我们将解释如何使用低级服务器 API,因为有些用例你可能想要这样做。
作业
对于这个作业,我们再次关注电子商务。想象一下,服务器上有需要处理的 CSV 文件。处理发生在服务器上的所有 CSV 文件逐个作为输入发送到 Web API 时。这个想法是这个过程将通过调用一个工具来启动。想象以下程序输出:
Type command> process-files
Notification: info - sales.csv processed
Notification: info - sales.csv processed
Notification: info - sales.csv processed
Files processed: 3
Type command> process-files
Files processed: 0
本作业的目的是学习如何使用通知。文件可以是内存中的条目列表,在处理时会移除。您需要创建一个服务器,其中包含要处理的文件,以及一个可以处理输入命令的客户端。
解决方案
问答
使用可流式 HTTP 的主要好处是什么?
-
A: 它允许更快地访问数据
-
B: 它需要更少的服务器资源
-
C: 它比其他协议更安全
可流式 HTTP 与 SSE 有何不同?
-
A: 可流式 HTTP 使用基于文本的格式,而 SSE 使用 JSON。
-
B: 可流式 HTTP 可以发送二进制数据,而 SSE 仅限于文本。
-
C: 可流式 HTTP 是单向的,而 SSE 是双向的。
参考文献
-
流式传输:
mcp-framework.com/docs/Transports/http-stream-transport/|
现在解锁本书的独家优惠
扫描此二维码或访问
packtpub.com/unlock,然后通过书名搜索本书。 |![]()
|| 注意:在开始之前准备好您的购买发票。* |
| --- |
第六章:高级服务器
在第三章中,你看到了如何构建 MCP 服务器。然而,还有另一种构建这些服务器的方法——即使用更高级的方法。使用这种更高级方法的原因是你想要有更多的控制。
在本章中,你将学习以下内容:
-
使用上下文管理器来管理你的服务器生命周期
-
改进你的服务器架构
-
理解对 MCP 服务器的低级访问
本章涵盖了以下主题:
-
为什么选择低级方法?
-
上下文管理器
-
MCP 服务器中的上下文管理器
-
低级访问
-
组织你的架构
为什么选择低级方法?
到目前为止,你可能正在想,“我为什么要这样做?之前的方法是如此简单!”好吧,有几个原因你可能想使用这种方法。你可能想做以下事情:
-
使用上下文管理器来管理你的服务器生命周期:在这里,你可以做一些事情,比如连接到数据库或其他与你的服务器相连的服务。通过更多地控制服务器的生命周期,你可以确保当服务器不再需要时,服务器被正确地初始化和清理。
-
改进你的服务器架构:对服务器构建方式有更多控制,允许在注册工具和资源以及处理传入请求方面有更多自由。这种增加的控制允许你以更易于维护和扩展的方式组织代码。本章将向你展示如何使用低级服务器和普通 MCP 服务器来组织你的代码。在两种情况下都可以创建一个干净的架构。然而,可以争辩说,低级服务器方法更为干净,因为你不必传递服务器实例。你将在本章后面了解更多关于最后那个陈述的含义。
-
在某些情况下前进的唯一方式:有些情况下,当你处理某些特性时,除了低级方法之外,没有其他前进的方式。这在使用第九章(关于采样)和第十章(关于启发式)时是正确的。
让我们深入探讨更低级的方法,这样你知道它是什么样子,以便你可以选择最适合你项目的方案。
上下文管理器
那么,什么是上下文管理器呢?上下文管理器是一种结构,它允许你在需要的时候精确地分配和释放资源。使用上下文管理器最常见的方式是使用with语句,这确保了即使在发生错误的情况下,资源也会被正确清理。通过使用上下文管理器,你的代码变得更干净、更易读。让我们看看一个简单的例子:
with Database_connection() as conn:
# Use the connection
result = conn.execute("SELECT * FROM table")
for row in result:
print(row)
使用 contextlib 创建上下文管理器
使用上下文管理器的另一种方法是使用contextlib模块(有一个对应的 NPM 库叫做contextlib)创建一个自定义上下文管理器。这允许你创建上下文管理器而不需要定义一个类。以下是一个例子:
import contextlib
@contextlib.contextmanager
def database_connection():
conn = connect_to_database()
try:
yield conn # This is where the resource is provided to the block
finally:
close_connection(conn) # Cleanup happens here
这里,你可以看到如何将DatabaseConnection类替换为一个使用contextlib.contextmanager装饰器的database_connection()函数。yield语句提供了资源给代码块,并且在代码块退出时执行yield语句之后的代码,确保了正确的清理。这比之前的例子更好吗?嗯,至少你输入的代码更少。
实现一个上下文管理器
如果你想知道如何实现一个上下文管理器,因为你好奇或者因为你不想再添加另一个依赖,以下是你可以这样做的方法:
class DatabaseConnection:
def __enter__(self):
self.conn = self.connect_to_database()
return self.conn
def __exit__(self, exc_type, exc_value, traceback):
self.close_connection(self.conn)
def connect_to_database(self):
# Logic to connect to the database
pass
def close_connection(self, conn):
# Logic to close the database connection
pass
在前面,DatabaseConnection类通过定义__enter__和__exit__方法实现了上下文管理器协议。当进入with块时调用__enter__方法,并返回资源(在这种情况下,是一个数据库连接)。当块退出时调用__exit__方法,并处理任何必要的清理,例如关闭连接。我们可以这样调用它:
with DatabaseConnection() as conn:
# Use the connection
result = conn.execute("SELECT * FROM table")
for row in result:
print(row)
没有上下文管理器,你的代码看起来会是这样:
conn = DatabaseConnection().connect_to_database()
try:
# Use the connection
result = conn.execute("SELECT * FROM table")
for row in result:
print(row)
finally:
DatabaseConnection().close_connection(conn)
想象一下如果你在finally块中忘记关闭连接会发生什么?你可能是一个非常自律的程序员,总是记得关闭连接,但在更大的代码库中,很容易忘记。上下文管理器通过确保资源总是被正确清理来帮助你避免这样的陷阱。
让我们看看在 MCP 服务器上下文中如何使用上下文管理器。
MCP 服务器中的上下文管理器
MCP 允许你控制你资源的生命周期管理。让我们看看一些代码:
async def load_settings() -> dict:
"""Load settings from a configuration file."""
# Simulate loading settings
return {"setting1": "value1", "setting2": "value2"}
@asynccontextmanager
async def server_lifespan(server: Server) -> AsyncIterator[dict]:
"""Manage server startup and shutdown lifecycle."""
# Initialize resources on startup
db = await Database.connect()
settings = await load_settings()
try:
yield {"db": db, "settings": settings}
finally:
# Clean up on shutdown
await db.disconnect()
在这里,我们做了几件事情:
-
定义了一个异步上下文管理器
server_lifespan,用于管理服务器的生命周期 -
在服务器启动时初始化资源,如数据库连接和设置,在服务器关闭时清理它们
-
将这些资源暴露给服务器的请求上下文,使得处理器可以轻松访问它们
说到处理器,让我们看看在以下代码中你如何在服务器的处理器中访问这些资源:
# Pass lifespan to server
server = Server("example-server", lifespan=server_lifespan)
# Access lifespan context in handlers
@server.call_tool()
async def query_db(name: str, arguments: dict) -> list:
ctx = server.request_context
db = ctx.lifespan_context["db"]
settings = ctx.lifespan_context["settings"]
# TODO: Use the database connection and settings
return await db.query(arguments["query"])
在前面的代码中,我们做了以下几件事情:
-
定义了一个工具
query_db,它访问在server_lifespan上下文管理器中初始化的资源。 -
通过使用
db = ctx.lifespan_context["db"]和settings = ctx.lifespan_context["settings"]代码从服务器的请求上下文的lifespan_context中访问数据库连接和设置。
太好了,现在我们理解了上下文管理以及它的存在原因。让我们在下一节中更详细地看看低级访问。
低级访问
让我们回顾一下我们最初是如何构建服务器的,这样我们就可以轻松地将其与低级访问的不同之处进行比较。以下是使用高级 API 构建简单 MCP 服务器的方法:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo")
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message}"
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
FastMCP类是你用来实例化服务器的东西——在这个例子中,一个名为mcp的实例。然后我们使用@mcp来定义资源、工具和提示。
这是一种高级构建服务器的方法,但如果你想要更多控制服务器构建的方式呢?以下是使用低级访问来实现这一点的办法。
让我们看看如何注册功能的不同之处。在过去,你可能习惯于使用与特定工具或资源等相关的装饰器。在低级服务器中有什么不同之处呢?你需要自己处理所有请求。而不是一次处理一个工具或资源,你需要在同一个地方处理与工具、资源和提示相关的所有请求。
首先,导入方式不同。看看我们是怎样从from mcp.server.lowlevel和Server导入的:
from mcp.server.lowlevel import Server
接着是实例化服务器,如下所示:
server = Server("low-level-server")
而不是使用@mcp.tools()或@mcp.resource(),你可以使用处理程序如@server.list_tools()和@server.call_tool()来注册。这是一个巨大的区别。区别在于,在低级服务器中,你不需要为每个功能都有一个装饰器,你需要自己处理所有工具请求,无论是调用工具还是列出工具、资源或提示。这意味着@server.list_tools()负责列出所有工具,你需要自己实现这个逻辑,而高级服务器会为你做这件事。以下是如何实现@server.list_tools()的一个例子:
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
tool_list = []
print(tools)
for tool in tools.tools.values():
tool_list.append(
types.Tool(
name=tool["name"],
description=tool["description"],
inputSchema=tool["input_schema"],
)
)
return tool_list
在前面的代码中,我们做了以下几件事:
-
定义了一个处理程序,
handle_list_tools,它响应list_tools请求。 -
使用了
@server.list_tools()装饰器将此处理程序注册到服务器上。 -
遍历
tools.tools.values()来收集所有工具,并以所需格式返回它们——在这个例子中,是一个types.Tool对象的列表。在这种情况下,tools.tools是一个包含我们创建的服务器上注册的所有工具的字典,所以它看起来像这样:{ "tools": { "echo_tool": { "name": "echo_tool", "description": "Echo a message as a tool", "input_schema": {"type": "object", "properties": {"message": {"type": "string"}}} } } }
这不是增加了工作量吗?实际上,让我们在下一节中探讨一下,这可能是组织代码的一个很好的方法。
组织你的架构
使用低级服务器的一个巨大优势是你可以控制你服务器的架构。你可以以对你项目有意义的方式组织你的代码。例如,你可以在名为tools的文件夹中定义所有你的工具。工具也不需要知道服务器实例。听起来很有希望,对吧?让我们看看下一步。
你也可以用高级服务器很好地组织你的代码,但通常会更混乱,因为你需要传递服务器实例,就像我们在本章最初所说的那样。
到目前为止,你已经在高级服务器中这样定义了 MCP 服务器功能:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo")
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
@mcp.tool()
def add_tool(a: int, b: int) -> int:
"""Add two numbers as a tool"""
return a + b
@mcp.tool()
def subtract_tool(a: int, b: int) -> int:
"""Subtract two numbers as a tool"""
return a - b
@mcp.tool()
def multiply_tool(a: int, b: int) -> int:
"""Multiply two numbers as a tool"""
return a * b
@mcp.tool()
def divide_tool(a: int, b: int) -> float:
"""Divide two numbers as a tool"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
定义一个服务器及其功能,如前面的示例,并没有什么不妥。然而,你可能倾向于保持你的入口点文件相对空(除了服务器定义之外)并定义所有功能在该文件中。因此,你可能会采用如下所示的项目结构:
project/
├── server.py
├── tools.py
然后,你的server.py可能看起来像这样:
# server.py
from mcp.server.fastmcp import FastMCP
import tools
mcp = FastMCP("Echo")
tools.register_tools(mcp)
# code for running the server
你的tools.py可能看起来像这样:
from mcp.server.fastmcp import FastMCP
def register_tools(mcp: FastMCP):
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
@mcp.tool()
def add_tool(a: int, b: int) -> int:
"""Add two numbers as a tool"""
return a + b
@mcp.tool()
def subtract_tool(a: int, b: int) -> int:
"""Subtract two numbers as a tool"""
return a - b
@mcp.tool()
def multiply_tool(a: int, b: int) -> int:
"""Multiply two numbers as a tool"""
return a * b
@mcp.tool()
def divide_tool(a: int, b: int) -> float:
"""Divide two numbers as a tool"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
你甚至可以为每个工具创建专门的文件:
project/
├── server.py
├── tools/
│ ├── echo.py
│ ├── add.py
│ ├── subtract.py
│ ├── multiply.py
│ └── divide.py
所以,是的,你可以以对你项目有意义的方式组织你的代码。很难摆脱传递服务器实例的需要。应该指出的是,高级服务器确实提供了一个create_tool()函数,允许你创建工具,而无需使用@mcp.tool()装饰器。然而,这是作者的观点,使用低级服务器来做这个目的更简单。让我们在下一节看看如何做到这一点。
在低级服务器中构建工具列表响应
那么,让我们看看低级访问是否可以帮助我们进一步改进我们的架构。目标是拥有一个服务器,它可以以易于维护和扩展的方式注册工具、资源和提示。
因此,到目前为止,我们关于低级服务器的说法如下:
-
代替使用
FastMCP,你使用mcp.server.lowlevel中的Server类 -
你可以使用处理程序如
@server.list_tools()和@server.call_tool()来注册功能,以处理列出所有工具和处理所有工具调用
让我们更详细地研究这些处理程序以及我们如何构建它们,看看我们如何利用它们:
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
tool_list = []
tool_list.append(
types.Tool(
name=tool["name"],
description=tool["description"],
inputSchema=tool["input_schema"],
)
)
return tool_list
在这里,我们观察到返回类型是types.Tool对象的列表。这在代码中得到了反映,我们像这样将一个types.Tool对象追加到tool_list中:
tool_list.append(
types.Tool(
name=tool["name"],
description=tool["description"],
inputSchema=tool["input_schema"],
)
)
组织你的代码并创建工具和模式
现在我们知道了如何注册工具,让我们看看我们如何组织代码。目标是实现以下内容:
-
为每个工具创建一个文件,这样我们可以轻松地管理代码
-
在一个地方注册所有工具
听起来像是一个伟大的目标,对吧?谁不想有可维护性?让我们创建一个如下所示的文件夹结构:
server.py
tools/
├── __init__.py
├── add.py
我们看到的是,我们有一个server.py文件,它将成为我们服务器的入口点,一个包含所有工具的tools/文件夹。tools/init.py文件用于注册收集所有工具,server.py和@mcp.list_tools()将用于注册所有工具。让我们首先看看__init__.py:
from .add import tool_add
tools = {
tool_add["name"] : tool_add
}
这看起来超级简单——只是一个字典,它从add.py文件中导入tool_add函数并将其添加到tools字典中。现在让我们看看add.py文件:
# add.py
from .schema import AddInputModel
async def add_handler(args) -> float:
try:
# Validate input using Pydantic model
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
# TODO: add Pydantic, so we can create an AddInputModel and validate args
"""Handler function for the add tool."""
return float(input_model.a) + float(input_model.b)
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
在前面的代码中,我们做了以下操作:
-
导入了
AddInputModel并将其作为input_schema添加 -
定义了一个
add_handler函数,它接受参数并返回它们的和 -
创建了一个包含工具名称、描述、输入模式和处理器函数的
tool_add字典
如您所见,没有导入任何 MCP 相关的内容,因此看起来相当干净。
最后,让我们看看schema.py文件:
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
在这里,我们使用Pydantic库来定义输入模型。随着我们向解决方案中添加更多工具,我们可以扩展此文件以包含新类型。
处理被调用的工具
到目前为止,你已经看到了我们如何处理要求列出所有工具的调用。但还有一个我们需要处理的情况,即当客户端尝试调用工具时。为此,我们需要对传入的工具调用请求执行以下操作:
-
识别要调用的工具。
-
解析参数,并在解析过程中验证它们。这正是我们的 Pydantic 模式将帮助我们的地方。
让我们从服务器上的请求开始:
# server.py
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
pass
注意我们如何需要引用@server.call_tool装饰器,以及返回类型需要是list[types.TextContext]。
接下来,让我们看看我们能否识别出正确的工具:
# server.py
if name not in tools.tools:
raise ValueError(f"Unknown tool: {name}")
tool = tools.tools[name]
好的,让我们准备调用工具上的处理程序并构造对调用客户端的响应:
# server.py
try:
result = await tool"handler"
except Exception as e:
raise ValueError(f"Error calling tool {name}: {str(e)}")
return [
types.TextContent(type="text", text=str(result))
]
注意我们如何在tool对象上调用handler属性,并将arguments作为参数。然而,工具上的处理程序看起来是什么样子,它是如何工作的呢?
# add.py
from .schema import AddInputModel
async def add_handler(args) -> float:
try:
# Validate input using Pydantic model
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
print (f"Adding {args['a']} and {args['b']}")
"""Handler function for the add tool."""
return float(args['a']) + float(args['b'])
在这里,我们使用try-catch将args传递给AddInputModel类。它接受一个字典,通过将其作为**args传递,我们将字典解包为关键字参数。如果输入无效,将引发ValueError,并显示一条消息说明出了什么问题。这样,您可以确保工具的输入始终有效,并符合预期的模式。
太好了,我们现在已经成功处理了列出所有工具和调用特定工具的情况,同时确保输入参数的某些验证也发生了。
我相信你可以进一步改进这个设置,但与最初的设置相比,这已经好多了。
摘要
在本章中,你学习了如何使用低级服务器为你的 MCP 服务器创建一个更易于维护的架构。你看到了如何注册工具、处理请求和使用模式验证输入。这种方法允许你轻松添加新工具并管理现有工具,使你的服务器更加灵活且易于维护。
在我们下一章中,我们将介绍如何构建可以与我们的 MCP 服务器交互的客户端。
作业
让我们看看我们能否组织我们在第三章中创建的电子商务服务器。以下是代码供参考。创建一个工具目录 - 使用 Pydantic 和低级 API。
# server.py
from mcp.server.fastmcp import FastMCP
import uuid
# Create an MCP server
mcp = FastMCP("Demo")
class Customer:
def __init__(self,id: int, name: str, email: str):
self.id = id
self.name = name
self.email = email
class Category:
def __init__(self, name: str, description: str):
self.id = uuid.uuid4()
self.name = name
self.description = description
class Product:
def __init__(self, name: str, price: float, description: str):
self.name = name
self.price = price
self.description = description
class CartItem:
def __init__(self, cart_id: int, product_id: int, quantity: int):
if cart_id != 0:
self.cart_id = cart_id
else:
self.cart_id = uuid.uuid4()
self.product_id = product_id
self.quantity = quantity
class Cart:
def __init__(self, cart_id: int, customer_id: int):
if cart_id != 0:
self.cart_id = cart_id
else:
self.cart_id = uuid.uuid4()
self.customer_id = customer_id
class Order:
def __init__(self, order_id: int, customer_id: int):
if order_id != 0:
self.order_id = order_id
else:
self.order_id = uuid.uuid4()
self.customer_id = customer_id
products = [
Product("Product 1", 10.0, "Description of Product 1"),
Product("Product 2", 20.0, "Description of Product 2"),
Product("Product 3", 30.0, "Description of Product 3")
]
orders = [
Order(1, 101),
Order(0, 101),
Order(0, 102)
]
carts = []
customers = [
Customer(1, "Customer 1", "email")
]
categories = [
Category("Category 1", "Description of Category 1"),
Category("Category 2", "Description of Category 2"),
Category("Category 3", "Description of Category 3")
]
product_catalog = [
{
"name": "Product 1",
"price": 10.0,
"description": "Description of Product 1",
"category_id": 1
},
{
"name": "Product 2",
"price": 20.0,
"description": "Description of Product 2",
"category_id": 2
},
{
"name": "Product 3",
"price": 30.0,
"description": "Description of Product 3",
"category_id": 3
}
]
# get orders
@mcp.tool()
def get_orders(customer_id:int = 0) -> [Order]:
"""get all orders"""
if customer_id != 0 and not any(customer.id == customer_id for customer in customers):
raise ValueError(f"Invalid customer_id: {customer_id}")
filtered_orders = orders
if customer_id != 0:
filtered_orders = [order for order in orders if order.customer_id == customer_id]
return [{"type": "text", "name": f"ID: {order.order_id},customer: {order.customer_id}"} for order in filtered_orders]
# get order by id
@mcp.tool()
def get_order(order_id:int) -> Order:
"""get order by id"""
for order in orders:
if order.order_id == order_id:
return {"type": "text", "name": f"ID: {order.order_ id},customer: {order.customer_id}"}
return None
# place order
@mcp.tool()
def place_order(customer_id:int) -> Order:
"""place order"""
if customer_id != 0 and not any(customer.id == customer_id for customer in customers):
raise ValueError(f"Invalid customer_id: {customer_id}")
new_order = Order(0, customer_id)
orders.append(new_order)
return {"type": "text", "name": f"ID: {new_order.order_id},customer: {new_order.customer_id}"}
# get carts
@mcp.tool()
def get_cart(customer_id:int) -> [Cart]:
"""get a singular cart"""
if customer_id != 0 and not any(customer.id == customer_id for customer in customers):
raise ValueError(f"Invalid customer_id: {customer_id}")
cart = next((cart for cart in carts if cart.customer_id == customer_ id), None)
if cart:
return {"type": "text", "name": f"ID: {cart.cart_id},customer: {cart.customer_id}"}
else:
return None
# get cart items
@mcp.tool()
def get_cart_items(cart_id:int) -> [CartItem]:
"""get cart items"""
cart_items = [item for item in carts if item.cart_id == cart_id]
return [{"type": "text", "name": f"ID: {item.cart_id},product: {item. product_id},quantity: {item.quantity}"} for item in cart_items]
# add to cart
@mcp.tool()
def add_to_cart(cart_id:int, product_id:int, quantity:int) -> CartItem:
"""add to cart"""
new_cart_item = CartItem(cart_id, product_id, quantity)
carts.append(new_cart_item)
return {"type": "text", "name": f"ID: {new_cart_item.cart_id},product: {new_cart_item.product_id},quantity: {new_cart_item.quantity}"}
# tool, all products
@mcp.tool()
def get_all_products() -> [Product]:
"""Get all products"""
return [{"type": "text", "name": f"ID: {product.name},price: {product. price},description: {product.description}"} for product in products]
# tool, product by id
@mcp.tool()
def get_product(product_id: int) -> Product:
"""Get product by ID"""
for product in products:
if product.name == product_id:
return {"type": "text", "name": f"ID: {product.name},price: {product.price},description: {product.description}"}
return None
# tool, all categories
@mcp.tool()
def get_all_categories() -> [Category]:
"""Get all categories"""
return [{"type": "text", "name": f"ID: {category.name},description: {category.description}"} for category in categories]
# tool, all customers
@mcp.tool()
def get_all_customers() -> [Customer]:
"""Get all customers"""
return [{"type": "text", "name": f"ID: {customer.id},name: {customer. name},email: {customer.email}"} for customer in customers]
解决方案
您可以通过github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter06/solutions/README.md访问解决方案。
问答
使用低级服务器的优点有哪些?
-
A: 你使用的内存更少
-
B: 你可以更好地控制请求的处理方式
-
C: 你可以定义自己的传输
|
现在解锁这本书的独家优惠
扫描此二维码或访问packtpub.com/unlock,然后通过书名搜索此书。 | 
|
| 注意:在开始之前准备好您的购买发票。* |
| --- |
第七章:构建客户端
要消费 MCP 服务器,你需要某种形式的客户端。例如,你可以使用Claude 桌面或虚拟工作室代码(VS Code),因为它们具有消费 MCP 服务器和处理功能发现的能力,并且能够使用它们。也有情况下你可能需要自己编写的客户端。这种情况的一个好例子是当你想在应用程序中构建 AI 功能时。例如,想象一下,你有一个电子商务应用程序,并希望有一个 AI 改进的搜索。MCP 服务器将是一个单独的应用程序,而客户端将集成到电子商务应用程序中。
考虑到这一点,让我们探讨如何构建客户端以及它包含的内容。
在本章中,你将学习以下内容:
-
使用 STDIO 和 SSE 传输构建客户端
-
消费 MCP 服务器及其功能
-
利用 LLM 来增强客户端体验
本章涵盖了以下主题:
-
构建客户端
-
练习:构建客户端
-
带有 LLM 的客户端
-
与 LLM 一起工作
-
练习:集成 LLM
构建客户端
那么,构建客户端需要哪些要素?在宏观层面,我们需要做以下事情:
-
设置客户端以连接到服务器。
-
列出功能。
-
选择一个功能来使用。
-
提示用户输入参数。
-
展示结果。
太好了,现在我们已经了解了高级步骤,让我们看看我们是否可以在接下来的练习中构建它,你可以随时跟随代码编写。
练习:构建客户端
在这个练习中,你将构建一个连接到服务器并使用其功能的客户端。你将使用 SDK 来构建客户端并调用服务器。客户端将是一个简单的命令行应用程序,允许你选择一个功能并提供其参数。然后客户端将调用服务器并显示结果。
设置客户端以连接到服务器
让我们首先创建建立到服务器连接所需的客户端代码:
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="mcp", # Executable
args=["run", "server.py"], # Optional command line arguments
env=None, # Optional environment variables
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write
) as session:
# Initialize the connection
await session.initialize()
# list features
if __name__ == "__main__":
import asyncio
asyncio.run(run())
在前面的代码中,我们做了以下事情:
-
创建了一个
StdioServerParameters对象,该对象指定了运行服务器和任何可选的命令行参数的命令。我们这样做的原因是因为服务器将与客户端同时运行,因此我们需要指定如何运行它。 -
定义了一个
run函数,该函数创建一个ClientSession对象并初始化到服务器的连接。在run函数内部,我们很快将添加列出和调用功能的代码。
列出功能
到目前为止,我们已经设置了客户端以连接到服务器。现在,让我们添加列出服务器上可用功能的代码。这取决于功能类型的不同而有所不同。让我们添加一些代码:
# List available resources
resources = await session.list_resources()
print("LISTING RESOURCES")
for resource in resources:
print("Resource: ", resource)
# List available tools
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)
在那里,我们有以下代码来列出功能:
-
列出可用资源:这将列出服务器上所有可用的资源。我们还打印资源名称到控制台。 -
列出可用工具:这列出了服务器中所有可用的工具,并将工具名称打印到控制台。我们也可以打印工具描述和输入模式,但现在我们只打印名称。
选择要使用的功能
让我们通过选择一个工具并调用它来展示如何使用我们列出的功能。在这种情况下,我们将使用我们在上一章中创建的add工具。add工具接受两个参数a和b,并返回两个数字的和。然而,想象一下现在用户已经看到了一个工具列表并选择了一个工具。现在让我们提示用户输入调用工具所需的参数:
# Read information from the first tool
tool_name = tools.tools[0].name
print(f"Using tool: {tool_name}")
first_value = input("Enter first value: ")
second_value = input("Enter second value: ")
# Call a tool
print("CALL TOOL")
result = await session.call_tool(tool_name, arguments={
"a": first_value, "b": second_value})
print(result.content)
在前面的代码中,我们做了以下操作:
-
读取列表中第一个工具的名称并将其打印到控制台。
-
提示用户输入第一个和第二个值作为工具的参数。
-
使用
call_tool方法调用工具,并将参数作为字典传递。结果被打印到控制台。
太好了,现在我们有一个可以连接到服务器、列出功能和调用工具的客户端。然而,这仍然相当程序化,并不非常用户友好。
完整的代码
在我们继续集成 LLM 之前,让我们展示客户端的完整代码:
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="mcp", # Executable
args=["run", "server.py"], # Optional command line arguments
env=None, # Optional environment variables
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write
) as session:
# Initialize the connection
await session.initialize()
# List available resources
resources = await session.list_resources()
print("LISTING RESOURCES")
for resource in resources:
print("Resource: ", resource)
# List available tools
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)
# Read information from the first tool
tool_name = tools.tools[0].name
print(f"Using tool: {tool_name}")
first_value = input("Enter first value: ")
second_value = input("Enter second value: ")
# Call a tool
print("CALL TOOL")
result = await session.call_tool(tool_name, arguments={"a": first_value, "b": second_value})
print(result.content)
# Read a resource
print("READING RESOURCE")
content, mime_type = await session.read_resource("greeting:// hello")
if __name__ == "__main__":
import asyncio
asyncio.run(run())
好的,如果你跟着代码输入,你现在应该有一个可以连接到服务器并使用其功能的运行客户端。你将在本章末尾的作业中再次有机会练习这个。
让我们通过集成 LLM 来改进客户端。你将看到这如何提供更好的用户体验,以及如何用它来抽象服务器使用的复杂性。
带有 LLM 的客户端
到目前为止,你已经看到了如何使用 STDIO 和 SSE 构建和测试客户端。然而,你可能已经注意到这种方法相当程序化,并不非常用户友好。也就是说,对于你想要使用的每个功能,你需要知道其确切名称和参数。这就是 LLM 发挥作用的地方。通过在客户端中涉及 LLM,你可以抽象掉“知道”的部分,而专注于“做”的部分。下面是如何工作的。
在使用 LLM 之前
这就是如何在没有 LLM 的情况下构建一个应用以及应用中的流程:
-
列出服务器功能。
-
用户选择一个功能,客户端请求参数。
-
对响应进行处理。
这种方法相当僵化,需要用户明确知道并选择列出的功能之一。那么,更好的方法是什么呢?
在涉及 LLM 之后
为了解决感觉僵化的客户端,想象一下用户不知道这些功能;他们只通过提示进行交流。带着这个想法,现在让我们看看应用的流程:
-
列出服务器功能。
-
将功能列表转换为 LLM 工具。
-
用户输入一个自然语言请求。
-
客户端将请求发送到 LLM(大型语言模型),LLM 会确定要使用哪个功能以及要发送哪些参数,如果没有匹配的功能,则返回一个通用的 LLM 响应。
用户体验的差异相当显著。这种第二种方法意味着用户不再需要了解功能,也不需要选择要使用的功能。相反,用户只需输入一个自然语言请求,LLM 就会处理其余部分。
让我们看看这在实践中是如何工作的。
与 LLM 合作
现在有许多 AI 提供商允许您调用一个 LLM。在这本书中,我们将使用GitHub 模型,因为这是一个免费选项,您只需要一个 GitHub 账户即可使用它。要使用 GitHub 模型,您要么需要在 GitHub Codespaces 中启动您的项目,要么设置一个具有正确权限的个人访问令牌(PAT)。您需要令牌的原因是您正在调用一个 API,令牌用作携带令牌以验证请求。例如,要通过Ollama使用本地 AI 模型,您就不需要令牌。您可以直接在源代码中输入令牌,但出于安全原因,建议将其保存在环境变量中。
那么,如果我们以前从未与 AI 合作过,我们需要了解什么呢?嗯,想法是发送一个提示并获取一个响应。提示是描述您希望 LLM 执行的自然语言文本。响应也是包含您提示答案的自然语言文本。
然而,要结合 MCP(多通道处理)使用 LLM,想法是让 LLM 根据特定的提示指示要调用哪些函数。例如,对于提示Add 1 and 2,如果我们在 MCP 服务器中定义了一个名为add的函数,并且具有相应的参数,LLM 应该指示使用参数a=1和b=2调用add函数。
下面是调用代码的示例。在下面的代码中,我们将调用一个 GitHub 模型,所以请确保您有一个 GitHub 账户并且已经创建了一个具有正确权限的 PAT(个人访问令牌),或者开始在 GitHub Codespaces 中启动它。
需要定义的重要事项如下:
-
GitHub 模型的端点
-
要使用的模型 - 在这种情况下,
gpt-4o -
要发送的提示 - 您可以从用户输入中收集这些数据或将其硬编码,如下面的示例所示
-
要使用的函数 - 在这种情况下,我们将使用我们在上一章中创建的
add工具
完全可以只发送一个提示并获取一个响应,但在这个例子中,我们希望 LLM 指示要调用哪个函数,因此我们将发送一个函数定义。函数定义是一个 JSON 对象,它描述了函数名称、描述和参数。参数使用 JSON 模式定义。
# json description of functions
functions = [
{
"type": "function",
"function": {
"name": "add",
"description": "Add two numbers",
"type": "function",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "The first number to add"
},
"b": {
"type": "number",
"description": "The second number to add"
}
},
"required": ["a", "b"]
}
}
}
]
# get token from environment variable
token = os.environ["GITHUB_TOKEN"]
# the endpoint for GitHub Models
endpoint = "https://models.github.ai/inference"
# the model to use
model_name = "gpt-4o"
# creation of chat client
client = OpenAI(
base_url=endpoint,
api_key=token
)
print("CALLING LLM")
response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a helpful assistant.",
},
{
"role": "user",
"content": prompt,
},
],
model=model_name,
tools = functions,
# Optional parameters
temperature=1.,
max_tokens=1000,
top_p=1\.
)
response_message = response.choices[0].message
print("LLM RESPONSE: ", response_message)
值得注意的是,除了提示之外,你还可以向 LLM 发送配置,例如 temperature、max_tokens 和 top p。我们不会深入探讨这些参数的含义——你可以在 OpenAI 文档中了解更多信息——但简而言之,它们控制着 LLM 响应的随机性和创造性,以及所谓的上下文窗口的大小。
练习:集成 LLM
因此,让我们看看我们如何将 LLM 集成到客户端中。目标是拥有更好的用户体验并抽象出使用服务器的复杂性。为了达到这个目标,我们需要采取以下步骤:
-
列出服务器功能:通过列出功能,我们可以看到我们有什么可用
-
将功能列表转换为 LLM 工具:来自 MCP 服务器的功能不能直接由 LLM 使用,因此我们需要将它们转换为 LLM 可以理解的形式
-
管理用户输入:这将允许用户输入自然语言请求,我们的客户端上的 LLM 将发出完成请求,并在这样做的同时,告诉我们使用哪个功能以及发送哪些参数。
让我们来做这件事!
列出服务器功能
如果你,例如,正在使用他人的 MCP 服务器,那么你可能将构建此客户端作为你做的第一件事。在这种情况下,在继续之前,请确保安装 MCP SDK。
第一步与之前没有区别。我们需要列出服务器上的功能。这是通过调用列出工具来完成的,如下所示:
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)
将功能列表转换为 LLM 工具
我们下一步很重要,因为我们将要转换功能列表,使其成为 LLM 可以理解的形式。这将为我们下一步做好准备,我们将使用 LLM 来确定使用哪个功能。以下是转换代码:
-
让我们添加转换代码函数:
def to_llm_tool(tool): tool_schema = { "type": "function", "function": { "name": tool.name, "description": tool.description, "type": "function", "parameters": { "type": "object", "properties": tool.inputSchema["properties"] } } } return tool_schema
以下代码定义了一个函数,该函数接受一个工具作为输入并将其转换为 LLM 可以理解的形式。
-
让我们调用转换代码:
functions = [] for tool in tools.tools: print("Tool: ", tool.name) print("Tool", tool.inputSchema["properties"]) functions.append(to_llm_tool(tool))
从代码中,你可以看到我们遍历工具的响应并调用每个工具的转换代码。
太好了,现在我们已经为使用 LLM 做好了充分的准备。下一步是管理用户输入并向 LLM 发送完成请求。在 LLM 的响应中,LLM 将告诉我们使用哪个函数以及参数。在这种情况下,要调用的函数将是服务器上的一个功能。
用户输入自然语言请求,LLM 发出完成请求
让我们看看我们现在如何调用 LLM,因为我们已经有了它可以使用的工具。
这分为两部分:第一部分是调用 LLM,第二部分是学习 LLM 是否返回了函数调用或通用响应。
-
调用 LLM:
def call_llm(prompt, functions): token = os.environ["GITHUB_TOKEN"] endpoint = "https://models.github.ai/inference" model_name = "gpt-4o" client = OpenAI( base_url=endpoint, api_key=token, ) print("CALLING LLM") response = client.complete( messages=[ { "role": "system", "content": "You are a helpful assistant.", }, { "role": "user", "content": prompt, }, ], model=model_name, tools = functions, # Optional parameters temperature=1., max_tokens=1000, top_p=1\. ) response_message = response.choices[0].message print("LLM RESPONSE: ", response_message) functions_to_call = []
在前面的代码中,我们做了以下操作:
-
创建了一个函数,该函数接受提示和函数列表作为输入,并返回 LLM 响应。
-
使用
complete方法调用 LLM,并将提示和函数作为参数传递。响应被打印到控制台。
-
让我们通过添加以下代码来检查 LLM 是否返回了函数调用:
if response_message.tool_calls: for tool_call in response_message.tool_calls: print("TOOL: ", tool_call) name = tool_call.function.name args = json.loads(tool_call.function.arguments) functions_to_call.append({ "name": name, "args": args }) return functions_to_call
在前面的代码中,我们做了以下操作:
-
通过检查响应消息中是否存在
tool_calls属性来检查 LLM 是否返回了函数调用 -
遍历
tool_calls并在控制台打印工具名称和参数 -
返回要调用的函数列表
客户端确定要使用哪个服务器功能以及要发送的参数
在这一点上,我们有了 LLM 的响应,它甚至返回了要调用的函数(如果有)。下一步是检查要调用的函数,并在需要时调用 MCP 服务器:
functions_to_call = call_llm(prompt, functions)
# call suggested functions
for f in functions_to_call:
result = await session.call_tool(f["name"], arguments=f["args"])
print("TOOLS result: ", result.content)
在前面的代码中,我们做了以下操作:
-
调用 LLM 并获取要调用的函数。
-
遍历要调用的函数,并使用
call_tool方法调用 MCP 服务器。结果被打印到控制台。
很好,不是吗?用户现在肯定在感谢你让他们的生活变得更轻松,因为他们可以使用自然语言与服务器交互。
摘要
在本章中,我们探讨了如何构建可以连接到 MCP 服务器并使用其功能的客户端。我们首先构建了一个简单的客户端,它可以列出并调用服务器上的功能。然后我们通过集成 LLM 来改进客户端,这使得我们可以通过启用与服务器自然语言交互来创建更好的用户体验。
在下一章中,我们将探讨如何使用 VS Code 和 Claude 桌面版来消费 MCP 服务器。
作业
对于这个作业,你将再次专注于电子商务。你将构建一个用户可以与电子商务服务器交互的体验:
-
请求特定类别的产品
-
使用自然语言将产品添加到购物车中
解决方案
这里有一个客户端的解决方案。它涵盖了有和无 LLM 的客户端。
你可以通过github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter07/solutions/README.md访问解决方案。
习题
客户端可以在 MCP 服务器上访问什么?
-
A: 提示、工具和资源
-
B: 工具、提示和服务
-
C: 工具和提示
将 LLM 添加到客户端有什么好处?
-
A: 将 LLM 放在服务器上更好
-
B: 它使客户端更快
-
C: 客户端的 LLM 允许最终用户使用提示与服务器交互,这为用户提供了更好的体验
参考文献
-
Python SDK:
github.com/modelcontextprotocol/python-sdk|
现在解锁此书的独家优惠
扫描此二维码或访问
packtpub.com/unlock,然后通过书名搜索此书。 |![]()
|| 注意:在开始之前准备好您的购买发票。* |
| --- |
第八章:消费服务器
到目前为止,我们已经探讨了如何创建服务器,但也通过您必须自己编写的定制客户端来消费它们。在本节中,我们将探讨如何使用现有软件,如Visual Studio Code(VS Code)或Claude Desktop来消费服务器。当我们说“消费”时,我们的意思是指安装服务器、配置它们,然后使用它们来运行工具或以某种方式与服务器交互。
在本章中,您将学习以下内容:
-
理解如何使用现有工具消费服务器
-
在 VS Code 中安装和配置服务器
-
使用
mcp.json文件进行服务器管理 -
管理服务器机密和配置
-
使用 VS Code 测试和与服务器交互
-
在消费服务器时应用安全最佳实践
本章涵盖了以下主题:
-
使用 Claude Desktop 和 VS Code 等主机消费
-
安装过程
-
添加服务器
-
本地与全局安装
-
小贴士和技巧
-
安全方面
-
服务器建议
使用 Claude Desktop 和 VS Code 等主机消费
消费服务器,好吧,这意味着什么?所以为了使用 MCP 服务器及其功能,您需要一种与之交互的方式。您在第七章中看到了如何创建服务器以及如何编写可以与之交互的客户端。这是一个完全有效的方式来消费服务器,但您需要编写代码。
另一种方法是使用现有的软件,如 Claude Desktop 或 VS Code。这些工具旨在与 MCP 服务器协同工作,并且它们还提供了一个大型语言模型,确保您可以通过提示以更用户友好的方式与服务器交互。
我们将这些两款软件视为主机,因为它们内置了 MCP 客户端,但它们也使用配置文件来跟踪已安装的服务器、可用的工具等等。
它们是如何工作的?好吧,它们是这样工作的:
-
通过选定的传输类型,如 STDIO、SSE 或 Streamable HTTP,启动与 MCP 服务器的连接
-
提供一个用户界面与服务器交互,并允许您输入提示、与工具、配置等进行交互
-
使用配置文件跟踪已安装的服务器、可用的工具和其他设置
这里的例子是 VS Code 的用户界面,它代表了这些主机的工作方式:

图 8.1 – VS Code 界面
在前面的图中,您可以看到以下内容:
-
列出通过已安装的服务器当前可用的工具
-
一个可以输入提示的聊天界面
接下来,让我们讨论特定的主机及其功能。
VS Code 中的 MCP 支持
VS Code 与 GitHub Copilot 一起,为 MCP 服务器提供了广泛的支持。有许多功能可以帮助您安装、配置和确保服务器的安全性。它支持使用案例,例如尝试您正在构建的服务器,以及将 VS Code 变成一个可以通过您的提示运行工具的代理应用程序。
这里是 VS Code 为 MCP 服务器提供的一些功能列表:
-
安装服务器:VS Code 允许您在全局级别和工作区级别安装服务器。它支持三种传输类型:STDIO、SSE 和可流式 HTTP。
-
管理工具:您可以为服务器启用和禁用工具,这对于确保客户端拥有所需的上下文非常有用。
-
与功能交互:除了工具之外,它还支持用户提示和资源等功能。
-
管理服务器:您可以添加和删除服务器并对其进行配置。
-
采样:它支持采样,这意味着当服务器发送样本请求时,您可以在发送响应之前与之交互并调整请求。
-
引申请求:它还支持引申请求,这是服务器需要从用户那里获取更多信息以继续请求的场景。
-
日志记录和调试:它还支持日志记录和调试,这在开发服务器或客户端时非常有用。
正在不断地提供额外的功能,如果您想尽早尝试这些功能,建议您使用VS Code Insiders,因为这些功能首先在那里发布。
克劳德桌面版
您还可以使用克劳德桌面版来使用 MCP 服务器。前往下载页面(claude.ai/download)以安装适用于您操作系统的客户端。就像 VS Code 一样,它提供了一个用户界面和一系列功能,用于与 MCP 服务器交互。
克劳德(Claude)的功能远不止与 MCP 服务器(MCP servers)协作;它是一个功能齐全的 AI 助手,或许可以与类似的产品如 ChatGPT 或 Copilot 相媲美。要查看完整的功能列表,请访问此页面:claude.ai/login?returnTo=%2F%3F#features。
安装过程
对于克劳德和 VS Code 来说,安装服务器有一个中心概念,即mcp.json文件。这是您的主要配置文件,您在其中添加有关服务器位置和所需额外配置的信息。实际上,这个 JSON 文件就像一个清单文件,列出了已安装的服务器。安装文件的行为是将条目添加到该文件中。以下是一个mcp.json文件的示例:
{
"inputs": [
],
"servers": {
"docs": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp"
}
}
}
快速提示:使用AI 代码解释器和快速复制功能来增强您的编码体验。在下一代 Packt Reader 中打开此书。点击复制按钮
(1)快速将代码复制到您的编码环境,或点击解释按钮
(2)让 AI 助手为您解释代码块。

下一代 Packt Reader 随本书免费赠送。扫描二维码 OR 访问 packtpub.com/unlock,然后使用搜索栏通过名称查找此书。请仔细检查显示的版本,以确保您获得正确的版本。

在此文件中,我们有一个 servers 属性,包含一个指向 learn.microsoft.com/api/mcp 的服务器条目;现在这个 MCP 服务器被认为是已安装的。
在服务器上安装和运行功能的完整安装过程看起来是这样的:
-
通过在
servers属性中添加mcp.json文件条目来安装服务器。 -
启动服务器。
-
通过输入与服务器中工具匹配的提示来使用服务器。
我们将在本章后面探讨这样一个场景,但现在你对它已经有了高屋建瓴的理解。让我们更深入地探讨 mcp.json 文件。
mcp.json 文件
mcp.json 文件有两个主要属性:
-
inputs:这用于定义敏感信息的占位符,例如 API 密钥或令牌。想法是定义你想要一个输入提示出现,并且答案应该被存储在一个你可以稍后引用的变量中。以下是一个例子:[ { "id": "my_api_key", "description": "The API key that my MCP server needs", "type": "promptString", "password": true } ]
根据前面的定义,VS Code 中的用户界面将启动一个输入提示,要求您填写此信息。然后需要与服务器条目配对,如下所示:
"my_mcp_server": {
"type": "http",
"url": "http://localhost:8000/mcp",
"headers": {
"Authorization": "Bearer ${my_api_key}"
}
}
注意 my_api_key 现在作为头属性传递,这样每次我们调用 "http://localhost:8000/mcp" 时,它也会传递 API 密钥,确保我们可以安全地访问我们的 MCP 服务器。以这种方式设置确保秘密不会出现在 mcp.json 中。
-
servers:此属性接受一个包含服务器条目的 JSON 对象。你需要知道的是,这些条目根据服务器使用的传输方式而有所不同。让我们为每种传输类型展示一些例子:-
使用可流式 HTTP:
"my_mcp_server": { "type": "http", "url": "http://localhost:8000/mcp" }
在这种情况下,它使用可流式 HTTP;我们可以看到,因为
type值是http,并且指定了url值。让我们看看 SSE。-
SSE:
"my_mcp_server": { "type": "sse", "url": "http://localhost:8000/sse" }
SSE,就像可流式 HTTP 一样,是一种用于远程访问服务器的传输方式。因此,在这两种情况下都使用
url值来指出这些服务器所在的位置。-
STDIO:
"playwright": { "command": "npx", "args": ["-y", "@executeautomation/playwright-mcp-server"] }
STDIO,或标准 I/O,需要指定一个
command和args属性,因为它需要指出如何启动所讨论的服务器。对于 SSE 或可流式 HTTP,则不需要,因为这些服务器位于远程。 -
添加服务器
你在上一节中已经看到如何添加各种服务器,但让我们来实际操作一下添加和使用服务器。让我们以Playwright,一个端到端(E2E)测试框架为例。要将它作为 MCP 服务器安装,你通常需要找到它的 GitHub 仓库并查看其安装说明。对于 Playwright,其仓库在这里:github.com/microsoft/playwright-mcp。
由于该服务器提供了许多功能,所以有很多说明,但让我们从其入门部分获取安装说明,它看起来如下:
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
}
这里,我们看到它显然是一个 STDIO 类型的服务器,因为其command和args属性已被填充。
步骤 1:安装服务器
让我们通过 VS Code 来安装它。这样做,我们就有几种不同的安装方式:
-
直接将条目添加到
mcp.json文件中。 -
使用用户界面,无论是通过查看
mcp.json文件时出现的添加服务器按钮,还是通过从命令面板运行MCP: 添加服务器命令,都可以触发用户界面,让你决定使用哪种传输方式,以及根据选择的传输方式提供url或command和args信息。
例如,让我们点击添加服务器;你应该会看到一个以下界面:

图 8.2 – 安装服务器
如你所见,安装服务器有无数种选项:不同的传输方式,一些也来自不同的包管理器,甚至 Docker。
事实上,如果你选择浏览 MCP 服务器…选项,它将带你去一个经过审查的 MCP 服务器列表(code.visualstudio.com/insider/mcp),并允许你从中选择一个服务器。
我们可以从其 GitHub 仓库获取与 Playwright 相关的完整说明,但让我们通过选择安装它来从服务器列表中安装,如下所示,点击安装 Playwright:

图 8.3 – 安装 Playwright
现在,你应该在 VS Code 中看到一个对话框打开,它提供了有关服务器的更多信息,以及安装说明。我们之前通过访问其 GitHub 仓库找到了安装信息,但这也是一种很好的方法。
好的,所以答案似乎是手动复制粘贴mcp.json中你需要的内容,或者使用 VS Code 中的许多选项之一。
步骤 2:管理服务器
现在,你已经添加了服务器,所以你的mcp.json看起来类似于以下 JSON(注意 Playwright 是如何被添加的):
{
"servers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
}
},
"inputs": []
}
现在是时候看看用户界面如何支持我们了。我们可以做的一件事是选择扩展;我们应该看到我们的已安装服务器列表如下:

图 8.4 – 已安装的服务器
从这个视图,我们可以点击服务器上的齿轮图标来与之交互:

图 8.5 – 与服务器交互
如您所见,有多个选项,从启动服务器到查看其日志,甚至 JSON 配置。您也可以从 mcp.json 文件中执行这些操作。
第 3 步:与服务器交互
让我们启动服务器,现在让我们转向从安装 GitHub Copilot 获得的聊天界面。确保代理已在下拉列表中选中,然后输入以下提示:
Navigate to https://tfl.gov.uk/. I want to go from Paddington to Heathrow, show me how to get there by underground, important use playwright tool
有时,VS Code 可能不太愿意使用工具,因此花些时间在提示上确保它确实使用了它。我已经添加了 important use playwright tool 来确保工具被触发。
您应该在聊天界面中看到以下内容:

图 8.6 – 使用工具
这显示了工具被触发,并要求您允许该工具作为此部分运行。您可以看到如何从您的提示中解析出 URL 并将其匹配为 Playwright 工具的输入。这将触发一系列工具调用,因为 Playwright 将遍历网站,定位输入字段,并尝试完成提示中设定的任务。
您可能需要有时重新启动聊天,但一旦它按预期工作,您应该会看到以下类似图示,表明 Playwright 已正确开始导航网站,并试图将您从 Paddington 带到 Heathrow:

图 8.7 – Playwright 查询
一旦完成,您可以通过各种方式与之交互以获取所需内容,甚至可以要求它从这个交互中生成 Playwright 测试,这正是其真正价值所在。
本地与全局安装
到目前为止,我们已在 .vscode/mcp.json 中安装了服务器,这意味着它们只安装在这个工作区中。您也可以在您的机器上全局安装服务器。要选择全局安装,请从命令面板中选择 MCP: Add Server。在最后一步,选择 Global,它将在 (Settings)/User/mcp/json 中创建一个 mcp.json 文件,这意味着它已将服务器条目添加到用户设置而不是这个特定的工作区实例。这意味着如果您打开 VS Code 的另一个实例,您将不需要再次安装此服务器。
这是服务器全局安装后用户级 mcp.json 文件的外观:
{
"servers": {
"my-mcp-server-6801ea17": {
"url": "https://learn.microsoft.com/api/mcp",
"type": "http"
}
},
"inputs": []
}
如您所见,它在实际的 mcp.json 文件中的安装方式没有区别,但区别在于 mcp.json 文件的位置。一般规则是,如果您经常使用服务器,则全局安装它;如果不经常使用,则在工作区中的 .vscode/mcp.json 中安装。
小贴士和技巧
到目前为止,我们已经了解了基础知识,但还有其他什么需要了解的吗?嗯,还有很多不同的设置。完整的特性列表可以在以下文档页面找到:code.visualstudio.com/docs/copilot/chat/mcp-servers。
调试
作为一名开发者,了解如何调试是一项关键技能,所以让我们来看看如何做到这一点。根据文档,目前支持 Node.js 调试(code.visualstudio.com/docs/copilot/customization/mcp-servers#_debug-an-mcp-server):
"server-ts": {
"command": "node",
"args": ["08 - consuming servers/code/build/index.js"],
"dev": {
"watch": "08 - consuming servers/code/build/**/*.js",
"debug": { "type": "node" }
}
}
根据前面的配置,您需要做的是以下这些:
-
设置您的
command和args属性以指导如何运行服务器。 -
添加具有两个不同属性的
dev:watch,这是一个 全局模式(GLOB),用于检查您的 JavaScript 文件是否有更改,以及debug,您在这里指定哪个进程正在调试它。在这种情况下,是 Node。一旦设置好并且服务器正在运行,应该会在上方有一个灰色的 debug 文本。请确保您已在合适的位置添加了断点,例如启动服务器、运行工具等。以下是您如何测试不同场景的方法:- 测试启动:在这里,我们已经在启动代码中添加了一个断点,我们只需要从
mcp.json启动服务器。您应该看到断点是如何被触发的,如下所示:![图 8.8 – 断点启动]()
图 8.8 – 断点启动
- 测试启动:在这里,我们已经在启动代码中添加了一个断点,我们只需要从
快速提示:需要查看此图像的高分辨率版本吗?在下一代 Packt Reader 中打开此书或在其 PDF/ePub 复印本中查看。
新一代 Packt Reader 以及本书的 免费 PDF/ePub 复印本 包含在您的购买中。扫描二维码或访问 packtpub.com/unlock,然后使用搜索栏通过名称查找此书。请仔细检查显示的版本,以确保您获得正确的版本。

-
测试工具:测试工具的一个好方法是运行 CLI 模式下的检查器。以下是一个测试具有输入参数
a和b的工具add的命令。将此命令应用于匹配您服务器上的操作和参数:npx @modelcontextprotocol/inspector --cli node ./build/index.js --method tools/call --tool-name add --tool-arg a=1 --tool-arg b=2
您应该在终端中直接看到类似以下内容的响应:
{
"content": [
{
"type": "text",
"text": "3"
}
]
}
故障排除
有时,您会在聊天区域的 工具 图标上看到错误指示。如果发生这种情况,打开终端区域的 输出 部分,您将看到问题所在。这个区域是一个很好的检查地方,因为您将看到 VS Code 和您的服务器之间的每一次交互,例如当它启动和停止服务器时,以及当它初始化、列出工具等。
当我们运行调试器并将其连接到服务器时,我们得到了以下信息写入输出:
2025-08-03 19:18:56.856 [info] Connection state: Starting
2025-08-03 19:18:56.857 [info] Connection state: Running
2025-08-03 19:18:56.857 [info] [editor -> server] {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true},"sampling":{},"elicitation":{}},"clientInfo":{"name":"Visual Studio Code - Insiders","version":"1.103.0-insider"}}}
2025-08-03 19:18:56.888 [warning] [server stderr] Debugger listening on ws://127.0.0.1:9230/7b07fce3-e6cc-46c7-a0b8-7b782fbe853a
2025-08-03 19:18:56.888 [warning] [server stderr] For help, see: https://nodejs.org/en/docs/inspector
2025-08-03 19:18:57.052 [warning] [server stderr] Debugger attached.
2025-08-03 19:19:01.960 [info] Waiting for server to respond to `initialize` request...
2025-08-03 19:19:06.960 [info] Waiting for server to respond to `initialize` request...
2025-08-03 19:19:10.858 [warning] Failed to parse message: "Starting MCP server...\n"
2025-08-03 19:19:10.874 [info] [server -> editor] {"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"demo-server","version":"1.0.0"}},"jsonrpc":"2.0","id":1}
2025-08-03 19:19:10.874 [info] [editor -> server] {"method":"notifications/initialized","jsonrpc":"2.0"}
2025-08-03 19:19:10.874 [info] [editor -> server] {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
2025-08-03 19:19:10.891 [info] [server -> editor] {"result":{"tools":[{"name":"add","title":"Addition Tool","description":"Add two numbers","inputSchema":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]},"jsonrpc":"2.0","id":2}
2025-08-03 19:19:10.891 [info] Discovered 1 tools
2025-08-03 19:26:34.115 [warning] [server stderr] Debugger ending on ws://127.0.0.1:9230/7b07fce3-e6cc-46c7-a0b8-7b782fbe853a
2025-08-03 19:26:34.115 [warning] [server stderr] For help, see: https://nodejs.org/en/docs/inspector
让我们从前面的响应中突出一些有趣的点:
-
编辑器与服务器握手:在这里,编辑器向服务器发送关于它支持哪些功能/能力的信息。它告诉服务器它支持
roots、sampling和elicitation:2025-08-03 19:18:56.857 [info] [editor -> server] {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true},"sampling":{},"elicitation":{}},"clientInfo":{"name":"Visual Studio Code - Insiders","version":"1.103.0-insider"}}} -
服务器响应编辑器握手:在这里,响应返回表示工具是受支持的:
2025-08-03 19:19:10.874 [info] [server -> editor] {"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"demo-server","version":"1.0.0"}},"jsonrpc":"2.0","id":1} -
服务器发送初始化通知:握手过程中的最后一步是发送
initialized通知。这是客户端发送给服务器以表明它已准备好交换数据的事情。在这种情况下,我们的客户端是 VS Code:2025-08-03 19:19:10.874 [info] [editor -> server] {"method":"notifications/initialized","jsonrpc":"2.0"} -
告诉我你的工具:现在客户端请求服务器提供其工具:
2025-08-03 19:19:10.874 [info] [editor -> server] {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} -
服务器响应工具请求:最后,服务器响应请求列出其工具,如下所示:
2025-08-03 19:19:10.891 [info] [server -> editor] {"result":{"tools":[{"name":"add","title":"Addition Tool","description":"Add two numbers","inputSchema":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additional Properties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]},"jsonrpc":"2.0","id":2}
工具管理
工具管理是使用 VS Code 的重要方面。在工具方面有一些有趣的场景,你的编辑器会提供帮助:
-
选择和取消选择工具:当你添加服务器时,可能会一次性添加很多工具。在两种情况下,无论是使用你的编辑器作为代理工具,还是确保调用正确的工具,你都可以选择哪些工具是激活的。你可以通过点击聊天中的工具图标来做出选择,这将显示所有工具的用户界面。选择/取消选择你想要激活或停用的工具。
-
运行特定工具:通常,当你想要运行工具时,你会尝试输入一个尽可能匹配特定工具描述的提示。如果你想确保选择了正确的工具,你可以在前面加上
#,这将标识该特定工具。例如,要运行名为add的工具,你可以构建一个这样的提示:#add 2 and 7 -
管理工具数量:一些模型对它们可以接受多少工具有限制。为了解决这个问题,有一个名为
github.copilot.chat.virtualTools.enabled的设置,它将分析提示并仅提交与提示匹配的工具。这是一个避免使用过多提示导致任何错误的好方法。确保你使用的是最新版本的 VS Code Insider。此功能正在积极开发中,可能会随时间而变化。 -
处理工具批准:当 VS Code 决定要运行一个工具时,它会显示为一个询问你点击继续的查询。在这个时候,你可以选择仅为此提示会话、为此工作区或始终允许。以下是工作原理:
-
输入以下提示:
#add 2 and 7
这会使 VS Code 要求你点击继续。现在,从下拉菜单中选择以允许此工作区。让我们再次运行它。
-
-
输入以下提示:
#add 2 and 9
这在没有征求我同意的情况下运行了工具,因为我之前已经授予它在工作区中运行的权限。要撤销此同意,我可以从命令面板运行Chat: Reset Tool Confirmations。
其他设置
不断有设置被添加到 Copilot 和 VS Code 中。确保你处于 VS Code Insiders 状态,在命令面板中输入MCP,你会看到许多专门针对 MCP 的设置:

图 8.9 – MCP 命令
如你所见,有很多可用的命令。也可以尝试输入Copilot,因为它与 MCP 一起使用。这是一个不断变化的部分,但如果你正在开发 MCP 服务器,那么充分利用你的编辑器就是你的工作之一。
安全方面
安全性是一个巨大的话题,但让我们在编辑器的使用背景下来讨论它。你可以做些一般性的事情来保持更安全,所以让我们尝试总结一份良好的实践清单:
-
将秘密排除在配置之外:这是如何在
mcp.json文件中指定配置以做到这一点的示例:{ "mcp": { "inputs": [ { "type": "promptString", "id": "my-key", "description": "Token for my API", "password": true } ], "servers": { "my-server": { "type": "http", "url": "https://my-secure-api/mcp", "headers" : { "Authorization": "Bearer ${input:my-key}" } } } } }
通过使用inputs元素,你可以确保秘密不会出现在配置文件中。其工作原理是,当服务器启动时,用户界面会提示你填写my-key的值,并且它会被安全地存储。然后你可以通过${input:my-key}在服务器条目中引用这个值。对于对https://my-secure-api/mcp的每个请求,都会添加一个包含你提供的值的Authorization头。
-
工具运行访问:建议不允许在工作区中运行的工具持续访问。最好每次请求时都给它访问权限。这也给你检查解析输入的机会。
-
限制服务器可以做什么:有方法可以限制服务器可以访问的内容。例如,有一个用于文件访问的 MCP 服务器。你应该将其限制在最多一个文件夹或你确信它不会造成任何损害的文件夹中。
-
使用受信任的服务器:GitHub 最近发布了一个经过审查的 MCP 服务器注册表。使用 VS Code Insiders 的最新版本,你可以输入
@mcp,它会显示一个你可以安装的 MCP 服务器列表。你也可以通过选择MCP: Add Server命令然后选择浏览 MCP 服务器来访问相同的列表。你的服务器列表应该看起来与这个图相似:

图 8.10 – MCP 注册表
通常,确保服务器背后的作者是可信赖的来源(例如,Stripe 是 Stripe MCP 服务器背后的公司,等等)。即便如此,也要求助于代码扫描工具并关注最新的安全新闻;漏洞总是时有发生。同样的注册表也可以通过这个链接找到(code.visualstudio.com/insider/mcp),但我更喜欢使用 VS Code 中的内置体验。还可以查看 GitHub 上的服务器仓库,因为它允许你通过它们的README文件了解更多关于每个服务器的信息(github.com/modelcontextprotocol/servers)。
服务器建议
那么,推荐哪些服务器?嗯,这取决于您的需求,但我会个人使用以下这些:
-
GitHub (
github.com/github/github-mcp-server): 这个服务器使我的生活变得更加轻松,因为我可以与问题、拉取请求(PRs)、委托工作给代理等交互 -
Playwright (
github.com/microsoft/playwright-mcp): 这对于导航网站非常棒,还可以从导航中生成测试,从而节省大量时间 -
Microsoft Learn MCP 服务器 (
github.com/microsoftdocs/mcp): 这是官方的 Microsoft Docs 和 Learn 服务器,它直接在您的编辑器中提供文档
还有更多,但先从这些开始,看看哪些适合您的场景。
摘要
在本章中,我们介绍了如何消费 MCP 服务器,包括您创建的和外部的服务器。此外,我们还讨论了安装后的服务器管理、配置、工具使用等。
最后,我们提供了一些关于安全实践的建议。这应该被视为一组最基本的做法,您应该做更多。
在下一章中,我们将介绍采样,这是一个高级主题,但在服务器需要将工作委托给您作为用户时非常有用。
作业
安装您选择的任何服务器,并通过提示尝试它。尝试从这里安装服务器:code.visualstudio.com/insider/mcp。
问答
如何安装 MCP 服务器?
-
A: 您可以像在 VS Code 中安装扩展一样安装它。
-
B: 您在终端中运行
mcp install <服务器名称>。 -
C: 服务器通过在
mcp.json中添加文本条目来安装。您还需要指定如何启动它或服务器所在的位置。
您可以在 github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter08/solutions/solution-quiz.md 访问解决方案。
|
现在解锁此书的独家优惠
扫描此二维码或访问 packtpub.com/unlock,然后通过名称搜索此书。 | 
|
| 注意: 在开始之前准备好您的购买发票。 |
| --- |
第九章:采样
MCP 最强大的功能之一是采样。首先,这意味着什么?好吧,让我们看看这个词的定义,并从中推断。梅里尔-韦伯斯特将采样定义为如下:
“对某物进行采样的行为或过程”
好的,所以我们需要一个样本,并且我们最终分析了这个样本,明白了。带着这个定义,让我们在 MCP 的背景下讨论它。在 MCP 中,采样意味着服务器正在向客户端发送一个采样请求,一个用于分析的样本。服务器为什么要这样做呢?很简单;服务器需要客户的帮助做一些事情。因为客户端拥有 LLM(即使服务器有时也可以拥有 LLM),服务器将任务委托给客户端,在那里 LLM 可以提供帮助。
到目前为止,这听起来合理,对吧?但我敢打赌你正在问为什么服务器会这样做。
在本章中,我们将进行以下操作:
-
理解采样的主题以及何时使用它
-
使用采样构建服务器实现并使用 VS Code 进行消费
-
在我们想要将此功能集成到我们的应用程序中时,将服务器实现连接到客户端实现。
本章涵盖了以下主题:
-
为什么需要采样?
-
实现采样
为什么需要采样?
正如我们一开始所说的,服务器想要将一些问题委托给客户端,特别是客户端的 LLM。那么,LLM 能帮助解决哪些服务器无法解决的问题呢?如果你思考这个问题,会发现有很多例子:生成产品描述、摘要、标签等等。
让我们先了解一下采样流程,以便我们了解在高级别上交互是如何发生的。
采样流程
在进行采样时,以下参与者会参与其中:
-
用户:用户通常在两个地方参与,作为初始动作的发起者,甚至作为人机交互中的接受者或修改采样请求。
-
服务器:服务器是发送采样请求的参与者。这个请求通常是从服务器功能中发送的,比如工具调用、读取资源或提示模板的请求。
-
客户端:客户端的职责是接收采样请求并向用户展示,以便用户可以决定如何处理它。用户将采样请求视为一项行动的建议。如果请求要求特定的模型、令牌数量等,那么这就是用户可以参考并接受或修改的内容。
-
LLM:客户端上的 LLM 负责完成采样请求,并接收来自服务器的提示,然后使用其生成能力生成响应。
下面是一个描述整体流程的图:

图 9.1 – 采样流程
快速提示:需要查看此图像的高分辨率版本吗?在下一代 Packt Reader 中打开此书或在其 PDF/ePub 副本中查看。
下一代 Packt Reader以及本书的免费 PDF/ePub 副本包含在您的购买中。扫描二维码或访问packtpub.com/unlock,然后使用搜索栏通过名称查找本书。仔细检查显示的版本,以确保您获得正确的版本。

值得明确的是,采样请求并非没有原因,而是有一个初始动作最终触发了它。例如,用户想要创建产品,或需要帮助写博客文章等,这反过来又导致服务器将部分任务委托给客户端。
让我们看看一些具体的场景,以便更好地理解。
场景
到目前为止,我们已经简要提到了一些场景,但让我们详细讨论它们。
写博客文章
写博客文章的行为是一个很好的案例,因为它有一些方面肯定属于用户,例如撰写草稿。然而,也有一些方面 LLM 做得更好,例如为摘要总结或甚至生成关键词(感谢 Kent Dodds 为此用例提供灵感):
-
用户将草稿博客文章提交到服务器。
-
服务器存储草稿,但请求帮助生成标签,因此发送了一个样本请求。
-
客户使用其 LLM 分析草稿并生成响应。
后台电子商务
在电子商务后台工作的人员的一个常见任务是产品的管理。通常,您从注册产品标题开始,并捕获其他可能合理的属性。然而,撰写引人入胜的描述可能是一项耗时的工作,而 LLM 在这方面可能比人类做得更好。以下是如何作为一个采样场景的用例:
-
管理员用户通过客户端添加一个带有标题和关键词的新产品。
-
服务器请求客户端帮助创建一个具有关键词上下文的引人入胜的产品描述。
-
客户生成这样的描述,服务器用更好的描述更新产品。
悬疑游戏
在玩游戏时,你经常会遇到你想要与之交谈的游戏角色;其中一些角色被称为NPC或非玩家角色。通常,这些角色的说话能力有限,因为这正是它们被编程的方式。这种限制减少了游戏体验,这正是 LLM 可以介入并做得更好的地方。以下是如何实现这一点的流程:
-
用户要求与一个角色交谈。
-
服务器检索角色信息,如姓名、描述、动机、线索等,并将其作为样本请求发送。
-
客户端从提示请求中检索字符信息,并使用该信息作为系统消息来生成一个愉快的对话响应。
现在我们对合适的场景有了更多的了解,让我们来讨论实际的消息看起来是什么样子的,因为理解正在发送和接收的内容非常重要。更重要的是,理解在向客户端发送样本请求时可以配置哪些信息非常重要。
消息
如果你使用 SDK,你几乎永远不会遇到 JSON-RPC,但有趣的是要知道可以作为采样请求一部分发送的内容,这样你就知道可以向客户端发送什么类型的指导。让我们看看以下消息:
请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Write a compelling description of this product:
tomato, here's some keywords: red, vegetable, fresh"
}
}
],
"modelPreferences": {
"hints": [
{
"name": "claude-3-sonnet"
}
],
"intelligencePriority": 0.8,
"speedPriority": 0.5
},
"systemPrompt": "You're a professional writing assistant and
tend to want to write descriptions in a poetic way",
"maxTokens": 100
}
}
在前面的消息中,以下内容特别引人关注:
-
messages: 这里是你发送的消息,这些消息将被输入到 LLM 中。 -
modelPreferences: 这个属性只是对理想中应该使用哪个模型的建议。用户是最终决定的人,但应将其视为建议。还要注意我们可以设置的其他属性,例如intelligencePriority和speedPriority。 -
systemPrompt: 这是一个重要的属性,因为这是 LLM 的个性,它可以极大地影响消息的结果。 -
maxTokens: 这个属性决定了将用于此任务多少个标记。
现在我们已经仔细查看服务器发送给客户端的内容,让我们看看客户端发送回的内容:
响应
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"role": "assistant",
"content": {
"type": "text",
"text": "The capital of France is Paris."
},
"model": "claude-3-sonnet-20240307",
"stopReason": "endTurn"
}
}
在这里,我们可以看到 LLM 的响应是如何在content属性中返回的,它还告诉我们最终使用了哪个模型,以及其他细节。
实现采样
现在,我们已经来到了本章最激动人心的部分,即如何实现采样。
我们将涵盖以下实施部分:
-
服务器端: 如何将其添加到 MCP 服务器
-
客户端: 如何启用它以及代码看起来像什么,包括接收请求和发送响应
服务器实现
要在服务器端实现采样,我们需要考虑采样请求应该在何时进行。通常,采样请求不会无中生有,而是在某个动作的上下文中发生。想象以下场景。一个在电子商务网站后台工作的用户添加新的销售产品。他们需要帮助编写描述,并且希望描述尽可能吸引人。因此,他们将使用客户端及其 LLM 的帮助。以下是它将如何展开:
-
用户的客户端调用服务器上的一个工具,请求创建一个新的产品。
-
服务器工具随后发送一个带有操作说明的样本请求,这被称为提示。
-
客户端随后从样本请求中获取提示,调用他们的 LLM,并返回答案:
from mcp.server.fastmcp import Context, FastMCP from mcp.server.session import ServerSession from mcp.types import SamplingMessage, TextContent from uuid import uuid4 mcp = FastMCP(name="Sampling Example") products = [] @mcp.tool() async def create_product(product_name: str, keywords: str, ctx: Context[ServerSession, None]) -> str: """Create a product and generate a product description using LLM sampling.""" # 1\. A new product is being created product = { "id": uuid4(), "name": product_name, "description": "" } prompt = f"Create a product description about {keywords}" # 2\. Creates a sampling message and passes the prompt as the payload result = await ctx.session.create_message( messages=[ SamplingMessage( role="user", content=TextContent(type="text", text=prompt), ) ], max_tokens=100, ) product["description"] = result.content.text products.append(product) # return the complete product return product
在前面的代码步骤 1 和 2 中,注意 ctx(上下文)对象的使用,调用 session.create_message 并传入 SamplingMessage,其中包含 user 值,以及 messages 被填充了你发送给客户端的提示:
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text=prompt),
)
],
max_tokens=100,
)
if __name__ == "__main__":
print("Starting server…")
mcp.run()
此外,请注意,我们在返回之前正在等待客户端回复。一旦客户端的消息返回,我们将结果分配给产品描述,最后返回产品:
product["description"] = result.content.text
products.append(product)
# return the complete product
return product
让我们在 VS Code 中测试一下。请确保执行以下操作:
-
在
mcp.json中创建一个服务器条目,如下所示:"sample-server": { "command": "python", "args": ["path/to/server/sample-server.py"] }
通过点击服务器条目顶部的 Start Server 链接确保服务器正在运行。
-
你还需要选择哪些模型可以与采样一起使用。为了进行选择,打开 扩展 视图,并注意底部的 MCP Servers – installed 部分。点击齿轮图标,为已安装的服务器配置 Model Access,并选择允许用于采样的模型,例如 Claude Sonnet。
-
在 VS Code 中打开 GitHub Copilot Chat 窗口,并确保聊天中选择了 Agent 模式(在顶部选择该图标或通过命令面板运行 Chat: Open Chat 命令)。现在,输入以下提示:
"create product called tomato with keywords red and vegetable and delicious"
你应该看到一个对话框请求你的权限来运行它,一旦允许,就会从 create_product 产生工具响应。以下是底层的操作过程:
-
提示已解析。以下是发送给工具的内容:
{ "keywords": "red, vegetable, delicious", "product_name": "tomato" } -
调用了
create_product工具。 -
样本请求已发送到客户端。因为 VS Code 中的客户端有自己的 LLM,它会像这样对样本请求产生响应:

图 9.2 – VS Code 中的采样
让我们看看生成的描述:
Introducing our **Red Garden Medley**—a vibrant selection of the freshest, most delicious red vegetables nature has to offer! Each hand-picked assortment features juicy tomatoes, crisp red bell peppers, and sweet red radishes, bursting with flavor and color. Perfect for salads, roasting, or snacking, these vegetables not only brighten your plate but also deliver a powerhouse of vitamins and antioxidants. Enjoy the taste of freshness with every bite—delicious, nutritious, and naturally red!
你不会买那个番茄吗? 😃
好的,太棒了。我们在 MCP 服务器上成功实现了采样,VS Code 作为客户端运行,确保了其正常工作。那么,在实际解决方案中集成这一功能时,我们如何实际构建一个客户端呢?这是一个很好的问题,我们将在下一节中讨论。
客户端实现
首先,你需要让服务器知道你支持采样作为一项功能。为此,你需要在创建客户端实例时传递配置,如下所示:
{
"capabilities": {
"sampling": {}
}
}
好的,我们需要了解什么?首先,采样是在你调用工具、资源和提示的正常流程之外的。你可能会想,这意味着什么。让我们看看我们如何监听传入的采样请求:
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write,
sampling_callback=handle_sampling_message) as session:
await session.initialize()
# call tools, resource and prompts and read responses
这段代码展示了我们通常如何连接到 MCP 服务器并初始化进程,以便我们稍后可以调用工具或执行我们想要的任何操作。不过,有一个区别:注意 sampling_callback=handle_sampling_message。这很重要,因为它允许我们监听传入的消息。
让我们看看 handle_sampling_message:
async def call_llm(prompt: str, system_prompt: str) -> str:
client = OpenAI(
base_url="https://models.github.ai/inference",
api_key=os.environ["GITHUB_TOKEN"],
)
response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": system_prompt,
},
{
"role": "user",
"content": prompt,
}
],
model="openai/gpt-4o-mini",
temperature=1,
max_tokens=200,
top_p=1
)
return response.choices[0].message.content
async def handle_sampling_message(
context: RequestContext[ClientSession, None], params:
types.CreateMessageRequestParams
) -> types.CreateMessageResult:
print(f"Sampling request: {params.messages}")
# 1\. parse out the incoming prompt
message = params.messages[0].content.text
# 2\. call the llm to get a response on our prompt query
response = await call_llm(message, "You're a helpful assistant,
keep to the topic, don't make things up too much but
definitely create a compelling product description")
# 3\. create the sample response
return types.CreateMessageResult(
role="assistant",
content=types.TextContent(
type="text",
text=response,
),
model="gpt-3.5-turbo",
stopReason="endTurn",
)
在前面的代码中,有三件事情我们应该注意:
-
在步骤
1中,这是我们对传入的产品消息(如果你愿意,可以称之为我们的任务)进行解析的方式。这是我们将其发送给 LLM 以获取响应的内容。在这种情况下,我们的响应应该是一个产品描述。 -
在步骤
2中,我们调用 LLM 以获取响应。 -
最后,我们构建一个样本响应并将其发送回 MCP 服务器。
辅助函数使用提示和系统消息调用 GitHub 模型,并解析出 LLM 的响应。
就这样,如果你现在测试运行客户端:github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter09/code/python/README.md
你应该看到以下类似的响应:
[08/16/25 19:31:40] INFO Processing request of type CallToolRequest server.py:624
Sampling request: [SamplingMessage(role='user', content=TextContent(type='text', text='Create a product description about paprika described by as red, juicy, vegetable', annotations=None, meta=None))]
[08/16/25 19:31:43] INFO Processing request of type ListToolsRequest server.py:624
result: {"id": 1, "name": "paprika", "description": "**Product Description: Paprika \u2013 The Vibrant Touch of Flavor**\n\nElevate your culinary creations with our premium Paprika, a stunning red spice derived from the most luscious, juicy peppers. This vibrant addition is more than just a seasoning; it\u2019s a burst of color and taste that brings warmth and depth to every dish.\n\nOur Paprika is sourced from high-quality, sun-ripened vegetables, meticulously harvested at their peak to ensure maximum flavor. With its rich, sweet notes and subtle smokiness, this natural spice delivers a delightful punch that enhances everything from savory stews and roasted meats to vibrant vegetable dishes and sauces.\n\nNot only is our Paprika a feast for the eyes with its brilliant red hue, but it's also packed with antioxidants and vitamins, making it a nutritious choice for health-conscious cooks. Whether you sprinkle it onto a beloved family recipe or use it to create something intentionally new, our Paprika is versatile enough to brighten any meal.\n\nTransform everyday cooking into an extraordinary experience with the irresistible"}
INFO Processing request of type CallToolRequest server.py:624
result: {
"id": 1,
"name": "paprika",
"description": "**Product Description: Paprika – The Vibrant Touch of Flavor**\n\nElevate your culinary creations with our premium Paprika, a stunning red spice derived from the most luscious, juicy peppers. This vibrant addition is more than just a seasoning; it's a burst of color and taste that brings warmth and depth to every dish.\n\nOur Paprika is sourced from high-quality, sun-ripened vegetables, meticulously harvested at their peak to ensure maximum flavor. With its rich, sweet notes and subtle smokiness, this natural spice delivers a delightful punch that enhances everything from savory stews and roasted meats to vibrant vegetable dishes and sauces.\n\nNot only is our Paprika a feast for the eyes with its brilliant red hue, but it's also packed with antioxidants and vitamins, making it a nutritious choice for health-conscious cooks. Whether you sprinkle it onto a beloved family recipe or use it to create something intentionally new, our Paprika is versatile enough to brighten any meal.\n\nTransform everyday cooking into an extraordinary experience with the irresistible"
}
客户端执行以下两个操作:
-
它使用产品名称和关键词调用
create_product工具,并最终生成一个具有 LLM 生成的描述的产品,如下所示:Product Description: Paprika – The Vibrant Touch of Flavor**\n\nElevate your culinary creations with our premium Paprika, a stunning red spice derived from the most luscious, juicy peppers. This vibrant addition is more than just a seasoning; it's a burst of color and taste that brings warmth and depth to every dish.\n\nOur Paprika is sourced from high-quality, sun-ripened vegetables, meticulously harvested at their peak to ensure maximum flavor. With its rich, sweet notes and subtle smokiness, this natural spice delivers a delightful punch that enhances everything from savory stews and roasted meats to vibrant vegetable dishes and sauces.\n\nNot only is our Paprika a feast for the eyes with its brilliant red hue, but it's also packed with antioxidants and vitamins, making it a nutritious choice for health-conscious cooks. Whether you sprinkle it onto a beloved family recipe or use it to create something intentionally new, our Paprika is versatile enough to brighten any meal.\n\nTransform everyday cooking into an extraordinary experience with the irresistible
难道这不像是你想要购买的辣椒吗?想象一下将这个发送给 LLM 并要求它生成一张图片(可选作业)。那会是一张多么令人惊叹的图片。
- 它调用
get_products工具,列出新添加的产品。
如你所见,这是一种将任务委托给客户端的绝佳方式,因为客户端更适合处理这些任务。
摘要
关于“采样”一词的含义,是指分析一个小样本。在 MCP 的上下文中,这涉及到委托,即服务器如何将部分工作委托给客户端。
一个场景通常由用户启动,例如撰写博客文章或希望在后台解决方案中创建产品。服务器最终创建一个采样请求,并将其作为需要帮助的任务的一部分发送给客户端。然后,客户端能够使用 LLM 响应来回应该请求。
应该还指出,样本请求包含有关模型、令牌使用、系统提示等方面的建议,并且应该涉及一个人类,该人类可以接受这些建议或根据他们的喜好进行更改。
这是一个很棒的功能,其中客户端的 LLM 可以被调用以提供帮助。
在下一章中,我们将深入探讨 MCP 的另一个强大功能,即“诱导”,这是通过设置一个流程来改善用户体验,在该流程中,用户被要求选择另一个选项或提供更多信息以帮助服务器更好地完成任务。
任务
在这个任务中,我们将把我们对在客户端和服务器中添加采样实现的所有知识应用到有趣的应用领域,即神秘游戏和对话部分。我们的想法是创建一个有趣的对话角色,它可以通过预编程的响应进行交谈。
这就是它的工作方式:
-
用户:与角色 N 交谈
-
服务器:检索角色信息
-
服务器:发送带有字符信息的采样请求
-
客户端:接收采样请求并使用 LLM 生成响应
-
服务器:存储响应以进行缓存和记录
为了帮助您,想象一下一个角色在 JSON 文件中定义如下:
[
{
"id": "1",
"name": "Monsieur Lestrange",
"description": "a 600 year old vampire",
"personality": "very polite and will tell you a great deal of what
it's like paying the electricity bill of a 1200 year old castle
with bad insulation. In fact, he's quite boring and would rather
talk about that over what you would expect like vampire hunters,
stakes etc"
}
]
解决方案
测验
使用抽样的原因是什么?
-
A: 您想从大量数据集中采样一小部分信息
-
B: 服务器需要帮助完成一个生成式 AI 类型的任务,客户可以使用其 LLM 来生成响应
-
C: 客户需要服务器的帮助来完成一项任务
谁发起抽样请求?
-
A: 两者之一
-
B: 客户
-
C: 服务器
|
现在解锁这本书的独家优惠
扫描此二维码或访问packtpub.com/unlock,然后通过书名搜索此书。 | 
|
| 注意: 在开始之前准备好您的购买发票。 |
| --- |
第十章:启发式方法
启发式意味着获取或产生某物的过程,尤其是信息或反应。
为什么这对 MCP 来说很重要?官方文档中有以下说明:
模型上下文协议(MCP)为服务器提供了一种标准化的方式,在交互过程中通过客户端请求用户额外的信息。这种流程允许客户端在保持对用户交互和数据共享控制的同时,使服务器能够动态地收集必要的信息。
那么,这意味着什么?这意味着,由于某种原因,服务器发现它需要额外涉及客户端,以便向用户请求更多信息。现在目的更明确了,对吧?
好吧,想象一下以下情况:作为一个用户,你正在尝试预订假日旅行,而你搜索的日期不可用。通过使用启发式方法,你可以改善这种情况——也就是说,作为服务器,你不仅说这次旅行不可用,还努力提出额外的问题,并可能建议这个日期附近的可用日期。现在,你可能会看到,这可能会在许多情况下非常有用,比如增加销售或预订的机会等等。
在本章中,我们将学习以下内容:
-
解释什么是启发式方法
-
学习何时使用它
-
构建启发式集成
本章涵盖了以下主题:
-
为什么需要启发式方法?
-
实现启发式方法
-
启发式流程
-
JSON-RPC 消息
-
实现服务器端功能
-
使用 VS Code 测试启发式方法
为什么需要启发式方法?
好吧,我们在本章的开头已经尝试描述了可能促使在 MCP 中使用启发式方法的情况,但让我们尝试总结一些主要动机因素:
-
任务复杂性:对于某些任务,在开始时提供所有必要的信息可能根本不可能。这可能是一些用户需要通过工作流程进行选择的情况。例如,用户在购买电影票时可能需要做出多个选择。他们可能一开始只想预订某一天的电影,但随后可能需要被询问是否需要高级座位或其他可定制的选项。或者,考虑预订火车票的情况,你可能需要做出选择,比如是否需要带编号的座位,票是实体票还是电子票,等等。你可以一开始就要求所有这些信息,但这可能会让用户体验变得繁琐,可能更好的做法是先要求较少的输入。
-
提高网站上的转化率:另一个角度是公司确保他们有一个更好的转化率,这意味着网站上更多的用户成为实际客户。例如,如果用户想要一件红色的毛衣,而您目前没有库存,那么您可能想询问用户是否可以接受其他颜色,或者想要注册等待名单,以便在库存到来时自动订购。这种行为更有可能提高公司的销售额。
-
提升用户体验:作为前面角度的合乎逻辑的结论,如果用户遇到的不只是“不”,而是合理的选项,那么整体的用户体验很可能会得到提升。
总的来说,引出可以是一个改善您应用程序的绝佳方式。让我们接下来尝试看看实现方面的内容。
实施引出
因此,我们想使用引出——很好。但首先,有一系列我们应该知道的指南。以下是官方文档的说明:
为了信任、安全和安全:
- 服务器不得使用引出请求敏感信息
应用程序应该:
-
提供一个界面,使服务器请求信息的来源清晰可见
-
允许用户在发送前审查和修改他们的回复
-
尊重用户隐私并提供清晰的拒绝和取消选项
引出流程
通常,在引出过程中发生的情况是,服务器决定它没有足够的信息来完成对工具、资源或提示的调用。
需要理解的重要一点是,这个过程是一个两步的过程:
-
服务器会询问客户端是否可以发起一个针对用户的引出请求。
-
客户端要求用户提交信息。
用户可以在 1)和 2)中接受或拒绝;请参见以下序列图解释此过程。为了使理解更简单,我们选择了一个预订旅行的过程:

图 10.1 – JSON-RPC 消息
JSON-RPC 消息
现在我们已经解释了整体流程,让我们看看 JSON-RPC 消息看起来像什么。
就像大多数 MCP 功能一样,在 JSON-RPC 中需要发送和接收特定的消息:
请求消息
{
"jsonrpc": "2.0",
"id": 1,
"method": "elicitation/create",
"params": {
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
}
}
在前面的消息中,我们可以看到message包含了我们请求的负载——用户需要做出的选择或对我们请求的解释。requestedSchema是一个定义您作为服务器需要什么信息的模式,因此在这里,您需要指定所需项目的名称、类型以及您想要施加的任何其他规则。请参见以下示例模式,其中服务器请求name、email和age。对于每个参数,我们指定了类型和描述,在某些地方,我们还指定了格式甚至验证规则,例如您至少需要 18 岁:
"requestedSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Your full name"
},
"email": {
"type": "string",
"format": "email",
"description": "Your email address"
},
"age": {
"type": "number",
"minimum": 18,
"description": "Your age"
}
},
"required": ["name", "email"]
}
让我们看看一个响应。具体来说,这是一个接受类型的响应,其中用户同意提交他们被请求的信息。
响应消息
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"action": "accept",
"content": {
"name": "Monalisa Octocat",
"email": "octocat@github.com",
"age": 30
}
}
}
实际上,用户可以说,“我不想提供这个信息”。如果发生这种情况,那么就会发送一个拒绝类型消息作为响应,看起来是这样的:
拒绝消息
{
"sonrpc": "2.0",
"id": 2,
"result": {
"action": "decline"
}
}
在这里,很明显用户拒绝提交请求的信息。
第三种响应类型是取消。它与拒绝非常相似,但更像用户通过输入Escape、点击关闭对话框来忽略引发对话框,所以这更像用户忽略了交互,而不是明确地说不。
请求架构类型
我们提到了一般请求架构和示例。然而,支持的类型相当多,了解它们的存在非常重要,这样你才能正确使用它们。这些类型作为引发过程的一部分呈现给用户,这意味着用户可以使用下拉列表、文本输入字段或某些其他 UI 元素来提供信息。
-
string:这一类型是关于请求一个字符串。你可以添加相当多的检查。下面是这个架构的样子:{ "type": "string", "title": "Display Name", "description": "Description text", "minLength": 3, "maxLength": 50, "pattern": "^[A-Za-z]+$", "format": "email" }
在这里,你可以看到你可以限制minLength和maxLength,甚至设置模式,这在你需要请求一个特定的允许结构时非常有用,例如地址、电话号码、社会保险号码等等。
-
number:这一类型稍微简单一些,但通过设置最小值和最大值,它有助于用户了解允许和不允许的内容。看看这个架构:{ "type": "number", // or "integer" "title": "Display Name", "description": "Description text", "minimum": 0, "maximum": 100 }
看看minimum和maximum值,你可以指定这些值。
-
boolean:对于这种类型,想法是让用户回答是或否。你也可以指定是否应该有一个默认值:{ "type": "boolean", "title": "Display Name", "description": "Description text", "default": false } -
enum:这一类型应该被视为一个选项列表,如果你想让用户在不同的日期之间选择旅行,例如,这可能会很有用:{ "type": "string", "title": "Display Name", "description": "Description text", "enum": ["option1", "option2", "option3"], "enumNames": ["Option 1", "Option 2", "Option 3"] }
考虑到这一点,让我们看看我们是否可以使用这些类型中的几个,因为我们将展示下一节中的实现部分。
实现服务器端功能
让我们从服务器开始。我们需要知道的是,服务器功能、工具、资源或提示应该运行其过程,如果它检测到需要更多信息,它应该生成一个引发消息。
首先,让我们看看我们如何生成这样的消息。首先,我们有一个if语句来检查是否应该生成消息。如果是这样,我们在上下文对象上调用elicit,同时提供一个message和一个要遵守的schema:
class BookingPreferences(BaseModel):
"""Schema for collecting user preferences."""
checkAlternative: bool = Field(description="Would you like
to check another date?")
alternativeDate: str = Field(
default="2024-12-26",
description="Alternative date (YYYY-MM-DD)",
)
def is_available_date -> bool:
pass
@mcp.tool()
def book_table(date: str,ctx: Context[ServerSession, None]) -> str:
# 1\. Check if data is available
if not is_available_date(date):
result = await ctx.elicit(
message=(f"No trips available on {date}. Would you
like to try another date?"),
schema=BookingPreferences,
)
然后应该检查用户和客户端的响应:
if result.action == "accept" and result.data:
if result.data.checkAlternative:
return f"[SUCCESS] Booked for {result.data.alternativeDate}"
return "[CANCELLED] No booking made"
return "[CANCELLED] Booking cancelled"
在这里,你可以看到我们如何调查action属性,以查看用户是否接受了主要操作并提交了额外的数据。如果是这样,我们就开始解析所选择的内容。如果没有接受,那么我们就发送一个取消消息。
现在,让我们一起看看:
from pydantic import BaseModel, Field
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
mcp = FastMCP(name="Elicitation Example")
class BookingPreferences(BaseModel):
"""Schema for collecting user preferences."""
checkAlternative: bool = Field(description="Would you like
to check another date?")
alternativeDate: str = Field(
default="2024-12-26",
description="Alternative date (YYYY-MM-DD)",
)
@mcp.tool()
async def book_trip(date: str, ctx: Context[ServerSession, None]) -> str:
"""Book a trip with date availability check."""
# Check if date is available
if not is_available_date(date):
# Date unavailable – ask user for alternative
result = await ctx.elicit(
message=(f"No trips available on {date}. Would you
like to try another date?"),
schema=BookingPreferences,
)
if result.action == "accept" and result.data:
if result.data.checkAlternative:
return f"[SUCCESS] Booked for
{result.data.alternativeDate}"
return "[CANCELLED] No booking made"
return "[CANCELLED] Booking cancelled"
# Date available
return f"[SUCCESS] Booked for {date}"
现在,让我们继续使用 VS Code 测试激发功能。
使用 VS Code 测试激发
要使用 VS Code 测试激发功能,您需要将 MCP 服务器添加到 mcp.json 文件的条目中,如下所示:
"server": {
"type": "sse",
"url": "http://localhost:8000/sse"
}
然后,在输入以下提示之前,请确保您处于 Agent 模式:
Book trip on 2025-02-01
您应该在用户界面中看到以下情况:
- 输入提示并工具调用:在这里,您输入您的请求以预订旅行,系统将其识别为工具调用。您需要批准工具调用才能继续:

图 10.2 – 输入提示并查看工具调用
- 批准工具调用:一旦您批准了工具调用,您应该看到界面告诉您所选数据正在忙碌,您将被要求做出响应,这意味着现在它将带您进入构建激发响应的阶段:

图 10.3 – 批准工具调用
- 构建激发响应:现在您需要根据用户的输入和系统的要求构建一个响应。这涉及到使用您之前定义的激发模式来收集所需的所有额外信息。以下是在用户界面中的样子。在下面的屏幕截图中,您会被问及是否想要做出响应。如果您选择 true,它将继续询问您另一个日期;如果不选择,它将停止激发过程:

图 10.4 – 构建激发响应
- 对激发做出响应:一旦您选择继续,您需要填写替代日期,如下所示:

图 10.5 – 对激发做出响应
- 最终结果:因为您已经提交了另一个日期,服务器将检查此响应是否可行。在下面的屏幕截图中,您可以看到这是正确的,并且您收到了预订确认:

图 10.6 – 最终结果
这就完成了服务器端的激发。
在客户端实现激发
太好了——现在我们对服务器端有了很好的理解,甚至知道如何使用 VS Code 进行测试。让我们继续实现客户端。
通常,当编写客户端时,您会处理一个 ClientSession 对象。这允许您管理到服务器的连接。除了 read_stream 和 write_stream,您还可以传递 elicitation_callback 来处理任何激发事件:
async with ClientSession(
read_stream,
write_stream,
elicitation_callback=elicitation_callback_handler) as session:
让我们看看 elicitation_callback_handler:
async def elicitation_callback_handler(context:
RequestContext[ClientSession, None], params: ElicitRequestParams):
print(f"[CLIENT] Received elicitation data: {params.message}")
如您所见,此处理程序接受两个参数:请求上下文和诱导请求参数。我们现在需要做的是创建一个客户端响应,并将其发送回服务器。您可以向服务器发送三种可能的响应:
-
accept:这意味着我们希望诱导过程继续。如果我们给出这样的响应,我们还应该遵守服务器设定的输入模式。例如,以下答案将符合:return ElicitResult(action="accept", content={ "checkAlternative": True, "alternativeDate": "2025-01-01" }) # should book 1 jan instead of initial 2nd Jan
在这里,您可以看到我们首先将action属性设置为accept,并将content设置为有效载荷,其中模式采用checkAlternative和alternativeDate。注意我们如何硬编码响应。在一个更类似生产的应用程序中,分配给alternativeDate的值应该是询问用户输入的结果。
-
decline:这意味着我们想要停止诱导过程。我们可以发送这样的响应:return ElicitResult(action="decline")
在这里,我们根本未设置content,这使得我们明确表示我们不会提供任何关于下降的额外信息或背景。这种下降响应在处理过程的早期就发生了。这可以比作用户简单地说不,而不提供更多细节。
-
accept:用户接受响应,但随后拒绝提供替代日期:# 1\. refuses no select other date return ElicitResult(action="accept", content={ "checkAlternative": False }) # should say no booking made, WORKS -->
这种下降发生在处理过程的一段时间后,在用户被提供选择替代日期的选项之后。
让我们尝试通过这个序列图来展示所有内容:

图 10.7 – 在 Python 客户端中实现诱导的流程
快速提示:需要查看此图像的高分辨率版本?请使用下一代 Packt Reader 打开此书或在其 PDF/ePub 副本中查看。
下一代 Packt Reader以及此书的免费 PDF/ePub 副本包含在您的购买中。扫描二维码或访问packtpub.com/unlock,然后使用搜索栏通过名称查找此书。请仔细检查显示的版本,以确保您获得正确的版本。

如您所见,通常,有几个排列组合需要跟踪用户可以在不同阶段取消的情况,但关键是确保客户端能够优雅地处理这些不同场景。
摘要
在本章中,我们详细介绍了诱导过程,包括它是如何启动的以及它可能发生的不同场景。我们还探讨了在此过程中可以向服务器发送的各种响应。诱导是指系统积极寻求从用户那里获取更多信息以完成请求或澄清意图。
我们还看到了用户如何在处理的不同阶段接受以及取消。
最后,启发式方法可以是一个强大的工具,用于改善用户交互并确保系统能够有效地满足用户需求。
在下一章中,我们将探讨如何使用各种身份验证方法(如基本认证、JWT 和 OAuth2.1)来保护您的 MCP 服务器和客户端。
作业
到目前为止,您已经看到了处理预订场景的代码。现在,您的任务是实现一个场景,其中用户完成预订流程,但被问及他们是否想要成为会员以获得未来预订的折扣。使用以下代码github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter10/code/README.md。
解决方案
测验
当以下情况发生时,启发式过程在技术上启动:
-
A: 服务器确定需要从用户那里获取更多信息以完成请求。
-
B: 用户提供了需要进一步澄清或详细说明的输入。
-
C: 系统在继续之前需要确认用户的意图。
|
立即解锁本书的独家优惠
扫描此二维码或访问packtpub.com/unlock,然后通过书名搜索此书。 | 
|
| 注意:在开始之前,请准备好您的购买发票。* |
| --- |
第十一章:保护您的应用程序
在许多情况下,在将 Web 应用程序投入生产之前,确保其安全是一个先决条件。当然,有些情况下你可能不需要这样做,但在大多数情况下,你想要确保你的应用程序是安全的,并且只有授权用户可以访问其某些部分。
让我们看看一些场景以及针对每个场景应考虑的安全措施:
| 场景 | 敏感度级别 | 安全措施 | 理由 |
| --- | --- | --- | --- |
| 任何人都可以访问的公开数据 | 极小风险暴露 | 基本安全措施或无 | 如果你想要防止机器人或类似程序过度使用你的 API,可以选择让用户注册 API 密钥 |
| 一些公开数据和一些受保护数据 | 中等风险暴露 | 基本安全措施(HTTPS 和 API 密钥) | 在允许公开访问非敏感数据的同时保护敏感数据 |
| 敏感个人信息(例如,健康记录和财务信息) | 高风险暴露 | 高级安全措施(HTTPS、OAuth 2.0/2.1、加密和 RBAC) | 遵守通用数据保护条例(GDPR)和健康保险可携带性和问责法案(HIPAA)等规定,并确保数据隐私和完整性 |
表 11.1 – 安全措施场景
考虑到这一点,让我们看看你可以在 Web 应用程序中实施的一些常见安全措施。
在本章中,您将学习以下内容:
-
在你的应用程序中添加基本认证
-
使用JSON Web Token(JWT)来保护您的应用程序
-
使用 OAuth2 来为您的应用程序提供更好的安全态势
本章涵盖了以下主题:
-
基本认证
-
使用 JWT 强化安全
-
JWT 是如何工作的?
-
创建 JWT
-
在我们的中间件(和 MCP 服务器)中集成 JWT
-
OAuth2
基本认证
基本认证是保护应用程序的最简单方法。它涉及在每个请求中向服务器发送用户名和密码。这绝对不是最安全的方法。如果它不够安全,为什么还要使用它?好吧,有些情况下,至少有一些安全比完全没有安全要好。例如,如果你有一个 API,它通常对公众开放,但你希望限制对某些端点的访问,基本认证可能是一个不错的选择。
它在底层是如何工作的?客户端在每个请求中发送一个Authorization头。这个头的值是单词Basic后跟一个空格和一个 Base64 编码的字符串,格式为username:password。然后服务器解码这个字符串并检查用户名和密码是否有效。以下是它的工作流程:

图 11.1 – 基本认证流程
有时基本认证由 API 密钥而不是用户名和密码组成。API 密钥以与用户名和密码相同的方式发送,但它只是一个标识客户端的单个字符串。然后服务器检查 API 密钥是否有效。
从代码的角度来看,以下是客户端发送带有基本认证的请求时的样子:
# send api key
import requests
import base64
api_key = 'your_api_key'
encoded_api_key = base64.b64encode(api_key.encode()).decode()
headers = {'Authorization': f'Basic {encoded_api_key}'}
response = requests.get('https://api.example.com/endpoint',
headers=headers)
print(response.json())
// send api key
const apiKey = 'your_api_key';
const encodedApiKey = btoa(apiKey);
const headers = new Headers();
headers.append('Authorization', `Basic ${encodedApiKey}`);
fetch('https://api.example.com/endpoint', { headers })
.then(response => response.json())
.then(data => console.log(data));
快速提示:使用AI 代码解释器和快速复制功能增强您的编码体验。在下一代 Packt Reader 中打开此书。点击复制按钮
(1)快速将代码复制到您的编码环境,或点击解释按钮
(2)让 AI 助手为您解释代码块。

新一代 Packt Reader随本书免费赠送。扫描二维码或访问packtpub.com/unlock,然后使用搜索栏通过书名查找本书。请仔细核对显示的版本,以确保您获得正确的版本。

为我们的 MCP 服务器使用基本认证
让我们利用这项技术来保护我们的 MCP 服务器。毕竟,我们不想让任何人都能访问我们的服务器,并可能滥用它。为了使这项技术生效,我们需要两样东西:
-
一个检查
Authorization头部并验证凭证的服务器中间件 -
发送带有
Authorization头部的客户端请求
实现 MCP 服务器并将其作为 Web 应用程序启动
首先,我们需要实现 MCP 服务器并将其作为 Web 应用程序启动。这样做,我们将有一个可以处理 MCP 请求和响应的 Web 服务器。完成这项工作后,我们可以向服务器添加中间件,以增加安全层。
这使用 Starlette;然而,对于您的 MCP 服务器来说,获取这个服务器并不容易。因此,您需要控制服务器的启动,并自己添加中间件。
首先,让我们创建我们的 MCP 服务器实例:
app = FastMCP(
name="MCP Resource Server",
instructions="Resource Server that validates tokens
via Authorization Server introspection",
host=settings["host"],
port=settings["port"],
debug=True,
)
其次,让我们看看我们如何创建 Starlette MCP 应用程序,它应该使用 Streamable HTTP 传输:
starlette_app = app.streamable_http_app()
接下来,我们需要运行服务器的启动代码。这是使用 Uvicorn 完成的:
async def run(starlette_app):
import uvicorn
config = uvicorn.Config(
starlette_app,
host=app.settings.host,
port=app.settings.port,
log_level=app.settings.log_level.lower(),
)
server = uvicorn.Server(config)
await server.serve()
run(starlette_app)
创建中间件
要创建我们的中间件,我们需要记住它应该如何工作。中间件应该检查Authorization头部,验证凭证,然后允许请求继续或返回错误响应。
考虑到这一点,以下是中间件可能的样子。我们需要中间件本身和一个验证令牌的函数。在 Starlette 中创建中间件是通过从BaseHTTPMiddleware派生来实现的。它有一个请求和一个call_next函数,如果验证成功,你应该调用它来继续请求。如果不成功,你应该返回一个带有错误代码的响应:
def valid_token(token: str) -> bool:
# remove the "Bearer " prefix
if token.startswith("Bearer "):
token = token[7:]
return token == "secret-token"
return False
class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
has_header = request.headers.get("Authorization")
if not has_header:
print("-> Missing Authorization header!")
return Response(status_code=401, content="Unauthorized")
if not valid_token(has_header):
print("-> Invalid token!")
return Response(status_code=403, content="Forbidden")
print("Valid token, proceeding...")
print(f"-> Received {request.method} {request.url}")
response = await call_next(request)
response.headers['Custom'] = 'Example'
return response
中间件的逻辑如下:
-
检查
Authorization头是否存在。如果不存在,则返回401 Unauthorized响应。 -
验证令牌;如果令牌无效,则返回
403 Forbidden响应。 -
如果令牌有效,继续请求并添加一个自定义头到响应中。我们通过调用
call_next(request)来继续请求,这将把请求传递给下一个中间件或实际的端点处理器。
最后的部分是将中间件添加到 Starlette 应用中:
middleware = [
Middleware(CustomHeaderMiddleware, header_value='Customized')
]
async def main():
print("Running MCP Resource Server...")
starlette_app = await setup(app)
print("Adding custom middleware...")
starlette_app.add_middleware(CustomHeaderMiddleware)
在此代码中,我们创建了一个中间件列表,并将CustomHeaderMiddleware添加到其中。然后,在main函数中,我们使用starlette_app.add_middleware(CustomHeaderMiddleware)将中间件添加到 Starlette 应用中。
测试中间件
要测试中间件,我们可以使用之前相同的客户端代码,但这次我们需要包含带有有效令牌的Authorization头。
下面是客户端代码可能的样子:
import requests
import base64
def get_auth_token():
api_key = "my_api_key"
token = base64.b64encode(api_key.encode()).decode()
return f"Bearer {token}"
headers = {'Authorization': get_auth_token()}
response = requests.get('http://127.0.0.1:8000/protected', headers=headers)
print(response.status_code)
print(response.text)
要使用 MCP 客户端进行测试,思路相同,但我们需要知道如何使用 MCP 客户端传递自定义头。下面是如何做到这一点的方法。
在这里,我们使用 MCP SDK 中的streamablehttp_client来创建一个客户端。我们通过headers参数传递Authorization头:
token = "secret-token"
async with streamablehttp_client(
url = f"http://localhost:{port}/mcp",
headers = {"Authorization": f"Bearer {token}"}
) as (
read_stream,
write_stream,
session_callback,
):
async with ClientSession(
read_stream,
write_stream
) as session:
await session.initialize()
# TODO, what you want done in the client,
e.g list tools, call tools etc.
让我们来看看 JWT。
使用 JWT 强化安全性
JWT 与基本认证相比有什么好处?嗯,使用 JWT,你可以对客户端可以做什么有更细粒度的控制。你可以在令牌中包含声明,指定客户端被允许做什么。例如,你可以包含一个声明,指定客户端只能读取数据,但不能写入数据。这看起来可能像这样:
{
"sub": "1234567890",
"name": "User Userson",
"admin": true,
"iat": 1516239022,
"exp": 1516242622,
"scopes": ["User.Read"]
}
此令牌负载指定客户端被允许读取用户数据。然后服务器可以检查令牌,看客户端是否有执行请求操作所需的范围。还有许多其他好处,例如以下内容:
-
无状态:服务器不需要存储任何会话信息。令牌包含所有用于验证客户端所需的信息。
-
可扩展性:由于服务器不需要存储任何会话信息,它可以轻松地进行横向扩展。
-
安全性:令牌可以被签名和/或加密,以确保其完整性和机密性。
-
灵活性:令牌可以包含任意数量的自定义声明,允许广泛的应用场景。
-
互操作性:JWT 是一个广泛采用的标准,使其与其他系统和服务的集成变得容易。
好的,所有这些都听起来很棒,但让我们来了解一下您需要做什么来将基本身份验证升级到 JWT。首先,让我们谈谈 JWT 是如何工作的。
JWT 是如何工作的?
JWT 是一种紧凑、URL 安全的表示声明的方式,用于在双方之间传输。JWT 中的声明编码为 JSON 对象。此 JSON 对象由三个部分组成,由点(.)分隔:
-
头部:这通常由两部分组成:令牌的类型(
JWT)和所使用的签名算法,例如 HMAC SHA256 或 RSA。头部通常看起来像这样:{ "alg": "HS256", "typ": "JWT" }
我们可以看到使用的算法是 HMAC SHA256,类型是JWT。这些信息对于服务器来说很重要,因为服务器需要知道如何验证令牌。
-
有效载荷:这包含声明。声明是关于实体(通常是用户)和附加数据的陈述。有三种类型的声明:注册、公共和私有声明。有效载荷通常看起来像这样:
{ "sub": "1234567890", "name": "User Userson", "admin": false, "iat": 1516239022, "exp": 1516242622, "scopes": ["User.Write", "User.Write"] }
此有效载荷代表一个 ID 为1234567890的用户,名为User Userson,不是管理员,并且具有User.Write和User.Read作用域。iat声明表示令牌签发的时间,而exp声明表示令牌过期的时间。
- 签名:这用于验证 JWT 的发送者是否为声明者本人,并确保消息在传输过程中未被更改。
创建 JWT
好的,所以我们知道了我们有哪些部分,但我们如何创建 JWT 呢?实际上,这相当简单。您可以使用库为您创建 JWT。以下是您如何做到这一点的示例:
# pip install PyJWT
import jwt
import datetime
def create_jwt():
header = {
"alg": "HS256",
"typ": "JWT"
}
payload = {
"sub": "1234567890", # Subject (user ID)
"name": "User Userson", # Custom claim
"admin": True, # Custom claim
"iat": datetime.datetime.utcnow(),# Issued at
"exp": datetime.datetime.utcnow() +
datetime.timedelta(hours=1), # Expiry
"scopes": ["Admin.Write", "User.Read"] # Custom claim for
scopes/permissions
}
secret = "your-256-bit-secret"
token = jwt.encode(payload, secret, algorithm="HS256", headers=header)
return token
使用这段代码,您可以创建一个包含头部、有效载荷和签名的 JWT。jwt.encode函数负责编码头部和有效载荷,并使用密钥对令牌进行签名。签名是作为jwt.encode函数的一部分创建的。现在,token变量是 Base64 编码格式,可以用作Authorization头部的承载令牌。如果您移除 Base64 编码,您会看到令牌由三个部分组成,由点(.)分隔,即头部、有效载荷和签名,如前所述。但是,要解码令牌,您会看到头部和有效载荷以 JSON 格式显示。
验证 JWT
我们还需要了解如何验证 JWT。我们在验证时确保令牌有效、未过期,以及签名正确。这绝对不是我们能做的全部,但这是一个良好的开始。以下是您如何验证 JWT 的方法:
import jwt
def validate_jwt(token: str) -> bool:
secret = "your-256-bit-secret"
try:
decoded = jwt.decode(token, secret, algorithms=["HS256"])
print("Decoded claims:")
for key, value in decoded.items():
print(f" {key}: {value}")
return True
except jwt.ExpiredSignatureError:
print("Token has expired")
return False
except jwt.InvalidTokenError:
print("Invalid token")
return False
此代码检查令牌是否有效、未过期,以及签名是否正确。如果令牌有效,它将打印解码的声明。如果令牌已过期或无效,它将打印错误信息。
我们之前提到,这些结构检查是一个良好的开始。我们还应该进行哪些其他检查?以下是一些您可以检查的想法:
-
iss(发行者)声明,以确保令牌是由受信任的权威机构签发的,例如您的认证服务器。 -
aud(受众)声明确保令牌是针对你的应用的。有效的值可以是你的 MCP 服务器 URL。 -
nbf(not before)声明确保令牌在特定时间之前不被使用。 -
范围或角色,以确保客户端具有执行请求操作所需的权限。范围示例可以是
User.Read、User.Write、Admin.Read和Admin.Write,角色可以是User、Admin等。它们看起来很相似,但范围通常比角色更细粒度。
在我们的中间件(和 MCP 服务器)中集成 JWT
到目前为止,你已经看到了我们如何执行基本身份验证并检查凭证是否有效,作为我们中间件的一部分。现在,让我们看看我们如何集成 JWT 验证。我们的计划如下:
-
创建一个用于测试的 JWT。我们将使用实用脚本来完成。在实际应用中,你会从身份提供者(IDP)如 Auth0、Keycloak 或 Entra ID 获取令牌。
-
更新中间件以验证 JWT。
-
更新客户端以在
Authorization头部发送 JWT。
创建用于测试的 JWT
这里是我们将用于创建 JWT 测试令牌的实用代码,包括生成 JWT 和验证它的函数:
import jwt
import datetime
def create_jwt():
header = {
"alg": "HS256",
"typ": "JWT"
}
payload = {
"sub": "1234567890", # Subject (user ID)
"name": "User Userson", # Custom claim
"admin": True, # Custom claim
"iat": datetime.datetime.utcnow(),# Issued at
"exp": datetime.datetime.utcnow() +
datetime.timedelta(hours=1), # Expiry
"scopes": ["Admin.Write", "User.Read"] # Custom claim for
scopes/permissions
}
token = jwt.encode(payload, "your-256-bit-secret",
algorithm="HS256", headers=header)
with open(".env", "w") as f:
f.write(f"JWT_TOKEN={token}\n")
def validate_jwt(token: str) -> str | None:
secret = "your-256-bit-secret"
try:
decoded = jwt.decode(token, secret, algorithms=["HS256"])
return decoded
except jwt.ExpiredSignatureError:
print("Token has expired")
return None
except jwt.InvalidTokenError:
print("Invalid token")
return None
if __name__ == "__main__":
create_jwt()
在此代码中,我们创建了一个包含头部、有效载荷和签名的 JWT。create_jwt函数生成令牌并将其写入.env文件。注意validate_jwt函数如何验证令牌,并在令牌有效时返回解码后的声明。
更新客户端以发送 JWT
让我们转到客户端。它需要从.env文件中加载令牌并将其发送到Authorization头部。以下是你可以这样做的方法:
import os
from dotenv import load_dotenv
load_dotenv()
def get_auth_token():
token = os.getenv("JWT_TOKEN")
return f"Bearer {token}"
headers = {'Authorization': get_auth_token()}
# omitted, creating and connecting the MCP client
更新服务器中间件以验证 JWT
最后,我们需要更新服务器中间件以验证 JWT。以下是我们可以这样做的方法:
from your_jwt_utility import validate_jwt # import the validate_jwt
function
```python
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
class JWTMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
token = request.headers.get("Authorization")
if not token or not token.startswith("Bearer "):
return Response("Unauthorized", status_code=401)
jwt_token = token.split(" ")[1]
decoded = validate_jwt(jwt_token)
if not decoded:
return Response("Unauthorized", status_code=401)
# TODO,检查现有用户、作用域等。
# 可选地将用户信息附加到 request.state
request.state.user = decoded
response = await call_next(request)
return response
```py
Now, the middleware checks for the `Authorization` header, validates the JWT, and either allows the request to proceed or returns a `401 Unauthorized` response. We’re also leaving a `TODO` task for you to check things such as existing users, scopes, and so on.
# OAuth2
We’ve definitely improved our security posture by moving from basic authentication to JWT. However, there’s still room for improvement. **OAuth2** is a widely adopted authorization framework that provides a more robust and flexible way to secure your application.
It allows you to delegate access to your resources without sharing credentials. What that means concretely is that there are three parties involved when accessing a resource:
* **Resource server**: This is the server that hosts the protected resources, in our case, the MCP server
* **Client**: This is the application that wants to access the protected resources, in our case, the MCP client
* **Authorization server**: This is the server that issues access tokens to the client after successfully authenticating the resource owner and obtaining authorization
What about the delegation part? Well, the resource owner (typically the user) can delegate access to the client by granting it an access token. The client can then use this access token to access the protected resources on behalf of the resource owner. This way, the client doesn’t need to know the resource owner’s credentials, and the resource owner can revoke access at any time by invalidating the access token. This is clearly a better approach than basic authentication. JWT, however, is often used to represent the access token in OAuth2\. So the real improvement with OAuth2 is that it represents a complete framework for managing access tokens, including how they are issued, validated, and revoked.
## OAuth2.1 code flow
OAuth2.1 is what’s supported by the MCP SDK. Or rather, the way it’s supported is by providing a middleware that lets you point out the following:
* **The authorization server**: This is used to issue and validate tokens
* **The resource server**: This is where your data lives and is typically your MCP server
* **The scopes you want to request**: This is where you define what access you want to the resource server
Here’s what the flow looks like:

Figure 11.2 – OAuth2.1 flow
**Quick tip**: Need to see a high-resolution version of this image? Open this book in the next-gen Packt Reader or view it in the PDF/ePub copy.
**The next-gen Packt Reader** and a **free PDF/ePub copy** of this book are included with your purchase. Scan the QR code OR visit [`packtpub.com/unlock`](https://packtpub.com/unlock), then use the search bar to find this book by name. Double-check the edition shown to make sure you get the right one.

The preceding flow tells us that the client will first check whether it has a valid token. If it does, it will use it to access the resource server. If not, it will ask the user to authenticate and authorize the client to access the resource server on its behalf. Once the client has a valid token, it can use it to access the resource server.
So, what does the MCP SDK do for us then? It provides middleware that lets us easily integrate OAuth2.1 into our MCP server. All we need to do is provide the following configuration options:
auth=AuthSettings(
issuer_url=AnyHttpUrl("https://auth.example.com"), #
授权服务器 URL
resource_server_url=AnyHttpUrl("http://localhost:3001"), # This
服务器 URL
required_scopes=["user"],
)
These are the fields of the middleware configuration:
* `issuer_url`: This is the URL of the authorization server that issues the tokens. In a real-world application, this would be the URL of your IdP, such as Auth0, Keycloak, Entra ID, and so on.
* `resource_server_url`: This is the URL of the resource server, in our case, the MCP server. This is used to validate that the token is intended for this resource server.
* `required_scopes`: This is a list of scopes that the client must have to access the resource server.
This middleware takes care of validating the token, checking the scopes, and ensuring that the token is intended for the resource server. It also handles the OAuth2 flow, including redirecting the user to the authorization server to obtain an access token if needed.
## OAuth 2.1 under the hood
To understand how the OAuth2.1 middleware works under the hood, let’s explain the OAuth2.1 code flow in more detail. Here’s how the flow works:
1. **Validate token or obtain authorization code**: The client checks whether it has a valid access token. If it does, it uses it to access the resource server. If not, it calls the `/authorize` endpoint on the authorization server to obtain an authorization code. This code is obtained when the client presents valid credentials and performs a login. The user is then redirected back to the client with the authorization code.
2. **Call** `/token` **to exchange authorization code for access token**: The client then calls the `/token` endpoint on the authorization server to exchange the authorization code for an access token. At this point, it’s ready to access the resource server.
3. **Access resource server**: Accessing the resource server is done by calling the desired endpoint and providing the access token in the `Authorization` header as a bearer token. The resource server then validates the token, checks the scopes, and ensures that the token is intended for this resource server. If everything checks out, it allows the request to proceed.
Just to get a sense of roughly what code is involved, here’s a simplified version of the OAuth2.1 middleware flow:
第 1 步:模拟浏览器重定向到/authorize
authorize_url = f"{AUTH_SERVER}/authorize?client_id={CLIENT_ID}
&redirect_uri={REDIRECT_URI}&state={STATE}
&code_challenge={CODE_CHALLENGE}&code_challenge_method=plain"
print(f"Requesting authorization: {authorize_url}")
response = requests.get(authorize_url, allow_redirects=False)
第 2 步:从重定向中提取授权代码
redirect_location = response.headers.get("Location")
如果没有重定向位置:
print("授权服务器未重定向。它是否正在运行?")
exit(1)
parsed_url = urlparse(redirect_location)
query_params = parse_qs(parsed_url.query)
auth_code = query_params.get("code", [None])[0]
print(f"收到授权代码: {auth_code}")
第 3 步:用代码交换访问令牌
token_response = requests.post(f"{AUTH_SERVER}/token", data={
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"code_verifier": CODE_VERIFIER
})
token_data = token_response.json()
access_token = token_data.get("access_token")
print(f"访问令牌: {access_token}")
第 4 步:调用资源服务器
resource_response = requests.get(f"{RESOURCE_SERVER}/userinfo", headers={
"Authorization": f"Bearer {access_token}"
})
print("用户信息响应:")
print(resource_response.json())
Here’s the code explained:
* **Step 1**: The client constructs the authorization URL and makes a `GET` request to the `/authorize` endpoint on the authorization server. The user is redirected to this URL to authenticate and authorize the client. The response contains a redirect URL with an authorization code.
* **Step 2**: The client extracts the authorization code from the redirect URL.
* **Step 3**: The client makes a `POST` request to the `/token` endpoint on the authorization server to exchange the authorization code for an access token.
* **Step 4**: The client makes a `GET` request to the resource server, providing the access token in the `Authorization` header as a bearer token. The resource server validates the token and returns the requested resource if the token is valid.
The MCP SDK provides a sample implementation of OAuth2.1 ([`github.com/modelcontextprotocol/python-sdk/blob/main/examples/servers/simple-auth/mcp_simple_auth/auth_server.py`](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/servers/simple-auth/mcp_simple_auth/auth_server.py)) that you’re encouraged to check out.
## Concluding thoughts on security with OAuth2.1
In a production scenario, the only code you would write yourself would be the client and the resource server. The authorization server would be a separate component/service, for example, handled via Entra ID if you’re using Azure, or Amazon Cognito if you’re on AWS. Auth0 is another good choice.
You need to decide what you are putting the authorization component in front of:
* **The web server**: If so, you can use standard web server middleware approaches with your chosen IdP, such as Entra ID, Amazon Cognito, Auth0, or similar.
* **The MCP server**: If choosing this, by all means, leverage auth components in the MCP SDK. You can still use standard middleware approaches here as well, but you will use the MCP SDK’s built-in authentication features.
* **A gateway (a reverse proxy)**: A gateway is a service such as Azure API Management or Gateway from AWS. Both of these services are capable of handling authentication and can simplify the architecture. Additionally, these services have features you might want to use around AI usage, such as content safety and semantic caching, and also features that help with resiliency and scaling.
Also, when it comes to authorization per tool, you might need to add extra code checks for the call to a tool to ensure that the user has the right scopes/permissions to use that tool. Different tools might require different scopes/permissions. This is not something that’s supported out of the box in the MCP SDK, but it’s something you can easily add yourself. Something such as the following, where `has_permission` is a function you implement to check whether the user has the required permission to call the tool:
PermissionToolMapping = {
"User.Read": ["GetUser", "ListUsers"],
"User.Write": ["CreateUser", "UpdateUser", "DeleteUser"],
"Admin.Read": ["GetAdmin", "ListAdmins"]
}
def has_permission(token: str, tool: str) -> bool:
decoded = validate_jwt(token)
if not decoded or "scopes" not in decoded:
return False
user_scopes = decoded["scopes"]
required_scopes = [scope for scope, tools in
PermissionToolMapping.items() if tool in tools]
return any(scope in user_scopes for scope in required_scopes)
@server.call_tool)
def call_tool((name: str, arguments: dict[str, Any], request:
types.CallToolRequest) -> types.CallToolResponse:
if(has_permission(token=request.headers["token"], tool=name):
proceed with calling tool
else:
raise Exception)
# 摘要
在本章中,你学习了保护你的 MCP 服务器和客户端的重要性。你看到了如何实现基本身份验证、JWT 和 OAuth2.1 以增强你应用程序的安全性。请记住,安全性是一个持续的过程,并且保持对最新最佳实践和技术更新的了解对于保护你的应用程序和数据至关重要。在我们的下一章和最后一章中,我们将探讨如何将你的 MCP 服务器推向生产并确保它是健壮和可扩展的。
# Assignment 1: 使用基本身份验证保护你的 MCP 服务器
在这个任务中,你被要求在你的 MCP 服务器中实现基本身份验证,这涉及到创建一个中间件来检查传入请求中的`Authorization`头。客户端应在`Authorization`头中发送凭证。这是保护你的 MCP 服务器的一个好步骤,但请记住,基本身份验证有其局限性,理想情况下应替换为更安全的方法。
请参考本节中关于基本身份验证提供的代码片段以获取指导。
# 解决方案 1
你可以在[`github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter11/code/basic/README.md`](https://github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter11/code/basic/README.md)访问解决方案。
# 任务 2:使用 JWT 保护你的 MCP 服务器
在这个任务中,你被要求通过在你的 MCP 服务器中实现 JWT 身份验证来改进你之前的任务。请参考本章关于 JWT 的部分以获取指导。
# 解决方案 2
你可以在[`github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter11/solutions/README.md`](https://github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter11/solutions/README.md)访问解决方案。
# 测验
以下哪种方法最安全?
+ A: 通过 HTTP 的基本身份验证
+ B: 通过 HTTP 的 JWT
+ C: OAuth 2.1
以下哪个选项最好地描述了在身份验证和授权上下文中的“范围”?
+ A: 客户端应用程序从资源服务器请求的具体权限或访问权限。
+ B: 用户会话的唯一标识符
+ C: 用于保护令牌的一种加密算法
你可以在[`github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter11/solutions/solution-quiz.md`](https://github.com/PacktPublishing/Learn-Model-Context-Protocol-with-Python/blob/main/Chapter11/solutions/solution-quiz.md)访问解决方案。
# 资源
+ 在这个免费课程中,整章内容都讲述了问题、攻击以及如何减轻它们:[`github.com/microsoft/mcp-for-beginners/tree/main/02-Security`](https://github.com/microsoft/mcp-for-beginners/tree/main/02-Security )
+ 本节课涵盖使用 MCP 的 Entra ID 安全:[`github.com/microsoft/mcp-for-beginners/tree/main/05-AdvancedTopics/mcp-security-entra`](https://github.com/microsoft/mcp-for-beginners/tree/main/05-AdvancedTopics/mcp-security-entra )
+ 此存储库展示了使用 Azure API Management 和 GitHub 上的 OAuth 的解决方案:[`github.com/Azure-Samples/mcp-auth-servers/blob/main/README.md`](https://github.com/Azure-Samples/mcp-auth-servers/blob/main/README.md )
+ 本章涵盖了安全最佳实践:[`github.com/microsoft/mcp-for-beginners/blob/main/05-AdvancedTopics/mcp-security/README.md`](https://github.com/microsoft/mcp-for-beginners/blob/main/05-AdvancedTopics/mcp-security/README.md)
|
#### 现在解锁这本书的独家优惠
扫描此二维码或访问 https://packtpub.com/unlock ,然后通过书名搜索此书。 |  |
| **注意***: 在开始之前准备好你的购买发票。 |
| --- |
# 第十二章:将 MCP 应用投入生产
欢迎来到本书的最后一章。很高兴你能走到这一步!你已经学会了如何构建服务器和客户端,也许你现在想知道如何迈出最后一步,将你所构建的内容与世界分享。
这是一个很好的问题,本章将指导你考虑所有应该考虑的事项,以确保你的 MCP 应用经过充分测试、可靠、安全,并在生产环境中表现良好。让我们开始吧!
本章涵盖了以下主题:
+ **架构和设计**:在这里,我们将探讨模块化设计、集成模式和由于 AI 带来的特定架构影响
+ **打包和分发**:在这里,我们将讨论不同的打包选项,包括独立、嵌入式、部署渠道和语义版本控制
+ **测试和部署自动化**:在这里,我们将探讨不同的测试策略,如单元测试、集成测试和 AI 测试,并确保这些测试在 CI/CD 管道中使用,以便有信心地部署
+ **运维和可观察性**:在这里,我们将讨论扩展、弹性模式、可观察性、治理和未来证明
# 架构和设计
在设计你的 MCP 应用时,有几个架构考虑因素需要记住。这包括模块化设计、集成模式和 AI 对架构的影响。在本节中,我们将探讨以下主题:
+ **集成模式**:MCP 如何融入你的现有架构,你可以使用哪些模式来确保无缝集成?
+ **文档**:你如何记录你的架构和设计决策,以确保清晰性和可维护性?
+ **架构审查**:在审查你的架构以确保它满足你的要求、可扩展、可维护和安全的时,需要考虑哪些关键方面?
现在我们有了概述,让我们深入探讨每个主题。
## 集成模式
MCP 自带一套集成模式,以促进客户端和服务器之间的无缝通信。然而,如果你使用流式 HTTP 或 SSE 这样的传输方式,那么这个服务器将通过一个使用 HTTP 的 Web 应用托管,很可能组织成一个 RESTful API。这可能意味着你需要开发或使用特定的中间件来处理 Web API 到 MCP 通信,以实现日志记录、安全等功能。以下是它可能的工作方式。注意 Web API 中使用的中间件。同时注意 Web API 很可能是 RESTful API,客户端和服务器使用 JSON-RPC,这是由 MCP 协议规定的:

图 12.1 – 集成模式流程
在这个序列图中,你可以看到不同的组件是如何相互交互的。用户向 Web API 发出请求,然后 Web API 将请求与其身份验证中间件进行比对。如果请求被允许,Web API 将请求数据转发给客户端。然后客户端使用 JSON-RPC 消息与服务器通信。
## 文档
我们很好地记录代码是很重要的,这样我们才能理解所有更大的部分以及它们是如何相互配合的。这将帮助当前和未来的开发者理解系统,并使其更容易维护和扩展。
文档对于开发者和用户来说都非常重要。良好的文档使代码更容易理解、维护和使用。在考虑文档时,需要考虑以下方面:
+ **记录代码并生成文档**:所有代码通常都应该有良好的文档,这意味着我们应该努力记录所有输入和输出的操作。作为开发者,我们知道过时的信息有时比没有文档更糟,因此我们应该努力从代码中生成文档。你想要的是让代码以 Open API 格式(以前称为 Swagger)生成文档。例如,这段代码的路线以易于使用框架从中生成文档的方式进行了记录:
```py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Booking(BaseModel):
id: int
title: str
description: str = False
when: str
@app.get("/booking/", response_model=Booking, tags=["bookings"],
summary="Book a trip", description="Lets user book a trip[]")
def book_trip():
return Booking(id=1, title="Trip to Paris", description="A
wonderful trip to Paris", when="2023-09-15")
```
当代码以 `tags`、`summary` 等方式记录时,这将用于自动生成 Open API 文档。无论你选择什么框架和运行时,都要确保你的代码易于记录且易于从代码中生成文档。
+ **测试也是文档**:测试有时是了解一个功能应该如何工作的最佳方式。它们提供了预期行为的具体示例,并有助于阐明复杂逻辑背后的意图。确保包含全面的测试,涵盖各种场景和边缘情况。
+ **包含上下文流程图和边缘情况处理**:Mermaid 图表正成为标准,并且 GitHub 也支持渲染各种流程图、序列图等,因此考虑使用 Mermaid 或其他创建这些图表的方法。直观地看到某物的工作方式可以节省开发者和代码使用者数小时的时间。
## 架构审查
你的架构应考虑以下方面:
+ **模块化设计**:确保所有功能都分离到逻辑边界内,以便所有区域都在它们自己的模块中。不同的运行时以不同的方式做这件事,但一个好的经验法则是你的代码应该以易于理解和维护的方式组织。此外,你应该努力将相关的代码放在一起,并最小化模块之间的依赖。最后,只有一个理由需要改变,这意味着每个模块应该只有一个职责。例如,你不会将解析逻辑和业务逻辑放在同一个模块中。
+ **验证**:实施强大的验证机制以确保数据完整性和正确性。这包括输入验证、输出验证以及不同组件之间的合同验证。这也有一个安全方面,因为恶意行为者可能会尝试利用系统中的漏洞。Python 使用 Pydantic 进行数据验证和设置管理,其 MCP SDK 利用这些验证库进行输入和输出,以解决正确性和安全问题。以下是一个使用输入验证的示例:
```py
from mcp.server.fastmcp import FastMCP
from uuid import uuid4
mcp = FastMCP(name="Tool Example")
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
users = []
@mcp.tool()
def create_user(user: User):
# Create user logic here
user.id = len(users) + 1
users.append(user)
return user
if __name__ == "__main__":
print("Starting MCP server...")
mcp.run()
```
在这个例子中,您可以看到有一个名为`create_user`的工具。它接受一个`User`模型作为输入,这将立即帮助进行验证和序列化。以下有效载荷将导致创建一个新用户:
```py
{
"id": 0,
"name": "chris",
"email": "chris@example.com"
}
另一方面,以下有效载荷将导致验证错误,因为缺少id:
{
"name": "chris",
"email": "chris@example.com"
}
这样,您可以设置对所有传入数据的验证,并确保只有正确数据进入您的系统。当然,从业务和安全的角度来看,在最终持久化数据之前,您可以根据需要添加更多验证规则。
-
AI 对架构的影响:如果您发布客户端和服务器,客户端很可能可以访问 AI 模型,这意味着从架构角度来看需要考虑一些特定的事情。这包括客户端如何与 AI 模型通信,如何管理客户端和服务器之间的上下文,以及如何处理任何潜在的故障或超时:
-
解耦架构:确保您解耦内容管理、模型调用和 UI。
-
延迟和可靠性,以及它们对 UI 的影响:在设计与 AI 模型交互时,也要考虑延迟和可靠性的影响。这意味着,例如,如果 AI 模型需要时间来返回响应,确保 UI 保持响应性并向用户提供反馈。如果 AI 因故障或速率限制而停止响应,需要优雅地处理这种情况。此外,确保您有回退机制,例如重试逻辑和断路器。我们将在本章后面更详细地讨论这一点。
-
令牌预算:令牌预算不仅是 AI 模型的运行成本需要考虑的因素,从架构角度来看也非常重要,因为我们需要实施缓存策略和其他机制来优化令牌使用。
-
-
安全:实施最佳安全实践以保护您的应用程序及其数据。这包括保护 API、管理用户身份验证和授权,并确保数据隐私以及符合法规。考虑您如何采用最小权限来最小化用户和服务器的访问权限。这意味着确保用户和服务只访问他们绝对需要的资源。这的一个后果是,您可能需要实施更细粒度的访问控制,并持续监控任何未经授权的访问尝试。
打包和分发
在你甚至输入第一行代码之前,你需要考虑你正在构建的内容。使用 MCP,你有不同的选项来打包和分发你的应用程序。
打包选项
让我们看看一些打包选项。
-
独立服务器:这是为公共访问或私人使用设计的。无论访问是私人的还是公共的,你都需要考虑身份验证、授权和数据隐私。但对于公司或组织内部的私人分发,你需要考虑可发现性,内部团队将如何找到并使用这项服务,以及是否符合内部政策。
-
嵌入式客户端/服务器:这样的系统可能由客户端和服务器组件组成,客户端很可能带有 AI 功能,因此请确保你考虑了这一点的影响,例如数据隐私和安全以及负责任的 AI 使用,以及可能适用的任何监管要求。
独立服务器
假设你的目标是仅构建一个 MCP 服务器;这意味着你很可能会专注于构建一个在你包装现有 API 之前不存在的 API。由于这是 MCP,你需要决定这个服务器应该在何处运行。以下是一些考虑因素:
-
本地机器:在这种情况下,服务器需要使用 STDIO 传输。此外,由于它运行在用户的机器上,从安全的角度来看,你如何确保它是沙盒化的并且无法访问它不应访问的资源?实际上,你是否应该允许它访问网络?这取决于你,但你应该考虑这些方面。参见这个文件系统 MCP 服务器,其中服务器附带配置,既限制了访问权限,指定了它有权访问的目录,还提供了如何在容器环境中运行它的说明,以确保 MCP 服务器尽可能少地访问(
github.com/modelcontextprotocol/servers/tree/main/src/filesystem)。 -
通过 URL 远程访问:如果你的服务器是远程访问的,你可能不需要过多关注沙盒化,但你确实需要考虑身份验证和授权,以及确保 API 端点安全的各个方面。对于身份验证/授权,考虑使用 OAuth2 或 API 密钥,并始终验证传入的请求。此外,考虑基于角色的访问控制(RBAC)以确保用户不会获得比他们需要的更多对各种服务器功能的访问权限。也就是说,考虑你是否需要管理员用户、普通用户或访客访问,以及所有资源应该具有什么类型的权限级别。
在这两种情况下,你很可能会将源代码存储在 GitHub 或类似的版本控制系统中的某个地方。
嵌入式客户端/服务器
将 MCP 集成到现有的架构中需要仔细规划。从分发角度来看,您的 MCP 实现可能会与您现有的服务和应用程序以相同的方式部署。从代码组织角度来看,您仍然可以将其创建为可调用的 API 或微服务架构的独立服务;选择权在您手中。但您需要意识到的是,交付 MCP 集成意味着您交付的不仅仅是服务器;您还需要交付一个 MCP 客户端。客户端将负责与 MCP 服务器通信、处理请求和管理上下文。
分发渠道
当涉及到分发渠道时,您有几个选择:
-
将服务器打包为 Docker 容器:如果您希望确保服务器在部署到任何位置时都能在一致的环境中运行,这是一个很好的选择。您可以将 Docker 镜像推送到容器注册库,如 Docker Hub 或 GitHub Container Registry,用户可以轻松地拉取并运行容器。使用此选项时,您应指定如何配置容器以及需要设置的任何环境变量,如下例所示:
FROM node:22.12-alpine AS builder WORKDIR /app COPY src/filesystem /app COPY tsconfig.json /tsconfig.json RUN --mount=type=cache,target=/root/.npm npm install RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev FROM node:22-alpine AS release WORKDIR /app COPY --from=builder /app/dist /app/dist COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package-lock.json /app/package-lock.json ENV NODE_ENV=production RUN npm ci --ignore-scripts --omit-dev ENTRYPOINT ["node", "/app/dist/index.js"]
上述示例来自示例文件系统 MCP 服务器(github.com/modelcontextprotocol/servers/tree/main/src/filesystem)。
-
通过包管理器分发:如果您的服务器是用 Node.js 或 Python 构建的,您可以分别将其打包为
npm模块或 PyPI 包。这使用户能够轻松地将您的服务器作为依赖项安装和管理在自己的项目中。对于.NET,相应的包管理器将是 NuGet,对于 Java,将是 Maven 或 Gradle。不同的包管理器对打包和分发您的代码有不同的规则,但通常涉及创建 README 文件、许可证以及将代码压缩成捆绑包。这对于您的 MCP 服务器用户查找和使用您所构建的内容来说是一个很好的方式。 -
仓库分发:您还可以通过直接提供源代码仓库的访问权限来分发您的服务器。这使用户能够克隆仓库并自行构建服务器,从而让他们对构建过程和依赖项有更多的控制。请确保您提供了如何运行和配置它的说明。Playwright 的 MCP 服务器(
github.com/RBC/microsoft-playwright-mcp)的此配置指令是一个很好的例子,展示了如何从 VS Code 或 Claude Code 等宿主开始使用它:{ "mcpServers": { "playwright": { "command": "npx", "args": [ "@playwright/mcp@latest" ] } } }
在本指令中,您可以看到如何使用npx启动服务器以及如何通过args提供参数。
采用语义版本控制
语义版本控制是我们应该采用的另一个代码规范。这是因为它使您和您的代码消费者更容易理解每个版本中变化的性质。
它支持以下版本:
-
当你进行不兼容的 API 更改时,这是一个主要版本
-
当你以向后兼容的方式添加功能时,这是一个次版本
-
当你进行向后兼容的错误修复时,这是一个补丁版本
你如何在软件中编码这取决于你对软件进行版本控制;例如,在版本 1.3.0 中,1 是主版本,3 是次版本,0 是修订版本。如果你只通过应用补丁来修复软件,那么你应该将其从 1.3.0 增加到 1.3.1。添加新功能应被视为次版本,因此你会将其从 1.3.0 增加到 1.4.0。许多更改,包括导致代码损坏的更改,被视为主要更改,因此版本应从 1.3.0 增加到 2.0.0。
通过这种方式编码,你创建了一个清晰且可预测的版本控制方案,向用户和开发者 alike 传达了更改的性质。因此,开发团队可以选择是否停留在 1.3.x(在这种情况下,他们只更新软件以修复错误和补丁更改),或者如果他们想要新功能但优先考虑稳定性,他们可以接受任何匹配 1.x.x 的版本,这将允许他们接收新功能,同时仍然处于一个稳定的基座上,不会带来破坏性更改的风险。
测试和部署自动化
测试是软件开发的关键部分,尤其是在与 AI 模型合作时尤为重要。自动测试有助于确保你的代码按预期运行,并能及早发现开发过程中的问题。此外,部署你的应用程序也应自动化,以确保一致性和可靠性。
测试策略
我们已经将测试作为一种文档形式提出来了。它也是确保代码按预期行为的关键部分。确保你编写的测试是全面的,并覆盖各种场景。你可能需要考虑以下测试:
-
单元测试:这些测试有助于确保单个组件按预期工作。在 MCP 的上下文中,考虑将解析逻辑拆分为单独的模块,以方便更容易地进行测试。
-
集成测试:这些测试验证了不同组件之间的交互,并确保它们按预期协同工作。对于 MCP,如果你的 MCP 集成是 Web 应用程序的一部分,那么设置端到端测试模拟用户与 UI 的交互可能是一个好主意。这样的测试将调用一个 Web 端点,该端点调用客户端,然后该客户端将调用 MCP 服务器功能来处理请求并返回响应。
-
AI 测试:由于 AI 模型可能表现出不可预测的行为,因此拥有专门验证其输出的测试至关重要。这包括测试各种输入场景和边缘情况,并确保模型的响应在可接受的参数范围内。考虑使用如对抗测试等技术来探测模型的弱点。对抗测试涉及创建旨在欺骗模型犯错的特定输入。同时,拥有一系列用于测试的提示也很不错,在这些提示下,系统应该运行良好或触发工具或其他功能。
通常,在测试中,关注各种方面,如性能、安全性和可用性。
部署自动化
构建应用、保护它、良好地架构和记录……这些都是好的,但没有一个稳健的部署流程,这一切都没有意义。那么,什么是稳健的呢?2025 年的稳健意味着我们可以点击一下按钮就部署某些东西,一天可以多次这样做,并且我们在部署时设置了多个安全措施。安全措施是我们确保测试运行并通过、遵循政策,并保持在某些指标内的步骤,例如,我们不使代码变慢,等等。
部署的方式有很多,例如 GitHub Actions 或 Jenkins。尽管如此,它们有一个共同点:要部署,你需要定义一个包含步骤的管道,并确保最终步骤导致一个可以部署的工件,或者你最终在生产中获得一个新的部署。
除了能够部署之外,我们还需要确保部署过程是可靠的并且可以一致地重复。此外,错误是会发生的,因此我们需要能够在出现问题的情况下回滚更改。
这对我们代码意味着什么?嗯,通常会导致创建一个.yml文件来表示之前提到的管道。
然后,你可能需要一个不同的配置或不同的环境,这也是需要考虑的。
对于 MCP 与普通软件相比,情况是否不同?嗯,区别在于意识到你可能需要发布 AI,因此你可能需要一个独立的 AI 部署管道,考虑模型性能、上下文管理等方面。
运营和可观察性
一旦你的应用程序投入生产,我们需要解决一系列问题:
-
可观察性:你知道你的应用表现如何吗?它是否在负载下运行,是否运行缓慢,是否失败,以及是否安全?
-
可扩展性和弹性:你的应用能否处理流量峰值?它能否进行扩展和缩减,并且对故障具有弹性?
-
监控和反馈:你知道何时出现问题吗?你能通知正确的人,并且你有反馈循环来随着时间的推移改进应用吗?
-
治理和合规性:你是否遵守了法规?你是否实施了正确的政策,并且是否负责任地管理数据?
-
未来证明:您为未来的变化做好准备了吗?您能否适应新技术,并且您是否持续改进您的应用?
可观测性
您真的知道您的应用表现如何吗?如果您知道,这意味着您在添加日志、追踪和指标方面非常勤奋。您可能已经添加了一个仪表板,以便您可以轻松地可视化所有这些,并且您甚至知道在需要时如何进行优化。我们大多数人渴望对我们的应用及其表现有这种程度的了解。当您为商业、客户等开发软件时,有很多风险。我们需要确保数据安全,应用需要以合理的速度响应并使用其被限制的资源,并且它需要正常工作。这听起来并不难,对吧?但实际上,尤其是当您开始为成千上万的客户或甚至数百万客户提供服务时,这尤其困难。但与其关注所有这些正确无误的难度,不如让我们谈谈我们至少需要实施的最重要的事情,以便能够观察我们的应用:
-
日志记录:日志记录对于理解您应用程序的行为至关重要。它有助于了解什么进入,什么出去,以及希望地,它通过应用程序的各个部分花费了多长时间。在适当的位置拥有日志记录可以帮助您识别瓶颈并优化性能。需要考虑的重要事项是日志级别(例如,info、debug 或 error)以及您包含的上下文(例如,用户 ID、请求 ID 等),以使您的日志更有用。
-
追踪:追踪对于理解请求通过您系统的流程至关重要。它允许您看到不同的服务如何交互以及瓶颈可能出现在哪里。实施分布式追踪以获得请求路径和延迟的完整视图。追踪与日志记录的不同之处在于它捕捉了请求的旅程。因此,它通常包含额外的信息,例如起源、目的地以及任何涉及的中间服务。
-
指标:指标关乎了解您服务器的健康状况、性能和可扩展性。因此,需要捕获的重要指标包括 CPU 和内存使用情况、请求吞吐量、响应时间和甚至错误率。捕获所有这些将为您提供对系统表现的良好理解。
对于可观测性,MCP 与传统应用并没有太大不同,但有一些独特的方面需要考虑,例如模型性能和令牌使用。对于 MCP 特别感兴趣的测量可能包括每个请求的令牌使用量与它被缓存或重用时的比较。此外,为了日志记录,MCP 内置了不同的日志,我们应该利用这些日志来指示错误、警告、正常日志等(modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging))。
可扩展性和弹性
部署 MCP 应用程序的另一个非常重要的方面是确保它们在负载下可以伸缩并保持弹性。您要解决的问题是要确保以下内容:
-
流量激增(Traffic spikes):您可以在不降低性能的情况下应对流量的突然增加。如果您是一家电子商务公司,这在业务关键时期尤为重要,您需要能够处理购物高峰时段购买量的增加。
-
弹性伸缩(Scaling up and down):您可以高效地管理资源以处理不同的负载。
-
弹性(Resilience):您可以从故障中快速恢复并最小化停机时间。做得好意味着用户几乎不会注意到故障,或者根本不会注意到。相反,用户将经历中断和服务的降级,可能会将业务转移到其他地方。
现在我们已经了解了主要问题和为什么我们应该关注这些问题,那么解决方案是什么呢?对于流量激增,我们需要能够快速地进行弹性伸缩。大多数云服务提供商都内置了这一功能。您需要决定您想要控制多少。例如,您是否希望指定在特定的 CPU 或内存负载上进行伸缩,或者您是否可以接受所选平台来处理这个问题?
此外,作为一名架构师,您还可以设计出这样的系统,例如,您可以通过消息队列而不是直接与数据库通信,等等。有几种方法可以做到这一点,只有您知道需要考虑多大的规模:
-
负载均衡(Load balancing):负载均衡的理念是将进入的流量分配到您的应用程序或服务的多个实例中,以确保没有单个实例被压垮。这提高了响应性和可用性。然而,为了您的解决方案,您可能将应用程序和人工智能视为不同的实体,因此为您的 MCP 服务器和人工智能模型端点设置不同的负载均衡方案。
-
速率限制(Rate limiting):速率限制是一种在特定时间框架内控制服务接收请求数量的技术。这有助于防止滥用,确保公平使用,并管理 API 成本。同样,就像负载均衡一样,您可能为您的 Web 应用程序和人工智能端点有不同的方案。
-
断路器(Circuit breakers):断路器的理念是检测故障并防止系统发出可能导致失败的请求。当达到一定的故障阈值时,断路器跳闸,随后一段时间内的请求将自动被拒绝。这允许系统恢复,并防止它被失败的请求所淹没。从用户体验的角度来看,断路器可以通过提供回退选项或优雅降级来帮助维持流畅的体验,当某些服务不可用时。
那么,我们如何实现这些机制呢?好吧,其中一些可以在应用层面完成,而其他一些可能需要基础设施支持。以下是一些策略:
-
负载均衡:使用负载均衡器将流量分配到您的应用程序或服务的多个实例。这可以通过云提供商的功能或专用负载均衡解决方案来完成。
-
速率限制:在 API 网关或应用级别实施速率限制,以控制用户或服务的请求数量。这有助于防止滥用并确保公平使用。
-
断路器:这些可以通过 API 网关来实现。
如果您使用云提供商,您应该考虑使用 Azure API Management 或 Amazon API Gateway 来有效地实施这些策略。这些服务将解决安全、可扩展性和可靠性问题,甚至具有帮助您处理 AI 问题的功能。它们共同的特点是使用声明性方法来定义这些策略。例如,Azure API Management 使用 XML 来定义速率限制、缓存和其他功能的策略。这使得应用和配置变得容易,而无需更改任何代码。
因此,这里的建议是调查您选择的云提供商,并利用他们的 API 管理解决方案来实施这些策略。请查看本章资源部分末尾的链接以获取更多信息。
监控和反馈
对于监控,我们已经提到了您可能会遇到的问题,例如性能瓶颈、错误率和用户行为模式。以下是一些解决这些问题的策略:
-
实施应用程序性能监控(APM)工具:使用 APM 工具来深入了解性能瓶颈和错误率。
-
解决错误率:实施自动错误跟踪和警报,以便快速识别和解决问题。
-
用户行为分析:利用分析工具来了解用户交互并识别潜在的改进领域。
-
AI 使用:您希望确保您的 AI 按预期使用,没有被滥用或误用,并且能够产生您期望的响应。为了解决这个问题,您应该定期采样请求并分析它们,以确保符合使用政策和性能预期。此外,添加反馈循环,让用户对 AI 生成的内容提供反馈。这样,您可以进行提示调整和上下文细化。您想要设置系统以减轻的事项还包括减轻提示注入攻击、不安全的内容生成以及其他潜在风险,如提及竞争对手的名称等。无论您最终选择哪种服务,都应该能够应对所有这些问题。
主要云提供商都有监控解决方案,可以帮助您设置仪表板、警报等。此外,对于 AI 服务使用,还有专门的分析工具可以帮助您分析提示。更多相关信息请参阅本章的资源部分。
管理和合规性
管理是确保你的应用程序在法律和道德标准的范围内运行。你需要遵守这些标准的程度取决于你所在的行业。有 GDPR 用于数据保护,HIPAA 用于健康信息,以及其他可能适用的法规。确保你有一个强大的合规框架。
从软件的角度来看,通过确保记录所有交互并创建一个 审计轨迹 以了解谁改变了什么以及何时改变,使遵守这些政策变得容易。你也可能考虑实施访问控制和数据加密,以进一步保护敏感信息。如果你正在提供人工智能,你需要关注你模型中的偏见和公平性。因此,你可能需要诸如健壮的系统提示和内容安全服务的使用,这些服务有助于实施你需要遵守的政策,无论是 GDPR 还是其他法规。
防御未来
好吧,所以你已经成功部署了一个满足你当前需求的系统。但未来怎么办?你如何保持安全、合规,以及你定义的成功所涉及的任何其他方面?
MCP 是一个相对较新的协议,因此它将继续发展。你需要跟踪这些变化;如果没有你应该应用的构造,也许有你应该停止使用的传输(SSE 已经被 Streamable HTTP 取代)。也可能会有关于某些功能使用的新的指导。你需要跟上所有这些。
与 MCP 相关的工具(例如,检查器工具)也可能发生变化,无论是工具数量的增加还是你与之交互的方式的变化。
然后,你有 SDKs,作为软件,它们正在不断变化。一些变化如此重大,以至于可能会破坏你的代码。你可能考虑至少保持某个主要版本一段时间,但确保你获得所有安全更新。结合 Dependabot、GitHub 高级安全等公认的工具,以确保你做出明智的选择,并了解通过保持某个 SDK 版本或转向新版本所承担的风险。
预见未来很难,但你可以在加强安全方面采取负责任的态度。软件变化迅速,新的威胁被定期发现。了解最新的安全实践,并准备好根据需要调整你的系统。
摘要
这是一个内容丰富的章节,有很多内容需要覆盖,但希望你已经通过提供的指导感到有所帮助。只要你知道你面临的问题,并积极努力解决它们,那么选择通过库、云服务或其他方式来解决它们的选择权在你手中。你可能需要比你想的更多的时间来关注安全,因为它正成为全球公司的主要关注点。
确保你根据生产前、生产中和后期制作需要做的事情来制定计划,并为未来做好准备。保持信息畅通。事情总会出问题;问题在于你如何应对这些意外,以及你采取了哪些措施来减轻任何潜在问题。
如果你读到这儿,这意味着你已经从这本书中学到了很多,从构建你的第一个服务器和客户端,与 LLM 交互,使用 VS Code 等工具消费服务器,最后以负责任的方式部署它。恭喜!我也很高兴在 LinkedIn 上与你联系,uk.linkedin.com/in/christoffer-noring-3257061。
资源
这里有一些有用的资源:
-
Azure AI Management 中的 API 网关:
docs.azure.cn/en-us/api-management/api-management-gateways-overview -
这是一个展示如何添加许多功能(如速率限制、监控、安全等)的出色仓库 – AI 网关:
github.com/Azure-Samples/AI-Gateway|
现在解锁这本书的独家优惠
扫描此二维码或访问
packtpub.com/unlock,然后通过书名搜索这本书。 |![]()
|注意:在开始之前,请准备好您的购买发票。*
第十三章:解锁您书籍的独家优惠
您购买的这本书包含以下独家优惠:
新一代 Packt 阅读器
AI 助手(测试版)
免版税 PDF/ePub 下载
如果您尚未解锁,请使用以下指南来解锁它们。这个过程只需几分钟,并且只需完成一次。
如何通过三个简单步骤解锁这些优惠
第 1 步
准备好您的购买发票,因为在第 3 步中您将需要它。如果您收到的是纸质发票,请用手机扫描并准备好作为 PDF、JPG 或 PNG 格式。
如需查找发票的更多帮助,请访问 www.packtpub.com/unlock-benefits/help。
注意:您是从 Packt 直接购买这本书的吗?您不需要发票。完成第 2 步后,您可以直接跳转到您的独家内容。
|
第 2 步
扫描此二维码或访问 packtpub.com/unlock。 |
|
| 在打开的页面(如果您在桌面电脑上,将类似于图 13.1),通过书名搜索这本书。确保您选择了正确的版本。A screenshot of a web page AI-generated content may be incorrect.图 13.1:桌面上的 Packt 解锁着陆页面 |
|---|
第 3 步
选择您的书籍后,登录您的 Packt 账户或免费创建一个新账户。登录后,上传您的发票。它可以以 PDF、PNG 或 JPG 格式,并且大小不能超过 10 MB。按照屏幕上的其余说明完成此过程。
|
需要帮助?
如果您遇到困难并需要帮助,请访问 www.packtpub.com/unlock-benefits/help 以获取有关如何查找您的发票以及更多详细问题的 FAQ。以下二维码将直接带您到帮助页面: |
|
注意:如果您仍然遇到问题,请联系 customercare@packt.com。
附录:使用现代 Python 构建 Web
这是为了那些想要了解如何使用 Python 及其现代构造的你们。你们可能是因为 AI 的普及而刚刚转向 Python,或者可能你们对编程还比较新手。无论如何,热烈的欢迎;这个附录是为你们准备的。它的目标是确保当你们阅读其他章节时,能够理解所有使用的构造,并能够从中受益。
类型提示和数据建模
尽管 Python 不强制类型,但它确实从类型中受益。类型提示可以使代码更易于阅读,并有助于早期捕获错误。
下面是一些没有类型提示的将产品添加到购物篮的代码:
basket = []
def add_product_to_basket(product):
basket.append(product)
虽然这段代码可以工作,但它缺乏关于添加到购物篮中的产品类型的清晰性。还有在访问product属性时出错的风险。因此,你应该考虑使用类型提示。
带有类型提示的改进代码
类型提示极大地帮助了 IDE 工具,使其更容易捕获错误并理解代码。它们也有助于提高可读性。例如,str、int等类型提示无需添加库即可正常工作。还有typing库,它引入了额外的类型,例如List和Optional。
使用类型提示可以使之前显示的代码变得更加易于阅读:
from typing import List, Optional
basket: List[dict] = []
def add_product_to_basket(product: dict) -> None:
basket.append(product)
这已经好多了,因为product现在明显是一个字典,而basket是一个字典列表。我们可以通过将product转换为类来进一步改进这一点,如下所示:
from typing import List
class Product:
def __init__(self, id: int, name: str, price: float):
self.id = id
self.name = name
self.price = price
basket: List[Product] = []
def add_product_to_basket(product: Product) -> None:
basket.append(product)
什么比这更好?答案是像Pydantic这样的库。
添加 Pydantic
那么,我们有哪些问题需要使用验证库呢?以下是一些例子:
-
确保数据完整性,以确保正在处理的数据是准确和可靠的
-
减少样板代码,因为它可以自动生成验证逻辑
-
提供清晰的错误消息,使调试问题更容易
-
简化复杂的数据验证逻辑,使代码更易于维护
没有使用 Pydantic
想象一下,如果你从网络请求中接收数据;数据以字典的形式存在,但需要特定类型,因此我们需要将其转换为修正它。以下是我们用 Python 代码描述的情况:
product_dictionary = {
"id": 1,
"name": "Sample Product",
"price": 19.99
}
class Product:
def __init__(self, id: int, name: str, price: float):
self.id = id
self.name = name
self.price = price
def from_dict(data: dict) -> Product:
return Product(
id=data["id"],
name=data["name"],
price=data["price"]
)
def service(data: dict) -> Product:
product = from_dict(data)
# Here you would typically save the product to a database
return product
p = service(product_dictionary)
print(p)
在这里,我们有一个数据以字典形式存在的情况,即product_dictionary,但我们需要将其转换为特定类型Product。我们需要创建一个from_dict函数来处理这种转换。这正是 Pydantic 大放异彩的地方,因为它可以帮助我们轻松验证和解析这些数据。
使用 Pydantic
介绍 Pydantic,让我们看看我们如何利用它来满足我们的用例。通过从BaseModel继承,我们可以创建一个模型,该模型会自动验证和解析我们的输入数据。请看以下示例,其中我们定义了一个Product模型:
from pydantic import BaseModel
class Product(BaseModel):
id: int
name: str
price: float
product_dictionary = {
"id": 1,
"name": "Sample Product",
"price": 19.99
}
product = Product(**product_dictionary)
print(product)
在前面的代码中,我们消除了手动转换函数的需求。Pydantic 负责验证和解析输入数据,确保其符合预期的结构。这不仅简化了我们的代码,还使其更加健壮且易于维护。
但这里有一个问题:如果我们向字典中提供错误的数据类型或缺失的字段,Pydantic 将引发验证错误。实际上,这段代码会导致崩溃,所以让我们看看我们如何确保捕获任何验证错误。
有信心地进行转换
如果字典不符合预期的结构,Pydantic 将引发验证错误,清楚地表明出了什么问题。让我们确保我们捕获任何验证错误:
from pydantic import ValidationError, BaseModel
class Product(BaseModel):
id: int
name: str
price: float
product_dictionary = {
"id": 1,
"name": "Sample Product",
"price": 19.99
}
try:
product = Product(**product_dictionary)
print(product)
except ValidationError as e:
print("Validation error:", e)
使用这段代码,我们用 try-except 块包装对 Pydantic 的调用,以优雅地处理任何验证错误。Pydantic 不仅处理字符串、数字和布尔值;它还可以处理更复杂的数据类型,如列表和字典。
更高级的对象
让我们来看一个稍微复杂一些的例子,其中包含一位教授及其办公时间可用性:
from pydantic import BaseModel, ValidationError
from typing import List, Dict
professor_dictionary = {
"id": 1,
"name": "Dr. Smith",
"office_hours": [
{"day": "Monday", "from_": 9, "to_": 12},
{"day": "Wednesday", "from_": 14, "to_": 17}
]
}
class OfficeHour(BaseModel):
day: str
from_: int
to_: int
class Professor(BaseModel):
id: int
name: str
office_hours: List[OfficeHour]
professor = Professor(**professor_dictionary)
print(professor)
在此代码中,我们定义了一个包含 ID、姓名和办公时间列表的 Professor 模型。每个办公时间由一个 OfficeHour 模型表示,该模型包括星期几以及开始和结束时间。这种结构使我们能够轻松地使用 Pydantic 验证和操作复杂的数据类型。
序列化
有时我们会有这样的情况,需要从 Pydantic 模型实例回到字典。这在序列化或我们需要与期望特定格式的 API 交互时很有用。为此,我们可以使用存在于每个模型上的 model_dump 方法。以下是使用方法:
from pydantic import BaseModel
from typing import List, Dict
class OfficeHour(BaseModel):
day: str
from_: int
to_: int
class Professor(BaseModel):
id: int
name: str
office_hours: List[OfficeHour]
professor_dict = {
"id": 1,
"name": "Dr. Smith",
"office_hours": [
{"day": "Monday", "from_": 9, "to_": 12},
{"day": "Wednesday", "from_": 14, "to_": 17}
]
}
professor = Professor(**professor_dict)
professor_serialized = professor.model_dump() # {"id": 1, "name":
"Dr. Smith", "office_hours": [{"day": "Monday", "from_": 9,
"to_": 12}, {"day": "Wednesday", "from_": 14, "to_": 17}]}}
print(professor)
print(professor_serialized)
您甚至可以使用 model_dump(include=...) 和 model_dump(exclude=...) 决定包含哪些字段:
print(m.model_dump(include={'foo', 'bar'}))
#> {'foo': 'hello', 'bar': {'whatever': 123}}
print(m.model_dump(exclude={'foo', 'bar'}))
Pydantic 在 MCP 内部 SDK 中被大量使用,并鼓励您在输入和输出验证中使用它。
您甚至可以编写自己的验证器,但我会把这留作您的练习。请参阅官方文档docs.pydantic.dev/latest/concepts/validators/。
async 和 await
验证代码很重要,但我们还需要理解另一个重要方面,那就是异步编程。异步编程允许我们编写能够同时执行多个任务而不阻塞主线程的代码。这在需要处理 I/O 密集型操作的场景中特别有用,例如进行 API 调用或从数据库中读取。
让我们谈谈 async/await,这是在 Python 中编写异步代码的语法。当你想要编写非阻塞代码,能够同时处理多个任务时,你应该使用 async/await。如果你的代码是阻塞的,那么最终用户的体验会受到影响,因为他们必须等待每个任务完成才能继续。想象一下在拥有许多用户的 Web 服务器上这个问题是如何成倍的。好吧,那么我们需要知道什么?首先,让我们从概念开始:
-
协程:当你用async def标记一个函数时,就创建了一个所谓的协程。这意味着这个函数可以被暂停和恢复,允许在此期间运行其他任务。让我们在下面的代码中看看这个暂停和恢复的行为:import asyncio async def fetch_data(): await asyncio.sleep(1) return {"data": "some data"}
在此代码中,我们定义了一个带有 async 关键字的函数 fetch_data。该函数本身调用 await asyncio.sleep(1),这意味着我们希望代码在这里停止一秒钟。然后,我们恢复并返回一个字典。这是非阻塞的吗?是的,因为当 asyncio.sleep 运行时,其他工作可以继续进行。
-
事件循环:事件循环是运行协程并管理其执行的部分。要与事件循环交互并运行你的
async代码,请调用asyncio.run(fetch_data()):import asyncio async def main(): data = await fetch_data() print(data) asyncio.run(main())
另一种与事件循环交互的方式是通过调用 asyncio.get_running_loop()。这将给你当前的事件循环实例:
-
await:你已经看到它的使用了,但任何标记为async的函数在调用时都应该使用await。 -
asyncio:我们已经在调用它的run方法时展示了它。它是一个在 Python 中提供异步编程支持的库。它允许你使用async/await语法编写并发代码。当你在与 FastAPI 等网络框架一起工作时,你经常会使用asyncio。
让我们看看使用 FastAPI 的一个 Web 应用程序示例:
from fastapi import FastAPI
app = FastAPI()
async def fetch_data():
await asyncio.sleep(1)
return {"data": "some data"}
@app.get("/data")
async def get_data():
data = await fetch_data()
return data
-
asyncio.gather:asyncio提供了另一个有用的方法,名为gather,它允许你并发运行多个协程并等待它们全部完成。当你需要所有任务的结果时,它通常比wait更方便。以下是使用它的方法:import asyncio async def fetch_data(url: str): print("Fetching data...") await asyncio.sleep(1) return {"data": f" Result from {url}: some data"} async def main(): # Gather multiple coroutines correctly by passing them as separate arguments results = await asyncio.gather( fetch_data("google.com"), fetch_data("bing.com"), fetch_data("yahoo.com"), ) print(results) asyncio.run(main())
在这里,每次调用 fetch_data 都将作为参数传递给 gather。虽然 gather 可能更方便,但它并不像 wait 那样提供对单个任务的很多控制。说到 wait,让我们看看它是如何工作的。
-
asyncio.wait:asyncio提供了一些有用的方法,其中之一就是wait方法,它允许你等待多个任务完成。当你想要并发运行多个任务并等待它们全部完成时,这特别有用。以下是使用它的方法:# python import asyncio from typing import List, Optional async def search_task(name: str, delay: int, workload: List[int], find_value: int, stop: asyncio.Event) -> Optional[str]: try: print(f"Task {name} started") await asyncio.sleep(delay) # simulate I/O if stop.is_set(): return None for no in workload: await asyncio.sleep(0) # yield to allow cancellation if no == find_value: stop.set() return name return None except asyncio.CancelledError: print(f"Task {name} cancelled") raise async def main(): stop = asyncio.Event() tasks = [ asyncio.create_task(search_task("A", 3, [1,2,3], 2, stop)), asyncio.create_task(search_task("B", 1, [4,5,6], 2, stop)), asyncio.create_task(search_task("C", 5, [7,8,9], 2, stop)), ] try: for finished in asyncio.as_completed(tasks): res = await finished if res: print("Found in", res) break finally: for t in tasks: if not t.done(): t.cancel() await asyncio.gather(*tasks, return_exceptions=True) asyncio.run(main())
在这里,代码创建了三个搜索任务,每个任务都有不同的延迟和工作负载。这些任务通过 asyncio.create_task 并发启动。使用 asyncio.as_completed 函数来处理完成的结果。如果一个任务找到了目标值,它将设置 stop 事件,这将取消其他任务。最后,所有任务都被等待以确保适当的清理。
正如您所看到的,在 Python 中使用asyncio管理并发任务的方式有很多灵活性。让我们再提一点:到目前为止,您已经看到了如何使您的函数async化,甚至您的 Web 框架。还有一个专门用于使 Web 请求async化的库,称为httpx。
httpx是一个功能齐全的 Python 3 HTTP 客户端,它提供了async能力。它允许您以异步方式发出 HTTP 请求,这使得它非常适合 I/O 密集型任务。以下是一个如何使用httpx和async的简单示例:
import httpx
import asyncio
# make a web request to google.com
async def fetch_web_page(url: str) -> httpx.Response:
async with httpx.AsyncClient() as client:
return await client.get(url)
async def main():
response = await fetch_web_page("https://www.google.com")
print(response.status_code)
asyncio.run(main())
太好了,现在我们对async有了更好的理解,让我们看看如何在 Web 环境中使用它。
Uvicorn:ASGI 服务器
首先,让我们看看我们是从哪里来的,即 WSGI。WSGI代表Web Server Gateway Interface,它是 Web 服务器和 Python Web 应用之间的一个标准接口。多年来,它一直是 Python Web 应用的事实标准。然而,为了满足现代 Web 应用的需求,特别是那些需要异步能力的应用,引入了异步服务器网关接口(ASGI)。因此,使用 ASGI,我们可以更有效地处理异步请求,这是 WSGI 所不能做到的。
Uvicorn 是一个快速的 ASGI 服务器实现,使用uvloop和httptools。它非常适合提供 FastAPI 应用服务,可以处理 HTTP 和 WebSocket 协议。uvloop是事件循环的快速实现,这是 Python 异步编程的核心。httptools是一个用于解析 HTTP 请求和响应的库。Uvicorn 与 FastAPI 无缝协作,例如,它可以启动服务器,使您的应用能够并发处理请求。以下是一个 Web 服务器示例:
#main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "World"}
您可以使用以下命令使用 Uvicorn 运行此 FastAPI 应用:
uvicorn main:app --reload
前面的代码所做的是运行一个名为main.py的文件,并查找名为app的 FastAPI 应用实例。然后它将启动服务器并启用热重载,因此您对代码所做的任何更改都将自动应用,无需重新启动服务器。
您还可以指定其他选项,例如主机和端口:
uvicorn main:app --host 0.0.0.0 --port 8000
这只是使用 Uvicorn 的开始,但能够使用 SSE 或 Streamable HTTP 等传输创建 MCP 服务器是一个很好的起点。
上下文管理器
在使用 MCP SDK 工作时,您还会经常看到上下文管理器的使用,那么这些是什么?上下文管理器是一个 Python 对象,它定义了执行with语句时要建立的运行时上下文。最常见的用例是资源管理,您希望确保资源被正确获取和释放。例如,如果您需要设置数据库连接或其他类型的资源,使用它是个好主意。以下是一个简单示例:
with db_resource("sqlite.db") as conn:
# Perform database operations
pass
您可能会问,是什么让我们能够使用with关键字。好吧,上下文管理器能够使用with关键字是因为它们实现了两个特殊方法:__enter__和__exit__。当执行with语句时,会调用__enter__方法,而当退出with语句内部的块时,会调用__exit__方法。这允许自动执行设置和清理操作。让我们看看前面的类是如何实现这些方法的:
class db_resource:
def __init__(self, db_name):
self.db_name = db_name
def __enter__(self):
# Code to establish the database connection
self.conn = sqlite3.connect(self.db_name)
return self.conn
def __exit__(self, exc_type, exc_value, traceback):
# Code to close the database connection
if self.conn:
self.conn.close()
上述代码演示了使用上下文管理器来管理数据库连接的使用方法。您可以看到__enter__方法是如何建立连接并返回它的,而__exit__方法确保在退出块时关闭连接,即使发生异常也是如此。
with语句还有另一个版本,即async with,它用于异步上下文管理器。当处理异步代码时,这些特别有用,例如使用asyncio或处理异步 I/O 操作时。SDK 在客户端使用这种模式,例如,用于建立初始服务器连接,如下所示:
server_params = StdioServerParameters(
command="python",
args=["server.py"]
)
async with stdio_client(server_params) as (read, write):
在此代码中,现在它调用异步上下文管理器的__aenter__和__aexit__方法,这有助于创建服务器连接。如果您继续查看客户端的 SDK 代码,您将看到完整的初始化代码如下:
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# do something with session
这里发生了什么?这里正在联合使用两个异步上下文管理器。外部上下文管理器(stdio_client)负责管理服务器连接的标准输入/输出流,而内部上下文管理器(ClientSession)负责管理客户端会话的生命周期。从实现的角度来看,这里有一些示例代码:
class stdio_client:
def __init__(self, params):
self.params = params
async def __aenter__(self):
self.read, self.write = await self._create_client()
return self.read, self.write
async def __aexit__(self, exc_type, exc_value, traceback):
await self._cleanup()
async def _create_client(self):
process = await asyncio.create_subprocess_exec(
self.params.command, *self.params.args,
env=self.params.env,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
return process.stdin, process.stdout
async def _cleanup(self):
pass
class ClientSession:
def __init__(self, read, write):
self.read = read
self.write = write
async def __aenter__(self):
# Code to initialize the client session
return self
async def __aexit__(self, exc_type, exc_value, traceback):
# Code to clean up the client session
pass
async def initialize(self):
# Code to initialize the client session
pass
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# do something with session
await session.initialize()
注意stdio_client中的__aenter__方法负责创建客户端连接并返回必要的流,而ClientSession中的__aenter__方法负责使用这些流初始化会话。所以,现在您可能已经理解为什么代码看起来是这个样子。也就是说,一些必要的初始化操作是在上下文管理器创建时发生的,而一些清理操作是在退出时发生的。如果我们不使用这种模式,很容易忘记清理部分,从而导致资源泄露。
contextmanager库
Python 中的contextmanager库提供了一种使用生成器函数创建上下文管理器的方法。您不需要使用__enter__和__exit__方法来定义类,而是可以使用@contextmanager装饰器将生成器函数转换为上下文管理器。让我们看看一个例子:
from contextlib import contextmanager
import asyncio
# create a database resource using context manager
@contextmanager
def database_connection():
conn = create_connection()
try:
yield conn
finally:
conn.close()
# use it
def main():
with database_connection() as db:
# use the database connection
pass
您可以看到我们不需要编写__enter__和__exit__,而是可以用@contextmanager装饰器来装饰一个函数,以达到相同的效果。这个库也被 MCP SDK 用来为各种资源设置上下文管理器。
uv:您的开发环境管理器
让我们总结这个附录的最后一个主题,即 uv。这不是您必须使用的东西,但强烈建议您这样做。原因如下:
-
它提供了一个一致的界面来管理您的开发环境。这意味着您可以在不担心冲突的情况下轻松地在不同的项目和它们的依赖项之间切换。
-
它简化了设置和管理虚拟环境的过程,使您能够更容易地在具有不同要求的多个项目上工作。因此,您不必键入
python -m venv env和source env/bin/activate,只需简单地使用uv init来创建和激活虚拟环境,这也会启动您的项目。 -
它简化了安装和管理依赖项的过程,允许您轻松地按需添加、删除和更新包。您习惯于键入
pip install <package>来安装一个包,但使用uv,您可以使用uv add <package>代替。 -
使用
project.toml进行项目管理,使操作更简单,允许您在单个文件中定义您项目的依赖项和设置,而不是分散在多个文件中。 -
您甚至可以使用
uv来管理您的 Docker 容器和镜像,使在一致的环境中部署您的应用程序变得更加容易。
您可以这样做:
-
使用以下命令安装:
pip install uv. -
在您的项目根目录中创建一个
uv配置文件:uv init -
使用
uv add命令添加依赖项:uv add <package> -
使用以下命令运行您的应用程序:
uv run
我们已经到达了这个附录的结尾。希望您对现代 Python 以及 uv、asyncio 和 Uvicorn 等工具有了更深入的了解。
|
现在解锁这本书的独家优惠
扫描此二维码或访问 packtpub.com/unlock,然后通过名称搜索此书。 | 
|
| 注意:在开始之前,请准备好您的购买发票。* |
|---|

订阅我们的在线数字图书馆,全面访问超过 7,000 本书和视频,以及领先的工具,帮助您规划个人发展并推进您的职业生涯。更多信息,请访问我们的网站。
为什么订阅?
-
使用来自 4,000 多位行业专业人士的实用电子书和视频,减少学习时间,增加编码时间
-
通过为您量身定制的技能计划提高您的学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于轻松访问关键信息
-
复制粘贴、打印和收藏内容
在 www.packtpub.com,您还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
您可能还会喜欢的其他书籍
如果您喜欢这本书,您可能会对 Packt 的以下其他书籍感兴趣:
Node.js 设计模式,第四版
卢西亚诺·马米诺和马里奥·卡西亚罗
ISBN: 978-1-80323-894-4
-
理解 Node.js 基础知识及其异步事件驱动架构
-
使用回调、承诺和 async/await 编写正确的异步代码
-
利用 Node.js 流创建数据驱动的处理管道
-
为生产级应用实现可信的软件设计模式
-
编写可测试的代码和自动化测试(单元测试、集成测试、端到端测试)
-
使用高级食谱:缓存、批处理、异步初始化、卸载 CPU 密集型工作
-
使用 Node.js 构建和扩展微服务和分布式系统
使用 HTML5 和 CSS 的响应式网页设计,第五版
本·弗莱恩
ISBN: 978-1-83702-823-8
-
利用颜色函数混合颜色并在颜色空间之间转换
-
使用媒体查询和容器查询来检测触摸/鼠标和颜色偏好
-
利用 HTML 语义来编写可访问的标记
-
使用 SVG 提供分辨率无关的图像,并学习高效地显示它们
-
仅使用 CSS 创建动画,当项目进入和离开视口时
-
发现 CSS 自定义属性并利用新的 CSS 函数
-
向 HTML 表单添加验证和界面元素
-
检查由 AI 工具生成的前端代码是否满足您的目标
Packt 正在寻找像您这样的作者
如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们已与成千上万的开发人员和科技专业人士合作,就像您一样,帮助他们与全球科技社区分享他们的见解。您可以提交一般申请,申请我们正在招募作者的特定热门话题,或提交您自己的想法。
分享您的想法
现在您已经完成了使用 Python 学习模型上下文协议,我们非常乐意听到您的想法!如果您从亚马逊购买了这本书,请点击此处直接转到该书的亚马逊评论页面并分享您的反馈或在该购买网站上留下评论。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。


浙公网安备 33010602011771号