Skip to main content
Glama
yinxianwei
by yinxianwei

weknora-mcp

极简 MCP 写入服务:让 Grok 把一个 URL 填进自定义连接器,就能把文档写进你的 WeKnora 知识库。

只有一个工具 write_wiki(title, content),只做写入,不做查询。


⚠️ 注意事项(部署前必读)

下面每一条都踩过坑或会直接导致服务不可用,请先看完再动手。

事项

说明

只能单实例

SQLite outbox 和后台 worker 都假设只有一个写者。不要加 --workers,也不要起第二个实例,否则会重复写入。

.env 靠当前目录定位

程序从工作目录读 .env。systemd 的 WorkingDirectory 绝不能省,否则会以「缺少 WEKNORA_API_KEY」启动失败。

仓库放 /home/root 下时

systemd unit 里的 ProtectHome 必须保持 no,否则服务读不到代码和 .env。放 /opt/srv 时不受影响。

ReadWritePaths 目录必须先存在

mkdir -p <repo>/data 再启动,否则 systemd 挂载失败、直接拒绝启动。

nginx 必须加 proxy_buffering off

否则 Streamable HTTP / SSE 会被缓冲,Grok 一直转圈。

token 会进 nginx access log

默认 log_format 记的是 $request,含 query string。建议改用 $uri

token 本身就在 URL 里

它会留在 Grok 连接器配置和浏览器历史中,务必用长随机串,泄露了就换一个。

服务只监听 127.0.0.1

别直接绑 0.0.0.0 又不填 MCP_ALLOWED_HOSTS,那等于把写入接口裸奔在公网。

data/ 里有你的文档原文

权限收紧到 700,不要提交、不要备份到公开位置。

只写不读

没有任何查询接口,Grok 无法确认写入结果。写入失败得自己看 /healthz 和 outbox CLI。

停止要留时间

停止时 worker 可能正在 POST,unit 的 TimeoutStopSec=30 是给它的收尾时间,改小了会中断正在写入的记录。

两个刻意的重复窗口

极端情况下可能重复写一条,这是换取一半代码量的取舍,见「状态与重试」。

Related MCP server: web2kb

为什么是「异步 + 本地队列」

Grok 对 MCP 工具调用有超时限制。一旦超时,它就会重发同一个调用,于是同一篇 文档在知识库里出现两遍 —— 这是最常见的投诉。

本服务的解法是:让工具调用路径上不存在任何网络请求

Grok ──write_wiki──> 写入本地 SQLite ──> 立刻返回 accepted(<10ms)
                          │
                          └──> 后台 worker ──> POST WeKnora
                                              成功 → done
                                              失败 → 每 30s 重推,20 次后 failed

由此得到两个保证:

问题

解法

Grok 超时重发导致重复

调用瞬间返回,不触发超时;再加 sha256(kb_id+title+content) 幂等键兜底

WeKnora 挂了文档丢失

先落本地 SQLite,worker 自动重推,进程重启后继续


快速开始

需要 Python 3.12+ 和 uv

uv sync                      # 装依赖(含 dev 组)
cp .env.example .env         # 然后填写 .env
uv run python -m app --check   # 配置自检,不启动
uv run python -m app           # 启动服务

.env 里三样必填:

WEKNORA_API_KEY=sk-...
WEKNORA_DEFAULT_KB_ID=kb_...          # 唯一的写入目标
MCP_TOKEN=$(openssl rand -hex 32)     # 至少 32 字符

启动成功后日志会打印:

监听 http://127.0.0.1:8765/mcp(Grok 填写 https://<域名>/mcp?token=<MCP_TOKEN>)

接入 Grok

在 Grok 的 grok.com/connectors → New Connector → Custom,填入:

https://你的域名/mcp?token=<MCP_TOKEN>

Grok 只支持 Streamable HTTP / SSE,所以必须经 HTTPS 暴露到公网。

本服务本身不监听公网,只监听 127.0.0.1:8765,由你的反向代理转发。

nginx 配置(本项目不自动安装,只给文档)

