Skip to content

feat(client): Built-in WASM Script Client + Auto .d.ts Return Type Inference (Solving Missing outputSchema in Code Mode) #3168

Open

Description

@maran-t

Solving Missing Return Types in MCP Code Mode via _meta.tsReturnType, Client-Side Auto .d.ts Inference & Built-in WASM Script Client

  • Status: Proposal
  • Type: Standards Track (Client SDK & Tool Metadata Extension)
  • Created: 2026-11-09

1. Abstract & Core Idea (LLM-Generated One-Time Script per Tool Call)

The Core Workflow:
Instead of the LLM making multiple static tools/call requests and receiving huge raw JSON responses into its context window, the MCP Client exposes the connected MCP tools as compact TypeScript interfaces (declare namespace sdk { ... }) and provides a single tool (execute_mcp_script). Using those TypeScript interfaces, the LLM dynamically writes and sends a custom, one-time JavaScript/TypeScript function (async (sdk) => { ... }) in a single tool call to:

  1. Call one or more MCP tools (including sequential dependent calls where Tool B uses the output of Tool A),
  2. Manipulate, filter, join, and aggregate the raw responses in-flight using custom logic, and
  3. Return only the final distilled result back to the LLM context window.

Solving the Two Blockers (Security & Missing Return Types):

  1. Zero-Trust Client-Side Execution (Solving Server Secret Risk): Because an LLM can generate arbitrary code, running that script on the MCP Server would risk exposing server variables and process.env secrets. By executing the LLM-generated script on the MCP Client inside a disposable QuickJS WebAssembly sandbox (quickjs-emscripten) with only the sdk proxy injected, server secrets are 100% unreachable and zero server changes are required.
  2. TypeScript Return Interfaces (_meta.tsReturnType & Auto-Inference): For the LLM to write working response-manipulation code on the first try, it must know the exact TypeScript structure of the tool's response. Because 95%+ of existing MCP servers omit outputSchema (which results in Promise<unknown>), this proposal introduces optional _meta.tsReturnType metadata plus automatic Client-side .d.ts return-type inference from live JSON responses.

2. Motivation

Standard MCP tool invocation suffers from four compounding bottlenecks in production host applications (such as Claude Code, Claude Desktop, Cursor, and IDE agents):

  1. Context Window Bloating (Data Moves to Compute):
    Calling a data-fetching tool (e.g., listCustomers, listIssues, queryLogs) dumps tens or hundreds of kilobytes of raw JSON directly into the LLM context window, even when the model only needs a filtered subset or aggregate count.
  2. Multi-Turn Dependent Call Latency & Context Re-Billing:
    When Tool B requires an ID or field from Tool A's output (Tool A -> Tool B -> Tool C), standard MCP requires a full LLM inference round-trip between every step—re-billing the entire conversation history on each turn.
  3. Missing Output Type Definitions on 95%+ of Existing MCP Servers:
    While MCP supports optional outputSchema, the vast majority of deployed MCP servers omit it and return serialized JSON inside content[0].text. Without knowing the exact return shape upfront, an LLM cannot reliably write a single-pass transformation script.
  4. Server-Side Execution Security Risks:
    Executing LLM-generated scripts on the MCP Server exposes server secrets (process.env, DB credentials, internal networks) unless every server author deploys heavy isolation infrastructure.

By placing a Zero-Trust WASM Sandbox and Automatic .d.ts Type Inferrer inside the MCP Client / Host Application layer, every existing MCP server gains >98% token reduction and single-turn multi-server orchestration with zero server modifications and zero server secret exposure.


3. Specification & Architecture

3.1. Tool Metadata Extension (_meta.tsReturnType) & Client Auto-Inference

  1. Explicit TypeScript Return Type (Optional Server Metadata):
    MCP Servers MAY attach _meta.tsReturnType (a compact TypeScript type string) or standard outputSchema to tool definitions in tools/list:
    {
      "name": "getCustomerById",
      "inputSchema": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] },
      "_meta": {
        "tsReturnType": "{ id: string; name: string; tier: 'enterprise' | 'pro' | 'free'; active: boolean } | null"
      }
    }
  2. Client-Side Automatic .d.ts Inference (Fallback for Existing MCP Servers):
    When an upstream tool omits both _meta.tsReturnType and outputSchema, the MCP Client inspects the first JSON response returned by tools/call, infers its TypeScript structural interface automatically, and caches the resulting .d.ts signature for subsequent turns.

3.2. Client-Generated Ambient TypeScript Declarations (declare namespace sdk)

The MCP Client aggregates all connected MCP servers into a single compact .d.ts declaration block:

declare namespace sdk {
  namespace crm {
    function listCustomers(args?: { includeInactive?: boolean }): Promise<Array<{ id: string; name: string; tier: 'enterprise' | 'pro' | 'free'; region: string; active: boolean }>>;
    function getCustomerById(args: { id: string }): Promise<{ id: string; name: string; tier: string; active: boolean } | null>;
  }
  namespace billing {
    function listInvoices(args?: { status?: "paid" | "overdue" | "draft" }): Promise<Array<{ invoiceId: string; customerId: string; amountUsd: number; status: string }>>;
  }
}

