Skip to content

feat: build every API artifact from Forge in a daily container and serve mcp-tools.json directly - #247

Open
mattzcarey wants to merge 1 commit into
mainfrom
feat/forge-tools
Open

mattzcarey wants to merge 1 commit into
mainfrom
feat/forge-tools

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

First of five stacked PRs; each targets the one before it: #247 → #267 (refusal explanations) → #268 (scope challenge) → #269 (OAuth-reachable direct tools) → #270 (structured output). Every commit passes npm run check on its own.

Builds every API artifact the Worker serves from Forge's openapi.forge.json, once a day in a container, and serves ?codemode=false from the generated mcp-tools.json directly.

sequenceDiagram
  participant Cron as scheduled (0 0 * * *)
  participant DO as ToolsBuilder DO
  participant C as ctx.container<br/>cloudflare/debian-trixie<br/>enableInternet: false
  participant E as BuildEgress
  participant GH as cloudflare/forge releases
  participant R2 as SPEC_BUCKET
  Cron->>DO: build()
  DO->>C: start(), exec(node -) ← bundled generator on stdin
  C->>E: GET forge.internal/openapi.forge.json
  E->>GH: newest openapi@<sha>
  C->>E: PUT artifacts.internal/spec.json, products.json, mcp-tools.json
  E->>R2: put (customMetadata.forgeRelease)
  DO->>C: destroy()
Loading
  • Container: native ctx.container, durable_object scheduling policy, managed cloudflare/debian-trixie image. No Dockerfile, no image build, no @cloudflare/containers.
  • Egress: the container has no internet. interceptOutboundHttp routes two hostnames to BuildEgress (a WorkerEntrypoint): GET forge.internal/openapi.forge.json and PUT artifacts.internal/<key> for the three artifact keys. Everything else gets a 403. The container holds no bindings or credentials.
  • Generator (scripts/generator/): walks Forge's command tree the way the cf CLI generator does, so tool names match cf commands (cf dns records create → dns_records_create). Request bodies keep the API's nested JSON Schema 2020-12 shape, with $defs for recursive schemas. account_id is optional everywhere. It also writes spec.json (all $refs resolved) and products.json for Code Mode.
    • npm run build:generator bundles it with esbuild into generated/tools-generator.mjs.txt (gitignored), which the Worker imports as a text module. Wrangler's build.command runs it before dev and deploy.
  • Serving: src/tools/non-codemode.ts serves tools/list from the artifact as-is and dispatches tools/call from the entry's routing metadata. src/isolate-cache.ts caches the artifacts per isolate for an hour.
  • Removed: the GitHub spec cron, OPENAPI_SPEC_URL, src/spec-processor.ts, and the old operation-to-tool code in src/openapi.ts (only the search types stay). One cron (0 0 * * *) remains.
  • Failures: a failed download, non-zero exit, 5-minute timeout or bad output leaves the previous artifacts in R2 and fails that cron run.
  • Forge dependency: vendored as vendor/cloudflare-forge-0.1.0.tgz, packed from public cloudflare/forge main, as cf does until Forge is on npm.

Warning

Breaking for ?codemode=false users: all tool names change to the cf command names (main's 3,659 operationId-based names; none survive). Code Mode (search/execute) is unaffected.

Tooling

  • wrangler 4.103 → 4.144: older versions lack durable_object and exec().
  • @cloudflare/vitest-pool-workers 0.16 → 0.22. It bundles wrangler 4.124, which rejects the container config, so overrides points it at the project's wrangler. Drop the override once the pool catches up.
  • Newer workerd makes globalThis.fetch read-only, so tests/fetch-retry.test.ts and tests/non-codemode.test.ts mock with vi.stubGlobal.
  • New dev dependencies: @cloudflare/forge (vendored), ajv (generator schema tests), esbuild (pinned to wrangler's version).
  • dev.enable_containers: false, so wrangler dev doesn't need Docker. npm run seed:local generates the artifacts from the newest Forge release into local R2.

Notes

  • The durable_object policy is in beta, and a container's policy can't be changed after it's created.
  • The workers pool can't run containers. tests/tools-builder.test.ts covers everything around the container for real: BuildEgress through the ctx.exports loopback, the Forge download (GitHub mocked with MSW), R2 writes, and the scheduled handler's dispatch.
  • Verified on Cloudflare: a manual build ran in 9.6s and wrote all three artifacts tagged with their forgeRelease.
  • Forge inconsistencies found along the way are tracked in https://gist.github.com/mattzcarey/52b3e08c64a5b4101a3a02f24387665b.

@mattzcarey

Copy link
Copy Markdown
Member Author

Running log of inconsistencies in Forge's OpenAPI document that affect the MCP tools and structured output (envelope conventions, nullability, value constraints, coverage gaps), with counts and real-response examples from staging. I'll keep it updated as testing continues: https://gist.github.com/mattzcarey/52b3e08c64a5b4101a3a02f24387665b

…rve mcp-tools.json directly

A ToolsBuilder Durable Object runs the bundled generator in a managed
cloudflare/debian-trixie container with no internet access. BuildEgress is
its only way out: GET forge.internal returns the newest Forge release and
PUT artifacts.internal stores spec.json, products.json and mcp-tools.json.

?codemode=false serves mcp-tools.json as generated: cf CLI tool names,
typed nested inputs, annotations, and account_id optional everywhere. The
GitHub spec cron, OPENAPI_SPEC_URL and the Worker-side spec processing are
gone.
@mattzcarey mattzcarey changed the title feat: build mcp-tools.json from Forge in a daily ToolsBuilder container feat: build every API artifact from Forge in a daily container and serve mcp-tools.json directly Oct 7, 2026
mattzcarey added a commit that referenced this pull request Oct 9, 2026
Staging already has Forge's ToolsBuilder namespace (#247), and Cloudflare
rejects a deploy that drops a provisioned class from exports. #247 replaces
the placeholder.
mattzcarey added a commit that referenced this pull request Oct 12, 2026
Staging already has Forge's ToolsBuilder namespace (#247), and Cloudflare
rejects a deploy that drops a provisioned class from exports. #247 replaces
the placeholder.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant