• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jacob-hartmann / quire-mcp / 21574865257

02 Feb 2026 02:06AM UTC coverage: 90.943% (+83.3%) from 7.646%
21574865257

push

github

jacob-hartmann
Refactor test assertions in attachment and comment tools to improve clarity and consistency. Remove unnecessary binding of mock client methods, enhancing readability and maintainability of test cases. Update .gitignore to exclude package.json for cleaner project structure.

835 of 929 branches covered (89.88%)

Branch coverage included in aggregate %.

1615 of 1765 relevant lines covered (91.5%)

14.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.51
/src/server/http-server.ts
1
/**
2
 * HTTP Server for MCP
3
 *
4
 * Creates an Express-based HTTP server with:
5
 * - OAuth authorization server endpoints (via mcpAuthRouter)
6
 * - OAuth callback from Quire
7
 * - Protected MCP endpoint with Bearer auth
8
 * - StreamableHTTP transport
9
 * - Security hardening (helmet, rate limiting, CORS, cache control)
10
 */
11

12
import { randomUUID } from "node:crypto";
13
import express from "express";
14
import helmet from "helmet";
15
import rateLimit from "express-rate-limit";
16
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
17
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
18
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
19
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
21
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
23

24
import type { HttpServerConfig } from "./config.js";
25
import {
26
  QuireProxyOAuthProvider,
27
  handleQuireOAuthCallback,
28
} from "./quire-oauth-provider.js";
29
import { getServerTokenStore } from "./server-token-store.js";
30
import { isCorsAllowedPath } from "./cors.js";
31
import {
32
  SESSION_ID_DISPLAY_LENGTH,
33
  JSONRPC_ERROR_INVALID_REQUEST,
34
  JSONRPC_ERROR_INTERNAL,
35
} from "../constants.js";
36
import { escapeHtml } from "../utils/html.js";
37
import { LRUCache } from "../utils/lru-cache.js";
38

39
// ---------------------------------------------------------------------------
40
// Constants
41
// ---------------------------------------------------------------------------
42

43
/** Token cleanup interval in milliseconds (5 minutes) */
44
const TOKEN_CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
2✔
45

46
/** Session idle timeout in milliseconds (30 minutes) */
47
const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
2✔
48

49
/** Rate limit: max requests per window */
50
const RATE_LIMIT_MAX = 100;
2✔
51

52
/** Rate limit window in milliseconds (1 minute) */
53
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
2✔
54

55
/** Maximum number of concurrent sessions to prevent memory exhaustion */
56
const MAX_SESSIONS = 1000;
2✔
57

58
// ---------------------------------------------------------------------------
59
// HTTP Server
60
// ---------------------------------------------------------------------------
61

62
/** Track session last activity for idle timeout */
63
interface SessionInfo {
64
  transport: StreamableHTTPServerTransport;
65
  lastActivity: number;
66
}
67

68
/**
69
 * Start the HTTP server with OAuth and MCP endpoints.
70
 */
