feat: 新增 MCP 服务,支持 AI 自动操作项目与配置
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 39s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 39s
- Streamable HTTP 传输,/mcp 端点,Bearer token 鉴权(data/mcp.json,已忽略) - 项目/配置完整增删改查工具,删除为两步式确认(delete_* + confirm_delete_*) - 写操作遵循系统模式,readonly 下拒绝 - 依赖 @modelcontextprotocol/sdk,新增单元测试
This commit is contained in:
parent
f9f3184f98
commit
963046edd9
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,5 +2,6 @@ node_modules/
|
||||
data/projects.json
|
||||
data/system.json
|
||||
data/configs/*.json
|
||||
data/mcp.json
|
||||
*.log
|
||||
.reasonix/*
|
||||
52
README.md
52
README.md
@ -19,10 +19,56 @@ npm start
|
||||
- **三种模式**:
|
||||
- 规划模式:定义配置结构(类型、说明、用途、值范围)。
|
||||
- 编辑模式:填写/修改配置值,含类型与范围校验。
|
||||
- 只读模式:仅查看,禁止修改。
|
||||
- 只读模式:仅查看,禁止修改。
|
||||
- **持久化**:当前实现 JSON 文件方案,支持导出下载与导入合并。持久化层已抽象,后续可扩展 XML / YAML / SQLite / MySQL / Nacos。
|
||||
- **MCP 服务**:通过 Model Context Protocol 暴露同一套能力,AI 可自动完成项目/配置的增删改查;删除为两步式确认,写操作受系统模式(只读)约束。
|
||||
|
||||
## 文档
|
||||
## MCP 服务
|
||||
|
||||
MCP 服务以 Streamable HTTP 传输运行在 `POST /mcp` 端点,供 AI(例如 Claude、Cursor 等支持 MCP 的客户端)自动操作。
|
||||
|
||||
### 配置
|
||||
|
||||
授权 token 写在 `data/mcp.json`(已被 .gitignore 忽略,不会提交):
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"token": "CHANGE_ME_REGISTRY_MCP_TOKEN"
|
||||
}
|
||||
```
|
||||
|
||||
客户端连接时需携带请求头 `Authorization: Bearer <token>`。若 token 为空字符串,则跳过鉴权(不推荐)。
|
||||
|
||||
### 配置示例(客户端)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"registry": {
|
||||
"url": "http://localhost:3000/mcp",
|
||||
"headers": { "Authorization": "Bearer CHANGE_ME_REGISTRY_MCP_TOKEN" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 可用工具
|
||||
|
||||
| 类别 | 工具 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 系统 | `get_mode` / `set_mode` | 读取/切换系统模式(plan/edit/readonly) |
|
||||
| 项目 | `list_projects` / `get_project` | 项目查询 |
|
||||
| 项目 | `create_project` | 创建项目(维度开关创建后不可改) |
|
||||
| 项目 | `update_project_mappings` | 调整维度映射方式(path/label) |
|
||||
| 项目 | `delete_project` / `confirm_delete_project` | 两步式删除:先 `delete_project` 获取令牌,再 `confirm_delete_project` 真正删除 |
|
||||
| 配置 | `list_configs` / `get_config` | 配置项查询 |
|
||||
| 配置 | `create_config` / `update_config` | 配置项新增/修改 |
|
||||
| 配置 | `delete_config` / `confirm_delete_config` | 两步式删除,机制同上 |
|
||||
|
||||
两步式删除返回 `confirmation_token`(5 分钟内有效、一次性),需调用对应的 `confirm_*` 工具并回传该令牌才会真正删除。
|
||||
|
||||
### 文档
|
||||
|
||||
- 需求文档:`docs/requirements.md`
|
||||
- 概要设计文档:`docs/design.md`
|
||||
@ -46,4 +92,4 @@ registry/
|
||||
|
||||
- 对接 SSO 单点登录(当前版本无认证)。
|
||||
- 扩展更多持久化方案(数据库、Nacos、SSH 远程配置文件)。
|
||||
- 项目维度转换机制。
|
||||
- 项目维度转换机制。
|
||||
|
||||
761
package-lock.json
generated
761
package-lock.json
generated
@ -9,9 +9,416 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-2.1.1.tgz",
|
||||
"integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
|
||||
"integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9 || ^2.0.5",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
"cors": "^2.8.5",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"hono": "^4.11.4",
|
||||
"jose": "^6.1.3",
|
||||
"json-schema-typed": "^8.0.2",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.25 || ^4.0",
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cfworker/json-schema": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.3.0.tgz",
|
||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.1.tgz",
|
||||
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.1.0.tgz",
|
||||
"integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.0.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
@ -25,6 +432,39 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@ -129,6 +569,37 @@
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz",
|
||||
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
|
||||
@ -231,6 +702,27 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz",
|
||||
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
|
||||
"integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz",
|
||||
@ -277,6 +769,70 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
|
||||
"integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.7.tgz",
|
||||
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@ -395,6 +951,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmmirror.com/hono/-/hono-4.13.7.tgz",
|
||||
"integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@ -433,6 +998,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.7.0.tgz",
|
||||
"integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@ -442,6 +1016,39 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.12",
|
||||
"resolved": "https://registry.npmmirror.com/jose/-/jose-6.2.12.tgz",
|
||||
"integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -526,6 +1133,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@ -550,6 +1166,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@ -559,12 +1184,30 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pkce-challenge": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -618,6 +1261,64 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/router/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/router/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/router/node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@ -695,6 +1396,27 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz",
|
||||
@ -824,6 +1546,45 @@
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.5.4.tgz",
|
||||
"integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.25.2",
|
||||
"resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
|
||||
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.28 || ^4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,9 +8,14 @@
|
||||
"dev": "node src/server.js",
|
||||
"test": "node --test"
|
||||
},
|
||||
"keywords": ["config", "registry", "microservice"],
|
||||
"keywords": [
|
||||
"config",
|
||||
"registry",
|
||||
"microservice"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
src/mcp/index.js
Normal file
113
src/mcp/index.js
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* MCP HTTP 传输层
|
||||
* 以 Streamable HTTP 协议在 /mcp 端点暴露 MCP 服务。
|
||||
* 鉴权:基于 data/mcp.json 中配置的 token,要求请求头 Authorization: Bearer <token>。
|
||||
*/
|
||||
const crypto = require("crypto");
|
||||
const express = require("express");
|
||||
const { randomUUID } = require("node:crypto");
|
||||
const {
|
||||
StreamableHTTPServerTransport,
|
||||
} = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
||||
const { isInitializeRequest } = require("@modelcontextprotocol/sdk/types.js");
|
||||
const { createMcpServer } = require("./tools");
|
||||
const { loadMcpConfig } = require("../persistence/jsonAdapter");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// sessionId -> transport,用于会话复用
|
||||
const transports = new Map();
|
||||
|
||||
/**
|
||||
* 校验请求携带的 Bearer token 是否与配置中的 token 匹配
|
||||
* @param {string} [header] - Authorization 请求头
|
||||
* @returns {boolean} 是否鉴权通过
|
||||
*/
|
||||
function isAuthorized(header) {
|
||||
const mcp = loadMcpConfig();
|
||||
// 未配置 token 时视为未启用鉴权
|
||||
if (!mcp.token) {
|
||||
return true;
|
||||
}
|
||||
const expected = mcp.token;
|
||||
const provided = header && header.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
||||
if (!provided || provided.length !== expected.length) {
|
||||
return false;
|
||||
}
|
||||
// 使用常量时间比较,避免时序侧信道
|
||||
const a = Buffer.from(provided);
|
||||
const b = Buffer.from(expected);
|
||||
return crypto.timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 鉴权中间件:token 校验失败返回 401
|
||||
*/
|
||||
router.use((req, res, next) => {
|
||||
if (!isAuthorized(req.headers.authorization)) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized: invalid or missing token" }, id: null });
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据会话 ID 或其下的 transport 处理请求
|
||||
* 若带 mcp-session-id 且存在则复用;否则仅初始化请求可新建会话
|
||||
*/
|
||||
async function handleRequest(req, res) {
|
||||
const sessionId = req.headers["mcp-session-id"];
|
||||
const jsonBody = req.body;
|
||||
|
||||
try {
|
||||
let transport;
|
||||
if (sessionId && transports.get(sessionId)) {
|
||||
transport = transports.get(sessionId);
|
||||
} else if (!sessionId && isInitializeRequest(jsonBody)) {
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sid) => {
|
||||
transports.set(sid, transport);
|
||||
},
|
||||
});
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
transports.delete(transport.sessionId);
|
||||
}
|
||||
};
|
||||
const server = createMcpServer();
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
return;
|
||||
} else {
|
||||
res.status(400).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Bad Request: missing or invalid mcp-session-id" },
|
||||
id: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (err) {
|
||||
console.error("MCP 请求处理失败:", err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST /mcp:初始化与工具调用
|
||||
router.post("/", (req, res) => handleRequest(req, res));
|
||||
|
||||
// GET /mcp:建立 SSE 流(会话恢复/服务器通知)
|
||||
router.get("/", (req, res) => handleRequest(req, res));
|
||||
|
||||
// DELETE /mcp:会话终止
|
||||
router.delete("/", (req, res) => handleRequest(req, res));
|
||||
|
||||
module.exports = router;
|
||||
515
src/mcp/tools.js
Normal file
515
src/mcp/tools.js
Normal file
@ -0,0 +1,515 @@
|
||||
/**
|
||||
* MCP 工具定义
|
||||
* 将 Registry 的项目/配置 CRUD 能力以 MCP 工具形式暴露给 AI。
|
||||
* 约定:
|
||||
* - 删除操作为“两步式”:delete_* 首次调用仅登记待删除项并返回确认令牌,
|
||||
* 确认令牌需再调用 confirm_* 工具才会真正执行删除。
|
||||
* - 读写均遵循系统当前模式:readonly 模式下所有写操作(增删改)被拒绝。
|
||||
*/
|
||||
const crypto = require("crypto");
|
||||
const z = require("zod/v4");
|
||||
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
||||
const projectService = require("../services/projectService");
|
||||
const configService = require("../services/configService");
|
||||
const { loadSystem, loadMcpConfig } = require("../persistence/jsonAdapter");
|
||||
|
||||
// 待确认删除的登记表:token -> { type, projectId, configId, expiresAt }
|
||||
const pendingDeletions = new Map();
|
||||
// 待确认删除令牌有效期(毫秒)
|
||||
const PENDING_TTL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 获取当前系统模式
|
||||
* @returns {string} plan/edit/readonly
|
||||
*/
|
||||
function currentMode() {
|
||||
try {
|
||||
return (loadSystem() || {}).mode || "edit";
|
||||
} catch {
|
||||
return "edit";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只读模式下拒绝写操作
|
||||
* @throws {Error} 只读模式
|
||||
*/
|
||||
function assertWritable() {
|
||||
if (currentMode() === "readonly") {
|
||||
throw new Error("系统当前为只读模式,不允许写操作");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成删除确认令牌
|
||||
* @returns {string} 随机令牌
|
||||
*/
|
||||
function generateToken() {
|
||||
return crypto.randomBytes(16).toString("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* 登记一个待删除操作,返回确认令牌
|
||||
* @param {string} type - 类型:project/config
|
||||
* @param {string} projectId - 项目 ID
|
||||
* @param {string} [configId] - 配置 ID(type=config 时)
|
||||
*/
|
||||
function createPendingDeletion(type, projectId, configId) {
|
||||
const token = generateToken();
|
||||
pendingDeletions.set(token, {
|
||||
type,
|
||||
projectId,
|
||||
configId,
|
||||
expiresAt: Date.now() + PENDING_TTL,
|
||||
});
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并取出待删除登记,同时清理过期项
|
||||
* @param {string} token - 确认令牌
|
||||
* @returns {Object|null} 登记对象或 null
|
||||
*/
|
||||
function takePendingDeletion(token) {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of pendingDeletions) {
|
||||
if (value.expiresAt < now) {
|
||||
pendingDeletions.delete(key);
|
||||
}
|
||||
}
|
||||
const entry = pendingDeletions.get(token);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
pendingDeletions.delete(token);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将服务层抛错转为可读文本
|
||||
* @param {Error} err - 错误对象
|
||||
* @returns {string} 错误信息
|
||||
*/
|
||||
function errText(err) {
|
||||
return err && err.message ? err.message : String(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建带删除确认提示的文本结果
|
||||
* @param {string} summary - 结果概要
|
||||
* @param {string} confirmTool - 待调用的确认工具名
|
||||
* @param {string} token - 确认令牌
|
||||
* @param {Object} extra - 附带的上下文数据
|
||||
*/
|
||||
function confirmResult(summary, confirmTool, token, extra) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${summary}\n\n该操作需要二次确认。请调用工具 ${confirmTool} 并提供 confirmation_token: ${token} 以完成删除(令牌 ${Math.round(PENDING_TTL / 60000)} 分钟内有效)。`,
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
status: "awaiting_confirmation",
|
||||
summary,
|
||||
confirmTool,
|
||||
confirmation_token: token,
|
||||
...extra,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并返回 MCP 服务器实例(仅定义工具,不绑定传输层)
|
||||
* @returns {McpServer} 已注册全部工具的服务实例
|
||||
*/
|
||||
function createMcpServer() {
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "registry-config-center",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== 系统 ====================
|
||||
|
||||
server.registerTool(
|
||||
"get_mode",
|
||||
{
|
||||
description: "获取系统当前模式(plan/edit/readonly)。",
|
||||
inputSchema: {},
|
||||
},
|
||||
async () => ({
|
||||
content: [{ type: "text", text: `当前模式:${currentMode()}` }],
|
||||
structuredContent: { mode: currentMode() },
|
||||
})
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"set_mode",
|
||||
{
|
||||
description: "切换系统模式。mode 取值为 plan(规划)、edit(编辑)、readonly(只读)。",
|
||||
inputSchema: {
|
||||
mode: z.enum(["plan", "edit", "readonly"]).describe("目标模式"),
|
||||
},
|
||||
},
|
||||
async ({ mode }) => {
|
||||
const { loadSystem, saveSystem } = require("../persistence/jsonAdapter");
|
||||
const system = loadSystem() || {};
|
||||
system.mode = mode;
|
||||
saveSystem(system);
|
||||
return {
|
||||
content: [{ type: "text", text: `模式已切换为:${mode}` }],
|
||||
structuredContent: { mode },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== 项目 ====================
|
||||
|
||||
server.registerTool(
|
||||
"list_projects",
|
||||
{
|
||||
description: "获取所有项目列表。",
|
||||
inputSchema: {},
|
||||
},
|
||||
async () => ({
|
||||
content: [
|
||||
{ type: "text", text: JSON.stringify(projectService.getAllProjects(), null, 2) },
|
||||
],
|
||||
structuredContent: { projects: projectService.getAllProjects() },
|
||||
})
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"get_project",
|
||||
{
|
||||
description: "根据 ID 获取单个项目详情。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
},
|
||||
},
|
||||
async ({ projectId }) => {
|
||||
const project = projectService.getProjectById(projectId);
|
||||
if (!project) {
|
||||
throw new Error(`项目不存在:${projectId}`);
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
|
||||
structuredContent: { project },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"create_project",
|
||||
{
|
||||
description:
|
||||
"创建新项目。必填 name;维度开关 enableIdc/enableEnvironment/enableGroup 创建后不可修改;映射方式默认 path(也可填 label)。",
|
||||
inputSchema: {
|
||||
name: z.string().describe("项目名称(必填)"),
|
||||
description: z.string().optional().describe("项目描述"),
|
||||
enableIdc: z.boolean().optional().describe("是否启用机房维度"),
|
||||
enableEnvironment: z.boolean().optional().describe("是否启用环境维度"),
|
||||
enableGroup: z.boolean().optional().describe("是否启用分组维度"),
|
||||
idcMapping: z.enum(["path", "label"]).optional().describe("机房映射方式"),
|
||||
environmentMapping: z.enum(["path", "label"]).optional().describe("环境映射方式"),
|
||||
groupMapping: z.enum(["path", "label"]).optional().describe("分组映射方式"),
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
assertWritable();
|
||||
const project = projectService.createProject(args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
|
||||
structuredContent: { project },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"update_project_mappings",
|
||||
{
|
||||
description: "更新项目的维度映射方式(path/label)。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
idcMapping: z.enum(["path", "label"]).optional().describe("机房映射方式"),
|
||||
environmentMapping: z.enum(["path", "label"]).optional().describe("环境映射方式"),
|
||||
groupMapping: z.enum(["path", "label"]).optional().describe("分组映射方式"),
|
||||
},
|
||||
},
|
||||
async ({ projectId, idcMapping, environmentMapping, groupMapping }) => {
|
||||
assertWritable();
|
||||
const project = projectService.updateMappings(projectId, {
|
||||
idcMapping,
|
||||
environmentMapping,
|
||||
groupMapping,
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
|
||||
structuredContent: { project },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"delete_project",
|
||||
{
|
||||
description:
|
||||
"删除项目及其所有配置(两步式)。首次调用只登记删除操作并返回 confirmation_token,需再调用 confirm_delete_project 才会真正删除。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
},
|
||||
},
|
||||
async ({ projectId }) => {
|
||||
assertWritable();
|
||||
const project = projectService.getProjectById(projectId);
|
||||
if (!project) {
|
||||
throw new Error(`项目不存在:${projectId}`);
|
||||
}
|
||||
const token = createPendingDeletion("project", projectId);
|
||||
return confirmResult(
|
||||
`已登记删除项目「${project.name}」(${projectId}),等待确认。`,
|
||||
"confirm_delete_project",
|
||||
token,
|
||||
{ projectId, projectName: project.name }
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"confirm_delete_project",
|
||||
{
|
||||
description: "确认并真正删除之前由 delete_project 登记的项目(需提供其返回的 confirmation_token)。",
|
||||
inputSchema: {
|
||||
confirmation_token: z.string().describe("delete_project 返回的确认令牌"),
|
||||
},
|
||||
},
|
||||
async ({ confirmation_token }) => {
|
||||
assertWritable();
|
||||
const entry = takePendingDeletion(confirmation_token);
|
||||
if (!entry) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "确认令牌无效或已过期,删除操作未执行。请重新调用 delete_project。" },
|
||||
],
|
||||
structuredContent: { status: "invalid_or_expired" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
if (entry.type !== "project") {
|
||||
return {
|
||||
content: [{ type: "text", text: "该确认令牌对应的不是项目删除操作。" }],
|
||||
structuredContent: { status: "type_mismatch" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const ok = projectService.deleteProject(entry.projectId);
|
||||
if (!ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: `项目不存在或已删除:${entry.projectId}` }],
|
||||
structuredContent: { status: "not_found" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `项目已删除:${entry.projectId}` }],
|
||||
structuredContent: { status: "deleted", projectId: entry.projectId },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== 配置项 ====================
|
||||
|
||||
server.registerTool(
|
||||
"list_configs",
|
||||
{
|
||||
description: "获取指定项目下所有配置项。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
},
|
||||
},
|
||||
async ({ projectId }) => {
|
||||
const configs = configService.getConfigsByProject(projectId);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(configs, null, 2) }],
|
||||
structuredContent: { configs },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"get_config",
|
||||
{
|
||||
description: "根据配置 ID 获取单个配置项。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
configId: z.string().describe("配置项 ID"),
|
||||
},
|
||||
},
|
||||
async ({ projectId, configId }) => {
|
||||
const config = configService.getConfigById(projectId, configId);
|
||||
if (!config) {
|
||||
throw new Error(`配置项不存在:${configId}`);
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(config, null, 2) }],
|
||||
structuredContent: { config },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"create_config",
|
||||
{
|
||||
description:
|
||||
"新增配置项。必填 projectId、key(可用 / 嵌套)、description。维度字段在项目启用对应维度时为必填。type 取值 string/number/boolean/json。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
key: z.string().describe("配置键名,可用 / 嵌套"),
|
||||
value: z.any().optional().describe("配置值"),
|
||||
type: z.enum(["string", "number", "boolean", "json"]).optional().describe("值类型"),
|
||||
description: z.string().describe("配置说明(必填)"),
|
||||
defaultValue: z.any().optional().describe("默认值"),
|
||||
valueRange: z.string().optional().describe("值范围(编辑模式校验)"),
|
||||
required: z.boolean().optional().describe("是否必填"),
|
||||
exists: z.boolean().optional().describe("当前是否存在/生效"),
|
||||
purpose: z.string().optional().describe("用途说明"),
|
||||
idc: z.string().optional().describe("机房(启用该维度时必填)"),
|
||||
environment: z.string().optional().describe("环境(启用该维度时必填)"),
|
||||
group: z.string().optional().describe("分组(启用该维度时必填)"),
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
assertWritable();
|
||||
const { projectId, ...data } = args;
|
||||
const mode = currentMode();
|
||||
const config = configService.createConfig(projectId, data, mode);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(config, null, 2) }],
|
||||
structuredContent: { config },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"update_config",
|
||||
{
|
||||
description:
|
||||
"修改配置项(只读模式下禁止)。plan 模式可修改 type/description/purpose/valueRange/required/key/维度;edit 模式可修改 value/exists/key/维度。可仅传需修改的字段。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
configId: z.string().describe("配置项 ID"),
|
||||
key: z.string().optional().describe("配置键名"),
|
||||
value: z.any().optional().describe("配置值"),
|
||||
type: z.enum(["string", "number", "boolean", "json"]).optional().describe("值类型"),
|
||||
description: z.string().optional().describe("配置说明"),
|
||||
defaultValue: z.any().optional().describe("默认值"),
|
||||
valueRange: z.string().optional().describe("值范围"),
|
||||
required: z.boolean().optional().describe("是否必填"),
|
||||
exists: z.boolean().optional().describe("当前是否存在/生效"),
|
||||
purpose: z.string().optional().describe("用途说明"),
|
||||
idc: z.string().optional().describe("机房"),
|
||||
environment: z.string().optional().describe("环境"),
|
||||
group: z.string().optional().describe("分组"),
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
assertWritable();
|
||||
const { projectId, configId, ...data } = args;
|
||||
const picks = {};
|
||||
for (const field of [
|
||||
"key", "value", "type", "description", "defaultValue", "valueRange",
|
||||
"required", "exists", "purpose", "idc", "environment", "group",
|
||||
]) {
|
||||
if (data[field] !== undefined) {
|
||||
picks[field] = data[field];
|
||||
}
|
||||
}
|
||||
const mode = currentMode();
|
||||
const config = configService.updateConfig(projectId, configId, picks, mode);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(config, null, 2) }],
|
||||
structuredContent: { config },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"delete_config",
|
||||
{
|
||||
description:
|
||||
"删除配置项(两步式)。首次调用只登记删除操作并返回 confirmation_token,需再调用 confirm_delete_config 才会真正删除。",
|
||||
inputSchema: {
|
||||
projectId: z.string().describe("项目 ID"),
|
||||
configId: z.string().describe("配置项 ID"),
|
||||
},
|
||||
},
|
||||
async ({ projectId, configId }) => {
|
||||
assertWritable();
|
||||
const config = configService.getConfigById(projectId, configId);
|
||||
if (!config) {
|
||||
throw new Error(`配置项不存在:${configId}`);
|
||||
}
|
||||
const token = createPendingDeletion("config", projectId, configId);
|
||||
return confirmResult(
|
||||
`已登记删除配置项「${config.key}」(${configId}),等待确认。`,
|
||||
"confirm_delete_config",
|
||||
token,
|
||||
{ projectId, configId, configKey: config.key }
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"confirm_delete_config",
|
||||
{
|
||||
description: "确认并真正删除之前由 delete_config 登记的配置项(需提供其返回的 confirmation_token)。",
|
||||
inputSchema: {
|
||||
confirmation_token: z.string().describe("delete_config 返回的确认令牌"),
|
||||
},
|
||||
},
|
||||
async ({ confirmation_token }) => {
|
||||
assertWritable();
|
||||
const entry = takePendingDeletion(confirmation_token);
|
||||
if (!entry) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "确认令牌无效或已过期,删除操作未执行。请重新调用 delete_config。" },
|
||||
],
|
||||
structuredContent: { status: "invalid_or_expired" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
if (entry.type !== "config") {
|
||||
return {
|
||||
content: [{ type: "text", text: "该确认令牌对应的不是配置项删除操作。" }],
|
||||
structuredContent: { status: "type_mismatch" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const mode = currentMode();
|
||||
const ok = configService.deleteConfig(entry.projectId, entry.configId, mode);
|
||||
if (!ok) {
|
||||
return {
|
||||
content: [{ type: "text", text: `配置项不存在或已删除:${entry.configId}` }],
|
||||
structuredContent: { status: "not_found" },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `配置项已删除:${entry.configId}` }],
|
||||
structuredContent: { status: "deleted", projectId: entry.projectId, configId: entry.configId },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
module.exports = { createMcpServer, currentMode };
|
||||
@ -10,6 +10,7 @@ const path = require("path");
|
||||
const DATA_DIR = path.join(__dirname, "..", "..", "data");
|
||||
const PROJECTS_FILE = path.join(DATA_DIR, "projects.json");
|
||||
const SYSTEM_FILE = path.join(DATA_DIR, "system.json");
|
||||
const MCP_FILE = path.join(DATA_DIR, "mcp.json");
|
||||
const CONFIGS_DIR = path.join(DATA_DIR, "configs");
|
||||
|
||||
/**
|
||||
@ -121,6 +122,24 @@ function saveSystem(system) {
|
||||
writeJsonFile(SYSTEM_FILE, system);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 MCP 服务配置
|
||||
* @returns {Object} MCP 配置对象(默认启用、空 token)
|
||||
*/
|
||||
function loadMcpConfig() {
|
||||
ensureDataDir();
|
||||
return readJsonFile(MCP_FILE) || { enabled: true, token: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 MCP 服务配置
|
||||
* @param {Object} mcp - MCP 配置对象
|
||||
*/
|
||||
function saveMcpConfig(mcp) {
|
||||
ensureDataDir();
|
||||
writeJsonFile(MCP_FILE, mcp);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadProjects,
|
||||
saveProjects,
|
||||
@ -129,4 +148,6 @@ module.exports = {
|
||||
deleteConfigs,
|
||||
loadSystem,
|
||||
saveSystem,
|
||||
};
|
||||
loadMcpConfig,
|
||||
saveMcpConfig,
|
||||
};
|
||||
|
||||
@ -8,6 +8,9 @@ const projectRoutes = require("./routes/projects");
|
||||
const configRoutes = require("./routes/configs");
|
||||
const { loadSystem, saveSystem } = require("./persistence/jsonAdapter");
|
||||
|
||||
// MCP 服务(Model Context Protocol,供 AI 自动操作)
|
||||
const mcpRoutes = require("./mcp");
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
@ -21,6 +24,9 @@ app.use(express.static(path.join(__dirname, "..", "public"), { etag: false, last
|
||||
app.use("/api/projects", projectRoutes);
|
||||
app.use("/api/projects/:projectId/configs", configRoutes);
|
||||
|
||||
// MCP 服务端点(Streamable HTTP 传输)
|
||||
app.use("/mcp", mcpRoutes);
|
||||
|
||||
/**
|
||||
* GET /api/mode
|
||||
* 获取当前系统模式
|
||||
@ -58,4 +64,4 @@ app.put("/api/mode", (req, res) => {
|
||||
// 启动服务
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Registry 服务已启动: http://localhost:${PORT}`);
|
||||
});
|
||||
});
|
||||
|
||||
148
test/mcp.test.js
Normal file
148
test/mcp.test.js
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* MCP 工具单元测试
|
||||
* 覆盖:工具注册完整性、删除两步式确认、只读模式下拒绝写操作
|
||||
* 运行:npm test
|
||||
*/
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { createMcpServer } = require("../src/mcp/tools");
|
||||
const { loadSystem, saveSystem } = require("../src/persistence/jsonAdapter");
|
||||
|
||||
// 恢复测试前的系统模式,避免影响其他用例
|
||||
const originalMode = (loadSystem() || {}).mode || "edit";
|
||||
test.after(() => {
|
||||
const system = loadSystem() || {};
|
||||
system.mode = originalMode;
|
||||
saveSystem(system);
|
||||
});
|
||||
|
||||
let server;
|
||||
test.before(() => {
|
||||
server = createMcpServer();
|
||||
});
|
||||
|
||||
/**
|
||||
* 直接调用已注册工具的处理函数
|
||||
* @param {string} name - 工具名
|
||||
* @param {Object} args - 工具参数
|
||||
* @returns {Promise<Object>} 工具结果
|
||||
*/
|
||||
async function callTool(name, args) {
|
||||
const tool = server._registeredTools[name];
|
||||
assert.ok(tool, `工具不存在: ${name}`);
|
||||
try {
|
||||
const result = await tool.handler(args, {});
|
||||
return {
|
||||
text: (result.content || []).map((c) => c.text).join("\n"),
|
||||
structuredContent: result.structuredContent,
|
||||
isError: result.isError === true,
|
||||
};
|
||||
} catch (err) {
|
||||
// 直接调用底层 handler 时,抛错不会自动转换为 isError 结果,这里统一转换
|
||||
return {
|
||||
text: err && err.message ? err.message : String(err),
|
||||
structuredContent: { error: true },
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test("注册了全部项目/配置 CRUD 及确认删除工具", () => {
|
||||
const names = new Set(Object.keys(server._registeredTools));
|
||||
for (const name of [
|
||||
"get_mode", "set_mode",
|
||||
"list_projects", "get_project", "create_project", "update_project_mappings",
|
||||
"delete_project", "confirm_delete_project",
|
||||
"list_configs", "get_config", "create_config", "update_config",
|
||||
"delete_config", "confirm_delete_config",
|
||||
]) {
|
||||
assert.ok(names.has(name), `缺少工具: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("删除项目为两步式:先登记待确认,确认后才真正删除", async () => {
|
||||
const { text: createText } = await callTool("create_project", {
|
||||
name: "MCP-Unittest-Proj",
|
||||
});
|
||||
const project = JSON.parse(createText.match(/\{[\s\S]*\}/)[0]);
|
||||
const pid = project.id;
|
||||
|
||||
const del = await callTool("delete_project", { projectId: pid });
|
||||
assert.strictEqual(del.structuredContent.status, "awaiting_confirmation");
|
||||
assert.ok(del.structuredContent.confirmation_token);
|
||||
|
||||
// 未确认前项目仍然存在
|
||||
const stillThere = await callTool("get_project", { projectId: pid });
|
||||
assert.ok(!stillThere.isError);
|
||||
|
||||
// 确认后项目被删除
|
||||
const confirm = await callTool("confirm_delete_project", {
|
||||
confirmation_token: del.structuredContent.confirmation_token,
|
||||
});
|
||||
assert.strictEqual(confirm.structuredContent.status, "deleted");
|
||||
const gone = await callTool("get_project", { projectId: pid });
|
||||
assert.ok(gone.isError, "确认删除后项目应不存在");
|
||||
});
|
||||
|
||||
test("重复使用确认令牌会失败(一次性令牌)", async () => {
|
||||
const { text: createText } = await callTool("create_project", { name: "MCP-Unittest-Token" });
|
||||
const project = JSON.parse(createText.match(/\{[\s\S]*\}/)[0]);
|
||||
const del = await callTool("delete_project", { projectId: project.id });
|
||||
const tok = del.structuredContent.confirmation_token;
|
||||
|
||||
await callTool("confirm_delete_project", { confirmation_token: tok });
|
||||
const second = await callTool("confirm_delete_project", { confirmation_token: tok });
|
||||
assert.ok(second.isError);
|
||||
assert.strictEqual(second.structuredContent.status, "invalid_or_expired");
|
||||
});
|
||||
|
||||
test("readonly 模式下拒绝项目/配置写操作,切换回 edit 后恢复", async () => {
|
||||
await callTool("set_mode", { mode: "readonly" });
|
||||
|
||||
const createProject = await callTool("create_project", { name: "不应创建" });
|
||||
assert.ok(createProject.isError);
|
||||
assert.match(createProject.text, /只读模式/);
|
||||
|
||||
const delProject = await callTool("delete_project", { projectId: "proj-mrlip49o-qsr2yx" });
|
||||
assert.ok(delProject.isError);
|
||||
assert.match(delProject.text, /只读模式/);
|
||||
|
||||
const createConfig = await callTool("create_config", {
|
||||
projectId: "proj-mrlip49o-qsr2yx",
|
||||
key: "x",
|
||||
description: "x",
|
||||
});
|
||||
assert.ok(createConfig.isError);
|
||||
assert.match(createConfig.text, /只读模式/);
|
||||
|
||||
await callTool("set_mode", { mode: "edit" });
|
||||
});
|
||||
|
||||
test("配置删除同样为两步式确认", async () => {
|
||||
const { text: createText } = await callTool("create_project", { name: "MCP-Unittest-Cfg" });
|
||||
const project = JSON.parse(createText.match(/\{[\s\S]*\}/)[0]);
|
||||
|
||||
const cfg = await callTool("create_config", {
|
||||
projectId: project.id,
|
||||
key: "a/b",
|
||||
description: "unit",
|
||||
value: "1",
|
||||
type: "number",
|
||||
});
|
||||
const cfgObj = JSON.parse(cfg.text.match(/\{[\s\S]*\}/)[0]);
|
||||
|
||||
const del = await callTool("delete_config", { projectId: project.id, configId: cfgObj.id });
|
||||
assert.strictEqual(del.structuredContent.status, "awaiting_confirmation");
|
||||
|
||||
const confirm = await callTool("confirm_delete_config", {
|
||||
confirmation_token: del.structuredContent.confirmation_token,
|
||||
});
|
||||
assert.strictEqual(confirm.structuredContent.status, "deleted");
|
||||
|
||||
// 清理测试项目
|
||||
const delProject = await callTool("delete_project", { projectId: project.id });
|
||||
const confirmProject = await callTool("confirm_delete_project", {
|
||||
confirmation_token: delProject.structuredContent.confirmation_token,
|
||||
});
|
||||
assert.strictEqual(confirmProject.structuredContent.status, "deleted");
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user