Skip to content

Commit 10aaed4

Browse files
committed
提交MCP笔记
1 parent 05f8293 commit 10aaed4

File tree

1 file changed

+304
-7
lines changed

1 file changed

+304
-7
lines changed

source/_posts/ai/MCP服务.md

+304-7
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ published: false
1515
## 1. 简介
1616

1717
模型上下文协议(MCP)是一个创新的开源协议,它重新定义了大语言模型(LLM)与外部世界的互动方式。MCP 提供了一种标准化方法,使任意大语言模型能够轻松连接各种数据源和工具,实现信息的无缝访问和处理。MCP 就像是 AI 应用程序的 USB-C 接口,为 AI 模型提供了一种标准化的方式来连接不同的数据源和工具。
18-
## 1.1 MCP架构
18+
## 2 Python MCP
1919

20-
### 1.1.1 服务架构
20+
### 2.1 服务架构
2121

2222
![[../../images/MCP服务架构.svg]]
2323

24-
### 1.1.2 Agent架构
24+
### 2.2 Agent架构
2525

2626
![[../../images/Agent架构.svg]]
2727

28-
### 1.1.3 简单客户端
28+
### 2.3 简单客户端
2929

3030
通过启动本地的一个客户端来实现循环对话调用大模型
3131

@@ -116,7 +116,7 @@ if __name__ == '__main__':
116116
asyncio.run(main())
117117
```
118118

119-
## 1.2 MCP服务器通讯机制
119+
### 2.4 MCP服务器通讯机制
120120

121121
**Model Context Protocol(MCP)** 是一种由Anthropic开源的协议,旨在将大型语言模型直接连接至数据源,实现无缝集成。根据 MCP 的规范,当前支持两种传输方式:
122122
- 标准输入输出(stdio):打开文件流的方式进行传输(同一个服务器,不需要通过端口监听)
@@ -138,7 +138,7 @@ if __name__ == '__main__':
138138
| :-----------: | :---------: | :------: | :-----------: |
139139
| stdio(标准输入输出) ||| 本地通信,低延迟,高速交互 |
140140
| http(网络api) ||| 分布式架构,远程通信 |
141-
## 1.3 简单天气查询服务端
141+
### 2.5 简单天气查询服务端
142142

143143
通过 **\@mcp.tool()** 来标注服务端提供的工具有哪些
144144

@@ -171,4 +171,301 @@ async def get_weather(city: str) -> dict[str, Any] | None:
171171
if __name__ == '__main__':
172172
# 使用标准 I/O 方式运行MCP服务器
173173
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

Comments
 (0)