Merge pull request #529 from modelcontextprotocol/fweinberger/client-localhost

fix: bind client to localhost to match server
This commit is contained in:
Felix Weinberger 2025-06-26 10:05:52 +01:00 • committed by GitHub
commit eb5be0d853
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 36 additions and 30 deletions

View file

@ -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

View file

@ -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

View file

@ -39,19 +39,20 @@ 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} 🚀`,
`🔍 MCP Inspector is up and running at http://${host}:${port} 🚀`,
);
});
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);

View file

@ -12,6 +12,14 @@ function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms, true));
}
function getClientUrl(port, authDisabled, sessionToken) {
const host = process.env.HOST || "localhost";
const baseUrl = `http://${host}:${port}`;
return authDisabled
? baseUrl
: `${baseUrl}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
}
async function startDevServer(serverOptions) {
const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } =
serverOptions;
@ -23,7 +31,7 @@ async function startDevServer(serverOptions) {
cwd: resolve(__dirname, "../..", "server"),
env: {
...process.env,
PORT: SERVER_PORT,
SERVER_PORT: SERVER_PORT,
CLIENT_PORT: CLIENT_PORT,
MCP_PROXY_TOKEN: sessionToken,
MCP_ENV_VARS: JSON.stringify(envVars),
@ -82,7 +90,7 @@ async function startProdServer(serverOptions) {
{
env: {
...process.env,
PORT: SERVER_PORT,
SERVER_PORT: SERVER_PORT,
CLIENT_PORT: CLIENT_PORT,
MCP_PROXY_TOKEN: sessionToken,
MCP_ENV_VARS: JSON.stringify(envVars),
@ -102,20 +110,19 @@ async function startDevClient(clientOptions) {
const { CLIENT_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: 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);
// Give vite time to start before opening browser
setTimeout(() => {
@ -139,7 +146,8 @@ async function startDevClient(clientOptions) {
}
async function startProdClient(clientOptions) {
const { CLIENT_PORT, authDisabled, sessionToken, abort } = clientOptions;
const { CLIENT_PORT, authDisabled, sessionToken, abort, cancelled } =
clientOptions;
const inspectorClientPath = resolve(
__dirname,
"../..",
@ -148,16 +156,14 @@ 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}`;
// Only auto-open browser if not cancelled
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && !cancelled) {
const url = getClientUrl(CLIENT_PORT, authDisabled, sessionToken);
open(url);
}
await spawnPromise("node", [inspectorClientPath], {
env: { ...process.env, PORT: CLIENT_PORT },
env: { ...process.env, CLIENT_PORT: CLIENT_PORT },
signal: abort.signal,
echoOutput: true,
});

View file

@ -104,12 +104,10 @@ const originValidationMiddleware = (
// Default origins based on CLIENT_PORT or use environment variable
const clientPort = process.env.CLIENT_PORT || "6274";
const defaultOrigins = [
`http://localhost:${clientPort}`,
`http://127.0.0.1:${clientPort}`,
const defaultOrigin = `http://localhost:${clientPort}`;
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [
defaultOrigin,
];
const allowedOrigins =
process.env.ALLOWED_ORIGINS?.split(",") || defaultOrigins;
if (origin && !allowedOrigins.includes(origin)) {
console.error(`Invalid origin: ${origin}`);
@ -530,8 +528,8 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
}
});
const PORT = parseInt(process.env.PORT || "6277", 10);
const HOST = process.env.HOST || "127.0.0.1";
const PORT = parseInt(process.env.SERVER_PORT || "6277", 10);
const HOST = process.env.HOST || "localhost";
const server = app.listen(PORT, HOST);
server.on("listening", () => {
@ -544,7 +542,8 @@ server.on("listening", () => {
// Display clickable URL with pre-filled token
const clientPort = process.env.CLIENT_PORT || "6274";
const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
const clientHost = process.env.HOST || "localhost";
const clientUrl = `http://${clientHost}:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
console.log(
`\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n`,
);