@qingflow-tech/qingflow-app-user-mcp 1.0.0

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 (109) hide show
  1. package/README.md +37 -0
  2. package/docs/local-agent-install.md +332 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-app-user-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +339 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow-app-user-mcp +15 -0
  10. package/skills/qingflow-app-user/SKILL.md +79 -0
  11. package/skills/qingflow-app-user/agents/openai.yaml +4 -0
  12. package/skills/qingflow-app-user/references/data-gotchas.md +29 -0
  13. package/skills/qingflow-app-user/references/environments.md +63 -0
  14. package/skills/qingflow-app-user/references/record-patterns.md +48 -0
  15. package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
  16. package/skills/qingflow-record-analysis/SKILL.md +158 -0
  17. package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
  18. package/skills/qingflow-record-analysis/references/analysis-gotchas.md +145 -0
  19. package/skills/qingflow-record-analysis/references/analysis-patterns.md +125 -0
  20. package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
  21. package/skills/qingflow-record-analysis/references/dsl-templates.md +93 -0
  22. package/skills/qingflow-record-delete/SKILL.md +29 -0
  23. package/skills/qingflow-record-import/SKILL.md +31 -0
  24. package/skills/qingflow-record-insert/SKILL.md +58 -0
  25. package/skills/qingflow-record-update/SKILL.md +42 -0
  26. package/skills/qingflow-task-ops/SKILL.md +123 -0
  27. package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
  28. package/skills/qingflow-task-ops/references/environments.md +44 -0
  29. package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
  30. package/src/qingflow_mcp/__init__.py +5 -0
  31. package/src/qingflow_mcp/__main__.py +5 -0
  32. package/src/qingflow_mcp/backend_client.py +649 -0
  33. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  34. package/src/qingflow_mcp/builder_facade/models.py +1836 -0
  35. package/src/qingflow_mcp/builder_facade/service.py +15044 -0
  36. package/src/qingflow_mcp/cli/__init__.py +1 -0
  37. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  38. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  39. package/src/qingflow_mcp/cli/commands/auth.py +44 -0
  40. package/src/qingflow_mcp/cli/commands/builder.py +538 -0
  41. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  42. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  43. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  44. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  45. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  46. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  47. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  48. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  49. package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
  50. package/src/qingflow_mcp/cli/context.py +60 -0
  51. package/src/qingflow_mcp/cli/formatters.py +334 -0
  52. package/src/qingflow_mcp/cli/json_io.py +50 -0
  53. package/src/qingflow_mcp/cli/main.py +178 -0
  54. package/src/qingflow_mcp/config.py +513 -0
  55. package/src/qingflow_mcp/errors.py +66 -0
  56. package/src/qingflow_mcp/import_store.py +121 -0
  57. package/src/qingflow_mcp/json_types.py +18 -0
  58. package/src/qingflow_mcp/list_type_labels.py +76 -0
  59. package/src/qingflow_mcp/public_surface.py +233 -0
  60. package/src/qingflow_mcp/repository_store.py +71 -0
  61. package/src/qingflow_mcp/response_trim.py +470 -0
  62. package/src/qingflow_mcp/server.py +212 -0
  63. package/src/qingflow_mcp/server_app_builder.py +533 -0
  64. package/src/qingflow_mcp/server_app_user.py +362 -0
  65. package/src/qingflow_mcp/session_store.py +302 -0
  66. package/src/qingflow_mcp/solution/__init__.py +6 -0
  67. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  69. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  70. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  71. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  72. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  73. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  74. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  75. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  76. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  77. package/src/qingflow_mcp/solution/design_session.py +222 -0
  78. package/src/qingflow_mcp/solution/design_store.py +100 -0
  79. package/src/qingflow_mcp/solution/executor.py +2398 -0
  80. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  81. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  82. package/src/qingflow_mcp/solution/run_store.py +244 -0
  83. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  84. package/src/qingflow_mcp/tools/__init__.py +1 -0
  85. package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
  86. package/src/qingflow_mcp/tools/app_tools.py +925 -0
  87. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  88. package/src/qingflow_mcp/tools/auth_tools.py +875 -0
  89. package/src/qingflow_mcp/tools/base.py +388 -0
  90. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  91. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  92. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  93. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  94. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  95. package/src/qingflow_mcp/tools/import_tools.py +2189 -0
  96. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  97. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  98. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  99. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  100. package/src/qingflow_mcp/tools/record_tools.py +14037 -0
  101. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  102. package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
  103. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  104. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  105. package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
  106. package/src/qingflow_mcp/tools/task_tools.py +890 -0
  107. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  108. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  109. package/src/qingflow_mcp/tools/workspace_tools.py +125 -0
@@ -0,0 +1,339 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const WINDOWS = process.platform === "win32";
7
+
8
+ function runChecked(command, args, options = {}) {
9
+ const result = spawnSync(command, args, {
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "pipe"],
12
+ ...options,
13
+ });
14
+ if (result.error) {
15
+ throw result.error;
16
+ }
17
+ if (result.status !== 0) {
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(" ")}`);
20
+ }
21
+ }
22
+
23
+ function commandWorks(command, args) {
24
+ const result = spawnSync(command, args, {
25
+ stdio: "ignore",
26
+ });
27
+ return result.status === 0;
28
+ }
29
+
30
+ export function getPackageRoot(metaUrl) {
31
+ return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..", "..");
32
+ }
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
+
71
+ export function getVenvDir(packageRoot) {
72
+ return path.join(packageRoot, ".npm-python");
73
+ }
74
+
75
+ export function getVenvPython(packageRoot) {
76
+ return WINDOWS
77
+ ? path.join(getVenvDir(packageRoot), "Scripts", "python.exe")
78
+ : path.join(getVenvDir(packageRoot), "bin", "python");
79
+ }
80
+
81
+ export function getVenvServerCommand(packageRoot, commandName = "qingflow-mcp") {
82
+ return WINDOWS
83
+ ? path.join(getVenvDir(packageRoot), "Scripts", `${commandName}.exe`)
84
+ : path.join(getVenvDir(packageRoot), "bin", commandName);
85
+ }
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
+
186
+ function getVenvPip(packageRoot) {
187
+ return WINDOWS
188
+ ? path.join(getVenvDir(packageRoot), "Scripts", "pip.exe")
189
+ : path.join(getVenvDir(packageRoot), "bin", "pip");
190
+ }
191
+
192
+ export function findPython() {
193
+ const preferred = process.env.QINGFLOW_MCP_PYTHON?.trim();
194
+ const candidates = preferred
195
+ ? [{ command: preferred, args: [], label: preferred }]
196
+ : WINDOWS
197
+ ? [
198
+ { command: "py", args: ["-3", "-V"], label: "py -3" },
199
+ { command: "python", args: ["-V"], label: "python" },
200
+ { command: "python3", args: ["-V"], label: "python3" },
201
+ ]
202
+ : [
203
+ { command: "python3", args: ["-V"], label: "python3" },
204
+ { command: "python", args: ["-V"], label: "python" },
205
+ ];
206
+
207
+ for (const candidate of candidates) {
208
+ if (commandWorks(candidate.command, candidate.args)) {
209
+ if (candidate.command === "py") {
210
+ return { command: "py", args: ["-3"], label: candidate.label };
211
+ }
212
+ return { command: candidate.command, args: [], label: candidate.label };
213
+ }
214
+ }
215
+
216
+ throw new Error(
217
+ "Python 3.11+ was not found. Set QINGFLOW_MCP_PYTHON to a Python 3 executable before running npm install."
218
+ );
219
+ }
220
+
221
+ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qingflow-mcp" } = {}) {
222
+ const python = findPython();
223
+ const runtime = inspectPythonEnv(packageRoot, commandName);
224
+ const venvPython = getVenvPython(packageRoot);
225
+ const { packageVersion, serverCommand, stampPath, venvDir, stampVersion } = runtime;
226
+
227
+ if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath) && stampVersion && stampVersion === packageVersion) {
228
+ return serverCommand;
229
+ }
230
+
231
+ if (force && fs.existsSync(venvDir)) {
232
+ fs.rmSync(venvDir, { recursive: true, force: true });
233
+ }
234
+
235
+ if (!fs.existsSync(venvPython)) {
236
+ runChecked(python.command, [...python.args, "-m", "venv", venvDir], { cwd: packageRoot });
237
+ }
238
+
239
+ const pip = getVenvPip(packageRoot);
240
+ runChecked(pip, ["install", "--disable-pip-version-check", "."], { cwd: packageRoot });
241
+
242
+ fs.writeFileSync(
243
+ stampPath,
244
+ JSON.stringify(
245
+ {
246
+ installed_at: new Date().toISOString(),
247
+ installer: "npm",
248
+ package_version: packageVersion,
249
+ },
250
+ null,
251
+ 2
252
+ )
253
+ );
254
+
255
+ if (!fs.existsSync(serverCommand)) {
256
+ throw new Error(`Bootstrap finished but ${serverCommand} was not created.`);
257
+ }
258
+
259
+ return serverCommand;
260
+ }
261
+
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
+ }
316
+
317
+ const child = spawn(serverCommand, args, {
318
+ stdio: ["pipe", "pipe", "pipe"],
319
+ env: process.env,
320
+ windowsHide: true,
321
+ });
322
+
323
+ proxyStreams(child);
324
+ forwardSignal(child, "SIGINT");
325
+ forwardSignal(child, "SIGTERM");
326
+
327
+ child.on("close", (code, signal) => {
328
+ if (signal) {
329
+ process.kill(process.pid, signal);
330
+ return;
331
+ }
332
+ process.exit(code ?? 0);
333
+ });
334
+
335
+ child.on("error", (error) => {
336
+ console.error(`[qingflow-mcp] Failed to start server: ${error.message}`);
337
+ process.exit(1);
338
+ });
339
+ }
@@ -0,0 +1,16 @@
1
+ import { ensurePythonEnv, getPackageRoot, installBundledSkills } from "../lib/runtime.mjs";
2
+
3
+ const packageRoot = getPackageRoot(import.meta.url);
4
+
5
+ try {
6
+ console.log("[qingflow-mcp] Bootstrapping Python runtime...");
7
+ ensurePythonEnv(packageRoot, { commandName: "qingflow-app-user-mcp" });
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
+ }
13
+ } catch (error) {
14
+ console.error(`[qingflow-mcp] postinstall failed: ${error.message}`);
15
+ process.exit(1);
16
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@qingflow-tech/qingflow-app-user-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "qingflow-app-user-mcp": "./npm/bin/qingflow-app-user-mcp.mjs"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "node ./npm/scripts/postinstall.mjs"
12
+ },
13
+ "files": [
14
+ "README.md",
15
+ "pyproject.toml",
16
+ "entry_point.py",
17
+ "src/qingflow_mcp/**/*.py",
18
+ "src/qingflow_mcp/py.typed",
19
+ "qingflow-app-user-mcp",
20
+ "npm/",
21
+ "docs/local-agent-install.md",
22
+ "skills/"
23
+ ],
24
+ "engines": {
25
+ "node": ">=16.16.0"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "qingflow",
30
+ "agent",
31
+ "stdio",
32
+ "app-user"
33
+ ]
34
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qingflow-mcp"
7
+ version = "0.2.0b87"
8
+ description = "User-authenticated MCP server for Qingflow"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Qingflow", email = "support@qingflow.com" }
14
+ ]
15
+ keywords = ["mcp", "qingflow", "automation", "workflow"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "mcp>=1.9.4,<2.0.0",
27
+ "httpx>=0.27,<1.0",
28
+ "keyring>=25.5,<26.0",
29
+ "openpyxl>=3.1,<4.0",
30
+ "pydantic>=2.8,<3.0",
31
+ "pycryptodome>=3.20,<4.0",
32
+ "python-socketio[client]>=5.11,<6.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=8.3,<9.0",
38
+ "respx>=0.22,<1.0",
39
+ ]
40
+ build = [
41
+ "pyinstaller>=6.0,<7.0",
42
+ "build>=1.0,<2.0",
43
+ "twine>=5.0,<6.0",
44
+ ]
45
+
46
+ [project.scripts]
47
+ qingflow-app-user-mcp = "qingflow_mcp.server_app_user:main"
48
+ qingflow-app-builder-mcp = "qingflow_mcp.server_app_builder:main"
49
+ qingflow = "qingflow_mcp.cli.main:main"
50
+
51
+ [project.urls]
52
+ Homepage = "https://github.com/qingflow/qingflow-mcp"
53
+ Documentation = "https://github.com/qingflow/qingflow-mcp#readme"
54
+ Repository = "https://github.com/qingflow/qingflow-mcp"
55
+ Issues = "https://github.com/qingflow/qingflow-mcp/issues"
56
+
57
+ [tool.setuptools.package-dir]
58
+ "" = "src"
59
+
60
+ [tool.setuptools.packages.find]
61
+ where = ["src"]
62
+
63
+ [tool.setuptools.package-data]
64
+ qingflow_mcp = ["py.typed"]
65
+
66
+ [tool.pytest.ini_options]
67
+ testpaths = ["tests"]
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ if [[ -n "${PYTHON_BIN:-}" ]]; then
5
+ PYTHON="${PYTHON_BIN}"
6
+ elif [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
7
+ PYTHON="$ROOT_DIR/.venv/bin/python"
8
+ elif command -v python3 >/dev/null 2>&1; then
9
+ PYTHON="$(command -v python3)"
10
+ else
11
+ echo "qingflow-app-user-mcp: python interpreter not found. Set PYTHON_BIN or create $ROOT_DIR/.venv" >&2
12
+ exit 1
13
+ fi
14
+ export PYTHONPATH="$ROOT_DIR/src${PYTHONPATH:+:$PYTHONPATH}"
15
+ exec "$PYTHON" -m qingflow_mcp.server_app_user "$@"
@@ -0,0 +1,79 @@
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
+
15
+ ## Default Paths
16
+
17
+ Route to exactly one of these specialized paths:
18
+
19
+ 1. Record insert
20
+ Switch to [$qingflow-record-insert](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-insert/SKILL.md)
21
+
22
+ 2. Record update
23
+ Switch to [$qingflow-record-update](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-update/SKILL.md)
24
+
25
+ 3. Record delete
26
+ Switch to [$qingflow-record-delete](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-delete/SKILL.md)
27
+
28
+ 4. Record import
29
+ Switch to [$qingflow-record-import](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-import/SKILL.md)
30
+
31
+ 5. Task workflow operations
32
+ Switch to [$qingflow-task-ops](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-task-ops/SKILL.md)
33
+
34
+ 6. Analysis
35
+ Switch to [$qingflow-record-analysis](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-analysis/SKILL.md)
36
+
37
+ 7. MCP connection / auth / workspace selection
38
+ Switch to [$qingflow-mcp-setup](/Users/yanqidong/.codex/skills/qingflow-mcp-setup/SKILL.md)
39
+
40
+ ## Routing Rules
41
+
42
+ - 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
43
+ - If the app is known but the available data range is unclear, call `app_get` first and inspect `accessible_views`
44
+ - If the task is about creating or new record entry, switch to `$qingflow-record-insert`
45
+ - If the task is about editing an existing record directly, switch to `$qingflow-record-update`
46
+ - If the task is about deleting records directly, switch to `$qingflow-record-delete`
47
+ - 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`
48
+ - 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`
49
+ - 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
50
+ - 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
51
+ - 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
52
+ - 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
53
+ - 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
54
+ - If the task is about grouped distributions, ratios, rankings, trends, insights, or any final statistical conclusion, switch to `$qingflow-record-analysis`
55
+ - If the MCP is not connected, authenticated, or bound to the right workspace, switch to `$qingflow-mcp-setup`
56
+
57
+ ## Shared Preconditions
58
+
59
+ - prefer canonical app ids, record ids, task ids, and workflow node ids over guessed names
60
+ - 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
61
+ - 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
62
+ - if the task can stay read-only, do not write or act
63
+ - if the task involves a user-uploaded import file, do not modify the file unless the user explicitly authorizes repair or normalization
64
+ - if the task involves record import, call `app_get` first and inspect `data.import_capability` before template download, file repair, or import start
65
+ - 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
66
+
67
+ ## Shared Helper
68
+
69
+ - `feedback_submit` is a cross-cutting helper for product feedback submission
70
+ - It does not require Qingflow login or workspace selection
71
+ - Use it only after the user explicitly confirms they want to submit feedback
72
+
73
+ ## Resources
74
+
75
+ - Record insert: [$qingflow-record-insert](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-insert/SKILL.md)
76
+ - Record update: [$qingflow-record-update](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-update/SKILL.md)
77
+ - Record delete: [$qingflow-record-delete](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-delete/SKILL.md)
78
+ - Record import: [$qingflow-record-import](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-import/SKILL.md)
79
+ - Dedicated analysis workflow: [$qingflow-record-analysis](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-analysis/SKILL.md)
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Qingflow App User"
3
+ short_description: "Route Qingflow operational tasks to insert, update, delete, import, task ops, or analysis"
4
+ default_prompt: "Use $qingflow-app-user as a router: switch to $qingflow-record-insert for new record entry, $qingflow-record-update for direct edits, $qingflow-record-delete for deletes, $qingflow-record-import for bulk import, $qingflow-task-ops for task-center, comments, directory, and workflow usage actions, and $qingflow-record-analysis for grouped analysis or final statistical conclusions."
@@ -0,0 +1,29 @@
1
+ # Data Gotchas
2
+
3
+ For final statistics, grouped distributions, rankings, trends, or insight-style conclusions, use [$qingflow-record-analysis](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-analysis/SKILL.md) instead of keeping that reasoning inside `$qingflow-app-user`.
4
+
5
+ ## Record Reads
6
+
7
+ - `record_list` is for browsing, export, and sample inspection only
8
+ - `record_get` is for one exact record
9
+ - Use `record_browse_schema_get` when field titles are uncertain instead of guessing ids
10
+ - Do not present paged browse output as if it were a grouped or full-population conclusion
11
+
12
+ ## Direct Writes
13
+
14
+ - `record_insert` is schema-first through `record_insert_schema_get`
15
+ - `record_update` is schema-first through `record_update_schema_get`
16
+ - `record_delete` does not need a schema-get step
17
+ - If a direct-write tool returns `ok=false`, the write was blocked and not executed
18
+ - Prefer `verify_write=true` for complex, relation-heavy, subtable, or production writes
19
+
20
+ ## Lookup Fields
21
+
22
+ - Member / department / relation fields may accept natural text, but MCP may return `needs_confirmation`
23
+ - Do not guess ids when the response returns candidate options
24
+ - Retry only after the user confirms the explicit candidate
25
+
26
+ ## Subtables and Attachments
27
+
28
+ - Subtable payloads stay under the parent table field as a row array
29
+ - Attachment fields are two-step: upload first, then write the returned upload payload
@@ -0,0 +1,63 @@
1
+ # Environment Switching
2
+
3
+ Use this reference before any data creation, update, delete, or workflow usage action.
4
+
5
+ ## Step 1: Resolve the active environment
6
+
7
+ Decide explicitly whether the task targets:
8
+
9
+ - `test`: demo, mock data, smoke usage validation, training scenarios
10
+ - `prod`: real operational data and live workflow actions
11
+
12
+ If the user did not specify an environment, default to `prod`.
13
+
14
+ ## Test Environment
15
+
16
+ Use test for:
17
+
18
+ - mock or smoke data entry
19
+ - business flow walkthroughs
20
+ - user acceptance demos
21
+ - data correction rehearsals
22
+
23
+ Test behavior:
24
+
25
+ - creating demo data is acceptable
26
+ - default to at least `5` records for mock or smoke datasets unless the user asks for fewer
27
+ - destructive cleanup is acceptable only when the record scope is explicit
28
+
29
+ Known current test backend:
30
+
31
+ - use an explicitly provided non-production backend
32
+
33
+ ## Production Environment
34
+
35
+ Use production for:
36
+
37
+ - live data entry
38
+ - live business record updates
39
+ - comments and workflow actions on real records
40
+ - controlled data correction or deletion
41
+
42
+ Production behavior:
43
+
44
+ - prefer search or get before any write
45
+ - restate the exact app and record scope before update or delete
46
+ - do not create mock, smoke, or demo data unless the user explicitly asks for it
47
+ - for bulk changes, summarize the target count before execution and the affected ids after execution
48
+ - destructive actions need explicit confirmation in the conversation context
49
+
50
+ Production guardrails:
51
+
52
+ - never assume a record id, app id, or workspace id
53
+ - treat `record_delete` as high risk
54
+ - if the task can be answered read-only, do not write
55
+
56
+ ## Reporting Rule
57
+
58
+ For app-user operations, always report:
59
+
60
+ - active environment
61
+ - target app
62
+ - operation type: read, create, update, delete, or workflow action
63
+ - affected record count or ids