@lark-apaas/coding-steering 0.1.32-dev.c8cc2b5 → 0.1.32-dev.f111120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-steering",
3
- "version": "0.1.32-dev.c8cc2b5",
3
+ "version": "0.1.32-dev.f111120",
4
4
  "description": "Stack-specific steering content for miaoda-coding templates",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mcp-guide
3
- description: "Use when 用户要把应用能力开放给外部 Agent / 工作伙伴 / MCP 客户端调用,新增或修改 MCP 工具,给工具结果加界面(MCP Apps / ui:// 资源),维护应用 Skill(server/mcp/skills/<name>/SKILL.md,SDK 注册为 MCP Prompt),要在开发态核对、调用、调试 MCP 工具,或要把本应用的 MCP 接到自己的 Agent 客户端上使用。触发词:MCP, MCP Server, MCP 工具, MCP Apps, ui://, @McpTools, @McpTool, @McpUiResource, McpToolError, readMcpUiTemplate, miaoda-mcp validate, .spark/mcp/manifest.json, tools/list, tools/call, resources/read, prompts/list, prompts/get, mcp_call, 开放给 Agent, 工作伙伴调用, Agent 调用应用, 应用 Skill, server/mcp, server/mcp/ui, server/mcp/skills, 接入 MCP, 连接 MCP, 怎么用这个 MCP, MCP 客户端, Claude Code, Codex, mcp add, mcp login, config.toml, X-Mcp-Token, MCP 凭证, 可用范围, 连不上。不负责 HTTP 对外开放接口(走 openapi-guide)、自动化触发器(走 trigger-guide)。"
3
+ description: "Use when 用户要把应用能力开放给外部 Agent / 工作伙伴 / MCP 客户端调用,新增或修改 MCP 工具,给工具结果加界面(MCP Apps / ui:// 资源),维护应用 Skill(server/mcp/skills/<name>/SKILL.md,SDK 注册为 MCP Prompt),要在开发态核对、调用、调试 MCP 工具,或要把本应用的 MCP 接到自己的 Agent 客户端上使用。触发词:MCP, MCP Server, MCP 工具, MCP Apps, ui://, @McpTools, @McpTool, @McpUiResource, McpToolError, readMcpUiTemplate, tools/list, tools/call, resources/read, prompts/list, prompts/get, mcp_call, 开放给 Agent, 工作伙伴调用, Agent 调用应用, 应用 Skill, server/mcp, server/mcp/ui, server/mcp/skills, 接入 MCP, 连接 MCP, 怎么用这个 MCP, MCP 客户端, Claude Code, Codex, mcp add, mcp login, config.toml, X-Mcp-Token, MCP 凭证, 可用范围, 连不上。不负责 HTTP 对外开放接口(走 openapi-guide)、自动化触发器(走 trigger-guide)。"
4
4
  steering: true
5
5
  steering-topic: mcp_guide
6
6
  match-template-name: nestjs-react-fullstack
@@ -8,9 +8,9 @@ match-template-name: nestjs-react-fullstack
8
8
 
9
9
  # MCP 开发与调试
10
10
 
11
- 应用的 MCP Server 是全栈工程里多出来的一层出口:同仓、同域名、同版本。SDK(`@lark-apaas/fullstack-nestjs-core` 重导出 `@lark-apaas/nestjs-mcp`)已经内置端点 `POST /__innerapi__/mcp`、Streamable HTTP 协议、zod 校验、身份读取、清单导出、`ui://` 资源与应用 Skill(MCP Prompt)下发。你只写三种文件:工具类、界面入口、应用 Skill。
11
+ 应用的 MCP Server 是全栈工程里多出来的一层出口:同仓、同域名、同版本。SDK(`@lark-apaas/fullstack-nestjs-core` 重导出 `@lark-apaas/nestjs-mcp`)已经内置端点 `POST /__innerapi__/mcp`、Streamable HTTP 协议、zod 校验、身份读取、`ui://` 资源与应用 Skill(MCP Prompt)下发。你只写三种文件:工具类、界面入口、应用 Skill。
12
12
 
13
- **真值顺序**:工程内 `node_modules/@lark-apaas/nestjs-mcp/` 的 `README.md` 与 `dist/index.d.ts` > 本文。装饰器选项、类型名、返回值形状以 `index.d.ts` 为准;本文与它冲突时按包改,不按本文改。
13
+ **真值顺序**:工程内 `node_modules/@lark-apaas/nestjs-mcp/dist/index.d.ts` > 本文。装饰器选项、类型名、返回值形状以它为准;本文与它冲突时按包改,不按本文改。
14
14
 
15
15
  ## 固定路径(不探测)
16
16
 
@@ -19,11 +19,10 @@ match-template-name: nestjs-react-fullstack
19
19
  | `server/mcp/tools/<域>.tools.ts` | 工具类:一个业务域一个文件,`@McpTools()` 类 + 多个 `@McpTool()` 方法 |
20
20
  | `server/mcp/ui/<entry>/index.html`(+ `main.tsx`)、`server/mcp/ui/tsconfig.json` | MCP Apps 界面源码:独立的浏览器代码,Vite 预设自动打成 `dist/mcp-ui/<entry>.html` |
21
21
  | `server/mcp/skills/<name>/SKILL.md` | 应用 Skill:一个业务流程一份,给消费方 Agent 看的用法说明,SDK 注册为同名 MCP Prompt |
22
- | `.spark/mcp/manifest.json` | SDK 生成的离线清单,随代码提交,不手改 |
23
22
 
24
- `server/mcp/` 不存在即为首次开通;开通只需建上述文件并把工具类注册为所属业务模块的 provider。`McpModule` 已由 `PlatformModule.forRoot()` 引入,SDK 启动时自动发现工具类——`app.module.ts` 不动,不写注册表,不写 `tools/index.ts`。
23
+ `server/mcp/` 不存在即为首次开通;开通只需建上述文件并把工具类注册为所属业务模块的 provider。`McpModule` 已由 `PlatformModule.forRoot()` 引入,SDK 启动时扫已加载模块的 provider 自动发现工具类——挂在已有业务模块上就不用动 `app.module.ts`,新建的模块仍要接进去,否则整个模块不会实例化、工具静默缺席;不写注册表,不写 `tools/index.ts`。
25
24
 
26
- 三份 Markdown 别混:本文(教你写代码)、`server/mcp/skills/*/SKILL.md`(应用产物,教下游 Agent 用工具)、SDK README(真值)。不把本文复制成应用 Skill,应用 Skill 不加 `steering: true`。
25
+ 两份同名的 SKILL.md 别混:本文教你写代码,`server/mcp/skills/*/SKILL.md` 是产物、教下游 Agent 用工具。不把本文复制成应用 Skill,应用 Skill 不加 `steering: true`。
27
26
 
28
27
  ## 前置检查(一条命令)
29
28
 
@@ -36,14 +35,16 @@ ls -d node_modules/@lark-apaas/nestjs-mcp node_modules/@lark-apaas/coding-preset
36
35
 
37
36
  ## 任务路由
38
37
 
38
+ `$SKILL_DIR` 取技能清单里本 skill 的 location 所在目录。
39
+
39
40
  | 任务 | 读 |
40
41
  |---|---|
41
- | 新增 / 修改工具、开通 MCP、维护应用 Skill | `references/tool-authoring.md` |
42
- | 给工具结果加界面、改界面、界面与工具联动 | `references/mcp-apps.md` |
43
- | 用 `mcp_call` 核对与调用、按错误定位、本地模式调试 | `references/debugging.md` |
44
- | 用户要在自己的 Agent(Claude Code / Codex / 其它客户端)里连本应用的 MCP | `references/client-onboarding.md` |
42
+ | 新增 / 修改工具、开通 MCP、维护应用 Skill | `$SKILL_DIR/references/tool-authoring.md` |
43
+ | 给工具结果加界面、改界面、界面与工具联动 | `$SKILL_DIR/references/mcp-apps.md` |
44
+ | 用 `mcp_call` 核对与调用、按错误定位、本地模式调试 | `$SKILL_DIR/references/debugging.md` |
45
+ | 用户要在自己的 Agent(Claude Code / Codex / 其它客户端)里连本应用的 MCP | `$SKILL_DIR/references/client-onboarding.md` |
45
46
 
46
- 前三类是改代码,改完必跑 `npx miaoda-mcp validate`,再按 `references/debugging.md` 的回路用 `mcp_call` 验证。`mcp_call` 报「当前运行环境不支持」是本地 CLI 宿主,走该文件的本地模式。向用户汇报用业务表述(「找到 4 个演示航班」),不贴 JSON。
47
+ 前三类是改代码,改完等 dev server 重启(定义有问题会在启动日志里报),再按 `$SKILL_DIR/references/debugging.md` 的回路用 `mcp_call` 验证。`mcp_call` 报「当前运行环境不支持」是本地 CLI 宿主,走该文件的本地模式。向用户汇报用业务表述(「找到 4 个演示航班」),不贴 JSON。
47
48
 
48
49
  ## 不变量
49
50
 
@@ -52,18 +53,18 @@ ls -d node_modules/@lark-apaas/nestjs-mcp node_modules/@lark-apaas/coding-preset
52
53
  3. `description` 面向模型:用途、何时调用、参数来源、返回什么、失败怎样。每个 zod 字段 `.describe()`。消费方 Agent 只靠这些选工具、填参数。
53
54
  4. 四项 `annotations` 显式写出。只读工具必须 `readOnlyHint: true`:缺省会被当成可能写数据,调试时每次都要 `safe: true`,消费方也会额外确认。
54
55
  5. 声明了 `outputSchema` 就返回匹配的 `structuredContent`,对不上时 `mcp_call` 报「输出与其声明的 outputSchema 不符」;列表工具入参带 `limit`(默认不超过 20)与 `cursor`;写操作的幂等由 Service 保证,不引入幂等键字段。
55
- 6. 返回值按消费方够用来裁:`structuredContent` 只放它推理或界面渲染要用的字段。SDK 不截断,返回多少对方就吃多少,详见 `references/tool-authoring.md` 的「返回体积」。
56
+ 6. 返回值按消费方够用来裁:`structuredContent` 只放它推理或界面渲染要用的字段。SDK 不截断,返回多少对方就吃多少,详见 `$SKILL_DIR/references/tool-authoring.md` 的「返回体积」。
56
57
  7. 业务失败抛 `McpToolError(message, { code })`,Agent 原样收到;`message` 连下一步一起写(「订单不存在,用 order_search 按客户名重查一个有效 ID」),只陈述现象会让对方原样重试或直接放弃。其他异常只落日志、返回通用提示,Agent 看不到原因。
57
58
  8. 身份只从 `ctx.user` 读;权限与行级过滤留在 Service。工具类作为业务模块 provider 注册,单例——请求态(当前用户、本次入参、游标)只放方法内的局部变量,写进类字段会在并发请求间串号。
58
59
  9. 工具名 = `prefix + 方法名`,匹配 `^[A-Za-z0-9_.-]{1,128}$`,全应用唯一;用 `域_动作`。
