@stigmer/mcp-server 3.12.1 → 3.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/mcp-server-stigmer.js +5 -3
- package/domains/agentexecutions/fetch.d.ts.map +1 -1
- package/domains/agentexecutions/fetch.js +3 -4
- package/domains/agentexecutions/fetch.js.map +1 -1
- package/gen/mcpserver.js +1 -1
- package/gen/mcpserver.js.map +1 -1
- package/gen/workflow.js +6 -6
- package/gen/workflow.js.map +1 -1
- package/package.json +3 -3
- package/readiness.d.ts +27 -0
- package/readiness.d.ts.map +1 -0
- package/readiness.js +81 -0
- package/readiness.js.map +1 -0
- package/server.d.ts.map +1 -1
- package/server.js +51 -17
- package/server.js.map +1 -1
- package/src/domains/agentexecutions/fetch.ts +3 -4
- package/src/gen/mcpserver.ts +1 -1
- package/src/gen/workflow.ts +6 -6
- package/src/http.integration.test.ts +46 -6
- package/src/readiness.test.ts +90 -0
- package/src/readiness.ts +94 -0
- package/src/server.ts +58 -15
package/src/server.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
|
19
19
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
20
20
|
|
|
21
21
|
import type { Config } from "./config.js";
|
|
22
|
+
import { createReadinessCheck, type ReadinessResult } from "./readiness.js";
|
|
22
23
|
import { registerAgentExecutionTools } from "./domains/agentexecutions/tools.js";
|
|
23
24
|
import { registerAgentResources } from "./domains/agents/resources.js";
|
|
24
25
|
import { registerAgentTools } from "./domains/agents/tools.js";
|
|
@@ -244,10 +245,11 @@ export async function serveHttp(
|
|
|
244
245
|
signal: AbortSignal,
|
|
245
246
|
): Promise<void> {
|
|
246
247
|
const sessions = new Map<string, StreamableHTTPServerTransport>();
|
|
248
|
+
const checkReady = createReadinessCheck(cfg.stigmerServerAddress);
|
|
247
249
|
|
|
248
250
|
const httpServer = createHttpServer((req, res) => {
|
|
249
251
|
logAccess(req, res);
|
|
250
|
-
void routeRequest(req, res, sessions, makeServer, cfg);
|
|
252
|
+
void routeRequest(req, res, sessions, makeServer, cfg, checkReady);
|
|
251
253
|
});
|
|
252
254
|
|
|
253
255
|
const addr = `:${cfg.httpPort}`;
|
|
@@ -301,13 +303,17 @@ export async function serveBoth(target: BackendTarget, cfg: Config, signal: Abor
|
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
/**
|
|
304
|
-
* Route an inbound HTTP request: liveness
|
|
305
|
-
* extraction, then delegation to the session's MCP transport (reusing an
|
|
306
|
+
* Route an inbound HTTP request: liveness/readiness probes, the non-validating
|
|
307
|
+
* Bearer extraction, then delegation to the session's MCP transport (reusing an
|
|
306
308
|
* existing session or creating one for an `initialize` request).
|
|
307
309
|
*
|
|
308
310
|
* The token is never validated here — presence is the only check, and it is
|
|
309
311
|
* forwarded unchanged to stigmer-server which performs validation. This mirrors
|
|
310
312
|
* the Go authMiddleware exactly (inventory §4.2).
|
|
313
|
+
*
|
|
314
|
+
* Every refusal is a JSON-RPC-framed error (the SDK transport's own
|
|
315
|
+
* convention — see jsonRpcError), never text/plain: strict MCP clients parse
|
|
316
|
+
* the body, and the spec keys session recovery on a recognizable 404.
|
|
311
317
|
*/
|
|
312
318
|
async function routeRequest(
|
|
313
319
|
req: IncomingMessage & { auth?: AuthInfo },
|
|
@@ -315,6 +321,7 @@ async function routeRequest(
|
|
|
315
321
|
sessions: Map<string, StreamableHTTPServerTransport>,
|
|
316
322
|
makeServer: RouteServerFactory,
|
|
317
323
|
cfg: Config,
|
|
324
|
+
checkReady: () => Promise<ReadinessResult>,
|
|
318
325
|
): Promise<void> {
|
|
319
326
|
if (req.method === "GET" && req.url === "/health") {
|
|
320
327
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -322,6 +329,21 @@ async function routeRequest(
|
|
|
322
329
|
return;
|
|
323
330
|
}
|
|
324
331
|
|
|
332
|
+
// Readiness = liveness AND a working backend hop (see readiness.ts for why
|
|
333
|
+
// only the readiness probe may point here). Public like /health: Kubernetes
|
|
334
|
+
// probes carry no bearer.
|
|
335
|
+
if (req.method === "GET" && requestPath(req) === "/ready") {
|
|
336
|
+
const result = await checkReady();
|
|
337
|
+
if (result.ready) {
|
|
338
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
339
|
+
res.end(`{"status":"ready"}\n`);
|
|
340
|
+
} else {
|
|
341
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
342
|
+
res.end(JSON.stringify({ status: "unready", reason: result.reason }) + "\n");
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
325
347
|
// RFC 9728 Protected Resource Metadata — public, unauthenticated, and served
|
|
326
348
|
// only when OAuth discovery is enabled. CORS-open so browser-based clients
|
|
327
349
|
// (e.g. Claude Desktop's connector GUI) can discover the authorization server.
|
|
@@ -335,11 +357,11 @@ async function routeRequest(
|
|
|
335
357
|
if (cfg.httpAuthEnabled) {
|
|
336
358
|
const token = extractBearerToken(req);
|
|
337
359
|
if (token === "") {
|
|
338
|
-
const headers: Record<string, string> = { "Content-Type": "text/plain" };
|
|
339
360
|
// RFC 9728 §5.1: point OAuth-capable clients at the metadata document.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
361
|
+
const challenge = cfg.oauth.enabled
|
|
362
|
+
? { "WWW-Authenticate": bearerChallenge(cfg) }
|
|
363
|
+
: undefined;
|
|
364
|
+
jsonRpcError(res, 401, -32000, "missing or malformed Authorization: Bearer header", challenge);
|
|
343
365
|
return;
|
|
344
366
|
}
|
|
345
367
|
req.auth = { token, clientId: "stigmer-mcp-passthrough", scopes: [] };
|
|
@@ -352,8 +374,12 @@ async function routeRequest(
|
|
|
352
374
|
if (sessionId !== undefined) {
|
|
353
375
|
const transport = sessions.get(sessionId);
|
|
354
376
|
if (transport === undefined) {
|
|
355
|
-
|
|
356
|
-
|
|
377
|
+
// 404 + code -32001 is the SDK transport's session-not-found shape, and
|
|
378
|
+
// the streamable-HTTP spec's recovery signal: on it, a client MUST open
|
|
379
|
+
// a new session with a fresh InitializeRequest. Losing sessions on pod
|
|
380
|
+
// restart is expected here — they are in-memory by design (single
|
|
381
|
+
// replica; see the deployment overlay).
|
|
382
|
+
jsonRpcError(res, 404, -32001, "Session not found: unknown or expired MCP session");
|
|
357
383
|
return;
|
|
358
384
|
}
|
|
359
385
|
await transport.handleRequest(req, res);
|
|
@@ -362,15 +388,13 @@ async function routeRequest(
|
|
|
362
388
|
|
|
363
389
|
// No session → only an initialize POST may open one.
|
|
364
390
|
if (req.method !== "POST") {
|
|
365
|
-
res
|
|
366
|
-
res.end("missing Mcp-Session-Id header");
|
|
391
|
+
jsonRpcError(res, 400, -32000, "Bad Request: Mcp-Session-Id header is required");
|
|
367
392
|
return;
|
|
368
393
|
}
|
|
369
394
|
|
|
370
395
|
const body = await readJsonBody(req);
|
|
371
396
|
if (!isInitializeRequest(body)) {
|
|
372
|
-
res
|
|
373
|
-
res.end("Bad Request: an initialize request is required to open a session");
|
|
397
|
+
jsonRpcError(res, 400, -32000, "Bad Request: an initialize request is required to open a session");
|
|
374
398
|
return;
|
|
375
399
|
}
|
|
376
400
|
|
|
@@ -379,8 +403,7 @@ async function routeRequest(
|
|
|
379
403
|
// routedServerFactory for the incident this prevents.
|
|
380
404
|
const server = makeServer(path);
|
|
381
405
|
if (server === undefined) {
|
|
382
|
-
res
|
|
383
|
-
res.end(`unknown MCP route: ${path}`);
|
|
406
|
+
jsonRpcError(res, 404, -32000, `unknown MCP route: ${path}`);
|
|
384
407
|
return;
|
|
385
408
|
}
|
|
386
409
|
|
|
@@ -411,6 +434,26 @@ export function stdioServer(target: BackendTarget, cfg: Config): McpServer {
|
|
|
411
434
|
return createServer(target);
|
|
412
435
|
}
|
|
413
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Write a JSON-RPC-framed refusal, byte-compatible with the SDK transport's
|
|
439
|
+
* own createJsonErrorResponse: `{"jsonrpc":"2.0","error":{code,message},"id":null}`
|
|
440
|
+
* with Content-Type application/json. The HTTP status stays authoritative (the
|
|
441
|
+
* streamable-HTTP spec keys on it); the body exists for strict MCP clients
|
|
442
|
+
* that parse refusals instead of surfacing an opaque content-type error
|
|
443
|
+
* (stigmer/stigmer#316). Code -32001 is reserved for session-not-found; the
|
|
444
|
+
* transport-level family uses -32000, both per the SDK's convention.
|
|
445
|
+
*/
|
|
446
|
+
function jsonRpcError(
|
|
447
|
+
res: ServerResponse,
|
|
448
|
+
status: number,
|
|
449
|
+
code: number,
|
|
450
|
+
message: string,
|
|
451
|
+
headers?: Record<string, string>,
|
|
452
|
+
): void {
|
|
453
|
+
res.writeHead(status, { "Content-Type": "application/json", ...headers });
|
|
454
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code, message }, id: null }));
|
|
455
|
+
}
|
|
456
|
+
|
|
414
457
|
/** Return a single header value, collapsing the array form Node may produce. */
|
|
415
458
|
function headerValue(req: IncomingMessage, name: string): string | undefined {
|
|
416
459
|
const v = req.headers[name];
|