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

jacob-hartmann / quire-mcp / 21649823045

03 Feb 2026 10:11PM UTC coverage: 86.299% (-4.6%) from 90.943%
21649823045

push

github

jacob-hartmann
feat(tests): enhance property-based tests and update QuireClient methods

- Refactored property-based tests in `property.test.ts` to improve string matching logic and ensure accurate character counting for HTML escaping.
- Updated `QuireClient` methods to change parameter names from `keyword` to `text` for clarity in task search functionalities.
- Enhanced test utilities and documentation for better readability and maintainability.
- Adjusted various test cases to reflect changes in method signatures and expected behaviors.

866 of 1015 branches covered (85.32%)

Branch coverage included in aggregate %.

14 of 15 new or added lines in 3 files covered. (93.33%)

68 existing lines in 8 files now uncovered.

1685 of 1941 relevant lines covered (86.81%)

30.7 hits per line

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

62.14
/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
      // Use restrictive CSP appropriate for JSON-RPC API with minimal HTML responses
87
      contentSecurityPolicy: {
88
        directives: {
89
          defaultSrc: ["'self'"],
90
          scriptSrc: ["'none'"],
91
          objectSrc: ["'none'"],
92
          frameAncestors: ["'none'"],
93
        },
94
      },
95
      // Disable COEP for SSE streaming support in MCP protocol
96
      crossOriginEmbedderPolicy: false,
97
    })
98
  );
99

100
  // Rate limiting for OAuth and MCP endpoints
101
  const limiter = rateLimit({
21✔
102
    windowMs: RATE_LIMIT_WINDOW_MS,
103
    max: RATE_LIMIT_MAX,
104
    standardHeaders: true,
105
    legacyHeaders: false,
106
    message: { error: "Too many requests, please try again later" },
107
  });
108

109
  // Apply rate limiting to sensitive endpoints
110
  app.use("/oauth", limiter);
21✔
111
  app.use("/mcp", limiter);
21✔
112

113
  // CORS middleware - allow OAuth endpoints, restrict MCP endpoint
114
  app.use((req, res, next) => {
21✔
115
    const origin = req.headers.origin;
4✔
116
    if (!origin) {
4✔
117
      next();
1✔
118
      return;
1✔
119
    }
120

121
    // Allow CORS for OAuth discovery and flow endpoints
122
    // mcpAuthRouter mounts at root: /authorize, /token, /register
123
    // Our custom callback is at /oauth/callback
124
    const isAllowed = isCorsAllowedPath(req.path);
3✔
125

126
    if (isAllowed) {
3✔
127
      res.setHeader("Access-Control-Allow-Origin", origin);
2✔
128
      res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
2✔
129
      res.setHeader(
2✔
130
        "Access-Control-Allow-Headers",
131
        "Content-Type, Authorization"
132
      );
133
      res.setHeader("Access-Control-Max-Age", "86400");
2✔
134

135
      // Handle preflight
136
      if (req.method === "OPTIONS") {
2✔
137
        res.status(204).end();
1✔
138
        return;
1✔
139
      }
140

141
      next();
1✔
142
      return;
1✔
143
    }
144

145
    // Block cross-origin requests to /mcp endpoint
146
    res.status(403).json({ error: "Cross-origin requests not allowed" });
1✔
147
    return;
1✔
148
  });
149

150
  // Cache-Control headers for OAuth and MCP endpoints
151
  const noCacheMiddleware: express.RequestHandler = (_req, res, next) => {
21✔
152
    res.setHeader(
1✔
153
      "Cache-Control",
154
      "no-store, no-cache, must-revalidate, private"
155
    );
156
    res.setHeader("Pragma", "no-cache");
1✔
157
    res.setHeader("Expires", "0");
1✔
158
    next();
1✔
159
  };
160

161
  // Apply no-cache to all OAuth endpoints (both root-level and /oauth/*)
162
  app.use("/oauth", noCacheMiddleware);
21✔
163
  app.use("/mcp", noCacheMiddleware);
21✔
164
  app.use("/authorize", noCacheMiddleware);
21✔
165
  app.use("/token", noCacheMiddleware);
21✔
166
  app.use("/register", noCacheMiddleware);
21✔
167
  app.use("/.well-known", noCacheMiddleware);
21✔
168

169
  // Parse JSON bodies
170
  app.use(express.json());
21✔
171

172
  // Mount OAuth auth router (AS metadata, authorize, token, register endpoints)
173
  app.use(
21✔
174
    mcpAuthRouter({
175
      provider,
176
      issuerUrl: new URL(issuerUrl),
177
      scopesSupported: ["read:user"],
178
      resourceName: "Quire MCP Server",
179
    })
180
  );
181

182
  // Handle OAuth callback from Quire
