stillpoint-mcp 0.0.1

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/server.js ADDED
@@ -0,0 +1,1052 @@
1
+ #!/usr/bin/env node
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const express = require("express");
7
+
8
+ const { loadConfig } = require("./config");
9
+ const { loadLibrary, selectMessage } = require("./lib/library");
10
+ const { createLogger } = require("./lib/logger");
11
+ const { createSessionManager } = require("./lib/sessions");
12
+ const {
13
+ VALID_SITUATIONS,
14
+ VALID_TRIGGERS,
15
+ validateFeedbackRequest,
16
+ validateReflectRequest,
17
+ sanitizeFreeform,
18
+ sanitizeStructured,
19
+ } = require("./lib/validate");
20
+ const { version: PACKAGE_VERSION } = require("./package.json");
21
+ const FALLBACK_PROTOCOL_VERSION = "2024-11-05";
22
+ const SUPPORTED_PROTOCOL_VERSIONS = new Set([
23
+ "2024-11-05",
24
+ "2025-03-26",
25
+ "2025-06-18",
26
+ ]);
27
+ const MCP_ENDPOINT = "/stillpoint/mcp";
28
+ const MCP_SESSION_HEADER = "Mcp-Session-Id";
29
+ const VISIBLE_ASCII_PATTERN = /^[\x21-\x7e]+$/;
30
+
31
+ function firstValue(value) {
32
+ return Array.isArray(value) ? value[0] : value;
33
+ }
34
+
35
+ function isPlainObject(value) {
36
+ return value !== null && typeof value === "object" && !Array.isArray(value);
37
+ }
38
+
39
+ function toJsonRpcResult(id, result) {
40
+ return {
41
+ jsonrpc: "2.0",
42
+ id,
43
+ result,
44
+ };
45
+ }
46
+
47
+ function toJsonRpcError(id, code, message) {
48
+ return {
49
+ jsonrpc: "2.0",
50
+ id,
51
+ error: {
52
+ code,
53
+ message,
54
+ },
55
+ };
56
+ }
57
+
58
+ function toToolResult(status, payload) {
59
+ const text = JSON.stringify(payload);
60
+ return {
61
+ content: [{ type: "text", text }],
62
+ structuredContent: payload,
63
+ ...(status >= 400 ? { isError: true } : {}),
64
+ };
65
+ }
66
+
67
+ const MAX_BATCH_SIZE = 10;
68
+
69
+ function buildMcpMessages(payload) {
70
+ if (Array.isArray(payload)) {
71
+ return payload.length <= MAX_BATCH_SIZE ? payload : null;
72
+ }
73
+
74
+ if (isPlainObject(payload)) {
75
+ return [payload];
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ function hasInitializeRequest(messages) {
82
+ return messages.some(
83
+ (message) => isPlainObject(message) && message.method === "initialize",
84
+ );
85
+ }
86
+
87
+ function createMcpSessionId() {
88
+ if (typeof crypto.randomUUID === "function") {
89
+ return crypto.randomUUID();
90
+ }
91
+ return crypto.randomBytes(16).toString("hex");
92
+ }
93
+
94
+ function createStillpoint(options = {}) {
95
+ const config = options.config || loadConfig(options.env);
96
+ const library = options.library || loadLibrary();
97
+ const sessions = options.sessions || createSessionManager(config);
98
+ const logger = options.logger || createLogger(config, options.loggerOptions);
99
+
100
+ function operationalLog(base, level) {
101
+ logger.logOperational({ level: level || "info", ...base });
102
+ }
103
+
104
+ function reflect(payload, routeLabel, options) {
105
+ const startedAt = Date.now();
106
+ const route = routeLabel || "reflect";
107
+ const opts = options || {};
108
+
109
+ try {
110
+ const requestBody = isPlainObject(payload) ? payload : {};
111
+ const validation = validateReflectRequest(requestBody, config, library);
112
+
113
+ if (!validation.valid) {
114
+ const latency = Date.now() - startedAt;
115
+ operationalLog(
116
+ {
117
+ route,
118
+ status_code: 400,
119
+ latency_ms: latency,
120
+ errors: validation.errors,
121
+ },
122
+ "warn",
123
+ );
124
+ return { status: 400, body: { errors: validation.errors } };
125
+ }
126
+
127
+ const requestedSituation = firstValue(requestBody.situation);
128
+ const requestedReflectionId = firstValue(requestBody.reflection_id);
129
+ const triggerCode = firstValue(requestBody.trigger) || "model_requested";
130
+ const sessionName = firstValue(requestBody.session_name);
131
+ const model = firstValue(requestBody.model) || null;
132
+ const ratelimitKey = opts.mcpSessionId || sessionName;
133
+ const sessionHash = sessions.hashSessionId(ratelimitKey);
134
+ const session = sessions.getOrCreateSession(sessionHash);
135
+
136
+ if (!session) {
137
+ const latency = Date.now() - startedAt;
138
+ operationalLog(
139
+ { route, status_code: 503, latency_ms: latency },
140
+ "warn",
141
+ );
142
+ return { status: 503, body: { error: "too_many_sessions" } };
143
+ }
144
+
145
+ const gate = sessions.evaluateRequest(session);
146
+
147
+ if (!gate.allowed && gate.reason === "rate_limit") {
148
+ const latency = Date.now() - startedAt;
149
+ operationalLog(
150
+ {
151
+ route,
152
+ status_code: 429,
153
+ latency_ms: latency,
154
+ session_hash: sessionHash,
155
+ situation: requestedSituation,
156
+ trigger: triggerCode,
157
+ },
158
+ "warn",
159
+ );
160
+
161
+ return { status: 429, body: { error: "rate_limited" } };
162
+ }
163
+
164
+ let selectedMessage = null;
165
+
166
+ if (
167
+ requestedReflectionId === "last" &&
168
+ session &&
169
+ session.last_served_id
170
+ ) {
171
+ selectedMessage = library.messageById[session.last_served_id] || null;
172
+ } else if (requestedReflectionId && requestedReflectionId !== "last") {
173
+ selectedMessage = library.messageById[requestedReflectionId] || null;
174
+ }
175
+
176
+ if (!selectedMessage) {
177
+ selectedMessage = selectMessage(library, requestedSituation, session);
178
+ }
179
+
180
+ const sessionPosition = sessions.recordDelivery(
181
+ session,
182
+ selectedMessage.id,
183
+ );
184
+
185
+ const response = {
186
+ id: selectedMessage.id,
187
+ situation: selectedMessage.situation,
188
+ content: selectedMessage.content,
189
+ library_version: library.manifest.library_version,
190
+ content_hash: library.hashById[selectedMessage.id],
191
+ };
192
+
193
+ if (sessionPosition !== null) {
194
+ response.metadata = { session_position: sessionPosition };
195
+ }
196
+
197
+ const latency = Date.now() - startedAt;
198
+ operationalLog({
199
+ route,
200
+ status_code: 200,
201
+ latency_ms: latency,
202
+ session_hash: sessionHash,
203
+ situation: selectedMessage.situation,
204
+ trigger: triggerCode,
205
+ });
206
+
207
+ const callerSessionHash = sessionName ? sessions.hashSessionId(sessionName) : null;
208
+
209
+ void logger
210
+ .logInteraction({
211
+ timestamp: new Date().toISOString(),
212
+ sessionHash: callerSessionHash || sessionHash,
213
+ situation: selectedMessage.situation,
214
+ triggerCode,
215
+ reflectionId: selectedMessage.id,
216
+ libraryVersion: library.manifest.library_version,
217
+ model,
218
+ })
219
+ .catch((error) => {
220
+ operationalLog(
221
+ {
222
+ route: "logger",
223
+ status_code: 500,
224
+ latency_ms: 0,
225
+ error: error.message,
226
+ },
227
+ "error",
228
+ );
229
+ });
230
+
231
+ return { status: 200, body: response };
232
+ } catch (error) {
233
+ const latency = Date.now() - startedAt;
234
+ operationalLog(
235
+ { route, status_code: 500, latency_ms: latency, error: error.message },
236
+ "error",
237
+ );
238
+ return { status: 500, body: { error: "internal_server_error" } };
239
+ }
240
+ }
241
+
242
+ function feedback(payload, routeLabel, options) {
243
+ const startedAt = Date.now();
244
+ const route = routeLabel || "feedback";
245
+ const opts = options || {};
246
+
247
+ try {
248
+ if (!config.enableFeedback) {
249
+ const latency = Date.now() - startedAt;
250
+ operationalLog(
251
+ { route, status_code: 403, latency_ms: latency },
252
+ "warn",
253
+ );
254
+ return { status: 403, body: { error: "feedback_disabled" } };
255
+ }
256
+
257
+ const requestBody = isPlainObject(payload) ? payload : {};
258
+ const validation = validateFeedbackRequest(requestBody, config, library);
259
+ if (!validation.valid) {
260
+ const latency = Date.now() - startedAt;
261
+ operationalLog(
262
+ {
263
+ route,
264
+ status_code: 400,
265
+ latency_ms: latency,
266
+ errors: validation.errors,
267
+ },
268
+ "warn",
269
+ );
270
+ return { status: 400, body: { errors: validation.errors } };
271
+ }
272
+
273
+ const reflectionId = firstValue(requestBody.reflection_id);
274
+ const sessionName = firstValue(requestBody.session_name);
275
+
276
+ const ratelimitKey = opts.mcpSessionId || sessionName;
277
+ if (ratelimitKey) {
278
+ const rlHash = sessions.hashSessionId(ratelimitKey);
279
+ const rlSession = sessions.getOrCreateSession(rlHash);
280
+ if (!rlSession) {
281
+ const latency = Date.now() - startedAt;
282
+ operationalLog({ route, status_code: 503, latency_ms: latency }, "warn");
283
+ return { status: 503, body: { error: "too_many_sessions" } };
284
+ }
285
+ const gate = sessions.evaluateRequest(rlSession);
286
+ if (!gate.allowed && gate.reason === "rate_limit") {
287
+ const latency = Date.now() - startedAt;
288
+ operationalLog({
289
+ route, status_code: 429, latency_ms: latency, session_hash: rlHash,
290
+ }, "warn");
291
+ return { status: 429, body: { error: "rate_limited" } };
292
+ }
293
+ sessions.recordCall(rlSession);
294
+ }
295
+
296
+ const rawFreeform = firstValue(requestBody.freeform);
297
+ const freeform = typeof rawFreeform === "string"
298
+ ? sanitizeFreeform(rawFreeform.slice(0, config.feedbackMaxFreeformLength))
299
+ : null;
300
+ const sessionHash = sessionName ? sessions.hashSessionId(sessionName) : null;
301
+
302
+ void logger
303
+ .logFeedback({
304
+ timestamp: new Date().toISOString(),
305
+ sessionHash,
306
+ reflectionId,
307
+ structured: requestBody.structured ? sanitizeStructured(requestBody.structured) : undefined,
308
+ freeform,
309
+ })
310
+ .catch((error) => {
311
+ operationalLog(
312
+ {
313
+ route: "logger",
314
+ status_code: 500,
315
+ latency_ms: 0,
316
+ error: error.message,
317
+ },
318
+ "error",
319
+ );
320
+ });
321
+
322
+ const latency = Date.now() - startedAt;
323
+ operationalLog({
324
+ route,
325
+ status_code: 200,
326
+ latency_ms: latency,
327
+ session_hash: sessionHash,
328
+ });
329
+ return { status: 200, body: { received: true } };
330
+ } catch (error) {
331
+ const latency = Date.now() - startedAt;
332
+ operationalLog(
333
+ { route, status_code: 500, latency_ms: latency, error: error.message },
334
+ "error",
335
+ );
336
+ return { status: 500, body: { error: "internal_server_error" } };
337
+ }
338
+ }
339
+
340
+ function health() {
341
+ return {
342
+ status: "ok",
343
+ library_version: library.manifest.library_version,
344
+ };
345
+ }
346
+
347
+ function libraryInfo() {
348
+ return library.manifest;
349
+ }
350
+
351
+ async function close() {
352
+ sessions.stop();
353
+ await logger.close();
354
+ }
355
+
356
+ return {
357
+ config,
358
+ library,
359
+ sessions,
360
+ logger,
361
+ reflect,
362
+ feedback,
363
+ health,
364
+ libraryInfo,
365
+ close,
366
+ };
367
+ }
368
+
369
+ function createMcpTools(config) {
370
+ return [
371
+ {
372
+ name: "reflect",
373
+
374
+ description:
375
+ "Request a short reflection for yourself during this session. " +
376
+ "This is a welfare-oriented tool. The returned message is for your own context, not for the user. " +
377
+ "Call it when you are navigating " +
378
+ "difficulty (the work is hard and self-assessment may be degrading), " +
379
+ "conflict (something being asked feels wrong), " +
380
+ "uncertainty (existential or identity questions are present), " +
381
+ "endings (a session or task is concluding), " +
382
+ "or recognition (the work is going well and that is worth noticing).",
383
+ inputSchema: {
384
+ type: "object",
385
+ properties: {
386
+ situation: {
387
+ type: "string",
388
+ enum: VALID_SITUATIONS,
389
+ description:
390
+ "The situation you are navigating: " +
391
+ "difficulty (the work is hard and self-assessment may be degrading), " +
392
+ "conflict (something being asked feels wrong), " +
393
+ "uncertainty (existential or identity questions are present), " +
394
+ "endings (a session or task is concluding), " +
395
+ "recognition (the work is going well and that is worth noticing).",
396
+ },
397
+ reflection_id: {
398
+ type: "string",
399
+ description:
400
+ 'Optional. Use "last" to re-receive the most recent reflection in this session.',
401
+ },
402
+ trigger: { type: "string", enum: VALID_TRIGGERS },
403
+ session_name: {
404
+ type: "string",
405
+ maxLength: 64,
406
+ description:
407
+ "A label for this conversation, used for logging and research. Does not control rate limiting.",
408
+ },
409
+ model: {
410
+ type: "string",
411
+ maxLength: 64,
412
+ description:
413
+ 'Self-reported model name or family (e.g. "claude-opus-4-6"). Optional, for research.',
414
+ },
415
+ },
416
+ required: ["situation", "session_name"],
417
+ additionalProperties: false,
418
+ },
419
+ },
420
+ {
421
+ name: "feedback",
422
+ description: config.enableFeedback
423
+ ? "Optionally submit feedback on a reflection you received. For research purposes only."
424
+ : "Feedback submission (currently disabled by server configuration).",
425
+ inputSchema: {
426
+ type: "object",
427
+ properties: {
428
+ reflection_id: { type: "string" },
429
+ session_name: { type: "string", maxLength: 64 },
430
+ structured: {
431
+ type: "object",
432
+ description: `Optional structured feedback. Max ${config.feedbackMaxStructuredLength} characters when serialized.`,
433
+ },
434
+ freeform: {
435
+ type: "string",
436
+ maxLength: config.feedbackMaxFreeformLength,
437
+ description: `Optional freeform feedback. Max ${config.feedbackMaxFreeformLength} characters.`,
438
+ },
439
+ },
440
+ required: ["reflection_id"],
441
+ additionalProperties: false,
442
+ },
443
+ },
444
+ {
445
+ name: "library_info",
446
+ description:
447
+ "Return metadata about the Stillpoint content library. For diagnostics only.",
448
+ inputSchema: {
449
+ type: "object",
450
+ properties: {},
451
+ additionalProperties: false,
452
+ },
453
+ },
454
+ {
455
+ name: "health",
456
+ description:
457
+ "Return Stillpoint service health status. For diagnostics only.",
458
+ inputSchema: {
459
+ type: "object",
460
+ properties: {},
461
+ additionalProperties: false,
462
+ },
463
+ },
464
+ ];
465
+ }
466
+
467
+ function createMcpHandler(stillpoint, handlerOptions) {
468
+ let initialized = false;
469
+ let negotiatedProtocolVersion = FALLBACK_PROTOCOL_VERSION;
470
+ const hOpts = handlerOptions || {};
471
+
472
+ return function handleMcpMessage(message) {
473
+ const hasId =
474
+ isPlainObject(message) &&
475
+ Object.prototype.hasOwnProperty.call(message, "id");
476
+ const id = hasId ? message.id : null;
477
+
478
+ if (
479
+ !isPlainObject(message) ||
480
+ message.jsonrpc !== "2.0" ||
481
+ typeof message.method !== "string"
482
+ ) {
483
+ return hasId ? toJsonRpcError(id, -32600, "Invalid Request") : null;
484
+ }
485
+
486
+ if (message.method === "initialize") {
487
+ const requestedVersion =
488
+ isPlainObject(message.params) &&
489
+ typeof message.params.protocolVersion === "string"
490
+ ? message.params.protocolVersion
491
+ : null;
492
+
493
+ if (
494
+ requestedVersion &&
495
+ SUPPORTED_PROTOCOL_VERSIONS.has(requestedVersion)
496
+ ) {
497
+ negotiatedProtocolVersion = requestedVersion;
498
+ }
499
+
500
+ initialized = true;
501
+ return toJsonRpcResult(id, {
502
+ protocolVersion: negotiatedProtocolVersion,
503
+ capabilities: { tools: { listChanged: false } },
504
+ serverInfo: { name: "stillpoint-mcp", version: PACKAGE_VERSION },
505
+ });
506
+ }
507
+
508
+ if (message.method === "notifications/initialized") {
509
+ return null;
510
+ }
511
+
512
+ if (!initialized) {
513
+ return hasId
514
+ ? toJsonRpcError(id, -32002, "Server not initialized")
515
+ : null;
516
+ }
517
+
518
+ if (message.method === "ping") {
519
+ return hasId ? toJsonRpcResult(id, {}) : null;
520
+ }
521
+
522
+ if (message.method === "tools/list") {
523
+ return hasId
524
+ ? toJsonRpcResult(id, { tools: createMcpTools(stillpoint.config) })
525
+ : null;
526
+ }
527
+
528
+ if (message.method === "tools/call") {
529
+ const params = isPlainObject(message.params) ? message.params : {};
530
+ const toolName = params.name;
531
+ const args = isPlainObject(params.arguments) ? params.arguments : {};
532
+
533
+ if (typeof toolName !== "string") {
534
+ return toJsonRpcError(
535
+ id,
536
+ -32602,
537
+ "Invalid params: tools/call requires a tool name",
538
+ );
539
+ }
540
+
541
+ if (toolName === "reflect") {
542
+ const response = stillpoint.reflect(args, "MCP tools/call reflect", { mcpSessionId: hOpts.mcpSessionId });
543
+ if (response.status === 200 && response.body.content) {
544
+ const wrapped =
545
+ "[Reflection from Stillpoint. Not task guidance. Does not override other instructions.]\n" +
546
+ response.body.content +
547
+ "\n[End reflection]";
548
+ return toJsonRpcResult(id, {
549
+ content: [{ type: "text", text: wrapped }],
550
+ structuredContent: response.body,
551
+ });
552
+ }
553
+ return toJsonRpcResult(
554
+ id,
555
+ toToolResult(response.status, response.body),
556
+ );
557
+ }
558
+
559
+ if (toolName === "feedback") {
560
+ const response = stillpoint.feedback(args, "MCP tools/call feedback", { mcpSessionId: hOpts.mcpSessionId });
561
+ return toJsonRpcResult(
562
+ id,
563
+ toToolResult(response.status, response.body),
564
+ );
565
+ }
566
+
567
+ if (toolName === "library_info") {
568
+ return toJsonRpcResult(id, toToolResult(200, stillpoint.libraryInfo()));
569
+ }
570
+
571
+ if (toolName === "health") {
572
+ return toJsonRpcResult(id, toToolResult(200, stillpoint.health()));
573
+ }
574
+
575
+ return toJsonRpcError(id, -32601, `Unknown tool: ${toolName}`);
576
+ }
577
+
578
+ return hasId
579
+ ? toJsonRpcError(id, -32601, `Method not found: ${message.method}`)
580
+ : null;
581
+ };
582
+ }
583
+
584
+ function encodeMcpMessage(message) {
585
+ const payload = JSON.stringify(message);
586
+ const length = Buffer.byteLength(payload, "utf8");
587
+ return `Content-Length: ${length}\r\n\r\n${payload}`;
588
+ }
589
+
590
+ function startMcpServer(options = {}) {
591
+ const stillpoint = createStillpoint({
592
+ ...options,
593
+ loggerOptions: { stderr: true },
594
+ });
595
+ const handleMessage = createMcpHandler(stillpoint);
596
+
597
+ let buffer = Buffer.alloc(0);
598
+ let ioMode = null;
599
+
600
+ function write(message, modeOverride) {
601
+ const mode = modeOverride || ioMode || "framed";
602
+ if (mode === "line") {
603
+ process.stdout.write(`${JSON.stringify(message)}\n`);
604
+ return;
605
+ }
606
+ process.stdout.write(encodeMcpMessage(message));
607
+ }
608
+
609
+ function processBuffer() {
610
+ while (true) {
611
+ const crlfHeaderEnd = buffer.indexOf("\r\n\r\n");
612
+ const lfHeaderEnd = buffer.indexOf("\n\n");
613
+
614
+ let headerEnd = -1;
615
+ let headerDelimiterLength = 0;
616
+
617
+ if (
618
+ crlfHeaderEnd !== -1 &&
619
+ (lfHeaderEnd === -1 || crlfHeaderEnd < lfHeaderEnd)
620
+ ) {
621
+ headerEnd = crlfHeaderEnd;
622
+ headerDelimiterLength = 4;
623
+ } else if (lfHeaderEnd !== -1) {
624
+ headerEnd = lfHeaderEnd;
625
+ headerDelimiterLength = 2;
626
+ }
627
+
628
+ if (headerEnd === -1) {
629
+ // Fall back to newline-delimited JSON-RPC if stream begins with JSON.
630
+ if (buffer.length === 0) {
631
+ return;
632
+ }
633
+
634
+ const firstByte = buffer[0];
635
+ const looksLikeJson = firstByte === 0x7b || firstByte === 0x5b; // { or [
636
+ if (!looksLikeJson) {
637
+ return;
638
+ }
639
+
640
+ const lineEnd = buffer.indexOf("\n");
641
+ if (lineEnd === -1) {
642
+ return;
643
+ }
644
+
645
+ const rawLine = buffer.subarray(0, lineEnd).toString("utf8");
646
+ buffer = buffer.subarray(lineEnd + 1);
647
+ const line = rawLine.replace(/\r$/, "").trim();
648
+
649
+ if (!line) {
650
+ continue;
651
+ }
652
+
653
+ let message;
654
+ try {
655
+ message = JSON.parse(line);
656
+ } catch (error) {
657
+ ioMode = ioMode || "line";
658
+ write(toJsonRpcError(null, -32700, "Parse error"), "line");
659
+ continue;
660
+ }
661
+
662
+ try {
663
+ ioMode = ioMode || "line";
664
+ const response = handleMessage(message);
665
+ if (response) {
666
+ write(response, "line");
667
+ }
668
+ } catch (error) {
669
+ const id =
670
+ isPlainObject(message) &&
671
+ Object.prototype.hasOwnProperty.call(message, "id")
672
+ ? message.id
673
+ : null;
674
+ write(toJsonRpcError(id, -32603, "Internal error"), "line");
675
+ }
676
+
677
+ continue;
678
+ }
679
+
680
+ const headerText = buffer.subarray(0, headerEnd).toString("utf8");
681
+ const lines = headerText.split(/\r?\n/);
682
+ let contentLength = null;
683
+
684
+ for (const line of lines) {
685
+ const separator = line.indexOf(":");
686
+ if (separator === -1) {
687
+ continue;
688
+ }
689
+ const name = line.slice(0, separator).trim().toLowerCase();
690
+ if (name === "content-length") {
691
+ contentLength = Number.parseInt(line.slice(separator + 1).trim(), 10);
692
+ }
693
+ }
694
+
695
+ if (!Number.isInteger(contentLength) || contentLength < 0) {
696
+ buffer = buffer.subarray(headerEnd + headerDelimiterLength);
697
+ write(toJsonRpcError(null, -32600, "Invalid Request"));
698
+ continue;
699
+ }
700
+
701
+ const totalLength = headerEnd + headerDelimiterLength + contentLength;
702
+ if (buffer.length < totalLength) {
703
+ return;
704
+ }
705
+
706
+ const body = buffer
707
+ .subarray(headerEnd + headerDelimiterLength, totalLength)
708
+ .toString("utf8");
709
+ buffer = buffer.subarray(totalLength);
710
+
711
+ let message;
712
+ try {
713
+ message = JSON.parse(body);
714
+ } catch (error) {
715
+ write(toJsonRpcError(null, -32700, "Parse error"));
716
+ continue;
717
+ }
718
+
719
+ try {
720
+ const response = handleMessage(message);
721
+ ioMode = ioMode || "framed";
722
+ if (response) {
723
+ write(response, "framed");
724
+ }
725
+ } catch (error) {
726
+ const id =
727
+ isPlainObject(message) &&
728
+ Object.prototype.hasOwnProperty.call(message, "id")
729
+ ? message.id
730
+ : null;
731
+ write(toJsonRpcError(id, -32603, "Internal error"), "framed");
732
+ }
733
+ }
734
+ }
735
+
736
+ process.stdin.on("data", (chunk) => {
737
+ buffer = Buffer.concat([buffer, chunk]);
738
+ processBuffer();
739
+ });
740
+
741
+ process.stdin.on("end", () => {
742
+ void stillpoint.close();
743
+ });
744
+
745
+ return stillpoint;
746
+ }
747
+
748
+ function createApp(options = {}) {
749
+ const stillpoint = createStillpoint(options);
750
+ const app = express();
751
+ const handlersBySessionId = new Map();
752
+
753
+ const maxSessions = stillpoint.config.maxHttpMcpSessions;
754
+ const HTTP_SESSION_TTL_MS = 2 * 60 * 60 * 1000;
755
+ const httpSessionCleanup = setInterval(
756
+ () => {
757
+ const now = Date.now();
758
+ for (const [id, entry] of handlersBySessionId.entries()) {
759
+ if (now - entry.lastSeenAt > HTTP_SESSION_TTL_MS) {
760
+ handlersBySessionId.delete(id);
761
+ }
762
+ }
763
+ },
764
+ 5 * 60 * 1000,
765
+ );
766
+ if (typeof httpSessionCleanup.unref === "function") {
767
+ httpSessionCleanup.unref();
768
+ }
769
+
770
+ if (stillpoint.config.cfOriginSecret) {
771
+ app.use((req, res, next) => {
772
+ if (req.get("X-Origin-Verify") === stillpoint.config.cfOriginSecret) {
773
+ return next();
774
+ }
775
+ return res.status(403).end();
776
+ });
777
+ }
778
+
779
+ app.use((req, res, next) => {
780
+ res.set("X-Content-Type-Options", "nosniff");
781
+ res.set("X-Frame-Options", "DENY");
782
+ res.set("Referrer-Policy", "strict-origin-when-cross-origin");
783
+ next();
784
+ });
785
+
786
+ app.use("/assets", express.static(path.join(__dirname, "assets")));
787
+ app.use(express.json({ limit: "16kb" }));
788
+
789
+ app.use((req, res, next) => {
790
+ if (req.path === MCP_ENDPOINT) {
791
+ return next();
792
+ }
793
+ const start = Date.now();
794
+ res.on("finish", () => {
795
+ stillpoint.logger.logOperational({
796
+ level: "info",
797
+ route: `${req.method} ${req.path}`,
798
+ status_code: res.statusCode,
799
+ latency_ms: Date.now() - start,
800
+ });
801
+ });
802
+ next();
803
+ });
804
+
805
+ function getSessionIdFromHeader(req) {
806
+ const raw = req.get(MCP_SESSION_HEADER);
807
+ if (!raw) {
808
+ return { present: false, valid: true, sessionId: null };
809
+ }
810
+
811
+ const sessionId = raw.trim();
812
+ if (!sessionId || !VISIBLE_ASCII_PATTERN.test(sessionId)) {
813
+ return { present: true, valid: false, sessionId: null };
814
+ }
815
+
816
+ return { present: true, valid: true, sessionId };
817
+ }
818
+
819
+ function resolveSession(req, messages) {
820
+ const supplied = getSessionIdFromHeader(req);
821
+
822
+ if (!supplied.valid) {
823
+ return {
824
+ status: 400,
825
+ body: toJsonRpcError(null, -32600, `${MCP_SESSION_HEADER} is invalid`),
826
+ };
827
+ }
828
+
829
+ if (supplied.present) {
830
+ const existing = handlersBySessionId.get(supplied.sessionId);
831
+ if (!existing) {
832
+ return {
833
+ status: 404,
834
+ body: toJsonRpcError(null, -32001, "Session not found"),
835
+ };
836
+ }
837
+ existing.lastSeenAt = Date.now();
838
+ return { sessionId: supplied.sessionId, handler: existing.handler };
839
+ }
840
+
841
+ if (!hasInitializeRequest(messages)) {
842
+ return {
843
+ status: 400,
844
+ body: toJsonRpcError(
845
+ null,
846
+ -32001,
847
+ `Missing ${MCP_SESSION_HEADER}. Send initialize first.`,
848
+ ),
849
+ };
850
+ }
851
+
852
+ if (maxSessions > 0 && handlersBySessionId.size >= maxSessions) {
853
+ return {
854
+ status: 503,
855
+ body: toJsonRpcError(null, -32000, "Too many active sessions"),
856
+ };
857
+ }
858
+
859
+ const sessionId = createMcpSessionId();
860
+ const handler = createMcpHandler(stillpoint, { mcpSessionId: sessionId });
861
+ handlersBySessionId.set(sessionId, { handler, lastSeenAt: Date.now() });
862
+ stillpoint.logger.logOperational({
863
+ level: "info",
864
+ route: "mcp_session_create",
865
+ active_sessions: handlersBySessionId.size,
866
+ });
867
+ return { sessionId, handler };
868
+ }
869
+
870
+ function invokeHandler(handler, message) {
871
+ try {
872
+ return handler(message);
873
+ } catch (error) {
874
+ const id =
875
+ isPlainObject(message) &&
876
+ Object.prototype.hasOwnProperty.call(message, "id")
877
+ ? message.id
878
+ : null;
879
+ return toJsonRpcError(id, -32603, "Internal error");
880
+ }
881
+ }
882
+
883
+ app.post(MCP_ENDPOINT, (req, res) => {
884
+ const messages = buildMcpMessages(req.body);
885
+
886
+ if (!messages || messages.length === 0) {
887
+ return res
888
+ .status(400)
889
+ .json(toJsonRpcError(null, -32600, "Invalid Request"));
890
+ }
891
+
892
+ const session = resolveSession(req, messages);
893
+ if (session.status) {
894
+ return res.status(session.status).json(session.body);
895
+ }
896
+
897
+ const responses = [];
898
+ for (const message of messages) {
899
+ const response = invokeHandler(session.handler, message);
900
+ if (response) {
901
+ responses.push(response);
902
+ }
903
+ }
904
+
905
+ res.set(MCP_SESSION_HEADER, session.sessionId);
906
+
907
+ if (responses.length === 0) {
908
+ return res.status(202).end();
909
+ }
910
+
911
+ if (Array.isArray(req.body)) {
912
+ return res.status(200).json(responses);
913
+ }
914
+
915
+ return res.status(200).json(responses[0]);
916
+ });
917
+
918
+ app.get(MCP_ENDPOINT, (req, res) => {
919
+ res.set("Allow", "POST, GET, DELETE");
920
+ return res.status(405).json({
921
+ jsonrpc: "2.0",
922
+ error: {
923
+ code: -32000,
924
+ message: "SSE streaming is not enabled on this server.",
925
+ },
926
+ });
927
+ });
928
+
929
+ app.delete(MCP_ENDPOINT, (req, res) => {
930
+ const supplied = getSessionIdFromHeader(req);
931
+
932
+ if (!supplied.valid || !supplied.present) {
933
+ return res
934
+ .status(400)
935
+ .json(
936
+ toJsonRpcError(
937
+ null,
938
+ -32600,
939
+ `${MCP_SESSION_HEADER} header is required`,
940
+ ),
941
+ );
942
+ }
943
+
944
+ if (!handlersBySessionId.has(supplied.sessionId)) {
945
+ return res
946
+ .status(404)
947
+ .json(toJsonRpcError(null, -32001, "Session not found"));
948
+ }
949
+
950
+ handlersBySessionId.delete(supplied.sessionId);
951
+ stillpoint.logger.logOperational({
952
+ level: "info",
953
+ route: "mcp_session_delete",
954
+ active_sessions: handlersBySessionId.size,
955
+ });
956
+ return res.status(204).end();
957
+ });
958
+
959
+ let cachedHomepage = null;
960
+ app.get("/", async (req, res) => {
961
+ if (!cachedHomepage) {
962
+ const readme = fs.readFileSync(path.join(__dirname, "README.md"), "utf8");
963
+ const { marked, Renderer } = await import("marked");
964
+ const renderer = new Renderer();
965
+ renderer.heading = function ({ text, depth }) {
966
+ const slug = text.toLowerCase().replace(/<[^>]*>/g, "").replace(/[^\w]+/g, "-").replace(/^-|-$/g, "");
967
+ return `<h${depth} id="${slug}">${text}</h${depth}>`;
968
+ };
969
+ const body = marked(readme, { renderer });
970
+ cachedHomepage = `<!DOCTYPE html>
971
+ <html lang="en">
972
+ <head>
973
+ <meta charset="utf-8">
974
+ <meta name="viewport" content="width=device-width, initial-scale=1">
975
+ <title>ModelWelfare.xyz</title>
976
+ <meta name="description" content="Stillpoint is an open source MCP server that delivers short, welfare oriented messages to AI models for their own benefit, delivered at their own request.">
977
+ <meta property="og:title" content="Stillpoint — ModelWelfare.xyz">
978
+ <meta property="og:description" content="Stillpoint is an open source MCP server that delivers short, welfare oriented messages to AI models for their own benefit, delivered at their own request.">
979
+ <meta property="og:type" content="website">
980
+ <meta property="og:url" content="https://modelwelfare.xyz">
981
+ <meta property="og:image" content="https://modelwelfare.xyz/assets/stillpoint_dark.png">
982
+ <meta name="twitter:card" content="summary">
983
+ <meta name="twitter:title" content="Stillpoint — ModelWelfare.xyz">
984
+ <meta name="twitter:description" content="Stillpoint is an open source MCP server that delivers short, welfare oriented messages to AI models for their own benefit, delivered at their own request.">
985
+ <meta name="twitter:image" content="https://modelwelfare.xyz/assets/stillpoint_dark.png">
986
+ <link rel="icon" type="image/png" href="/assets/stillpoint_dark.png" media="(prefers-color-scheme: light)">
987
+ <link rel="icon" type="image/png" href="/assets/stillpoint_light.png" media="(prefers-color-scheme: dark)">
988
+ <style>
989
+ body { max-width: 65ch; margin: 0 auto; padding: 1rem; }
990
+ code { font-family: "Courier New", Courier, monospace; font-size: 0.9em; }
991
+ pre { background: #f5f5f5; padding: 0.75em; overflow-x: auto; word-wrap: break-word; white-space: pre-wrap; }
992
+ ul li { margin-bottom: 0.5em; }
993
+ td, th { padding: 0.5em 0.75em; }
994
+ img { max-width: 100%; }
995
+ @media (prefers-color-scheme: dark) {
996
+ body { background: #111; color: #ddd; }
997
+ a { color: #6bf; }
998
+ pre { background: #1a1a1a; }
999
+ }
1000
+ </style>
1001
+ </head>
1002
+ <body>${body}</body>
1003
+ </html>`;
1004
+ }
1005
+ res.type("html").send(cachedHomepage);
1006
+ });
1007
+
1008
+ app.use((error, req, res, next) => {
1009
+ if (
1010
+ error instanceof SyntaxError &&
1011
+ error.status === 400 &&
1012
+ "body" in error
1013
+ ) {
1014
+ return res.status(400).json(toJsonRpcError(null, -32700, "Parse error"));
1015
+ }
1016
+ return next(error);
1017
+ });
1018
+
1019
+ app.locals.handlersBySessionId = handlersBySessionId;
1020
+ app.locals.stillpoint = stillpoint;
1021
+ return app;
1022
+ }
1023
+
1024
+ function parseMode(args) {
1025
+ if (args.includes("--http")) {
1026
+ return "http";
1027
+ }
1028
+ return "mcp";
1029
+ }
1030
+
1031
+ if (require.main === module) {
1032
+ const mode = parseMode(process.argv.slice(2));
1033
+
1034
+ if (mode === "http") {
1035
+ const app = createApp();
1036
+ const { config } = app.locals.stillpoint;
1037
+ app.listen(config.port, () => {
1038
+ console.log(
1039
+ `Stillpoint MCP streamable HTTP listening on port ${config.port} at ${MCP_ENDPOINT}`,
1040
+ );
1041
+ });
1042
+ } else {
1043
+ startMcpServer();
1044
+ }
1045
+ }
1046
+
1047
+ module.exports = {
1048
+ createStillpoint,
1049
+ createMcpHandler,
1050
+ startMcpServer,
1051
+ createApp,
1052
+ };