fix(mcp-server): migrate to MCPServer high-level API (mcp 2.x) [v1.1.0]

MCP Python SDK 2.0 removed the low-level Server decorator API
(@app.list_tools() / @app.call_tool() / app.get_capabilities()), so the
server crashed at startup with:
  AttributeError: 'Server' object has no attribute 'list_tools'

The released PyPI package weknora-mcp==1.0.1 declared 'mcp>=1.0.0' with
no upper bound, so uvx resolved to the latest 2.0.0 and the plugin's
weknora MCP server could no longer connect.

Fix: rewrite the server on top of the mcp 2.x high-level MCPServer API
(formerly FastMCP), and cap the dependency at 'mcp>=2,<3'.

- weknora_mcp_server.py: replace @app.list_tools()/@app.call_tool() with
  28 @mcp.tool() function-signature tools (input schema auto-generated
  from type hints, descriptions from docstrings, plain return values
  serialized by the framework). Transports use run_stdio_async() /
  sse_app() / streamable_http_app(). WeKnoraClient business logic is
  unchanged.
- deps: mcp>=2,<3 in pyproject.toml / requirements.txt / setup.py;
  commit uv.lock pinning mcp 2.0.0.
- version bump to 1.1.0 (pyproject, setup, __init__, main --version,
  server_version).
