feat(builder): require a bearer token on both HTTP routes when auth is on
POST /messages is gated too, and cross-checked against the session's own identity - otherwise a valid token for one caller could drive another caller's session by guessing its id.
This commit is contained in:
@@ -26,31 +26,57 @@ if (existsSync(envPath)) {
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { registerVectorTools } from "./vector/index.js";
|
||||
import { loadAuthConfig, resolveIdentity } from "./auth.js";
|
||||
|
||||
function createMcpServer() {
|
||||
function createMcpServer(identity: string) {
|
||||
const server = new McpServer({
|
||||
name: "vector-mcp-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
registerVectorTools(server);
|
||||
registerVectorTools(server, identity);
|
||||
return server;
|
||||
}
|
||||
|
||||
console.error('Vector MCP Server starting...');
|
||||
|
||||
const authConfig = loadAuthConfig();
|
||||
if (authConfig.enabled) {
|
||||
console.error(`Auth: enabled (${authConfig.identities.size} identit${authConfig.identities.size === 1 ? 'y' : 'ies'} configured)`);
|
||||
} else {
|
||||
console.error('Auth: DISABLED — every caller is treated as "anonymous" and shares one rate-limit bucket.');
|
||||
console.error('Auth: set MCP_AUTH_TOKENS to require a bearer token. Do not run a public instance without it.');
|
||||
}
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '3000');
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
const sessionIdentities = new Map<string, string>();
|
||||
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
const url = new URL(req.url!, `http://localhost:${PORT}`);
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/sse') {
|
||||
const auth = resolveIdentity(req.headers.authorization, authConfig);
|
||||
if (!auth.ok) {
|
||||
res.writeHead(401, { 'Content-Type': 'text/plain', 'WWW-Authenticate': 'Bearer' });
|
||||
res.end(auth.reason);
|
||||
return;
|
||||
}
|
||||
const transport = new SSEServerTransport('/messages', res);
|
||||
const session = createMcpServer();
|
||||
const session = createMcpServer(auth.identity);
|
||||
sessionIdentities.set(transport.sessionId, auth.identity);
|
||||
transports.set(transport.sessionId, transport);
|
||||
res.on('close', () => transports.delete(transport.sessionId));
|
||||
res.on('close', () => {
|
||||
transports.delete(transport.sessionId);
|
||||
sessionIdentities.delete(transport.sessionId);
|
||||
});
|
||||
await session.connect(transport);
|
||||
} else if (req.method === 'POST' && url.pathname === '/messages') {
|
||||
const auth = resolveIdentity(req.headers.authorization, authConfig);
|
||||
if (!auth.ok) {
|
||||
res.writeHead(401, { 'Content-Type': 'text/plain', 'WWW-Authenticate': 'Bearer' });
|
||||
res.end(auth.reason);
|
||||
return;
|
||||
}
|
||||
const sessionId = url.searchParams.get('sessionId');
|
||||
if (!sessionId) {
|
||||
res.writeHead(400);
|
||||
@@ -63,6 +89,12 @@ const httpServer = createServer(async (req, res) => {
|
||||
res.end('Session not found');
|
||||
return;
|
||||
}
|
||||
// A valid token for identity A must not be able to drive identity B's session.
|
||||
if (sessionIdentities.get(sessionId) !== auth.identity) {
|
||||
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
||||
res.end('This session belongs to a different identity.');
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
|
||||
Reference in New Issue
Block a user