71
export async function startHttpServer(
72
  getServer: () => McpServer,
73
  config: HttpServerConfig
74
): Promise<void> {
75
  const { host, port, issuerUrl } = config;
21✔
76

77
  // Create OAuth provider
78
  const provider = new QuireProxyOAuthProvider(config);
21✔
79

80
  // Create Express app with DNS rebinding protection
81
  const app = createMcpExpressApp({ host });
21✔
82

83
  // Security headers via helmet
84
  app.use(
21✔
85
    helmet({
86
      // Disable contentSecurityPolicy as MCP uses JSON-RPC
87
      contentSecurityPolicy: false,
88
      // Keep other security headers
89
      crossOriginEmbedderPolicy: false,
90
    })
91
  );
92

93
  // Rate limiting for OAuth and MCP endpoints
94
  const limiter = rateLimit({
21✔
95
    windowMs: RATE_LIMIT_WINDOW_MS,
96
    max: RATE_LIMIT_MAX,
97
    standardHeaders: true,
98
    legacyHeaders: false,
99
    message: { error: "Too many requests, please try again later" },
100
  });
101

102
  // Apply rate limiting to sensitive endpoints
103
  app.use("/oauth", limiter);
21✔
104
  app.use("/mcp", limiter);
21✔
105

106
  // CORS middleware - allow OAuth endpoints, restrict MCP endpoint
107
  app.use((req, res, next) => {
21✔
108
    const origin = req.headers.origin;
4✔
109
    if (!origin) {
4✔
110
      next();
1✔
111
      return;
1✔
112
    }
113

114
    // Allow CORS for OAuth discovery and flow endpoints
115
    // mcpAuthRouter mounts at root: /authorize, /token, /register
116
    // Our custom callback is at /oauth/callback
117
    const isAllowed = isCorsAllowedPath(req.path);
3✔
118

119
    if (isAllowed) {
3✔
120
      res.setHeader("Access-Control-Allow-Origin", origin);
2✔
121
      res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
2✔
122
      res.setHeader(
2✔
123
        "Access-Control-Allow-Headers",
124
        "Content-Type, Authorization"
125
      );
126
      res.setHeader("Access-Control-Max-Age", "86400");
2✔
127

128
      // Handle preflight
129
      if (req.method === "OPTIONS") {
2✔
130
        res.status(204).end();
1✔
131
        return;
1✔
132
      }
133

134
      next();
1✔
135
      return;
1✔
136
    }
137

138
    // Block cross-origin requests to /mcp endpoint
139
    res.status(403).json({ error: "Cross-origin requests not allowed" });
1✔
140
    return;
1✔
141
  });
142

143
  // Cache-Control headers for OAuth and MCP endpoints
144
  const noCacheMiddleware: express.RequestHandler = (_req, res, next) => {
21✔
145
    res.setHeader(
1✔
146
      "Cache-Control",
147
      "no-store, no-cache, must-revalidate, private"
148
    );
149
    res.setHeader("Pragma", "no-cache");
1✔
150
    res.setHeader("Expires", "0");
1✔
151
    next();
1✔
152
  };
153

154
  // Apply no-cache to all OAuth endpoints (both root-level and /oauth/*)
155
  app.use("/oauth", noCacheMiddleware);
21✔
156
  app.use("/mcp", noCacheMiddleware);
21✔
157
  app.use("/authorize", noCacheMiddleware);
21✔
158
  app.use("/token", noCacheMiddleware);
21✔
159
  app.use("/register", noCacheMiddleware);
21✔
160
  app.use("/.well-known", noCacheMiddleware);
21✔
161

162
  // Parse JSON bodies
163
  app.use(express.json());
21✔
164

165
  // Mount OAuth auth router (AS metadata, authorize, token, register endpoints)
166
  app.use(
21✔
167
    mcpAuthRouter({
168
      provider,
169
      issuerUrl: new URL(issuerUrl),
170
      scopesSupported: ["read:user"],
171
      resourceName: "Quire MCP Server",
172
    })
173
  );
174

175
  // Handle OAuth callback from Quire
176
  app.get("/oauth/callback", async (req, res) => {
21✔
177
    const { code, state, error, error_description } = req.query;
6✔
178

179
    // Handle error from Quire
180
    if (error) {
6✔
181
      const errorMsg = typeof error === "string" ? error : "Unknown error";
3!
182
      const errorDesc =
183
        typeof error_description === "string" ? error_description : errorMsg;
3✔
184
      console.error(`[quire-mcp] Quire OAuth error: ${errorMsg}`);
3✔
185
      res.status(400).send(`
3✔
186
        <!DOCTYPE html>
187
        <html>
188
        <head><title>Authorization Failed</title></head>
189
        <body>
190
          <h1>Authorization Failed</h1>
191
          <p>${escapeHtml(errorDesc)}</p>
192
          <p>You can close this window.</p>
193
        </body>
194
        </html>
195
      `);
196
      return;
3✔
197
    }
198

199
    // Validate params
200
    if (typeof code !== "string" || typeof state !== "string") {
3✔
201
      res.status(400).send(`
1✔
202
        <!DOCTYPE html>
203
        <html>
204
        <head><title>Invalid Request</title></head>
205
        <body>
206
          <h1>Invalid Request</h1>
207
          <p>Missing code or state parameter.</p>
208
        </body>
209
        </html>
210
      `);
211
      return;
1✔
212
    }
213

214
    // Process callback
215
    const result = await handleQuireOAuthCallback(
2✔
216
      config,
217
      getServerTokenStore(),
218
      code,
219
      state
220
    );
221

222
    if ("error" in result) {
2✔
223
      res.status(400).send(`
1✔
224
        <!DOCTYPE html>
225
        <html>
226
        <head><title>Authorization Failed</title></head>
227
        <body>
228
          <h1>Authorization Failed</h1>
229
          <p>${escapeHtml(result.errorDescription)}</p>
230
          <p>You can close this window.</p>
231
        </body>
232
        </html>
233
      `);
234
      return;
1✔
235
    }
236

237
    res.redirect(result.redirectUrl);
1✔
238
  });
239

240
  // Create session-to-transport map for stateful connections with activity tracking
241
  // Uses LRU cache to prevent memory exhaustion from spam attacks
242
  const sessions = new LRUCache<SessionInfo>({
21✔
243
    maxSize: MAX_SESSIONS,
244
    onEvict: (sessionId, session) => {
245
      console.error(
×
246
        `[quire-mcp] Evicting session ${sessionId.slice(0, SESSION_ID_DISPLAY_LENGTH)} (max sessions reached)`
247
      );
248
      try {
×
249
        session.transport.close().catch((err: unknown) => {
×
250
          console.error(
×
251
            `[quire-mcp] Error closing evicted session:`,
252
            err instanceof Error ? err.message : err
×
253
          );
254
        });
255
      } catch {
256
        // Ignore close errors on eviction
257
      }
258
    },
259
  });
260

261
  // Get resource metadata URL for WWW-Authenticate header
262
  const resourceMetadataUrl = `${issuerUrl}/.well-known/oauth-protected-resource`;
21✔
263

264
  // Helper to update session activity
265
  const touchSession = (sessionId: string): void => {
21✔
266
    const session = sessions.get(sessionId);
×
267
    if (session) {
×
268
      session.lastActivity = Date.now();
×
269
      // Re-set to update LRU order
270
      sessions.set(sessionId, session);
×
271
    }
272
  };
273

274
  // Helper to clean up a session
275
  const cleanupSession = (sessionId: string): void => {
21✔
276
    const session = sessions.get(sessionId);
×
277
    if (session) {
×
278
      try {
×
279
        session.transport.close().catch((err: unknown) => {
×
280
          console.error(
×
281
            `[quire-mcp] Error closing session ${sessionId.slice(0, SESSION_ID_DISPLAY_LENGTH)}:`,
282
            err instanceof Error ? err.message : err
×
283
          );
284
        });
285
      } catch (err) {
286
        console.error(
×
287
          `[quire-mcp] Error closing session ${sessionId.slice(0, SESSION_ID_DISPLAY_LENGTH)}:`,
288
          err instanceof Error ? err.message : err
×
289
        );
290
      }
291
      sessions.delete(sessionId);
×
292
    }
293
  };
294

295
  // MCP POST handler
296
  const mcpPostHandler: express.RequestHandler = async (req, res) => {
21✔
297
    const sessionId = req.headers["mcp-session-id"] as string | undefined;
2✔
298

299
    try {
2✔
300
      let transport: StreamableHTTPServerTransport;
301

302
      const existingSession = sessionId ? sessions.get(sessionId) : undefined;
2!
303
      if (sessionId && existingSession) {
2!
304
        // Reuse existing transport and update activity
305
        transport = existingSession.transport;
×
306
        touchSession(sessionId);
×
307
      } else if (!sessionId && isInitializeRequest(req.body)) {
2✔
308
        // New initialization request
309
        transport = new StreamableHTTPServerTransport({
1✔
310
          sessionIdGenerator: () => randomUUID(),
×
311
          onsessioninitialized: (sid) => {
312
            sessions.set(sid, {
×
313
              transport,
314
              lastActivity: Date.now(),
315
            });
316
          },
317
        });
318

319
        // Clean up on close
320
        transport.onclose = () => {
1✔
321
          const sid = transport.sessionId;
×
322
          if (sid && sessions.has(sid)) {
×
323
            sessions.delete(sid);
×
324
          }
325
        };
326

327
        // Connect transport to a new server instance
328
        // Cast required due to exactOptionalPropertyTypes incompatibility with SDK
329
        const server = getServer();
1✔
330
        await server.connect(transport as unknown as Transport);
1✔
331
        await transport.handleRequest(req, res, req.body);
1✔
332
        return;
×
333
      } else {
334
        // Invalid request
335
        res.status(400).json({
1✔
336
          jsonrpc: "2.0",
337
          error: {
338
            code: JSONRPC_ERROR_INVALID_REQUEST,
339
            message: "Bad Request: No valid session ID provided",
340
          },
341
          id: null,
342
        });
343
        return;
1✔
344
      }
345

346
      await transport.handleRequest(req, res, req.body);
×
347
    } catch (error) {
348
      console.error("[quire-mcp] Error handling MCP request:", error);
1✔
349
      if (!res.headersSent) {
1!
350
        res.status(500).json({
1✔
351
          jsonrpc: "2.0",
352
          error: {
353
            code: JSONRPC_ERROR_INTERNAL,
354
            message: "Internal server error",
355
          },
356
          id: null,
357
        });
358
      }
359
    }
360
  };
361

362
  // MCP GET handler (SSE streams)
363
  const mcpGetHandler: express.RequestHandler = async (req, res) => {
21✔
364
    const sessionId = req.headers["mcp-session-id"] as string | undefined;
2✔
365

366
    const session = sessionId ? sessions.get(sessionId) : undefined;
2✔
367
    if (!sessionId || !session) {
2!
368
      res.status(400).json({
2✔
369
        jsonrpc: "2.0",
370
        error: {
371
          code: JSONRPC_ERROR_INVALID_REQUEST,
372
          message: "Invalid or missing session ID",
373
        },
374
        id: null,
375
      });
376
      return;
2✔
377
    }
378

379
    touchSession(sessionId);
×
380
    await session.transport.handleRequest(req, res);
×
381
  };
382

383
  // MCP DELETE handler (session termination)
384
  const mcpDeleteHandler: express.RequestHandler = async (req, res) => {
21✔
385
    const sessionId = req.headers["mcp-session-id"] as string | undefined;
1✔
386
    const session = sessionId ? sessions.get(sessionId) : undefined;
1!
387

388
    if (!sessionId || !session) {
1!
389
      res.status(400).json({
1✔
390
        jsonrpc: "2.0",
391
        error: {
392
          code: JSONRPC_ERROR_INVALID_REQUEST,
393
          message: "Invalid or missing session ID",
394
        },
395
        id: null,
396
      });
397
      return;
1✔
398
    }
399

400
    try {
×
401
      await session.transport.handleRequest(req, res);
×
402
    } catch (error) {
403
      console.error("[quire-mcp] Error handling session termination:", error);
×
404
      if (!res.headersSent) {
×
405
        res.status(500).json({
×
406
          jsonrpc: "2.0",
407
          error: {
408
            code: JSONRPC_ERROR_INTERNAL,
409
            message: "Error processing session termination",
410
          },
411
          id: null,
412
        });
413
      }
414
    }
415
  };
416

417
  // Set up routes with Bearer auth middleware
418
  const authMiddleware = requireBearerAuth({
21✔
419
    verifier: provider,
420
    resourceMetadataUrl,
421
  });
422

423
  app.post("/mcp", authMiddleware, mcpPostHandler);
21✔
424
  app.get("/mcp", authMiddleware, mcpGetHandler);
21✔
425
  app.delete("/mcp", authMiddleware, mcpDeleteHandler);
21✔
426

427
  // Start automatic token cleanup interval
428
  const tokenStore = getServerTokenStore();
21✔
429
  const cleanupInterval = setInterval(() => {
21✔
430
    tokenStore.cleanup();
×
431

432
    // Also clean up idle sessions
433
    const now = Date.now();
×
434
    for (const [sessionId, session] of sessions.entries()) {
×
435
      if (now - session.lastActivity > SESSION_IDLE_TIMEOUT_MS) {
×
436
        console.error(
×
437
          `[quire-mcp] Closing idle session ${sessionId.slice(0, SESSION_ID_DISPLAY_LENGTH)}...`
438
        );
439
        cleanupSession(sessionId);
×
440
      }
441
    }
442
  }, TOKEN_CLEANUP_INTERVAL_MS);
443

444
  // Don't keep the process alive just for cleanup
445
  cleanupInterval.unref();
21✔
446

447
  // Start listening
448
  await new Promise<void>((resolve, reject) => {
21✔
449
    const server = app.listen(port, host, () => {
21✔
450
      console.error(`[quire-mcp] HTTP server listening on ${host}:${port}`);
21✔
451
      console.error(
21✔
452
        `[quire-mcp] OAuth metadata: ${issuerUrl}/.well-known/oauth-authorization-server`
453
      );
454
      console.error(`[quire-mcp] MCP endpoint: ${issuerUrl}/mcp`);
21✔
455
      resolve();
21✔
456
    });
457

458
    server.on("error", reject);
21✔
459

460
    // Graceful shutdown handler
461
    const shutdown = (signal: string): void => {
21✔
462
      console.error(
×
463
        `[quire-mcp] Received ${signal}, shutting down gracefully...`
464
      );
465

466
      // Stop accepting new connections
467
      server.close(() => {
×
468
        console.error("[quire-mcp] HTTP server closed");
×
469
      });
470

471
      // Clear the cleanup interval
472
      clearInterval(cleanupInterval);
×
473

474
      // Close all active sessions
475
      console.error(
×
476
        `[quire-mcp] Closing ${sessions.size} active session(s)...`
477
      );
478
      for (const [sessionId, session] of sessions.entries()) {
×
479
        try {
×
480
          session.transport.close().catch((err: unknown) => {
×
481
            console.error(
×
482
              `[quire-mcp] Error closing session ${sessionId.slice(0, SESSION_ID_DISPLAY_LENGTH)}:`,
483
              err instanceof Error ? err.message : err
×
484
            );
485
          });
486
        } catch {
487
          // Ignore close errors during shutdown
488
        }
489
      }
490
      sessions.clear();
×
491

492
      // Give connections time to close gracefully, then force exit
493
      setTimeout(() => {
×
494
        console.error("[quire-mcp] Shutdown complete");
×
495
        process.exit(0);
×
496
      }, 5000);
497
    };
498

499
    // Handle termination signals
500
    process.on("SIGINT", () => {
21✔
501
      shutdown("SIGINT");
×
502
    });
503
    process.on("SIGTERM", () => {
21✔
504
      shutdown("SIGTERM");
×
505
    });
506
  });
507
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc