tina4-nodejs 3.13.50 → 3.13.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.50",
3
+ "version": "3.13.51",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -612,12 +612,16 @@ export class DevAdmin {
612
612
  // MCP tool introspection over the built-in MCP server (browser dev-admin REST shim)
613
613
  { method: "GET", pattern: "/__dev/api/mcp/tools", handler: handleMcpTools },
614
614
  { method: "POST", pattern: "/__dev/api/mcp/call", handler: handleMcpCall },
615
- // MCP JSON-RPC + SSE endpoints that REAL MCP clients (Claude Code/Desktop)
616
- // speak. POST /__dev/mcp[/message] -> JSON-RPC handleMessage; GET
617
- // /__dev/mcp/sse -> SSE handshake announcing the message endpoint. Mirrors
618
- // the Python v3 fix (POST /__dev/mcp + /__dev/mcp/message, GET /__dev/mcp/sse).
619
- { method: "POST", pattern: "/__dev/mcp", handler: handleMcpMessage },
620
- { method: "POST", pattern: "/__dev/mcp/message", handler: handleMcpMessage },
615
+ // MCP transport endpoints that REAL MCP clients (Claude Code/Desktop) speak.
616
+ // /__dev/mcp is the Streamable HTTP endpoint (current transport): POST = a
617
+ // JSON-RPC message (initialize issues Mcp-Session-Id), DELETE = terminate a
618
+ // session, GET = 405 (this server initiates no messages). /message + /sse
619
+ // are the legacy 2024-11-05 HTTP+SSE transport, kept for older SSE-only
620
+ // clients. Mirrors the Python master.
621
+ { method: "POST", pattern: "/__dev/mcp", handler: handleMcpStreamable },
622
+ { method: "DELETE", pattern: "/__dev/mcp", handler: handleMcpDelete },
623
+ { method: "GET", pattern: "/__dev/mcp", handler: handleMcpGet405 },
624
+ { method: "POST", pattern: "/__dev/mcp/message", handler: handleMcpLegacyMessage },
621
625
  { method: "GET", pattern: "/__dev/mcp/sse", handler: handleMcpSse },
622
626
  ];
623
627
  for (const route of mcpRoutes) {
@@ -2241,13 +2245,31 @@ const handleMcpCall: RouteHandler = async (req, res) => {
2241
2245
  /**
2242
2246
  * JSON-RPC message endpoint for real MCP clients.
2243
2247
  *
2244
- * Mounted at POST /__dev/mcp and POST /__dev/mcp/message. Forwards the request
2245
- * body to the default dev MCP server's handleMessage() and returns the JSON-RPC
2246
- * response. Notifications / id-less requests yield an empty 204. Mirrors the
2247
- * Python v3 fix. The /__dev path is always public (auth-bypassed), so MCP
2248
- * clients connect without a token.
2248
+ * Mounted at POST /__dev/mcp (Streamable HTTP). Forwards the body to the default
2249
+ * dev MCP server via dispatchHttp(): initialize issues an Mcp-Session-Id header,
2250
+ * an unknown session id is 404 (client re-inits), a notification is 202, else
2251
+ * 200 with the JSON-RPC response as application/json. The /__dev path is always
2252
+ * public (auth-bypassed), so MCP clients connect without a token.
2249
2253
  */
2250
- const handleMcpMessage: RouteHandler = async (req, res) => {
2254
+ function mcpNormalizeBody(body: unknown): string | Record<string, unknown> {
2255
+ if (typeof body === "object" && body !== null) return body as Record<string, unknown>;
2256
+ if (typeof body === "string") return body;
2257
+ return String(body ?? "");
2258
+ }
2259
+
2260
+ function applyMcpOutcome(
2261
+ res: Parameters<RouteHandler>[1],
2262
+ outcome: { status: number; headers: Record<string, string>; body: string },
2263
+ ): void {
2264
+ for (const [n, v] of Object.entries(outcome.headers)) res.addHeader(n, v);
2265
+ if (!outcome.body) {
2266
+ res.send("", outcome.status);
2267
+ return;
2268
+ }
2269
+ res.json(JSON.parse(outcome.body), outcome.status);
2270
+ }
2271
+
2272
+ const handleMcpStreamable: RouteHandler = async (req, res) => {
2251
2273
  if (!mcpRequestAllowed(req)) {
2252
2274
  res.json({ error: "MCP forbidden" }, 404);
2253
2275
  return;
@@ -2255,48 +2277,71 @@ const handleMcpMessage: RouteHandler = async (req, res) => {
2255
2277
  try {
2256
2278
  const { getDefaultDevServer } = await import("./mcp.js");
2257
2279
  const server = getDefaultDevServer();
2258
- const body = req.body;
2259
- let raw: string | Record<string, unknown>;
2260
- if (typeof body === "object" && body !== null) {
2261
- raw = body as Record<string, unknown>;
2262
- } else {
2263
- raw = typeof body === "string" ? body : String(body ?? "");
2264
- }
2265
- const result = await server.handleMessage(raw);
2266
- if (!result) {
2267
- // Notification / no id — nothing to return.
2268
- res.send("", 204);
2269
- return;
2270
- }
2271
- res.json(JSON.parse(result));
2280
+ const sessionId = req.header("mcp-session-id") ?? "";
2281
+ applyMcpOutcome(res, await server.dispatchHttp(mcpNormalizeBody(req.body), sessionId));
2282
+ } catch (e) {
2283
+ res.json({ error: (e as Error).message }, 500);
2284
+ }
2285
+ };
2286
+
2287
+ /** DELETE /__dev/mcp terminate the session named by Mcp-Session-Id. */
2288
+ const handleMcpDelete: RouteHandler = async (req, res) => {
2289
+ if (!mcpRequestAllowed(req)) {
2290
+ res.json({ error: "MCP forbidden" }, 404);
2291
+ return;
2292
+ }
2293
+ const { getDefaultDevServer } = await import("./mcp.js");
2294
+ getDefaultDevServer().closeSession(req.header("mcp-session-id") ?? "");
2295
+ res.send("", 204);
2296
+ };
2297
+
2298
+ /** GET /__dev/mcp — 405: this server initiates no messages (use /sse for a stream). */
2299
+ const handleMcpGet405: RouteHandler = (req, res) => {
2300
+ if (!mcpRequestAllowed(req)) {
2301
+ res.json({ error: "MCP forbidden" }, 404);
2302
+ return;
2303
+ }
2304
+ res.addHeader("Allow", "POST, DELETE");
2305
+ res.json({ error: "method not allowed" }, 405);
2306
+ };
2307
+
2308
+ /**
2309
+ * POST /__dev/mcp/message — legacy HTTP+SSE message sink. Delivers the JSON-RPC
2310
+ * response on the matching open SSE stream (202 here); with no open stream it
2311
+ * degrades to an inline Streamable HTTP response, so the path serves a plain
2312
+ * POST client too.
2313
+ */
2314
+ const handleMcpLegacyMessage: RouteHandler = async (req, res) => {
2315
+ if (!mcpRequestAllowed(req)) {
2316
+ res.json({ error: "MCP forbidden" }, 404);
2317
+ return;
2318
+ }
2319
+ try {
2320
+ const { getDefaultDevServer } = await import("./mcp.js");
2321
+ const server = getDefaultDevServer();
2322
+ const sessionId = (req.query?.sessionId as string) || (req.header("mcp-session-id") ?? "");
2323
+ applyMcpOutcome(res, await server.dispatchSseMessage(mcpNormalizeBody(req.body), sessionId));
2272
2324
  } catch (e) {
2273
2325
  res.json({ error: (e as Error).message }, 500);
2274
2326
  }
2275
2327
  };
2276
2328
 
2277
2329
  /**
2278
- * SSE handshake endpoint for real MCP clients.
2279
- *
2280
- * Mounted at GET /__dev/mcp/sse. Announces the JSON-RPC message endpoint via an
2281
- * `endpoint` event, exactly like the canonical McpServer.registerRoutes() and
2282
- * the Python v3 fix. Content-Type text/event-stream, status 200.
2330
+ * GET /__dev/mcp/sse — legacy HTTP+SSE stream. Opens a persistent SSE connection:
2331
+ * first the `endpoint` event naming the (session-tagged) POST target, then each
2332
+ * JSON-RPC response as it arrives. Node's single event loop lets a separate POST
2333
+ * to /message feed this stream via an in-process channel.
2283
2334
  */
2284
2335
  const handleMcpSse: RouteHandler = async (req, res) => {
2285
2336
  if (!mcpRequestAllowed(req)) {
2286
2337
  res.json({ error: "MCP forbidden" }, 404);
2287
2338
  return;
2288
2339
  }
2289
- // req.path is the path only (no query); turn /__dev/mcp/sse into the message
2290
- // endpoint /__dev/mcp/message that the client should POST to.
2291
- const reqPath = req.path || "/__dev/mcp/sse";
2292
- const endpointUrl = reqPath.replace(/\/sse$/, "/message");
2293
- const sseData = `event: endpoint\ndata: ${endpointUrl}\n\n`;
2294
- res.raw.writeHead(200, {
2295
- "Content-Type": "text/event-stream",
2296
- "Cache-Control": "no-cache",
2297
- Connection: "keep-alive",
2298
- });
2299
- res.raw.end(sseData);
2340
+ const { getDefaultDevServer } = await import("./mcp.js");
2341
+ const server = getDefaultDevServer();
2342
+ const sessionId = server.openSession();
2343
+ const base = (req.path || "/__dev/mcp/sse").replace(/\/sse$/, "");
2344
+ await res.stream(server.sseStream(sessionId, `${base}/message?sessionId=${sessionId}`));
2300
2345
  };
2301
2346
 
2302
2347
  const handleScaffoldList: RouteHandler = (_req, res) => {
@@ -10,7 +10,7 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
 
12
12
  interface McpDiscovery {
13
- mcpServers: Record<string, { url: string; description: string }>;
13
+ mcpServers: Record<string, { type?: string; url: string; description: string }>;
14
14
  }
15
15
 
16
16
  const TINA4_DIR = ".tina4";
@@ -34,8 +34,9 @@ export function writeMcpDiscovery(projectRoot: string, port: number): boolean {
34
34
  const desired: McpDiscovery = {
35
35
  mcpServers: {
36
36
  "tina4-live-docs": {
37
- url: `http://localhost:${portStr}/__dev/api/mcp`,
38
- description: "Live API docs for this Tina4 project (framework + user code)",
37
+ type: "http",
38
+ url: `http://localhost:${portStr}/__dev/mcp`,
39
+ description: "Live API docs + dev tools for this Tina4 project (framework + user code)",
39
40
  },
40
41
  },
41
42
  };
@@ -19,6 +19,7 @@ import * as path from "node:path";
19
19
  import * as os from "node:os";
20
20
  import { spawnSync } from "node:child_process";
21
21
  import { createRequire } from "node:module";
22
+ import { randomBytes } from "node:crypto";
22
23
 
23
24
  // Synchronous CommonJS-style require that works under real ESM (where the
24
25
  // bare `require` global is undefined). Dev-tool handlers are synchronous, so
@@ -96,6 +97,12 @@ export const METHOD_NOT_FOUND = -32601;
96
97
  export const INVALID_PARAMS = -32602;
97
98
  export const INTERNAL_ERROR = -32603;
98
99
 
100
+ // MCP protocol versions this server can speak, newest first. The 2025-* versions
101
+ // are the Streamable HTTP era; 2024-11-05 is the legacy HTTP+SSE transport we
102
+ // still accept for older clients (Claude Desktop et al.).
103
+ export const SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"] as const;
104
+ export const LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
105
+
99
106
  export function encodeResponse(requestId: number | string | null | undefined, result: unknown): string {
100
107
  return JSON.stringify({ jsonrpc: "2.0", id: requestId, result });
101
108
  }
@@ -295,6 +302,15 @@ export class McpServer {
295
302
  private _resources: Map<string, McpResourceDefinition> = new Map();
296
303
  private _initialized = false;
297
304
 
305
+ // Streamable HTTP session ids issued at initialize time -> creation ts. A
306
+ // request bearing an unknown id gets a 404 so the client re-initializes.
307
+ private _sessions: Map<string, number> = new Map();
308
+ // Open legacy HTTP+SSE streams keyed by session id. GET /sse registers a
309
+ // channel; POST /message pushes each JSON-RPC response onto it so it streams
310
+ // back on the open connection (the 2024-11-05 transport). Node is single-
311
+ // threaded, so this in-process map is the whole coordination mechanism.
312
+ private _sseChannels: Map<string, { buffer: string[]; wake: (() => void) | null }> = new Map();
313
+
298
314
  constructor(mcpPath: string, name = "Tina4 MCP", version = "1.0.0") {
299
315
  this.path = mcpPath.replace(/\/+$/, "");
300
316
  this.name = name;
@@ -302,6 +318,128 @@ export class McpServer {
302
318
  McpServer._instances.push(this);
303
319
  }
304
320
 
321
+ // ── Session lifecycle + protocol negotiation ──────────────────
322
+
323
+ /** Mint a new session id and remember it. Called on `initialize`. */
324
+ openSession(): string {
325
+ const sid = randomBytes(16).toString("hex");
326
+ this._sessions.set(sid, Date.now());
327
+ return sid;
328
+ }
329
+
330
+ /** True when `sessionId` was issued by this server and is still open. */
331
+ isValidSession(sessionId: string | undefined | null): boolean {
332
+ return !!sessionId && this._sessions.has(sessionId);
333
+ }
334
+
335
+ /** Forget a session (client DELETE or SSE stream close). */
336
+ closeSession(sessionId: string | undefined | null): boolean {
337
+ return sessionId ? this._sessions.delete(sessionId) : false;
338
+ }
339
+
340
+ /**
341
+ * Pick the protocol version to run on. Echo the client's requested version
342
+ * when we support it (proper negotiation), else fall back to the newest we
343
+ * speak so an unversioned/old client still connects.
344
+ */
345
+ negotiateProtocolVersion(requested: string | undefined | null): string {
346
+ return requested && (SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(requested)
347
+ ? requested
348
+ : LATEST_PROTOCOL_VERSION;
349
+ }
350
+
351
+ private _peekMethod(raw: string | Record<string, unknown>): string | null {
352
+ try {
353
+ const obj = typeof raw === "object" && raw !== null ? raw : JSON.parse(String(raw || "{}"));
354
+ return obj && typeof obj === "object" ? ((obj as Record<string, unknown>).method as string) ?? null : null;
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+
360
+ // ── Streamable HTTP transport (transport-agnostic, mirrors Python) ──
361
+
362
+ /**
363
+ * Streamable HTTP POST handler. initialize mints a session id (returned in
364
+ * the Mcp-Session-Id response header); a non-initialize request with an
365
+ * unknown session id is a 404 (client re-inits); a notification is 202; else
366
+ * 200 with the JSON-RPC response as application/json (which the spec permits
367
+ * for a POST that resolves to a single response).
368
+ */
369
+ async dispatchHttp(
370
+ raw: string | Record<string, unknown>,
371
+ sessionId = "",
372
+ ): Promise<{ status: number; headers: Record<string, string>; body: string }> {
373
+ const isInit = this._peekMethod(raw) === "initialize";
374
+ if (!isInit && sessionId && !this.isValidSession(sessionId)) {
375
+ return { status: 404, headers: {}, body: encodeError(null, INVALID_REQUEST, "session not found") };
376
+ }
377
+ const body = await this.handleMessage(raw);
378
+ const headers: Record<string, string> = {};
379
+ if (isInit) headers["Mcp-Session-Id"] = this.openSession();
380
+ if (!body) return { status: 202, headers, body: "" };
381
+ return { status: 200, headers, body };
382
+ }
383
+
384
+ /**
385
+ * Legacy HTTP+SSE POST /message handler. When a live SSE stream is open for
386
+ * `sessionId`, run the message and push the response down that stream (202
387
+ * here); with no open stream it degrades to an inline Streamable HTTP
388
+ * response, so the same path serves a legacy SSE client and a plain POST.
389
+ */
390
+ async dispatchSseMessage(
391
+ raw: string | Record<string, unknown>,
392
+ sessionId = "",
393
+ ): Promise<{ status: number; headers: Record<string, string>; body: string }> {
394
+ const channel = sessionId ? this._sseChannels.get(sessionId) : undefined;
395
+ if (!channel) return this.dispatchHttp(raw, sessionId);
396
+ const body = await this.handleMessage(raw);
397
+ if (body) {
398
+ channel.buffer.push(body);
399
+ if (channel.wake) {
400
+ const wake = channel.wake;
401
+ channel.wake = null;
402
+ wake();
403
+ }
404
+ }
405
+ return { status: 202, headers: {}, body: "" };
406
+ }
407
+
408
+ /**
409
+ * Async generator of SSE frames for the legacy HTTP+SSE transport. Emits the
410
+ * `endpoint` event first (naming the POST target), then each queued JSON-RPC
411
+ * response as it arrives, with periodic keep-alive comments. Registers the
412
+ * per-session channel up front and tears it down (plus the session) when the
413
+ * client disconnects and the generator is closed.
414
+ */
415
+ async *sseStream(sessionId: string, endpointUrl: string, keepaliveMs = 15000): AsyncGenerator<string> {
416
+ const channel: { buffer: string[]; wake: (() => void) | null } = { buffer: [], wake: null };
417
+ this._sseChannels.set(sessionId, channel);
418
+ try {
419
+ yield `event: endpoint\ndata: ${endpointUrl}\n\n`;
420
+ for (;;) {
421
+ if (channel.buffer.length > 0) {
422
+ yield `event: message\ndata: ${channel.buffer.shift()}\n\n`;
423
+ continue;
424
+ }
425
+ const gotMessage = await new Promise<boolean>((resolve) => {
426
+ const timer = setTimeout(() => {
427
+ channel.wake = null;
428
+ resolve(false);
429
+ }, keepaliveMs);
430
+ channel.wake = () => {
431
+ clearTimeout(timer);
432
+ resolve(true);
433
+ };
434
+ });
435
+ if (!gotMessage) yield `: keep-alive\n\n`;
436
+ }
437
+ } finally {
438
+ this._sseChannels.delete(sessionId);
439
+ this.closeSession(sessionId);
440
+ }
441
+ }
442
+
305
443
  registerTool(
306
444
  name: string,
307
445
  handler: (args: Record<string, unknown>) => unknown,
@@ -375,10 +513,11 @@ export class McpServer {
375
513
  }
376
514
  }
377
515
 
378
- private _handleInitialize(_params: Record<string, unknown>): Record<string, unknown> {
516
+ private _handleInitialize(params: Record<string, unknown>): Record<string, unknown> {
379
517
  this._initialized = true;
518
+ const requested = params ? (params.protocolVersion as string | undefined) : undefined;
380
519
  return {
381
- protocolVersion: "2024-11-05",
520
+ protocolVersion: this.negotiateProtocolVersion(requested),
382
521
  capabilities: {
383
522
  tools: { listChanged: false },
384
523
  resources: { subscribe: false, listChanged: false },
@@ -485,55 +624,64 @@ export class McpServer {
485
624
  };
486
625
  }
487
626
 
627
+ /** Coerce a route handler's parsed body into what the dispatchers accept. */
628
+ private _normalizeBody(body: unknown): string | Record<string, unknown> {
629
+ if (typeof body === "object" && body !== null) return body as Record<string, unknown>;
630
+ if (typeof body === "string") return body;
631
+ return String(body ?? "");
632
+ }
633
+
488
634
  /**
489
- * Register HTTP routes for this MCP server on the Tina4 router.
635
+ * Register HTTP routes for this MCP server on the Tina4 router. Mounts both
636
+ * supported transports on `path`:
637
+ * POST {path} — Streamable HTTP (current transport)
638
+ * POST {path}/message — legacy HTTP+SSE message sink (+ inline fallback)
639
+ * GET {path}/sse — legacy HTTP+SSE stream (persistent)
490
640
  *
491
- * Registers:
492
- * POST {path}/message — JSON-RPC message endpoint
493
- * GET {path}/sse SSE endpoint for streaming
641
+ * A Streamable HTTP client (Claude Code `--transport http`) POSTs to `{path}`
642
+ * and reads the JSON-RPC response inline, with an Mcp-Session-Id header on
643
+ * initialize. A legacy SSE client GETs `{path}/sse`, gets the endpoint event,
644
+ * and its responses stream back on that connection.
494
645
  */
495
646
  registerRoutes(router: {
496
647
  post: (pattern: string, handler: (req: unknown, res: unknown) => unknown) => { noAuth: () => unknown };
497
648
  get: (pattern: string, handler: (req: unknown, res: unknown) => unknown) => { noAuth: () => unknown };
498
649
  }): void {
499
650
  const server = this;
500
- const msgPath = `${this.path}/message`;
501
- const ssePath = `${this.path}/sse`;
651
+ type Resp = ((data: unknown, status?: number, contentType?: string) => unknown) & {
652
+ addHeader?: (name: string, value: string) => unknown;
653
+ stream?: (source: AsyncIterable<string>, contentType?: string) => unknown;
654
+ };
655
+ const session = (req: { headers?: Record<string, string> }): string =>
656
+ (req.headers?.["mcp-session-id"] as string) || "";
657
+ const apply = (response: Resp, outcome: { status: number; headers: Record<string, string>; body: string }) => {
658
+ for (const [n, v] of Object.entries(outcome.headers)) response.addHeader?.(n, v);
659
+ if (!outcome.body) return response("", outcome.status);
660
+ return response(JSON.parse(outcome.body), outcome.status);
661
+ };
502
662
 
503
663
  router
504
- .post(msgPath, async (req: unknown, res: unknown) => {
505
- const request = req as { body: unknown; url?: string };
506
- const response = res as ((data: unknown, status?: number, contentType?: string) => unknown);
507
- const body = request.body;
508
- let raw: string | Record<string, unknown>;
509
- if (typeof body === "object" && body !== null) {
510
- raw = body as Record<string, unknown>;
511
- } else {
512
- raw = typeof body === "string" ? body : String(body);
513
- }
514
- const result = await server.handleMessage(raw);
515
- if (!result) {
516
- return response("", 204);
517
- }
518
- return response(JSON.parse(result));
664
+ .post(this.path, async (req: unknown, res: unknown) => {
665
+ const request = req as { body: unknown; headers?: Record<string, string> };
666
+ return apply(res as Resp, await server.dispatchHttp(server._normalizeBody(request.body), session(request)));
519
667
  })
520
668
  .noAuth();
521
669
 
522
670
  router
523
- .get(ssePath, (req: unknown, res: unknown) => {
524
- const request = req as { url?: string; headers?: Record<string, string> };
525
- const response = res as {
526
- header: (name: string, value: string) => unknown;
527
- send: (data: string, status?: number, contentType?: string) => unknown;
528
- };
529
- // Determine base URL for the endpoint
530
- const reqUrl = request.url || ssePath;
531
- const endpointUrl = reqUrl.replace(/\/sse$/, "/message");
532
- const sseData = `event: endpoint\ndata: ${endpointUrl}\n\n`;
533
- response.header("Content-Type", "text/event-stream");
534
- response.header("Cache-Control", "no-cache");
535
- response.header("Connection", "keep-alive");
536
- return response.send(sseData, 200, "text/event-stream");
671
+ .post(`${this.path}/message`, async (req: unknown, res: unknown) => {
672
+ const request = req as { body: unknown; headers?: Record<string, string>; query?: Record<string, string> };
673
+ const sessionId = (request.query?.sessionId as string) || session(request);
674
+ return apply(res as Resp, await server.dispatchSseMessage(server._normalizeBody(request.body), sessionId));
675
+ })
676
+ .noAuth();
677
+
678
+ router
679
+ .get(`${this.path}/sse`, (req: unknown, res: unknown) => {
680
+ const request = req as { path?: string };
681
+ const response = res as Resp;
682
+ const sessionId = server.openSession();
683
+ const base = (request.path || `${server.path}/sse`).replace(/\/sse$/, "");
684
+ return response.stream!(server.sseStream(sessionId, `${base}/message?sessionId=${sessionId}`));
537
685
  })
538
686
  .noAuth();
539
687
  }
@@ -563,7 +711,8 @@ export class McpServer {
563
711
 
564
712
  const serverKey = this.name.toLowerCase().replace(/ /g, "-");
565
713
  (config.mcpServers as Record<string, unknown>)[serverKey] = {
566
- url: `http://localhost:${port}${this.path}/sse`,
714
+ type: "http",
715
+ url: `http://localhost:${port}${this.path}`,
567
716
  };
568
717
 
569
718
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n", "utf-8");