@qingflow-tech/qingflow-app-user-mcp 1.0.43 → 1.1.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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-user-mcp@1.0.43
6
+ npm install @qingflow-tech/qingflow-app-user-mcp@1.1.0
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.43 qingflow-app-user-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.1.0 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
@@ -1,7 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
2
5
 
3
- import { getPackageRoot, spawnServer } from "../lib/runtime.mjs";
6
+ const PKG_BY_BIN = {
7
+ qingflow: "qingflow-cli",
8
+ "qingflow-app-user-mcp": "qingflow-app-user-mcp",
9
+ "qingflow-app-builder-mcp": "qingflow-app-builder-mcp",
10
+ };
4
11
 
5
- const packageRoot = getPackageRoot(import.meta.url);
12
+ function resolvePackageModule(metaUrl, ...segments) {
13
+ const scriptPath = fileURLToPath(metaUrl);
14
+ const scriptDir = path.dirname(scriptPath);
15
+ const direct = path.join(scriptDir, ...segments);
16
+ if (fs.existsSync(direct)) {
17
+ return pathToFileURL(direct).href;
18
+ }
19
+ if (path.basename(scriptDir) === ".bin") {
20
+ const binName = path.basename(scriptPath);
21
+ const pkgName = PKG_BY_BIN[binName];
22
+ if (!pkgName) {
23
+ throw new Error(`Unknown qingflow command: ${binName}`);
24
+ }
25
+ const scope = process.env.QINGFLOW_NPM_SCOPE || "@qingflow-tech";
26
+ const fromBin = path.join(scriptDir, "..", scope, pkgName, "npm", ...segments);
27
+ if (fs.existsSync(fromBin)) {
28
+ return pathToFileURL(fromBin).href;
29
+ }
30
+ }
31
+ throw new Error(`Cannot locate ${segments.join("/")} from ${scriptPath}`);
32
+ }
6
33
 
34
+ const { getPackageRoot, spawnServer } = await import(resolvePackageModule(import.meta.url, "lib", "runtime.mjs"));
35
+ const packageRoot = getPackageRoot(import.meta.url);
7
36
  spawnServer(packageRoot, process.argv.slice(2), "qingflow-app-user-mcp", { allowRuntimeBootstrap: false });
@@ -5,6 +5,12 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  const WINDOWS = process.platform === "win32";
7
7
 
