feat: Implement clipboard serialization/deserialization for MCP prompts

This commit is contained in:
Aleksander Grygier 2026-01-27 11:56:02 +01:00
parent ed7f18ac30
commit 9f5bcf0b02
1 changed files with 76 additions and 15 deletions

View File

@ -104,11 +104,18 @@ export function formatMessageForClipboard(
extras?: DatabaseMessageExtra[], extras?: DatabaseMessageExtra[],
asPlainText: boolean = false asPlainText: boolean = false
): string { ): string {
// Filter only text attachments (TEXT type and legacy CONTEXT type) // Filter text-like attachments (TEXT, LEGACY_CONTEXT, and MCP_PROMPT types)
const textAttachments = const textAttachments =
extras?.filter( 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) { if (textAttachments.length === 0) {
@ -123,11 +130,24 @@ export function formatMessageForClipboard(
return parts.join('\n\n'); return parts.join('\n\n');
} }
const clipboardAttachments: ClipboardTextAttachment[] = textAttachments.map((att) => ({ const clipboardAttachments: ClipboardAttachment[] = textAttachments.map((att) => {
type: AttachmentType.TEXT, if (att.type === AttachmentType.MCP_PROMPT) {
name: att.name, const mcpAtt = att as DatabaseMessageExtraMcpPrompt;
content: att.content 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)}`; return `${JSON.stringify(content)}\n${JSON.stringify(clipboardAttachments, null, 2)}`;
} }
@ -142,7 +162,8 @@ export function formatMessageForClipboard(
export function parseClipboardContent(clipboardText: string): ParsedClipboardContent { export function parseClipboardContent(clipboardText: string): ParsedClipboardContent {
const defaultResult: ParsedClipboardContent = { const defaultResult: ParsedClipboardContent = {
message: clipboardText, message: clipboardText,
textAttachments: [] textAttachments: [],
mcpPromptAttachments: []
}; };
if (!clipboardText.startsWith('"')) { if (!clipboardText.startsWith('"')) {
@ -184,17 +205,28 @@ export function parseClipboardContent(clipboardText: string): ParsedClipboardCon
if (!remainingPart || !remainingPart.startsWith('[')) { if (!remainingPart || !remainingPart.startsWith('[')) {
return { return {
message, message,
textAttachments: [] textAttachments: [],
mcpPromptAttachments: []
}; };
} }
const attachments = JSON.parse(remainingPart) as unknown[]; const attachments = JSON.parse(remainingPart) as unknown[];
const validAttachments: ClipboardTextAttachment[] = []; const validTextAttachments: ClipboardTextAttachment[] = [];
const validMcpPromptAttachments: ClipboardMcpPromptAttachment[] = [];
for (const att of attachments) { for (const att of attachments) {
if (isValidTextAttachment(att)) { if (isValidMcpPromptAttachment(att)) {
validAttachments.push({ 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, type: AttachmentType.TEXT,
name: att.name, name: att.name,
content: att.content content: att.content
@ -204,13 +236,42 @@ export function parseClipboardContent(clipboardText: string): ParsedClipboardCon
return { return {
message, message,
textAttachments: validAttachments textAttachments: validTextAttachments,
mcpPromptAttachments: validMcpPromptAttachments
}; };
} catch { } catch {
return defaultResult; 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<string, string>;
} {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const record = obj as Record<string, unknown>;
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 * Type guard to validate a text attachment object
* @param obj The object to validate * @param obj The object to validate
@ -243,5 +304,5 @@ export function hasClipboardAttachments(clipboardText: string): boolean {
} }
const parsed = parseClipboardContent(clipboardText); const parsed = parseClipboardContent(clipboardText);
return parsed.textAttachments.length > 0; return parsed.textAttachments.length > 0 || parsed.mcpPromptAttachments.length > 0;
} }