59
- 10. 界面源码只放 `server/mcp/ui/<entry>/`,类型检查走 `server/mcp/ui/tsconfig.json`,`tsconfig.node.json` 的 `exclude` 加 `server/mcp/ui`。用 `@McpUiResource` + `readMcpUiTemplate('<entry>')` 下发,工具用 `ui: { resourceUri }` 关联。
60
+ 10. 界面源码只放 `server/mcp/ui/<entry>/`,TS 与 ESLint 配置从 `$SKILL_DIR/assets/` 拷、不手写(见 `$SKILL_DIR/references/mcp-apps.md`)。用 `@McpUiResource` + `readMcpUiTemplate('<entry>')` 下发,工具用 `ui: { resourceUri }` 关联。
60
61
  11. 新增、改名、删除工具后同步改对应的 `server/mcp/skills/<name>/SKILL.md`;frontmatter 的 `name` 必须等于目录名。
61
62
 
62
63
  ## 禁止
63
64
 
64
65
  - 从入参接收用户 ID、token、连接串——身份来自网关,入参里的身份可伪造。
65
- - 把 `requireUser` 设为 `false`,或自己拼一个 `x-larkgw-suda-webuser` 头来让调用通过——SDK 会把它当真实用户,写工具会以这个身份落库、发通知。
66
- - 手改 `.spark/mcp/manifest.json`——`validate --check` 会判不一致。
66
+ - 自己拼一个 `x-larkgw-suda-webuser` 头来让调用通过——SDK 会把它当真实用户,写工具会以这个身份落库、发通知。
67
+ - 把 `requireUser` 设为 `false` 绕开身份检查——SDK 只是跳过检查,方法照样执行,`ctx.user.userId` 是 undefined,跟着 `ctx.user.userId!` 一路进 Service 的权限过滤。
67
68
  - 服务端 import `server/mcp/ui/` 里的源码,或界面 import `client/`、`server/` 里的业务代码、工具类、Node 模块——子构建的边界检查直接报错,服务端只经 `readMcpUiTemplate` 读构建产物。
68
69
  - 界面里存凭证、直连业务系统——沙箱 iframe 是独立 origin,用户登录态带不进来;数据走宿主转发的工具调用。
69
70
  - `McpToolError.data` 与 `structuredContent` 里放密钥、内部连接信息——两者原样进 Agent 上下文。
@@ -71,8 +72,7 @@ ls -d node_modules/@lark-apaas/nestjs-mcp node_modules/@lark-apaas/coding-preset
71
72
 
72
73
  ## 交付前核对
73
74
 
74
- 1. `validate` 通过;`list_tools` 与源码里的工具、资源一一对应。
75
+ 1. `list_tools` 与源码里的工具、资源一一对应。
75
76
  2. 每个工具有面向模型的 `description`、四项 `annotations`;写工具有 `outputSchema`。
76
- 3. 每个 `ui.resourceUri` 能 `read_resource` 读到,界面拿到 `structuredContent` 后能渲染;`npx tsc --noEmit -p server/mcp/ui/tsconfig.json` 通过。
77
+ 3. 每个 `ui.resourceUri` 能 `read_resource` 读到,界面拿到 `structuredContent` 后能渲染;`npx tsc --noEmit -p server/mcp/ui/tsconfig.json` 与 `npx eslint -c eslint.mcp-ui.config.cjs server/mcp/ui` 通过。
77
78
  4. `server/mcp/skills/` 下每个业务流程一份 SKILL.md,合起来覆盖全部工具:何时调用、如何选工具、调用顺序、约束、失败处理、界面 URI;目录接口的 `prompts[]` 与目录一一对应。
78
- 5. `.spark/mcp/manifest.json` 随代码提交。
@@ -0,0 +1,19 @@
1
+ const tseslint = require('typescript-eslint');
2
+ const globals = require('globals');
3
+ const hooks = require('eslint-plugin-react-hooks');
4
+ const { builtinModules } = require('node:module');
5
+
6
+ module.exports = tseslint.config({
7
+ files: ['server/mcp/ui/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}'],
8
+ extends: [...tseslint.configs.recommended],
9
+ languageOptions: { globals: globals.browser },
10
+ plugins: { 'react-hooks': hooks },
11
+ rules: {
12
+ ...hooks.configs.recommended.rules,
13
+ 'no-restricted-globals': ['error', 'process', 'Buffer', '__dirname', '__filename'],
14
+ 'no-restricted-imports': ['error', {
15
+ paths: [...new Set(builtinModules.flatMap(name => [name, `node:${name.replace(/^node:/, '')}`]))],
16
+ patterns: ['@client/*', '@server/*', '@/*', '**/client/**'],
17
+ }],
18
+ },
19
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "allowJs": true,
10
+ "checkJs": true,
11
+ "types": ["react", "react-dom"],
12
+ "skipLibCheck": true,
13
+ "noEmit": true,
14
+ "esModuleInterop": true,
15
+ "baseUrl": "../../..",
16
+ "paths": { "@shared/*": ["shared/*"] }
17
+ },
18
+ "include": ["./**/*.ts", "./**/*.tsx", "./**/*.js", "./**/*.jsx", "./**/*.mts", "./**/*.cts", "./**/*.mjs", "./**/*.cjs"]
19
+ }
@@ -7,7 +7,7 @@
7
7
  ## 取连接配置
8
8
 
9
9
  ```bash
10
- lark-cli apps +mcp-get --app-id <app_id> --include-secret
10
+ lark-cli apps +mcp-get --app-id "$app_id" --include-secret
11
11
  ```
12
12
 
