@qingflow-tech/qingflow-cli 1.1.42 → 1.1.43-beta.1

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 (42) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/pyproject.toml +3 -2
  4. package/src/qingflow_mcp/app_form_compat.py +115 -0
  5. package/src/qingflow_mcp/app_form_validator.py +199 -0
  6. package/src/qingflow_mcp/builder_facade/app_form.py +2836 -0
  7. package/src/qingflow_mcp/builder_facade/app_form_schema.py +269 -0
  8. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +37 -0
  9. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +1766 -0
  10. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +76 -0
  11. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +76 -0
  12. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +76 -0
  13. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +223 -0
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +538 -0
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +76 -0
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +76 -0
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +149 -0
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +76 -0
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +76 -0
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +76 -0
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +76 -0
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +92 -0
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +76 -0
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +76 -0
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +338 -0
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +137 -0
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +92 -0
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +92 -0
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +810 -0
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +76 -0
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +713 -0
  32. package/src/qingflow_mcp/builder_facade/service.py +5 -2
  33. package/src/qingflow_mcp/cli/commands/builder.py +91 -28
  34. package/src/qingflow_mcp/cli/json_io.py +21 -2
  35. package/src/qingflow_mcp/cli/main.py +3 -39
  36. package/src/qingflow_mcp/cloud_oauth_proxy.py +96 -3
  37. package/src/qingflow_mcp/public_surface.py +5 -2
  38. package/src/qingflow_mcp/response_trim.py +8 -0
  39. package/src/qingflow_mcp/server_app_builder.py +38 -86
  40. package/src/qingflow_mcp/server_cloud.py +66 -1
  41. package/src/qingflow_mcp/tools/ai_builder_tools.py +93 -100
  42. package/src/qingflow_mcp/tools/app_tools.py +46 -65
@@ -9191,6 +9191,7 @@ class AiBuilderFacade:
9191
9191
  remove_fields: list[FieldRemovePatch],
9192
9192
  inline_form_layout_sections: list[LayoutSectionPatch] | None = None,
9193
9193
  ) -> JSONObject:
9194
+ # TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
9194
9195
  apply_started_at = time.perf_counter()
9195
9196
  schema_write_ms: int | None = None
9196
9197
  inline_layout_ms: int | None = None
@@ -10182,6 +10183,7 @@ class AiBuilderFacade:
10182
10183
  sections: list[LayoutSectionPatch],
10183
10184
  publish: bool = True,
10184
10185
  ) -> JSONObject:
10186
+ # TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
10185
10187
  requested_sections = [section.model_dump(mode="json", exclude_none=True) for section in sections]
10186
10188
  normalized_args = {
10187
10189
  "app_key": app_key,
@@ -22236,7 +22238,7 @@ def _compile_code_block_binding_fields(
22236
22238
  alias_name = str(output_item.get("alias") or "").strip()
22237
22239
  alias_path = str(output_item.get("path") or "").strip()
22238
22240
  existing_alias = existing_alias_by_name.get(alias_name) or {}
22239
- alias_id = _coerce_positive_int(existing_alias.get("alias_id"))
22241
+ alias_id = _coerce_positive_int(output_item.get("alias_id")) or _coerce_positive_int(existing_alias.get("alias_id"))
22240
22242
  compiled_outputs.append(
22241
22243
  {
22242
22244
  "alias_name": alias_name,
@@ -22649,7 +22651,8 @@ def _compile_q_linker_binding_fields(
22649
22651
  {
22650
22652
  "alias_name": alias_name,
22651
22653
  "alias_path": alias_path,
22652
- "alias_id": _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
22654
+ "alias_id": _coerce_positive_int(output_item.get("alias_id"))
22655
+ or _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
22653
22656
  "target_field": {
22654
22657
  "field_id": target_field.get("field_id"),
22655
22658
  "que_id": target_ref,
@@ -4,6 +4,7 @@ import argparse
4
4
  from copy import deepcopy
5
5
 
6
6
  from ..context import CliContext
7
+ from ...app_form_validator import AppFormValidationError, validate_declaration
7
8
  from ..json_io import load_json_value
8
9
  from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
9
10
 
@@ -98,6 +99,29 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
98
99
  package_apply.add_argument("--config-file", required=True)
99
100
  package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary", force_json_output=True)
100
101
 
102
+ app_form = builder_subparsers.add_parser("app-form", help="声明式应用表单")
103
+ app_form_subparsers = app_form.add_subparsers(dest="builder_app_form_command", required=True)
104
+
105
+ app_form_schema = app_form_subparsers.add_parser("schema", help="读取 AppForm Schema")
106
+ app_form_schema.add_argument("--schema-version")
107
+ app_form_schema.add_argument("--field-type")
108
+ app_form_schema.set_defaults(handler=_handle_app_form_schema, format_hint="builder_summary", force_json_output=True)
109
+
110
+ app_form_get = app_form_subparsers.add_parser("get", help="读取 canonical AppForm")
111
+ app_form_get.add_argument("--app-key", required=True)
112
+ app_form_get.add_argument("--schema-version")
113
+ app_form_get.add_argument("--being-draft", action=argparse.BooleanOptionalAction, default=False)
114
+ app_form_get.set_defaults(handler=_handle_app_form_get, format_hint="builder_summary", force_json_output=True)
115
+
116
+ app_form_apply = app_form_subparsers.add_parser("apply", help="应用完整 AppForm 声明")
117
+ app_form_apply.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
118
+ app_form_apply.set_defaults(handler=_handle_app_form_apply, format_hint="builder_summary", force_json_output=True)
119
+
120
+ app_form_validate = app_form_subparsers.add_parser("validate", help="只校验 AppForm 声明,不访问后端")
121
+ app_form_validate.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
122
+ app_form_validate.add_argument("--schema-version", help="固定使用的 AppForm Schema 版本")
123
+ app_form_validate.set_defaults(handler=_handle_app_form_validate, format_hint="builder_summary", force_json_output=True)
124
+
101
125
  app = builder_subparsers.add_parser("app", help="应用")
102
126
  app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
103
127
 
@@ -113,6 +137,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
113
137
  app_release_lock.add_argument("--lock-owner-name", required=True)
114
138
  app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)
115
139
 
140
+ app_delete = app_subparsers.add_parser("delete", help="删除应用")
141
+ app_delete.add_argument("--app-key", required=True)
142
+ app_delete.set_defaults(handler=_handle_app_delete, format_hint="builder_summary", force_json_output=True)
143
+
116
144
  app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
117
145
  app_get.add_argument(
118
146
  "builder_app_get_section",
@@ -202,34 +230,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
202
230
  portal_delete.add_argument("--dash-key", required=True)
203
231
  portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
204
232
 
205
- schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
206
- schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
207
- schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
208
- schema_apply_apply.add_argument("--app-key", default="")
209
- schema_apply_apply.add_argument("--package-id", type=int)
210
- schema_apply_apply.add_argument("--app-name", default="")
211
- schema_apply_apply.add_argument("--app-title", default="")
212
- schema_apply_apply.add_argument("--icon")
213
- schema_apply_apply.add_argument("--color")
214
- schema_apply_apply.add_argument("--visibility-file")
215
- schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
216
- schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 对象;每项可带 client_key/app_name/form,支持 relation target_app_ref")
217
- schema_apply_apply.add_argument("--form-file", help="单应用表单结构 JSON 数组;form[].rows[][] 同时声明字段和表单分组布局")
218
- schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
219
- schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
220
- schema_apply_apply.add_argument("--remove-fields-file")
221
- schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary", force_json_output=True)
222
-
223
- layout_apply = builder_subparsers.add_parser("layout", help="布局")
224
- layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
225
- layout_apply_apply = layout_apply_subparsers.add_parser("apply", help="执行布局变更")
226
- layout_apply_apply.add_argument("--app-key", default="")
227
- layout_apply_apply.add_argument("--apps-file", help="多应用布局 JSON 数组;每项包含 app_key/mode?/publish?/sections")
228
- layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
229
- layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
230
- layout_apply_apply.add_argument("--sections-file")
231
- layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
232
-
233
233
  views_apply = builder_subparsers.add_parser("views", help="视图")
234
234
  views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
235
235
  views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
@@ -331,6 +331,67 @@ def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
331
331
  return context.builder.builder_tool_contract(tool_name=args.tool_name)
332
332
 
333
333
 
334
+ def _handle_app_form_schema(args: argparse.Namespace, context: CliContext) -> dict:
335
+ return context.builder.app_form_schema(
336
+ schema_version=args.schema_version,
337
+ field_type=args.field_type,
338
+ )
339
+
340
+
341
+ def _handle_app_form_get(args: argparse.Namespace, context: CliContext) -> dict:
342
+ return context.builder.app_form_get(
343
+ profile=args.profile,
344
+ app_key=args.app_key,
345
+ schema_version=args.schema_version,
346
+ being_draft=bool(args.being_draft),
347
+ )
348
+
349
+
350
+ def _handle_app_form_apply(args: argparse.Namespace, context: CliContext) -> dict:
351
+ payload = load_json_value(
352
+ args.file,
353
+ option_name="--file",
354
+ max_bytes=2 * 1024 * 1024,
355
+ max_depth=64,
356
+ )
357
+ if not isinstance(payload, dict):
358
+ raise_config_error("builder app-form apply --file must contain a JSON object")
359
+ return context.builder.app_form_apply(profile=args.profile, spec=payload)
360
+
361
+
362
+ def _handle_app_form_validate(args: argparse.Namespace, _context: CliContext) -> dict:
363
+ payload = load_json_value(
364
+ args.file,
365
+ option_name="--file",
366
+ max_bytes=2 * 1024 * 1024,
367
+ max_depth=64,
368
+ )
369
+ if not isinstance(payload, dict):
370
+ raise_config_error("builder app-form validate --file must contain a JSON object")
371
+ try:
372
+ result = validate_declaration(payload, schema_version=args.schema_version)
373
+ except AppFormValidationError as error:
374
+ return {
375
+ "category": "validation",
376
+ "status": "failed",
377
+ "ok": False,
378
+ "error_code": "INVALID_SPEC",
379
+ "message": "AppForm declaration failed validation",
380
+ "details": {
381
+ "schemaVersion": error.schema_version,
382
+ "apiVersion": error.api_version,
383
+ "issues": error.issues,
384
+ },
385
+ }
386
+ except (ValueError, RuntimeError) as error:
387
+ raise_config_error(str(error), error_code="VALIDATION_ERROR")
388
+ return {"category": "validation", "status": "success", "ok": True, **result, "message": "AppForm declaration is valid"}
389
+
390
+
391
+ def _handle_app_delete(args: argparse.Namespace, context: CliContext) -> dict:
392
+ return context.builder.app_delete(profile=args.profile, app_key=args.app_key)
393
+
394
+
334
395
  def _handle_icon_catalog(args: argparse.Namespace, context: CliContext) -> dict:
335
396
  return context.builder.workspace_icon_catalog_get(profile=args.profile)
336
397
 
@@ -554,6 +615,8 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
554
615
  return context.builder.chart_get(profile=args.profile, chart_id=args.chart_id)
555
616
 
556
617
 
618
+ # TODO(deprecation): The legacy schema/layout CLI routes are removed. Keep the implementation below only
619
+ # while internal migration and focused regression coverage still depend on the imperative adapters.
557
620
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
558
621
  apps_payload = _load_apps_file_arg(args.apps_file)
559
622
  apps = apps_payload["apps"]
@@ -8,7 +8,13 @@ from typing import Any
8
8
  from ..errors import QingflowApiError
9
9
 
10
10
 
11
- def load_json_value(path: str, *, option_name: str) -> Any:
11
+ def load_json_value(
12
+ path: str,
13
+ *,
14
+ option_name: str,
15
+ max_bytes: int | None = None,
16
+ max_depth: int | None = None,
17
+ ) -> Any:
12
18
  if not path:
13
19
  raise QingflowApiError.config_error(f"{option_name} is required")
14
20
  if path == "-":
@@ -18,10 +24,23 @@ def load_json_value(path: str, *, option_name: str) -> Any:
18
24
  raw = Path(path).expanduser().read_text(encoding="utf-8")
19
25
  except OSError as exc:
20
26
  raise QingflowApiError.config_error(f"failed to read {option_name} from '{path}': {exc}") from exc
27
+ if max_bytes is not None and len(raw.encode("utf-8")) > max_bytes:
28
+ raise QingflowApiError.config_error(f"{option_name} exceeds the {max_bytes}-byte limit")
21
29
  try:
22
- return json.loads(raw)
30
+ value = json.loads(raw)
23
31
  except json.JSONDecodeError as exc:
24
32
  raise QingflowApiError.config_error(f"{option_name} must contain valid JSON: {exc}") from exc
33
+ if max_depth is not None:
34
+ stack = [(value, 1)]
35
+ while stack:
36
+ item, depth = stack.pop()
37
+ if depth > max_depth:
38
+ raise QingflowApiError.config_error(f"{option_name} exceeds the maximum nesting depth {max_depth}")
39
+ if isinstance(item, dict):
40
+ stack.extend((child, depth + 1) for child in item.values())
41
+ elif isinstance(item, list):
42
+ stack.extend((child, depth + 1) for child in item)
43
+ return value
25
44
 
26
45
 
27
46
  def load_json_object(path: str, *, option_name: str) -> dict[str, Any]:
@@ -79,23 +79,6 @@ def run(
79
79
  return 2
80
80
  except SystemExit as exc:
81
81
  return int(exc.code or 0)
82
- try:
83
- _validate_conditional_args(args)
84
- except _CliArgumentError as exc:
85
- if _should_force_json_output_argv(normalized_argv):
86
- payload = {
87
- "category": "config",
88
- "status": "failed",
89
- "error_code": "ARGUMENT_ERROR",
90
- "message": exc.message,
91
- "details": {"usage": exc.usage.strip(), "prog": exc.prog},
92
- }
93
- payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
94
- emit_json_result(payload, stream=out)
95
- return 2
96
- err.write(exc.usage)
97
- err.write(f"{exc.prog}: error: {exc.message}\n")
98
- return 2
99
82
  setattr(args, "_stdin", sys.stdin)
100
83
  setattr(args, "_stdout_stream", out)
101
84
  setattr(args, "_stderr_stream", err)
@@ -161,21 +144,6 @@ def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
161
144
  return 0
162
145
 
163
146
 
164
- def _validate_conditional_args(args: argparse.Namespace) -> None:
165
- if (
166
- getattr(args, "command", "") in {"builder", "build"}
167
- and getattr(args, "builder_command", "") == "layout"
168
- and getattr(args, "builder_layout_command", "") == "apply"
169
- and not getattr(args, "apps_file", None)
170
- and not getattr(args, "sections_file", None)
171
- ):
172
- raise _CliArgumentError(
173
- prog="qingflow builder layout apply",
174
- message="--sections-file is required",
175
- usage="usage: qingflow builder layout apply --app-key APP_KEY --sections-file SECTIONS.json\n",
176
- )
177
-
178
-
179
147
  def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
180
148
  return "--json" in argv
181
149
 
@@ -231,8 +199,10 @@ def _should_force_json_output_argv(argv: list[str]) -> bool:
231
199
  return False
232
200
  section = tokens[1]
233
201
  action = tokens[2]
234
- if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
202
+ if section in {"package", "button", "associated-resource", "associated-resources", "portal", "views", "flow", "charts"}:
235
203
  return action == "apply"
204
+ if section == "app-form":
205
+ return action in {"schema", "get", "validate", "apply"}
236
206
  if section == "publish":
237
207
  return action == "verify"
238
208
  if section == "app":
@@ -283,10 +253,6 @@ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
283
253
  return "portal_apply"
284
254
  if section == "portal" and getattr(args, "builder_portal_command", "") == "delete":
285
255
  return "portal_delete"
286
- if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
287
- return "app_schema_apply"
288
- if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
289
- return "app_layout_apply"
290
256
  if section == "views" and getattr(args, "builder_views_command", "") == "apply":
291
257
  return "app_views_apply"
292
258
  if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
@@ -312,8 +278,6 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
312
278
  "associated-resource": "app_associated_resources_apply",
313
279
  "associated-resources": "app_associated_resources_apply",
314
280
  "portal": "portal_apply",
315
- "schema": "app_schema_apply",
316
- "layout": "app_layout_apply",
317
281
  "views": "app_views_apply",
318
282
  "flow": "app_flow_apply",
319
283
  "charts": "app_charts_apply",
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from dataclasses import dataclass
4
5
  from typing import Any
5
6
  from urllib.parse import urlsplit, urlunsplit
@@ -22,8 +23,16 @@ from .json_types import JSONObject
22
23
 
23
24
  PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource"
24
25
  AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server"
26
+ OAUTH_ENDPOINT_PATHS = {
27
+ "authorization_endpoint": "/oauth/authorize",
28
+ "token_endpoint": "/oauth/token",
29
+ "registration_endpoint": "/oauth/register",
30
+ }
31
+ FORWARDED_REQUEST_HEADERS = {"accept", "content-type"}
25
32
  HOP_BY_HOP_RESPONSE_HEADERS = {
26
33
  "connection",
34
+ # httpx decodes compressed response bodies before exposing response.content.
35
+ "content-encoding",
27
36
  "keep-alive",
28
37
  "proxy-authenticate",
29
38
  "proxy-authorization",
@@ -58,10 +67,12 @@ class CloudOAuthProxyConfig:
58
67
 
59
68
  @property
60
69
  def resource_metadata_url(self) -> str:
70
+ return f"{self.metadata_issuer_url}{PROTECTED_RESOURCE_METADATA_PATH}"
71
+
72
+ @property
73
+ def metadata_issuer_url(self) -> str:
61
74
  parsed = urlsplit(self.resource_url)
62
- return urlunsplit(
63
- (parsed.scheme, parsed.netloc, PROTECTED_RESOURCE_METADATA_PATH, "", "")
64
- )
75
+ return urlunsplit((parsed.scheme, parsed.netloc, "", "", ""))
65
76
 
66
77
  def backend_metadata_path(self, request_path: str) -> str | None:
67
78
  if request_path == PROTECTED_RESOURCE_METADATA_PATH or request_path.startswith(
@@ -74,6 +85,11 @@ class CloudOAuthProxyConfig:
74
85
  return AUTHORIZATION_SERVER_METADATA_PATH
75
86
  return None
76
87
 
88
+ def backend_oauth_endpoint_path(self, request_path: str) -> str | None:
89
+ if request_path in OAUTH_ENDPOINT_PATHS.values():
90
+ return request_path
91
+ return None
92
+
77
93
 
78
94
  @dataclass(frozen=True, slots=True)
79
95
  class OAuthProxyResponse:
@@ -131,9 +147,86 @@ class OAuthServiceProxy:
131
147
  return OAuthProxyResponse(
132
148
  status_code=response.status_code,
133
149
  headers=headers,
150
+ body=self._rewrite_metadata_body(backend_path, response),
151
+ )
152
+
153
+ def request_oauth_endpoint(
154
+ self,
155
+ method: str,
156
+ backend_path: str,
157
+ query_string: bytes = b"",
158
+ body: bytes = b"",
159
+ headers: tuple[tuple[bytes, bytes], ...] = (),
160
+ ) -> OAuthProxyResponse:
161
+ url = f"{self.config.service_base_url}/{backend_path.lstrip('/')}"
162
+ if query_string:
163
+ url = f"{url}?{query_string.decode('latin1')}"
164
+ forwarded_headers = {
165
+ name.decode("latin1"): value.decode("latin1")
166
+ for name, value in headers
167
+ if name.decode("latin1").lower() in FORWARDED_REQUEST_HEADERS
168
+ }
169
+ try:
170
+ response = self._client.request(
171
+ method.upper(),
172
+ url,
173
+ content=body,
174
+ headers=forwarded_headers,
175
+ )
176
+ except httpx.RequestError as error:
177
+ raise QingflowApiError(
178
+ category="network",
179
+ message=f"OAuth service request failed: {error}",
180
+ ) from error
181
+
182
+ response_headers = tuple(
183
+ (name.encode("latin1"), value.encode("latin1"))
184
+ for name, value in response.headers.multi_items()
185
+ if name.lower() not in HOP_BY_HOP_RESPONSE_HEADERS
186
+ and name.lower() != "content-length"
187
+ )
188
+ return OAuthProxyResponse(
189
+ status_code=response.status_code,
190
+ headers=response_headers,
134
191
  body=response.content,
135
192
  )
136
193
 
194
+ def _rewrite_metadata_body(
195
+ self,
196
+ backend_path: str,
197
+ response: httpx.Response,
198
+ ) -> bytes:
199
+ if not 200 <= response.status_code < 300 or not response.content:
200
+ return response.content
201
+ try:
202
+ payload = json.loads(response.content)
203
+ except (UnicodeDecodeError, json.JSONDecodeError):
204
+ return response.content
205
+ if not isinstance(payload, dict):
206
+ return response.content
207
+
208
+ changed = False
209
+ issuer = self.config.metadata_issuer_url
210
+ if backend_path == PROTECTED_RESOURCE_METADATA_PATH:
211
+ for field_name in ("authorization_servers", "authorizationServers"):
212
+ if field_name in payload:
213
+ payload[field_name] = [issuer]
214
+ changed = True
215
+ elif backend_path == AUTHORIZATION_SERVER_METADATA_PATH:
216
+ if "issuer" in payload:
217
+ payload["issuer"] = issuer
218
+ changed = True
219
+ for field_name, endpoint_path in OAUTH_ENDPOINT_PATHS.items():
220
+ if field_name in payload:
221
+ payload[field_name] = f"{issuer}{endpoint_path}"
222
+ changed = True
223
+
224
+ if not changed:
225
+ return response.content
226
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode(
227
+ "utf-8"
228
+ )
229
+
137
230
 
138
231
  @dataclass(slots=True)
139
232
  class OAuthAccessTokenResolver:
@@ -142,6 +142,11 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
142
142
  PublicToolSpec(BUILDER_DOMAIN, "role_create", ("role_create",), ("builder", "role", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
143
  PublicToolSpec(BUILDER_DOMAIN, "app_release_edit_lock_if_mine", ("app_release_edit_lock_if_mine",), ("builder", "app", "release-edit-lock-if-mine"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
144
144
  PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
145
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_schema", ("app_form_schema",), ("builder", "app-form", "schema")),
146
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_validate", (), ("builder", "app-form", "validate"), mcp_public=False),
147
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_get", ("app_form_get",), ("builder", "app-form", "get"), cli_show_effective_context=True),
148
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_apply", ("app_form_apply",), ("builder", "app-form", "apply"), cli_show_effective_context=True, cli_context_write=True),
149
+ PublicToolSpec(BUILDER_DOMAIN, "app_delete", ("app_delete",), ("builder", "app", "delete"), cli_show_effective_context=True, cli_context_write=True),
145
150
  PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
146
151
  PublicToolSpec(BUILDER_DOMAIN, "app_custom_buttons_apply", ("app_custom_buttons_apply",), ("builder", "button", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
147
152
  PublicToolSpec(BUILDER_DOMAIN, "app_associated_resources_apply", ("app_associated_resources_apply",), ("builder", "associated-resource", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
@@ -158,8 +163,6 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
158
163
  PublicToolSpec(BUILDER_DOMAIN, "portal_get", ("portal_get",), ("builder", "portal", "get"), has_contract=True, cli_show_effective_context=True),
159
164
  PublicToolSpec(BUILDER_DOMAIN, "view_get", ("view_get",), ("builder", "view", "get"), has_contract=True, cli_show_effective_context=True),
160
165
  PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
161
- PublicToolSpec(BUILDER_DOMAIN, "app_schema_apply", ("app_schema_apply",), ("builder", "schema", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
162
- PublicToolSpec(BUILDER_DOMAIN, "app_layout_apply", ("app_layout_apply",), ("builder", "layout", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
163
166
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_get_schema", ("app_flow_get_schema",), ("builder", "flow", "schema"), has_contract=True, cli_show_effective_context=True),
164
167
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_get", ("app_flow_get",), ("builder", "flow", "get"), has_contract=True, cli_show_effective_context=True),
165
168
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
@@ -1190,6 +1190,10 @@ _register_policy(
1190
1190
  "role_create",
1191
1191
  "app_release_edit_lock_if_mine",
1192
1192
  "app_resolve",
1193
+ "app_form_schema",
1194
+ "app_form_get",
1195
+ "app_form_apply",
1196
+ "app_delete",
1193
1197
  "button_style_catalog_get",
1194
1198
  "app_custom_buttons_apply",
1195
1199
  "app_associated_resources_apply",
@@ -1198,7 +1202,11 @@ _register_policy(
1198
1202
  "app_get_layout",
1199
1203
  "app_get_views",
1200
1204
  "app_get_flow",
1205
+ "app_flow_get_schema",
1206
+ "app_flow_get",
1201
1207
  "app_get_charts",
1208
+ "app_get_buttons",
1209
+ "app_get_associated_resources",
1202
1210
  "app_schema_apply",
1203
1211
  "app_layout_apply",
1204
1212
  "app_flow_apply",