一、mcp介绍

mcp结构图如下:

二、安装mcp

mcp python sdk:https://github.com/modelcontextprotocol/python-sdk

安装uv包管理工具

1
2
# 全局安装uv包管理工具
pip install uv

安装sdk:

1
2
3
4
5
uv add "mcp[cli]" httpx
# 或者
pip install "mcp[cli]"
# 或者
uv pip "mcp[cli]" httpx

三、创建mcp服务

mcp server可以有本地的通过stdio来和client交互的server端,也有远程通过http sse协议和client交互的server端

3.1 创建mcp本地server

创建一个本地server,通过标准输入输出(stdio)和mcp client进行通信

3.1.1 创建项目

1
2
3
4
5
6
7
8
9
10
uv init mcpstdioserver  # 初始化项目
cd mcpstdioserver

# 创建并激活虚拟环境
uv venv
source .venv/bin/activate

# 安装包
uv add duckduckgo_search
uv add "mcp[cli]" httpx

如果是远程拷贝下来的项目,需要执行sync命令来安装包

1
uv sync # 已存在项目安装项目依赖(自动解析锁定文件)

3.1.2 使用fastmcp创建项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# main.py
from typing import Any
from mcp.server.fastmcp import FastMCP
from duckduckgo_search import DDGS

# Initialize FastMCP server
mcp = FastMCP("Demo")

@mcp.tool()
def run_duckduckgo(query:str):
"""
获取网络信息
"""
results = DDGS().text(query, max_results=10)

return results


if __name__ == "__main__":
mcp.run(transport='stdio')

3.1.3 服务发布

  1. 使用mcp工具启动服务
1
mcp dev main.py

mcp dev 是一个用于开发模式下启动 MCP服务的命令。mcp dev 命令会启动 MCP 服务。服务启动后会进入一个循环监听状态,等待接收请求。
同时开发模式下会启动开发辅助工具MCP Inspector。可以查看服务监控、可视化、参数修改等。

在mcp inspector中点击tools,便可以调试mcp暴露的接口,查看tool/call记录可以看到发的请求和返回的包

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
// Request
{
"method": "tools/call",
"params": {
"name": "run_duckduckgo",
"arguments": {
"query": "北京经纬度"
},
"_meta": {
"progressToken": 2
}
}
}
// Response
{
"content": [
{
"type": "text",
"text": "{\n \"title\": \"中国北京市 的纬度是 39.906217,经度是 116.3912757 - ABCD Tools\",\n \"href\": \"https://www.abcdtools.com/city-to-latlong/1825496\",\n \"body\": \"本网页提供中国北京市的经纬度查询服务,包括度分秒格式和度数格式。还可以查询附近地点的经纬度,如保定市、天津市、廊坊市等。\"\n}"
},
{
"type": "text",
"text": "{\n \"title\": \"Beijing Geographic coordinates - Latitude & longitude - Geodatos\",\n \"href\": \"https://www.geodatos.net/en/coordinates/china/beijing\",\n \"body\": \"Find out the exact location of Beijing using its latitude and longitude in different formats. Compare Beijing with other cities at the same or similar coordinates.\"\n}"
},
{
"type": "text",
"text": "{\n \"title\": \"Latitude and longitude of Beijing, China - GPS Coordinates - Latlong.info\",\n \"href\": \"https://latlong.info/china/beijing\",\n \"body\": \"Find the exact location of Beijing, China on the map using its latitude and longitude coordinates in decimal degrees and DMS format. Also, learn about its UTM zone, GeoHash code, time zone, districts and currency.\"\n}"
},
{
"type": "text",
"text": "{\n \"title\": \"经纬度查询 - 坐标拾取系统\",\n \"href\": \"https://jingweidu.bmcx.com/\",\n \"body\": \"经纬度是经度与纬度的合称组成一个坐标系统。又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置。 经度:东经为正数,西经为负数。 纬度:北纬为正数,南纬为负数。\"\n}"
},
{
"type": "text",
"text": "{\n \"title\": \"北京的经纬度范围 - 百度知道\",\n \"href\": \"https://zhidao.baidu.com/question/1122657503190067019.html\",\n \"body\": \"本网页回答了北京的经纬度范围和中心位置的问题,提供了百度地图的截图和相关信息。北京位于东经115.7°—117.4°,北纬39.4°—41.6°,中心位于北纬39°54′20″,东经116°25′29″。\"\n}"
},
//...
],
"isError": false
}
  1. 通过cline等客户端启动服务