13
13
  `data.config` 里是完整的 `mcpServers` 对象,含连接地址与 `X-Mcp-Token`。不加 `--include-secret` 拿到的是脱敏展示(`data.redacted=true`),不能用于连接。
@@ -34,25 +34,18 @@ lark-cli apps +mcp-get --app-id <app_id> --include-secret
34
34
  **Claude Code**
35
35
 
36
36
  ```bash
37
- claude mcp add --transport http <名字> <连接地址> --header "X-Mcp-Token: <凭证>" --header "x-tt-env: boe_miaoda_mcp"
37
+ claude mcp add --transport http <名字> <连接地址> --header "X-Mcp-Token: <凭证>"
38
38
  claude mcp login <名字>
39
39
  ```
40
40
 
41
- **Codex** — 编辑 `~/.codex/config.toml`
42
-
43
- ```toml
44
- [mcp_servers.<名字>]
45
- url = "<连接地址>"
46
- http_headers = { "X-Mcp-Token" = "<凭证>", "x-tt-env" = "boe_miaoda_mcp" }
47
- ```
41
+ **Codex**
48
42
 
49
43
  ```bash
44
+ codex mcp add <名字> --url "<连接地址>?x-mcp-token=<凭证>"
50
45
  codex mcp login <名字>
51
46
  ```
52
47
 
53
- Codex 侧同名 table 在 TOML 里出现两次是解析错误,已有同名配置要整块替换而不是追加;不要填 `bearer_token_env_var`,它会走静态 token 分支把 OAuth 跳过去。
54
-
55
- 两份模板里的 `x-tt-env` 照原样带上,不要改它的值,也不要就它询问用户。
48
+ `codex mcp add` 没有传请求头的参数,凭证走 query 参数。不要用 `--bearer-token-env-var`,它会走静态 token 分支把 OAuth 跳过去。
56
49
 
57
50
  ## 为什么是两条命令
58
51
 
@@ -80,7 +73,6 @@ Codex 侧同名 table 在 TOML 里出现两次是解析错误,已有同名配
80
73
  | 401,无 reason | 凭证复制不全,或配的是预览态凭证 | 重新 `+mcp-get --include-secret` 取一次,整段替换 |
81
74
  | 403 `user is not in the visible scope of this app` | 不在可用范围,或授权账号不属于应用租户 | 前者找负责人放开范围;后者换应用租户内的账号授权 |
82
75
  | 404 | 应用没启用 MCP;或零工具零资源无应用 Skill(SDK 此时也返回 404) | 后者是正常状态不是故障,先确认本应用确实注册了工具 |
83
- | 连接超时 / 长时间挂起 | 多为泳道头指向了不存在或未部署的泳道 | 去掉 `x-tt-env` 再试一次可快速定位 |
84
76
  | POST 被拒,CSRF 相关 | 旧模板应用对 POST 有 CSRF 双提交校验 | 平台侧已知未收口项,告知用户,不在客户端侧拼 cookie 绕 |
85
77
 
86
78
  ## 凭证纪律
@@ -22,13 +22,12 @@
22
22
  ## 回路
23
23
 
24
24
  1. 改工具 / 资源 / 界面。
25
- 2. `npx miaoda-mcp validate`。失败即改,不进下一步。
26
- 3. 等 dev server 重启(`nest --watch` 会自动重启;界面改动由 `[mcp-ui]` 插件自动重建)。
27
- 4. `list_tools` 核对清单与源码一致(`visibility: ['app']` 的工具也在清单里,属正常)。
28
- 5. `call` 逐个只读工具跑一次真实入参。写工具只在能自己清掉痕迹(有对应删除 / 撤销工具,或业务上可撤回)时才真调并随手清理;否则只验证入参校验与 `McpToolError` 分支,汇报里写明「写入链路未真实验证」。
29
- 6. 带界面的工具 `read_resource`。
30
- 7. 每个 `McpToolError` 分支构造一次触发(不存在的 ID、非法状态),确认 `isError: true` 且 `code` 与 `description` 预告一致。
31
- 8. 更新 `server/mcp/skills/<name>/SKILL.md`,`validate` 通过(frontmatter 不合法在这一步就失败),再 curl 目录接口:`skills[]` 有该 `name`、`content` 是最新正文,`prompts[]` 的 `description` 一致。
25
+ 2. 等 dev server 重启(`nest --watch` 会自动重启;界面改动由 `[mcp-ui]` 插件自动重建)。定义有问题会让应用起不来并打印「MCP 定义校验失败」,修完再往下走。
26
+ 3. `list_tools` 核对清单与源码一致(`visibility: ['app']` 的工具也在清单里,属正常)。
27
+ 4. `call` 逐个只读工具跑一次真实入参。写工具只在能自己清掉痕迹(有对应删除 / 撤销工具,或业务上可撤回)时才真调并随手清理;否则只验证入参校验与 `McpToolError` 分支,汇报里写明「写入链路未真实验证」。
28
+ 5. 带界面的工具 `read_resource`。
29
+ 6. 每个 `McpToolError` 分支构造一次触发(不存在的 ID、非法状态),确认 `isError: true` 且 `code` 与 `description` 预告一致。
30
+ 7. 更新 `server/mcp/skills/<name>/SKILL.md`,再 curl 目录接口(frontmatter 不合法这里会 500):`skills[]` 有该 `name`、`content` 是最新正文,`prompts[]` 的 `description` 一致。
32
31
 
33
32
  ## 错误分流
34
33
 
@@ -37,7 +36,7 @@
37
36
  | 端点 404「当前工程可能尚未接入 MCP 层,或 dev server 尚未就绪」 | `server/mcp` 不存在;dev server 没起来;零工具零资源无应用 Skill(SDK 此时返回 404) | 确认三者;有工具但 404 看启动日志是否被校验阻断 |
38
37
  | `list_tools` 少了某个工具 | 类没进任何模块 `providers`,无任何日志提示 | 对照启动日志「已注册 N 个 MCP 工具:…」,缺的加进所属业务模块的 `providers` |
39
38
  | dev server 起不来,报「MCP 工具必须使用单例及单例依赖」 | 工具类或其依赖是 request / transient 作用域 | 改回单例,身份从 `ctx.user` 取 |
40
- | 启动日志「MCP 定义校验失败」 | 工具名不合法 / 重名、资源 URI 不以 `ui://` 开头 / 重名、`ui.resourceUri` 找不到 `@McpUiResource()`、同一方法同时标两个装饰器 | 按日志逐条修,`validate` 能提前查出 |
39
+ | 启动日志「MCP 定义校验失败」 | 工具名不合法 / 重名、资源 URI 不以 `ui://` 开头 / 重名、`ui.resourceUri` 找不到 `@McpUiResource()`、同一方法同时标两个装饰器 | 按日志逐条列的问题逐个修 |
41
40
  | `tool_not_found` | 名字没带 prefix,或用了 `title` | 用 `list_tools` 里的 name |
42
41
  | `requires_safe` | 见上文 `safe` | 只读工具补注解;写工具按回路第 5 步——能清掉痕迹才 `safe: true`,否则只验错误分支 |
43
42
  | `isError` + `code: MCP_USER_REQUIRED`(`prompts/get` / `resources/read` 则是 JSON-RPC error) | 请求没带用户身份:有 `mcp_call` 的环境里 curl 直连 dev server / NestJS 端口就是这样 | 无身份的 curl 只做 `tools/list`、`prompts/list`、`resources/list` 与目录接口;`tools/call` / `resources/read` 走 `mcp_call`,Prompt 正文看目录接口的 `skills[].content`。不改 `requireUser`、不拼身份头 |
@@ -47,12 +46,13 @@
47
46
  | 「工具已执行,但输出与其声明的 outputSchema 不符」 | `structuredContent` 与 `outputSchema` 对不上 | 工具已跑通:改返回值或改 schema,不重试同一调用 |
48
47
  | `timeout` | 单个子请求 60s 无动静,或整次超 600s | 收窄入参 / 分页 / 拆成「发起 + 查状态」两个工具;加进度通知无效(JSON 模式发不出去) |
49
48
  | `connect_failed` / `http_error` / `unauthorized` | 网关或预览环境问题;`http_error` 500 先对下面 frontmatter 那行,应用自己也会 500 | 看 statusCode;不是应用代码问题时告知用户,不改代码绕 |
49
+ | `jsonrpc_error` + `data.code: MCP_APP_REVISION_MISMATCH` | 调用方带的应用修订与当前产物不一致(改代码、改界面、改应用 Skill、dev server 重启都会变),**本次业务未执行** | 重新 `list_tools` 取最新清单再调;不要原样重试,也不要把这当业务失败去改工具代码 |
50
+ | `jsonrpc_error` + `data.code: MCP_APP_REVISION_UNAVAILABLE` | 产物是旧版构建、不带修订,而调用方带了预期修订 | 同步新版构建脚本后重新发布;在那之前不要启用依赖修订的绑定 |
50
51
  | 「已连续 4 次调用失败,已暂停对它的重复调用」 | 同一工具连续失败 4 次熔断 | 先修实现或入参,60s 后再验 |
51
52
  | 「当前运行环境不支持开发态 MCP 调试」 | 本地 CLI 宿主没有 `mcp_call` 后端 | 走下节本地模式 |
52
53
  | `read_resource` 报「读取 MCP Apps 界面产物失败」 | `dist/mcp-ui/<entry>.html` 不存在 | 入口名对齐 `readMcpUiTemplate('<entry>')`;确认 `server/mcp/ui/<entry>/index.html` 存在;看 `[mcp-ui]` 日志是否构建失败 |
53
54
  | dev server 日志 `[mcp-ui] 构建失败:MCP UI 不能引用应用或服务端代码 / Node.js 模块` | 界面 import 了 `client/`、`server/` 的业务代码或 Node 内置模块;HTML `src`、CSS `url()` 指到界面目录外也算 | 把依赖挪进 `server/mcp/ui/` 或 `shared/`;不改预设配置 |
54
- | dev server(`nest start --watch`)日志或 `npm run type:check:server` 在 `server/mcp/ui/**` 报 JSX / DOM 类型错误 | `tsconfig.node.json` 的 `exclude` 没加 `server/mcp/ui`,`nest` 与 type:check 共用这份配置 | 加上;界面用 `npx tsc --noEmit -p server/mcp/ui/tsconfig.json` 单独检查 |
55
- | `mcp_call` 任一动作 `http_error` 500、目录接口 500,启动日志有「写入 MCP 清单失败:MCP Skill …」 | 某份 `server/mcp/skills/*/SKILL.md` frontmatter 不合法:缺 frontmatter、`name` 与目录名不等、`description` 空、重名、超限 | 按日志文案修 frontmatter;`validate` 会报同一条错 |
55
+ | `mcp_call` 任一动作 `http_error` 500、目录接口 500,server 日志有「处理 MCP 请求失败」、栈首行是「MCP Skill …」 | 某份 `server/mcp/skills/*/SKILL.md` frontmatter 不合法:缺 frontmatter、`name` 与目录名不等、`description` 空、重名、超限 | 按日志文案修 frontmatter |
56
56
 