8
+ const PKG_BY_BIN = {
9
+ qingflow: "qingflow-cli",
10
+ "qingflow-app-user-mcp": "qingflow-app-user-mcp",
11
+ "qingflow-app-builder-mcp": "qingflow-app-builder-mcp",
12
+ };
13
+
8
14
  function runChecked(command, args, options = {}) {
9
15
  const result = spawnSync(command, args, {
10
16
  encoding: "utf8",
@@ -28,7 +34,18 @@ function commandWorks(command, args) {
28
34
  }
29
35
 
30
36
  export function getPackageRoot(metaUrl) {
31
- return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..", "..");
37
+ const scriptPath = fileURLToPath(metaUrl);
38
+ const scriptDir = path.dirname(scriptPath);
39
+ if (path.basename(scriptDir) === ".bin") {
40
+ const binName = path.basename(scriptPath);
41
+ const pkgName = PKG_BY_BIN[binName];
42
+ if (!pkgName) {
43
+ throw new Error(`Unknown qingflow command: ${binName}`);
44
+ }
45
+ const scope = process.env.QINGFLOW_NPM_SCOPE || "@qingflow-tech";
46
+ return path.join(scriptDir, "..", scope, pkgName);
47
+ }
48
+ return path.resolve(scriptDir, "..", "..");
32
49
  }
33
50
 
34
51
  export function getCodexHome() {
@@ -480,6 +497,17 @@ function getVenvPip(packageRoot) {
480
497
  : path.join(getVenvDir(packageRoot), "bin", "pip");
481
498
  }
482
499
 
500
+ function findOfflineProjectWheel(findLinksDir) {
501
+ const wheels = fs
502
+ .readdirSync(findLinksDir)
503
+ .filter((name) => /^qingflow_mcp-.*\.whl$/i.test(name))
504
+ .sort();
505
+ if (wheels.length === 0) {
506
+ throw new Error(`Offline install expected a qingflow_mcp wheel in ${findLinksDir}`);
507
+ }
508
+ return path.join(findLinksDir, wheels[wheels.length - 1]);
509
+ }
510
+
483
511
  export function findPython() {
484
512
  const preferred = process.env.QINGFLOW_MCP_PYTHON?.trim();
485
513
  const candidates = preferred
@@ -528,7 +556,20 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
528
556
  }
529
557
 
530
558
  const pip = getVenvPip(packageRoot);
531
- runChecked(pip, ["install", "--disable-pip-version-check", "."], { cwd: packageRoot });
559
+ const pipArgs = ["install", "--disable-pip-version-check"];
560
+ const offlineFindLinks = process.env.QINGFLOW_MCP_PIP_FIND_LINKS?.trim();
561
+ if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1") {
562
+ pipArgs.push("--no-index");
563
+ }
564
+ if (offlineFindLinks) {
565
+ pipArgs.push("--find-links", offlineFindLinks);
566
+ }
567
+ if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1" && offlineFindLinks) {
568
+ pipArgs.push(findOfflineProjectWheel(offlineFindLinks));
569
+ } else {
570
+ pipArgs.push(".");
571
+ }
572
+ runChecked(pip, pipArgs, { cwd: packageRoot });
532
573
 
533
574
  fs.writeFileSync(
534
575
  stampPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-user-mcp",
3
- "version": "1.0.43",
3
+ "version": "1.1.0",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
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 = "1.0.43"
7
+ version = "1.1.0"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow App User
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  ## Overview
13
13
 
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow MCP Setup
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  ## Overview
13
13
 
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow Record Analysis
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  Use this skill only for final statistical conclusions: counts, distributions, ratios, averages, rankings, trends, comparisons, and analysis reports.
13
13
 
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow Record Delete
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  ## Default Path
13
13
 
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow Record Import
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  ## Default Path
13
13
 
@@ -8,7 +8,7 @@ metadata:
8
8
 
9
9
  # Qingflow Record Insert
10
10
 
11
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
12
12
 
13
13
  ## Default Path
14
14
 
@@ -84,7 +84,7 @@ Without `record_id` / `workflow_node_id` / `fields-file`, the result is a static
84
84
  ## Working Rules
85
85
 
86
86
  1. Start with `record_insert_schema_get`
87
- 2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`
87
+ 2. Read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, `payload_template`, field-level `expected_format`, `example_value`, and `options`
88
88
  3. Inside every field bucket, read field-level `linkage` first when present; it is the canonical static hint for linked visibility, reference-driven auto fill, or formula-driven fields
89
89
  4. Inside `optional_fields`, pay special attention to any field with `may_become_required=true`; these are writable fields that can become required when linked visibility or option-driven rules activate
90
90
  5. Build `items` as `[{"fields": {...}}]`, where each `fields` map uses field titles from the insert schema
@@ -102,6 +102,8 @@ Without `record_id` / `workflow_node_id` / `fields-file`, the result is a static
102
102
  17. Treat nested schema shape as guidance, not a brittle contract; do not hard-code transient implementation details like optional nested `field_id` shape when composing inserts
103
103
  18. For `partial_success`, read `created_record_ids`, then repair only the failed `items[].row_number` using `failed_fields`; never retry the whole batch after any row has `write_executed=true`
104
104
  19. Do not put Qingflow system fields in `fields`: `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, `流程标题`. They are generated by the platform and can be read after creation, not manually inserted.
105
+ 20. When the user asks to add sample records after system setup, still generate values from schema: required fields first, select values from `options`, scalar/date/amount values from `expected_format` and `example_value`; do not invent values outside the insert schema.
106
+ 21. For ratio, completion-rate, score, and percentage-like fields, obey the schema's `expected_format/example_value`. If the field was modeled as money/amount or otherwise only accepts integers, do not invent decimal percentages; either use an integer value that matches the schema or report that the field should be modeled as `number` for decimal ratios.
105
107
 
106
108
  ## Field Notes
107
109
 
@@ -146,6 +148,7 @@ qingflow --json record insert --app-key APP_KEY --items-file records.json
146
148
  - Do not fill platform system fields such as `数据ID`, `编号`, `申请人`, `创建时间`, or `更新时间`
147
149
  - Do not flatten subtable leaf fields to the top level
148
150
  - Do not invent member / department / relation candidates outside schema or candidate scope
151
+ - Do not invent select option labels outside schema `options`
149
152
  - Do not pre-query or silently guess member / department / relation ids when a natural string is enough
150
153
  - Do not retry a whole batch after `created_record_ids` is non-empty
151
154
  - Do not bind logic to a transient nested schema serialization detail when the field title and parent table already identify the legal payload shape
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow Record Update
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  ## Default Path
13
13
 
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow Task Ops
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.2`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  ## Overview
13
13
 
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b1017"
8
+ _FALLBACK_VERSION = "0.2.0b1099"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -203,10 +203,6 @@ class LayoutPreset(str, Enum):
203
203
  single_section = "single_section"
204
204
 
205
205
 
206
- class FlowPreset(str, Enum):
207
- basic_approval = "basic_approval"
208
- basic_fill_then_approve = "basic_fill_then_approve"
209
-
210
206
 
211
207
  class ViewsPreset(str, Enum):
212
208
  default_table = "default_table"
@@ -2342,7 +2338,6 @@ AppReadSummaryResponse = AppGetResponse
2342
2338
  AppFieldsReadResponse = AppGetFieldsResponse
2343
2339
  AppLayoutReadResponse = AppGetLayoutResponse
2344
2340
  AppViewsReadResponse = AppGetViewsResponse
2345
- AppFlowReadResponse = AppGetFlowResponse
2346
2341
  AppChartsReadResponse = AppGetChartsResponse
2347
2342
 
2348
2343
 
@@ -2432,40 +2427,6 @@ class LayoutPlanRequest(StrictModel):
2432
2427
  return payload
2433
2428
 
2434
2429
 
2435
- class FlowPlanRequest(StrictModel):
2436
- app_key: str
2437
- mode: str = "replace"
2438
- nodes: list[FlowNodePatch] = Field(default_factory=list)
2439
- transitions: list[FlowTransitionPatch] = Field(default_factory=list)
2440
- preset: FlowPreset | None = None
2441
-
2442
- @model_validator(mode="before")
2443
- @classmethod
2444
- def normalize_mode_alias(cls, value: Any) -> Any:
2445
- if not isinstance(value, dict):
2446
- return value
2447
- payload = dict(value)
2448
- if str(payload.get("mode") or "").strip().lower() == "overwrite":
2449
- payload["mode"] = "replace"
2450
- raw_preset = payload.get("preset")
2451
- if raw_preset is None and isinstance(payload.get("base_preset"), str):
2452
- raw_preset = payload["base_preset"]
2453
- payload["preset"] = raw_preset
2454
- if isinstance(raw_preset, str):
2455
- normalized_preset = raw_preset.strip().lower()
2456
- preset_aliases = {
2457
- "default_approval": FlowPreset.basic_approval.value,
2458
- "approval": FlowPreset.basic_approval.value,
2459
- "basic approval": FlowPreset.basic_approval.value,
2460
- "default_fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2461
- "default-fill-then-approve": FlowPreset.basic_fill_then_approve.value,
2462
- "fill_then_approve": FlowPreset.basic_fill_then_approve.value,
2463
- }
2464
- if normalized_preset in preset_aliases:
2465
- payload["preset"] = preset_aliases[normalized_preset]
2466
- return payload
2467
-
2468
-
2469
2430
  class ViewsPlanRequest(StrictModel):
2470
2431
  app_key: str
2471
2432
  upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)