Repository navigation
feat(mcp): serve forge-generated direct tools at ?forge=true - #300
Merged
Merged
Conversation
mattzcarey
force-pushed
the
feat/forge-mcp-direct-tools
branch
from
October 20, 2026 14:54
4c3ac32 to
52d7217
Compare
mattzcarey
added this pull request to stack #306
October 20, 2026 15:42
mattzcarey
force-pushed
the
feat/forge-mcp-direct-tools
branch
from
October 20, 2026 16:03
52d7217 to
6812f77
Compare
A forge plugin (src/forge-tools/) builds the direct-tool catalogue with the generic @cloudflare/forge-mcp tools plus the Cloudflare policy: optional account_id, the two values of merged account_or_zone paths, and accepted API token permissions in each tool's extensions. npm run generate:tools builds and runs it with forge; the seed script uploads the catalogue to R2 as forge-mcp-tools.json. /mcp?forge=true serves the catalogue next to docs and whoami: tools/list from the isolate cache, tools/call with the session's account filled in, requests built by forge-mcp, and refusals explained with scope step-up as the other direct tools do. ?codemode=false is unchanged.
Replace the forge-mcp plugin catalogue in R2 with code: forge's MCP target (--target mcp, on the forge-mcp branch pinned in forge.json) writes TypeScript for @modelcontextprotocol/server into src/generated/forge-mcp, which is committed. Each tool has its schema and a generated request function; /mcp?forge=true imports the module lazily and applies the Cloudflare policy (optional account_id, account_or_zone values) once per isolate. .github/workflows/forge-tools.yml builds that forge, regenerates from the latest openapi.forge.json every six hours, runs the type check and tests, and opens or updates a bot/forge-tools pull request when the generated code changes. Merging it deploys. Drops the vendored @cloudflare/forge-mcp tarball, the @cloudflare/forge and jco dev dependencies, the WebAssembly plugin, and the R2 artifact.
…ithout the SDK - forge.json: bump to 3f7e168, whose generated files import the tool type and request helpers from _/tool.ts, so importing the tools doesn't import the MCP SDK - forge-tools.yml: mint an agent-think GitHub App token when its secrets are set, so CI runs on the bot pull request; fall back to GITHUB_TOKEN - AGENTS.md: a separate uploaded module measured no better than the lazy import, since workerd compiles every uploaded module at startup
The generated direct tools move into their own Worker, cloudflare-api-mcp-forge-tools (src/forge-tools/worker.ts, wrangler.forge-tools.jsonc), which the main Worker calls over a FORGE_TOOLS service binding. workerd compiles every module a Worker uploads at startup, so even a lazy import cost every main isolate ~27 ms; now the main Worker is back to its 2 MB, ~40 ms startup, and only isolates serving ?forge=true compile the tools. The service holds no credentials and sends nothing: list() returns the tools, and plan() returns the request a call makes (src/forge-tools/plan.ts), which the main Worker sends with the session's token, explaining refusals and stepping up scopes as before. npm run dev runs both Workers; npm run types types the binding from both configs; vitest builds the service and runs it as an auxiliary Worker, so tests call it over the real binding.
As #251 does for execute: application/scim+json and other +json types are JSON, so they're parsed and formatted like application/json.
mattzcarey
force-pushed
the
feat/forge-mcp-direct-tools
branch
from
October 20, 2026 16:14
6812f77 to
10b1f27
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #297.
/mcp?forge=trueserves direct tools whose code forge generates fromopenapi.forge.json, as an opt-in preview.?codemode=falseand Code Mode are unchanged.The generator is a new
--target mcpin forge, on the long-livedforge-mcpbranch of mattzcarey/forge. It writes TypeScript for@modelcontextprotocol/server, which is committed here insrc/generated/forge-mcp/. A workflow regenerates it every six hours and opens a PR when it changes, and merging that PR deploys. The generated tools run in their own Worker, so the main Worker's startup is unchanged.Each generated tool is plain code:
Generated code (
src/generated/forge-mcp/, never edited by hand, skipped by oxfmt and oxlint):tools.tsschemas.tsfiles with model schemas shared by every tool that uses themindex.tsexporting every toolREADME.mdlisting the 27 methods without a tool and why_/Forge tools service (
src/forge-tools/worker.ts,wrangler.forge-tools.jsonc): a separate Worker that holds the generated tools and applies the Cloudflare rules (optionalaccount_id, theaccounts/zonesenum on merged paths). It has no secrets, no outbound requests and no route. Its two RPC methods:list(): the tools astools/listsends themplan(name, args, { baseUrl, sessionAccountId }): the request a call makes (src/forge-tools/plan.ts)Main Worker (
src/tools/forge-tools.ts): servesdocsandwhoamiitself. It sends each planned request with the session's token, which never leaves it, and explains refusals and steps up scopes like the existing direct tools. It parses+jsonresponses as JSON, as fix: preserve successful JSON responses without a REST envelope #251 does forexecute.Why a second Worker: workerd compiles every module a Worker uploads at startup, so bundling the tools into the main Worker cost every isolate, Code Mode included. Measured with
wrangler check startup, in startup CPU:?forge=true)The RPC hop adds about 20–40 ms to
tools/list.CI (
.github/workflows/forge-tools.yml): builds forge at the commit inforge.jsonand regenerates. When the code changes, it runs the type check and tests, then force-pushesbot/forge-toolsand opens or updates the PR.AGENT_THINK_APP_IDandAGENT_THINK_PRIVATE_KEYset, it acts as the agent-think GitHub App (team-apps), so CI runs on the PR as usual.GITHUB_TOKEN, whose PRs don't start CI.Bump
forge.jsonto change the generator.This drops the vendored forge-mcp tarball, the
@cloudflare/forgeand jco dev dependencies, the WebAssembly plugin and the R2 artifact. The lockfile is unchanged from #297.Important
The main Worker binds to the service, and a Worker whose service binding target doesn't exist fails to deploy. Deploy
cloudflare-api-mcp-forge-tools(and-staging) before this merges:npm run deploy:forge-tools:prodandnpm run deploy:forge-tools. Workers Builds also needs a second build for the new Worker, with deploy commandnpx wrangler deploy -c wrangler.forge-tools.jsonc --env production.Note
Tool names are forge's, matching the
cfCLI (zones_list,d1_list,zero_trust_access_applications_list). That's why this is a separate opt-in rather than a change to?codemode=false.Tests ran, and they call the real service over its binding: Vitest builds it and runs it as an auxiliary Worker. One full run had 3 failures I couldn't reproduce in the next five runs, so watch CI for a flaky test.
I also ran
npm run dev(both Workers) with my OAuth token and the MCP TS client:tools/listreturned 3,055 tools in 128–152 msaccounts_list,zones_list,d1_listand the merged Access tool returned live datad1_listwithoutaccount_idasked which account to use