MCP入门级简单尝试

前言

既然MCP都已经出现了,甚至已经纳入面试题目了,就简单尝试一下这个新玩意儿。

建立一个最简单的工具

我们可以从其他的一些博客中了解到,MCP本质上就是一个MCP Server配上一个MMCP Client>,然后MCP Client就可以调用MCP Server提供的服务。

既然如此,我们的首要任务也就是建立一个相当简易的MCP Server

无参

既然要尽可能简单,那就输出Hello, World!吧。

所以,我们的方法就可以定义出来:

1
def hello() -> str: return "Hello, World!"

为了让MCP发现他是一个工具类方法,我们再加一个装饰器:

1
2
3
4
5
6
7
8
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("HelloServer")

@mcp.tool(description="Say hello to the world")
def hello() -> str: return "Hello, World!"

if __name__ == "__main__":
mcp.run()

看上去没啥问题,我们把这段写进server/basic_server.py文件中。

通过查看源码,我们可以知道,我们没有指定host(主机域名或IP)、port(主机开放端口)、transport(服务提供方式),于是MCP会给我们分配一个默认的配置:

  • host127.0.0.1
  • port8000
  • transportstdio

也就是说,上述代码,我们创立了一个运行在命令行中的MCP服务器。

然后,我们就可以在client中调用MCP了:

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
import asyncio
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client

async def hello() -> None:
# 通过 stdio 启动本地的 server.py
server = StdioServerParameters(
command="python",
args=["servers/basic_server.py"],
)

# 连接并初始化会话
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()

# 调用 "hello" 工具
result = await session.call_tool("hello")

# 打印文本类型的返回内容
for c in result.content:
if isinstance(c, types.TextContent):
print(c.text)
break

if __name__ == "__main__":
asyncio.run(hello())

可以看到,官方大量使用了async进行异步传输,所以我们也将大量使用awaitasyncio进行异步传输。

我们最后在执行这个客户端的时候,他会首先利用StdioServerParameters从终端启动MCP Server,然后再启动MCP Client,最后进行异步交互。

在交互过程中,我们拿到所有的输出,然后选择我们需要的输出。按道理来说,这个案例里面只会输出TextContent类型的结果,我们也只取其中的text属性。

也就是说,在MCP Client执行的终端里面,我们会看到Hello World!MCP Server那边因为没有单独启动,所以什么都没有。

有参

既然无参弄完了,那就试试有参?

就比如说,我输入什么东西,他都会返回:Hello, <your-input>

说来也简单,就是每个都带上参数就好了:

1
2
3
4
5
6
7
8
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("HelloServer")

@mcp.tool(description="Say hello to anything")
def hello(text: str) -> str: return f"Hello, {text}!"

if __name__ == "__main__":
mcp.run()

整挺好。写进servers/param_server.py中。

客户端也改一下:

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
import asyncio
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client

async def hello(text: str) -> None:
# 通过 stdio 启动本地的 server.py
server = StdioServerParameters(
command="python",
args=["servers/param_server.py"],
)

# 连接并初始化会话
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()

# 调用 "hello" 工具
result = await session.call_tool("hello", {"text": text})

# 打印文本类型的返回内容
for c in result.content:
if isinstance(c, types.TextContent):
print(c.text)
break

if __name__ == "__main__":
asyncio.run(hello("LangChain"))

于是,客户端就会输出:Hello, LangChain!

改用传输方式

我们可以从其他的博客中,看到传输方式包含stdiossehttp三种。因为stdio过于受限,sse又逐步暴露出更多的缺点,所以接下来的案例就直接上http了。

首先,因为默认给出来的就是stdio,所以我们首先要改一下MCP Server的传输方式配置,就像这样:

1
2
3
4
5
6
7
8
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("HelloServer")

@mcp.tool(description="Say hello to anything")
def hello(text: str) -> str: return f"Hello, {text}!"

if __name__ == "__main__":
mcp.run(transport="streamable-http")

虽然说,上面把transport参数和hostport参数放在了一起,但实际上他们在不同的位置起作用。transport参数在run中指定,而hostport参数在FastMCP构造函数中指定。

