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:
- Connects to external MCP servers at startup via
--mcp-serveror--mcp-config - Exposes those tools through
globalThis.mcpinside the JavaScript sandbox - 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 namesmcp.listTools(serverName): returns tool schemas for a given servermcp.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:
- Direct programmatic: JavaScript calls
mcp.callTool()internally - Agent-driven: LLM selects
runjs__github__create_issuefrom 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 Mode | State Scope | Error Handling | Latency |
|---|---|---|---|
| LLM-selected tool | Outer MCP session | Host catches errors, LLM sees failure message | 2-5s (includes LLM decision time) |
| Programmatic pass-through | JavaScript heap inside V8 | JavaScript try/catch, no LLM visibility | 50-200ms (direct RPC) |
| Stub tool (agent calls, JS executes) | Hybrid: outer session + JS heap | Host sees JS exceptions, LLM sees tool failure | 2-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:
- Agent selects tool
- Host validates parameters against schema
- Host executes tool
- Host returns error message to LLM
- LLM sees failure, can retry or pivot
Pass-through flow:
- JavaScript calls
mcp.callTool() - V8 runtime validates parameters (or doesn’t, depending on your code)
- Upstream MCP server executes
- JavaScript receives error object
- 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_schemabeforewrite_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_jsinvocation - 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.