From 770f99308695196d0d7086390169e3b88a1dd0ca Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 27 Jan 2026 11:56:02 +0100 Subject: [PATCH] feat: Implement clipboard serialization/deserialization for MCP prompts --- tools/server/webui/src/lib/utils/clipboard.ts | 91 ++++++++++++++++--- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/tools/server/webui/src/lib/utils/clipboard.ts b/tools/server/webui/src/lib/utils/clipboard.ts index 79b4ee2da8..7c3437cac5 100644 --- a/tools/server/webui/src/lib/utils/clipboard.ts +++ b/tools/server/webui/src/lib/utils/clipboard.ts @@ -104,11 +104,18 @@ export function formatMessageForClipboard( extras?: DatabaseMessageExtra[], asPlainText: boolean = false ): string { - // Filter only text attachments (TEXT type and legacy CONTEXT type) + // Filter text-like attachments (TEXT, LEGACY_CONTEXT, and MCP_PROMPT types) const textAttachments = extras?.filter( - (extra): extra is DatabaseMessageExtraTextFile | DatabaseMessageExtraLegacyContext => - extra.type === AttachmentType.TEXT || extra.type === AttachmentType.LEGACY_CONTEXT + ( + extra + ): extra is + | DatabaseMessageExtraTextFile + | DatabaseMessageExtraLegacyContext + | DatabaseMessageExtraMcpPrompt => + extra.type === AttachmentType.TEXT || + extra.type === AttachmentType.LEGACY_CONTEXT || + extra.type === AttachmentType.MCP_PROMPT ) ?? []; if (textAttachments.length === 0) { @@ -123,11 +130,24 @@ export function formatMessageForClipboard( return parts.join('\n\n'); } - const clipboardAttachments: ClipboardTextAttachment[] = textAttachments.map((att) => ({ - type: AttachmentType.TEXT, - name: att.name, - content: att.content - })); + const clipboardAttachments: ClipboardAttachment[] = textAttachments.map((att) => { + if (att.type === AttachmentType.MCP_PROMPT) { + const mcpAtt = att as DatabaseMessageExtraMcpPrompt; + return { + type: AttachmentType.MCP_PROMPT, + name: mcpAtt.name, + serverName: mcpAtt.serverName, + promptName: mcpAtt.promptName, + content: mcpAtt.content, + arguments: mcpAtt.arguments + } as ClipboardMcpPromptAttachment; + } + return { + type: AttachmentType.TEXT, + name: att.name, + content: att.content + } as ClipboardTextAttachment; + }); return `${JSON.stringify(content)}\n${JSON.stringify(clipboardAttachments, null, 2)}`; } @@ -142,7 +162,8 @@ export function formatMessageForClipboard( export function parseClipboardContent(clipboardText: string): ParsedClipboardContent { const defaultResult: ParsedClipboardContent = { message: clipboardText, - textAttachments: [] + textAttachments: [], + mcpPromptAttachments: [] }; if (!clipboardText.startsWith('"')) { @@ -184,17 +205,28 @@ export function parseClipboardContent(clipboardText: string): ParsedClipboardCon if (!remainingPart || !remainingPart.startsWith('[')) { return { message, - textAttachments: [] + textAttachments: [], + mcpPromptAttachments: [] }; } const attachments = JSON.parse(remainingPart) as unknown[]; - const validAttachments: ClipboardTextAttachment[] = []; + const validTextAttachments: ClipboardTextAttachment[] = []; + const validMcpPromptAttachments: ClipboardMcpPromptAttachment[] = []; for (const att of attachments) { - if (isValidTextAttachment(att)) { - validAttachments.push({ + if (isValidMcpPromptAttachment(att)) { + validMcpPromptAttachments.push({ + type: AttachmentType.MCP_PROMPT, + name: att.name, + serverName: att.serverName, + promptName: att.promptName, + content: att.content, + arguments: att.arguments + }); + } else if (isValidTextAttachment(att)) { + validTextAttachments.push({ type: AttachmentType.TEXT, name: att.name, content: att.content @@ -204,13 +236,42 @@ export function parseClipboardContent(clipboardText: string): ParsedClipboardCon return { message, - textAttachments: validAttachments + textAttachments: validTextAttachments, + mcpPromptAttachments: validMcpPromptAttachments }; } catch { return defaultResult; } } +/** + * Type guard to validate an MCP prompt attachment object + * @param obj The object to validate + * @returns true if the object is a valid MCP prompt attachment + */ +function isValidMcpPromptAttachment(obj: unknown): obj is { + type: string; + name: string; + serverName: string; + promptName: string; + content: string; + arguments?: Record; +} { + if (typeof obj !== 'object' || obj === null) { + return false; + } + + const record = obj as Record; + + return ( + (record.type === AttachmentType.MCP_PROMPT || record.type === 'MCP_PROMPT') && + typeof record.name === 'string' && + typeof record.serverName === 'string' && + typeof record.promptName === 'string' && + typeof record.content === 'string' + ); +} + /** * Type guard to validate a text attachment object * @param obj The object to validate @@ -243,5 +304,5 @@ export function hasClipboardAttachments(clipboardText: string): boolean { } const parsed = parseClipboardContent(clipboardText); - return parsed.textAttachments.length > 0; + return parsed.textAttachments.length > 0 || parsed.mcpPromptAttachments.length > 0; }