本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/mcp_server插件开发方案_2026-10-11.md。
mcp_server 插件开发方案(让 FlintHub 成为 MCP Server)
文档性质:评估稿(只做方案,不写代码)
目标读者:站长 / 项目评审
依据:
docs/插件开发规范.md(权威规范)、app/Helpers/Plugin.php、app/Core/Router.php、app/Models/*、现有插件实现(ai_assistant/forum_required_tag/content_review/mod_system/user_level)、MCP 官方规范原始输入:站长提供的《MCP Server 插件规划(供 Agent 参考)》
版本:v1(2026-10-11)
0. 一句话结论
可行,且比原规划更简单 — 但必须改两处关键设计。
- 传输必须做成「无状态 Streamable HTTP + 纯 JSON 响应」,不启用 SSE / 长连接 / 会话。这样插件在普通 php-fpm 请求模型下即可跑通,不需要 Swoole / ReactPHP / 常驻进程。(MCP 规范明确允许服务器用
application/json返回单对象而不开 SSE。) - 「动态 Schema」不能直接去读
forum_required_tag的库或类——那违反《插件开发规范》「插件间禁止直耦」。正确做法是新增一个自定义钩子契约mcp_tool_schema,由关心规则的一方(如forum_required_tag)自己订阅、自己上报规则;无人订阅时工具走「最小 schema + 可读错误」。
除此之外,原规划的判断(插件形态、Token 体系、走 Model 层、不做管理操作)全部成立,可以直接沿用。
建议分期:P0(Token + 只读工具)→ P1(发帖 / 回帖)→ P2(生态联动)→ P3(审计 / 限流 / 后台)→ P4(点赞 / 收藏等高级工具)。
1. 前提验证(先答「能不能插件全包」)
1.1 结论
能。 不需要动核心:/mcp 走 route_register 注册;Token 存插件独立 SQLite;写操作全部落到核心 Model 层。
1.2 证据链(逐条可复核)
| # | 问题 | 结论 | 证据 |
|---|---|---|---|
| 1 | 插件能注册根路径路由吗 | 能 | app/Core/Router.php 提供 get/post/any/group;oidc_provider 已成功注册站点根 /.well-known/openid-configuration |
| 2 | 插件能做独立数据库吗 | 能 | Plugin::db($name) → plugins/{name}/data/{name}.sqlite(app/Helpers/Plugin.php:113-141);Plugin::ensureSchema($name,$ddl) 幂等建表(同文件 :150-153) |
| 3 | 插件私有库会被 Web 直接下载吗 | 不会 | Plugin::db() 会自动补写 data/.htaccess + data/web.config 防护(app/Helpers/Plugin.php:118-139) |
| 4 | 插件能发起自定义钩子吗 | 能(关键) | Plugin::hook(string $hookName, array $params = []) 是 public static,按 plugin.json 的 hooks 键任意名分发(app/Helpers/Plugin.php:673-728)。→ 可用来做去耦的跨插件契约 |
| 5 | 能不能以「某用户」身份发帖/回帖 | 能 | Thread::insert()(app/Models/Thread.php:54-180)、Post::insert()(app/Models/Post.php:52-172)。ai_assistant 正是这样以「AI 会员(绑定真实用户)」身份发帖:publishReply() → Post::insert() + Points::award() |
| 6 | 核心有没有现成的 API/Token 鉴权 | 没有 | 全库搜 Authorization / Bearer / api_token / access_token,app/ 下零命中。→ Token 体系必须插件自建 |
| 7 | 现有插件生态里有没有同构先例 | 有 | plugins/ai_assistant/:非真人身份写帖 + 敏感词过滤 + 审核模式 + 积分 + 日志,几乎就是 mcp_server 的「同步版」 |
1.3 由此产生的三条硬边界(必须写进插件自检)
- 不碰核心:
/mcp只注册路由;不修改任何app/下文件;不新增核心钩子名(用自定义钩子名做扩展点)。 - 不自己写业务表:所有帖子/回复必须经
Thread::insert()/Post::insert(),禁止插件直写核心业务库(否则插件生态的钩子全部不会触发)。 - 不给
/mcp加 CSRF:第三方 Agent 拿不到 CSRF token;鉴权只能靠Authorization: Bearer <token>。(与oidc_provider的 token 端点同理——那处也明确不调Csrf::verifyOrDie()。)
2. 总体设计
2.1 定位与边界
| 内容 | |
|---|---|
| 做什么 | 让本机 Agent(Claude Desktop / Cursor / Trae 等)以「某个已登录用户」的身份,读(列版块 / 搜帖 / 看帖 / 看回帖 / 看用户公开资料)与写(发主题 / 回帖 / 编辑自己的帖 / 软删自己的帖) |
| 不做 | ① 管理操作(删别人帖、封号、改版块)② 私信 ③ 后台数据 ④ 隐藏版块 / 隐藏内容 ⑤ 越权(Token 权限 ≤ 绑定用户权限) |
| 形态 | 独立插件,可单独启用/禁用 |
| 依赖 | 无硬依赖;对 forum_required_tag / content_review / mod_system / user_level / points_mall 一律软依赖 + 优雅降级 |
2.2 命名与目录硬约束(含必须先踩平的坑)
- 插件目录名:
mcp_server(全小写 + 下划线,符合/^[a-z0-9_]+$/)。 - ⚠️ 目录名含下划线 → 核心生命周期类名推导会失败(规范 §4.1 /
docs/插件开发规范.md:350-390)→ 必须在Plugin.php末尾加class_alias: - 命名空间统一写
Plugin\McpServer(PascalCase 段) class_alias(\Plugin\McpServer\Plugin::class, '\Plugin\mcp_server\Plugin')- 这条与
oidc_provider/oidc_client完全同款,已是本库既定写法。 - ⚠️ 路由前缀不能与真实目录同名(规范 §11.34b):
/mcp站点根无同名目录,安全。
2.3 路由规划表
| 方法 | 路径 | 用途 | 鉴权 |
|---|---|---|---|
| POST | /mcp | MCP 唯一端点(JSON-RPC 2.0;initialize / tools/list / tools/call / ping) | Authorization: Bearer <mcp_token>(initialize 亦要求,避免匿名探测) |
| GET | /mcp | 规范要求端点支持 GET;返回 405(不提供服务器→客户端 SSE 流) | — |
| GET | /mcp-token | 前台:Token 管理页(生成 / 查看 / 撤销) | 登录态(Session) |
| POST | /mcp-token/create | 前台:生成 Token | 登录态 + CSRF |
| POST | /mcp-token/revoke | 前台:撤销 Token | 登录态 + CSRF |
| — | /admin/mcp-server | 后台:总开关 / 权限分级 / 限流 / 审核策略 / 审计日志 | 管理员(admin_route_register,需声明 route:admin) |
⚠️ 注册顺序:
/mcp-token/*必须早于任何/mcp-token通配;/mcp与/mcp-token不冲突(精确匹配优先)。
3. 协议与传输设计(本方案的技术核心)
3.1 协议版本策略
| 版本 | 状态 | 结论 |
|---|---|---|
| 2025-03-26 | 引入 Streamable HTTP | 兼容下限 |
| 2025-06-18 | 客户端覆盖最好 | 本项目实现基线 |
| 2025-11-25 | 官方 Versioning 页标注 current | 握手时代,兼容即可 |
| 2026-07-28 | 破坏性大改:删掉 initialize 握手与 Mcp-Session-Id、删 GET SSE、新增 server/discover | 暂不作为唯一实现(客户端采纳未普及) |
落地口径:
initialize时若客户端protocolVersion在此表内 → 原样回;否则回我们支持的最新版本(规范要求服务器必须返回一个自己支持的版本)。- 容忍客户端跳过 initialize 直接
tools/list(无状态服务器的自然行为),不做「必须先握手」的强校验。
3.2 传输形态:无状态 Streamable HTTP
| 项 | 设计 | 依据 |
|---|---|---|
| 端点 | 单端点 POST /mcp | 规范要求单 MCP endpoint |
Accept | 要求同时含 application/json 与 text/event-stream(客户端会带);服务器只回 application/json | 规范允许二选一 |
| 响应 | 单个 JSON-RPC 对象,Content-Type: application/json | 不开 SSE → 不需要常驻进程 |
| 会话 | 不分配 Mcp-Session-Id,每请求独立 | 无状态,php-fpm 友好 |
| 通知 | notifications/initialized 等 → 202 Accepted,无 body | 规范要求 |
| GET | 405 | 规范允许 |
MCP-Protocol-Version 头 | 读取并宽容处理:缺失按 2025-03-26 视作默认;非法/不支持才回 400 | 规范要求 |
Origin | 必须校验(缺失或非白名单 → 403),防 DNS rebinding | 规范 MUST |
⚠️ 这是本方案能否落地的前提。若要做 SSE / 服务器推送,就得上 Swoole/ReactPHP 常驻进程——本期不做。
3.3 JSON-RPC 方法最小集(只做 tools)
| 方法 | 处理 |
|---|---|
initialize | 回 protocolVersion + capabilities.tools(不做 listChanged)+ serverInfo{name,version} + instructions(可放一句「本站 Token 权限说明」) |
notifications/initialized | 202 空响应 |
ping | 空 result |
tools/list | 返回按 Token 权限过滤后的工具数组(nextCursor 省略,工具数量少,不做分页) |
tools/call | params.name + params.arguments → 执行 → result.content[] + result.isError |
错误口径(重要,直接决定 Agent 能否自我修正):
- 协议级错误(未知方法 / 未知工具 / 参数结构非法)→ JSON-RPC
error(如-32602)。 - 业务级错误(版块要前缀、积分不足、无权限、被限流、被封禁)→
result.isError: true+content[0].text写人类可读中文,让模型自己改参数重试。 - 例:
{"isError":true,"content":[{"type":"text","text":"该版块要求选择标题前缀,请从 [BUG提交] [优化提交] 中选一个"}]}
3.4 鉴权:自定义 Bearer Token
- 规范中 Authorization 是 OPTIONAL;只有在「支持鉴权」时才 SHOULD 遵循 OAuth 章。→ 自定义静态 Bearer 不违规。
- 规范同时禁止 token passthrough(不得接受/透传非本服务签发的令牌)→ 我们只认本插件签发的 token,符合要求。
- 三家客户端均支持静态请求头:
- Cursor:
mcp.json的headers - Trae:HTTP 类型的
headers - Claude:Custom Connector → 「No sign-in」+ Request headers(beta)
3.5 客户端接入配置(示例)
{
"mcpServers": {
"flinthub": {
"url": "https://www.flinthub.top/mcp",
"headers": { "Authorization": "Bearer fh_mcp_xxxxxxxx" }
}
}
}本地 stdio 形态本期不做(PHP CLI 桥接需要额外分发一个脚本,收益低、维护成本高)。
4. 详细方案
4.1 目录结构
plugins/mcp_server/
├── plugin.json # permissions: ["route:admin"]
├── Plugin.php # 生命周期 + class_alias(目录含下划线)
├── McpServer.php # JSON-RPC 2.0 分发(initialize/ping/tools.list/tools.call)
├── Transport.php # HTTP 层:Origin 校验、Accept 校验、Bearer 解析、202/405/400 语义
├── TokenManager.php # Token 生成 / 校验 / 撤销 / 限流 / 额度
├── ToolRegistry.php # 工具清单 + 权限过滤 + 动态 schema 合并
├── Tools/
│ ├── ReadTools.php # list_categories / search_threads / get_thread / list_replies / get_user
│ └── WriteTools.php # create_thread / reply_thread / edit_post / delete_post
├── Audit.php # 审计日志写入(截断 + 保留期)
├── FrontController.php # 前台:Token 管理页
├── AdminController.php # 后台:开关 / 限流 / 策略 / 审计
├── hook/
│ ├── init_after.php # 幂等建表
│ ├── route_register.php # /mcp + /mcp-token*
│ └── admin_route_register.php
├── views/
│ ├── token.php # 前台 Token 页
│ └── admin.php # 后台
├── assets/{script.js,style.css}
├── lang/{zh,en,zh_tw}.php
└── data/mcp_server.sqlite # 独立库(Web 层已被 Plugin::db() 自动拦截)4.2 plugin.json 关键字段
{
"name": "MCP 服务",
"version": "1.0.0",
"author": "FlintHub",
"description": "让本机 AI Agent(Claude Desktop / Cursor / Trae)以你的账号读写论坛",
"activated": false,
"category": "integration",
"icon": "robot",
"admin_url": "/admin/mcp-server",
"hooks": {
"init_after": "hook/init_after.php",
"route_register": "hook/route_register.php",
"admin_route_register": "hook/admin_route_register.php"
},
"permissions": ["route:admin"]
}icon必须是核心图标表里真实存在的项(robot已在ai_assistant使用,开发前需再 grep 核对)。- 不声明
net:fetch(MCP 不主动外呼;纯 JSON 响应无需外网);不声明system:settings。 - 权限强制模式:
permission_enforce默认 1(app/Helpers/Plugin.php:155-160)→ 声明必须准确,否则后台路由会被拒。
4.3 Token 体系(用户级)
用户在前台生成 Token → 只在生成时展示一次 → 库里只存 hash| 项 | 设计 |
|---|---|
| 形态 | fh_mcp_ + 32 字节随机(base64url);前缀便于正则识别与日志打码 |
| 存储 | 只存 sha256 hash;明文永不回显、不入日志 |
| 绑定 | user_id —— 权限完全按该用户走 |
| 分级 | read(默认)/ write(用户主动开)/ admin(默认关,不推荐) |
| 生命周期 | 可设过期(默认不过期)、可随时撤销、可查看最近使用时间 |
| 展示 | 前台 Token 页展示「权限范围 / 最近使用 / 创建时间 / 撤销」 |
校验链:Bearer → 取 hash → 查库 → 校验 enabled/expires → 取 user_id → 载入用户 → 权限/封禁/限流校验 → 执行。
4.4 权限对齐(核心原则)
MCP 请求 → Token 验证 → 用户身份 → 调核心 Model(与「用户正常发帖」同一条路径)→ 落库
↑
版块权限 / 等级 / 积分 / 审核 全部自然生效- 绝不自己写库(否则
content_review、points_mall、user_level的钩子全部不触发)。 - 审计独立:审计日志额外记录「本次写操作由 MCP 发起」(token_id / tool / 结果)。
- ⚠️ Model 调用方式必须逐一实测:
ai_assistant的踩坑记录明确写着「Model 读取必须使用实例方法」(plugins/ai_assistant/说明.md:157-164)。Thread::insert()/Post::insert()是实例方法还是静态方法,开发第一步就核实,不要照抄本方案的写法。
4.5 工具清单
只读工具
| 工具 | 参数(JSON Schema 概要) | 落点 |
|---|---|---|
list_categories | 无 | 版块列表(含「是否可发帖」标记) |
search_threads | keyword / category_id? / limit? | 搜索(复用核心搜索路径,开发前确认调用点) |
get_thread | thread_id | Thread::find() / getById()(app/Models/Thread.php:184-221) |
list_replies | thread_id / page? | Post::getByThread()(app/Models/Post.php:337-399) |
get_user | user_id 或 username | 仅公开字段(绝不含邮箱/私信/后台字段) |
可写工具
| 工具 | 参数 | 落点 |
|---|---|---|
create_thread | category_id / title / content / prefix? / tags? | Thread::insert() |
reply_thread | thread_id / content / reply_to? | Post::insert() |
edit_post | post_id / content | 仅自己的帖 |
delete_post | post_id | 软删,仅自己的帖 |
P4 候选(见 §4.6,可能不是核心能力)
| 工具 | 落点 | 说明 |
|---|---|---|
vote(点赞/踩) | 疑为插件 like_points 提供 | 核心可能无内置点赞 → 软依赖 |
favorite(收藏) | 疑为插件 post_favorite 提供 | 同上 |
⚠️ 重要修正:
like_points与post_favorite都出现在官方插件市场清单里,说明点赞/收藏很可能是插件能力而非核心能力。若如此,vote/favorite不应写进 P0/P1,而应放到 P4,并做成「提供方插件在线才注册该工具」的动态注册(见 §4.6)。开发前必须先核实这两个能力的归属。
4.6 动态 Schema 机制(★ 与原规划的关键差异)
问题:forum_required_tag 会在发帖时强制前缀/标签;Agent 不知道规则 → 发帖被拒 → 体验差。
原规划的解法:MCP 直接读 forum_required_tag 的配置。
问题所在:该插件的数据在它自己的独立库(plugins/forum_required_tag/data/forum_required_tag.sqlite,表 frt_forum_tags / frt_forum_prefixes),需要调它的静态方法 Plugin\ForumRequiredTag\Plugin::getForumTags($forumId)。这属于插件间直耦,违反《插件开发规范》§11.6。
正确解法:自定义钩子契约 mcp_tool_schema
┌──────────────────────────────────────┐
tools/list ──────►│ ToolRegistry │
│ 1. 内置基础 schema │
│ 2. Plugin::hook('mcp_tool_schema', …) │──► 各插件订阅
│ 3. 合并 → 返回给 Agent │ (forum_required_tag
└──────────────────────────────────────┘ 上报前缀 enum / 标签规则)McpServer在tools/list与tools/call前调用Plugin::hook('mcp_tool_schema', $ctx)($ctx含category_id、tool_name)。- 任何插件想参与,只需在自己
plugin.json里加一行"hooks": { "mcp_tool_schema": "hook/mcp_tool_schema.php" }。 forum_required_tag上报:该版块的前缀列表(enum)与标签规则(required/ 数量下限),前缀展示形式为[xxx],库内存储不含方括号。- 无人订阅时零影响:退回基础 schema;发帖被拒时,由核心/插件的拒绝信息转成可读中文回给 Agent。
- ✅ 这样
mcp_server不认识forum_required_tag,两个插件永不直耦,未来任何插件都能自助接入。
同样的机制可用于三件事:
- 动态 schema(版本前缀 enum、标签必填)
- 动态工具注册(
like_points/post_favorite在线时才暴露vote/favorite) - 动态前置提示(
content_review上报「本版块发帖将进入审核」)
⚠️ 契约的两个硬约束:① 钩子参数用引用回传(
Plugin::hook()的extract(..., EXTR_REFS)支持),② 钩子文件必须include_once安全(不声明全局符号,或接受只执行一次)。
4.7 安全约束
| 项 | 要求 |
|---|---|
| 权限分级 | 默认 read;write 由用户主动开;admin 默认关 |
| 限流 | 按 Token(非按 IP):写操作每 Token 每分钟 N 次(建议 5)。⚠️ 核心 RateLimiter::check($action,$max,$window) 是 IP + Session 双计数(app/Helpers/RateLimiter.php:225-253),MCP 无 Session → 必须插件自建 Token 级计数(用独立库计数表) |
| 冷静期 | 同一 Token 对同一 tool + category_id 的重复调用间隔 ≥ X 秒(建议 30s) |
| 额度 | 每 Token 每日发帖上限(建议 10),可后台配置 |
| 审核 | 「AI 发帖先过审核」开关;开启时写操作不进正文库而是走审核路径(复用 content_review 的既有判定入口) |
| 审计 | 所有调用落 mcp_logs:token_id / user_id / tool / 参数摘要 / 结果 / ip_hash / 时间;参数与结果截断存储;保留 90 天 |
| 越权 | Token 权限 ≤ 绑定用户权限;每次调用都校验封禁状态 |
| 隐私 | 不读私信、不读后台数据、不读隐藏版块;get_user 只回公开字段 |
| 不越权重放 | 写工具必须幂等考虑:冷静期 + 额度 + 可选 idempotency_key 参数 |
4.8 内容归属与标注
- 内容归属 Token 绑定用户(无独立「MCP 用户」概念)。
- 可选标注「此帖由 MCP 客户端发布」:不改核心 → 用渲染钩子注入;标记存独立库(
mcp_post_marks表,记thread_id/post_id)。 - 后台总开关:是否允许 AI 发帖(有些站长不想让 AI 灌水)。
4.9 独立库 schema(初稿,SQLite 语法)
-- Token
CREATE TABLE IF NOT EXISTS mcp_tokens (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
name TEXT NOT NULL DEFAULT '',
scope TEXT NOT NULL DEFAULT 'read', -- read | write
enabled INTEGER NOT NULL DEFAULT 1,
daily_quota INTEGER NOT NULL DEFAULT 10,
last_used_at INTEGER,
expires_at INTEGER,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mcp_tokens_user ON mcp_tokens(user_id);
-- 审计
CREATE TABLE IF NOT EXISTS mcp_logs (
id INTEGER PRIMARY KEY,
token_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
tool TEXT NOT NULL,
params TEXT NOT NULL DEFAULT '', -- 截断
result TEXT NOT NULL DEFAULT '', -- 截断
ip_hash TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mcp_logs_created ON mcp_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_mcp_logs_token ON mcp_logs(token_id, created_at);
-- 限流计数(Token 级)
CREATE TABLE IF NOT EXISTS mcp_rate (
id INTEGER PRIMARY KEY,
token_id INTEGER NOT NULL,
action TEXT NOT NULL, -- 如 write / tool:create_thread
window_at INTEGER NOT NULL, -- 窗口起点(分钟/天)
counter INTEGER NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_mcp_rate ON mcp_rate(token_id, action, window_at);
-- 内容标记(「由 MCP 发布」)
CREATE TABLE IF NOT EXISTS mcp_post_marks (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL, -- thread | post
ref_id INTEGER NOT NULL,
token_id INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_mcp_marks ON mcp_post_marks(kind, ref_id);建表走
Plugin::ensureSchema('mcp_server', $ddl);表名统一mcp_前缀防冲突。
4.10 前台 / 后台
前台(用户侧):生成 Token(一次性展示 + 复制)、权限范围(只读/可写)、最近使用记录、撤销。
后台(管理员侧):
| 项 | 默认 |
|---|---|
| 总开关(启用/关闭 MCP) | 关 |
| 允许的最高权限等级 | read |
| 是否允许 AI 发帖 | 关 |
| AI 发帖是否走审核 | 开(站长可关) |
| 写操作限流(次/分钟/Token) | 5 |
| 每日发帖额度(帖/Token/天) | 10 |
| 冷静期(秒) | 30 |
| 审计保留天数 | 90 |
| 审计日志列表 + 一键撤销某 Token 全部会话 | — |
5. 与现有插件生态的交互
总原则:MCP 走 Model 层 → 插件钩子自然触发;规则读取走 mcp_tool_schema 钩子 → 插件自己上报;两者都不产生直耦。
| 插件 | 影响的 MCP 工具 | 需要读取的规则源 | 需要反映到 schema / 错误信息 |
|---|---|---|---|
forum_required_tag | create_thread / edit_post | 独立库 frt_forum_prefixes / frt_forum_tags(通过它自己订阅 mcp_tool_schema 上报) | prefix 加 enum;tags 标 required 与数量下限;错误信息给可选项清单 |
content_review | create_thread / reply_thread | cr_* settings;isThreadReviewRequired() | 返回体标注「已提交审核,暂不可见」;schema 里给出前置提示 |
mod_system | 全部 | mod_bans;isBanned() | 被禁用户 → 每次调用直接拒绝(Token 视同失效) |
user_level | create_thread / reply_thread | user_level_cfg 的 gate 字段;calc() | 等级不足 → 可读中文拒绝,提示所需等级 |
points_mall | create_thread / reply_thread | 走 Model 自动扣 | 积分不足 → 核心错误转可读中文 |
ai_assistant | get_thread 等 | — | 反向防护:Agent 读到的 AI 回复应可识别(避免「AI 读 AI」,参考其 防 AI 回 AI 逻辑) |
like_points / post_favorite | vote / favorite(P4) | 待核实 | 在线才注册该工具(动态注册) |
6. 风险清单与规避
| # | 风险 | 级别 | 规避 |
|---|---|---|---|
| 1 | php-fpm 下开 SSE 会占死 worker | 高 | 不做 SSE,纯 JSON 响应(本方案已定) |
| 2 | 插件间直耦(读别人的库) | 高 | 走 mcp_tool_schema 自定义钩子契约 |
| 3 | Token 明文泄露 | 高 | 只存 hash、仅生成时展示、日志打码 |
| 4 | Agent 被诱导滥用(灌水/刷帖) | 高 | Token 级限流 + 冷静期 + 每日额度 + 站长总开关 |
| 5 | 越权(Token 权限 > 用户权限) | 高 | 每次调用重新校验用户权限与封禁状态 |
| 6 | CSRF 误加到 /mcp | 中 | 明确不加 Csrf::verifyOrDie(),代码注释写明原因 |
| 7 | Origin 未校验 → DNS rebinding | 中 | 规范 MUST:校验 Origin,非白名单 403 |
| 8 | MCP-Protocol-Version 版本不一致 | 中 | 宽容读取;非法/不支持才 400 |
| 9 | Model 实例/静态方法搞错 | 中 | 开发第一步实测(ai_assistant 已有踩坑记录) |
| 10 | 点赞/收藏可能是插件能力 | 中 | 先核实归属;P4 + 动态注册 |
| 11 | /mcp 与真实目录冲突 | 低 | 已验证站点根无 mcp 目录 |
| 12 | 目录名下划线导致 activate 静默跳过 | 中 | class_alias 硬要求 |
| 13 | 审计表膨胀 | 低 | 保留 90 天 + params/result 截断 |
| 14 | Agent 看到的工具名/描述被误当可信 | 低 | description 只写事实,不含指令性内容 |
| 15 | 协议版本快速演进(2026-07-28 破坏性变更) | 中 | 锁 2025-06-18 基线;后续再加 server/discover |
7. 分期实施与验收标准
验收标准 = 逐条勾选,全绿才算过。
P0:Token 体系 + 只读工具(先跑通「读」)
- 插件可激活;
Plugin::db('mcp_server')建库成功;data/.htaccess自动生成 - 前台可生成 Token(明文只显示一次);库里只有 hash
initialize握手通过(Claude / Cursor / Trae 至少 1 个真实客户端连上)tools/list返回 5 个只读工具search_threads/get_thread/list_replies/list_categories/get_user均返回正确数据- 无 Token / 错 Token → 401;
get_user不含邮箱等私密字段 - GET
/mcp→ 405;Origin 非法 → 403
P1:可写工具(跑通「发帖 / 回帖」)
create_thread落库成功,且帖子归属 Token 绑定用户reply_thread落库成功,reply_count/last_reply_time正确edit_post/delete_post只能操作自己的帖;操作别人的 → 可读拒绝- 审计日志记录 token_id / user / tool / 结果 / ip_hash
- Token 级限流生效(超限返回可读中文);无关 CSRF 报错
P2:生态联动
- 开启
forum_required_tag后,create_thread的 schema 出现前缀 enum(且mcp_server代码里零forum_required_tag字样) - 不填前缀发帖 → 错误信息列出可选前缀,Agent 能据此重试成功
- 开启
content_review后,发帖返回体标注「已提交审核」 mod_system封禁用户 → 全部工具拒绝
P3:审计 / 限流 / 后台
- 后台总开关可关闭整个 MCP(关闭后
/mcp一律 401/503) - 后台可配限流/额度/冷静期/审核策略,改完立即生效
- 后台审计列表可翻页、可按 Token / 用户筛选
- 站长可一键撤销某 Token 全部权限
P4:高级工具
- 点赞/收藏归属核实完成;若为插件能力 → 做成「插件在线才注册工具」
vote/favorite与前台行为一致(积分、通知等钩子均触发)
出包
php cli/release_build.php --type=plugin --name=mcp_server --version=1.0.0 --dry-run→ 9 项门禁全 PASS(含#9 命名空间一致性)data/(含 sqlite)不进包;plugin.json/ 表单 / zip 名三处版本号一致
8. 待拍板决策(建议,供评审确认)
| # | 问题 | **建议** | 影响面 |
|---|---|---|---|
| D1 | 传输协议 | 无状态 Streamable HTTP + 纯 JSON(不开 SSE、不分配 Session) | 决定能否在 php-fpm 直接跑;最关键 |
| D2 | 协议版本 | 锁 2025-06-18 为基线,兼容 2025-11-25 | 决定握手实现与未来兼容成本 |
| D3 | 默认权限 | 只读,write 用户主动开 | 安全基线 |
| D4 | 插件生态联动方式 | 自定义钩子 mcp_tool_schema(禁止直读他插件库) | 决定是否违反 §11.6;第二关键 |
| D5 | AI 发帖是否默认走审核 | 默认开(站长可关) | 内容风险 |
| D6 | 是否标注「由 MCP 发布」 | 默认标注(可开关) | 透明度 |
| D7 | 限流阈值 | 写操作 5 次/分钟/Token;额度 10 帖/天/Token;冷静期 30s | 防灌水 |
| D8 | 限流实现 | 插件自建 Token 级计数(核心 RateLimiter 是 IP+Session 口径,不适用) | 实现方式 |
| D9 | 点赞/收藏归属 | 先核实;非核心能力则降到 P4 + 动态注册 | 工具范围 |
| D10 | 是否支持本地 stdio | 本期不做 | 分发形态 |
| D11 | 会话/SSE | 不做 | 运行环境依赖 |
| D12 | 分期节奏 | P0+P1 先做,验证「读 + 发帖回帖」核心链路,其余按 §7 推进 | 人力投入 |
附:本方案用到的规范条目索引
§三条硬规则(basename 校验 / SQL 预处理 / 转义) / §红线 8(AJAX 必须 CSRF,但协议端点例外需注明) / §2 目录结构 / §3 plugin.json(icon 必须真实存在) / §4.1 生命周期类名推导 + class_alias / §5.1 自动加载器方向不对称 / §8 语言包(键前缀 plugin.{插件名}.*) / §9 钩子清单与通信约定 / §10.3 SQLite DDL / §11.6 插件间禁止直耦 / §11.10 后台 Tab 标准 / §11.15 建表兜底 / §11.28 登录回跳 / §11.31 禁止每请求写库 / §11.34 命名空间逐字节 / §11.34b 前台路由避开真实目录 / §11.35 三条发布路径 / §12.2 出包前自检
外部依据:MCP 规范 Versioning / Transports(2025-06-18) / Authorization / Tools / Security Best Practices;Claude · Cursor · Trae 的 MCP 接入文档。