- check_imports.py: validate MCPServer / ToolError / mcp_types imports.
- ci(.github/workflows/mcp-server.yml): publish on merge to main +
  manual dispatch, not only on tags; add PyPI version-dedup check so
  re-runs on an unchanged version stay green (absorbed from PR #2399).

Verified: stdio initialize + tools/list returns all 28 tools; no
list_tools AttributeError; tool dispatch + error wrapping confirmed.

Co-authored-by: wizardchen <wizardchen@tencent.com>
This commit is contained in:
veater
2026-07-30 15:52:28 +08:00
committed by lyingbug
co-authored by wizardchen
parent cbe668c06f
commit 37837c00a9
11 changed files with 2110 additions and 858 deletions
+43 -3
View File
@@ -4,7 +4,13 @@ name: Build & Publish (mcp-server)
# Publishing uses PyPI Trusted Publishing (OIDC) — no API token required,
# only the GitHub→PyPI OIDC trust configured on the repo.
#
# Release tags: mcp-server-v* (e.g. mcp-server-v1.0.1)
# Release policy: publishing does NOT depend on tags. Bump the version in
# mcp-server/pyproject.toml (and the synced version strings in setup.py,
# __init__.py, weknora_mcp_server.py server_version, main.py --version),
# merge to main, and the pipeline builds + publishes automatically.
# A pre-publish check queries PyPI and skips upload if that version is
# already published, so re-running on an unchanged version stays green.
# Releases also still trigger on mcp-server-v* tags (back-compat).
on:
push:
@@ -55,7 +61,8 @@ jobs:
name: Build sdist + wheel
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/mcp-server-v')
# Build on push to main, manual runs, and release tags — but never on PRs.
if: github.event_name != 'pull_request'
defaults:
run:
working-directory: mcp-server
@@ -90,18 +97,51 @@ jobs:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/mcp-server-v')
# Publish on push to main, manual runs, and release tags — but never on PRs.
if: github.event_name != 'pull_request'
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/project/weknora-mcp/
steps:
- uses: actions/checkout@v4
- name: Check if version is already published
id: check_pypi
working-directory: mcp-server
# Read the version from pyproject.toml and query PyPI. If that exact
# version already exists, skip publishing (so re-runs on an unchanged
# version stay green instead of failing with "File already exists").
run: |
set -e
version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
echo "packaged version: $version"
exists="$(python3 -c '
import json, sys, urllib.request
v = sys.argv[1]
try:
d = json.load(urllib.request.urlopen("https://pypi.org/pypi/weknora-mcp/json"))
print("true" if v in d.get("releases", {}) else "false")
except Exception:
# If PyPI is unreachable or the project is brand-new (404), treat as unpublished.
print("false")
' "$version")"
echo "already on PyPI: $exists"
if [ "$exists" = "true" ]; then
echo "should_publish=false" >> "$GITHUB_OUTPUT"
echo "::notice::weknora-mcp $version is already on PyPI; skipping publish. Bump the version to release."
else
echo "should_publish=true" >> "$GITHUB_OUTPUT"
fi
- name: Download artifacts
if: steps.check_pypi.outputs.should_publish == 'true'
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
if: steps.check_pypi.outputs.should_publish == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
+16 -1
View File
@@ -7,8 +7,23 @@
## [Unreleased]
## [1.1.0] - 2026-07-30
### 修复
- 将 `mcp` 依赖上限锁定为 `<2`,避免 `pip install` / `uv sync` 自动安装 MCP Python SDK 2.x 导致 `@app.list_tools()` 启动失败(`AttributeError: 'Server' object has no attribute 'list_tools'`)
- 修复 MCP Python SDK 2.x 下服务器启动崩溃(`AttributeError: 'Server' object has no attribute 'list_tools'`)。
SDK 2.0 移除了低层 `Server` 的装饰器 API(`@app.list_tools()` / `@app.call_tool()` / `app.get_capabilities()`),
而通过 `uvx` 拉起已发布包时会解析到最新 2.x,导致连接被关闭。
### 变更
- 将 MCP 服务器实现从低层 `Server` 迁移到高层 `MCPServer` API(mcp 2.x,原 FastMCP 改名)。
- 28 个工具重写为 `@mcp.tool()` 函数签名式:输入参数走类型标注(schema 由框架自动生成),
描述走 docstring,返回纯 Python 值由框架序列化。
- 传输层改用 `run_stdio_async()` / `sse_app()` / `streamable_http_app()`,鉴权仍由 `MCPAuthMiddleware` 包裹。
- `WeKnoraClient` 业务逻辑(REST/SSE 调用、resolve_*、wiki)保持不变。
- 依赖上限调整为 `mcp>=2,<3`(发布包与开发环境一致,避免再次因无上限被解析到未来破坏性版本)。
### 注意
- 本版本要求运行环境 `mcp>=2`。若临时需要旧版 SDK,可在启动命令加 `--with "mcp<2"`,但建议升级到本版本。
## [1.0.1] - 2026-07-28
+2 -3
View File
@@ -137,9 +137,8 @@ WeKnoraMCP/
### 添加新功能
1. 在 `WeKnoraClient` 类中添加新的 API 方法
2. 在 `handle_list_tools()` 中注册新工具
3. 在 `handle_call_tool()` 中实现工具逻辑
4. 更新文档和测试
2. 用 `@mcp.tool()` 装饰器注册一个新工具函数:参数用类型标注(schema 自动生成),描述写在 docstring 里,函数体调用上面新增的客户端方法
3. 更新文档和测试
### 测试
```bash
+1 -1
View File
@@ -5,7 +5,7 @@ WeKnora MCP Server Package
A Model Context Protocol server that provides access to the WeKnora knowledge management API.
"""
__version__ = "1.0.1"
__version__ = "1.1.0"
__author__ = "WeKnora Team"
__description__ = "WeKnora MCP Server - Model Context Protocol server for WeKnora API"
+19 -24
View File
@@ -6,41 +6,36 @@
"""
try:
import mcp.types as types
print("✓ mcp.types 导入成功")
except ImportError as e:
print(f"✗ mcp.types 导入失败: {e}")
try:
from mcp.server import NotificationOptions, Server
print("✓ mcp.server 导入成功")
except ImportError as e:
print(f"✗ mcp.server 导入失败: {e}")
try:
import mcp.server.stdio
import mcp.server.stdio # noqa: F401
print("✓ mcp.server.stdio 导入成功")
except ImportError as e:
print(f"✗ mcp.server.stdio 导入失败: {e}")
try:
from mcp.server.models import InitializationOptions
# mcp 2.x high-level server API (MCPServer, formerly FastMCP).
from mcp.server import MCPServer
print("✓ InitializationOptions 从 mcp.server.models 导入成功")
except ImportError:
try:
from mcp import InitializationOptions
print("✓ MCPServer 从 mcp.server 导入成功")
except ImportError as e:
print(f"✗ MCPServer 导入失败: {e}")
print("✓ InitializationOptions 从 mcp 导入成功")
except ImportError as e:
print(f"✗ InitializationOptions 导入失败: {e}")
try:
from mcp.server.mcpserver.exceptions import ToolError # noqa: F401
print("✓ ToolError 从 mcp.server.mcpserver.exceptions 导入成功")
except ImportError as e:
print(f"✗ ToolError 导入失败: {e}")
try:
import mcp_types # noqa: F401 # standalone protocol types in mcp 2.x
print("✓ mcp_types 导入成功")
except ImportError as e:
print(f"✗ mcp_types 导入失败: {e}")
# 检查 MCP 包结构
import mcp
print(f"\nMCP 包版本: {getattr(mcp, '__version__', '未知')}")
print(f"MCP 包路径: {mcp.__file__}")
print(f"MCP 包内容: {dir(mcp)}")
+1 -1
View File
@@ -85,7 +85,7 @@ def parse_arguments():
parser.add_argument("--verbose", "-v", action="store_true", help="启用详细日志输出")
parser.add_argument(
"--version", action="version", version="WeKnora MCP Server 1.0.1"
"--version", action="version", version="WeKnora MCP Server 1.1.0"
)
parser.add_argument(
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "weknora-mcp"
version = "1.0.1"
version = "1.1.0"
description = "WeKnora MCP Server - Model Context Protocol server for WeKnora API"
readme = "README.md"
license = {text = "MIT"}
@@ -30,7 +30,7 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"mcp>=1.28,<2",
"mcp>=2,<3",
"requests>=2.31.0",
"starlette>=0.27.0",
"uvicorn>=0.24.0",
+1 -1
View File
@@ -1,4 +1,4 @@
mcp>=1.28,<2
mcp>=2,<3
requests>=2.31.0
starlette>=0.27.0
uvicorn>=0.24.0
+2 -2
View File
@@ -24,7 +24,7 @@ def read_requirements():
]
except FileNotFoundError:
return [
"mcp>=1.28,<2",
"mcp>=2,<3",
"requests>=2.31.0",
"starlette>=0.27.0",
"uvicorn>=0.24.0",
@@ -33,7 +33,7 @@ def read_requirements():
setup(
name="weknora-mcp",
version="1.0.1",
version="1.1.0",
author="WeKnora Team",
author_email="support@weknora.com",
description="WeKnora MCP Server - Model Context Protocol server for WeKnora API",
+1652
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff