mirror of
https://github.com/penpot/penpot.git
synced 2026-09-26 05:46:13 +00:00
* ⬆️ 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
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
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)]);
|
||
}
|
||
}
|