mcp-proxy.jsā¢8.81 kB
#!/usr/bin/env node
import {
InMemoryEventStore,
proxyServer,
startHTTPServer
} from "../chunk-MXVPEZER.js";
// src/bin/mcp-proxy.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { EventSource } from "eventsource";
import { setTimeout } from "timers";
import util from "util";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
// src/StdioClientTransport.ts
import {
ReadBuffer,
serializeMessage
} from "@modelcontextprotocol/sdk/shared/stdio.js";
import { spawn } from "child_process";
// src/JSONFilterTransform.ts
import { Transform } from "stream";
var JSONFilterTransform = class extends Transform {
buffer = "";
constructor() {
super({ objectMode: false });
}
_flush(callback) {
if (this.buffer.trim().startsWith("{")) {
callback(null, Buffer.from(this.buffer));
} else {
callback(null, null);
}
}
_transform(chunk, _encoding, callback) {
this.buffer += chunk.toString();
const lines = this.buffer.split("\n");
this.buffer = lines.pop() || "";
const jsonLines = [];
const nonJsonLines = [];
for (const line of lines) {
if (line.trim().startsWith("{")) {
jsonLines.push(line);
} else {
nonJsonLines.push(line);
}
}
if (nonJsonLines.length > 0) {
console.warn("[mcp-proxy] ignoring non-JSON output", nonJsonLines);
}
if (jsonLines.length > 0) {
const output = jsonLines.join("\n") + "\n";
callback(null, Buffer.from(output));
} else {
callback(null, null);
}
}
};
// src/StdioClientTransport.ts
var StdioClientTransport = class {
onclose;
onerror;
onmessage;
/**
* The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped".
*
* This is only available after the process has been started.
*/
get stderr() {
return this.process?.stderr ?? null;
}
abortController = new AbortController();
onEvent;
process;
readBuffer = new ReadBuffer();
serverParams;
constructor(server) {
this.serverParams = server;
this.onEvent = server.onEvent;
}
async close() {
this.onEvent?.({
type: "close"
});
this.abortController.abort();
this.process = void 0;
this.readBuffer.clear();
}
send(message) {
return new Promise((resolve) => {
if (!this.process?.stdin) {
throw new Error("Not connected");
}
const json = serializeMessage(message);
if (this.process.stdin.write(json)) {
resolve();
} else {
this.process.stdin.once("drain", resolve);
}
});
}
/**
* Starts the server process and prepares to communicate with it.
*/
async start() {
if (this.process) {
throw new Error(
"StdioClientTransport already started! If using Client class, note that connect() calls start() automatically."
);
}
return new Promise((resolve, reject) => {
this.process = spawn(
this.serverParams.command,
this.serverParams.args ?? [],
{
cwd: this.serverParams.cwd,
env: this.serverParams.env,
shell: this.serverParams.shell ?? false,
signal: this.abortController.signal,
stdio: ["pipe", "pipe", this.serverParams.stderr ?? "inherit"]
}
);
this.process.on("error", (error) => {
if (error.name === "AbortError") {
this.onclose?.();
return;
}
reject(error);
this.onerror?.(error);
});
this.process.on("spawn", () => {
resolve();
});
this.process.on("close", (_code) => {
this.onEvent?.({
type: "close"
});
this.process = void 0;
this.onclose?.();
});
this.process.stdin?.on("error", (error) => {
this.onEvent?.({
error,
type: "error"
});
this.onerror?.(error);
});
const jsonFilterTransform = new JSONFilterTransform();
this.process.stdout?.pipe(jsonFilterTransform);
jsonFilterTransform.on("data", (chunk) => {
this.onEvent?.({
chunk: chunk.toString(),
type: "data"
});
this.readBuffer.append(chunk);
this.processReadBuffer();
});
jsonFilterTransform.on("error", (error) => {
this.onEvent?.({
error,
type: "error"
});
this.onerror?.(error);
});
});
}
processReadBuffer() {
while (true) {
try {
const message = this.readBuffer.readMessage();
if (message === null) {
break;
}
this.onEvent?.({
message,
type: "message"
});
this.onmessage?.(message);
} catch (error) {
this.onEvent?.({
error,
type: "error"
});
this.onerror?.(error);
}
}
}
};
// src/bin/mcp-proxy.ts
util.inspect.defaultOptions.depth = 8;
if (!("EventSource" in global)) {
global.EventSource = EventSource;
}
var argv = await yargs(hideBin(process.argv)).scriptName("mcp-proxy").command("$0 <command> [args...]", "Run a command with MCP arguments").positional("command", {
demandOption: true,
describe: "The command to run",
type: "string"
}).positional("args", {
array: true,
describe: "The arguments to pass to the command",
type: "string"
}).env("MCP_PROXY").parserConfiguration({
"populate--": true
}).options({
debug: {
default: false,
describe: "Enable debug logging",
type: "boolean"
},
endpoint: {
describe: "The endpoint to listen on",
type: "string"
},
gracefulShutdownTimeout: {
default: 5e3,
describe: "The timeout (in milliseconds) for graceful shutdown",
type: "number"
},
host: {
default: "::",
describe: "The host to listen on",
type: "string"
},
port: {
default: 8080,
describe: "The port to listen on",
type: "number"
},
server: {
choices: ["sse", "stream"],
describe: "The server type to use (sse or stream). By default, both are enabled",
type: "string"
},
shell: {
default: false,
describe: "Spawn the server via the user's shell",
type: "boolean"
},
sseEndpoint: {
default: "/sse",
describe: "The SSE endpoint to listen on",
type: "string"
},
streamEndpoint: {
default: "/mcp",
describe: "The stream endpoint to listen on",
type: "string"
}
}).help().parseAsync();
if (!argv.command) {
throw new Error("No command specified");
}
var finalCommand = argv.command;
var finalArgs = argv["--"] || argv.args;
var connect = async (client) => {
const transport = new StdioClientTransport({
args: finalArgs,
command: finalCommand,
env: process.env,
onEvent: (event) => {
if (argv.debug) {
console.debug("transport event", event);
}
},
shell: argv.shell,
stderr: "pipe"
});
await client.connect(transport);
};
var proxy = async () => {
const client = new Client(
{
name: "mcp-proxy",
version: "1.0.0"
},
{
capabilities: {}
}
);
await connect(client);
const serverVersion = client.getServerVersion();
const serverCapabilities = client.getServerCapabilities();
console.info("starting server on port %d", argv.port);
const createServer = async () => {
const server2 = new Server(serverVersion, {
capabilities: serverCapabilities
});
proxyServer({
client,
server: server2,
serverCapabilities
});
return server2;
};
const server = await startHTTPServer({
createServer,
eventStore: new InMemoryEventStore(),
host: argv.host,
port: argv.port,
sseEndpoint: argv.server && argv.server !== "sse" ? null : argv.sseEndpoint ?? argv.endpoint,
streamEndpoint: argv.server && argv.server !== "stream" ? null : argv.streamEndpoint ?? argv.endpoint
});
return {
close: () => {
return server.close();
}
};
};
var createGracefulShutdown = ({
server,
timeout
}) => {
const gracefulShutdown = () => {
console.info("received shutdown signal; shutting down");
server.close();
setTimeout(() => {
process.exit(1);
}, timeout).unref();
};
process.on("SIGTERM", gracefulShutdown);
process.on("SIGINT", gracefulShutdown);
return () => {
server.close();
};
};
var main = async () => {
try {
const server = await proxy();
createGracefulShutdown({
server,
timeout: argv.gracefulShutdownTimeout
});
} catch (error) {
console.error("could not start the proxy", error);
setTimeout(() => {
process.exit(1);
}, 1e3);
}
};
await main();
//# sourceMappingURL=mcp-proxy.js.map