penpot/mcp/packages/server/src/ToolResponse.ts
Dr. Dominik Jain 6031f6c318
⬆️ Upgrade MCP SDK to v2, removing SSE support and HTTP session management (#11841)
* ⬆️ Upgrade MCP SDK to v2 and remove HTTP sessions

MCP's per-request protocol removes the need to retain HTTP sessions.
Use the v2 handler to manage each request's transport and lifecycle,
so requests can reach any server instance without session affinity
or the workaround that adopts sessions through private SDK fields.

Keep legacy SSE support and the shared plugin and Redis bridges.
Remove the shared expiry checker, including legacy SSE idle expiry;
SSE connections now remain until disconnection or server shutdown.
Verify stateless requests, token isolation, and legacy compatibility.

Resolves #11827

AI-assisted-by: gpt-6-astra

* 🔥 Remove legacy MCP SSE support

Use Streamable HTTP as the sole MCP client transport so the server no
longer needs a separate SSE connection registry or lifecycle.

Remove /sse and /messages, their nginx routes, and the server-legacy
dependency. Legacy SSE clients must switch to /mcp; older Streamable
HTTP clients remain supported. Document the migration and verify that
the removed endpoints return 404.

Resolves #11846

AI-assisted-by: gpt-6-astra
2026-09-23 15:07:19 +02:00

105 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { CallToolResult } from "@modelcontextprotocol/server";
type CallToolContent = CallToolResult["content"][number];
type TextItem = Extract<CallToolContent, { type: "text" }>;
type ImageItem = Extract<CallToolContent, { type: "image" }>;
export class TextContent implements TextItem {
[x: string]: unknown;
readonly type = "text" as const;
constructor(public text: string) {}
/**
* @param data - Text data as string or as object (from JSON representation where indices are mapped to character codes)
*/
public static textData(data: string | object): string {
if (typeof data === "object") {
// convert object containing character codes (as obtained from JSON conversion of string) back to string
return String.fromCharCode(...(Object.values(data) as number[]));
} else {
return data;
}
}
}
export class ImageContent implements ImageItem {
[x: string]: unknown;
readonly type = "image" as const;
/**
* @param data - Base64-encoded image data
* @param mimeType - MIME type of the image (e.g., "image/png")
*/
constructor(
public data: string,
public mimeType: string
) {}
/**
* Utility function for ensuring a consistent Uint8Array representation of byte data.
* Input can be one of:
* - a `Uint8Array` (already in the desired form);
* - a base64 envelope `{ __type: "base64", data: <base64 string> }` produced by the plugin
* to avoid the ~10x JSON expansion of typed arrays (see penpot/penpot#9420);
* - a numeric-keyed object obtained from `JSON.stringify`-ing a `Uint8Array` (legacy fallback).
*
* @param data - data as `Uint8Array`, base64 envelope, or numeric-keyed object
* @return data as `Uint8Array`
*/
public static byteData(data: Uint8Array | object): Uint8Array {
if (data instanceof Uint8Array) {
return data;
}
// recognize the base64 envelope produced by the plugin's ExecuteCodeTaskHandler
const envelope = data as { __type?: unknown; data?: unknown };
if (envelope.__type === "base64" && typeof envelope.data === "string") {
return new Uint8Array(Buffer.from(envelope.data, "base64"));
}
// legacy fallback: object (as obtained from JSON conversion of Uint8Array) back to Uint8Array
return new Uint8Array(Object.values(data) as number[]);
}
}
export class PNGImageContent extends ImageContent {
/**
* @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array)
*/
constructor(data: Uint8Array | object) {
let array = ImageContent.byteData(data);
super(Buffer.from(array).toString("base64"), "image/png");
}
}
export class ToolResponse implements CallToolResult {
[x: string]: unknown;
content: CallToolContent[]; // <- IMPORTANT: protocol’s union
constructor(content: CallToolContent[]) {
this.content = content;
}
}
export class TextResponse extends ToolResponse {
constructor(text: string) {
super([new TextContent(text)]);
}
/**
* Creates a TextResponse from text data given as string or as object (from JSON representation where indices are mapped to
* character codes).
*
* @param data - Text data as string or as object (from JSON representation where indices are mapped to character codes)
*/
public static fromData(data: string | object): TextResponse {
return new TextResponse(TextContent.textData(data));
}
}
export class PNGResponse extends ToolResponse {
/**
* @param data - PNG image data as Uint8Array or as object (from JSON conversion of Uint8Array)
*/
constructor(data: Uint8Array | object) {
super([new PNGImageContent(data)]);
}
}