@josephyan/qingflow-app-user-mcp 0.2.0-beta.48 → 0.2.0-beta.49

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 @josephyan/qingflow-app-user-mcp@0.2.0-beta.48
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.49
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.48 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.49 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.48",
3
+ "version": "0.2.0-beta.49",
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 = "0.2.0b48"
7
+ version = "0.2.0b49"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -984,14 +984,26 @@ class AiBuilderFacade:
984
984
  candidate_key = str(item.get("app_key") or "").strip()
985
985
  if not candidate_key:
986
986
  continue
987
+ tag_ids = _coerce_int_list(item.get("tag_ids"))
987
988
  tag_id = _coerce_positive_int(item.get("tag_id"))
988
- if package_tag_id is not None and package_tag_id > 0 and tag_id != package_tag_id:
989
- continue
989
+ if tag_id is not None and tag_id not in tag_ids:
990
+ tag_ids.append(tag_id)
991
+ if package_tag_id is not None and package_tag_id > 0:
992
+ try:
993
+ base = self.apps.app_get_base(profile=profile, app_key=candidate_key, include_raw=True)
994
+ except (QingflowApiError, RuntimeError):
995
+ continue
996
+ result = base.get("result") if isinstance(base.get("result"), dict) else {}
997
+ resolved_tag_ids = _coerce_int_list(result.get("tagIds"))
998
+ if resolved_tag_ids:
999
+ tag_ids = resolved_tag_ids
1000
+ if package_tag_id not in tag_ids:
1001
+ continue
990
1002
  matches.append(
991
1003
  {
992
1004
  "app_key": candidate_key,
993
1005
  "app_name": title,
994
- "tag_ids": [tag_id] if tag_id is not None else [],
1006
+ "tag_ids": tag_ids,
995
1007
  }
996
1008
  )
997
1009
  if not matches:
@@ -5,6 +5,7 @@ import re
5
5
  import time
6
6
  from dataclasses import dataclass
7
7
  from datetime import UTC, datetime
8
+ from decimal import Decimal, InvalidOperation
8
9
  from typing import Any, cast
9
10
 
10
11
  from mcp.server.fastmcp import FastMCP
@@ -4265,6 +4266,8 @@ class RecordTools(ToolBase):
4265
4266
  return [_attachment_value(value) for value in _expand_values(raw_values)]
4266
4267
  if field.que_type in RELATION_QUE_TYPES:
4267
4268
  return [_relation_value(value) for value in _expand_values(raw_values)]
4269
+ if field.que_type == 8:
4270
+ return [{"value": _normalize_amount_value_for_write(field, raw_values[0])}]
4268
4271
  return [{"value": _stringify_json(raw_values[0])}]
4269
4272
 
4270
4273
  def _normalize_subtable_rows(
@@ -6370,6 +6373,71 @@ def _coerce_amount(value: JSONValue) -> float | None:
6370
6373
  return None
6371
6374
 
6372
6375
 
6376
+ def _normalize_amount_value_for_write(field: FormField, value: JSONValue) -> str:
6377
+ if isinstance(value, bool) or value is None:
6378
+ raise RecordInputError(
6379
+ message=f"field '{field.que_title}' requires a numeric amount",
6380
+ error_code="INVALID_AMOUNT_VALUE",
6381
+ fix_hint="Pass a numeric value or numeric string for the amount field.",
6382
+ details={"field": _field_ref_payload(field), "received_value": value},
6383
+ )
6384
+ if isinstance(value, int):
6385
+ return str(value)
6386
+ if isinstance(value, float):
6387
+ decimal_value = Decimal(str(value))
6388
+ elif isinstance(value, str):
6389
+ text = value.strip()
6390
+ if not text:
6391
+ raise RecordInputError(
6392
+ message=f"field '{field.que_title}' requires a numeric amount",
6393
+ error_code="INVALID_AMOUNT_VALUE",
6394
+ fix_hint="Pass a numeric value or numeric string for the amount field.",
6395
+ details={"field": _field_ref_payload(field), "received_value": value},
6396
+ )
6397
+ negative = False
6398
+ if text.startswith("(") and text.endswith(")"):
6399
+ negative = True
6400
+ text = text[1:-1].strip()
6401
+ for symbol in ("¥", "¥", "$"):
6402
+ text = text.replace(symbol, "")
6403
+ text = text.replace(",", "").replace(" ", "")
6404
+ if negative and text and not text.startswith("-"):
6405
+ text = f"-{text}"
6406
+ try:
6407
+ decimal_value = Decimal(text)
6408
+ except InvalidOperation as exc:
6409
+ raise RecordInputError(
6410
+ message=f"field '{field.que_title}' requires a numeric amount",
6411
+ error_code="INVALID_AMOUNT_VALUE",
6412
+ fix_hint="Pass a numeric value or numeric string for the amount field.",
6413
+ details={"field": _field_ref_payload(field), "received_value": value},
6414
+ ) from exc
6415
+ else:
6416
+ raise RecordInputError(
6417
+ message=f"field '{field.que_title}' requires a numeric amount",
6418
+ error_code="INVALID_AMOUNT_VALUE",
6419
+ fix_hint="Pass a numeric value or numeric string for the amount field.",
6420
+ details={"field": _field_ref_payload(field), "received_value": value},
6421
+ )
6422
+
6423
+ allow_decimal = bool((field.raw or {}).get("canDecimal"))
6424
+ if not allow_decimal:
6425
+ integral_value = decimal_value.to_integral_value()
6426
+ if decimal_value != integral_value:
6427
+ raise RecordInputError(
6428
+ message=f"field '{field.que_title}' requires an integer amount",
6429
+ error_code="INVALID_AMOUNT_VALUE",
6430
+ fix_hint="Pass an integer value for this amount field, or remove the decimal part.",
6431
+ details={"field": _field_ref_payload(field), "received_value": value},
6432
+ )
6433
+ return format(integral_value, "f").split(".")[0]
6434
+
6435
+ normalized = format(decimal_value, "f")
6436
+ if "." in normalized:
6437
+ normalized = normalized.rstrip("0").rstrip(".")
6438
+ return normalized or "0"
6439
+
6440
+
6373
6441
  def _to_time_bucket(value: JSONValue, bucket: str) -> str:
6374
6442
  text = _stringify_json(value).strip()
6375
6443
  if not text: