Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ options:
- `--port`: Specify the port to listen on (default: 8080)
- `--debug`: Enable debug logging
- `--shell`: Spawn the server via the user's shell
- `--apiKey`: API key for authenticating requests (uses X-API-Key header)

### Passing arguments to the wrapped command

Expand Down Expand Up @@ -83,6 +84,64 @@ npx mcp-proxy --port 8080 --stateless --server stream tsx server.js
- **Request isolation**: When you need complete independence between requests
- **Simple deployments**: When you don't need to maintain connection state

### API Key Authentication

MCP Proxy supports optional API key authentication to secure your endpoints. When enabled, clients must provide a valid API key in the `X-API-Key` header to access the proxy.

#### Enabling Authentication

Authentication is disabled by default for backward compatibility. To enable it, provide an API key via:

**Command-line:**
```bash
npx mcp-proxy --port 8080 --apiKey "your-secret-key" tsx server.js
```

**Environment variable:**
```bash
export MCP_PROXY_API_KEY="your-secret-key"
npx mcp-proxy --port 8080 tsx server.js
```

#### Client Configuration

Clients must include the API key in the `X-API-Key` header:

```typescript
// For streamable HTTP transport
const transport = new StreamableHTTPClientTransport(
new URL('http://localhost:8080/mcp'),
{
headers: {
'X-API-Key': 'your-secret-key'
}
}
);

// For SSE transport
const transport = new SSEClientTransport(
new URL('http://localhost:8080/sse'),
{
headers: {
'X-API-Key': 'your-secret-key'
}
}
);
```

#### Exempt Endpoints

The following endpoints do not require authentication:
- `/ping` - Health check endpoint
- `OPTIONS` requests - CORS preflight requests

#### Security Notes

- **Use HTTPS in production**: API keys should only be transmitted over secure connections
- **Keep keys secure**: Never commit API keys to version control
- **Generate strong keys**: Use cryptographically secure random strings for API keys
- **Rotate keys regularly**: Change API keys periodically for better security

### Node.js SDK

The Node.js SDK provides several utilities that are used to create a proxy.
Expand Down Expand Up @@ -137,6 +196,7 @@ Options:
- `sseEndpoint`: SSE endpoint path (default: "/sse", set to null to disable)
- `streamEndpoint`: Streamable HTTP endpoint path (default: "/mcp", set to null to disable)
- `stateless`: Enable stateless mode for HTTP streamable transport (default: false)
- `apiKey`: API key for authenticating requests (optional)
- `onConnect`: Callback when a server connects (optional)
- `onClose`: Callback when a server disconnects (optional)
- `onUnhandledRequest`: Callback for unhandled HTTP requests (optional)
Expand Down
117 changes: 117 additions & 0 deletions src/authentication.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { IncomingMessage } from "http";
import { describe, expect, it } from "vitest";

import { AuthenticationMiddleware } from "./authentication.js";

