diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 2630a495..573672cc 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -21,8 +21,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 + id: setup_node with: - node-version: 18 + node-version-file: package.json + cache: npm # Cache Playwright browsers - name: Cache Playwright browsers @@ -69,7 +71,7 @@ jobs: job-summary: true icon-style: "emojis" custom-info: | - **Test Environment:** Ubuntu Latest, Node.js 18 + **Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }} **Browsers:** Chromium, Firefox š [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 65e98fa4..d01f7175 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version-file: package.json cache: npm # Working around https://github.com/npm/cli/issues/4828 @@ -53,7 +53,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version-file: package.json cache: npm registry-url: "https://registry.npmjs.org" diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..980f29c2 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.x.x diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1eeeb864..0129a9e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Thanks for your interest in contributing! This guide explains how to get involve 1. Fork the repository and clone it locally 2. Install dependencies with `npm install` 3. Run `npm run dev` to start both client and server in development mode -4. Use the web UI at http://127.0.0.1:6274 to interact with the inspector +4. Use the web UI at http://localhost:6274 to interact with the inspector ## Development Process & Pull Requests diff --git a/README.md b/README.md index 0c20df14..6368ed04 100644 --- a/README.md +++ b/README.md @@ -168,20 +168,20 @@ DANGEROUSLY_OMIT_AUTH=true npm start #### Local-only Binding -By default, the MCP Inspector proxy server binds only to `127.0.0.1` (localhost) to prevent network access. This ensures the server is not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable: +By default, both the MCP Inspector proxy server and client bind only to `localhost` to prevent network access. This ensures they are not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable: ```bash HOST=0.0.0.0 npm start ``` -**Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes. +**Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes and both services to network access. #### DNS Rebinding Protection To prevent DNS rebinding attacks, the MCP Inspector validates the `Origin` header on incoming requests. By default, only requests from the client origin are allowed (respects `CLIENT_PORT` if set, defaulting to port 6274). You can configure additional allowed origins by setting the `ALLOWED_ORIGINS` environment variable (comma-separated list): ```bash -ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://localhost:8000 npm start +ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start ``` ### Configuration diff --git a/client/bin/client.js b/client/bin/client.js index 7179e19e..2a7419e6 100755 --- a/client/bin/client.js +++ b/client/bin/client.js @@ -1,5 +1,6 @@ #!/usr/bin/env node +import open from "open"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import handler from "serve-handler"; @@ -39,19 +40,23 @@ const server = http.createServer((request, response) => { return handler(request, response, handlerOptions); }); -const port = process.env.PORT || 6274; +const port = parseInt(process.env.CLIENT_PORT || "6274", 10); +const host = process.env.HOST || "localhost"; server.on("listening", () => { - console.log( - `š MCP Inspector is up and running at http://127.0.0.1:${port} š`, - ); + const url = process.env.INSPECTOR_URL || `http://${host}:${port}`; + 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); + } }); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { console.error( - `ā MCP Inspector PORT IS IN USE at http://127.0.0.1:${port} ā `, + `ā MCP Inspector PORT IS IN USE at http://${host}:${port} ā `, ); } else { throw err; } }); -server.listen(port); +server.listen(port, host); diff --git a/client/bin/start.js b/client/bin/start.js index ad75078a..70ca046e 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -7,11 +7,26 @@ 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 } = serverOptions; @@ -23,8 +38,8 @@ async function startDevServer(serverOptions) { cwd: resolve(__dirname, "../..", "server"), env: { ...process.env, - PORT: SERVER_PORT, - CLIENT_PORT: CLIENT_PORT, + SERVER_PORT, + CLIENT_PORT, MCP_PROXY_TOKEN: sessionToken, MCP_ENV_VARS: JSON.stringify(envVars), }, @@ -82,8 +97,8 @@ async function startProdServer(serverOptions) { { env: { ...process.env, - PORT: SERVER_PORT, - CLIENT_PORT: CLIENT_PORT, + SERVER_PORT, + CLIENT_PORT, MCP_PROXY_TOKEN: sessionToken, MCP_ENV_VARS: JSON.stringify(envVars), }, @@ -99,30 +114,40 @@ async function startProdServer(serverOptions) { } async function startDevClient(clientOptions) { - const { CLIENT_PORT, authDisabled, sessionToken, abort, cancelled } = - clientOptions; + const { + CLIENT_PORT, + SERVER_PORT, + authDisabled, + sessionToken, + abort, + cancelled, + } = clientOptions; const clientCommand = "npx"; - const clientArgs = ["vite", "--port", CLIENT_PORT]; + const host = process.env.HOST || "localhost"; + const clientArgs = ["vite", "--port", CLIENT_PORT, "--host", host]; const client = spawn(clientCommand, clientArgs, { cwd: resolve(__dirname, ".."), - env: { ...process.env, PORT: CLIENT_PORT }, + env: { ...process.env, CLIENT_PORT }, signal: abort.signal, echoOutput: true, }); - // Auto-open browser after vite starts - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { - const url = authDisabled - ? `http://127.0.0.1:${CLIENT_PORT}` - : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; + const url = getClientUrl( + CLIENT_PORT, + authDisabled, + sessionToken, + SERVER_PORT, + ); - // Give vite time to start before opening browser - setTimeout(() => { + // 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); - console.log(`\nš Opening browser at: ${url}\n`); - }, 3000); - } + } + }, 3000); await new Promise((resolve) => { client.subscribe({ @@ -139,7 +164,14 @@ async function startDevClient(clientOptions) { } async function startProdClient(clientOptions) { - const { CLIENT_PORT, authDisabled, sessionToken, abort } = clientOptions; + const { + CLIENT_PORT, + SERVER_PORT, + authDisabled, + sessionToken, + abort, + cancelled, + } = clientOptions; const inspectorClientPath = resolve( __dirname, "../..", @@ -148,16 +180,19 @@ async function startProdClient(clientOptions) { "client.js", ); - // Auto-open browser with token - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { - const url = authDisabled - ? `http://127.0.0.1:${CLIENT_PORT}` - : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; - open(url); - } + const url = getClientUrl( + CLIENT_PORT, + authDisabled, + sessionToken, + SERVER_PORT, + ); await spawnPromise("node", [inspectorClientPath], { - env: { ...process.env, PORT: CLIENT_PORT }, + env: { + ...process.env, + CLIENT_PORT, + INSPECTOR_URL: url, + }, signal: abort.signal, echoOutput: true, }); @@ -204,7 +239,7 @@ async function main() { } const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274"; - const SERVER_PORT = process.env.SERVER_PORT ?? "6277"; + const SERVER_PORT = process.env.SERVER_PORT ?? DEFAULT_MCP_PROXY_LISTEN_PORT; console.log( isDev @@ -249,6 +284,7 @@ async function main() { try { const clientOptions = { CLIENT_PORT, + SERVER_PORT, authDisabled, sessionToken, abort, diff --git a/client/index.html b/client/index.html index 00f5600b..b3736a82 100644 --- a/client/index.html +++ b/client/index.html @@ -7,7 +7,7 @@