@josephyan/qingflow-app-user-mcp 0.2.0-beta.1 → 0.2.0-beta.1000

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.
Files changed (94) hide show
  1. package/README.md +18 -2
  2. package/docs/local-agent-install.md +95 -14
  3. package/npm/bin/qingflow-app-user-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +212 -12
  5. package/npm/scripts/postinstall.mjs +5 -1
  6. package/package.json +3 -2
  7. package/pyproject.toml +4 -1
  8. package/skills/qingflow-app-user/SKILL.md +88 -0
  9. package/skills/qingflow-app-user/agents/openai.yaml +4 -0
  10. package/skills/qingflow-app-user/references/data-gotchas.md +29 -0
  11. package/skills/qingflow-app-user/references/environments.md +63 -0
  12. package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
  13. package/skills/qingflow-app-user/references/record-patterns.md +48 -0
  14. package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
  15. package/skills/qingflow-record-analysis/SKILL.md +200 -0
  16. package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
  17. package/skills/qingflow-record-analysis/references/analysis-gotchas.md +145 -0
  18. package/skills/qingflow-record-analysis/references/analysis-patterns.md +125 -0
  19. package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
  20. package/skills/qingflow-record-analysis/references/dsl-templates.md +93 -0
  21. package/skills/qingflow-record-delete/SKILL.md +29 -0
  22. package/skills/qingflow-record-import/SKILL.md +31 -0
  23. package/skills/qingflow-record-insert/SKILL.md +61 -0
  24. package/skills/qingflow-record-update/SKILL.md +45 -0
  25. package/skills/qingflow-task-ops/SKILL.md +144 -0
  26. package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
  27. package/skills/qingflow-task-ops/references/environments.md +44 -0
  28. package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
  29. package/src/qingflow_mcp/__init__.py +33 -1
  30. package/src/qingflow_mcp/backend_client.py +313 -0
  31. package/src/qingflow_mcp/builder_facade/models.py +1511 -11
  32. package/src/qingflow_mcp/builder_facade/service.py +15564 -1793
  33. package/src/qingflow_mcp/cli/__init__.py +1 -0
  34. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  35. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  36. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  37. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  38. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  39. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  40. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  41. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  42. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  43. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  44. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  45. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  46. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  47. package/src/qingflow_mcp/cli/context.py +60 -0
  48. package/src/qingflow_mcp/cli/formatters.py +573 -0
  49. package/src/qingflow_mcp/cli/json_io.py +50 -0
  50. package/src/qingflow_mcp/cli/main.py +186 -0
  51. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  52. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  53. package/src/qingflow_mcp/config.py +225 -0
  54. package/src/qingflow_mcp/errors.py +2 -2
  55. package/src/qingflow_mcp/id_utils.py +49 -0
  56. package/src/qingflow_mcp/import_store.py +121 -0
  57. package/src/qingflow_mcp/list_type_labels.py +24 -0
  58. package/src/qingflow_mcp/public_surface.py +243 -0
  59. package/src/qingflow_mcp/repository_store.py +71 -0
  60. package/src/qingflow_mcp/response_trim.py +841 -0
  61. package/src/qingflow_mcp/server.py +164 -18
  62. package/src/qingflow_mcp/server_app_builder.py +344 -119
  63. package/src/qingflow_mcp/server_app_user.py +269 -217
  64. package/src/qingflow_mcp/session_store.py +141 -21
  65. package/src/qingflow_mcp/solution/compiler/form_compiler.py +44 -5
  66. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  67. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  68. package/src/qingflow_mcp/solution/executor.py +109 -13
  69. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +3047 -171
  71. package/src/qingflow_mcp/tools/app_tools.py +507 -15
  72. package/src/qingflow_mcp/tools/approval_tools.py +641 -77
  73. package/src/qingflow_mcp/tools/auth_tools.py +823 -248
  74. package/src/qingflow_mcp/tools/base.py +204 -4
  75. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  76. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  77. package/src/qingflow_mcp/tools/directory_tools.py +234 -35
  78. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  79. package/src/qingflow_mcp/tools/file_tools.py +26 -1
  80. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  81. package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
  82. package/src/qingflow_mcp/tools/package_tools.py +104 -5
  83. package/src/qingflow_mcp/tools/portal_tools.py +59 -1
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +147 -8
  85. package/src/qingflow_mcp/tools/record_tools.py +11161 -1177
  86. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  87. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  88. package/src/qingflow_mcp/tools/role_tools.py +19 -1
  89. package/src/qingflow_mcp/tools/solution_tools.py +171 -4
  90. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  91. package/src/qingflow_mcp/tools/task_tools.py +426 -229
  92. package/src/qingflow_mcp/tools/view_tools.py +56 -1
  93. package/src/qingflow_mcp/tools/workflow_tools.py +143 -5
  94. package/src/qingflow_mcp/tools/workspace_tools.py +141 -62
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.1
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.1000
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.1 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.1000 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
@@ -19,3 +19,19 @@ Environment:
19
19
  - `QINGFLOW_MCP_HOME`
