Repository navigation
RFC: Progressive Type-Feedback MCP target (--target mcp) #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
teamchong
wants to merge
6
commits into
cloudflare:main
Choose a base branch
from
teamchong:rfc/mcp
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
242ca48
docs(rfc): add Progressive Type-Feedback MCP target specification
teamchong 7d2d19a
docs(rfc): simplify isolate execution to use execute() without plan()
teamchong 4e28733
docs(rfc): clarify direct tools vs server-side codemode clients
teamchong 561a08e
docs(rfc): replace keyword search with code-based search, dts(), and …
teamchong 2976943
docs(rfc): remove forge://sdk MCP resources and keep only search and …
teamchong 0fdd62c
docs(rfc): replace defaultParams/unresolvedParams with include(method…
teamchong File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| # RFC: Progressive Type-Feedback MCP target (`--target mcp`) | ||
|
|
||
| ## Summary | ||
|
|
||
| `forge <spec> --target mcp --out <dir>` generates a Model Context Protocol (MCP) runtime (`mcp.ts` and `_/mcp.ts`) layered on top of the generated TypeScript SDK (`--target ts`). | ||
|
|
||
| MCP clients fall into two categories: codemode-native clients (with built-in client-side tool search or code execution) that consume direct tool catalogues, and standard MCP clients that rely on server-side `search` and `execute` tools. This RFC focuses on the server-side `search` and `execute` runtime backed by the generated TypeScript SDK. | ||
|
|
||
| ## Motivation | ||
|
|
||
| For standard MCP clients without client-side tool search, large OpenAPI descriptions (thousands of operations and models) hit two bottlenecks: | ||
|
|
||
| 1. **Exposing every operation directly in `tools/list` overflows naive client context.** Sending 3,000+ tool schemas upfront produces megabytes of JSON. | ||
| 2. **Raw OpenAPI `search` + `execute` is brittle.** Exposing `search` and `execute` over raw `spec.paths` forces the model to write custom JavaScript filters against deeply nested OpenAPI JSON and hand-craft `fetch(path, body)` calls without static type checking. A single `snake_case` vs `camelCase` mistake or nested field error causes a live HTTP `400`/`404` round-trip. | ||
| Forge already compiles OpenAPI into a hierarchical TypeScript SDK (`Group` -> `Method` -> `Model` -> `Type`). Using that SDK as both the schema representation and the execution layer solves both problems: | ||
|
|
||
| - **Compact 1-level `.d.ts` views.** A 1-level `.d.ts` declaration is ~300–600 tokens (~10x smaller than the equivalent dereferenced JSON Schema). | ||
| - **Pre-flight type feedback.** Validating arguments against the SDK schema before sending any HTTP request catches unknown methods, `snake_case` wire names, read-only fields, missing required parameters, and nested union/model type errors, returning compiler-style diagnostics with the relevant 1-level `.d.ts` attached in the same turn. | ||
|
|
||
| ## Overview | ||
|
|
||
| ```text | ||
| OpenAPI JSON ──► forge --target mcp ──► Typed TS SDK (client.ts, <group>/client.ts, models.ts) | ||
| └──► MCP Runtime (mcp.ts, _/mcp.ts) | ||
| ├── Tool 1: search({ code? }) ──► 1-level .d.ts (~300–600 tokens) | ||
| └── Tool 2: execute({ code }) ──► Pre-flight validator | ||
| ├── Type error ──► TS2339/TS2561/TS2540/TS2741/TS2322 + 1-level .d.ts (0 HTTP calls) | ||
| └── Valid ──► Calls Forge TS Transport | ||
| ``` | ||
|
|
||
| 1. **`search({ code? })`** runs read-only JavaScript against the SDK's `methods`, `groups`, `models`, and `dts("<symbol>")` helper (or resolves a symbol path / HTTP route directly) and returns a 1-level `.d.ts` TypeScript declaration. | ||
| 2. **`execute({ code })`** runs JavaScript/TypeScript against the wrapped `client`. Returning an uninvoked namespace or method inspects its `.d.ts`; calling `await client.<group>.<method>(...)` validates arguments before making an HTTP request. | ||
|
|
||
| ## MCP protocol surface (`tools/list` & `tools/call`) | ||
|
|
||
| `tools/list` (`mcp.tools`) advertises two tools: | ||
|
|
||
| | Tool | Input schema | Behavior | | ||
| | --- | --- | --- | | ||
| | `search` | `{ code?: string }` | Evaluates read-only JavaScript against `methods` (`{ path, group, name, httpMethod, route, summary, description }`), `groups`, `models`, and `dts("<symbol>")` (or resolves an SDK group, method, model, or HTTP route directly) and returns a 1-level `.d.ts` TypeScript declaration and JSDoc. | | ||
| | `execute` | `{ code: string }` | Evaluates a JavaScript expression or async function body against the wrapped Forge `client`. Validates arguments pre-flight before sending any HTTP request. | | ||
|
|
||
| #### `tools/call` -> `search` | ||
|
|
||
| Request: | ||
|
|
||
| ```json | ||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 2, | ||
| "method": "tools/call", | ||
| "params": { | ||
| "name": "search", | ||
| "arguments": { "code": "dts('dns.records')" } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Response (`content[0].text`): | ||
|
|
||
| ```ts | ||
| // Group: dns.records | ||
| export namespace dns.records { | ||
| export interface Client { | ||
| /** Create DNS Record */ | ||
| create(request: CreateRequest, options?: RequestOptions): Promise<CreateResponse>; // POST /zones/{zoneId}/dns_records | ||
| /** List DNS Records */ | ||
| list(request: ListRequest, options?: RequestOptions): Promise<ListResponse>; // GET /zones/{zoneId}/dns_records | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Drilling into `dns.records.create` (`{ "name": "search", "arguments": { "code": "dts('dns.records.create')" } }`): | ||
| ```ts | ||
| // Method: client.dns.records.create (POST /zones/{zoneId}/dns_records) | ||
|
|
||
| /** | ||
| * Create DNS Record | ||
| * | ||
| * Creates a new DNS record for a zone. | ||
| */ | ||
| create(request: CreateRequest, options?: RequestOptions): Promise<CreateResponse>; // POST /zones/{zoneId}/dns_records | ||
|
|
||
| export interface CreateRequest { | ||
| /** Zone identifier. */ | ||
| zoneId: string; // path param | ||
| /** DNS record name. */ | ||
| name: string; | ||
| /** DNS record type. */ | ||
| type: 'A' | 'AAAA' | 'CNAME'; | ||
| /** Time to live in seconds. */ | ||
| ttl?: number; | ||
| /** Record-specific settings. */ | ||
| settings?: dns.records.RecordSettings; | ||
| } | ||
|
|
||
| export type CreateResponse = dns.records.RecordResponse; | ||
|
|
||
| // Referenced models (use search("<model>") to inspect): dns.records.RecordResponse, dns.records.RecordSettings | ||
| ``` | ||
|
|
||
| #### `tools/call` -> `execute` (pre-flight compiler feedback) | ||
|
|
||
| If the caller skips `search` or passes a raw OpenAPI `snake_case` wire name (`zone_id` instead of `zoneId`) or a bad nested type (`settings.ipv4Only: "yes"`), `execute` rejects the call before any HTTP request is made and attaches the relevant 1-level `.d.ts` definitions: | ||
|
|
||
| Request: | ||
|
|
||
| ```json | ||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 3, | ||
| "method": "tools/call", | ||
| "params": { | ||
| "name": "execute", | ||
| "arguments": { | ||
| "code": "await client.dns.records.create({ zone_id: 'z1', name: 'api.example.com', type: 'A', settings: { ipv4Only: 'yes' } })" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Response (`isError: true`, 0 HTTP requests sent): | ||
|
|
||
| ```text | ||
| TS2741 at request.zoneId: Property 'zoneId' (string) is missing in type 'CreateRequest' but required. | ||
| TS2561 at request.zone_id: Object literal may only specify known properties, but 'zone_id' does not exist in type 'CreateRequest'. Did you mean 'zoneId' (the SDK's camelCase name for wire field 'zone_id')? | ||
| TS2322 at request.settings.ipv4Only: Type '"yes"' is not assignable to type 'boolean'. | ||
|
|
||
| --- TypeScript Definition (.d.ts) --- | ||
| // Method: client.dns.records.create (POST /zones/{zoneId}/dns_records) | ||
| export interface CreateRequest { | ||
| zoneId: string; // path param | ||
| name: string; | ||
| type: 'A' | 'AAAA' | 'CNAME'; | ||
| ttl?: number; | ||
| settings?: dns.records.RecordSettings; | ||
| } | ||
|
|
||
| // Model: dns.records.RecordSettings (OpenAPI schema 'RecordSettings') | ||
| export interface RecordSettings { | ||
| /** Only resolve IPv4. */ | ||
| ipv4Only?: boolean; | ||
| /** Flatten CNAME at apex. */ | ||
| flattenCname?: boolean; | ||
| } | ||
| ``` | ||
|
|
||
| ## Progressive 1-level `.d.ts` rules | ||
|
|
||
| To keep every `search()` response and error payload bounded (~300–600 tokens) regardless of specification size: | ||
|
|
||
| | View | Query example | Rendered output | | ||
| | --------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | Root index | `search()` or `search("dts()")` | Top-level groups on `Client` with subgroup previews and method counts. | | ||
| | Group | `search("dts('dns.records')")` | 1-level `dns.records.Client` interface with child groups, method signatures, and HTTP route comments. | | ||
| | Method | `search("dts('dns.records.create')")` | Method JSDoc (including OpenAPI `x-*` extensions), signature, 1-level `CreateRequest` and `CreateResponse`, and nominal referenced model names. | | ||
| | Model | `search("dts('dns.records.RecordSettings')")` | 1-level `RecordSettings` declaration and its referenced model names. | | ||
| | Code filter / HTTP route | `search("methods.filter(m => m.route.includes('/dns_records'))")` or `search("POST /zones/{zoneId}/dns_records")` | Matching method signature(s) grouped by namespace, or full 1-level `.d.ts` when a single method matches. | | ||
|
|
||
| - **Inline primitives, nominal complex models.** Scalar, enum, and primitive-array `$ref`s are inlined directly into `CreateRequest`, while nested object/union models (`settings?: dns.records.RecordSettings`) stay nominal until inspected. | ||
| - **Flat union request bodies.** When an operation's flat request body is a union of object models, common and branch fields are merged into a single 1-level `CreateRequest` interface (with path/query parameters and required fields listed first). | ||
|
|
||
| ## Pre-flight validation diagnostics | ||
|
|
||
| Before `execute()` sends an HTTP request, `validateMethodCall` checks the argument object against `MethodSchema`: | ||
|
|
||
| | Diagnostic code | Condition | Attached `.d.ts` context | | ||
| | ------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | ||
| | `TS2339` | Unknown group or method on `client` (e.g. `client.dns.records.add`) | Scoped fuzzy suggestions (`Did you mean 'create'?`) + parent group `.d.ts`. | | ||
| | `TS2561` | OpenAPI `snake_case` wire name passed instead of SDK `camelCase` property (e.g. `zone_id` instead of `zoneId`) | Exact `camelCase` suggestion + method `.d.ts` (and nested model `.d.ts` if nested). | | ||
| | `TS2540` | Assignment to a `readonly` property in a request payload | Method `.d.ts` + offending model `.d.ts`. | | ||
| | `TS2741` | Missing required path, query, or body property | Method `.d.ts` + offending model `.d.ts`. | | ||
| | `TS2322` / `TS2554` | Type mismatch (scalar, enum, array bounds, object, union, intersection) or wrong argument count | Method `.d.ts` + offending nested model `.d.ts`. | | ||
|
|
||
| ## Runtime options (`include` & `evaluator` sandbox) | ||
|
|
||
| ```ts | ||
| const mcp = createMcp( | ||
| { token: 'session-token', baseUrl: 'https://api.example.com/v1', fetch: hostTransportFetch }, | ||
| { | ||
| include: (method) => !method.deprecated && method.httpMethod === 'GET', | ||
| evaluator: (code, globals, functionBody) => sandbox.eval(functionBody, globals), | ||
| }, | ||
| ); | ||
| ``` | ||
|
|
||
| - **`include?: (method: MethodSchema) => boolean`**: Filters which SDK methods are exposed in `search()`, `dts()`, and `execute()` (and prunes groups that have no remaining methods). Each `MethodSchema` includes `httpMethod`, `route`, `deprecated`, and OpenAPI `extensions` (`x-*`), so servers can scope the visible surface by HTTP verb, deprecation status, or caller permissions. | ||
| - **`evaluator?: CodeEvaluator | false`**: Pluggable JavaScript evaluator for string snippets passed to `search()` and `execute()` so untrusted agent code runs in an isolated sandbox (such as a Cloudflare Dynamic Worker isolate, `@cloudflare/codemode`, QuickJS, or Node `vm.runInNewContext`) and works in runtimes that disallow dynamic code generation (`new Function` / `eval`). Direct symbol paths (`"dns.records.create"`, `'dts("dns.records")'`) and HTTP routes (`"POST /zones/{zoneId}/dns_records"`) in `search()` resolve via pure static lookup without invoking `evaluator` or `new Function`. Passing `evaluator: false` disables dynamic string evaluation while keeping static `search()` lookups and callback-form `execute(async (client) => ...)` active. | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also maybe not true. I would love to validate that the models are better at using a generated sdk they have never seen before rather than a hierarchical open api spec which can be easily expressed in types (in training data)