当然,我们还可以看源码,发现run(transport="streamable-http")实际等同于run_streamable_http_async(),因此我们还可以这么写:

1
2
3
4
5
6
7
8
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("HelloServer")

@mcp.tool(description="Say hello to anything")
def hello(text: str) -> str: return f"Hello, {text}!"

if __name__ == "__main__":
mcp.run_streamable_http_async()

值得注意的是,我们在其中并没有指定访问路由。这是因为MCP Server默认访问路由就是http://127.0.0.1:8000/mcp/。如果需要改动,同样在构造函数中指明:

1
2
3
4
5
6
mcp = FastMCP(
name = "HelloServer", # 名称,可以为None
host = "localhost", # 默认值
port = 8000, # 默认值
streamable_http_path = "/mcp" # 默认值
)

然后客户端的改动说大也不大:

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 import ClientSession, types
from mcp.client.streamable_http import streamablehttp_client

async def hello(text: str):
# Connect to HTTP streaming server
async with streamablehttp_client("http://localhost:8000/mcp/") as (read, write, get_session_id_callback):
async with ClientSession(read, write) as session:
await session.initialize()

# Call "hello" tool
result = await session.call_tool("hello", {"text": text})

# Print text-type return content
for c in result.content:
if isinstance(c, types.TextContent):
print(c.text)
break

if __name__ == "__main__":
asyncio.run(hello("LangChain"))

这里值得注意的是,streamablehttp_client包含三个内容:MemoryObjectReceiveStreamMemoryObjectSendStreamGetSessionIdCallback,从字面意义上区分也就是readwriteget_session_id_callback。而ClientSession的构造函数中,大量参数与上述三个的交集只有两个,分别是MemoryObjectReceiveStreamMemoryObjectSendStream,也就是只需要readwrite

剩下的就没变化了。

日志解析

虽然说在客户端这边只有一个Hello Langchain!,但是由于HTTP请求会单独拉起一个MCP Server,所以在MCP Server日志中,会产生一些日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INFO:     127.0.0.1:37280 - "POST /mcp/ HTTP/1.1" 307 Temporary Redirect
[08/13/25 15:55:33] INFO Created new transport with session ID: 6cf4fa2b3f8941cba2aff10439e268f9 streamable_http_manager.py:233
INFO: 127.0.0.1:37280 - "POST /mcp HTTP/1.1" 200 OK
INFO: 127.0.0.1:37294 - "POST /mcp/ HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:37308 - "GET /mcp/ HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:37294 - "POST /mcp HTTP/1.1" 202 Accepted
INFO: 127.0.0.1:37308 - "GET /mcp HTTP/1.1" 200 OK
INFO: 127.0.0.1:37322 - "POST /mcp/ HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:37322 - "POST /mcp HTTP/1.1" 200 OK
INFO Processing request of type CallToolRequest server.py:625
INFO: 127.0.0.1:37328 - "POST /mcp/ HTTP/1.1" 307 Temporary Redirect
INFO: 127.0.0.1:37328 - "POST /mcp HTTP/1.1" 200 OK
INFO Processing request of type ListToolsRequest server.py:625
INFO: 127.0.0.1:37344 - "DELETE /mcp/ HTTP/1.1" 307 Temporary Redirect
INFO Terminating session: 6cf4fa2b3f8941cba2aff10439e268f9 streamable_http.py:630
INFO: 127.0.0.1:37344 - "DELETE /mcp HTTP/1.1" 200 OK

这么大一串,其实看下来就是这么个流程:

  • 首先,默认服务是/mcp,你请求到了/mcp/,没事,给你掰回去(Redirecting);
  • 掰回去了,对上号了,用GET请求给你发一个号牌(SessionId);
  • 然后,收到了一个POST请求:CallToolRequest
  • 根据请求,先查一下字典里有没有这项服务:ListToolsRequest;
  • 执行请求并返回;
  • 结束会话(DELETE

这一套流程走完之后,一个依赖HTTPMCP请求就这样结束了。