20
20
 
21
21
  This package bootstraps a local Python runtime on first install and then starts the `qingflow-app-user-mcp` stdio MCP server.
22
+
23
+ Bundled skills:
24
+
25
+ - `skills/qingflow-app-user`
26
+ - `skills/qingflow-record-insert`
27
+ - `skills/qingflow-record-update`
28
+ - `skills/qingflow-record-delete`
29
+ - `skills/qingflow-record-import`
30
+ - `skills/qingflow-task-ops`
31
+ - `skills/qingflow-record-analysis`
32
+
33
+ Note:
34
+
35
+ - The skill files are included in the npm package.
36
+ - On install, the package copies them to `$CODEX_HOME/skills` (or `~/.codex/skills` if `CODEX_HOME` is unset).
37
+ - If a stdio MCP client reports `Transport closed`, delete `.npm-python`, reinstall the package, and make sure CLI/user/builder packages are on the same version. The stdio entrypoints refuse runtime bootstrap so install logs never corrupt MCP stdout.
@@ -1,9 +1,30 @@
1
1
  # 本地智能体安装
2
2
 
3
- 这个目录下现在只保留两个本地命令入口:
3
+ 这个目录下现在有三个本地命令入口:
4
4
 
5
- 1. 记录/待办优先的 `qingflow-app-user-mcp`
6
- 2. 精简 builder 的 `qingflow-app-builder-mcp`
5
+ 1. 统一 CLI:`qingflow`
6
+ 2. 记录/待办优先的 `qingflow-app-user-mcp`
7
+ 3. 精简 builder 的 `qingflow-app-builder-mcp`
8
+
9
+ ## 本地鉴权推荐方案
10
+
11
+ 本地模式现在推荐优先使用 `credential` 建立会话,而不是直接注入 `token`。
12
+
13
+ 推荐链路:
14
+
15
+ 1. createClaw 或其它本地宿主为当前实例保存 `credential`
16
+ 2. 本地 MCP 调用 `auth_use_credential`
17
+ 3. MCP 用该 `credential` 请求 apaas `/mcp/auth/context`
18
+ 4. 解析并保存 `token / wsId / qfVersion / uid`
19
+ 5. 业务工具直接使用这份上下文
20
+
21
+ `auth_use_credential` 是本地唯一鉴权主路径。
22
+
23
+ 补充说明:
24
+
25
+ - 对 stdio MCP 来说,主路径仍然只有 `auth_use_credential`。
26
+ - 如果你是在终端里直接使用 `qingflow` CLI,可以额外使用 `qingflow auth login` 作为“人类登录”入口;默认会提示轻流邮箱和隐藏密码,拿到 `token` 后建立本地 CLI 会话。
27
+ - 也就是说,这次新增的是 CLI 的登录入口,不是给 MCP 增加第二套会话模型。
7
28
 
8
29
  ## npm 安装器适用场景
9
30
 
@@ -62,15 +83,17 @@ npm run pack:npm
62
83
  会生成:
63
84
 
64
85
  ```bash
65
- dist/npm/josephyan-qingflow-app-user-mcp-<version>.tgz
66
- dist/npm/josephyan-qingflow-app-builder-mcp-<version>.tgz
86
+ dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
87
+ dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
88
+ dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
67
89
  ```
68
90
 
69
91
  然后在目标机器安装:
70
92
 
71
93
  ```bash
72
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-user-mcp-<version>.tgz
73
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-builder-mcp-<version>.tgz
94
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
95
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
96
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
74
97
  ```
75
98
 
76
99
  安装时会自动:
@@ -78,7 +101,7 @@ npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-builder-mcp-<versi
78
101
  1. 创建 `.npm-python/`
79
102
  2. 在其中建立 Python 虚拟环境
80
103
  3. 执行 `pip install .`
81
- 4. 在安装位置暴露 `qingflow-app-user-mcp`、`qingflow-app-builder-mcp` 命令
104
+ 4. 在安装位置暴露 `qingflow`、`qingflow-app-user-mcp`、`qingflow-app-builder-mcp` 命令
82
105
 
83
106
  ## 本地验证
84
107
 
@@ -86,6 +109,7 @@ npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-builder-mcp-<versi
86
109
 
87
110
  ```bash
88
111
  cd qingflow-support/mcp-server
112
+ node ./npm/bin/qingflow.mjs --help
89
113
  node ./npm/bin/qingflow-app-user-mcp.mjs
90
114
  node ./npm/bin/qingflow-app-builder-mcp.mjs
91
115
  ```
@@ -93,6 +117,7 @@ node ./npm/bin/qingflow-app-builder-mcp.mjs
93
117
  如果你是全局安装:
94
118
 
95
119
  ```bash
120
+ qingflow --help
96
121
  qingflow-app-user-mcp
97
122
  qingflow-app-builder-mcp
98
123
  ```
@@ -100,6 +125,7 @@ qingflow-app-builder-mcp
100
125
  如果你是把包安装到了某个本地 agent workspace,命令通常位于:
101
126
 
102
127
  ```bash
128
+ /absolute/path/to/agent-workspace/node_modules/.bin/qingflow
103
129
  /absolute/path/to/agent-workspace/node_modules/.bin/qingflow-app-user-mcp
104
130
  /absolute/path/to/agent-workspace/node_modules/.bin/qingflow-app-builder-mcp
105
131
  ```
@@ -107,6 +133,7 @@ qingflow-app-builder-mcp
107
133
  如果你是从 tgz 安装到某个空目录,命令通常位于:
108
134
 
109
135
  ```bash
136
+ /absolute/path/to/install-dir/node_modules/.bin/qingflow
110
137
  /absolute/path/to/install-dir/node_modules/.bin/qingflow-app-user-mcp
111
138
  /absolute/path/to/install-dir/node_modules/.bin/qingflow-app-builder-mcp
112
139
  ```
@@ -129,7 +156,10 @@ qingflow-app-builder-mcp
129
156
  ],
130
157
  "env": {
131
158
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
132
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
159
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
160
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
161
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
162
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
133
163
  }
134
164
  }
135
165
  }
@@ -146,7 +176,10 @@ qingflow-app-builder-mcp
146
176
  "args": [],
147
177
  "env": {
148
178
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
149
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
179
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
180
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
181
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
182
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
150
183
  }
151
184
  }
152
185
  }
@@ -163,7 +196,10 @@ qingflow-app-builder-mcp
163
196
  "args": [],
164
197
  "env": {
165
198
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
166
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
199
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
200
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
201
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
202
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
167
203
  }
168
204
  }
169
205
  }
@@ -184,7 +220,10 @@ qingflow-app-builder-mcp
184
220
  "@josephyan/qingflow-app-user-mcp"
185
221
  ],
186
222
  "env": {
187
- "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
223
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
224
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
225
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
226
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
188
227
  }
189
228
  },
190
229
  "qingflow-builder": {
@@ -194,7 +233,10 @@ qingflow-app-builder-mcp
194
233
  "@josephyan/qingflow-app-builder-mcp"
195
234
  ],
196
235
  "env": {
197
- "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
236
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
237
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
238
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
239
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
198
240
  }
199
241
  }
200
242
  }
@@ -202,9 +244,10 @@ qingflow-app-builder-mcp
202
244
  ```
203
245
 
204
246
  说明:
205
- - 源码目录 `npm install` 不会把命令加到全局 PATH;这种模式请用 `node ./npm/bin/qingflow-app-user-mcp.mjs` 或 `node ./npm/bin/qingflow-app-builder-mcp.mjs`
247
+ - 源码目录 `npm install` 不会把命令加到全局 PATH;这种模式请用 `node ./npm/bin/qingflow.mjs`、`node ./npm/bin/qingflow-app-user-mcp.mjs` 或 `node ./npm/bin/qingflow-app-builder-mcp.mjs`
206
248
  - `npx` 方式适合临时安装或容器化本地 agent
207
249
  - 全局安装方式更适合长期固定使用的本机开发环境
250
+ - 计费接口使用当前登录会话的 `token` 与 `wsId` 请求头,可通过 `QINGFLOW_MCP_CREDIT_APAAS_BASE_URL/PATH` 覆盖调用记录接口地址
208
251
 
209
252
  ## 排障
210
253
 
@@ -226,3 +269,41 @@ rm -rf .npm-python
226
269
  ```bash