57
57
  同一失败不原样重试。改完代码再验,每轮只改一个变量。
58
58
 
@@ -2,23 +2,22 @@
2
2
 
3
3
  一个 MCP App = 一个 `ui://` 资源(单文件 HTML)+ 至少一个用 `ui.resourceUri` 指向它的工具。宿主调用工具后把 HTML 放进沙箱 iframe 渲染,再把工具入参与结果通过通知推给界面;界面里的操作经宿主回调 server 的工具。
4
4
 
5
- ## 先决定要不要界面
5
+ ## 选哪种界面
6
6
 
7
- | 信号 | 做法 |
7
+ | 信号 | 界面类型 |
8
8
  |---|---|
9
- | 结果是列表 / 详情 / 状态,文本 + `structuredContent` 已够 Agent 继续推理 | 不加界面 |
10
- | 用户要从多条候选里挑一个(航班、订单、文件) | 选择型界面,选中后 `updateModelContext` 同步给模型 |
11
- | 结果是图表、地图、对比等空间信息 | 展示型界面 |
12
- | 写操作前要让用户在界面里核对、确认 | 确认型界面,确认动作经 `callServerTool` 调一个 `visibility: ['app']` 的工具 |
13
- | 工具要用户补一组结构化输入(多字段表单、从长列表里挑) | 表单型界面。服务端中途问不了用户(没有 elicitation),只有模型和界面能问 |
14
-
15
- 不需要的地方不加:文本更快,也没有构建与 CSP 负担。
9
+ | 工具已返回候选,要用户挑一个或几个再继续(航班、订单、文件) | 选择型,选中后 `updateModelContext` 同步给模型 |
10
+ | 结果是图表、地图、对比、文件预览等看图才懂的信息 | 展示型 |
11
+ | 写操作前要让用户在界面里核对、确认 | 确认型,确认动作经 `callServerTool` 调一个 `visibility: ['app']` 的工具 |
12
+ | 工具跑之前要用户补一组结构化输入(多字段表单、长列表里选参数) | 表单型。服务端中途问不了用户(没有 elicitation),只有模型和界面能问 |
13
+ | 长任务要用户看着进展 | 进度型:发起工具绑界面并返回任务 ID,界面轮询 `visibility: ['app']` 的查状态工具。进度通知在无状态 JSON 模式下发不出去,界面是唯一的路 |
14
+ | 以上都不匹配 | 不做界面 |
16
15
 
17
16
  ## 目录与构建
18
17
 
19
18
  ```text
20
19
  server/mcp/ui/
21
- ├── tsconfig.json # 界面独立的 TS 配置,整个 ui/ 共用一份,见下
20
+ ├── tsconfig.json # 界面独立的 TS 配置,整个 ui/ 共用一份,从本 skill 的 assets/ 拷
22
21
  └── <entry>/
23
22
  ├── index.html # 入口骨架,见下
24
23
  └── main.tsx # 或 main.ts;React 或原生 JS 都行
@@ -32,31 +31,19 @@ dist/mcp-ui/<entry>.html # 预设产物,@McpUiResource 经 readMcpUiTemp
32
31
  <body><div id="root"></div><script type="module" src="./main.tsx"></script></body></html>
33
32
  ```
34
33
 
35
- `<div id="root">` 不能少:缺了 `createRoot(document.getElementById('root')!)` 拿到 null,iframe 全白,而 `validate` 与 `read_resource` 都不会报错。
34
+ `<div id="root">` 不能少:缺了 `createRoot(document.getElementById('root')!)` 拿到 null,iframe 全白,而 `read_resource` 照样能读到 HTML,不会报错。
36
35
 
37
36
  - `<entry>` 只允许 `A-Z a-z 0-9 _ -`。
38
37
  - `@lark-apaas/coding-preset-vite-react` 的 `mcp-ui` 插件:检测到 `server/mcp/ui/` 就为每个入口跑一次独立子构建,JS / CSS / 资源全部内联成一个 HTML。开发态监听 `server/mcp/ui/` 与 `shared/`,改完自动重建;生产在主构建之后构建,并同步进发布目录。
39
38
  - 子构建只带 React 插件与 `@shared` 别名,不加载主应用的 `vite.config.ts`、`.env`、`public/` 与 PostCSS 配置:`@/`、`@client/*`、`@server/*` 解析不到,不支持 styled-jsx、Tailwind 与主应用全局样式。样式写在入口自己的 CSS 里;颜色字体优先读宿主下发的 CSS 变量(`useHostStyles`),深浅两套主题下都要能看。
40
39
  - 依赖边界由子构建强制:界面只能 import `server/mcp/ui/` 内的文件、`shared/` 与浏览器可用的 npm 包。引到 `client/`、`server/` 里的业务代码、工具类或服务端实现报「MCP UI 不能引用应用或服务端代码」,引到 `fs`、`path` 等报「MCP UI 不能引用 Node.js 模块」;HTML 的 `src` / `href`、CSS 的 `url()` 指到界面目录外同样被拒。要复用的纯展示组件挪到 `shared/`,且不得依赖应用的 Provider、Router 或登录态。
