ient setups, standard I/O (stdio) provides minimal overhead. For multi-client or remote scenarios, Streamable HTTP enables connection sharing, authentication, and rate limiting.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioTransport } from "./transports/stdio.js";
import { HttpTransport } from "./transports/http.js";
export class TerminalBridge {
private server: McpServer;
private transport: StdioTransport | HttpTransport;
constructor(mode: "stdio" | "http", config: BridgeConfig) {
this.server = new McpServer({ name: "terminal-bridge", version: "1.0.0" });
this.transport = mode === "http"
? new HttpTransport(config.httpPort, config.authToken)
: new StdioTransport();
this.registerTools();
}
private registerTools() {
this.server.tool("run_command", "Execute a validated terminal command", {
command: { type: "string", description: "Base executable" },
args: { type: "array", items: { type: "string" }, description: "Command arguments" },
session_id: { type: "string", description: "Target session identifier" }
}, async (params) => this.executeWithPolicy(params));
}
async start() {
await this.transport.bind(this.server);
console.log(`Bridge active on ${this.transport.getEndpoint()}`);
}
}
Architecture Rationale: Separating transport from tool registration allows the same policy engine to serve both local and remote clients without duplication. HTTP transport includes bearer token validation and connection pooling, while stdio relies on process isolation.
2. Policy Engine & Parameterized Execution
The security boundary lives in the execution layer. Instead of passing a single string to a shell, commands and arguments are split. This forces execFile() semantics, which bypasses shell interpretation entirely.
import { spawn } from "child_process";
import { CommandPolicy } from "./policy/engine.js";
export class ExecutionEngine {
private policy: CommandPolicy;
private timeoutMs: number;
constructor(policy: CommandPolicy, timeoutMs = 60000) {
this.policy = policy;
this.timeoutMs = timeoutMs;
}
async run(command: string, args: string[]): Promise<ExecutionResult> {
const validation = this.policy.validate(command, args);
if (!validation.allowed) {
throw new Error(`Execution blocked: ${validation.reason}`);
}
return new Promise((resolve, reject) => {
const proc = spawn(command, args, {
stdio: "pipe",
env: process.env,
timeout: this.timeoutMs
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (chunk) => stdout += chunk.toString());
proc.stderr.on("data", (chunk) => stderr += chunk.toString());
proc.on("close", (code) => {
resolve({ exitCode: code, stdout, stderr });
});
proc.on("error", (err) => reject(err));
});
}
}
Why spawn/execFile over exec? Shell execution interprets metacharacters (;, |, &, $()), enabling injection attacks even when the AI intends benign input. Parameterized execution passes arguments directly to the OS process table, eliminating shell parsing. This is the single most effective mitigation against command injection.
3. Session State Management
AI workflows often require context persistence across multiple commands. A session manager tracks working directories, environment variables, and execution history without leaking state between isolated tasks.
export class SessionRegistry {
private sessions: Map<string, SessionState> = new Map();
create(id: string, cwd: string, envOverrides: Record<string, string> = {}): SessionState {
const state: SessionState = {
id,
cwd,
env: { ...process.env, ...envOverrides },
createdAt: Date.now(),
lastActive: Date.now()
};
this.sessions.set(id, state);
return state;
}
sync(id: string, updates: Partial<SessionState>): void {
const session = this.sessions.get(id);
if (!session) throw new Error("Session not found");
Object.assign(session, updates, { lastActive: Date.now() });
}
destroy(id: string): boolean {
return this.sessions.delete(id);
}
}
Sessions enable multi-project workflows where an AI agent can switch between frontend and backend contexts without cross-contamination. The registry enforces isolation by binding environment variables and working directories to session identifiers rather than global process state.
Pitfall Guide
1. Treating allowUnknownCommands: true as Production-Ready
Explanation: The default permissive mode allows any command not explicitly blacklisted. While convenient for local development, it exposes the system to novel binaries, package managers, and scripting languages that can bypass pattern detection.
Fix: Set allowUnknownCommands: false in team or CI environments. Maintain a curated whitelist of approved tools and update it through version-controlled configuration.
2. Ignoring Command Timeouts and Context Window Bloat
Explanation: Long-running processes (e.g., npm run build, docker compose up) can stream megabytes of output, exhausting the AI's context window and causing token overflow or silent truncation.
Fix: Enforce a hard timeout (default 60 seconds). Capture output in fixed-size buffers and return only the final state or error summary. Use streaming only for interactive debugging sessions with explicit user consent.
3. Mixing Transport Protocols Without Authentication
Explanation: Exposing an HTTP transport without bearer tokens allows any local process or network scanner to execute commands. Stdio relies on process isolation, but HTTP requires explicit access control.
Fix: Always configure LING_TERM_AUTH_TOKEN or equivalent when using HTTP. Validate tokens at the transport layer before routing to the policy engine. Implement rate limiting to prevent brute-force or denial-of-service patterns.
4. Relying on Pattern Detection Alone
Explanation: Regex-based injection detection catches known attack vectors but fails against obfuscated commands, encoded payloads, or logic bombs that don't match static patterns.
Fix: Treat pattern detection as a secondary layer. Primary security must come from parameterized execution and strict allowlists. Use detection to log suspicious attempts and trigger alerts, not as the sole defense.
5. Using Relative Paths in Client Configurations
Explanation: MCP clients resolve paths relative to their own working directory, not the server's. Relative paths cause ENOENT errors or spawn the wrong binary.
Fix: Always use absolute paths in MCP client JSON configurations. Validate path existence during server startup and fail fast with clear error messages.
6. Forgetting to Sync Terminal State Across Sessions
Explanation: AI agents often assume the shell retains state between commands. Without explicit session synchronization, cd commands, environment exports, and alias definitions are lost.
Fix: Implement a sync_terminal tool that updates session state after stateful commands. Return the new working directory and active environment variables in the response payload.
Explanation: AI models trained on mixed datasets may generate Linux-specific commands (ls, grep, df) for Windows environments, or vice versa.
Fix: Implement platform-aware command mapping. Translate common operations to OS-native equivalents (dir vs ls, Get-Process vs ps). Document supported platforms and test against Windows, macOS, and Linux CI runners.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Local solo development | Stdio transport + allowUnknownCommands: true | Zero configuration, fast iteration, process isolation sufficient | None |
| Team development environment | HTTP transport + allowUnknownCommands: false + token auth | Centralized control, prevents rogue binaries, audit trail | Minimal (server overhead) |
| CI/CD pipeline integration | HTTP transport + strict whitelist + 30s timeout | Deterministic execution, prevents runaway jobs, fits pipeline SLAs | Low (infrastructure) |
| Multi-project workspace | Session-based routing + state sync | Isolates contexts, prevents env leakage, enables parallel workflows | Low (memory overhead) |
| High-security production | HTTP + mTLS + command signing + audit logging | Defense-in-depth, non-repudiation, compliance alignment | Moderate (cert management, logging storage) |
Configuration Template
{
"mcpServers": {
"terminal-bridge": {
"command": "node",
"args": ["/absolute/path/to/bridge/dist/index.js"],
"env": {
"TRANSPORT_MODE": "http",
"HTTP_PORT": "9529",
"AUTH_TOKEN": "your-secure-token-here",
"ALLOW_UNKNOWN_COMMANDS": "false",
"EXEC_TIMEOUT_MS": "60000"
}
}
}
}
For HTTP clients, connect to http://127.0.0.1:9529/mcp and include:
Authorization: Bearer your-secure-token-here
Quick Start Guide
- Install dependencies: Ensure Node.js >= 18 is available. Run
npm install @modelcontextprotocol/sdk in your project directory.
- Initialize the bridge: Create a TypeScript entry point using the transport and policy engine patterns above. Compile with
tsc or run via tsx.
- Configure your AI client: Add the JSON configuration template to your MCP client settings. Replace the path with your compiled entry point.
- Start the server: Execute
node dist/index.js http to launch the HTTP transport. Verify connectivity with curl http://127.0.0.1:9529/health.
- Test execution: Prompt your AI assistant with a safe command like
list files in current directory. Verify the response passes through the policy engine and returns structured output.
The architecture scales from local prototyping to team-wide deployment by swapping transport layers and tightening policy boundaries. Treat the terminal bridge as an API surface, not a shell, and AI agents become reliable operators rather than security liabilities.