mcp-toolserver
mcp-toolserver
一个 MCP 服务器,提供四个真实工具(文档搜索、SQL、算术、语料库自省),外加一个 agent 客户端:它连接该服务器、在运行时发现这些工具,并把它们与 Claude 串联起来,回答单个工具无法独自回答的问题。
它演示了什么
Model Context Protocol(模型上下文协议) —— 一种开放协议(Anthropic,2024 年 11 月),将 AI 应用连接外部工具和数据方式标准化。没有它,每个 AI 应用都需要为每个工具做定制集成,每个工具也需要为每个 AI 应用做定制集成——这是一个 N×M 的问题。MCP 将其变成 N+M:工具提供方构建一个 MCP 服务器,任何兼容 MCP 的客户端都可以使用它,无需任何定制集成代码。本仓库就是其中的一个具体小型实例:这里的服务器和客户端互不了解对方内部实现,只了解它们之间的协议。
动态工具发现 —— agent 客户端从不硬编码工具列表。它在连接时调用
list_tools(),并把服务器当前通告的任何工具转换成 Anthropic 的 tool-use 格式。在服务端新增或移除一个工具,客户端都会自动发现,无需改动任何客户端代码。多步工具串联 —— 单一问题可能需要先后使用两个不同的工具(先查出一个数值,再拿来计算),而 agent 循环能自行处理这一切:Claude 用第一个工具的结果去调用第二个工具,而无需被明确告知。
Related MCP server: Nexus
四个工具
工具 | 签名 | 用途 |
|
| 对 docmind 已收录的 PDF 语料库做语义搜索(稠密向量 embeddings + Chroma)。每个分块返回 |
|
| 对一个小型seed演示(预置种子数据)公司数据库( |
|
| 算术求值( |
|
| 已收录语料库的清单:每份文档返回 |
每个工具的 docstring 本身就是它的 MCP 描述——LLM 正是靠着读它来决定何时调用该工具,所以它们是写给 LLM 看的,不是写给快速浏览源码的人看的。
真实运行演示
下面三种情况都是真实运行的,跑在真实的 Claude API 和一个真实派生的 MCP 服务器子进程上——不是伪造的调用轨迹。最先展示的是真正串联了两个工具的那一个,因为那才是最有意思的场景。
1. 多步串联:query_database → calculate
“工程部门的平均工资是多少?给整个部门涨 12% 的价格是多少?”
Answer:
Here's the breakdown for the Engineering department:
| Metric | Value |
|---|---|
| Average Salary | $141,600 |
| Total Current Payroll | $708,000 |
| Cost of 12% Raise | $84,960 |
| New Total Payroll | $792,960 |
A 12% raise across all Engineering employees would cost an additional $84,960,
bringing the department's total payroll from $708,000 to $792,960.
Iterations: 3
Tool calls:
1. query_database({'sql': "SELECT AVG(salary) as avg_salary, SUM(salary) as total_salary FROM employees WHERE department_id = (SELECT id FROM departments WHERE name = 'Engineering')"})
-> [{'avg_salary': 141600.0, 'total_salary': 708000}]
2. calculate({'expression': '708000 * 0.12'})
-> 84960.0Claude 亲自写出了 SQL、读取了结果,然后又亲自写出并执行了算术表达式——上面的工具输入数据全部自动由 Claude 带出来的,不是硬编码。(手动验证:种子数据中 Engineering 部门 5 名员工工资合计 $708,000,÷5 = 平均 $141,600,×0.12 = $84,960。)
如果想看完整对话……GXP1
2. 单一工具:query_database
工程部门有多少员工?
Answer:
There are 5 employees in the Engineering department.
Iterations: 2
Tool calls:
1. query_database({'sql': "SELECT COUNT(*) as employee_count FROM employees e JOIN departments d ON e.department_id = d.id WHERE d.name = 'Engineering'"})
-> [{'employee_count': 5}]3. 单一工具:search_documents
什么是 corrective RAG?
Answer:
## Corrective RAG (CRAG)
Corrective RAG (CRAG) is an enhanced version of standard Retrieval-Augmented
Generation (RAG) that adds a self-correction step after the initial retrieval
phase. [...] Standard (vanilla) RAG simply takes the top-k retrieved documents
and passes them directly to the language model generator -- regardless of
whether those documents actually answer the question. CRAG improves on this by
checking retrieval quality before generation.
[... full answer continues with the retrieve -> grade -> (generate | rewrite &
retry) flow and the latency/LLM-call tradeoff, condensed here for length ...]
Iterations: 2
Tool calls:
1. search_documents({'query': 'corrective RAG'})
-> [5 chunks from langgraph_agents.pdf and llm_evaluation.pdf, scores 0.44-0.58]这个答案来自实际检索到的相关文本(docmind 的 langgraph_agents.pdf),不是来自关于 CRAG 的一般常识——Claude 其实也懂 CRAG,但在这次问答中并未要求它使用那些常识。
安全
query_database —— 分层防御,而不是单点防线:
应用级关键字/形状检查 —— 在进入 SQLite 之前就淘汰所有不是单个
SELECT(或WITH ... SELECT)的语句。这里的路上拦截了INSERT、UPDATE、DELETE、DROP、ALTER、CREATE、ATTACH、DETACH、PRAGMA、VACUUM、REINDEX,并且直接拒绝多语句(;分隔)。SQLite 原生只读模式 —— 连接本身通过 URI 中的
?mode=ro打开。这是由 SQLite 引擎强制生效的,不是应用代码来做这件事,所以当第 1 步存在缺口时,“只读”就是真正的后端防线:任何一个能奇谈绕过关键字检查的查询,在内的前提下都无法写入。行数上限 —— 每次查询都会被包成
SELECT * FROM (<query>) LIMIT 500,所以无论查询怎么要求,都不可能返回超过 500 列或行。真实时间(wall-clock)上限 —— 使用
sqlite3的 progress handler 检查已运行时长,超过期限就籽中止该语句。
calculate —— AST 白名单而不是 eval(): 这个工具会用 ast.parse(..., mode="eval") 解析表达式,再手动遍历 AST;只允许 Constant(数值)、BinOp(+ - * / ** %)和 UnaryOp(+/-)节点。任何其他的内容——比如 Name 变量查询、Call 调用、Attribute 属性访问——在遍历器里都没有对应的分支,因而会立刻抛出 ValueError。这正是它不会执行当前这个攻击,因为全部对应分支都是空:在调用路径上,根本不存在任何能执行 Call 节点的路径,而不是靠危险调用黑名单匹配。
设计决策
,而不是 Anthropic SDK 的 beta Tool Runner。 SDK 自带了一个 MCP bridge(
anthropic.lib.tools.mcp)可直接将 MCP 工具插到 Tool Runner 中。但这里没有用它,因为本项目目标是一个明确、可检查的返回契约 ——{answer, tool_calls: [{tool, input, output}], iterations}——需要围绕每一步手工记账。要用 Tool Runner 反而会把项目想展示的那些机制(循环控制、单次调用追踪)隐藏掉。stdio 传输,而不是 streamable-http。 客户端会把MCP服务器按需拉起成自己的子进程;代理双方都在同一个信任边界,也没有网络链路,于是 stdio 的简单性(不需要端口、不需要多余的鉴权)非常合适。streamable-http 本身也是支持的(
--transport streamable-http/ 环境变量MCP_TRANSPORT),用于服务器与客户端真正以独立进程/机器形式传输的场景,但本项目没有对此做任何对生产加固(见 已知限制)。8 次 8 次迭代上限。 避免一个出现失控的循环导致最坏情况下的时间和延迟过高——这也是 docmind 的 rewrite cap 相同的思路。上面三个演示运行都用了 2~3 次就完成了;8 次某个很富余的上限,它是用来拦住真畸形请求或模型不稳定时,而不是常规使用中会被碰到的全家。
与 docmind 的关系
search_documents 和 list_documents 直接读取 docmind 自己持久化的 Chroma collection(DOCMIND_CHROMA_PATH,默认现在指向相邻的 docmind 项目里的 data/chroma),查询时使用和 docmind 当时 intake 时用相同的 all-MiniLM-L6-v2 模型做 embedding。这层关联在代码上和 docmind 没有任何绑定——它只不过是一个配置好的路径下的 Chroma collection——所以这个项目也语料库的第二个消费者,而不是源。它是这样一个小小的验证:docmind 的检索层并不只接在 docmind 自家的 FastAPI 后端上;任何知道这个 collection 位置的协议感知客户端也都能直接访问。
已知限制
示例 SQL 数据库非常小且数据是捏造的夸张(12 名员工,5 个部门),本项目当前没有在真实规模或对抗性数据库环境中做过测试。
SQL 关键字/否定列表是对查询文本做一个正则判断,并没有拿真正的 SQL 解析器来跑——它会误( 例如一个不构成风险的
pragma_table_info()表值函数引用 ),也原则上也可能漏掉某些没法想到的语法。在不会被拦截的情况下由一个“只读连接”模式作为不依赖该列表完备性的最终防线。calculate只支持数值字面量与注释里列出的 6 个运算符——不支持函数(sqrt、sin…… 也不支持变量。刻意保持极小化,而不是做一个通用表达式引擎。MCP 服务器没有接入认证。对 stdio 这个正是刚合适(进程内单边条件下),但若按照当前实现那样跑在
streamable-http上,任何人只要能够触达这个端口,就可以调用任何工具,包括query_database。8 次迭代上限是硬性终止,而非优雅降级——某个合法问题需要多于约 4 次工具往返时,它得到的会是“在循环 8 次后停止”报错,而不是真实答案。
CLI 调用之间没有上下文链接记忆——每次
python -m toolserver.client.agent "..."提问都会从一个全新的空会话开始,不保留历史。不支持流式输出——每个循环迭代都是一次阻塞的
messages.create调用;某个比较慢的工具或长输出就会卡掉整一个人回合。测试完成了对 Anthropic 客户端和 MCP
Client的完全 mock(这是设计如此——测试套件不访问任何真实 API)。这意味着任一 SDK 出现 schema 漂移的话单凭pytest完全不可能发现;项目上面验证过的那些“实际运行演示”,是唯一的真实 API 验证方式——它是手动验证,并包含不进 CI。
安装与运行
需要 Python 3.12、环境变量中提供一个 ANTHROPIC_API_KEY,并且(若要运行 search_documents/list_documents)需要一个已经进入了 docmind 的 repo 副本。
git clone https://github.com/roshano3o3/mcp-toolserver.git
cd mcp-toolserver
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
cp .env.example .env # edit .env and set ANTHROPIC_API_KEY默认情况下,.env.example 里的 DOCMIND_CHROMA_PATH 指向一个相邻 docmind 仓库checkout 的 data/chroma。请把它指向你自己 docmind 语料所在的真实位置,或者干脆忽略它——query_database、calculate 以及 list_documents 的错误路径在完全没有 docmind 仓库时也能正常运行。
直接运行 agent(它会自动把 MCP server 作为子进程跑起来,不需要单独启动一个 server 进程):
python -m toolserver.client.agent "How many employees are in the Engineering department?"也可以单独启动 MCP server,例如需要把其他 MCP 客户端指到这个 server 时:
python -m toolserver.server # stdio (default)
python -m toolserver.server --transport streamable-http # http://127.0.0.1:8765/mcp by default运行测试:
pytest
ruff check .对着已安装 SDK 的验证(不是凭空记忆盲目写的)
mcp==2.0.0 稳定版本与旧版 mcp.server.fastmcp.FastMCP API 差异很大——那个新老 library 在这样的版本已经被替换。下面所有这些结论都是靠读已安装包的源码,并跑了 live smoke test(in-process 和真实 stdio 子进程两种方式)验证,而不是代称数据背出来:
服务端:
from mcp.server.mcpserver import MCPServer——MCPServer("name")创建一个服务。工具用@server.tool()注册(括号是必需的;不区分)@server.tool完全没有括号的话在注册时会故意照成错误。server.run(transport="stdio" | "sse" | "streamable-http")。客户端:
from mcp.client import Client—— 新的统一客户端,替换了大多数情况下的直接用ClientSession。它接收一个进程内的Server/MCPServer,一个 URL 字符串,或一个Transport(例如stdio_client(StdioServerParameters(...)))。发现工具:
await client.list_tools()→ 返回ListToolsResult,其中每个工具包含name、description、input_schema,它们与 Anthropic message 工具调用格式的字段名称完全一致,客户端转换为 from 。 ,几乎只是直接映射,无需做 schema 翻译。工具运行结果:
CallToolResult同时携带.content(MCP content block 的列表,永远有条目)和.structured_content(一个带类型的{"result": ...}dict,在工具函数本身返回了类型标注时会填充——这里四个工具正好都属于这种情况)。
This server cannot be deployed
Maintenance
Related MCP Connectors
AI Visibility and Content Intelligence tools for Claude and MCP-compatible agents.
One MCP endpoint for Claude, GPT & Gemini: 100+ tools + no-code connectors + agent workers.
Multiple MCP tools, persistent graph memory, token-saving data pointers, and more.
- WauldoOAuthcom.wauldo
Stateless agentic tools over MCP: concept extraction, long-context, knowledge graph, planning.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables MCP-compatible clients to perform retrieval-grounded Q&A over FordA dataset notes and safe arithmetic calculations through callable tools.-
- AlicenseNot gradedqualityCmaintenanceEnables models to query enterprise systems—structured data, live GitHub REST API, and unstructured document embeddings—through a single, per-caller scoped MCP tool surface with task-shaped tools and recovery-aware errors.90 npmISC
- FlicenseNot gradedqualityCmaintenanceEnables an LLM to perform safe arithmetic calculations and web searches through a standardized MCP interface, returning results to generate informed responses.-
- AlicenseNot gradedqualityBmaintenanceEnables Claude Code and Codex assistants to research and read/write a team's knowledge base, manage projects and files, and use integrations through MCP tools.Apache 2.0