@josephyan/qingflow-cli 0.2.0-beta.58 → 0.2.0-beta.60

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 (35) hide show
  1. package/README.md +3 -2
  2. package/docs/local-agent-install.md +9 -0
  3. package/npm/bin/qingflow.mjs +1 -1
  4. package/npm/lib/runtime.mjs +156 -21
  5. package/package.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/src/qingflow_mcp/builder_facade/service.py +670 -191
  8. package/src/qingflow_mcp/cli/commands/app.py +16 -16
  9. package/src/qingflow_mcp/cli/commands/auth.py +19 -16
  10. package/src/qingflow_mcp/cli/commands/builder.py +124 -162
  11. package/src/qingflow_mcp/cli/commands/common.py +21 -95
  12. package/src/qingflow_mcp/cli/commands/imports.py +42 -34
  13. package/src/qingflow_mcp/cli/commands/record.py +131 -133
  14. package/src/qingflow_mcp/cli/commands/task.py +43 -44
  15. package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
  16. package/src/qingflow_mcp/cli/context.py +35 -32
  17. package/src/qingflow_mcp/cli/formatters.py +124 -121
  18. package/src/qingflow_mcp/cli/main.py +52 -17
  19. package/src/qingflow_mcp/server_app_builder.py +122 -190
  20. package/src/qingflow_mcp/server_app_user.py +63 -662
  21. package/src/qingflow_mcp/solution/executor.py +63 -4
  22. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  23. package/src/qingflow_mcp/ops/__init__.py +0 -3
  24. package/src/qingflow_mcp/ops/apps.py +0 -64
  25. package/src/qingflow_mcp/ops/auth.py +0 -121
  26. package/src/qingflow_mcp/ops/base.py +0 -290
  27. package/src/qingflow_mcp/ops/builder.py +0 -357
  28. package/src/qingflow_mcp/ops/context.py +0 -120
  29. package/src/qingflow_mcp/ops/directory.py +0 -171
  30. package/src/qingflow_mcp/ops/feedback.py +0 -49
  31. package/src/qingflow_mcp/ops/files.py +0 -78
  32. package/src/qingflow_mcp/ops/imports.py +0 -140
  33. package/src/qingflow_mcp/ops/records.py +0 -415
  34. package/src/qingflow_mcp/ops/tasks.py +0 -171
  35. package/src/qingflow_mcp/ops/workspace.py +0 -76
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.58
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.60
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.58 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.60 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
@@ -28,3 +28,4 @@ Note:
28
28
 
29
29
  - The skill files are included in the npm package.
30
30
  - On install, the package copies them to `$CODEX_HOME/skills` (or `~/.codex/skills` if `CODEX_HOME` is unset).
31
+ - 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.
@@ -233,3 +233,12 @@ rm -rf .npm-python
233
233
  ```bash
234
234
  npm install
235
235
  ```
236
+
237
+ 如果 MCP 客户端一调用工具就报 `Transport closed`,优先检查这几件事:
238
+
239
+ 1. 不要混用不同版本的 `@josephyan/qingflow-cli`、`@josephyan/qingflow-app-user-mcp`、`@josephyan/qingflow-app-builder-mcp`
240
+ 2. 删除安装目录下的 `.npm-python`
241
+ 3. 重新执行 `npm install` 或重新安装对应 tgz/npm 包
242
+ 4. 再启动 MCP 客户端
243
+
244
+ 现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
@@ -2,4 +2,4 @@
2
2
  import { spawnServer, getPackageRoot } from "../lib/runtime.mjs";
3
3
 
4
4
  const packageRoot = getPackageRoot(import.meta.url);
5
- spawnServer(packageRoot, process.argv.slice(2), "qingflow");
5
+ spawnServer(packageRoot, process.argv.slice(2), "qingflow", { allowRuntimeBootstrap: true });
@@ -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
 
@@ -92,6 +94,95 @@ function readPackageVersion(packageRoot) {
92
94
  }
93
95
  }
94
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
+
95
186
  function getVenvPip(packageRoot) {
96
187
  return WINDOWS
97
188
  ? path.join(getVenvDir(packageRoot), "Scripts", "pip.exe")
@@ -129,20 +220,9 @@ export function findPython() {
129
220
 
130
221
  export function ensurePythonEnv(packageRoot, { force = false, commandName = "qingflow-mcp" } = {}) {
131
222
  const python = findPython();
132
- const venvDir = getVenvDir(packageRoot);
223
+ const runtime = inspectPythonEnv(packageRoot, commandName);
133
224
  const venvPython = getVenvPython(packageRoot);
134
- const serverCommand = getVenvServerCommand(packageRoot, commandName);
135
- const stampPath = path.join(venvDir, ".bootstrap.json");
136
- const packageVersion = readPackageVersion(packageRoot);
137
- let stampVersion = null;
138
- if (fs.existsSync(stampPath)) {
139
- try {
140
- const payload = JSON.parse(fs.readFileSync(stampPath, "utf8"));
141
- stampVersion = typeof payload.package_version === "string" && payload.package_version.trim() ? payload.package_version.trim() : null;
142
- } catch {
143
- stampVersion = null;
144
- }
145
- }
225
+ const { packageVersion, serverCommand, stampPath, venvDir, stampVersion } = runtime;
146
226
 
147
227
  if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath) && stampVersion && stampVersion === packageVersion) {
148
228
  return serverCommand;
@@ -179,17 +259,72 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
179
259
  return serverCommand;
180
260
  }
181
261
 
182
- export function spawnServer(packageRoot, args, commandName = "qingflow-mcp") {
183
- const serverCommand = fs.existsSync(getVenvServerCommand(packageRoot, commandName))
184
- ? getVenvServerCommand(packageRoot, commandName)
185
- : 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(packageRoot, args, commandName = "qingflow-mcp", { allowRuntimeBootstrap = false } = {}) {
291
+ let runtime = inspectPythonEnv(packageRoot, commandName);
292
+ let serverCommand = runtime.serverCommand;
293
+
294
+ if (!runtime.ready) {
295
+ if (!allowRuntimeBootstrap) {
296
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
297
+ process.exit(1);
298
+ return;
299
+ }
300
+
301
+ try {
302
+ serverCommand = ensurePythonEnv(packageRoot, { commandName });
303
+ runtime = inspectPythonEnv(packageRoot, commandName);
304
+ } catch (error) {
305
+ console.error(`[qingflow-mcp] Failed to prepare Python runtime for ${commandName}: ${error.message}`);
306
+ process.exit(1);
307
+ return;
308
+ }
309
+
310
+ if (!runtime.ready) {
311
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
312
+ process.exit(1);
313
+ return;
314
+ }
315
+ }
186
316
 
187
317
  const child = spawn(serverCommand, args, {
188
- stdio: "inherit",
318
+ stdio: ["pipe", "pipe", "pipe"],
189
319
  env: process.env,
320
+ windowsHide: true,
190
321
  });
191
322
 
192
- child.on("exit", (code, signal) => {
323
+ proxyStreams(child);
324
+ forwardSignal(child, "SIGINT");
325
+ forwardSignal(child, "SIGTERM");
326
+
327
+ child.on("close", (code, signal) => {
193
328
  if (signal) {
194
329
  process.kill(process.pid, signal);
195
330
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.58",
3
+ "version": "0.2.0-beta.60",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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.0b58"
7
+ version = "0.2.0b60"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"