location /mcp {
    proxy_pass http://127.0.0.1:8765/mcp;
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Connection        "";

    # ↓↓↓ 这三行是 Streamable HTTP / SSE 的命门,漏了就一定出问题
    proxy_buffering off;
    proxy_cache     off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;

    chunked_transfer_encoding on;
    add_header X-Accel-Buffering no;
}

两个提醒(已汇总到顶部「注意事项」):proxy_buffering off 必须加, token 会进 nginx access log。

如果你把 MCP_ALLOWED_HOSTS=你的域名 填上,服务会启用 Host 白名单校验, 非白名单 Host 直接返回 421。留空则关闭该校验。

注意:不要把服务直接绑到 0.0.0.0 又不填 MCP_ALLOWED_HOSTS


部署:Linux + systemd

仓库里带了一份可直接用的 unit 文件:deploy/weknora-mcp.service

0. 前提

服务器上装好 Python 3.12+。

依赖由 uv 管理。venv 建好后就自包含了,运行期不需要 uv, 只在建环境 / 更新时用一次。

1. 准备目录(用默认用户,不新建账号)

不需要 useradd 直接用你要运行服务的那个用户就行,unit 里也不写 Group=, systemd 会自动使用该用户的默认主组。

mkdir -p /opt/weknora-mcp        # 换成你自己的部署目录

如果部署目录在 /root/home 下,就由对应的归属用户运行, unit 里的 User= 与该用户保持一致即可。

2. 放代码、建 venv

下面都假设你已经以「要运行服务的那个用户」登录。uv 会装到 ~/.local/bin, 所以要用登录 shell(bash -lc)才找得到它:

curl -LsSf https://astral.sh/uv/install.sh | sh

git clone <你的仓库地址> /opt/weknora-mcp
cd /opt/weknora-mcp
uv sync

换过部署目录的话,unit 文件顶部写明了哪 5 处要一起改,并给了 sed 命令。

3. 写 .env、建 data 目录

cp .env.example .env
chmod 600 .env
vi .env                          # 填 API Key / KB ID / MCP_TOKEN

mkdir -p data
chmod 700 data                   # 里面有你的文档原文,别对别人开放

uv run python -m app --check     # 配置自检,通过后再继续

不需要配 EnvironmentFile= .env 由程序自己从 WorkingDirectory 读取, 这就是「配置放在项目根目录」的含义。所以 .env 必须可被服务用户读到, 且 data/ 必须可写(这个目录必须先存在,否则 unit 的 ReadWritePaths 会让 systemd 拒绝启动)。

4. 安装并启动

sudo cp deploy/weknora-mcp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now weknora-mcp

5. 查看状态与日志

systemctl status weknora-mcp
journalctl -u weknora-mcp -f                    # 实时日志
journalctl -u weknora-mcp -n 100 --no-pager     # 最近 100 行

如果 ExecStartPre 的配置自检失败,服务会直接进入 failed 而不会带病运行, 具体原因看 journalctl -u weknora-mcp -n 50

6. 更新代码

cd /opt/weknora-mcp
git pull
uv sync
sudo systemctl restart weknora-mcp

只改 .env 的话不用 systemctl daemon-reload,直接 restart 即可 (只有改 unit 文件本身才需要 reload)。


调用行为(给模型看的语义)

write_wiki 的返回永远是「已受理」,不是「已写入」:

{
  "ok": true,
  "status": "accepted",
  "task_id": "w-20260912-7f3a1c",
  "title": "会议纪要",
  "characters": 1234,
  "message": "已受理并排队写入知识库,请勿重复调用本工具。"
}

重复调用同一份内容时:

{
  "ok": true,
  "status": "accepted",
  "task_id": "w-20260912-7f3a1c",
  "deduplicated": true,
  "message": "同一内容已受理过,本次未重复入队,请勿再次调用本工具。"
}

参数非法是唯一会返回 ok: false 的情况:

{ "ok": false, "status": "rejected", "error": "title_required", "message": "title 不能为空" }

ok: true 只表示「传输层收到了」,业务状态永远放在 status 字段里。


状态与重试

状态

含义

会重推吗

pending

已入队,等待推送

✅ 每 30s

done

POST 成功(2xx)

failed