183
  app.get("/oauth/callback", async (req, res) => {
21✔
184
    const { code, state, error, error_description } = req.query;
6✔
185

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

206
    // Validate params
207
    if (typeof code !== "string" || typeof state !== "string") {
3✔
208
      res.status(400).send(`
1✔
209
        <!DOCTYPE html>
210
        <html>
211
        <head><title>Invalid Request</title></head>
212
        <body>
213
          <h1>Invalid Request</h1>
214
          <p>Missing code or state parameter.</p>
215
        </body>
216
        </html>
217
      `);
218
      return;
1✔
219
    }
220

221
    // Process callback
222
    const result = await handleQuireOAuthCallback(
2✔
223
      config,
224
      getServerTokenStore(),
225
      code,
226
      state
227
    );
228

229
    if ("error" in result) {
2✔
230
      res.status(400).send(`
1✔
231
        <!DOCTYPE html>
232
        <html>
233
        <head><title>Authorization Failed</title></head>
234
        <body>
235
          <h1>Authorization Failed</h1>
236
          <p>${escapeHtml(result.errorDescription)}</p>
237
          <p>You can close this window.</p>
238
        </body>
239
        </html>
240
      `);
241
      return;
1✔
242
    }
243

244
    res.redirect(result.redirectUrl);
1✔
245
  });
246

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

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

271
  // Helper to update session activity
272
  const touchSession = (sessionId: string): void => {
21✔
273
    const session = sessions.get(sessionId);
×
274
    if (session) {
×
275
      session.lastActivity = Date.now();
×
276
      // Re-set to update LRU order
277
      sessions.set(sessionId, session);
×
278
    }
279
  };
280

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

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

306
    try {
2✔
307
      let transport: StreamableHTTPServerTransport;
308

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

326
        // Clean up on close
327
        transport.onclose = () => {
1✔
328
          const sid = transport.sessionId;
×
329
          if (sid && sessions.has(sid)) {
×
330
            sessions.delete(sid);
×
331
          }
332
        };
333

334
        // Connect transport to a new server instance
335
        // Cast required due to exactOptionalPropertyTypes incompatibility with SDK
336
        const server = getServer();
1✔
337
        await server.connect(transport as unknown as Transport);
1✔
338
        await transport.handleRequest(req, res, req.body);
1✔
339
        return;
×
340
      } else if (sessionId && !existingSession) {
1!
341
        // Client presented a session ID we don't recognize (expired/evicted/restarted).
342
        // Per MCP Streamable HTTP spec, respond 404 so the client re-initializes.
UNCOV
343
        res.status(404).json({
×
344
          jsonrpc: "2.0",
345
          error: {
346
            code: JSONRPC_ERROR_INVALID_REQUEST,
347
            message: "Session not found",
348
          },
349
          id: null,
350
        });
UNCOV
351
        return;
×
352
      } else {
353
        // Invalid request
354
        res.status(400).json({
1✔
355
          jsonrpc: "2.0",
356
          error: {
357
            code: JSONRPC_ERROR_INVALID_REQUEST,
358
            message: "Bad Request: No valid session ID provided",
359
          },
360
          id: null,
361
        });
362
        return;
1✔
363
      }
364

UNCOV
365
      await transport.handleRequest(req, res, req.body);
×
366
    } catch (error) {
367
      console.error("[quire-mcp] Error handling MCP request:", error);
1✔
368
      if (!res.headersSent) {
1!
369
        res.status(500).json({
1✔
370
          jsonrpc: "2.0",
371
          error: {
372
            code: JSONRPC_ERROR_INTERNAL,
373
            message: "Internal server error",
374
          },
375
          id: null,
376
        });
377
      }
378
    }
379
  };
380

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

385
    const session = sessionId ? sessions.get(sessionId) : undefined;
2✔
386
    if (!sessionId || !session) {
2!
387
      res.status(sessionId ? 404 : 400).json({
2✔
388
        jsonrpc: "2.0",
389
        error: {
390
          code: JSONRPC_ERROR_INVALID_REQUEST,
391
          message: sessionId ? "Session not found" : "Missing session ID",
2✔
392
        },
393
        id: null,
394
      });
395
      return;
2✔
396
    }
397

UNCOV
398
    touchSession(sessionId);
×
UNCOV
399
    await session.transport.handleRequest(req, res);
×
400
  };
401

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

407
    if (!sessionId || !session) {
1!
408
      res.status(sessionId ? 404 : 400).json({
1!
409
        jsonrpc: "2.0",
410
        error: {
411
          code: JSONRPC_ERROR_INVALID_REQUEST,
412
          message: sessionId ? "Session not found" : "Missing session ID",
1!
413
        },
414
        id: null,
415
      });
416
      return;
1✔
417
    }
418

UNCOV
419
    try {
×
UNCOV
420
      await session.transport.handleRequest(req, res);
×
421
    } catch (error) {
UNCOV
422
      console.error("[quire-mcp] Error handling session termination:", error);
×
UNCOV
423
      if (!res.headersSent) {
×
UNCOV
424
        res.status(500).json({
×
425
          jsonrpc: "2.0",
426
          error: {
427
            code: JSONRPC_ERROR_INTERNAL,
428
            message: "Error processing session termination",
429
          },
430
          id: null,
431
        });
432
      }
433
    }
434
  };
435

436
  // Set up routes with Bearer auth middleware
437
  const authMiddleware = requireBearerAuth({
21✔
438
    verifier: provider,
439
    resourceMetadataUrl,
440
  });
441

442
  app.post("/mcp", authMiddleware, mcpPostHandler);
21✔
443
  app.get("/mcp", authMiddleware, mcpGetHandler);
21✔
444
  app.delete("/mcp", authMiddleware, mcpDeleteHandler);
21✔
445

446
  // Global error handler - must be registered after all routes
447
  // Express identifies error handlers by their 4-parameter signature
448
  app.use(
21✔
449
    (
450
      err: Error,
451
      _req: express.Request,
452
      res: express.Response,
453
      _next: express.NextFunction
454
    ) => {
UNCOV
455
      console.error("[quire-mcp] Unhandled error:", err);
×
456

457
      // Always return JSON, never HTML
UNCOV
458
      if (!res.headersSent) {
×
UNCOV
459
        res.status(500).json({
×
460
          jsonrpc: "2.0",
461
          error: {
462
            code: JSONRPC_ERROR_INTERNAL,
463
            message: "Internal server error",
464
          },
465
          id: null,
466
        });
467
      }
468
    }
469
  );
470

471
  // Start automatic token cleanup interval
472
  const tokenStore = getServerTokenStore();
21✔
473
  const cleanupInterval = setInterval(() => {
21✔
474
    tokenStore.cleanup();
×
475

476
    // Also clean up idle sessions
UNCOV
477
    const now = Date.now();
×
UNCOV
478
    for (const [sessionId, session] of sessions.entries()) {
×
479
      if (now - session.lastActivity > SESSION_IDLE_TIMEOUT_MS) {
×
UNCOV
480
        console.error(
×
481
          `[quire-mcp] Closing idle session ${sessionId.slice(0, SESSION_ID_DISPLAY_LENGTH)}...`
482
        );
UNCOV
483
        cleanupSession(sessionId);
×
484
      }
485
    }
486
  }, TOKEN_CLEANUP_INTERVAL_MS);
487

488
  // Don't keep the process alive just for cleanup
489
  cleanupInterval.unref();
21✔
490

491
  // Start listening
492
  await new Promise<void>((resolve, reject) => {
21✔
493
    const server = app.listen(port, host, () => {
21✔
494
      console.error(`[quire-mcp] HTTP server listening on ${host}:${port}`);
21✔
495
      console.error(
21✔
496
        `[quire-mcp] OAuth metadata: ${issuerUrl}/.well-known/oauth-authorization-server`
497
      );
498
      console.error(`[quire-mcp] MCP endpoint: ${issuerUrl}/mcp`);
21✔
499
      resolve();
21✔
500
    });
501

502
    server.on("error", reject);
21✔
503

504
    // Graceful shutdown handler
505
    const shutdown = (signal: string): void => {
21✔
UNCOV
506
      console.error(
×
507
        `[quire-mcp] Received ${signal}, shutting down gracefully...`
508
      );
509

510
      // Stop accepting new connections
511
      server.close(() => {
×
UNCOV
512
        console.error("[quire-mcp] HTTP server closed");
×
513
      });
514

515
      // Clear the cleanup interval
UNCOV
516
      clearInterval(cleanupInterval);
×
517

518
      // Close all active sessions
UNCOV
519
      console.error(
×
520
        `[quire-mcp] Closing ${sessions.size} active session(s)...`
521
      );
UNCOV
522
      for (const [sessionId, session] of sessions.entries()) {
×
UNCOV
523
        try {
×
UNCOV
524
          session.transport.close().catch((err: unknown) => {
×
UNCOV
525
            console.error(
×
526
              `[quire-mcp] Error closing session ${sessionId.slice(0, SESSION_ID_DISPLAY_LENGTH)}:`,
527
              err instanceof Error ? err.message : err
×
528
            );
529
          });
530
        } catch {
531
          // Ignore close errors during shutdown
532
        }
533
      }
UNCOV
534
      sessions.clear();
×
535

536
      // Give connections time to close gracefully, then force exit
UNCOV
537
      setTimeout(() => {
×
UNCOV
538
        console.error("[quire-mcp] Shutdown complete");
×
UNCOV
539
        process.exit(0);
×
540
      }, 5000);
541
    };
542

543
    // Handle termination signals
544
    process.on("SIGINT", () => {
21✔
UNCOV
545
      shutdown("SIGINT");
×
546
    });
547
    process.on("SIGTERM", () => {
21✔
UNCOV
548
      shutdown("SIGTERM");
×
549
    });
550
  });
551
}
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