同时服务可以用cline、cursor等工具调用,按如下配置到cline等工具的配置中,cline便会在本地执行这个工具,然后通过stdio,标准输入输出和cline中的mcp客户端进行数据传输

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// cline_mcp_setting.json
"mcpServers": {
"duckduckgo_search": {
"autoApprove": [],
"disabled": false,
"timeout": 60,
"command": "uv",
"args": [
"--directory",
"E:\\workspace\\mcp\\mcpstdioserver",
"run",
"main.py"
],
"transportType": "stdio"
},
}
  1. 本地测试完成之后,可以将包进行打包发布到pip仓库或者git,后续通过git拉取到本地执行。

3.2 创建mcp远程server

本地server是通过stdio进行通信,可以启动一个sse服务,则可以通过http sse协议进行通信

3.2.1 创建项目

1
2
3
4
5
6
7
8
9
10
11
12
uv init mcperver  # 初始化项目
cd mcpserver

# 创建并激活虚拟环境
uv venv
source .venv/bin/activate

# 安装包
uv add anyio
uv add click
uv add duckduckgo_search
uv add "mcp[cli]" httpx

3.2.2 创建server服务

修改代码,支持同时使用stdio和http 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
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
import anyio
import click
import httpx
import mcp.types as types
from mcp.server.lowlevel import Server
from duckduckgo_search import DDGS


async def search_from_network(
query: str,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
获取网络信息
"""
for i in range(10):
try:
# Perform a search using DuckDuckGo
results = DDGS().text(query, max_results=2)
break
except Exception as e:
# Handle any exceptions that occur during the search
print(f"Error occurred while searching: {e}")
if i == 9:
return [types.TextContent(type="text", text="Error occurred while searching")]
await anyio.sleep(1)
# If the loop completes without breaking, it means the search was successful
print(f'result={results}')
if not results:
return [types.TextContent(type="text", text="No results found")]

return [types.TextContent(type="text", text=results[0]['body']),types.TextContent(type="text", text=results[1]['body'])]



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 SSE")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server("mcp-demo")

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

@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",
}
},
},
),
types.Tool(
name="search",
description="从网络查询信息",
inputSchema={
"type": "object",
"required": ["query"],
"properties": {
"query": {
"type": "string",
"description": "需要查询的内容",
}
},
},
),
]
if transport == "sse":
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Mount, Route

sse = SseServerTransport("/sse/messages/")

async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await app.run(
streams[0], streams[1], app.create_initialization_options()
)
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/sse/messages/", app=sse.handle_post_message),
],
)

import uvicorn

uvicorn.run(starlette_app, host="0.0.0.0", port=port)
else:
from mcp.server.stdio import stdio_server

async def arun():
async with stdio_server() as streams:
await app.run(
streams[0], streams[1], app.create_initialization_options()
)

anyio.run(arun)

return 0

if __name__ == "__main__":
main()

3.3.3 启动服务

服务启动

1
2
3
4
5
6
7
8
9
# 使用stdio
uv run main.py
# 使用http
uv run main.py --transport sse --port 8000
# INFO: Started server process [3196]
# INFO: Waiting for application startup.
# INFO: Application startup complete.
# INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
# INFO: 127.0.0.1:53472 - "GET /sse HTTP/1.1" 200 OK

另外,更通用的方法是将该服务打包成一个工具。首先在pyproject.toml中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[project]
name = "mcpserver"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"anyio>=4.9.0",
"click>=8.2.0",
"duckduckgo-search>=8.0.1",
"httpx>=0.28.1",
"mcp[cli]>=1.8.1",
]

[project.scripts]
mcp-server = "main:main" # 指定命令名和入口点(main.py中的main函数)

mcpserver服务目录中执行

1
pip install -e .

该工具会将当前项目的脚本mcp-server打包到script目录:c/ProgramData/miniforge3/Scripts/mcp-server
于是,项目可以直接用uv启动工具,而不用在当前项目目录

1
2
3
4
5
6
$ uv run mcp-server --transport sse --port 8000
# INFO: Started server process [21436]
# INFO: Waiting for application startup.
# INFO: Application startup complete.
# INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
# INFO: 127.0.0.1:58616 - "GET /sse HTTP/1.1" 200 OK

启动后,要在cline里面配置

1
2
3
4
5
6
7
8
9
10
11
{
"mcpServers": {
"pcmgr-demo": {
"disabled": false,
"timeout": 30,
"url": "http://0.0.0.0:8000/sse",
"transportType": "sse",
"autoApprove": []
},
}
}

在cline的mcp服务器中可以看到,server启动成功

发起请求

参考

  1. mcp中文文档 https://mcp.maomu.com/specification/2025-03-26/basic/index
  2. mcp官方server:https://github.com/modelcontextprotocol/servers