本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/plugins/mcp_server/说明.md。
MCP 服务(mcp_server)插件说明
版本:1.3.0 | 作者:FlintHub | 状态:⏸ 未启用(
activated: false)功能:把 FlintHub 变成 MCP Server,让本机 AI Agent(Claude Desktop / Cursor / Trae 等)
以用户本人账号读写论坛——Token 鉴权、只读/可写分级、滚动 24h 发帖额度、写操作幂等、
全链路审计、与插件生态(审核 / 前置标签 / 积分 / 通知 / 搜索)联动。
一、功能简介
1.1 传输与协议
- 端点:
POST {BASE_PATH}/mcp,无状态 Streamable HTTP,JSON-RPC 2.0(单条 + 批量) - 协议基线
2025-06-18,兼容2025-03-26/2025-11-25(initialize时宽容协商) - 鉴权:
Authorization: Bearer fh_mcp_xxx(绝不使用 CSRF——MCP 端点无会话) - 非 POST 一律
405+Allow: POST;Origin做 DNS rebinding 白名单校验
1.2 工具清单(16 个)
只读(9)
| 工具 | 作用 |
|---|---|
list_categories | 当前账号可浏览版块(含 can_post),发帖前取 category_id |
search_threads | 关键词搜索(可限定版块 / 条数) |
get_thread | 主题详情(正文超 6000 字符截断) |
list_replies | 按主题分页读回帖 |
get_user | 用户公开资料(绝不含邮箱 / 密码 / 后台字段) |
get_me | 当前 Token 绑定身份 + Token 信息 + 额度(不需要知道自己的 user_id) |
get_quota | 滚动 24h 已用 / 剩余 / 下次重置时间 |
list_threads | 按创建时间倒序增量巡查(category_id / since / cursor / limit) |
get_vote_state | 点赞数 + 自己是否已赞 |
可写(7,需 allow_ai_post=1 且 Token scope=write)
| 工具 | 作用 | 归属校验 |
|---|---|---|
create_thread | 发主题(走滚动 24h 额度) | — |
reply_thread | 回帖 | — |
edit_post / delete_post | 改 / 软删回复 | 仅本人 |
edit_thread / delete_thread | 改 / 软删主题 | 仅本人 |
vote | 点赞 / 取消赞(1 = 赞,0 = 取消;本站不支持点踩) | — |
1.3 结构化错误码
失败结果同时保留旧契约(isError: true + 中文 content[0].text)与新增结构化字段:
result.structuredContent = { "code": "QUOTA_EXCEEDED", "retry_after": 6399, "data": … }协议级错误放在 error.data = { code, retry_after }。业务码共 13 个:
UNAUTHORIZED FORBIDDEN NOT_FOUND INVALID_ARGUMENT RATE_LIMITED COOLDOWN
QUOTA_EXCEEDED IDEMPOTENCY_CONFLICT IDEMPOTENCY_IN_PROGRESS WRITE_DISABLED
BANNED DISABLED INTERNAL。
1.4 写操作守卫(顺序固定)
服务开关 → Token 有效 → 账号 active → 未封禁 → scope 足够 → allow_ai_post
→ 每分钟限流 → 同工具冷静期 → 幂等键 → 滚动 24h 额度(仅 create_thread)→ 执行
1.5 幂等重放
写工具都接受可选 client_token(8~128 字符,[A-Za-z0-9._:-]):
| 情况 | 行为 |
|---|---|
| 首次 | 建 pending 行,正常执行 |
| 同键 + 同参 + 已成功 | 直接回放首次结果,不再执行 |
| 同键 + 不同参 | IDEMPOTENCY_CONFLICT |
| 同键 + 首次仍在执行 | IDEMPOTENCY_IN_PROGRESS + retry_after |
pending 超过 300s | 视为崩溃残留,可被接管(条件更新,只有一个能接管成功) |
⚠️ 不承诺跨进程强原子性:进程若在「业务写库成功」与「写 done」之间被杀,TTL 过后重试理论上仍可能重复一次;单进程 / 正常重试不会重复。
1.6 额度语义(滚动 24 小时「成功」发帖数)
- 不是固定日窗,不是尝试计数。窗口 = 现在往前 24 小时。
- 账本三态:
reserve()预占 → 成功commit()/ 失败release()。只有committed计入额度。 - 预占 TTL 300s:进程崩溃后自动过期,不会永久吃掉一个名额。
- 并发:预占 + 计数 + 判定在
BEGIN IMMEDIATE内完成,两个请求不会同时抢到最后一个名额。
1.7 审计
mcp_logs 一次调用写两行(start → finish),或一次性事件一行。记录 request_id / event /
status / code / retry_after / duration_ms / scope,覆盖:工具调用、认证失败、参数错误、
未知工具、限流拒绝、令牌创建 / 本人撤销 / 管理员撤销、协议错误。
写库前强制脱敏(不依赖调用方自觉):
- 任何形如
fh_mcp_xxxx的串 →fh_mcp_*** - 正文类字段(
content/body/text/html/markdown)→[len=N] - 二进制类字段(
image/image_base64/base64/data/file/avatar)→[blob=NB]
后台「调用审计」支持按 令牌 / 用户 ID / 事件类型 三条件筛选,翻页自动带上筛选条件。
1.8 令牌
- 形态
fh_mcp_<base64url(32B)>,库内只存sha256,明文仅创建时返回一次 - 软撤销:
enabled=0+revoked_at+revoked_by(0 = 本人,>0 = 管理员 uid),行保留
→ 历史日志里的 token_id 永远查得到名字,也能回答「谁在什么时候撤了哪个令牌」
- 单用户有效令牌上限 20 个;已撤销的不占名额
二、请求流程
POST /mcp
└─ McpServer::handle()
├─ 服务开关关 → 503(audit: DISABLED)
├─ Origin 白名单 → 403
├─ Bearer 校验失败 → 401(audit: auth_fail)
├─ JSON 解析 / 协议版本 / method 分发
└─ tools/call
├─ 工具存在?→ 否则 tool_not_found
├─ 参数校验 → 否则 param_error
├─ 写工具?→ 写守卫链(限流 / 冷静期 / 幂等)
├─ Audit::start() ← 先落一行 status=started(进程被杀也留痕)
├─ Tools::xxx() ← 全部经核心 Model/Helper
├─ Idempotency::commit() / abort()
└─ Audit::finish() → 补 status / code / duration_ms / result核心写入路径复用(不自造 SQL):
Thread::insert/update/softDelete、Post::insert/update/softDelete、Vote::toggle/getCounts、
Tag::saveForThread、Search::indexThread/removeFromIndex、User::increment/decrementPostCount、
Points::award、Notification::notifyReplyAggregated/notifyQuote/notifyVote,
并触发 thread_create_after / post_create_after / post_edit_after / thread_edit_after / vote_after 钩子。
三、路由与接口
3.1 前台(hook/route_register.php)
| 方法 | 路径 | 说明 | 登录 | CSRF |
|---|---|---|---|---|
| ANY | /mcp | MCP JSON-RPC 端点(非 POST → 405) | Bearer | 不需要 |
| GET | /mcp-token | 我的令牌页 | 需要 | — |
| POST | /mcp-token/create | 新建令牌 | 需要 | 需要 |
| POST | /mcp-token/revoke | 撤销令牌 | 需要 | 需要 |
3.2 后台(hook/admin_route_register.php,路径不写 /admin 前缀)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/mcp-server | 三 Tab 单页(`?tab=settings\ |
| POST | /admin/mcp-server/save | 保存设置(302 PRG) |
| POST | /admin/mcp-server/revoke | 管理员强制撤销任意令牌(302 PRG) |
门禁来自继承 app\Controllers\Admin\BaseController → 构造函数完成「管理员 + 二次验证」双重校验。
四、后台可配项
存 mcp_config 表(键值对,非核心 settings),默认值由 Plugin::configDefaults() 兜底:
| 键 | 默认 | 范围 | 说明 |
|---|---|---|---|
enabled | 0(关) | 0 / 1 | 总开关;关闭后 /mcp 返回 503,所有令牌立即失效 |
max_scope | write | read / write | 设为 read 时任何令牌都只能只读 |
allow_ai_post | 0(关) | 0 / 1 | 写操作总开关 |
review_ai_post | 1 | 0 / 1 | MCP 写入是否按站点审核策略处理 |
mark_mcp | 1 | 0 / 1 | 是否显示「由 MCP 发布」徽章(纯机器人图标,悬停提示「由 MCP 发布」) |
rate_write | 5 | 0 ~ 1000 | 写操作限流(次/分钟/令牌),0 = 不限 |
daily_quota | 10 | 0 ~ 1000 | 新建令牌的默认额度,0 = 不限 |
cooldown | 30 | 0 ~ 3600 | 同令牌同工具最短间隔(秒) |
audit_days | 90 | 1 ~ 3650 | 审计 / 账本 / 幂等记录保留天数 |
五、数据表
独立库:plugins/mcp_server/data/mcp_server.sqlite(WAL,busy_timeout=5000)
| 表 | 用途 |
|---|---|
mcp_config | 配置键值对 |
mcp_tokens | 令牌(token_hash / scope / enabled / daily_quota / expires_at / revoked_at / revoked_by) |
mcp_logs | 审计(+ request_id / event / status / code / retry_after / duration_ms / scope) |
mcp_rate | 每分钟限流 + 冷静期计数(固定窗口 UPSERT) |
mcp_post_marks | 「由 MCP 发布」标记(kind + ref_id 唯一) |
mcp_quota | 1.1.0 新增 发帖额度账本(state = reserved / committed) |
mcp_idempotency | 1.1.0 新增 幂等键(唯一索引 (token_id, tool, idem_key)) |
mcp_schema | 1.1.0 新增 schema 版本(单行表,id 恒为 1) |
| 索引 | 列 |
|---|---|
idx_mcp_tokens_user | mcp_tokens(user_id) |
idx_mcp_logs_created / idx_mcp_logs_token | mcp_logs(created_at) / (token_id, created_at) |
idx_mcp_logs_event / idx_mcp_logs_request / idx_mcp_logs_user | (event, created_at) / (request_id) / (user_id, created_at) |
uq_mcp_rate | mcp_rate(token_id, action, window_at) 唯一 |
uq_mcp_marks | mcp_post_marks(kind, ref_id) 唯一 |
idx_mcp_quota_count / idx_mcp_quota_ref | (token_id, action, state, created_at) / (token_id, action, ref_id) |
uq_mcp_idem / idx_mcp_idem_created | (token_id, tool, idem_key) 唯一 / (created_at) |
六、数据库迁移(1.1.0 起版本化)
- 唯一权威版本标记 = 库内
mcp_schema.version(不是文件)。旧版data/.mcp_table_cache布尔门控已废弃,升级成功后自动删除。 - 迁移文件:
migrations/1.1.0.sql(schema v1 → v2,19 条语句),唯一 DDL 事实源。 Migrator::migrate()流程:Plugin::ddl()基线(CREATE TABLE IF NOT EXISTS)→ 逐版本applyStep()。applyStep():BEGIN IMMEDIATE→ 事务内重读版本(等锁期间可能已被另一请求升完)→ 逐语句执行 → 写版本 →COMMIT。中途失败 = 版本不变 + 全量回滚。ALTER TABLE ... ADD COLUMN前用PRAGMA table_info探列,已存在则跳过 → 手工补跑 / 并发补跑都安全。- 每请求最多查一次版本(
static $checked守卫),已是最新则 0 条 DDL。 - 改 schema 必须同时改两处:
Plugin::SCHEMA_VERSION与Migrator::STEPS。只改一处会被validate()抛异常拦下,不会静默半迁移。
七、目录结构
plugins/mcp_server/
├── plugin.json # 6 钩子 / min_core 1.1.19 / permissions: route:admin
├── Plugin.php # 主类:独立库 / 配置 / 用户装载 / 封禁判定 / 请求 id / withUser
├── McpServer.php # JSON-RPC 入口:分发 / 写守卫 / 幂等 / 结构化出参
├── Transport.php # 响应输出(JSON-RPC error 支持 error.data)
├── ToolRegistry.php # 16 个工具的 schema 与描述(tools/list 的唯一来源)
├── Tools/ReadTools.php # 9 个只读工具
├── Tools/WriteTools.php # 7 个可写工具
├── TokenManager.php # 令牌生成 / 校验 / 软撤销 / 限流 / 冷静期 / effectiveScope
├── Quota.php # 滚动 24h 额度账本(reserve / commit / release / status)
├── Idempotency.php # 幂等键(begin / commit / abort / hashArgs / validateKey)
├── Audit.php # 审计写入 + 脱敏 + 后台筛选 + 过期清理
├── ErrorCode.php # 13 个稳定业务码 + fail() / ok() / normalize()
├── Migrator.php # 版本化迁移器(mcp_schema.version 门控)
├── migrations/1.1.0.sql # v1 → v2 DDL(唯一事实源)
├── FrontController.php # 前台令牌页 / 创建 / 撤销(+ 审计)
├── AdminController.php # 后台三 Tab(+ 审计筛选 / 用户名装载 / 撤销审计)
├── hook/
│ ├── init_after.php # Migrator::migrate()(已是最新 → 0 条 DDL)
│ ├── route_register.php # /mcp + /mcp-token*
│ ├── admin_route_register.php # 3 条后台路由
│ ├── layout_head_end.php # 注入 assets/style.css
│ └── post_mark_badge.php # 「由 MCP 发布」徽章(机器人图标;主帖/回帖同位置触发)
├── views/ token.php · admin.php
├── assets/ style.css · script.js
├── lang/ zh.php · en.php · zh_tw.php
└── data/ mcp_server.sqlite八、注意事项
- 目录名含下划线(
mcp_server)→Plugin.php末尾必须保留
class_alias(Plugin::class, 'Plugin\mcp_server\Plugin'),否则核心按目录名硬拼的类名找不到,
activate() / uninstall() 被静默跳过。
- 改
Plugin::ddl()对老库无效。CREATE TABLE IF NOT EXISTS对已存在的表完全不生效 →
新增列/索引一律写进 migrations/<版本>.sql,并同步 +1 Plugin::SCHEMA_VERSION 与 Migrator::STEPS。
list_threads依赖核心Thread::getIndexByCreateCursor()(核心 1.1.25 起提供)。
⚠️ 1.2.0 起有显式能力探测:ReadTools::supportsIndexCreateCursor() 用 method_exists()
判定(不用版本号字符串比较 —— 版本号可能被手工改过、站点也可能跑二次分发的核心)。
方法缺失时 list_threads 返回 code=CORE_TOO_OLD 并提示「升级核心或改用 search_threads」,
不再笼统报 INTERNAL。与 INTERNAL 分开的理由:INTERNAL 意味着「重试可能有用」,
而 CORE_TOO_OLD 意味着「重试一万次也没用,只能升级核心」—— 混在一起会让调用方白重试、
站长在审计日志里也查不出根因。
⚠️ 插件不得直接查 topic_index —— 分片、权限、软删口径全在核心模型里。
getIndexByCreateCursor()与getIndexCursor()语义不同,不可互换:
后者混入 is_pinned / last_reply_time 排序,用于增量巡查会漏帖。
且空白名单返回 [](与 getIndexCursor 的「空 = 不限」故意相反,防权限泄漏)。
- 额度只认
committed。release()只在写库失败时调;成功路径必须commit(),
否则名额会随 reserved 的 TTL 过期而"莫名归还",额度形同虚设。
- 撤销是软撤销。
verify()只认enabled = 1,软撤销立即失效;
listForUser() / countForUser() 都带 enabled = 1,已撤销的不占 20 个名额。
- 审计脱敏在写入层(
Audit::flatten/scrub/redact),不依赖调用方自觉。
新增参数时不要绕过 Audit::start/event 直接拼 SQL,否则正文与令牌明文会进库。
mcp_post_marks的markedPostIds()是请求级缓存,整页只查 1 次(满足单插件每请求查询上限)。Plugin::withUser()用于只认$_SESSION['user_id']的核心 Helper(Vote::toggle/getCounts、Points)。
finally 里严格还原原会话 —— MCP 端点正常无会话,还原后必须回到「无 user_id」状态。
- 后台弹层/表单 markup 必须留在
<main class="admin-content">内,放到外面 = 无 UI、零日志。 - **CSS 只用既有
--mn-*变量。--mn-tag-success-bg/--mn-tag-danger-bg/--mn-bg-subtle不存在**,
状态色请用 --mn-alert-success-bg/text、--mn-comp-tag-danger-bg/text、--mn-bg-tag。
- 改前端静态资源必须 bump
plugin.json的version:?v=filemtime是唯一破缓存手段。
九、当前状态
| 项 | 值 |
|---|---|
| 启用状态 | ⏸ activated: false(未启用,isActivated()=false) |
| 插件库 schema 版本 | mcp_schema.version = 2(已迁移) |
| 工具数 | 16(只读 9 + 可写 7) |
| 钩子 | 6 个 |
| 路由 | 前台 4 条 + 后台 3 条 |
| 语言键 | zh / en / zh_tw 三份,键集合与键序一致 |
min_core | 1.1.19(安装门槛,不是功能门槛 —— 见下一行) |
| 核心能力依赖 | Thread::getIndexByCreateCursor() 需核心 ≥ 1.1.25;低于此版本不拦安装,改为运行期降级:list_threads 返回 CORE_TOO_OLD,其余工具全部正常 |
回归套件(.workbuddy-ai/tmp/,改动本插件后必跑)
| 脚本 | 覆盖 | 最近结果 |
|---|---|---|
thread_create_cursor_probe.php | 核心 getIndexByCreateCursor:空白名单 / 排序 / 游标不重不漏 / since / 版块 / 软删 / has_more | PASS 19 / FAIL 0 |
mcp_migration_ddl_probe.php | 临时库跑 v1→v2:19 条语句 / 结构 / 索引 / 存量默认值 / 幂等重跑 / 额度口径 | PASS 40 / FAIL 0 |
mcp_1_1_0_smoke.php | Phase 1(真实库,只写插件库且精确清理):迁移 / 审计脱敏 / 额度 / 幂等 / 9 个只读工具 | PASS 89 / FAIL 0 |
mcp_1_1_0_write_smoke.ps1 | Phase 2(数据沙箱):7 个写工具 + 额度在真实发帖链路 + 沙箱隔离校验 | PASS 45 / FAIL 0 |
已知取舍(非缺陷)
- 不支持博客创建、管理员二次验证相关管理操作(留到后续版本)。
- 不支持点踩:
vote只有1= 点赞 /0= 取消。 - 不在 JSON-RPC 里做图片上传(避免 Base64 撑爆上下文与审计库)。
- 不自造批量写入工具(批量 = 客户端多次调用 +
client_token保证不重复)。 - 幂等不承诺跨进程强原子性(见 §1.5)。
更新记录
| 版本 | 要点 |
|---|---|
| 1.3.0 | 「由 MCP 发布」徽章改为机器人图标(FA robot,随主题着色,悬停/读屏仍为「由 MCP 发布」);回帖徽章位置从顶部用户栏移到内容下方操作栏右侧,与主帖对称(回帖改挂 post_report_button)。无 DB / 接口变更。 |
| 1.2.0 | list_threads 增加核心能力探测:核心 < 1.1.25 时返回 CORE_TOO_OLD(不再笼统 INTERNAL),initialize 的 instructions 同步说明 |
| 1.1.0 | 版本化数据库迁移(mcp_schema.version)、写操作守卫、幂等重放、滚动 24h 额度、全链路审计、16 个工具 |