3.3. Client-Side Zero-Trust WASM Execution (McpScriptClient.executeScript)

When the LLM invokes execute_mcp_script, the MCP Client:

  1. Instantiates a disposable QuickJS WebAssembly runtime (quickjs-emscripten) with a strict memory cap (default: 16 MB) and CPU deadline interrupt handler (default: 500 ms).
  2. Injects only a frozen capability proxy object (sdk.<server>.<tool>). The WASM linear memory contains no process, env, fs, require, fetch, or shared V8 prototype chain with the host.
  3. Executes the LLM's async (sdk) => { ... } function:
    • Parallel calls (await Promise.all([sdk.crm.listCustomers(), sdk.billing.listInvoices()])) and sequential dependent chains (Tool A -> output -> Tool B) resolve inside the sandbox via standard MCP client.callTool() calls on the host.
    • Raw JSON responses are parsed inside WASM memory, filtered/joined/reduced by the script, and immediately freed when the WASM context is disposed.
  4. Returns only the final serialized return value to the LLM context window.

3.4. End-to-End Example (Dependent Tool Chaining + Filtering in 1 Turn)

Instead of 3 sequential LLM turns dumping 300 KB of raw JSON into the context window, the LLM submits a single execute_mcp_script call:

async (sdk) => {
  // Step 1: Fetch customers and filter in-memory inside WASM sandbox
  const allCustomers = await sdk.crm.listCustomers({ includeInactive: true });
  const target = allCustomers.find(c => !c.active && c.tier === "enterprise");
  if (!target) return null;

  // Step 2: Dependent call on the same MCP server using `target.id` from Step 1
  const verifiedCustomer = await sdk.crm.getCustomerById({ id: target.id });

  // Step 3: Cross-server join with billing invoices
  const overdue = await sdk.billing.listInvoices({ status: "overdue" });
  const customerOverdue = overdue.filter(inv => inv.customerId === verifiedCustomer.id);

  // Only this compact object (~150 bytes) is returned to the LLM context window
  return {
    customerId: verifiedCustomer.id,
    customerName: verifiedCustomer.name,
    overdueCount: customerOverdue.length,
    overdueTotalUsd: customerOverdue.reduce((sum, i) => sum + i.amountUsd, 0)
  };
}

4. Security Analysis

All untrusted code runs exclusively inside the MCP Client's disposable QuickJS WebAssembly linear memory space:

Threat Vector Mitigation Verified Outcome
MCP Server Secret Leakage (process.env) Script executes on the MCP Client inside WASM; upstream MCP servers only receive standard JSON-RPC tools/call requests. ReferenceError: 'process' is not defined
Prototype Chain Escape (constructor.constructor) Separate WASM heap; no shared V8 objects or prototypes with the Node.js host process. ReferenceError: 'process' is not defined
Filesystem / OS Access (fs, child_process) No WASI filesystem or OS imports mounted into the QuickJS WASM module. ReferenceError: 'require' is not defined
Data Exfiltration (fetch, sockets) Zero network bindings inside the WASM runtime. ReferenceError: 'fetch' is not defined
CPU / Memory Denial of Service (while(true){}) Enforced runtime.setMemoryLimit() and shouldInterruptAfterDeadline() interrupt handler. InternalError: interrupted at deadline

5. Benchmark Results (Reference Implementation)

Evaluated on a multi-server task joining 500 CRM customer records (crm MCP server) with 1,500 billing invoice records (billing MCP server) plus a 3-step sequential dependent lookup (listCustomers -> getCustomerById -> listInvoices):

Metric Standard MCP Client-Side Script Mode Improvement
LLM Tool Call Round-Trips 3 sequential turns 1 single turn 3x fewer turns
Raw Payload Entered into LLM Context 305,711 bytes 1,168 bytes 99.62% reduction
Total LLM Context Tokens (incl. .d.ts + script) ~76,428 tokens ~965 tokens 98.74% token savings
Dependent Chain (Tool A -> Tool B -> Tool C) 3 LLM turns 1 LLM turn (15.6 ms local) Eliminates 2 full context re-sends

6. Backward Compatibility

  • 100% Backward-Compatible with Existing MCP Servers: Requires zero changes to deployed MCP servers.
  • Hybrid or Pure Exposure in Host Apps: Host applications (Claude Code, Claude Desktop, etc.) can expose execute_mcp_script alongside standard tools (Hybrid Mode) or replace raw tool schemas with compact .d.ts declarations when tool counts or payload sizes exceed a threshold.

