rote-cli 0.13.0__tar.gz → 0.14.0__tar.gz

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 (76) hide show
  1. {rote_cli-0.13.0 → rote_cli-0.14.0}/PKG-INFO +41 -18
  2. {rote_cli-0.13.0 → rote_cli-0.14.0}/README.md +34 -11
  3. {rote_cli-0.13.0 → rote_cli-0.14.0}/pyproject.toml +5 -5
  4. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/__init__.py +1 -1
  5. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/__init__.py +10 -2
  6. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/_py_common.py +24 -17
  7. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/_ts_common.py +351 -3
  8. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/cloudflare.py +141 -46
  9. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/cli.py +213 -23
  10. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/__init__.py +139 -2
  11. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/__init__.py +9 -0
  12. rote_cli-0.14.0/src/rote/compiler/drivers/_heartbeat.py +52 -0
  13. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/anthropic_api.py +161 -22
  14. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/claude.py +129 -56
  15. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/openai_api.py +218 -19
  16. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/events.py +2 -0
  17. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/deploy_rote_cloud.py +10 -6
  18. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/baseline.py +5 -6
  19. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/pricing.py +70 -2
  20. rote_cli-0.14.0/src/rote/mcp/live_tools.py +303 -0
  21. {rote_cli-0.13.0 → rote_cli-0.14.0}/.gitignore +0 -0
  22. {rote_cli-0.13.0 → rote_cli-0.14.0}/LICENSE +0 -0
  23. {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/SKILL.md +0 -0
  24. {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/crystallization-heuristics.md +0 -0
  25. {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/eval-estimates.md +0 -0
  26. {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/implementation.md +0 -0
  27. {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/ir-schema.md +0 -0
  28. {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/llm-judge-extraction.md +0 -0
  29. {rote_cli-0.13.0 → rote_cli-0.14.0}/skills/rote-compile/references/node-kinds.md +0 -0
  30. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/__main__.py +0 -0
  31. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/_dbos.py +0 -0
  32. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/_common.py +0 -0
  33. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/dbos.py +0 -0
  34. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/dbos_ts.py +0 -0
  35. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/inngest.py +0 -0
  36. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/python.py +0 -0
  37. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/adapters/temporal.py +0 -0
  38. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/app_registry.py +0 -0
  39. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/cloud_auth.py +0 -0
  40. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/cloud_compile.py +0 -0
  41. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/_fs_tools.py +0 -0
  42. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/drivers/codex.py +0 -0
  43. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/compiler/update.py +0 -0
  44. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/config.py +0 -0
  45. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/contracts.py +0 -0
  46. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/deploy.py +0 -0
  47. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/__init__.py +0 -0
  48. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/empirical.py +0 -0
  49. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/estimate.py +0 -0
  50. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/priors.py +0 -0
  51. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/scorecard.py +0 -0
  52. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/sidecar.py +0 -0
  53. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/eval/tokens.py +0 -0
  54. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/inference/__init__.py +0 -0
  55. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/inference/_runtime_helper.py +0 -0
  56. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/init_wizard.py +0 -0
  57. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/ir.py +0 -0
  58. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/__init__.py +0 -0
  59. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/_runtime_helper.py +0 -0
  60. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/auth.py +0 -0
  61. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/registry.py +0 -0
  62. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/release.py +0 -0
  63. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/mcp/tokens.py +0 -0
  64. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/probe.py +0 -0
  65. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/py.typed +0 -0
  66. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/__init__.py +0 -0
  67. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/_node.py +0 -0
  68. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/cloudflare.py +0 -0
  69. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/dbos_ts.py +0 -0
  70. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/inngest.py +0 -0
  71. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/runners/temporal.py +0 -0
  72. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/__init__.py +0 -0
  73. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/backends.py +0 -0
  74. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/registry.py +0 -0
  75. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/serve/server.py +0 -0
  76. {rote_cli-0.13.0 → rote_cli-0.14.0}/src/rote/skill_source.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: rote-cli
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Compile AI agent skills into cheap, fast, deterministic pipelines that run without an LLM in the loop
5
5
  Project-URL: Homepage, https://roteskills.com
6
6
  Project-URL: Repository, https://github.com/trevhud/rote
@@ -29,11 +29,11 @@ Requires-Dist: pyyaml>=6.0
29
29
  Provides-Extra: api
30
30
  Requires-Dist: anthropic>=0.89; extra == 'api'
31
31
  Provides-Extra: dbos
32
- Requires-Dist: dbos>=2.26; extra == 'dbos'
32
+ Requires-Dist: dbos<3,>=2.26; extra == 'dbos'
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: anthropic>=0.89; extra == 'dev'
35
- Requires-Dist: dbos>=2.26; extra == 'dev'
36
- Requires-Dist: fastmcp>=3.4.2; extra == 'dev'
35
+ Requires-Dist: dbos<3,>=2.26; extra == 'dev'
36
+ Requires-Dist: fastmcp<4,>=3.4.2; extra == 'dev'
37
37
  Requires-Dist: httpx>=0.28; extra == 'dev'
38
38
  Requires-Dist: mypy>=1.10; extra == 'dev'
39
39
  Requires-Dist: openai>=2.45; extra == 'dev'
@@ -43,12 +43,12 @@ Requires-Dist: ruff>=0.6; extra == 'dev'
43
43
  Requires-Dist: temporalio>=1.7; extra == 'dev'
44
44
  Requires-Dist: types-pyyaml; extra == 'dev'
45
45
  Provides-Extra: mcp
46
- Requires-Dist: fastmcp>=3.4.2; extra == 'mcp'
46
+ Requires-Dist: fastmcp<4,>=3.4.2; extra == 'mcp'
47
47
  Requires-Dist: httpx>=0.28; extra == 'mcp'
48
48
  Provides-Extra: openai-api
49
49
  Requires-Dist: openai>=2.45; extra == 'openai-api'
50
50
  Provides-Extra: serve
51
- Requires-Dist: fastmcp>=3.4.2; extra == 'serve'
51
+ Requires-Dist: fastmcp<4,>=3.4.2; extra == 'serve'
52
52
  Requires-Dist: httpx>=0.28; extra == 'serve'
53
53
  Provides-Extra: temporal
54
54
  Requires-Dist: temporalio>=1.7; extra == 'temporal'
@@ -257,6 +257,24 @@ override with `--agent`. The output directory splits into `compiled/`
257
257
  (the agent's `pipeline.yaml`, `extracted/`, `signatures/`, eval seeds,
258
258
  and a `compile-report.md`) and `runtime/<runtime>/` (the adapter's
259
259
  emitted code + a README on how to run, signal gates, and deploy).
260
+ `compiled/compile-metrics.json` preserves final token counts, session ID,
261
+ and the driver's cost estimate before emission starts, so an adapter
262
+ failure does not lose the paid run's metrics. Missing final output stays
263
+ null with `usage_complete: false`; unchanged updates retain prior metrics.
264
+
265
+ Local `api` and `openai-api` compilation automatically discovers your
266
+ registered, authenticated MCP servers and exposes their read-only tools.
267
+ Pass `--no-mcp` to skip that discovery and compile without live MCP calls:
268
+
269
+ ```sh
270
+ rote compile examples/bdr-outreach/skill --local --agent api --no-mcp --out /tmp/bdr-compiled
271
+ ```
272
+
273
+ This leaves the emitted pipeline's MCP bindings intact. The flag also works
274
+ with `rote analyze` and with an API driver selected through config or
275
+ auto-detection. It rejects cloud compilation, subprocess drivers, and
276
+ `--baseline`, whose raw skill run uses its own MCP tools. API drivers bill
277
+ through your API credentials.
260
278
 
261
279
  ### Other commands
262
280
 
@@ -501,6 +519,9 @@ rote/
501
519
  archetype: a bounded browser-automation loop stays one agent node
502
520
  while the date math, filtering, and reporting around it compile to
503
521
  code, plus the measured runs that forced the loop-aware cost model
522
+ - [`examples/rote-compile/`](examples/rote-compile/): the compiler compiling
523
+ its own core skill, with preserved artifacts and an offline audit of
524
+ the generated workflow's runtime and data-flow failures
504
525
 
505
526
  ---
506
527
 
@@ -508,17 +529,19 @@ rote/
508
529
 
509
530
  In rough priority order:
510
531
 
511
- 1. **Re-compile BDR end-to-end with `signature_spec`**: the bundled IR
512
- was hand-extended with structured schemas; the rubric now teaches the
513
- field, but no real run has produced one yet.
514
- 2. **Pre-filter as a `pure_function` node**: today hard thresholds are
515
- lifted into a judge's `forward()`, which works for Temporal but not
516
- Cloudflare; a separate node makes the short-circuit uniform.
517
- 3. **More example skills**: BDR is one shape; research-heavy,
518
- retrieval-heavy, and code-review skills stress the IR differently.
519
- 4. **The compiler compiling itself**: `rote-compile` is a SKILL.md;
520
- pointing `rote compile` at it should crystallize its rubric-grade
521
- pieces and leave only the genuinely fuzzy judgments in the loop.
532
+ 1. **Pre-filter as a `pure_function` node**: today hard thresholds are
533
+ lifted into a judge's `forward()`, which works for the Python
534
+ runtimes but not the TypeScript ones; a separate node makes the
535
+ short-circuit uniform across all six.
536
+ 2. **Make self-compilation executable**: the
537
+ [preserved core-skill run](examples/rote-compile/) produced valid IR,
538
+ and runtime emission now succeeds. Its offline audit still has
539
+ data-flow and agent-result contract failures. Close those gaps before
540
+ treating it as a working replacement compiler.
541
+ 3. **More example skills**: four archetypes are covered today (BDR
542
+ outreach, 100%-roteness, data-heavy, agent-loop). Research-heavy,
543
+ retrieval-heavy, and code-review skills each stress the IR in ways
544
+ none of those four do.
522
545
 
523
546
  ---
524
547
 
@@ -201,6 +201,24 @@ override with `--agent`. The output directory splits into `compiled/`
201
201
  (the agent's `pipeline.yaml`, `extracted/`, `signatures/`, eval seeds,
202
202
  and a `compile-report.md`) and `runtime/<runtime>/` (the adapter's
203
203
  emitted code + a README on how to run, signal gates, and deploy).
204
+ `compiled/compile-metrics.json` preserves final token counts, session ID,
205
+ and the driver's cost estimate before emission starts, so an adapter
206
+ failure does not lose the paid run's metrics. Missing final output stays
207
+ null with `usage_complete: false`; unchanged updates retain prior metrics.
208
+
209
+ Local `api` and `openai-api` compilation automatically discovers your
210
+ registered, authenticated MCP servers and exposes their read-only tools.
211
+ Pass `--no-mcp` to skip that discovery and compile without live MCP calls:
212
+
213
+ ```sh
214
+ rote compile examples/bdr-outreach/skill --local --agent api --no-mcp --out /tmp/bdr-compiled
215
+ ```
216
+
217
+ This leaves the emitted pipeline's MCP bindings intact. The flag also works
218
+ with `rote analyze` and with an API driver selected through config or
219
+ auto-detection. It rejects cloud compilation, subprocess drivers, and
220
+ `--baseline`, whose raw skill run uses its own MCP tools. API drivers bill
221
+ through your API credentials.
204
222
 
205
223
  ### Other commands
206
224
 
@@ -445,6 +463,9 @@ rote/
445
463
  archetype: a bounded browser-automation loop stays one agent node
446
464
  while the date math, filtering, and reporting around it compile to
447
465
  code, plus the measured runs that forced the loop-aware cost model
466
+ - [`examples/rote-compile/`](examples/rote-compile/): the compiler compiling
467
+ its own core skill, with preserved artifacts and an offline audit of
468
+ the generated workflow's runtime and data-flow failures
448
469
 
449
470
  ---
450
471
 
@@ -452,17 +473,19 @@ rote/
452
473
 
453
474
  In rough priority order:
454
475
 
455
- 1. **Re-compile BDR end-to-end with `signature_spec`**: the bundled IR
456
- was hand-extended with structured schemas; the rubric now teaches the
457
- field, but no real run has produced one yet.
458
- 2. **Pre-filter as a `pure_function` node**: today hard thresholds are
459
- lifted into a judge's `forward()`, which works for Temporal but not
460
- Cloudflare; a separate node makes the short-circuit uniform.
461
- 3. **More example skills**: BDR is one shape; research-heavy,
462
- retrieval-heavy, and code-review skills stress the IR differently.
463
- 4. **The compiler compiling itself**: `rote-compile` is a SKILL.md;
464
- pointing `rote compile` at it should crystallize its rubric-grade
465
- pieces and leave only the genuinely fuzzy judgments in the loop.
476
+ 1. **Pre-filter as a `pure_function` node**: today hard thresholds are
477
+ lifted into a judge's `forward()`, which works for the Python
478
+ runtimes but not the TypeScript ones; a separate node makes the
479
+ short-circuit uniform across all six.
480
+ 2. **Make self-compilation executable**: the
481
+ [preserved core-skill run](examples/rote-compile/) produced valid IR,
482
+ and runtime emission now succeeds. Its offline audit still has
483
+ data-flow and agent-result contract failures. Close those gaps before
484
+ treating it as a working replacement compiler.
485
+ 3. **More example skills**: four archetypes are covered today (BDR
486
+ outreach, 100%-roteness, data-heavy, agent-loop). Research-heavy,
487
+ retrieval-heavy, and code-review skills each stress the IR in ways
488
+ none of those four do.
466
489
 
467
490
  ---
468
491
 
@@ -69,14 +69,14 @@ openai-api = [
69
69
  "openai>=2.45",
70
70
  ]
71
71
  dbos = [
72
- "dbos>=2.26",
72
+ "dbos>=2.26,<3",
73
73
  ]
74
74
  serve = [
75
- "fastmcp>=3.4.2",
75
+ "fastmcp>=3.4.2,<4",
76
76
  "httpx>=0.28",
77
77
  ]
78
78
  mcp = [
79
- "fastmcp>=3.4.2",
79
+ "fastmcp>=3.4.2,<4",
80
80
  "httpx>=0.28",
81
81
  ]
82
82
  dev = [
@@ -88,8 +88,8 @@ dev = [
88
88
  "temporalio>=1.7",
89
89
  "anthropic>=0.89",
90
90
  "openai>=2.45",
91
- "dbos>=2.26",
92
- "fastmcp>=3.4.2",
91
+ "dbos>=2.26,<3",
92
+ "fastmcp>=3.4.2,<4",
93
93
  "httpx>=0.28",
94
94
  ]
95
95
 
@@ -1,3 +1,3 @@
1
1
  """rote — compile fuzzy AI skills into deterministic workflows."""
2
2
 
3
- __version__ = "0.13.0"
3
+ __version__ = "0.14.0"
@@ -56,9 +56,17 @@ def _cloudflare_adapter_factory(**options: Any) -> Adapter:
56
56
  from rote.adapters.cloudflare import CloudflareAdapter, CloudflareAdapterConfig
57
57
 
58
58
  backend = _validated_backend(options)
59
- if backend is None:
59
+ mcp_client = options.get("mcp_client")
60
+ if mcp_client not in (None, "direct", "binding"):
61
+ raise ValueError(f"mcp_client must be 'direct' or 'binding', got {mcp_client!r}")
62
+ if backend is None and mcp_client is None:
60
63
  return CloudflareAdapter()
61
- return CloudflareAdapter(CloudflareAdapterConfig(external_backend=backend))
64
+ return CloudflareAdapter(
65
+ CloudflareAdapterConfig(
66
+ external_backend=backend or "mcp",
67
+ mcp_client=mcp_client or "direct",
68
+ )
69
+ )
62
70
 
63
71
 
64
72
  def _source_dir(options: dict[str, Any]) -> Path | None:
@@ -232,38 +232,47 @@ class _SchemaToPydantic:
232
232
 
233
233
  The Python analog of the Cloudflare adapter's ``json_schema_to_zod``.
234
234
  Named ``$defs`` become named classes / ``Literal`` aliases; inline
235
- object schemas get synthesized names. Blocks are accumulated in
236
- dependency order (a class is emitted only after everything it
237
- references), so the generated module imports cleanly top-to-bottom.
235
+ object schemas get synthesized names. Each root schema resolves its
236
+ own definitions; generated names and dependency-ordered blocks are
237
+ shared across the module.
238
238
  """
239
239
 
240
- def __init__(self) -> None:
240
+ def __init__(self, *, reserved_names: tuple[str, ...] = ()) -> None:
241
241
  self._defs: dict[str, dict[str, Any]] = {}
242
242
  self._blocks: list[str] = []
243
243
  self._emitted: dict[str, str] = {} # $def name -> python name
244
244
  self._in_progress: set[str] = set()
245
- self._used_names: set[str] = set()
245
+ self._used_names: set[str] = {
246
+ *keyword.kwlist,
247
+ *reserved_names,
248
+ "Any",
249
+ "Literal",
250
+ "BaseModel",
251
+ "ConfigDict",
252
+ "Field",
253
+ "PROMPT",
254
+ "MODEL",
255
+ "TEMPERATURE",
256
+ }
246
257
  self.uses_literal = False
247
258
  self.uses_config_dict = False
248
259
  self.uses_field = False
249
260
 
250
261
  # ── public API ──
251
262
 
252
- def add_defs(self, defs: dict[str, Any]) -> None:
253
- for name, schema in defs.items():
254
- if name in self._defs and self._defs[name] != schema:
255
- raise ValueError(
256
- f"signature emission: $defs entry {name!r} appears in both input "
257
- f"and output schemas with different definitions"
258
- )
259
- self._defs[name] = schema
260
-
261
263
  def emit_root(self, class_name: str, schema: dict[str, Any]) -> None:
262
264
  if schema.get("type") != "object":
263
265
  raise ValueError(
264
266
  f"signature emission: root signature schema for {class_name} must be "
265
267
  f"an object schema, got type={schema.get('type')!r}"
266
268
  )
269
+ # A #/$defs reference belongs to its root document. Sharing this
270
+ # cache across input/output can bind even identical wrappers to the
271
+ # wrong nested definition. Only Python symbol allocation is global.
272
+ self._defs = schema.get("$defs", {})
273
+ self._emitted.clear()
274
+ self._in_progress.clear()
275
+ self._used_names.add(class_name)
267
276
  self._emit_object_class(class_name, schema)
268
277
 
269
278
  @property
@@ -498,9 +507,7 @@ def emit_signature_module(
498
507
  spec = node.signature_spec
499
508
  pascal = _to_pascal_case(node.id)
500
509
 
501
- converter = _SchemaToPydantic()
502
- converter.add_defs(spec.input_schema.get("$defs", {}))
503
- converter.add_defs(spec.output_schema.get("$defs", {}))
510
+ converter = _SchemaToPydantic(reserved_names=(pascal, f"{pascal}Input", f"{pascal}Output"))
504
511
  converter.emit_root(f"{pascal}Input", spec.input_schema)
505
512
  converter.emit_root(f"{pascal}Output", spec.output_schema)
506
513
 
@@ -1699,7 +1699,9 @@ function isUnauthorizedWorkers(err: unknown): boolean {
1699
1699
  """
1700
1700
 
1701
1701
 
1702
- def emit_workers_mcp_call_module(node: Node, *, generated_by: str) -> str:
1702
+ def emit_workers_mcp_call_module(
1703
+ node: Node, *, generated_by: str, mcp_client: str = "direct"
1704
+ ) -> str:
1703
1705
  """Emit ``src/extracted/<id>.ts`` as a working Workers MCP call.
1704
1706
 
1705
1707
  Same module path and function name as the stub, but the function
@@ -1735,8 +1737,23 @@ def emit_workers_mcp_call_module(node: Node, *, generated_by: str) -> str:
1735
1737
  f" * {desc_first}",
1736
1738
  " *",
1737
1739
  f" * Auto-generated by {generated_by}: invokes tool {binding.tool!r} on MCP",
1738
- f" * server {binding.server!r} over Streamable HTTP, authenticated from",
1739
- f" * Worker secrets provisioned by `rote mcp export {binding.server}`.",
1740
+ # The provisioning sentence has to match the helper this build
1741
+ # actually emitted. Binding mode reads no per-server secrets at
1742
+ # all, and this header is the file an operator opens when a call
1743
+ # fails, so naming the wrong mechanism sends them to configure
1744
+ # secrets nothing will ever read.
1745
+ *(
1746
+ [
1747
+ f" * server {binding.server!r} through the platform's ROTE_MCP service",
1748
+ " * binding. The platform provisions the connection; this build reads",
1749
+ " * no per-server secrets.",
1750
+ ]
1751
+ if mcp_client == "binding"
1752
+ else [
1753
+ f" * server {binding.server!r} over Streamable HTTP, authenticated from",
1754
+ f" * Worker secrets provisioned by `rote mcp export {binding.server}`.",
1755
+ ]
1756
+ ),
1740
1757
  " * Swap to a direct vendor-SDK call with `rote emit --backend api`.",
1741
1758
  *mandatory_lines,
1742
1759
  " */",
@@ -1762,6 +1779,337 @@ def emit_workers_mcp_call_module(node: Node, *, generated_by: str) -> str:
1762
1779
  return "\n".join(lines)
1763
1780
 
1764
1781
 
1782
+ #: Source of the BINDING variant of ``src/extracted/_roteMcp.ts`` —
1783
+ #: emitted when the Cloudflare adapter runs with ``mcp_client="binding"``.
1784
+ #: Same exported surface as :data:`ROTE_MCP_WORKERS_HELPER_TS` (the node
1785
+ #: modules and agent loops import both identically), but every operation
1786
+ #: is delegated to a platform-provisioned ``ROTE_MCP`` service binding:
1787
+ #: no token store, no OAuth refresh, no per-server secrets, and no MCP
1788
+ #: SDK dependency. Every proxy call carries a signed caller-auth object
1789
+ #: built from dispatcher-injected env vars (``ROTE_TENANT_ID`` /
1790
+ #: ``ROTE_PIPELINE`` / ``ROTE_RUN_ID`` / ``ROTE_MCP_SIG``) so tenant
1791
+ #: isolates cannot impersonate each other. The platform proxy signals a
1792
+ #: credential problem with a plain Error whose message starts with
1793
+ #: ``ROTE_MCP_AUTH_NEEDED:`` (RPC boundaries strip class identity, so
1794
+ #: the signal rides in the message); the helper rethrows that as
1795
+ #: RoteMcpAuthNeeded so the emitted park-on-auth loop works unchanged.
1796
+ ROTE_MCP_BINDING_HELPER_TS = """/**
1797
+ * MCP helper for platform-managed connections (generated by rote — do
1798
+ * not edit; the source of truth is
1799
+ * rote.adapters._ts_common.ROTE_MCP_BINDING_HELPER_TS).
1800
+ *
1801
+ * Every MCP operation is delegated to the `ROTE_MCP` service binding —
1802
+ * an RPC stub the hosting platform provisions on this Worker. The
1803
+ * platform owns endpoint resolution, credentials, and token refresh, so
1804
+ * this variant carries no token store, no OAuth, and no per-server
1805
+ * secrets. Every call carries a signed caller-auth object built from
1806
+ * the dispatcher-injected ROTE_TENANT_ID / ROTE_PIPELINE / ROTE_RUN_ID /
1807
+ * ROTE_MCP_SIG variables, so tenant isolates cannot impersonate each
1808
+ * other. When the platform proxy reports a missing or dead credential
1809
+ * (a plain Error whose message starts with "ROTE_MCP_AUTH_NEEDED:"),
1810
+ * the helper rethrows RoteMcpAuthNeeded so the workflow's park-on-auth
1811
+ * loop suspends the instance until the connection is re-authorized.
1812
+ */
1813
+
1814
+ /** One tool as the server advertises it. */
1815
+ export interface McpToolSpec {
1816
+ name: string;
1817
+ description: string;
1818
+ inputSchema: Record<string, unknown>;
1819
+ }
1820
+
1821
+ /** ROTE_MCP_SIG authenticates tenant + pipeline; run_id is attribution. */
1822
+ export interface RoteMcpAuth {
1823
+ tenant_id: string;
1824
+ pipeline: string;
1825
+ run_id?: string;
1826
+ sig: string;
1827
+ }
1828
+
1829
+ /** One call's outcome, as the proxy reports it. */
1830
+ export interface RoteMcpCallResult {
1831
+ /** Text string, or the tool's structured object. */
1832
+ content: unknown;
1833
+ is_error: boolean;
1834
+ }
1835
+
1836
+ /** One tool as the proxy advertises it (snake_case wire shape). */
1837
+ export interface RoteMcpToolInfo {
1838
+ name: string;
1839
+ description: string | null;
1840
+ input_schema: unknown;
1841
+ }
1842
+
1843
+ /** The platform's MCP proxy, provisioned as an RPC service binding. */
1844
+ export interface RoteMcpBinding {
1845
+ call(
1846
+ auth: RoteMcpAuth,
1847
+ server: string,
1848
+ tool: string,
1849
+ args: Record<string, unknown>,
1850
+ ): Promise<RoteMcpCallResult>;
1851
+ listTools(auth: RoteMcpAuth, server: string): Promise<RoteMcpToolInfo[]>;
1852
+ }
1853
+
1854
+ export interface RoteMcpEnv {
1855
+ ROTE_MCP: RoteMcpBinding;
1856
+ [key: string]: unknown;
1857
+ }
1858
+
1859
+ /**
1860
+ * The MCP server's platform connection needs to be re-authorized.
1861
+ * Detect with isRoteMcpAuthNeeded(), never `instanceof`: Workflows
1862
+ * serialize errors across hibernation and replay boundaries — class
1863
+ * identity is lost, `name` survives.
1864
+ */
1865
+ export class RoteMcpAuthNeeded extends Error {
1866
+ readonly server: string;
1867
+ readonly reason: string;
1868
+
1869
+ constructor(server: string, reason: string) {
1870
+ super(
1871
+ `MCP server '${server}' needs (re)authentication — ${reason}. ` +
1872
+ `Re-authorize the connection on the platform hosting this worker.`,
1873
+ );
1874
+ this.name = "RoteMcpAuthNeeded";
1875
+ this.server = server;
1876
+ this.reason = reason;
1877
+ }
1878
+ }
1879
+
1880
+ /** True if a RoteMcpAuthNeeded hides anywhere in the error tree. */
1881
+ export function isRoteMcpAuthNeeded(err: unknown): boolean {
1882
+ let node: unknown = err;
1883
+ for (let depth = 0; node != null && depth < 16; depth++) {
1884
+ const e = node as { name?: unknown; errors?: unknown; cause?: unknown };
1885
+ if (e.name === "RoteMcpAuthNeeded") return true;
1886
+ if (Array.isArray(e.errors) && e.errors.some((sub) => isRoteMcpAuthNeeded(sub))) {
1887
+ return true;
1888
+ }
1889
+ node = e.cause;
1890
+ }
1891
+ return false;
1892
+ }
1893
+
1894
+ // Concrete JSON types — same rationale as the direct helper: step.do
1895
+ // constrains callback returns to Rpc.Serializable, which `unknown`
1896
+ // members do not satisfy.
1897
+ export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
1898
+ export type JsonObject = { [key: string]: JsonValue };
1899
+
1900
+ /** The platform proxy's auth-failure contract: errors arrive as plain
1901
+ * Error (the RPC boundary strips class identity), so the signal rides
1902
+ * in the message prefix. */
1903
+ const AUTH_PREFIX = "ROTE_MCP_AUTH_NEEDED:";
1904
+
1905
+ function translate(err: unknown, server: string): unknown {
1906
+ const msg = err instanceof Error ? err.message : String(err);
1907
+ if (msg.startsWith(AUTH_PREFIX)) {
1908
+ let reason = msg.slice(AUTH_PREFIX.length).trim();
1909
+ // The proxy's detail leads with the server name (its parse token);
1910
+ // RoteMcpAuthNeeded's own message already names the server, so drop
1911
+ // the leading token rather than saying it twice.
1912
+ if (reason === server) {
1913
+ reason = "";
1914
+ } else if (reason.startsWith(`${server} `)) {
1915
+ reason = reason.slice(server.length + 1).trim();
1916
+ }
1917
+ return new RoteMcpAuthNeeded(
1918
+ server,
1919
+ reason || "the platform reports the connection needs re-authorization",
1920
+ );
1921
+ }
1922
+ return err;
1923
+ }
1924
+
1925
+ function requireBinding(env: RoteMcpEnv): void {
1926
+ if (!env.ROTE_MCP) {
1927
+ throw new Error(
1928
+ "the ROTE_MCP service binding is not configured — this build " +
1929
+ "delegates MCP calls to the hosting platform, which must " +
1930
+ "provision the binding",
1931
+ );
1932
+ }
1933
+ }
1934
+
1935
+ function requireVar(env: RoteMcpEnv, name: string): string {
1936
+ const value = env[name];
1937
+ if (typeof value !== "string" || value.length === 0) {
1938
+ throw new Error(
1939
+ `the ${name} variable is not set — binding-mode MCP runs only on ` +
1940
+ "the platform, whose dispatcher injects it into the isolate",
1941
+ );
1942
+ }
1943
+ return value;
1944
+ }
1945
+
1946
+ /** The signed caller identity sent on every proxy call. */
1947
+ function buildAuth(env: RoteMcpEnv): RoteMcpAuth {
1948
+ // ROTE_MCP_SIG first: its absence is the clearest sign this build is
1949
+ // running off-platform, and the config error should say so.
1950
+ const sig = requireVar(env, "ROTE_MCP_SIG");
1951
+ const auth: RoteMcpAuth = {
1952
+ tenant_id: requireVar(env, "ROTE_TENANT_ID"),
1953
+ pipeline: requireVar(env, "ROTE_PIPELINE"),
1954
+ sig,
1955
+ };
1956
+ const runId = env.ROTE_RUN_ID;
1957
+ if (typeof runId === "string" && runId.length > 0) {
1958
+ auth.run_id = runId;
1959
+ }
1960
+ return auth;
1961
+ }
1962
+
1963
+ /**
1964
+ * Call one tool on an MCP server through the platform proxy.
1965
+ *
1966
+ * `is_error` results throw exactly the way the direct helper treats
1967
+ * `result.isError`, and string content is JSON-parsed the way the
1968
+ * direct helper parses text blocks — so a workflow step behaves the
1969
+ * same under either client mode.
1970
+ */
1971
+ export async function callMcpTool(
1972
+ env: RoteMcpEnv,
1973
+ server: string,
1974
+ _pipelineUrl: string | null,
1975
+ tool: string,
1976
+ args: Record<string, unknown>,
1977
+ ): Promise<JsonObject> {
1978
+ requireBinding(env);
1979
+ const auth = buildAuth(env);
1980
+ let result: RoteMcpCallResult;
1981
+ try {
1982
+ result = await env.ROTE_MCP.call(auth, server, tool, args);
1983
+ } catch (err) {
1984
+ throw translate(err, server);
1985
+ }
1986
+ if (result.is_error) {
1987
+ throw new Error(
1988
+ `MCP tool '${tool}' on '${server}' returned an error: ` +
1989
+ JSON.stringify(result.content),
1990
+ );
1991
+ }
1992
+ const content = result.content;
1993
+ if (typeof content === "string") {
1994
+ return (content ? JSON.parse(content) : {}) as JsonObject;
1995
+ }
1996
+ return (content ?? {}) as JsonObject;
1997
+ }
1998
+
1999
+ /**
2000
+ * Every tool `server` advertises, fetched through the platform proxy.
2001
+ *
2002
+ * The proxy's snake_case wire shape (`input_schema`, nullable
2003
+ * description) is normalized into McpToolSpec so bindAgentTools and the
2004
+ * agent loops see the same shape under either client mode.
2005
+ */
2006
+ export async function listMcpTools(
2007
+ env: RoteMcpEnv,
2008
+ server: string,
2009
+ _pipelineUrl: string | null,
2010
+ ): Promise<McpToolSpec[]> {
2011
+ requireBinding(env);
2012
+ const auth = buildAuth(env);
2013
+ let tools: RoteMcpToolInfo[];
2014
+ try {
2015
+ tools = await env.ROTE_MCP.listTools(auth, server);
2016
+ } catch (err) {
2017
+ throw translate(err, server);
2018
+ }
2019
+ return tools.map((tool) => ({
2020
+ name: tool.name,
2021
+ description: tool.description ?? "",
2022
+ inputSchema: (tool.input_schema ?? {
2023
+ type: "object",
2024
+ properties: {},
2025
+ }) as Record<string, unknown>,
2026
+ }));
2027
+ }
2028
+
2029
+ /** A tool bound to a callable — structurally a BoundTool for the agent loop. */
2030
+ export interface BoundMcpTool {
2031
+ name: string;
2032
+ description: string;
2033
+ parameters: Record<string, unknown>;
2034
+ run: (args: Record<string, unknown>) => Promise<JsonObject>;
2035
+ }
2036
+
2037
+ /**
2038
+ * Candidate servers for tool discovery — same contract as the direct
2039
+ * helper: the pipeline's declared servers, with ROTE_MCP_SERVERS
2040
+ * replacing the list wholesale when an operator overrides it.
2041
+ */
2042
+ function agentServerNames(env: RoteMcpEnv, declared: string[]): string[] {
2043
+ const override = env.ROTE_MCP_SERVERS;
2044
+ if (typeof override === "string" && override.length > 0) {
2045
+ return override
2046
+ .split(",")
2047
+ .map((name) => name.trim())
2048
+ .filter(Boolean);
2049
+ }
2050
+ return [...new Set(declared)].sort();
2051
+ }
2052
+
2053
+ /**
2054
+ * Bind the tools an agent_loop declared to callables the loop can invoke.
2055
+ *
2056
+ * Same contract as the direct helper: discovery is best-effort per server
2057
+ * (a server that is down may not be one this loop needed), a declared
2058
+ * tool no reachable server provides is fatal, and an auth failure that
2059
+ * plausibly explains the gap is rethrown so the Workflow parks on it.
2060
+ */
2061
+ export async function bindAgentTools(
2062
+ env: RoteMcpEnv,
2063
+ allowed: string[],
2064
+ declaredServers: string[],
2065
+ serverUrls: Record<string, string | null> = {},
2066
+ toolServers: Record<string, string> = {},
2067
+ ): Promise<BoundMcpTool[]> {
2068
+ const wanted = new Set(allowed);
2069
+ const bound = new Map<string, BoundMcpTool>();
2070
+ const failures: unknown[] = [];
2071
+
2072
+ for (const server of agentServerNames(env, declaredServers)) {
2073
+ const url = serverUrls[server] ?? null;
2074
+ let specs: McpToolSpec[];
2075
+ try {
2076
+ specs = await listMcpTools(env, server, url);
2077
+ } catch (err) {
2078
+ failures.push(err);
2079
+ continue;
2080
+ }
2081
+ for (const spec of specs) {
2082
+ if (!wanted.has(spec.name) || bound.has(spec.name)) continue;
2083
+ // Resolved tools bind only from their own server — see the
2084
+ // direct helper's twin for why first-wins is not good enough.
2085
+ const pinned = toolServers[spec.name];
2086
+ if (pinned !== undefined && pinned !== server) continue;
2087
+ bound.set(spec.name, {
2088
+ name: spec.name,
2089
+ description: spec.description,
2090
+ parameters: spec.inputSchema,
2091
+ run: (args) => callMcpTool(env, server, url, spec.name, args),
2092
+ });
2093
+ }
2094
+ }
2095
+
2096
+ const missing = allowed.filter((name) => !bound.has(name));
2097
+ if (missing.length > 0) {
2098
+ const authFailure = failures.find((err) => isRoteMcpAuthNeeded(err));
2099
+ if (authFailure) throw authFailure;
2100
+ const detail = failures
2101
+ .map((err) => (err instanceof Error ? err.message : String(err)))
2102
+ .join("; ");
2103
+ throw new Error(
2104
+ `agent tools not provided by any reachable MCP server: ${missing.join(", ")}` +
2105
+ (detail ? ` (unreachable: ${detail})` : ""),
2106
+ );
2107
+ }
2108
+ return [...bound.values()];
2109
+ }
2110
+ """
2111
+
2112
+
1765
2113
  # ───────── signatures/_roteInference.ts (agent loops) ─────────
1766
2114
 
1767
2115
  #: The default Workers AI model for an agent loop. Tool-capable and the