@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.
- package/README.md +37 -0
- package/docs/local-agent-install.md +332 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow-app-user-mcp.mjs +7 -0
- package/npm/lib/runtime.mjs +339 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow-app-user-mcp +15 -0
- package/skills/qingflow-app-user/SKILL.md +79 -0
- package/skills/qingflow-app-user/agents/openai.yaml +4 -0
- package/skills/qingflow-app-user/references/data-gotchas.md +29 -0
- package/skills/qingflow-app-user/references/environments.md +63 -0
- package/skills/qingflow-app-user/references/record-patterns.md +48 -0
- package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
- package/skills/qingflow-record-analysis/SKILL.md +158 -0
- package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +145 -0
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +125 -0
- package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
- package/skills/qingflow-record-analysis/references/dsl-templates.md +93 -0
- package/skills/qingflow-record-delete/SKILL.md +29 -0
- package/skills/qingflow-record-import/SKILL.md +31 -0
- package/skills/qingflow-record-insert/SKILL.md +58 -0
- package/skills/qingflow-record-update/SKILL.md +42 -0
- package/skills/qingflow-task-ops/SKILL.md +123 -0
- package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
- package/skills/qingflow-task-ops/references/environments.md +44 -0
- package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1836 -0
- package/src/qingflow_mcp/builder_facade/service.py +15044 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +44 -0
- package/src/qingflow_mcp/cli/commands/builder.py +538 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +334 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +178 -0
- package/src/qingflow_mcp/config.py +513 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/public_surface.py +233 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +470 -0
- package/src/qingflow_mcp/server.py +212 -0
- package/src/qingflow_mcp/server_app_builder.py +533 -0
- package/src/qingflow_mcp/server_app_user.py +362 -0
- package/src/qingflow_mcp/session_store.py +302 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2398 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
- package/src/qingflow_mcp/tools/app_tools.py +925 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +875 -0
- package/src/qingflow_mcp/tools/base.py +388 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2189 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14037 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
- package/src/qingflow_mcp/tools/task_tools.py +890 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- 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
|