文档问答MCP服务器
一个本地可运行的MCP(模型上下文协议)服务器,对PDF文档进行索引并公开 query_documents 用于源归因的基础问答工具。内置FastMCP、ChromaDB、句子转换器和Gemini 2.5 Flash。
建筑
flowchart TD
subgraph ingestion ["Ingestion Pipeline (run once via ingest.py)"]
PDFs["data/pdfs/*.pdf"] --> Parser["parser.py
PyMuPDF"]
Parser -->|"pages with metadata"| Chunker["chunker.py
sliding window"]
Chunker -->|"overlapping chunks"| Embedder["embedder.py
all-MiniLM-L6-v2"]
Embedder --> ChromaDB["data/chroma_db/"]
end
subgraph server ["MCP Server (always on via mcp_server.py)"]
Client["Claude / MCP Client"] -->|"query_documents(question)"| Server["server.py"]
Client -->|"list_documents()"| Server
Server --> Retriever["retriever.py
cosine search"]
Retriever --> ChromaDB2["data/chroma_db/"]
ChromaDB2 -->|"top-k chunks"| LLM["llm.py
Gemini 2.5 Flash"]
LLM --> Server
Server -->|"JSON: answer + sources"| Client
end
ChromaDB -.->|"persisted index"| ChromaDB2先决条件
- Python 3.10+
- A免费 Gemini API密钥 (仅用于LLM答案,不用于嵌入)
设置
# Clone and enter the project
cd nexla-mcp-doc-qa
# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Set your API key
cp .env.example .env
# Edit .env and add your GEMINI_API_KEY
# Run ingestion (one-time, indexes all PDFs)
python3 ingest.py
# Start the MCP server
python3 mcp_server.py项目结构
nexla-mcp-doc-qa/
├── mcp_server.py # Entry point: runs the MCP server
├── ingest.py # Entry point: runs the ingestion pipeline
├── src/
│ ├── config.py # All constants: paths, models, chunk params
│ ├── server.py # FastMCP app + tool handlers
│ ├── ingestion/ # Offline — run once
│ │ ├── parser.py # PyMuPDF: PDF → [{text, doc_name, page}]
│ │ ├── chunker.py # Sliding window: pages → overlapping chunks
│ │ ├── embedder.py # all-MiniLM-L6-v2 → ChromaDB storage
│ │ └── pipeline.py # Orchestrator: parser → chunker → embedder
│ └── core/ # Online — used at query time
│ ├── retriever.py # Embed question → ChromaDB cosine search
│ └── llm.py # Build prompt → Gemini 2.5 Flash → parse JSON
├── data/
│ ├── pdfs/ # Input PDFs (5 AI/ML research papers)
│ └── chroma_db/ # Vector index (auto-created, gitignored)
├── requirements.txt
├── .env.example
└── .gitignore索引文档
| 文档 | 描述 |
|---|---|
attention_is_all_you_need.pdf | 原始变压器纸(Vaswani等人) |
bert_paper.pdf | BERT:深层双向变压器的预培训 |
gpt2_paper.pdf | 语言模型是无监督多任务学习者(GPT-2) |
rag_paper.pdf | 知识密集型任务的检索增强生成 |
ai_index_report_2024.pdf | AI指数报告——高效多模态LLMs调查 |
MCP工具
query_documents
询问有关索引PDF文档的问题。
输入:
{ "question": "What is the attention mechanism?" }输出:
{
"answer": "The attention mechanism allows the model to jointly attend to information from different representation subspaces at different positions. The Transformer uses multi-head attention, where queries, keys, and values are linearly projected h times, attention is applied in parallel, and the results are concatenated.",
"sources": [
{ "doc_name": "attention_is_all_you_need.pdf", "page": 3 },
{ "doc_name": "attention_is_all_you_need.pdf", "page": 5 }
],
"confidence": "high"
}list_documents
列出所有索引文档,包括页数和块数。
输出:
[
{ "doc_name": "ai_index_report_2024.pdf", "pages": 36, "chunks": 58 },
{ "doc_name": "attention_is_all_you_need.pdf", "pages": 15, "chunks": 20 },
{ "doc_name": "bert_paper.pdf", "pages": 16, "chunks": 31 },
{ "doc_name": "gpt2_paper.pdf", "pages": 24, "chunks": 43 },
{ "doc_name": "rag_paper.pdf", "pages": 19, "chunks": 29 }
]响应架构
| 字段 | 类型 | 描述 |
|---|---|---|
answer | string | 基于文档上下文的固定答案 |
sources | 阵列 | [{doc_name, page}] --贡献的每一个来源 |
confidence | 字符串 | "high" (直接陈述), "medium" (推断), "none" (未找到) |
交互示例
1.单文档问题
查询: *“什么是Transformer架构?”*
{
"answer": "The Transformer is a model architecture that eschews recurrence and instead relies entirely on an attention mechanism to draw global dependencies between input and output. It uses stacked self-attention and point-wise, fully connected layers for both the encoder and decoder.",
"sources": [
{ "doc_name": "attention_is_all_you_need.pdf", "page": 3 },
{ "doc_name": "attention_is_all_you_need.pdf", "page": 5 }
],
"confidence": "high"
}2.多文档问题
查询: *“BERT和GPT-2在训练前目标上有何不同?”*
{
"answer": "BERT uses a masked language model (MLM) objective, where random tokens are masked and the model predicts them using bidirectional context. GPT-2 uses a standard left-to-right language model objective, predicting the next token given all previous tokens. BERT is bidirectional while GPT-2 is unidirectional.",
"sources": [
{ "doc_name": "bert_paper.pdf", "page": 3 },
{ "doc_name": "bert_paper.pdf", "page": 13 },
{ "doc_name": "gpt2_paper.pdf", "page": 3 }
],
"confidence": "high"
}3.范围外问题
查询: *“谷歌的股价是多少?”*
{
"answer": "The answer to this question was not found in the indexed documents.",
"sources": [],
"confidence": "none"
}Claude桌面配置
将此添加到您的Claude桌面 claude_desktop_config.json:
{
"mcpServers": {
"document-qa": {
"command": "python3",
"args": ["mcp_server.py"],
"cwd": "/absolute/path/to/nexla-mcp-doc-qa"
}
}
}MCP检验员测试
# Install MCP Inspector
npx @modelcontextprotocol/inspector python3 mcp_server.py冷启动试验
# Delete the vector index and re-ingest from scratch
rm -rf data/chroma_db/
python3 ingest.py
python3 mcp_server.py技术栈
| 组件 | 技术 | 为什么 |
|---|---|---|
| MCP框架 | FastMCP 3.1.1 | Python,最小样板 |
| PDF解析 | PyMuPDF(fitz) | 快速,处理复杂布局 |
| 嵌入 | all-MiniLM-L6-v2 | 免费,本地,无API依赖关系 |
| 矢量存储 | ChromaDB | 嵌入式,持久到磁盘,零配置 |
| LLM | Gemini 2.5 Flash | 免费层,1M上下文窗口 |
设计决策
- API嵌入之上的本地嵌入: 切换自
text-embedding-004(已弃用)all-MiniLM-L6-v2.本地运行,零成本,无速率限制,嵌入模型与LLM完全解耦——Gemini只看到检索到的文本,从不看到向量。 - 单词级重叠组块: 具有50个单词重叠的500个单词块保留了块边界处的上下文。基于单词(而不是基于字符),以避免拆分中间单词。
- 误食: 重新
ingest.py如果ChromaDB集合已填充,则跳过。删除data/chroma_db/强制进行彻底的重新索引。 - 结构化JSON响应: 每个工具都返回带有源属性的可解析JSON,因此MCP客户端可以提供带有引用的答案。
Vibe编码——人工智能辅助开发过程
工具和设置
| 工具 | 角色 |
|---|---|
| 光标IDE | 主要开发环境 |
| 克劳德(人类学) | Cursor内的AI编码助手,用于代理模式 |
整个项目是使用Cursor的AI代理作为配对编程伙伴构建的。我做了 不 从ChatGPT复制粘贴或编写一次性提示。相反,我将人工智能视为初级工程师——我设置了架构,给出了分阶段的指令,审查了每一个输出,并在出现错误时进行了纠正。
______________________________________________________________________
我如何指导人工智能——我的激励策略
我做了 不 写一个巨大的提示,比如 *“给我建一个RAG系统。”* 这会产生平庸、不可测试的代码。相反,我使用了 分阶段、计划优先的方法:
第一步:在接触任何代码之前,我写了一个构建计划。
在生成一行代码之前,我创建了一个详细的计划文档(document_q&a_mcp_server.plan.md)其中规定:
- 确切的文件夹结构
- 技术栈,每种选择都有其基本原理
- 4个构建阶段,每个阶段都有具体的可交付成果
- 建筑流程图(美人鱼)
- 响应架构
- 要避免的陷阱(幂等摄入、速率限制、抗幻觉提示)
这个计划变成了“合同”——每个AI提示都引用了它。AI没有决定架构;我做到了。
第二步:逐步指示,而不是“构建一切”
我一次给人工智能一个阶段,并在进入下一个阶段之前验证每个阶段都有效:
| 阶段 | 我提示了什么 | 我评论了什么 |
|---|---|---|
| 第一阶段——脚手架 | *创建文件夹结构、config.py、requirements.txt、.gitignore、.env.example* | 已验证所有路径均正确解析,并已检查 config.py 集中每个常量 |
| 第2阶段——摄入 | *为摄取管道编写parser.py、chunker.py、embedder.py、pipeline.py* | 冉 python3 ingest.py,验证每个文档的块计数,检查ChromaDB是否已填充 |
| 第3阶段——核心+MCP | *使用query_documents和list_documents工具编写retriever.py、llm.py、server.py* | 通过MCP Inspector使用3种查询类型进行测试,检查JSON响应格式 |
| 第四阶段——波兰语 | *“编写README,包括架构、设置、工具文档、示例交互、Vibe编码部分”* | 阅读每一行,更正索赔,添加缺失部分 |
步骤3:每个阶段后都有明确的验证提示。
在每个阶段之后,我不仅继续前进,我还问: *“验证此阶段是否正常工作,错误处理是否正确,管道是否正确。”* 这捕获了真正的错误(记录如下)。
______________________________________________________________________
人工智能在哪里表现出色(我保留了它的产出)
- 清洁模块分离。 AI自然为每个文件模块生成一个函数(
parser.py,chunker.py等等),具有清晰的输入/输出。这种关注点分离正是我想要的——每个模块都是可独立测试的。
- 正确的库API。 PyMuPDF(
fitz.open(),page.get_text()),ChromaDB(PersistentClient,collection.add()),快速MCP(@mcp.tool())——人工智能知道这些API,并在第一次尝试时就生成了工作代码。
- 误食。 AI包括一个签到
embedder.py:如果ChromaDB集合已经有数据,则跳过重新嵌入。我没有要求这个;它主动添加了它。这可以防止意外的数据复制,这是一个真正的生产问题。
- 抗幻觉系统提示。 系统提示
llm.py有明确的规则: *“仅从提供的上下文中回答。如果没有找到,请诚实地说出来。”* 人工智能构建了置信水平(high/medium/none)JSON响应模式正确。
- Markdown代码围栏剥离。 Gemini有时会封装JSON ```
`json ...```阻碍。AI补充道_parse_response()` 在JSON解析之前剥离这些数据——这是一个微妙但重要的细节。
______________________________________________________________________
我在哪里超越或纠正了AI
以下是我在开发过程中所做的真实、具体的更正:
1.嵌入模型弃用是最大的支点
最初的计划使用了谷歌的 text-embedding-004 通过 google-generativeai 包裹。在第2阶段测试中,摄入崩溃:
models/text-embedding-004 is not found for API version v1beta人工智能的第一个修复是错误的——它试图更新模型名称前缀。真正的问题是 google-generativeai SDK已完全弃用。我推回:
- 第一轮: AI建议删除
models/前缀→ 仍然失败 - 第二轮: AI迁移到新
google-genaiSDK → 型号名称text-embedding-004也是日落 - 第三轮: AI尝试
gemini-embedding-001→ 仍然存在API问题 - 我的决定: 我问 *“如果我们使用句子转换器(全MiniLM-L6-v2)怎么办?它适用于双子座吗?”* 人工智能证实,嵌入模型和LLM在RAG中是解耦的。双子座只看到文本,从不看到向量。所以我把开关转到 100%本地嵌入这消除了API对整个摄入管道的依赖性、成本和速率限制。
课程: 人工智能无法诊断出未经训练的弃用。我必须确定根本原因(弃用的API,而不是代码错误),并提出架构中枢。
2.ChromaDB中的异常类型错误
AI写道 retriever.py 抓住 ValueError 当集合不存在时。测试表明ChromaDB实际上提高了 chromadb.errors.NotFoundError。我通过运行错误处理测试并将回溯反馈给AI来发现这一点,然后AI将其修复以捕获 Exception 带着一个 RuntimeError 重新提高。
3.阶段3中缺少错误路径
在第三阶段“完成”后,我明确地问: *“检查第3阶段的错误处理。”* AI发现了三个缺口:
llm.py:response.text可能是None如果Gemini的安全过滤器阻止了处理的响应llm.py:没有try/except在...周围generate_content()调用网络故障会使整个MCP服务器崩溃server.py:两个MCP工具都没有错误。包装异常会终止MCP连接,而不是返回JSON错误
这些是真正的错误,会导致生产失败。当我提示进行审查时,人工智能发现了它们,但在最初的实施中没有包括它们。
4.MCP检查器配置
AI给出了连接MCP检查器的错误指示。它最初建议手动输入代理令牌和配置,而正确的方法很简单:
npx @modelcontextprotocol/inspector python3 mcp_server.py我必须通过读取实际的MCP检查器输出并纠正连接方法来调试它。
5.我添加的Web UI,不在原计划中
最初的计划只要求安装MCP服务器。通过MCP Inspector进行测试后,我意识到视觉演示会更具吸引力。我指示AI构建一个FastAPI+HTML web UI,包括:
- 置信度计
- 源分布可视化
- 证据块查看器
- 使用实时索引上传PDF
人工智能构建了用户界面,但我驱动了每一个功能请求和用户体验决策。
______________________________________________________________________
什么不起作用
- 广泛的提示。 当我问 *“构建摄入管道”* AI生成了可工作但无错误路径的代码。它只在我特别要求时添加了适当的错误处理 *“验证错误处理是否正确。”* 除非你把它推到边缘情况,否则人工智能会为快乐的道路进行优化。
- API知识陈旧。 AI不知道
text-embedding-004已弃用或google-generativeai被替换为google-genai。我必须向它提供真实的错误消息和回溯,以便它进行修复。
- 配置多于解释。 在设置MCP检查员时,AI给出了技术上错误的分步说明。我必须调试实际的工具并纠正工作流程。
______________________________________________________________________
我的观点:正向部署工程工作流中的AI工具
人工智能是一种力量倍增器,而不是工程判断的替代品。
以下是我从构建这个项目中学到的:
- 建筑必须来自人类。 在任何AI生成的代码存在之前,我编写了计划,选择了技术栈,定义了模块边界,并决定了响应模式。AI填写了实现细节,但结构是我的。
- AI最擅长“已知模式”代码。 PDF解析、ChromaDB集成、FastMCP工具定义——这些都是AI很好处理的记录良好的模式。它节省了我阅读文档和编写样板的时间。
- 人工智能在“未知未知”漏洞方面表现最差。 弃用的API、微妙的异常类型不匹配、安全过滤器边缘情况——这些都需要真正的调试和真正的错误输出。人工智能无法预测它们;只有在我向它出示证据后,它才能修复它们。
- 逐步>一次全部。 一次性推动整个项目会产生一个脆弱的巨石。使用验证检查点分阶段构建可以及早发现错误并保持代码库的干净。
- 90/10法则适用。 人工智能生成了大约90%的工作代码。其他约10%的错误路径、API迁移、到本地嵌入的体系结构枢轴-花费了约50%的总开发时间,并且完全由人驱动。这10%才是真正的工程所在。
在一个前沿部署的角色中,我会像这样使用人工智能:首先计划,快速生成,无情地验证,并拥有每一个决定。速度增益是真实的,但质量标准仍然取决于工程师。