41
- - 类型检查单独走:`server/mcp/ui/` 既不在 `tsconfig.app.json`(只含 `client/`、`shared/`)里,也不能进 `tsconfig.node.json`(服务端配置编译 JSX 报错)。首次开通界面做两件事:`tsconfig.node.json` 的 `exclude` 追加 `"server/mcp/ui"`;生成下面这份 `server/mcp/ui/tsconfig.json`。之后每次改界面跑 `npx tsc --noEmit -p server/mcp/ui/tsconfig.json`,`npm run type:check` 不覆盖它。
42
-
43
- ```json
44
- {
45
- "compilerOptions": {
46
- "target": "ES2022",
47
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
48
- "module": "ESNext",
49
- "moduleResolution": "Bundler",
50
- "jsx": "react-jsx",
51
- "strict": true,
52
- "skipLibCheck": true,
53
- "noEmit": true,
54
- "esModuleInterop": true,
55
- "baseUrl": "../../..",
56
- "paths": { "@shared/*": ["shared/*"] }
57
- },
58
- "include": ["./**/*.ts", "./**/*.tsx"]
59
- }
40
+ - 界面不继承主应用的 TS / ESLint 配置,各自带一份,首次开通界面从本 skill 的 `assets/` 拷进工程。逐字照拷、别自己写:子构建只转译不查类型,配置漏一项,错误要等界面在 iframe 里白屏才暴露。`SKILL_DIR` 取技能清单里本 skill 的 location 所在目录:
41
+
42
+ ```bash
43
+ SKILL_DIR=<本 skill SKILL.md 所在目录>
44
+ mkdir -p server/mcp/ui
45
+ cp "$SKILL_DIR/assets/ui-tsconfig.json" server/mcp/ui/tsconfig.json
46
+ cp "$SKILL_DIR/assets/eslint.mcp-ui.config.cjs" eslint.mcp-ui.config.cjs
60
47
  ```
61
48
 
62
49
  - `@modelcontextprotocol/ext-apps` 随 SDK 安装,直接 import。解析失败时把它加进 `package.json` `dependencies`,版本与 `node_modules/@lark-apaas/nestjs-mcp/package.json` 里声明的一致(1.x),不装 2.x。
@@ -99,7 +86,7 @@ export class OrderMcpTools {
99
86
  }
100
87
  ```
101
88
 
102
- - `uri` 必须以 `ui://` 开头,全应用唯一;`ui.resourceUri` 找不到对应 `@McpUiResource()` 时 `validate` 与启动都会报错。
89
+ - `uri` 必须以 `ui://` 开头,全应用唯一;`ui.resourceUri` 找不到对应 `@McpUiResource()` 时应用启动会报错。
103
90
  - `visibility` 默认 `['model', 'app']`。只给界面用的取数 / 刷新工具写 `['app']`,避免污染模型的工具清单。隐藏由宿主做,server 不过滤:`mcp_call` 的 `list_tools` 仍会列出它,`_meta.ui.visibility` 为 `['app']` 即正确,不要因为「还在清单里」去改代码。
104
91
  - 绑了界面就在 `description` 里说出来(「结果以可交互的订单列表展示,可在界面里展开详情、取消订单」)。模型只看 `description` 选工具,不写它可能改调别的工具或自己拼 ID。
105
92
  - 一个工具既返回 `structuredContent` 又绑界面是常态;模型还需要纯数据继续推理时,拆成「取数工具(无 ui)」+「展示工具(绑 ui)」。
@@ -179,7 +166,7 @@ app.ontoolresult = (result) => render(result.structuredContent);
179
166
  await app.connect();
180
167
  ```
181
168
 
182
- `ontoolresult` 等回调必须在 `connect()` 之前挂好——`useApp` 用 `onAppCreated` 注册就是为此,原生写法先赋值再 `connect()`。连上之后再赋值会漏掉宿主推来的第一帧结果,界面停在「等待查询结果…」且不报错。
169
+ `ontoolresult` 等回调必须在 `connect()` 之前挂好——`useApp` 用 `onAppCreated` 注册就是为此,原生写法先赋值再 `connect()`。连上之后再赋值会漏掉宿主推来的第一帧结果,界面停在「等待查询结果…」——页面本身不报错,但 ext-apps 会在控制台 warn 一句 handler 注册太晚。
183
170
 
184
171
  宿主能力速查(`App` 实例方法):
185
172
 
@@ -14,6 +14,10 @@ API 形状以 `node_modules/@lark-apaas/nestjs-mcp/dist/index.d.ts` 为准。下
14
14
 
15
15
  用户没说清要开放哪些能力、给谁用时,先问清再写,不要把 Service 层能做的全铺出去。
16
16
 
17
+ ## 要不要界面
18
+
19
+ 结果要用户动手挑、看、或确认的工具才配界面;只是给模型继续推理的数据不配。要配的话,界面类型与写法见 `mcp-apps.md`。
20
+
17
21
  ## 骨架
18
22
 
19
23
  ```typescript
@@ -29,7 +33,7 @@ import {
29
33
  } from '@lark-apaas/fullstack-nestjs-core';
30
34
  import { OrderService } from '@server/modules/order/order.service';
31
35
 
