Tool calling is where many AI agent projects slow down. Each model, client, and external service tends to have its own way of describing tools, passing arguments, and returning results. MCP, the Model Context Protocol, addresses that friction by defining a common layer between an LLM and the tools it can use. The model talks to tools through a consistent format, whether those tools are HTTP APIs, local programs, or hardware-facing utilities.
The practical benefit is reuse. A tool written once can be plugged into any MCP-aware client, such as Cherry Studio or Cursor, without a custom adapter for each host. MCP also supports two transport styles: STDIO for local invocation and SSE for remote invocation. That means the same basic development approach can serve both a quick local experiment and a shared cloud service.
What MCP standardizes
MCP is essentially a contract for how an LLM discovers and calls external tools. It standardizes tool descriptions, parameter formats, and communication. That removes much of the per-tool integration work that otherwise accumulates across projects. Cross-platform compatibility comes from the transport layer: STDIO works well for local development and single-machine tools, while SSE exposes an MCP service over HTTP for remote clients and multiple users.
<table> <thead> <tr> <th>Transport</th> <th>How it works</th> <th>Strengths</th> <th>Limits</th> <th>Good fit</th> </tr> </thead> <tbody> <tr> <td>STDIO</td> <td>The model communicates with the MCP tool through standard input and output</td> <td>No network needed, low latency, simple configuration</td> <td>Local only, not shared across clients</td> <td>Local debugging and single-machine tools</td> </tr> <tr> <td>SSE</td> <td>An HTTP-based server pushes messages, and the model calls the MCP service over the network</td> <td>Remote access, multiple clients can share the service</td> <td>Requires a deployed server and network access</td> <td>Team collaboration and cloud-hosted tools</td> </tr> </tbody> </table>A complete MCP tool has three parts. The tool function implements the actual behavior, such as web search or arithmetic. The protocol wrapper exposes that function through the MCP SDK and follows the expected schema. The transport module handles STDIO or SSE and carries messages between the tool and the model.
Setting up the Python environment
For Python work, uv is a practical choice because it handles environment creation and dependency resolution quickly. Install it first:
# macOS/Linux 安装 uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows 安装(PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
# 验证安装
uv --version # 输出 uv x.y.z 即成功
MCP development is best with Python 3.10 or newer. The example here uses Python 3.13.7:
# 安装指定版本 Python
uv python install 3.13.7
# 查看已安装 Python 版本
uv python list
Create the project, set up a virtual environment, and add the core dependencies:
# 初始化 MCP 项目(指定 Python 版本)
uv init mcp-web-search -p 3.13
cd mcp-web-search
# 创建并激活虚拟环境
uv venv
# Windows 激活
.venv\Scripts\activate.bat
# macOS/Linux 激活
source .venv/bin/activate
# 安装核心依赖
uv add "mcp[cli]" # MCP 核心 SDK
uv add httpx # HTTP 请求库(用于调用外部 API)
uv add python-dotenv # 环境变量管理
uv add openai # 可选,用于大模型交互测试
uv add logging # 日志管理
After initialization, the project layout is intentionally small:
mcp-web-search/
├── .env # 环境变量配置(存储 API Key 等敏感信息)
├── pyproject.toml # 项目依赖与配置
├── .venv/ # 虚拟环境
└── web_search.py # MCP 工具核心代码
For VS Code, Python, Python Debugger, DotENV, and Pylance are useful additions. They provide core language support, debugging, .env syntax highlighting, and type checking.
Building a multi-purpose MCP tool
The example tool in this guide combines three functions: web search, integer addition, and host information lookup. That covers API calls, local computation, system interaction, and error handling in one small service.
Sensitive values belong in a .env file rather than in source code:
# .env 文件内容
ZHI_PU_AI_API_KEY=你的智谱 AI API Key # 用于网页搜索功能
The main implementation uses FastMCP to register tools and run the server:
import httpx
import json
import os
import logging
import platform
from dotenv import load_dotenv
from mcp.server import FastMCP
# 1. 初始化配置
# 加载环境变量
load_dotenv()
# 配置日志(便于调试)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("mcp-web-search")
# 2. 初始化 MCP 服务器(服务名称:web-search)
app = FastMCP("web-search")
# 3. 验证敏感配置
ZHI_PU_API_KEY = os.getenv("ZHI_PU_AI_API_KEY")
if not ZHI_PU_API_KEY:
logger.error("未配置 ZHI_PU_AI_API_KEY 环境变量")
raise ValueError("请在 .env 文件中配置 ZHI_PU_AI_API_KEY")
# 4. 工具函数 1:网页搜索(调用智谱 AI 网页搜索 API)
@app.tool()
async def web_search(query: str) -> str:
"""
搜索互联网最新信息,支持各类公开内容查询(如新闻、知识、数据)
:param query: 搜索关键词(如"2025 年 AI 发展趋势")
:return: 搜索结果的结构化总结(多个结果用三个换行分隔)
"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
# 构建请求参数
request_data = {
"tool": "web-search-pro",
"messages": [{"role": "user", "content": query}],
"stream": False
}
# 发送请求
response = await client.post(
url="https://open.bigmodel.cn/api/paas/v4/tools",
headers={"Authorization": ZHI_PU_API_KEY},
json=request_data
)
# 处理 HTTP 错误(非 200 状态码抛出异常)
response.raise_for_status()
response_json = response.json()
# 解析搜索结果
search_results = []
for choice in response_json.get("choices", []):
tool_calls = choice.get("message", {}).get("tool_calls", [])
for tool_call in tool_calls:
result_list = tool_call.get("search_result", [])
for result in result_list:
content = result.get("content", "")
if content.strip():
search_results.append(content)
# 无结果处理
if not search_results:
return "未找到相关搜索结果,请尝试调整关键词"
return "\n\n\n".join(search_results)
except httpx.HTTPStatusError as e:
error_msg = f"HTTP 错误:状态码 {e.response.status_code},内容:{e.response.text[:200]}"
logger.error(error_msg)
return f"网页搜索失败:{error_msg}"
except httpx.RequestError as e:
error_msg = f"网络请求失败:{str(e)}"
logger.error(error_msg)
return f"网页搜索失败:{error_msg}"
except json.JSONDecodeError as e:
error_msg = f"响应解析失败:{str(e)}"
logger.error(error_msg)
return f"网页搜索失败:{error_msg}"
except Exception as e:
logger.exception("网页搜索发生未知错误")
return f"网页搜索失败:未知错误 - {str(e)}"
# 5. 工具函数 2:数值加法(本地计算工具)
@app.tool()
def add(a: int, b: int) -> int:
"""
计算两个整数的和,支持正负整数运算
:param a: 第一个整数
:param b: 第二个整数
:return: 两个整数的和
"""
try:
result = a + b
logger.info(f"加法运算:{a} + {b} = {result}")
return result
except Exception as e:
logger.error(f"加法运算失败:{str(e)}")
raise ValueError(f"计算失败:{str(e)}")
# 6. 工具函数 3:获取主机信息(本地系统工具)
@app.tool()
def get_host_info() -> str:
"""
获取当前主机的系统信息,包括操作系统、硬件架构等
:return: 结构化的主机信息(JSON 字符串格式)
"""
try:
host_info = {
"操作系统": platform.system(),
"主机名": platform.node(),
"系统版本": platform.release(),
"内核版本": platform.version(),
"硬件架构": platform.machine(),
"处理器": platform.processor() or "未知"
}
# 格式化 JSON 输出(便于大模型解析)
return json.dumps(host_info, ensure_ascii=False, indent=4)
except Exception as e:
logger.error(f"获取主机信息失败:{str(e)}")
return f"获取主机信息失败:{str(e)}"
# 7. 启动 MCP 服务(默认 STDIO 传输)
if __name__ == "__main__":
logger.info("MCP 工具服务启动中...")
# 支持通过命令行参数指定传输方式(如 --transport sse)
app.run(transport="stdio")
The details matter more than the code volume. Each tool needs a clear docstring because the model uses that description to decide when the tool is relevant. Type annotations on arguments and return values help the model pass arguments correctly. Every tool should catch exceptions and return a useful message instead of letting the service crash. The decorator choice also has meaning: @app.tool() is for actions with side effects, such as API calls or data changes, while @app.resource() is intended for read-only resources and resembles an HTTP GET. This example uses only @app.tool().
For secrets, python-dotenv keeps API keys out of source files. In production, environment variables can also be set at the system level, such as in ~/.zshrc on Linux, which reduces the chance of accidental disclosure.
Debugging and client integration
STDIO mode is the quickest way to start the service locally:
# 启动 MCP 工具(STDIO 模式)
uv run web_search.py
The process waits for input from the model or a debugging client. MCP Inspector provides a more direct way to exercise each tool:
# 安装 MCP Inspector(需 Node.js 20+)
npm install -g @modelcontextprotocol/inspector
# 启动调试
mcp-inspector uv run web_search.py
In the inspector, select a tool such as web_search, enter parameters, and run it. Logs appear in real time, which makes it easier to see whether the failure is in argument handling, the external API, or the response parser.
Cherry Studio is a useful client for an end-to-end check. Open Settings, go to MCP Servers, and add an MCP server. Point the directory to the mcp-web-search project root and use uv run web_search.py as the startup command. Start the server; if no red warning appears in the upper-right corner, the connection is active. Then test with natural language: 'Calculate 123 + 456' should invoke add, '2025 Python features' should invoke web_search, and 'Show the current system information' should invoke get_host_info.
Moving to SSE for remote access
When a team needs to share the tool, SSE mode turns the same service into an HTTP-accessible endpoint. The startup block can be adjusted to accept transport, host, and port arguments:
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="MCP 工具服务(支持 STDIO/SSE)")
parser.add_argument("--transport", default="stdio", choices=["stdio", "sse"], help="传输方式")
parser.add_argument("--host", default="0.0.0.0", help="SSE 服务绑定地址")
parser.add_argument("--port", default=8000, type=int, help="SSE 服务端口")
args = parser.parse_args()
logger.info(f"MCP 服务启动:传输方式={args.transport},端口={args.port}")
if args.transport == "sse":
app.run(transport="sse", host=args.host, port=args.port)
else:
app.run(transport="stdio")
Start the SSE service on port 8000:
# 启动 SSE 服务(绑定 8000 端口)
uv run web_search.py --transport sse --port 8000
In the AI client, choose SSE transport and enter the service address, for example http://your-server-ip:8000/mcp. For production, open the port in the firewall, keep the process alive with nohup or systemd, write logs to a file for later inspection, and add API key validation so that only authorized clients can reach the service.
Publishing and reuse
A local STDIO tool can be distributed through PyPI for Python users, who can install it with pip and run it directly. A Node.js tool can go through npm and be invoked with npx. For SSE services, deploy to a cloud server and provide a public address, or register the tool in an MCP marketplace such as Anthropic MCP Hub so other developers can discover it.
A few development habits pay off. Combine tools when a workflow needs multiple steps, such as search, data processing, and report generation. Use Pydantic when arguments need stronger validation, such as string length or numeric ranges. Add caching with something like lru_cache to high-frequency tools that make repeated API calls or calculations. MCP itself is not limited to Python; Node.js and other languages are also supported, so the language can follow the use case.
Common failure points
Environment variable loading is a frequent first stumble. Check that .env sits in the project root and that the variable name matches exactly. A quick print(os.getenv('ZHI_PU_API_KEY')) confirms whether the value is visible to the process.
If the MCP service fails to start, the usual causes are missing dependencies, an incompatible Python version, or a port conflict. Reinstall dependencies with uv, confirm Python 3.10 or newer, and change the SSE port if 8000 is already occupied.
When the model cannot call a tool, inspect the tool description and type annotations first. The docstring must make the tool's purpose obvious, every argument and return value needs a type, and the client transport must match the server transport. A STDIO client cannot connect to an SSE server, and an SSE client cannot launch a local STDIO process by itself.
If web search returns a failure, verify the API key, check network access to the search API, and confirm that the endpoint parameters have not changed. The error message returned by the tool should be specific enough to distinguish an authentication problem from a timeout or a parsing failure.
The larger value of MCP is that a tool becomes portable across clients. The same implementation can support local experiments, team workflows, and cloud deployment without a separate adapter for each environment. As the protocol matures, more transport options, multimodal tools, and permission controls are likely to appear. The practical starting point is still small: define one useful tool, describe it clearly, handle its errors, and connect it to a client that speaks MCP.