Activity

  1. changed the title [-]feat(client): Client-Side Sandboxed Script Execution & Auto .d.ts Interface Discovery[/-] [+][Proposal] Solving the Missing outputSchema Problem in MCP Code Mode: _meta.tsReturnType & Client-Side Auto .d.ts Inference[/+] on Nov 9, 2026
  2. changed the title [-][Proposal] Solving the Missing outputSchema Problem in MCP Code Mode: _meta.tsReturnType & Client-Side Auto .d.ts Inference[/-] [+]feat(client): Built-in WASM Script Client + Auto .d.ts Return Type Inference (Solving Missing outputSchema in Code Mode)[/+] on Nov 9, 2026
  3. maran-t commented on Nov 9, 2026

    @maran-t
    Author

    TL;DR

    When the LLM inspects the MCP tool list (and their TypeScript input/return interfaces) and sees that a request requires dependent tool calls or response filtering, instead of sending one tool call at a time and waiting for each raw response to come back to the model, the LLM sends a single generated script as a one tool call.

    That single script calls all the required MCP tools sequentially/in parallel, performs the required filtering and intermediate logic between the tool call responses locally inside the MCP Client's sandbox, and returns only the final required output in a single tool call turn.

  4. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    v1Issues / PRs related to v1.x
    on Nov 10, 2026
  5. SalyyS1 commented on Nov 11, 2026

    @SalyyS1

    Interesting proposal. One thing we found while benchmarking MCP context overhead for mcp-slim across 60 real MCP tool definitions (git, filesystem, postgres, github) is that the token bloat split happens at two distinct points:

    1. Upfront tools/list schema overhead (12,500+ tokens before the first turn):
      When generating compact .d.ts definitions from upstream inputSchema (Section 1), FastMCP and Pydantic servers emit nested $defs / {"$ref": "#/$defs/..."} pointers and verbose anyOf: [{"type": "string"}, {"type": "null"}] wrappers. Inlining $ref pointers recursively and collapsing nullable anyOf / ["T", "null"] unions into T | null before emitting the .d.ts (or compact JSON schema) cuts the declaration footprint by ~50% on its own, and pairing it with on-demand tool search (mcp_search_tools + mcp_call_tool) drops the upfront schema footprint by 98.7% (12,512 -> 168 tokens).

    2. When tools/call responses don't need multi-step control flow:
      For dependent multi-tool loops (Tool B consuming Tool A), a sandboxed script runner avoids round-trip latency. For single-call tools where the bloat is just verbose REST JSON (e.g. GitHub API returning 18 *_url hypermedia fields, nulls, and empty arrays per item), deterministic wire-level JSON/NDJSON compaction at the transport boundary strips ~43-55% of output tokens in <0.5 ms without requiring the model to generate code.

  6. maran-t commented on Nov 12, 2026

    @maran-t
    Author

    Great insights @SalyyS1 — both optimizations complement this architecture really well!

    1. Schema normalization during .d.ts generation

    Agreed. Resolving $defs / $ref references and simplifying nullable anyOf unions would make the generated TypeScript declarations significantly more compact.

    The generator should preserve JSON Schema semantics, particularly the distinction between optional properties (field?: T) and required nullable properties (field: T | null). Shared or recursive references could also be represented as named types to avoid unnecessary duplication.

    2. Syntactic compaction vs. semantic script execution

    I think these are complementary optimization layers:

    • Layer 1 — Deterministic compaction: For straightforward single-tool calls, schema-aware JSON compaction can reduce context overhead without requiring LLM-generated code.
    • Layer 2 — Sandboxed execution (execute_mcp_script): For semantic filtering, aggregation, cross-server joins, and dependent calls (Tool A -> Tool B -> Tool C), executing a generated script inside the MCP client avoids exposing intermediate payloads to the LLM and eliminates intermediate inference round-trips.

    One important distinction: compaction should be configurable and preserve fields required for execution. For legacy tools without outputSchema, I'd favor inferring types from the original response structure, then compacting the resulting .d.ts representation rather than inferring from potentially lossy data.

    3. On-demand discovery + script execution

    The mcp_search_tools approach could also integrate nicely here.

    Instead of exposing every connected tool's .d.ts upfront, the client could discover relevant tool definitions on demand, generate compact TypeScript interfaces, and let the LLM produce a single script that orchestrates those tools through the WASM sandbox.

    That introduces a discovery step when interfaces aren't already cached, but avoids repeated LLM round-trips during dependent execution.

    Overall, I see schema normalization, lazy discovery, deterministic payload compaction, and sandboxed script execution as complementary capabilities rather than competing approaches.

    Appreciate sharing the benchmarks and implementation insights!

  7. SalyyS1 commented on Nov 12, 2026

    @SalyyS1

    @maran-t Good call on ordering type inference before payload compaction for legacy tools without outputSchema.

    If OutputSlimmer strips keys whose values happened to be null or [] in the first sample response, inferring the .d.ts after compaction would miss those optional properties (field?: T | null) on subsequent calls where they are populated. Running .d.ts shape inference on the raw upstream JSON response (and unioning observed property types across calls when the cache warms up) while applying wire compaction only to the payload returned to the model gives you complete TypeScript definitions alongside minimal context usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions