=== NEW SERVERS ADDED (7) === - servers/closebot — 119 tools, 14 modules, 4,656 lines TS (Stage 7) - servers/google-console — Google Search Console MCP (Stage 7) - servers/meta-ads — Meta/Facebook Ads MCP (Stage 8) - servers/twilio — Twilio communications MCP (Stage 8) - servers/competitor-research — Competitive intel MCP (Stage 6) - servers/n8n-apps — n8n workflow MCP apps (Stage 6) - servers/reonomy — Commercial real estate MCP (Stage 1) === FACTORY INFRASTRUCTURE ADDED === - infra/factory-tools — mcp-jest, mcp-validator, mcp-add, MCP Inspector - 60 test configs, 702 auto-generated test cases - All 30 servers score 100/100 protocol compliance - infra/command-center — Pipeline state, operator playbook, dashboard config - infra/factory-reviews — Automated eval reports === DOCS ADDED === - docs/MCP-FACTORY.md — Factory overview - docs/reports/ — 5 pipeline evaluation reports - docs/research/ — Browser MCP research === RULES ESTABLISHED === - CONTRIBUTING.md — All MCP work MUST go in this repo - README.md — Full inventory of 37 servers + infra docs - .gitignore — Updated for Python venvs TOTAL: 37 MCP servers + full factory pipeline in one repo. This is now the single source of truth for all MCP work.
351 lines
8.0 KiB
JavaScript
Executable File
351 lines
8.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import open from "open";
|
|
import { resolve, dirname } from "path";
|
|
import { spawnPromise, spawn } from "spawn-rx";
|
|
import { fileURLToPath } from "url";
|
|
import { randomBytes } from "crypto";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms, true));
|
|
}
|
|
|
|
function getClientUrl(port, authDisabled, sessionToken, serverPort) {
|
|
const host = process.env.HOST || "localhost";
|
|
const baseUrl = `http://${host}:${port}`;
|
|
|
|
const params = new URLSearchParams();
|
|
if (serverPort && serverPort !== DEFAULT_MCP_PROXY_LISTEN_PORT) {
|
|
params.set("MCP_PROXY_PORT", serverPort);
|
|
}
|
|
if (!authDisabled) {
|
|
params.set("MCP_PROXY_AUTH_TOKEN", sessionToken);
|
|
}
|
|
return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl;
|
|
}
|
|
|
|
async function startDevServer(serverOptions) {
|
|
const {
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
sessionToken,
|
|
envVars,
|
|
abort,
|
|
transport,
|
|
serverUrl,
|
|
} = serverOptions;
|
|
const serverCommand = "npx";
|
|
const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"];
|
|
const isWindows = process.platform === "win32";
|
|
|
|
const spawnOptions = {
|
|
cwd: resolve(__dirname, "../..", "server"),
|
|
env: {
|
|
...process.env,
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
MCP_PROXY_AUTH_TOKEN: sessionToken,
|
|
MCP_ENV_VARS: JSON.stringify(envVars),
|
|
...(transport ? { MCP_TRANSPORT: transport } : {}),
|
|
...(serverUrl ? { MCP_SERVER_URL: serverUrl } : {}),
|
|
},
|
|
signal: abort.signal,
|
|
echoOutput: true,
|
|
};
|
|
|
|
// For Windows, we need to ignore stdin to simulate < NUL
|
|
// spawn-rx's 'stdin' option expects an Observable, not 'ignore'
|
|
// Use Node's stdio option instead
|
|
if (isWindows) {
|
|
spawnOptions.stdio = ["ignore", "pipe", "pipe"];
|
|
}
|
|
|
|
const server = spawn(serverCommand, serverArgs, spawnOptions);
|
|
|
|
// Give server time to start
|
|
const serverOk = await Promise.race([
|
|
new Promise((resolve) => {
|
|
server.subscribe({
|
|
complete: () => resolve(false),
|
|
error: () => resolve(false),
|
|
next: () => {}, // We're using echoOutput
|
|
});
|
|
}),
|
|
delay(3000).then(() => true),
|
|
]);
|
|
|
|
return { server, serverOk };
|
|
}
|
|
|
|
async function startProdServer(serverOptions) {
|
|
const {
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
sessionToken,
|
|
envVars,
|
|
abort,
|
|
command,
|
|
mcpServerArgs,
|
|
transport,
|
|
serverUrl,
|
|
} = serverOptions;
|
|
const inspectorServerPath = resolve(
|
|
__dirname,
|
|
"../..",
|
|
"server",
|
|
"build",
|
|
"index.js",
|
|
);
|
|
|
|
const server = spawnPromise(
|
|
"node",
|
|
[
|
|
inspectorServerPath,
|
|
...(command ? [`--command=${command}`] : []),
|
|
...(mcpServerArgs && mcpServerArgs.length > 0
|
|
? [`--args=${mcpServerArgs.join(" ")}`]
|
|
: []),
|
|
...(transport ? [`--transport=${transport}`] : []),
|
|
...(serverUrl ? [`--server-url=${serverUrl}`] : []),
|
|
],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
MCP_PROXY_AUTH_TOKEN: sessionToken,
|
|
MCP_ENV_VARS: JSON.stringify(envVars),
|
|
},
|
|
signal: abort.signal,
|
|
echoOutput: true,
|
|
},
|
|
);
|
|
|
|
// Make sure server started before starting client
|
|
const serverOk = await Promise.race([server, delay(2 * 1000)]);
|
|
|
|
return { server, serverOk };
|
|
}
|
|
|
|
async function startDevClient(clientOptions) {
|
|
const {
|
|
CLIENT_PORT,
|
|
SERVER_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
abort,
|
|
cancelled,
|
|
} = clientOptions;
|
|
const clientCommand = "npx";
|
|
const host = process.env.HOST || "localhost";
|
|
const clientArgs = ["vite", "--port", CLIENT_PORT, "--host", host];
|
|
const isWindows = process.platform === "win32";
|
|
|
|
const spawnOptions = {
|
|
cwd: resolve(__dirname, ".."),
|
|
env: { ...process.env, CLIENT_PORT },
|
|
signal: abort.signal,
|
|
echoOutput: true,
|
|
};
|
|
|
|
// For Windows, we need to ignore stdin to prevent hanging
|
|
if (isWindows) {
|
|
spawnOptions.stdio = ["ignore", "pipe", "pipe"];
|
|
}
|
|
|
|
const client = spawn(clientCommand, clientArgs, spawnOptions);
|
|
|
|
const url = getClientUrl(
|
|
CLIENT_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
SERVER_PORT,
|
|
);
|
|
|
|
// Give vite time to start before opening or logging the URL
|
|
setTimeout(() => {
|
|
console.log(`\n🚀 MCP Inspector is up and running at:\n ${url}\n`);
|
|
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
|
|
console.log("🌐 Opening browser...");
|
|
open(url);
|
|
}
|
|
}, 3000);
|
|
|
|
await new Promise((resolve) => {
|
|
client.subscribe({
|
|
complete: resolve,
|
|
error: (err) => {
|
|
if (!cancelled || process.env.DEBUG) {
|
|
console.error("Client error:", err);
|
|
}
|
|
resolve(null);
|
|
},
|
|
next: () => {}, // We're using echoOutput
|
|
});
|
|
});
|
|
}
|
|
|
|
async function startProdClient(clientOptions) {
|
|
const {
|
|
CLIENT_PORT,
|
|
SERVER_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
abort,
|
|
cancelled,
|
|
} = clientOptions;
|
|
const inspectorClientPath = resolve(
|
|
__dirname,
|
|
"../..",
|
|
"client",
|
|
"bin",
|
|
"client.js",
|
|
);
|
|
|
|
const url = getClientUrl(
|
|
CLIENT_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
SERVER_PORT,
|
|
);
|
|
|
|
await spawnPromise("node", [inspectorClientPath], {
|
|
env: {
|
|
...process.env,
|
|
CLIENT_PORT,
|
|
INSPECTOR_URL: url,
|
|
},
|
|
signal: abort.signal,
|
|
echoOutput: true,
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const envVars = {};
|
|
const mcpServerArgs = [];
|
|
let command = null;
|
|
let parsingFlags = true;
|
|
let isDev = false;
|
|
let transport = null;
|
|
let serverUrl = null;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
|
|
if (parsingFlags && arg === "--") {
|
|
parsingFlags = false;
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "--dev") {
|
|
isDev = true;
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "--transport" && i + 1 < args.length) {
|
|
transport = args[++i];
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "--server-url" && i + 1 < args.length) {
|
|
serverUrl = args[++i];
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
|
|
const envVar = args[++i];
|
|
const equalsIndex = envVar.indexOf("=");
|
|
|
|
if (equalsIndex !== -1) {
|
|
const key = envVar.substring(0, equalsIndex);
|
|
const value = envVar.substring(equalsIndex + 1);
|
|
envVars[key] = value;
|
|
} else {
|
|
envVars[envVar] = "";
|
|
}
|
|
} else if (!command && !isDev) {
|
|
command = arg;
|
|
} else if (!isDev) {
|
|
mcpServerArgs.push(arg);
|
|
}
|
|
}
|
|
|
|
const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274";
|
|
const SERVER_PORT = process.env.SERVER_PORT ?? DEFAULT_MCP_PROXY_LISTEN_PORT;
|
|
|
|
console.log(
|
|
isDev
|
|
? "Starting MCP inspector in development mode..."
|
|
: "Starting MCP inspector...",
|
|
);
|
|
|
|
// Use provided token from environment or generate a new one
|
|
const sessionToken =
|
|
process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString("hex");
|
|
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
|
|
|
const abort = new AbortController();
|
|
|
|
let cancelled = false;
|
|
process.on("SIGINT", () => {
|
|
cancelled = true;
|
|
abort.abort();
|
|
});
|
|
|
|
let server, serverOk;
|
|
|
|
try {
|
|
const serverOptions = {
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
sessionToken,
|
|
envVars,
|
|
abort,
|
|
command,
|
|
mcpServerArgs,
|
|
transport,
|
|
serverUrl,
|
|
};
|
|
|
|
const result = isDev
|
|
? await startDevServer(serverOptions)
|
|
: await startProdServer(serverOptions);
|
|
|
|
server = result.server;
|
|
serverOk = result.serverOk;
|
|
} catch (error) {}
|
|
|
|
if (serverOk) {
|
|
try {
|
|
const clientOptions = {
|
|
CLIENT_PORT,
|
|
SERVER_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
abort,
|
|
cancelled,
|
|
};
|
|
|
|
await (isDev
|
|
? startDevClient(clientOptions)
|
|
: startProdClient(clientOptions));
|
|
} catch (e) {
|
|
if (!cancelled || process.env.DEBUG) throw e;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
main()
|
|
.then((_) => process.exit(0))
|
|
.catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|