连续失败 20 次(≈10 分钟)

❌ 永久终止,需人工

两个刻意接受的重复窗口(换取一半的代码量):

  • POST 成功后、写 done 之前进程崩溃 → 重启后重推,可能重复一条

  • POST 超时但 WeKnora 其实已收到 → 重推,可能重复一条

如果实际中真的遇到,再补「重推前按标题查一次」即可。


运维:outbox CLI

乐观确认的代价是失败会静默,所以必须有地方看。这些不是 MCP 工具,Grok 调不到。

uv run python -m app outbox list                   # 概览
uv run python -m app outbox list --status failed   # 只看失败的
uv run python -m app outbox show <task_id>         # 看完整内容与错误
uv run python -m app outbox retry <task_id>        # 重置为 pending,让 worker 重推

健康检查(免鉴权):

curl http://127.0.0.1:8765/healthz
# {"status":"ok","pending":0,"done":12,"failed":1,...}

failed > 0status 会变成 degraded,可直接接监控告警。


配置项

变量

默认值

说明

WEKNORA_BASE_URL

https://weknora.weixin.qq.com/api/v1

WeKnora API 地址

WEKNORA_API_KEY

必填

WeKnora 凭证

WEKNORA_DEFAULT_KB_ID

必填

唯一目标知识库

MCP_HOST / MCP_PORT

127.0.0.1 / 8765

监听地址

MCP_PATH

/mcp

MCP 端点路径

MCP_TOKEN

必填

URL token,≥32 字符

MCP_DB_PATH

./data/weknora-writes.db

outbox 数据库

MCP_JSON_RESPONSE

true

POST 回 JSON(推荐)而非 SSE 流

MCP_ALLOWED_HOSTS

逗号分隔的 Host 白名单;空=关闭校验

MCP_POST_TIMEOUT

10

单次推送超时(秒)

MCP_RETRY_INTERVAL

30

重推间隔(秒)

MCP_MAX_ATTEMPTS

20

连续失败上限(≈10 分钟)

MCP_MAX_CONTENT_CHARS

100000

正文长度上限

MCP_LOG_LEVEL

info

日志级别

必须单进程运行。 SQLite outbox 与 worker 都假设只有一个写者, 不要用 --workers 2 或同时起多个实例。


开发

uv sync                          # 装依赖(含 dev 组的 pytest / ruff)
uv run ruff check .              # 静态检查
uv run ruff check --fix .        # 自动修复(改完务必重跑测试)
uv run pytest                    # 54 个测试
uv run python -m app --check     # 配置自检

tests/test_server.py 会真的起一个 HTTP 服务,走完整的 MCP 握手, 并覆盖三个最容易翻车的点:nginx 反代的 Host 头、token 鉴权、幂等去重。 tests/test_weknora.pyMockTransport 盯住发往 WeKnora 的请求形状 (尤其是 X-API-Key 这个头,写错会得到 401)。

防泄漏钩子

仓库带了一个 pre-commit 钩子,拦住 .env、密钥样式和大文件。 它在 .git/ 里不会被版本控制,所以要用仓库内这份:

git config core.hooksPath .githooks

注意:core.hooksPath 是本地配置,重新 clone 或删掉 .git 后需要重新设一次。


目录结构

app/              服务代码(7 个模块)
tests/            单元测试 + 真实 HTTP 的 MCP 端到端测试
deploy/           systemd unit 文件(文档用途,不自动安装)
.githooks/        pre-commit 防泄漏钩子
pyproject.toml    依赖、打包与 ruff 配置
uv.lock           锁定版本
README.md
LICENSE           MIT

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to fetch webpages, convert them to Markdown, index into SQLite FTS5, and query the knowledge base through MCP tools.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides structured search, schema-validated writes, and linting for a markdown knowledge base, enabling agents to operate the wiki over a single streamable-HTTP MCP endpoint.
    1
    -
  • F
    license
    A
    quality
    B
    maintenance
    Provides a lightweight personal knowledge base MCP server that compiles raw materials into interconnected Wiki pages, with hybrid search (BM25, optional vector, link expansion) and tools for querying, reading, writing, and ingesting content.
    5
    -