227
270
  npm install
228
271
  ```
272
+
273
+ 如果 MCP 客户端一调用工具就报 `Transport closed`,优先检查这几件事:
274
+
275
+ 1. 不要混用不同版本的 `@josephyan/qingflow-cli`、`@josephyan/qingflow-app-user-mcp`、`@josephyan/qingflow-app-builder-mcp`
276
+ 2. 删除安装目录下的 `.npm-python`
277
+ 3. 重新执行 `npm install` 或重新安装对应 tgz/npm 包
278
+ 4. 再启动 MCP 客户端
279
+
280
+ 现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
281
+
282
+ ## createClaw 本地接入示例
283
+
284
+ 如果 createClaw 已经为当前本地实例保存了 `credential`,推荐在首次建链时调用:
285
+
286
+ ```bash
287
+ qingflow auth use-credential \
288
+ --base-url https://qingflow.com/api \
289
+ --credential-stdin
290
+ ```
291
+
292
+ 然后把 `credential` 写到 stdin。
293
+
294
+ 等价 MCP 工具调用参数:
295
+
296
+ ```json
297
+ {
298
+ "profile": "default",
299
+ "base_url": "https://qingflow.com/api",
300
+ "credential": "1602853_277941",
301
+ "persist": false
302
+ }
303
+ ```
304
+
305
+ 说明:
306
+
307
+ - 本地会把解析后的 `token` 和原始 `credential` 写入 profile 文件,用于后续 CLI 命令恢复会话
308
+ - `persist=true` 时,本地还会优先把解析后的 `token` 和原始 `credential` 同步写入系统 keychain
309
+ - 当前工作区以 `/mcp/auth/context` 返回的 `wsId` 为准,不再通过本地 MCP 显式切换
@@ -4,4 +4,4 @@ import { getPackageRoot, spawnServer } from "../lib/runtime.mjs";
4
4
 
5
5
  const packageRoot = getPackageRoot(import.meta.url);
6
6
 
7
- spawnServer(packageRoot, process.argv.slice(2), "qingflow-app-user-mcp");
7
+ spawnServer(packageRoot, process.argv.slice(2), "qingflow-app-user-mcp", { allowRuntimeBootstrap: false });
@@ -7,14 +7,16 @@ const WINDOWS = process.platform === "win32";
7
7
 
8
8
  function runChecked(command, args, options = {}) {
9
9
  const result = spawnSync(command, args, {
10
- stdio: "inherit",
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "pipe"],
11
12
  ...options,
12
13
  });
13
14
  if (result.error) {
14
15
  throw result.error;
15
16
  }
16
17
  if (result.status !== 0) {
17
- throw new Error(`Command failed: ${command} ${args.join(" ")}`);
18
+ const details = [result.stderr, result.stdout].filter((value) => typeof value === "string" && value.trim()).join("\n");
19
+ throw new Error(details ? `Command failed: ${command} ${args.join(" ")}\n${details}` : `Command failed: ${command} ${args.join(" ")}`);
18
20
  }
19
21
  }
20
22
 
@@ -29,6 +31,43 @@ export function getPackageRoot(metaUrl) {
29
31
  return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..", "..");
30
32
  }
31
33
 
34
+ export function getCodexHome() {
35
+ const configured = process.env.CODEX_HOME?.trim();
36
+ if (configured) {
37
+ return path.resolve(configured);
38
+ }
39
+ const home = process.env.HOME || process.env.USERPROFILE;
40
+ if (!home) {
41
+ throw new Error("Cannot resolve CODEX_HOME because HOME is not set.");
42
+ }
43
+ return path.join(home, ".codex");
44
+ }
45
+
46
+ export function installBundledSkills(packageRoot) {
47
+ const skillsSrc = path.join(packageRoot, "skills");
48
+ if (!fs.existsSync(skillsSrc)) {
49
+ return { installed: [], skipped: true, destination: null };
50
+ }
51
+
52
+ const codexHome = getCodexHome();
53
+ const skillsDestRoot = path.join(codexHome, "skills");
54
+ fs.mkdirSync(skillsDestRoot, { recursive: true });
55
+
56
+ const installed = [];
57
+ for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
58
+ if (!entry.isDirectory()) {
59
+ continue;
60
+ }
61
+ const src = path.join(skillsSrc, entry.name);
62
+ const dest = path.join(skillsDestRoot, entry.name);
63
+ fs.rmSync(dest, { recursive: true, force: true });
64
+ fs.cpSync(src, dest, { recursive: true });
65
+ installed.push(entry.name);
66
+ }
67
+
68
+ return { installed, skipped: false, destination: skillsDestRoot };
69
+ }
70
+
32
71
  export function getVenvDir(packageRoot) {
33
72
  return path.join(packageRoot, ".npm-python");
34
73
  }
@@ -45,6 +84,105 @@ export function getVenvServerCommand(packageRoot, commandName = "qingflow-mcp")
45
84
  : path.join(getVenvDir(packageRoot), "bin", commandName);
46
85
  }
47
86
 
87
+ function readPackageVersion(packageRoot) {
88
+ const packageJsonPath = path.join(packageRoot, "package.json");
89
+ try {
90
+ const payload = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
91
+ return typeof payload.version === "string" && payload.version.trim() ? payload.version.trim() : null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function readBootstrapStamp(stampPath) {
98
+ if (!fs.existsSync(stampPath)) {
99
+ return { exists: false, version: null };
100
+ }
101
+ try {
102
+ const payload = JSON.parse(fs.readFileSync(stampPath, "utf8"));
103
+ const version = typeof payload.package_version === "string" && payload.package_version.trim() ? payload.package_version.trim() : null;
104
+ return { exists: true, version };
105
+ } catch {
106
+ return { exists: true, version: null };
107
+ }
108
+ }
109
+
110
+ export function inspectPythonEnv(packageRoot, commandName = "qingflow-mcp") {
111
+ const venvDir = getVenvDir(packageRoot);
112
+ const serverCommand = getVenvServerCommand(packageRoot, commandName);
113
+ const stampPath = path.join(venvDir, ".bootstrap.json");
114
+ const packageVersion = readPackageVersion(packageRoot);
115
+ const stamp = readBootstrapStamp(stampPath);
116
+ const problems = [];
117
+
118
+ if (!packageVersion) {
119
+ problems.push("package-version-missing");
120
+ }
121
+ if (!fs.existsSync(serverCommand)) {
122
+ problems.push("server-command-missing");
123
+ }
124
+ if (!stamp.exists) {
125
+ problems.push("bootstrap-stamp-missing");
126
+ } else if (!stamp.version) {
127
+ problems.push("bootstrap-stamp-invalid");
128
+ } else if (packageVersion && stamp.version !== packageVersion) {
129
+ problems.push("bootstrap-version-mismatch");
130
+ }
131
+
132
+ return {
133
+ ready: problems.length === 0,
134
+ packageVersion,
135
+ stampVersion: stamp.version,
136
+ stampExists: stamp.exists,
137
+ stampPath,
138
+ serverCommand,
139
+ venvDir,
140
+ problems,
141
+ };
142
+ }
143
+
144
+ function formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap = false } = {}) {
145
+ const problemLines = [];
146
+ for (const problem of runtime.problems) {
147
+ switch (problem) {
148
+ case "package-version-missing":
149
+ problemLines.push("- package.json is missing a valid version field");
150
+ break;
151
+ case "server-command-missing":
152
+ problemLines.push(`- missing Python entrypoint: ${runtime.serverCommand}`);
153
+ break;
154
+ case "bootstrap-stamp-missing":
155
+ problemLines.push(`- missing bootstrap stamp: ${runtime.stampPath}`);
156
+ break;
157
+ case "bootstrap-stamp-invalid":
158
+ problemLines.push(`- bootstrap stamp is unreadable or invalid: ${runtime.stampPath}`);
159
+ break;
160
+ case "bootstrap-version-mismatch":
161
+ problemLines.push(
162
+ `- bootstrap version mismatch: package=${runtime.packageVersion ?? "unknown"}, runtime=${runtime.stampVersion ?? "unknown"}`
163
+ );
164
+ break;
165
+ default:
166
+ problemLines.push(`- ${problem}`);
167
+ break;
168
+ }
169
+ }
170
+
171
+ const action = allowRuntimeBootstrap
172
+ ? "Delete .npm-python and retry, or rerun npm install to rebuild the embedded Python runtime."
173
+ : "Delete .npm-python and rerun npm install, or reinstall the npm package before starting the MCP server.";
174
+
175
+ const bootstrapNote = allowRuntimeBootstrap
176
+ ? ""
177
+ : "\nRuntime bootstrap is disabled for stdio MCP entrypoints so install logs can never corrupt the MCP stdout transport.";
178
+
179
+ return [
180
+ `[qingflow-mcp] Python runtime for ${commandName} is not ready.`,
181
+ ...problemLines,
182
+ action + bootstrapNote,
183
+ ].join("\n");
184
+ }
185
+
48
186
  function getVenvPip(packageRoot) {
49
187
  return WINDOWS
50
188
  ? path.join(getVenvDir(packageRoot), "Scripts", "pip.exe")
@@ -82,12 +220,11 @@ export function findPython() {
82
220
 
83
221
  export function ensurePythonEnv(packageRoot, { force = false, commandName = "qingflow-mcp" } = {}) {
84
222
  const python = findPython();
85
- const venvDir = getVenvDir(packageRoot);
223
+ const runtime = inspectPythonEnv(packageRoot, commandName);
86
224
  const venvPython = getVenvPython(packageRoot);
87
- const serverCommand = getVenvServerCommand(packageRoot, commandName);
88
- const stampPath = path.join(venvDir, ".bootstrap.json");
225
+ const { packageVersion, serverCommand, stampPath, venvDir, stampVersion } = runtime;
89
226
 
90
- if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath)) {
227
+ if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath) && stampVersion && stampVersion === packageVersion) {
91
228
  return serverCommand;
92
229
  }
93
230
 
@@ -108,6 +245,7 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
108
245
  {
109
246
  installed_at: new Date().toISOString(),
110
247
  installer: "npm",
248
+ package_version: packageVersion,
111
249
  },
112
250
  null,
113
251
  2
@@ -121,17 +259,79 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
121
259
  return serverCommand;
122
260
  }
123
261
 
124
- export function spawnServer(packageRoot, args, commandName = "qingflow-mcp") {
125
- const serverCommand = fs.existsSync(getVenvServerCommand(packageRoot, commandName))
126
- ? getVenvServerCommand(packageRoot, commandName)
127
- : ensurePythonEnv(packageRoot, { commandName });
262
+ function proxyStreams(child) {
263
+ if (process.stdin.readable && child.stdin) {
264
+ process.stdin.pipe(child.stdin);
265
+ child.stdin.on("error", (error) => {
266
+ if (error.code !== "EPIPE") {
267
+ console.error(`[qingflow-mcp] Failed to forward stdin: ${error.message}`);
268
+ }
269
+ });
270
+ } else if (child.stdin) {
271
+ child.stdin.end();
272
+ }
273
+
274
+ if (child.stdout) {
275
+ child.stdout.pipe(process.stdout);
276
+ }
277
+ if (child.stderr) {
278
+ child.stderr.pipe(process.stderr);
279
+ }
280
+ }
281
+
282
+ function forwardSignal(child, signal) {
283
+ process.on(signal, () => {
284
+ if (!child.killed) {
285
+ child.kill(signal);
286
+ }
287
+ });
288
+ }
289
+
290
+ export function spawnServer(
291
+ packageRoot,
292
+ args,
293
+ commandName = "qingflow-mcp",
294
+ { allowRuntimeBootstrap = false, stdio = "proxy" } = {},
295
+ ) {
296
+ let runtime = inspectPythonEnv(packageRoot, commandName);
297
+ let serverCommand = runtime.serverCommand;
298
+
299
+ if (!runtime.ready) {
300
+ if (!allowRuntimeBootstrap) {
301
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
302
+ process.exit(1);
303
+ return;
304
+ }
305
+
306
+ try {
307
+ serverCommand = ensurePythonEnv(packageRoot, { commandName });
308
+ runtime = inspectPythonEnv(packageRoot, commandName);
309
+ } catch (error) {
310
+ console.error(`[qingflow-mcp] Failed to prepare Python runtime for ${commandName}: ${error.message}`);
311
+ process.exit(1);
312
+ return;
313
+ }
314
+
315
+ if (!runtime.ready) {
316
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
317
+ process.exit(1);
318
+ return;
319
+ }
320
+ }
128
321
 
129
322
  const child = spawn(serverCommand, args, {
130
- stdio: "inherit",
323
+ stdio: stdio === "inherit" ? "inherit" : ["pipe", "pipe", "pipe"],
131
324
  env: process.env,
325
+ windowsHide: true,
132
326
  });
133
327
 
134
- child.on("exit", (code, signal) => {
328
+ if (stdio !== "inherit") {
329
+ proxyStreams(child);
330
+ }
331
+ forwardSignal(child, "SIGINT");
332
+ forwardSignal(child, "SIGTERM");
333
+
334
+ child.on("close", (code, signal) => {
135
335
  if (signal) {
136
336
  process.kill(process.pid, signal);
137
337
  return;
@@ -1,4 +1,4 @@
1
- import { ensurePythonEnv, getPackageRoot } from "../lib/runtime.mjs";
1
+ import { ensurePythonEnv, getPackageRoot, installBundledSkills } from "../lib/runtime.mjs";
2
2
 
3
3
  const packageRoot = getPackageRoot(import.meta.url);
4
4
 
@@ -6,6 +6,10 @@ try {
6
6
  console.log("[qingflow-mcp] Bootstrapping Python runtime...");
7
7
  ensurePythonEnv(packageRoot, { commandName: "qingflow-app-user-mcp" });
8
8
  console.log("[qingflow-mcp] Python runtime is ready.");
9
+ const skills = installBundledSkills(packageRoot);
10
+ if (!skills.skipped) {
11
+ console.log(`[qingflow-mcp] Installed skills to ${skills.destination}: ${skills.installed.join(", ")}`);
12
+ }
9
13
  } catch (error) {
10
14
  console.error(`[qingflow-mcp] postinstall failed: ${error.message}`);
11
15
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.2.0-beta.1000",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,7 +18,8 @@
18
18
  "src/qingflow_mcp/py.typed",
19
19
  "qingflow-app-user-mcp",
20
20
  "npm/",
21
- "docs/local-agent-install.md"
21
+ "docs/local-agent-install.md",
22
+ "skills/"
22
23
  ],
23
24
  "engines": {
24
25
  "node": ">=16.16.0"
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b1"
7
+ version = "0.2.0b1000"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -26,8 +26,10 @@ dependencies = [
26
26
  "mcp>=1.9.4,<2.0.0",
27
27
  "httpx>=0.27,<1.0",
28
28
  "keyring>=25.5,<26.0",
29
+ "openpyxl>=3.1,<4.0",
29
30
  "pydantic>=2.8,<3.0",
30
31
  "pycryptodome>=3.20,<4.0",
32
+ "python-socketio[client]>=5.11,<6.0",
31
33
  ]
32
34
 
33
35
  [project.optional-dependencies]
@@ -44,6 +46,7 @@ build = [
44
46
  [project.scripts]
45
47
  qingflow-app-user-mcp = "qingflow_mcp.server_app_user:main"
46
48
  qingflow-app-builder-mcp = "qingflow_mcp.server_app_builder:main"
49
+ qingflow = "qingflow_mcp.cli.main:main"
47
50
 
48
51
  [project.urls]
49
52
  Homepage = "https://github.com/qingflow/qingflow-mcp"
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: qingflow-app-user
3
+ description: Route Qingflow end-user requests to the right specialized operational skill after the MCP is already connected and authenticated. Use when the task is operational but it is not yet clear whether it is record CRUD or final analysis.
4
+ metadata:
5
+ short-description: Router for Qingflow operational skills
6
+ ---
7
+
8
+ # Qingflow App User
9
+
10
+ ## Overview
11
+
12
+ This skill is a lightweight router for operational Qingflow work.
13
+ Assumes MCP is connected, authenticated, and on the correct workspace.
14
+ Before routing, skim the shared maintenance baseline: [public-surface-sync.md](references/public-surface-sync.md).
15
+
16
+ ## Default Paths
17
+
18
+ Route to exactly one of these specialized paths:
19
+
20
+ 1. Record insert
21
+ Switch to [$qingflow-record-insert](../qingflow-record-insert/SKILL.md)
22
+
23
+ 2. Record update
24
+ Switch to [$qingflow-record-update](../qingflow-record-update/SKILL.md)
25
+
26
+ 3. Record delete
27
+ Switch to [$qingflow-record-delete](../qingflow-record-delete/SKILL.md)
28
+
29
+ 4. Record import
30
+ Switch to [$qingflow-record-import](../qingflow-record-import/SKILL.md)
31
+
32
+ 5. Task workflow operations
33
+ Switch to [$qingflow-task-ops](../qingflow-task-ops/SKILL.md)
34
+
35
+ 6. Analysis
36
+ Switch to [$qingflow-record-analysis](../qingflow-record-analysis/SKILL.md)
37
+
38
+ 7. MCP connection / auth / workspace selection
39
+ Switch to [$qingflow-mcp-setup](../qingflow-mcp-setup/SKILL.md)
40
+
41
+ 8. App / view / workflow / chart / portal / package configuration
42
+ Switch to [$qingflow-app-builder](../qingflow-app-builder/SKILL.md)
43
+
44
+ ## Routing Rules
45
+
46
+ - If the user does not know the target `app_key`, discover apps first with `app_list` or `app_search`, then route to the specialized skill
47
+ - If the app is known but the available data range is unclear, call `app_get` first and inspect `accessible_views`
48
+ - If the task is about creating or new record entry, switch to `$qingflow-record-insert`
49
+ - If the task is about editing an existing record directly, switch to `$qingflow-record-update`
50
+ - If the task is about deleting records directly, switch to `$qingflow-record-delete`
51
+ - If the task is about import templates, import capability discovery, import-file verification, authorized local file repair, import execution, or import status, switch to `$qingflow-record-import`
52
+ - If the task is about todo discovery, task context, approval actions, rollback or transfer, associated report review, or workflow log review, switch to `$qingflow-task-ops`
53
+ - If the task is about package, app, field, layout, workflow, view, chart, portal, visibility, icon, or app base configuration, switch to `$qingflow-app-builder`
54
+ - If the task involves member, department, or relation fields and the user only has natural names/titles, keep the same route; direct write now supports backend-native auto resolution and may return `needs_confirmation` with candidates instead of failing blind
55
+ - If the task involves linked visibility, upstream/downstream field dependencies, reference-driven auto fill, or formula-driven defaulting, keep the same insert/update route and read field-level `linkage` from the schema before composing payloads
56
+ - If the task is about subtable writes, still route to the matching insert/update skill, but shape the payload as parent subtable field -> row array; do not route users toward top-level leaf selectors
57
+ - If the task is insert-focused and readback consistency matters, keep the same route and prefer `record_get / record_list` with `output_profile="normalized"` after the write
58
+ - If the user sounds like an ordinary workflow assignee rather than a system operator, prefer `$qingflow-task-ops` over direct record mutation whenever both paths could fit
59
+ - If the task is about task discovery by natural language query, still route to `$qingflow-task-ops`; `task_list --query` now uses backend search first and only falls back to local matching when backend returns zero rows
60
+ - If the task is about grouped distributions, ratios, rankings, trends, insights, or any final statistical conclusion, switch to `$qingflow-record-analysis`
61
+ - If the MCP is not connected, authenticated, or bound to the right workspace, switch to `$qingflow-mcp-setup`
62
+
63
+ ## Shared Preconditions
64
+
65
+ - prefer canonical app ids, record ids, task ids, and workflow node ids over guessed names
66
+ - if a field or target is still ambiguous after schema/task lookup, ask the user to confirm from a short candidate list instead of guessing
67
+ - if schema fields include `linkage.sources` or `linkage.affects_fields`, treat those as the preferred high-level explanation of field dependencies instead of trying to infer hidden front-end logic
68
+ - if the task can stay read-only, do not write or act
69
+ - if the task involves a user-uploaded import file, do not modify the file unless the user explicitly authorizes repair or normalization
70
+ - if the task involves record import, call `app_get` first and inspect `data.import_capability` before template download, file repair, or import start
71
+ - if the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, summarize the gap, ask whether to submit feedback, and call `feedback_submit` only after explicit user confirmation
72
+
73
+ ## Shared Helper
74
+
75
+ - `feedback_submit` is a cross-cutting helper for product feedback submission
76
+ - It does not require Qingflow login or workspace selection
77
+ - Use it only after the user explicitly confirms they want to submit feedback
78
+
79
+ ## Resources
80
+
81
+ - Shared public-surface baseline: [public-surface-sync.md](references/public-surface-sync.md)
82
+ - Record insert: [$qingflow-record-insert](../qingflow-record-insert/SKILL.md)
83
+ - Record update: [$qingflow-record-update](../qingflow-record-update/SKILL.md)
84
+ - Record delete: [$qingflow-record-delete](../qingflow-record-delete/SKILL.md)
85
+ - Record import: [$qingflow-record-import](../qingflow-record-import/SKILL.md)
86
+ - Task workflow operations: [$qingflow-task-ops](../qingflow-task-ops/SKILL.md)
87
+ - Dedicated analysis workflow: [$qingflow-record-analysis](../qingflow-record-analysis/SKILL.md)
88
+ - Builder workflow: [$qingflow-app-builder](../qingflow-app-builder/SKILL.md)