Skip to content

Commit 775195f

Browse files
committed
feat: Add authentication to the endpoints
1 parent 0250857 commit 775195f

File tree

6 files changed

+607
-0
lines changed

6 files changed

+607
-0
lines changed

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ options:
3737
- `--port`: Specify the port to listen on (default: 8080)
3838
- `--debug`: Enable debug logging
3939
- `--shell`: Spawn the server via the user's shell
40+
- `--apiKey`: API key for authenticating requests (uses X-API-Key header)
4041

4142
### Passing arguments to the wrapped command
4243

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

87+
### API Key Authentication
88+
89+
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.
90+
91+
#### Enabling Authentication
92+
93+
Authentication is disabled by default for backward compatibility. To enable it, provide an API key via:
94+
95+
**Command-line:**
96+
```bash
97+
npx mcp-proxy --port 8080 --apiKey "your-secret-key" tsx server.js
98+
```
99+
100+
**Environment variable:**
101+
```bash
102+
export MCP_PROXY_API_KEY="your-secret-key"
103+
npx mcp-proxy --port 8080 tsx server.js
104+
```
105+
106+
#### Client Configuration
107+
108+
Clients must include the API key in the `X-API-Key` header:
109+
110+
```typescript
111+
// For streamable HTTP transport
112+
const transport = new StreamableHTTPClientTransport(
113+
new URL('http://localhost:8080/mcp'),
114+
{
115+
headers: {
116+
'X-API-Key': 'your-secret-key'
117+
}
118+
}
119+
);
120+
121+
// For SSE transport
122+
const transport = new SSEClientTransport(
123+
new URL('http://localhost:8080/sse'),
124+
{
125+
headers: {
126+
'X-API-Key': 'your-secret-key'
127+
}
128+
}
129+
);
130+
```
131+
132+
#### Exempt Endpoints
133+
134+
The following endpoints do not require authentication:
135+
- `/ping` - Health check endpoint
136+
- `OPTIONS` requests - CORS preflight requests
137+
138+
#### Security Notes
139+
140+
- **Use HTTPS in production**: API keys should only be transmitted over secure connections
141+
- **Keep keys secure**: Never commit API keys to version control
142+
- **Generate strong keys**: Use cryptographically secure random strings for API keys
143+
- **Rotate keys regularly**: Change API keys periodically for better security
144+
86145
### Node.js SDK
87146

88147
The Node.js SDK provides several utilities that are used to create a proxy.
@@ -137,6 +196,7 @@ Options:
137196
- `sseEndpoint`: SSE endpoint path (default: "/sse", set to null to disable)
138197
- `streamEndpoint`: Streamable HTTP endpoint path (default: "/mcp", set to null to disable)
139198
- `stateless`: Enable stateless mode for HTTP streamable transport (default: false)
199+
- `apiKey`: API key for authenticating requests (optional)
140200
- `onConnect`: Callback when a server connects (optional)
141201
- `onClose`: Callback when a server disconnects (optional)
142202
- `onUnhandledRequest`: Callback for unhandled HTTP requests (optional)

