abhigyanpatwari/GitNexus · error

Refusing to start the MCP HTTP server on a non-loopback host

Error message

Refusing to start the MCP HTTP server on a non-loopback host (${host}) without authentication — it would expose all indexed repos to anyone who can reach it. Pass --auth-token (or set GITNEXUS_MCP_AUTH_TOKEN), or bind --host 127.0.0.1. This applies to --host 0.0.0.0 and --host :: as well.

What it means

startMcpHttpServer refuses to bind a non-loopback host unless an auth token is configured — a deliberate security guard so exposing the HTTP MCP server cannot silently publish every indexed repo to anyone on the network. Loopback binds (127.0.0.1, ::1) stay open by default; 0.0.0.0, ::, LAN IPs, and hostnames all require --auth-token / GITNEXUS_MCP_AUTH_TOKEN.

Source

Thrown at gitnexus/src/mcp/http-transport.ts:477

 * - POST /mcp        — Streamable HTTP (modern clients)
 * - GET /sse         — legacy SSE stream (old clients)
 * - POST /messages   — legacy SSE message endpoint
 *
 * @param backend   LocalBackend instance
 * @param options   Server configuration
 * @returns         The listening http.Server
 */
export async function startMcpHttpServer(
  backend: LocalBackend,
  options: McpHttpOptions,
): Promise<HttpServer> {
  const { port, host, authToken } = options;

  // Refuse to start an unauthenticated server on a non-loopback interface — that
  // would silently expose every indexed repo to anyone who can reach the host.
  // Loopback binds stay open by default; non-loopback binds require a token.
  if (!authToken && !isLoopbackHost(host)) {
    throw new Error(
      `Refusing to start the MCP HTTP server on a non-loopback host (${host}) without ` +
        'authentication — it would expose all indexed repos to anyone who can reach it. ' +
        'Pass --auth-token (or set GITNEXUS_MCP_AUTH_TOKEN), or bind --host 127.0.0.1. ' +
        'This applies to --host 0.0.0.0 and --host :: as well.',
    );
  }

  const repositoryPolicy = options.repositoryPolicy ?? (await createMcpRepositoryPolicy(backend));

  const app: Express = express();

  // Suppress X-Powered-By to reduce information leakage.
  app.disable('x-powered-by');

  // PNA (Chrome 130+ Private Network Access) preflight support.
  // The browser sends `Access-Control-Request-Private-Network: true` ONLY on the
  // CORS preflight (an OPTIONS request); emit the matching allow header only then,
  // never on actual GET/POST responses. Runs before cors() so the header survives

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Provide a token: start with --auth-token <strong-secret> or export GITNEXUS_MCP_AUTH_TOKEN, and have clients send it per the server's auth scheme.
  2. Or keep binding loopback: --host 127.0.0.1, and put an authenticating reverse proxy (nginx/Traefik) in front for remote access.
  3. In Docker, bind 127.0.0.1 inside the container only if you use a proxy sidecar; otherwise pass the token and publish the port deliberately.
  4. Never widen the bind to work around the error without also adding auth — the guard exists because the endpoint exposes all indexed repos.

Example fix

# before: unauthenticated non-loopback bind (refuses to start)
$ gitnexus serve --host 0.0.0.0 --port 4747
# → Refusing to start the MCP HTTP server on a non-loopback host ...

# after: authenticated non-loopback bind
$ GITNEXUS_MCP_AUTH_TOKEN=$(openssl rand -hex 32) gitnexus serve --host 0.0.0.0 --port 4747
# or stay loopback behind an auth proxy:
$ gitnexus serve --host 127.0.0.1 --port 4747
Defensive patterns

Strategy: validation

Validate before calling

// Resolve and validate bind config before starting the server
function resolveMcpBind(opts: { host?: string; authToken?: string }): { host: string; authToken?: string } {
  const host = opts.host ?? '127.0.0.1';
  const authToken = opts.authToken ?? process.env.GITNEXUS_MCP_AUTH_TOKEN;
  const loopback = ['127.0.0.1', '::1', 'localhost'].includes(host);
  if (!loopback && !authToken) {
    throw new Error('Refusing non-loopback bind without GITNEXUS_MCP_AUTH_TOKEN / --auth-token');
  }
  return { host, authToken };
}

Try / catch

try {
  await startMcpHttpServer(backend, options);
} catch (err) {
  if (err instanceof Error && err.message.includes('without authentication')) {
    // intentional guard: fail deployment loudly, never bind quietly
    failDeployment('MCP HTTP requires auth for non-loopback binds');
  }
  throw err;
}

Prevention

When it happens

Trigger: Starting the MCP HTTP server with --host 0.0.0.0 (common in Docker, where 127.0.0.1 makes the port unreachable from outside the container), --host ::, a LAN IP, or a resolvable hostname, without passing --auth-token or setting GITNEXUS_MCP_AUTH_TOKEN.

Common situations: Dockerizing GitNexus and switching the bind to 0.0.0.0 for port mapping; sharing an MCP instance across a team LAN; copying a loopback dev config into a deployment; Kubernetes sidecar deployments exposing the port.

Understand the failure class

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/94c6c5637ed61940. Report an issue: GitHub.