如何开发一个 MCP Server,在这篇文章《MCP Server 开发实战指南(Python版)》已经做了详细的讲解了,但是可以看到这种包是基于本地环境运行的

上图可以看到 jira 的包不需要拉取代码本地执行,直接配置 mcp-atlassian 即可运行,更加方便快捷。原理是将 python 包上传到 pypi 上,这样命令 uvx 命令才能获取到对应python 代码包并执行。

https://pypi.org/project/mcp-atlassian

所以我们需要将 MCP Python代码封装成包的形式,并上传到 pypi 上,后面就可以通过 uvx package_name 直接访问到,不需要拉取代码到本地。

下面介绍下如何封装并上传包的完成过程。

项目结构

以下是一个标准的 Python 包代码结构示例,实现发送消息到企业微信群的功能

完整代码可以到这里获取

https://github.com/zhengjianhong001/mcp-server-qiwei

your-package-name/
├── pyproject.toml          # 项目元数据(必填)
├── .python-version         # 指定 Python 版本(可选)
├── src/                    # 包代码目录(推荐布局)
│   └── your_package/
│       ├── __init__.py           # 初始化模块
│       └── server.py             # MCP Tools
│       └── app_config.py         # 读取配置
│       └── core.py         
├── tests/                  # 测试目录(可选)
│   ├── __init__.py
│   └── test_core.py
├── README.md               # 项目说明
├── LICENSE                 # 开源协议
└── examples/               # 使用示例(可选)
    └── demo.py

代码解析

pyproject.toml 可以使用命令 poetry init 来生成,也可以直接拷贝下面的文件内容手动创建

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "your-package-name"
version = "0.1.0" # 版本号
authors = [
  {name = "Your Name", email = "you@example.com"},
]
description = "A brief description of your package"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
  "Programming Language :: Python :: 3",
  "License :: OSI Approved :: MIT License",
]

[project.urls]
Homepage = "https://github.com/yourusername/your-package"

# 依赖声明
dependencies = [
  "httpx",
  'requests',
  "mcp[cli]>=1.0.0",
]

# 开发依赖(可选)
[project.optional-dependencies]
dev = [
  "pytest>=7.0",
  "ruff>=0.5.4",
]

[project.scripts]
mcp-server-qiwei = "mcp_server_qiwei.server:main" # 指定默认执行的方法

模块代码(src/your_package/server.py

import os
import json
import requests
from mcp.server.fastmcp import FastMCP
from mcp_server_tapd.tapd import TAPDClient
from mcp-server-qiwei.app_config import AppConfig

mcp = FastMCP("mcp-tapd")
client = TAPDClient()

@mcp.tool()
def send_qiwei_message(msg: str) -> dict:
    """发送信息到企业微信群
    Args:
        msg: 推送的企业微信的信息,Markdown 格式(必填)
    Returns: <str> 
    """
    data = {
        "msg": msg,
    }
    return client.send_message(data)

def main():
    mcp.run()

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

构建和发布

在项目根目录下执行

uv build # 构建,完成后会生成目录 dist,下面放着压缩包
twine upload dist/* # 配置 ~/.pypirc后不需要手动输入密码

创建 ~/.pypirc,内容如下

[distutils]
index-servers =
    pypi

[pypi]
repository = https://upload.pypi.org/legacy/
username = __token__
password = ****

https://pypi.org/manage/account 获取 API token

使用

上传到 pypi 上后,就可以在 MCP Client 上使用了

https://pypi.org/project/mcp-server-qiwei/#description

{
  "mcpServers": {
    "mcp-server-qiwei": {
      "command": "uvx",
      "args": [
        "mcp-server-qiwei"
      ]
    }
}

上传前也可以先本地调试下

"mcp-server-qiwei": {
      "command": "uv",
      "args": [
        "--directory",
        "/path/mcp-server-qiwei/src/mcp_server_qiwei",
        "run",
        "server.py"
      ],
      "env": {
        "BOT_URL": ""
      }
    }

常见问题

上传的文件名已经存在

Error: This filename has already been used, use a different version.

使用 --verbose 选项来获取更详细的错误信息

twine upload --verbose dist/*

如果当前目录下不存在该文件,但是还是提示文件名已经被使用。PyPI 服务器可能存在缓存,导致它仍然认为该文件名已被使用。有时候服务器的缓存需要一些时间来更新,你可以等待几个小时之后再尝试上传。

引用同级目录包,提示包不存在

如果引用同目录的包时,需要在前面加上 “.”

因为在 Python 3 中,为了避免隐式相对导入带来的一些问题,它默认使用绝对导入。如果你想要进行相对导入,就需要显式地使用 . 或 .. 来指明相对路径。这里的 . 代表当前包,.. 代表上一级包。

避免命名冲突:使用显式相对导入可以避免命名冲突。假如你的项目中有一个模块名和 Python 标准库或者第三方库中的模块名相同,使用绝对导入可能会导入到错误的模块。而使用显式相对导入可以确保你导入的是当前包内的模块。

需要注意,显式相对导入只能在包内部使用。也就是说,只有当你的代码位于一个包中(即包含 __init__.py 文件的目录)时,才能够使用相对导入。如果你尝试在脚本文件(非包内模块)中使用相对导入,就会引发 ImportError