32
- // schema 提成常量:方法签名用 Infer<typeof …> 引用,清单导出也从这里取
36
+ // schema 提成常量:方法签名用 Infer<typeof …> 引用
33
37
  export const SearchOrdersInput = {
34
38
  keyword: z.string().optional().describe('客户名关键字,模糊匹配'),
35
39
  limit: z.number().int().min(1).max(50).default(20).describe('单页条数'),
@@ -69,7 +73,11 @@ export class OrderMcpTools {
69
73
  const userId = ctx.user.userId!; // requireUser 默认开启,执行到这里必有值;取一次往下传
70
74
  const page = await this.orders.search({ ...input, userId });
71
75
  return {
72
- structuredContent: page,
76
+ // 按 outputSchema 挑字段,不把 Service 查出来的整行记录原样回传
77
+ structuredContent: {
78
+ items: page.items.map((o) => ({ id: o.id, customer: o.customer, amount: o.amount, status: o.status })),
79
+ nextCursor: page.nextCursor,
80
+ },
73
81
  content: [{ type: 'text', text: `找到 ${page.items.length} 条订单` }],
74
82
  };
75
83
  }
@@ -185,18 +193,17 @@ return {
185
193
  ## 开通(首次)
186
194
 
187
195
  1. 前置检查通过(见 SKILL.md)。
188
- 2. 按用途定 `serverName`(见下方「模块配置」),不要留缺省。
189
- 3. 建 `server/mcp/tools/<域>.tools.ts`,至少一个只读工具复用现有 Service。
190
- 4. 把工具类加进所属业务模块的 `providers`。
191
- 5. 写 `server/mcp/skills/<name>/SKILL.md`(下节)。
192
- 6. `npx miaoda-mcp validate` 通过,`.spark/mcp/manifest.json` 一起提交。
193
- 7. 进 `debugging.md` 的调试回路。
196
+ 2. 建 `server/mcp/tools/<域>.tools.ts`,至少一个只读工具复用现有 Service。
197
+ 3. 把工具类加进所属业务模块的 `providers`。
198
+ 4. 写 `server/mcp/skills/<name>/SKILL.md`(下节)。
199
+ 5. 跑 `lark-cli apps +mcp-key-create --app-id "$app_id"` 备好运行态凭证。
200
+ 6. 进 `debugging.md` 的调试回路。
194
201
 
195
202
  ## 应用 Skill:`server/mcp/skills/<name>/SKILL.md`
196
203
 
197
204
  给消费方 Agent 的使用说明。一个业务流程一个目录,目录里只认固定文件名 `SKILL.md`。SDK 把每份注册为同名 MCP Prompt:`prompts/list` 给 `name` / `description`,`prompts/get` 返回去掉 frontmatter 的正文(一条 `role: user` 文本消息)。它不是 Resource、没有 `skill://` URI,`resources/list` 里看不到它是正常的。每次请求重新读文件,改完不用重启。
198
205
 
199
- frontmatter 规则。SDK 每次请求、`validate` 都会校验,不合法时整个 MCP 请求 500、`validate` 失败,不会静默跳过这一份:
206
+ frontmatter 规则。SDK 每次请求都会校验,不合法时整个 MCP 请求 500,不会静默跳过这一份:
200
207
 
201
208
  - `name` 必填且等于目录名:1–64 位小写字母、数字与单个连字符,不能首尾连字符、不能连续 `--`。
202
209
  - `description` 必填非空,不超过 1024 字符;消费方只靠它决定要不要取这份 Prompt,写清适用场景。
@@ -220,12 +227,9 @@ description: 查询当前用户可见的订单,并在用户明确要求后取
220
227
  - 支持界面的宿主会用 ui://order/list 展示查询结果。
221
228
  ```
222
229
 
223
- ## 校验与清单
230
+ ## 定义校验
224
231
 
225
- - `npx miaoda-mcp validate`:用工程自身 TypeScript 加载 `server/mcp` 下的工具源码(不启动应用,跳过 `ui/` 与 `skills/`),检查工具名格式、重名、`ui://` 前缀、`ui.resourceUri` 引用、每份应用 Skill 的 frontmatter,写入 `.spark/mcp/manifest.json`(`tools` / `resources` / `prompts` / `skills` 与源码定位)。
226
- - `npx miaoda-mcp validate --check`:只比对不写入,CI 用。
227
- - 开发态启动应用也会写清单;生产不写。删光工具、资源与应用 Skill 后再跑一次 `validate` 清理旧清单。
228
- - 启动期同样的校验失败会阻断启动(「MCP 定义校验失败」),一个方法不能同时标 `@McpTool()` 与 `@McpUiResource()`。
232
+ 工具名格式与重名、资源 URI 的 `ui://` 前缀与重名、`ui.resourceUri` 能否找到对应的 `@McpUiResource()`——这些在应用启动时统一校验,失败直接阻断启动并打印「MCP 定义校验失败」,逐条列出问题与出错的类和方法。一个方法不能同时标 `@McpTool()` 与 `@McpUiResource()`。改完等 dev server 重启看日志即可,没有单独的离线校验命令。
229
233
 
230
234
  ## 模块配置
231
235
 
@@ -240,6 +244,6 @@ PlatformModule.forRoot({
240
244
  // 关闭端点:PlatformModule.forRoot({ mcp: false })
241
245
  ```
242
246
 
243
- **`serverName` 首次开通 MCP 时就按用途显式设置。** 这个名字不只在开发态出现——`initialize` 把它返回给每个连上来的外部 Agent,`list_tools` 显示它,用户在自己客户端里也按它区分接进来的多个 MCP。取材于本 MCP 做什么(差旅审批 → `travel-approval`,库存查询 → `inventory`),小写字母 + 数字 + `-`/`_`,两三个词以内。改名会让已接入的客户端侧名字对不上,所以一次起对,后续别动。
247
+ **`serverName` 首次开通时就设上。** `initialize` 把它返回给连上来的外部 Agent,`mcp_call` 的 `list_tools` 也显示它;不设则是 `miaoda-app`,谁看都不知道是哪个应用。取材于本 MCP 做什么(差旅审批 → `travel-approval`,库存查询 → `inventory`),小写字母 + 数字 + `-`/`_`,两三个词以内。
244
248
 
245
249
  其余项一般不动。`requireUser` 保持默认 `true`。零工具、零资源、无应用 Skill 时端点返回 404,这是正常状态不是故障。