src/authentication.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { IncomingMessage } from "http";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { AuthenticationMiddleware } from "./authentication.js";
5+
6+
describe("AuthenticationMiddleware", () => {
7+
const createMockRequest = (headers: Record<string, string> = {}): IncomingMessage => {
8+
// Simulate Node.js http module behavior which converts all header names to lowercase
9+
const lowercaseHeaders: Record<string, string> = {};
10+
for (const [key, value] of Object.entries(headers)) {
11+
lowercaseHeaders[key.toLowerCase()] = value;
12+
}
13+
return {
14+
headers: lowercaseHeaders,
15+
} as IncomingMessage;
16+
};
17+
18+
describe("when no auth is configured", () => {
19+
it("should allow all requests", () => {
20+
const middleware = new AuthenticationMiddleware({});
21+
const req = createMockRequest();
22+
23+
expect(middleware.validateRequest(req)).toBe(true);
24+
});
25+
26+
it("should allow requests even with headers", () => {
27+
const middleware = new AuthenticationMiddleware({});
28+
const req = createMockRequest({ "x-api-key": "some-key" });
29+
30+
expect(middleware.validateRequest(req)).toBe(true);
31+
});
32+
});
33+
34+
describe("X-API-Key validation", () => {
35+
const apiKey = "test-api-key-123";
36+
37+
it("should accept valid API key", () => {
38+
const middleware = new AuthenticationMiddleware({ apiKey });
39+
const req = createMockRequest({ "x-api-key": apiKey });
40+
41+
expect(middleware.validateRequest(req)).toBe(true);
42+
});
43+
44+
it("should reject missing API key", () => {
45+
const middleware = new AuthenticationMiddleware({ apiKey });
46+
const req = createMockRequest();
47+
48+
expect(middleware.validateRequest(req)).toBe(false);
49+
});
50+
51+
it("should reject incorrect API key", () => {
52+
const middleware = new AuthenticationMiddleware({ apiKey });
53+
const req = createMockRequest({ "x-api-key": "wrong-key" });
54+
55+
expect(middleware.validateRequest(req)).toBe(false);
56+
});
57+
58+
it("should reject empty API key", () => {
59+
const middleware = new AuthenticationMiddleware({ apiKey });
60+
const req = createMockRequest({ "x-api-key": "" });
61+
62+
expect(middleware.validateRequest(req)).toBe(false);
63+
});
64+
65+
it("should be case-insensitive for header names", () => {
66+
const middleware = new AuthenticationMiddleware({ apiKey });
67+
const req = createMockRequest({ "X-API-KEY": apiKey });
68+
69+
expect(middleware.validateRequest(req)).toBe(true);
70+
});
71+
72+
it("should work with mixed case header names", () => {
73+
const middleware = new AuthenticationMiddleware({ apiKey });
74+
const req = createMockRequest({ "X-Api-Key": apiKey });
75+
76+
expect(middleware.validateRequest(req)).toBe(true);
77+
});
78+
79+
it("should handle array headers (if multiple same headers)", () => {
80+
const middleware = new AuthenticationMiddleware({ apiKey });
81+
const req = {
82+
headers: {
83+
"x-api-key": [apiKey, "another-key"],
84+
},
85+
} as unknown as IncomingMessage;
86+
87+
// Should fail because header is an array, not a string
88+
expect(middleware.validateRequest(req)).toBe(false);
89+
});
90+
});
91+
92+
describe("getUnauthorizedResponse", () => {
93+
it("should return proper unauthorized response", () => {
94+
const middleware = new AuthenticationMiddleware({ apiKey: "test" });
95+
const response = middleware.getUnauthorizedResponse();
96+
97+
expect(response.headers["Content-Type"]).toBe("application/json");
98+
99+
const body = JSON.parse(response.body);
100+
expect(body.error.code).toBe(401);
101+
expect(body.error.message).toBe("Unauthorized: Invalid or missing API key");
102+
expect(body.jsonrpc).toBe("2.0");
103+
expect(body.id).toBe(null);
104+
});
105+
106+
it("should have consistent format regardless of configuration", () => {
107+
const middleware1 = new AuthenticationMiddleware({});
108+
const middleware2 = new AuthenticationMiddleware({ apiKey: "test" });
109+
110+
const response1 = middleware1.getUnauthorizedResponse();
111+
const response2 = middleware2.getUnauthorizedResponse();
112+
113+
expect(response1.headers).toEqual(response2.headers);
114+
expect(response1.body).toEqual(response2.body);
115+
});
116+
});
117+
});

src/authentication.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { IncomingMessage } from "http";
2+
3+
export interface AuthConfig {
4+
apiKey?: string;
5+
}
6+
7+
export class AuthenticationMiddleware {
8+
constructor(private config: AuthConfig = {}) {}
9+
10+
getUnauthorizedResponse() {
11+
return {
12+
body: JSON.stringify({
13+
error: {
14+
code: 401,
15+
message: "Unauthorized: Invalid or missing API key",
16+
},
17+
id: null,
18+
jsonrpc: "2.0",
19+
}),
20+
headers: {
21+
"Content-Type": "application/json",
22+
},
23+
};
24+
}
25+
26+
validateRequest(req: IncomingMessage): boolean {
27+
// No auth required if no API key configured (backward compatibility)
28+
if (!this.config.apiKey) {
29+
return true;
30+
}
31+
32+
// Check X-API-Key header (case-insensitive)
33+
// Node.js http module automatically converts all header names to lowercase
34+
const apiKey = req.headers["x-api-key"];
35+
36+
if (!apiKey || typeof apiKey !== "string") {
37+
return false;
38+
}
39+
40+
return apiKey === this.config.apiKey;
41+
}
42+
}

src/bin/mcp-proxy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const argv = await yargs(hideBin(process.argv))
3838
"populate--": true,
3939
})
4040
.options({
41+
apiKey: {
42+
describe: "API key for authenticating requests (uses X-API-Key header)",
43+
type: "string",
44+
},
4145
debug: {
4246
default: false,
4347
describe: "Enable debug logging",
@@ -160,6 +164,7 @@ const proxy = async () => {
160164
};
161165

162166
const server = await startHTTPServer({
167+
apiKey: argv.apiKey,
163168
createServer,
164169
eventStore: new InMemoryEventStore(),
165170
host: argv.host,

0 commit comments

Comments
 (0)