describe("AuthenticationMiddleware", () => {
const createMockRequest = (headers: Record<string, string> = {}): IncomingMessage => {
// Simulate Node.js http module behavior which converts all header names to lowercase
const lowercaseHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
lowercaseHeaders[key.toLowerCase()] = value;
}
return {
headers: lowercaseHeaders,
} as IncomingMessage;
};

describe("when no auth is configured", () => {
it("should allow all requests", () => {
const middleware = new AuthenticationMiddleware({});
const req = createMockRequest();

expect(middleware.validateRequest(req)).toBe(true);
});

it("should allow requests even with headers", () => {
const middleware = new AuthenticationMiddleware({});
const req = createMockRequest({ "x-api-key": "some-key" });

expect(middleware.validateRequest(req)).toBe(true);
});
});

describe("X-API-Key validation", () => {
const apiKey = "test-api-key-123";

it("should accept valid API key", () => {
const middleware = new AuthenticationMiddleware({ apiKey });
const req = createMockRequest({ "x-api-key": apiKey });

expect(middleware.validateRequest(req)).toBe(true);
});

it("should reject missing API key", () => {
const middleware = new AuthenticationMiddleware({ apiKey });
const req = createMockRequest();

expect(middleware.validateRequest(req)).toBe(false);
});

it("should reject incorrect API key", () => {
const middleware = new AuthenticationMiddleware({ apiKey });
const req = createMockRequest({ "x-api-key": "wrong-key" });

expect(middleware.validateRequest(req)).toBe(false);
});

it("should reject empty API key", () => {
const middleware = new AuthenticationMiddleware({ apiKey });
const req = createMockRequest({ "x-api-key": "" });

expect(middleware.validateRequest(req)).toBe(false);
});

it("should be case-insensitive for header names", () => {
const middleware = new AuthenticationMiddleware({ apiKey });
const req = createMockRequest({ "X-API-KEY": apiKey });

expect(middleware.validateRequest(req)).toBe(true);
});

it("should work with mixed case header names", () => {
const middleware = new AuthenticationMiddleware({ apiKey });
const req = createMockRequest({ "X-Api-Key": apiKey });

expect(middleware.validateRequest(req)).toBe(true);
});

it("should handle array headers (if multiple same headers)", () => {
const middleware = new AuthenticationMiddleware({ apiKey });
const req = {
headers: {
"x-api-key": [apiKey, "another-key"],
},
} as unknown as IncomingMessage;

// Should fail because header is an array, not a string
expect(middleware.validateRequest(req)).toBe(false);
});
});

describe("getUnauthorizedResponse", () => {
it("should return proper unauthorized response", () => {
const middleware = new AuthenticationMiddleware({ apiKey: "test" });
const response = middleware.getUnauthorizedResponse();

expect(response.headers["Content-Type"]).toBe("application/json");

const body = JSON.parse(response.body);
expect(body.error.code).toBe(401);
expect(body.error.message).toBe("Unauthorized: Invalid or missing API key");
expect(body.jsonrpc).toBe("2.0");
expect(body.id).toBe(null);
});

it("should have consistent format regardless of configuration", () => {
const middleware1 = new AuthenticationMiddleware({});
const middleware2 = new AuthenticationMiddleware({ apiKey: "test" });

const response1 = middleware1.getUnauthorizedResponse();
const response2 = middleware2.getUnauthorizedResponse();

expect(response1.headers).toEqual(response2.headers);
expect(response1.body).toEqual(response2.body);
});
});
});
43 changes: 43 additions & 0 deletions src/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { IncomingMessage } from "http";

export interface AuthConfig {
apiKey?: string;
}

export class AuthenticationMiddleware {
constructor(private config: AuthConfig = {}) {}

getUnauthorizedResponse() {
return {
body: JSON.stringify({
error: {
code: 401,
message: "Unauthorized: Invalid or missing API key",
},
id: null,
jsonrpc: "2.0",
}),
headers: {
"Content-Type": "application/json",
},
};
}

validateRequest(req: IncomingMessage): boolean {
// No auth required if no API key configured (backward compatibility)
if (!this.config.apiKey) {
return true;
}

// Check X-API-Key header (case-insensitive)
// Node.js http module automatically converts all header names to lowercase
const apiKey = req.headers["x-api-key"];

if (!apiKey || typeof apiKey !== "string") {
return false;
}

return apiKey === this.config.apiKey;
}
}

5 changes: 5 additions & 0 deletions src/bin/mcp-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const argv = await yargs(hideBin(process.argv))
"populate--": true,
})
.options({
apiKey: {
describe: "API key for authenticating requests (uses X-API-Key header)",
type: "string",
},
debug: {
default: false,
describe: "Enable debug logging",
Expand Down Expand Up @@ -160,6 +164,7 @@ const proxy = async () => {
};

const server = await startHTTPServer({
apiKey: argv.apiKey,
createServer,
eventStore: new InMemoryEventStore(),
host: argv.host,
Expand Down
Loading