@@ -15,17 +15,17 @@ published: false
15
15
## 1. 简介
16
16
17
17
模型上下文协议(MCP)是一个创新的开源协议,它重新定义了大语言模型(LLM)与外部世界的互动方式。MCP 提供了一种标准化方法,使任意大语言模型能够轻松连接各种数据源和工具,实现信息的无缝访问和处理。MCP 就像是 AI 应用程序的 USB-C 接口,为 AI 模型提供了一种标准化的方式来连接不同的数据源和工具。
18
- ## 1.1 MCP架构
18
+ ## 2 Python MCP
19
19
20
- ### 1.1 .1 服务架构
20
+ ### 2 .1 服务架构
21
21
22
22
![ [ ../../images/MCP服务架构.svg]]
23
23
24
- ### 1.1 .2 Agent架构
24
+ ### 2 .2 Agent架构
25
25
26
26
![ [ ../../images/Agent架构.svg]]
27
27
28
- ### 1.1 .3 简单客户端
28
+ ### 2 .3 简单客户端
29
29
30
30
通过启动本地的一个客户端来实现循环对话调用大模型
31
31
@@ -116,7 +116,7 @@ if __name__ == '__main__':
116
116
asyncio.run(main())
117
117
```
118
118
119
- ## 1.2 MCP服务器通讯机制
119
+ ### 2.4 MCP服务器通讯机制
120
120
121
121
** Model Context Protocol(MCP)** 是一种由Anthropic开源的协议,旨在将大型语言模型直接连接至数据源,实现无缝集成。根据 MCP 的规范,当前支持两种传输方式:
122
122
- 标准输入输出(stdio):打开文件流的方式进行传输(同一个服务器,不需要通过端口监听)
@@ -138,7 +138,7 @@ if __name__ == '__main__':
138
138
| :-----------: | :---------: | :------: | :-----------: |
139
139
| stdio(标准输入输出) | ✔ | ❌ | 本地通信,低延迟,高速交互 |
140
140
| http(网络api) | ❌ | ✔ | 分布式架构,远程通信 |
141
- ## 1.3 简单天气查询服务端
141
+ ### 2.5 简单天气查询服务端
142
142
143
143
通过 ** \@ mcp.tool()** 来标注服务端提供的工具有哪些
144
144
@@ -171,4 +171,301 @@ async def get_weather(city: str) -> dict[str, Any] | None:
171
171
if __name__ == ' __main__' :
172
172
# 使用标准 I/O 方式运行MCP服务器
173
173
mcp.run(transport = ' stdio' )
174
- ```
174
+ ```
175
+
176
+ ### 2.6 天气查询客户端
177
+
178
+ ``` python
179
+ import asyncio
180
+ import json
181
+ from typing import Optional
182
+
183
+ from mcp import ClientSession, StdioServerParameters
184
+ from mcp.client.stdio import stdio_client
185
+ from openai import OpenAI
186
+ from contextlib import AsyncExitStack
187
+
188
+ BASE_URL = " https://dashscope.aliyuncs.com/compatible-mode/v1"
189
+ MODEL_NAME = " qwen-max"
190
+ KEY = " xxx"
191
+
192
+
193
+ class MCPClient :
194
+
195
+ def __init__ (self ):
196
+ """ 初始化MCP客户端"""
197
+ self .openai_api_key = KEY
198
+ self .openai_api_base = BASE_URL
199
+ self .model = MODEL_NAME
200
+
201
+ if not self .openai_api_key:
202
+ raise ValueError (" 请设置您的OpenAI API密钥" )
203
+
204
+ self .client = OpenAI(
205
+ api_key = self .openai_api_key,
206
+ base_url = self .openai_api_base,
207
+ )
208
+ self .session: Optional[ClientSession] = None
209
+ self .exit_stack = AsyncExitStack()
210
+
211
+ # 处理对话请求
212
+ async def process_query (self , query : str ) -> str :
213
+ messages = [{
214
+ " role" : " system" ,
215
+ " content" : " 你是一个智能助手,帮助用户回答问题" ,
216
+ }, {
217
+ " role" : " user" ,
218
+ " content" : query,
219
+ }]
220
+
221
+ # 获取到工具列表
222
+ response = await self .session.list_tools()
223
+ available_tools = [
224
+ {
225
+ " type" : " function" ,
226
+ " function" : {
227
+ " name" : tool.name,
228
+ " description" : tool.description,
229
+ " input_schema" : tool.inputSchema,
230
+ }
231
+ }
232
+ for tool in response.tools]
233
+
234
+ try :
235
+ response = await asyncio.get_event_loop().run_in_executor(
236
+ None ,
237
+ lambda : self .client.chat.completions.create(
238
+ model = MODEL_NAME ,
239
+ messages = messages,
240
+ tools = available_tools,
241
+ )
242
+ )
243
+ content = response.choices[0 ]
244
+ if content.finish_reason == " tool_calls" :
245
+ # 如果使用的是工具,解析工具
246
+ tool_call = content.message.tool_calls[0 ]
247
+ tool_name = tool_call.function.name
248
+ tool_args = json.loads(tool_call.function.arguments)
249
+
250
+ # 执行工具
251
+ result = await self .session.call_tool(tool_name, tool_args)
252
+ print (f " \n\n 工具调用:[ { tool_name} ],参数:[ { tool_args} ] " )
253
+
254
+ # 将工具返回结果存入message中,model_dump()克隆一下消息
255
+ messages.append(content.message.model_dump())
256
+ messages.append({
257
+ " role" : " tool" ,
258
+ " content" : result.content[0 ].text,
259
+ " tool_call_id" : tool_call.id,
260
+ })
261
+
262
+ response = self .client.chat.completions.create(
263
+ model = MODEL_NAME ,
264
+ messages = messages,
265
+ )
266
+
267
+ return response.choices[0 ].message.content
268
+
269
+ # 正常返回
270
+ return content.message.content
271
+ except Exception as e:
272
+ return f " 调用OpenAI API错误: { str (e)} "
273
+
274
+ async def connect_to_server (self , server_script_path : str ):
275
+ """ 连接到 MCP 服务器的连接 """ is_python = server_script_path.endswith(" .py" )
276
+ is_js = server_script_path.endswith(" .js" )
277
+ if not (is_python or is_js):
278
+ raise ValueError (" 服务器脚本路径必须以 .py 或 .js 结尾" )
279
+
280
+ command = " python" if is_python else " node"
281
+
282
+ server_params = StdioServerParameters(
283
+ command = command,
284
+ args = [server_script_path],
285
+ env = None
286
+ )
287
+
288
+ stdio_transport = await self .exit_stack.enter_async_context(
289
+ stdio_client(server_params)
290
+ )
291
+ read, write = stdio_transport
292
+ self .session = await self .exit_stack.enter_async_context(ClientSession(read, write))
293
+ # 初始化会话
294
+ await self .session.initialize()
295
+ # 列出工具
296
+ response = await self .session.list_tools()
297
+ tools = response.tools
298
+ print (" \n 已经连接到服务器,支持以下工具:" , [tools.name for tools in tools])
299
+
300
+ async def chat_loop (self ):
301
+ """ 运行交互式聊天循环"""
302
+ print (" \n MCP客户端已启动!输入 ‘quit’ 退出" )
303
+
304
+ while True :
305
+ try :
306
+ user_input = input (" 请输入您的问题:" ).strip()
307
+ if user_input.lower() == " quit" :
308
+ print (" 退出交互式聊天" )
309
+ break
310
+ response = await self .process_query(user_input)
311
+ print (f " 大模型: { response} " )
312
+ except Exception as e:
313
+ print (f " 发生错误: { str (e)} " )
314
+
315
+ async def cleanup (self ):
316
+ """ 清理资源"""
317
+ print (" Cleaning up resources..." )
318
+ await self .exit_stack.aclose()
319
+
320
+
321
+ async def main ():
322
+ mcp_client = MCPClient()
323
+
324
+ try :
325
+ await mcp_client.connect_to_server(" ./mcp_server.py" )
326
+ await mcp_client.chat_loop()
327
+ finally :
328
+ await mcp_client.cleanup()
329
+
330
+
331
+ if __name__ == ' __main__' :
332
+ asyncio.run(main())
333
+ ```
334
+
335
+ ### 2.7 MCP概念
336
+
337
+ - Tools:服务器暴露可执行功能,供LLM调用以与外部系统交互
338
+ - Resources:服务器暴露数据和内容,供客户端读取并作为LLM上下文
339
+ - Prompts:服务器定义可复用的提示模板,引导LLM交互
340
+ - Sampling:让服务器借助客户端向LLM发起完成请求,实现复杂的智能行为
341
+ - Roots:客户端给服务器指定的一些地址,用来高速服务器该关注哪些资源和去哪里找这些资源
342
+
343
+ #### 2.7.1 Tools
344
+
345
+ 服务器所支持的工具能力,使用提供的装饰器就可以定义对应的工具
346
+
347
+ ``` python
348
+ @mcp.tool ()
349
+ async def get_weather (city : str ) -> dict[str , Any] | None :
350
+ """
351
+ 获取天气
352
+ :param city: 城市名称(需要使用英文,如Beijing)
353
+ :return: 天气数据字典;若出错返回包含 error信息的字典
354
+ """ return await fetch_weather(city)
355
+ ```
356
+
357
+ 服务端连接通了session会话就可以通过对应的代码来进行查询支持哪些工具
358
+
359
+ ``` python
360
+ session.list_tools()
361
+ ```
362
+
363
+ #### 2.7.2 Resources
364
+ 类似于服务端定义了一个api接口用于查询数据,可以给大模型提供上下文
365
+
366
+ ``` python
367
+ @mcp.resource (uri = " echo://hello" )
368
+ def resource () -> str :
369
+ """ Echo a message as a resource"""
370
+ return f " Resource echo: hello "
371
+
372
+
373
+ @mcp.resource (uri = " echo://{message} /{age} " )
374
+ def message (message : str , age : int ) -> str :
375
+ """ Echo a message as a resource"""
376
+ return f " 你好, { message} , { age} "
377
+ ```
378
+
379
+ 服务端查询时,如果使用了 {message} 作为占位符会解析为 ** resource_templates** 使用 ** list_resources** 只能获取到普通的资源
380
+ ``` python
381
+ # 查询资源
382
+ resources = await self .session.list_resources()
383
+ print (" \n 已经连接到服务器,支持以下资源:" , [resources.name for resources in resources.resources])
384
+
385
+ # 查询资源
386
+ templates = await self .session.list_resource_templates()
387
+ print (" \n 已经连接到服务器,支持以下模板资源:" , [resources.name for resources in templates.resourceTemplates])
388
+
389
+ resource_result = await self .session.read_resource(" echo://hello" )
390
+ for content in resource_result.contents:
391
+ # 对返回的字符串进行编码
392
+ print (f " 读取资源内容: { unquote(content.text)} " )
393
+
394
+ resource_result = await self .session.read_resource(" echo://张三/18" )
395
+ for content in resource_result.contents:
396
+ print (f " 读取资源内容: { unquote(content.text)} " )
397
+ ```
398
+
399
+ #### 2.7.3 Prompt
400
+ 提示词,用于在服务端定义好自己的提示词来进行复用
401
+
402
+ ``` python
403
+ @mcp.prompt ()
404
+ def review_code (code : str ) -> str :
405
+ return f " Please review this code: \n\n { code} "
406
+
407
+ @mcp.prompt ()
408
+ def debug_error (error : str ) -> list[base.Message]:
409
+ return [
410
+ base.UserMessage(" I'm seeing this error:" ),
411
+ base.UserMessage(error),
412
+ base.AssistantMessage(" I'll help debug that. What have you tried so far?" ),
413
+ ]
414
+ ```
415
+
416
+ ``` python
417
+ # 查询提示词
418
+ prompt_result = await self .session.list_prompts()
419
+ print (" \n 已经连接到服务器,支持以下提示词:" , [prompt.name for prompt in prompt_result.prompts])
420
+
421
+ get_prompt = await self .session.get_prompt(" review_code" , { " code" : " hello world" })
422
+ for message in get_prompt.messages:
423
+ print (f " 提示词内容: { message} " )
424
+ ```
425
+
426
+ #### 2.7.4 Images
427
+ MCP提供的一个Image类,可以自动处理图像数据
428
+
429
+ ``` python
430
+ from mcp.server.fastmcp import FastMCP, Image
431
+
432
+ @mcp.tool ()
433
+ def create_thumbnail (image_url : str ) -> Image:
434
+ """ Create a thumbnail from an image"""
435
+ img = PILImage.open(image_url)
436
+ img.thumbnail((100 , 100 ))
437
+ return Image(data = img.tobytes(), format = " jpg" )
438
+ ```
439
+
440
+ ``` python
441
+ # 调用图片工具
442
+ image = await self .session.call_tool(" create_thumbnail" , {" image_url" : " /Users/Documents/图片/WechatIMG47.jpg" })
443
+ print (" 读取图片资源:" , image)
444
+ ```
445
+
446
+ #### 2.7.5 Context
447
+ Context 对象为您的工具和资源提供对 MCP 功能的访问权限,在服务端的工具中可以进行使用并且相互之间进行调用
448
+
449
+ ``` python
450
+ from mcp.server.fastmcp import FastMCP, Context
451
+
452
+ @mcp.tool ()
453
+ async def test (city : str , ctx : Context) -> str :
454
+ """
455
+ 获取天气
456
+ :param city: 城市名称(需要使用英文,如Beijing)
457
+ :return: 天气描述
458
+ """ get_weather_city = await ctx.read_resource(f " echo:// { city} /25 " )
459
+ result: str = " "
460
+ for content in get_weather_city:
461
+ result += unquote(content.content)
462
+ return result
463
+ ```
464
+
465
+ ``` python
466
+ # 调用天气工具使用Context对象
467
+ weather = await self .session.call_tool(name = " test" , arguments = {" city" : " 北京" })
468
+ print (" 天气信息:" , weather)
469
+ ```
470
+
471
+ ## Spring MCP
0 commit comments