mech.app
AI Agents

MCP Pass-Through: Programmatic Tool Calling Without the LLM Loop

How MCP pass-through mode lets you invoke tools directly from code, bypassing agent selection for deterministic workflows and hybrid orchestration.

Source: r33drichards.github.io
MCP Pass-Through: Programmatic Tool Calling Without the LLM Loop

Most MCP implementations assume the LLM picks the tool. The agent sees a list of available functions, decides which one to call, and the host executes it. Pass-through mode flips that assumption. You write JavaScript that calls MCP tools directly, no agent selection required.

This matters when you need deterministic sequences, when you want to test tool chains without burning tokens, or when you’re building hybrid systems where some steps are scripted and others are agent-driven.

What Pass-Through Actually Does

MCP pass-through registers upstream MCP servers as callable functions inside a JavaScript runtime. Instead of exposing tools to an LLM, you expose them to code running in V8.

The mcp-v8 implementation does three things:

  1. Connects to external MCP servers at startup via --mcp-server or --mcp-config
  2. Exposes those tools through globalThis.mcp inside the JavaScript sandbox
  3. Optionally publishes stub tools on its own MCP surface for discovery

The runtime sees upstream tools as first-class functions. You call them with mcp.callTool(server, tool, args) and get results back synchronously (or as promises, depending on the transport).

The Invocation Surface

Inside the V8 runtime, you get three primitives:

  • mcp.servers: array of connected server names
  • mcp.listTools(serverName): returns tool schemas for a given server
  • mcp.callTool(serverName, toolName, args): executes the tool and returns the result

Here’s what a direct tool call looks like:

const tools = mcp.listTools("github");

const result = await mcp.callTool("github", "create_issue", {
  owner: "acme",
  repo: "roadmap",
  title: "Document MCP pass-through",
  body: "We need examples of hybrid orchestration patterns."
});

console.log(JSON.stringify(result, null, 2));

The tool call still happens through run_js from the outer MCP client’s perspective. The LLM doesn’t see create_issue as a separate tool. It sees run_js, and the JavaScript inside decides which upstream tools to invoke.

Stub Tools for Progressive Disclosure

You can also publish stub tools on the outer MCP surface. These mirror upstream tools with prefixed names like runjs__github__create_issue.

This gives you two invocation paths:

  1. Direct programmatic: JavaScript calls mcp.callTool() internally
  2. Agent-driven: LLM selects runjs__github__create_issue from the tool list

The stub approach preserves native MCP tool discovery. The agent sees all available tools in one list, even if some are backed by pass-through calls under the hood.

State Management Implications

When you mix programmatic and agent-driven calls in the same session, you need to track which execution context owns which state.

Invocation ModeState ScopeError HandlingLatency
LLM-selected toolOuter MCP sessionHost catches errors, LLM sees failure message2-5s (includes LLM decision time)
Programmatic pass-throughJavaScript heap inside V8JavaScript try/catch, no LLM visibility50-200ms (direct RPC)
Stub tool (agent calls, JS executes)Hybrid: outer session + JS heapHost sees JS exceptions, LLM sees tool failure2-5s + 50-200ms

If your JavaScript modifies state (writes to a database, updates a file), that state persists across tool calls within the same run_js invocation. The outer MCP session doesn’t see intermediate steps unless you explicitly log them.

Error Boundaries

Programmatic tool calls fail differently than agent-selected ones.

LLM-mediated flow:

  1. Agent selects tool
  2. Host validates parameters against schema
  3. Host executes tool
  4. Host returns error message to LLM
  5. LLM sees failure, can retry or pivot

Pass-through flow:

  1. JavaScript calls mcp.callTool()
  2. V8 runtime validates parameters (or doesn’t, depending on your code)
  3. Upstream MCP server executes
  4. JavaScript receives error object
  5. JavaScript decides how to handle it (retry, log, throw)

The LLM never sees pass-through failures unless your JavaScript explicitly surfaces them. This is useful for retries and fallback logic, but it means you lose the agent’s ability to reason about tool failures.

When to Use Programmatic Calls

Pass-through makes sense when:

  • You need deterministic tool sequences (e.g., always call validate_schema before write_file)
  • You’re testing tool chains without LLM overhead
  • You want to implement retry logic or circuit breakers in code
  • You’re building a hybrid agent where some steps are scripted and others are autonomous

It doesn’t make sense when:

  • The agent needs to decide which tools to call based on context
  • You want the LLM to handle error recovery
  • You’re optimizing for agent autonomy over execution speed

Hybrid Orchestration Pattern

The most interesting use case is mixing both modes. Let the agent decide high-level strategy, but script the low-level plumbing.

Example: an agent that writes code might use LLM selection to decide which file to edit, then use programmatic calls to validate syntax, run tests, and commit changes in a fixed sequence.

// Agent decided to edit server.js
const fileContent = await mcp.callTool("filesystem", "read_file", {
  path: "server.js"
});

// Programmatic sequence: edit, validate, test
const edited = transformCode(fileContent);
await mcp.callTool("filesystem", "write_file", {
  path: "server.js",
  content: edited
});

const lintResult = await mcp.callTool("linter", "check", {
  path: "server.js"
});

if (lintResult.errors.length > 0) {
  throw new Error("Lint failed, rolling back");
}

const testResult = await mcp.callTool("test_runner", "run", {
  suite: "unit"
});

if (!testResult.passed) {
  throw new Error("Tests failed, rolling back");
}

await mcp.callTool("git", "commit", {
  message: "Refactor server.js",
  files: ["server.js"]
});

The agent picks the file. The script enforces the workflow.

Security and Policy Boundaries

Pass-through calls bypass LLM-level guardrails. If you’re using prompt-based safety checks (e.g., “never delete production databases”), those don’t apply to programmatic invocations.

You need to enforce policy at the MCP server level or inside the JavaScript runtime. The mcp-v8 implementation supports OPA policies, which can gate tool calls regardless of invocation path.

Policy enforcement happens before the tool executes, so both agent-driven and programmatic calls hit the same rules.

Observability Gaps

Standard MCP logging captures tool calls initiated by the LLM. Programmatic calls inside JavaScript are invisible unless you instrument them.

If you’re running pass-through in production, you need:

  • Explicit logging inside JavaScript for each mcp.callTool()
  • Correlation IDs to link programmatic calls back to the outer run_js invocation
  • Separate metrics for agent-driven vs. code-driven tool usage

Without this, you’ll see run_js in your logs but won’t know which upstream tools actually ran.

Technical Verdict

Use MCP pass-through when you need deterministic tool sequences or want to test tool chains without LLM overhead. It’s ideal for hybrid agents where some steps are scripted and others are autonomous.

Avoid it if you want the LLM to handle error recovery or if you’re optimizing for agent autonomy. Programmatic calls bypass the agent’s reasoning loop, which means you lose adaptive behavior.

The hybrid pattern (agent picks strategy, code enforces workflow) is the sweet spot. It gives you speed and determinism where you need it, and agent flexibility everywhere else.