#!/usr/bin/env node
// lax-mcp: stdio by default, --http for Streamable HTTP. Logs go to stderr only; stdout carries MCP frames.
import { realpathSync } from 'node:fs';
import { createServer, type IncomingMessage } from 'node:http';
import { fileURLToPath } from 'node:url';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createLaxMcpServer, DEFAULT_BASE_URL, VERSION } from './server.js';
const log = (line: string) => process.stderr.write(`[lax-mcp] ${line}\n`);
export function parseArgs(argv: string[], env: NodeJS.ProcessEnv = process.env) {
let http = false;
let port = Number(env.PORT ?? 3333);
let baseUrl = env.LAX_BASE_URL ?? DEFAULT_BASE_URL;
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--http') http = true;
else if (a === '--port') port = Number(argv[++i]);
else if (a.startsWith('--port=')) port = Number(a.slice(7));
else if (a === '--base-url') baseUrl = argv[++i];
else if (a === '--help' || a === '-h') return { help: true as const, http, port, baseUrl };
}
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('port must be 0 to 65535');
return { help: false as const, http, port, baseUrl };
}
async function readJson(req: IncomingMessage): Promise<unknown> {
const chunks: Buffer[] = [];
let size = 0;
for await (const c of req) {
size += (c as Buffer).length;
if (size > 1_000_000) throw new Error('body too large');
chunks.push(c as Buffer);
}
const raw = Buffer.concat(chunks).toString('utf8');
return raw ? JSON.parse(raw) : undefined;
}
export function startHttp(port: number, baseUrl: string) {
// Stateless Streamable HTTP: a fresh server and transport per POST, so nothing is shared between clients.
const srv = createServer(async (req, res) => {
const path = (req.url ?? '/').split('?')[0];
if (path === '/healthz') {
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, version: VERSION }));
return;
}
if (path !== '/mcp') {
res.writeHead(404).end();
return;
}
if (req.method !== 'POST') {
res.writeHead(405, { allow: 'POST' }).end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null }));
return;
}
try {
const body = await readJson(req);
const server = createLaxMcpServer({ baseUrl, log });
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
res.on('close', () => {
void transport.close();
void server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, body);
} catch {
log('http request failed');
if (!res.headersSent) res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Bad request' }, id: null }));
}
});
return new Promise<typeof srv>((resolve) => srv.listen(port, () => resolve(srv)));
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
if (opts.help) {
process.stderr.write('Usage: lax-mcp [--http] [--port N] [--base-url URL]\n stdio by default; --http serves Streamable HTTP on /mcp (port from --port, PORT or 3333).\n');
return;
}
if (opts.http) {
const srv = await startHttp(opts.port, opts.baseUrl);
const addr = srv.address();
log(`Streamable HTTP on http://localhost:${typeof addr === 'object' && addr ? addr.port : opts.port}/mcp`);
} else {
const server = createLaxMcpServer({ baseUrl: opts.baseUrl, log });
await server.connect(new StdioServerTransport());
log('ready on stdio');
}
}
function isMain(): boolean {
if (!process.argv[1]) return false;
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) main().catch((e) => {
log(`fatal: ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
});
agentgate-source: code mirror on Cloudflare Pages. All repos