MCP Server 笔记

MCP Server 笔记

1.条件与环境

1.1 前提条件

软件/账号 是否必须
VSCode 1.93+ Y
Node.js 18.x+ JavaScript/TypeScript 实现的MCP服务必须
Python 3.10+ Y
uv Y
AI API Key Y

1.2 本文运行环境

平台/软件 版本
操作系统 Windows 10 企业版 22H2 19045.4046 64位
VSCode 1.98.2
Python 3.12.3
uv 0.6.8
MCP Python SDK 1.5.0、1.6.0

2.官方案例学习

2.1 天气服务 / Python

2.1.1 创建服务

1.按官方示例【点击直达】初始化Python项目【点击直达

2.创建weather.py,复制官方示例代码【点击直达

2.1.2 配置服务

通过Cline进行配置和测试。

点击【Configure MCP Servers】、右侧窗口自动打开cline_mcp_settings.json文件、手动添加服务配置:

image-20250320181216673

2.1.3 测试服务

在任务对话框中输入任务:

image-20250320180707902

检测到天气服务:

image-20250320180834230

image-20250320180924428

2.1.4 小结

该案例示范了:

  • 项目脚手架使用
  • 通过FastMCP快速构建stdio服务

2.2 简单Tool / Python

2.2.1 构建项目

1.下载官方示例代码【点击直达

2.运行:

1
2
cd simple-tool
uv run mcp-simple-tool

会自动创建虚拟环境并下载依赖(出现Installed xxx packages xxx就可以关闭窗口了):

image-20250328155606836

2.2.2 测试服务

通过代码进行测试:

stdio方式

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client


async def main():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "mcp-simple-tool"])
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()

# List available tools
tools = await session.list_tools()
print(tools)

# Call the fetch tool
result = await session.call_tool("fetch", {"url": "https://example.com"})
print(result)


asyncio.run(main())

直接运行:

1
python client-stdio.py

image-20250328160033917

SSE方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import asyncio
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client


async def main():
async with sse_client("http://localhost:8000/sse") as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()

# List available tools
tools = await session.list_tools()
print(tools)

# Call the fetch tool
result = await session.call_tool("fetch", {"url": "https://example.com"})
print(result)


asyncio.run(main())

SSE方式,服务端和客户端为分别独立的进程,运行客户端之前需启动服务:

1
uv run mcp-simple-tool --transport sse --port 8000

image-20250328160154479

再启动客户端:

1
python client-sse.py

image-20250328160308223

2.2.3 小结

该案例示范了:

  • 1个服务同时支持stdioSSE这2种transpot
  • tools/list服务端实现
  • tools/call(简单网页内容抓取)服务实现

2.3 简单Resource / Python

2.3.1 构建项目

1.下载官方示例代码【点击直达

2.运行:

1
2
cd simple-resource
uv run mcp-simple-resource

会自动创建虚拟环境并下载依赖(出现Installed xxx packages xxx就可以关闭窗口了):

image-20250328154218528

2.3.2 测试服务

通过代码进行测试:

stdio方式

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import asyncio
from mcp.types import AnyUrl
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client


async def main():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "mcp-simple-resource"])
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()

# List available resources
resources = await session.list_resources()
print(resources)

# Get a specific resource
resource = await session.read_resource(AnyUrl("file:///greeting.txt"))
print(resource)


asyncio.run(main())

直接运行:

1
python client-stdio.py

image-20250328155244738

SSE方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio
from mcp.types import AnyUrl
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client


async def main():
async with sse_client("http://localhost:8000/sse") as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()

# List available resources
resources = await session.list_resources()
print(resources)

# Get a specific resource
resource = await session.read_resource(AnyUrl("file:///greeting.txt"))
print(resource)


asyncio.run(main())

SSE方式,服务端和客户端为分别独立的进程,运行客户端之前需启动服务:

1
uv run mcp-simple-resource --transport sse --port 8000

image-20250328154413027

再启动客户端:

1
python client-sse.py

image-20250328154635205

2.2.3 小结

该案例示范了:

  • 1个服务同时支持stdioSSE这2种transpot
  • resources/list服务端实现
  • resources/read(文本文件)服务实现
  • 客户端通过代码(ClientSession.list_resources)获取resources list
  • 客户端通过代码(ClientSession.read_resource)获取某个resource

2.4 简单Prompt / Python

2.4.1 构建项目

1.下载官方示例代码【点击直达

2.运行:

1
2
cd simple-prompt
uv run mcp-simple-prompt

会自动创建虚拟环境并下载依赖(出现Installed xxx packages xxx就可以关闭窗口了):

image-20250328155704897

2.4.2 测试服务

通过代码进行测试:

stdio方式

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client


async def main():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "mcp-simple-prompt"])
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()

# List available prompts
prompts = await session.list_prompts()
print(prompts)

# Get the prompt with arguments
prompt = await session.get_prompt(
"simple",
{
"context": "User is a software developer",
"topic": "Python async programming",
},
)
print(prompt)


asyncio.run(main())

直接运行:

1
uv run client-stdio.py

image-20250328161926423

SSE方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import asyncio
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client


async def main():
async with sse_client("http://localhost:8000/sse") as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()

# List available prompts
prompts = await session.list_prompts()
print(prompts)

# Get the prompt with arguments
prompt = await session.get_prompt(
"simple",
{
"context": "User is a software developer",
"topic": "Python async programming",
},
)
print(prompt)


asyncio.run(main())

SSE方式,服务端和客户端为分别独立的进程,运行客户端之前需启动服务:

1
uv run mcp-simple-prompt --transport sse --port 8000

image-20250328161433618

再启动客户端:

1
uv run client-sse.py

image-20250328161839963

2.4.3 小结

该案例示范了:

  • 1个服务同时支持stdioSSE这2种transpot
  • prompts/list服务端实现
  • prompts/get(简单prompt模板获取)服务实现

3.开发实践

3.1 WebSocket transport服务 / Python

3.1.1 初始化项目

1.运行:

1
2
3
4
5
uv init mcp-server-transport-websocket
cd mcp-server-transport-websocket
uv venv
.venv\Scripts\activate
uv add mcp[cli] httpx websockets hypercorn

2.使用官方案例的代码组织结构,参考【简单Tool】项目目录下的pyproject.toml文件内容,将相关内容复制到本项目的pyproject.toml文件中

3.1.2 编写代码

Python SDK(v1.5.0、v1.60)中已有WebSocket相关实现,参考【简单Tool】项目将Transports文档和server.py丢给Cursor进行改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import anyio
import asyncio
import click
import httpx
import logging
import sys
from contextlib import contextmanager
from starlette.types import Scope, Receive, Send
import mcp.types as types
from mcp.server.lowlevel import Server

logger = logging.getLogger(__name__)
# Add basic logging config
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')


async def fetch_website(
url: str,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
headers = {
"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"
}
async with httpx.AsyncClient(follow_redirects=True, headers=headers) as client:
response = await client.get(url)
response.raise_for_status()
return [types.TextContent(type="text", text=response.text)]


@click.command()
@click.option("--port", default=8000, help="Port to listen on for WS")
@click.option(
"--transport",
type=click.Choice(["stdio", "ws"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server("mcp-server-websocket")

@app.call_tool()
async def fetch_tool(
name: str, arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name != "fetch":
raise ValueError(f"Unknown tool: {name}")
if "url" not in arguments:
raise ValueError("Missing required argument 'url'")
return await fetch_website(arguments["url"])

@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="fetch",
description="Fetches a website and returns its content",
inputSchema={
"type": "object",
"required": ["url"],
"properties": {
"url": {
"type": "string",
"description": "URL to fetch",
}
},
},
)
]

if transport == "ws":
from mcp.server.websocket import websocket_server
from starlette.applications import Starlette
from starlette.endpoints import WebSocketEndpoint
from starlette.routing import WebSocketRoute
from starlette.websockets import WebSocket
from hypercorn.asyncio import serve
from hypercorn.config import Config
import asyncio

config = Config()
config.bind = [f"0.0.0.0:{port}"]
config.loglevel = "debug"

# Define the endpoint class inside main to capture 'app'
class MCPWebSocketEndpoint(WebSocketEndpoint):
async def on_connect(self, websocket: WebSocket) -> None:
"""Handles incoming WebSocket connections."""
# await websocket.accept() # 不能加这行代码、加了就会报错:RuntimeError: Expected ASGI message "websocket.connect", but got 'websocket.receive'
logger.info("WebSocket connection accepted via Endpoint")

# Run MCP server within the connection scope
try:
print(f"================== WS Endpoint Connect =================={websocket}")
# Create the MCP transport using the websocket's scope, receive, and send
ws_transport = websocket_server(websocket.scope, websocket.receive, websocket.send)
# The websocket_server is an async context manager that provides the streams
async with ws_transport as streams:
# 'app' is captured from the outer 'main' scope
await app.run(
streams[0], streams[1], app.create_initialization_options()
)
logger.info("MCP app.run finished for this WebSocket.")
except Exception as e:
# Log the error and re-raise to ensure Starlette handles disconnect
logger.error(f"Error during MCP run in WebSocket: {e}", exc_info=True)
# Note: Starlette's WebSocketEndpoint automatically handles sending a close frame on unhandled exceptions.
raise
finally:
# Ensure disconnect is logged even if app.run exits normally
logger.info(f"Exiting on_connect for WebSocket:{websocket}")


async def on_receive(self, websocket: WebSocket, data: any) -> None:
"""Handles receiving data on the WebSocket."""
# The mcp.server.websocket.websocket_server should handle receiving messages internally.
# This method might not be strictly necessary for the MCP logic itself,
# but it's part of the WebSocketEndpoint interface.
# If this gets called unexpectedly, it might indicate an issue.
logger.warning(f"Endpoint on_receive called with data: {data} - This might indicate an issue if MCP transport should solely handle receives.")
# We do not process data here; the transport handles it within app.run.

async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
"""Handles WebSocket disconnection."""
logger.info(f"WebSocket disconnected via Endpoint with code {close_code}")
# Add any necessary cleanup logic here if needed.

starlette_app = Starlette(debug=True, routes=[
WebSocketRoute("/ws", endpoint=MCPWebSocketEndpoint) # Use the class directly
])

async def run_server():
logger.info(f"Starting WebSocket server on port {port} using Endpoint")
await serve(starlette_app, config)

asyncio.run(run_server())
else:
# Process using stdio transport
import asyncio
from mcp.server.stdio import stdio_server

async def run_stdio_server():
logger.info("Starting stdio server")
async with stdio_server() as streams:
await app.run(
streams[0], streams[1], app.create_initialization_options()
)

asyncio.run(run_stdio_server())

return 0

注:AI生成的代码存在一处致命错误:on_connect方法中不能加await websocket.accept()这行代码,否则运行时报错:RuntimeError: Expected ASGI message “websocket.connect”, but got ‘websocket.receive’

3.1.3 测试服务

目前Cline(v3.10.1)/Cursor(v0.48.7)都不支持配置WebSocket服务,通过代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio
from mcp.client.session import ClientSession
from mcp.client.websocket import websocket_client


async def main():
async with websocket_client("ws://localhost:8000/ws") as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()

# List available tools
tools = await session.list_tools()
print(
f"Available tools: {', '.join([t.name for t in tools.tools])}")

# Call the fetch tool
result = await session.call_tool("fetch", {"url": "https://example.com"})
print(f"==================Result=================={result}")


asyncio.run(main())

启动服务端:

1
uv run mcp-simple-tool --transport ws --port 8000

image-20250416100540900

运行客户端:

1
uv run client-ws.py

image-20250416100626241

3.1.4 小结

官方文档中没有提到WebSocket这种实现,未来可能会出现变化。

3.2 WebMVC SSE服务 / Java

详见这里:【点击直达

相关链接