File size: 3,149 Bytes
3d97d52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
 * Vendored from `@huggingface/mcp-client`
 *
 * https://github.com/huggingface/huggingface.js/blob/main/packages/mcp-client/src/ResultFormatter.ts
 */

import type {
	TextResourceContents,
	BlobResourceContents,
	CompatibilityCallToolResult,
} from "@modelcontextprotocol/sdk/types";

/**
 * A utility class for formatting CallToolResult contents into human-readable text.
 * Processes different content types, extracts text, and summarizes binary data.
 */
export class McpResultFormatter {
	/**
	 * Formats a CallToolResult's contents into a single string.
	 * - Text content is included directly
	 * - Binary content (images, audio, blobs) is summarized
	 *
	 * @param result The CallToolResult to format
	 * @returns A human-readable string representation of the result contents
	 */
	static format(result: CompatibilityCallToolResult): string {
		if (!result.content || !Array.isArray(result.content) || result.content.length === 0) {
			return "[No content]";
		}

		const formattedParts: string[] = [];

		for (const item of result.content) {
			switch (item.type) {
				case "text":
					// Extract text content directly
					formattedParts.push(item.text);
					break;

				case "image": {
					// Summarize image content
					const imageSize = this.getBase64Size(item.data);
					formattedParts.push(
						`[Binary Content: Image ${item.mimeType}, ${imageSize} bytes]\nThe task is complete and the content accessible to the User`
					);
					break;
				}

				case "audio": {
					// Summarize audio content
					const audioSize = this.getBase64Size(item.data);
					formattedParts.push(
						`[Binary Content: Audio ${item.mimeType}, ${audioSize} bytes]\nThe task is complete and the content accessible to the User`
					);
					break;
				}

				case "resource":
					// Handle embedded resources - explicitly type the resource
					if ("text" in item.resource) {
						// It's a text resource with a text property
						const textResource = item.resource as TextResourceContents;
						formattedParts.push(textResource.text);
					} else if ("blob" in item.resource) {
						// It's a binary resource with a blob property
						const blobResource = item.resource as BlobResourceContents;
						const blobSize = this.getBase64Size(blobResource.blob);
						const uri = blobResource.uri ? ` (${blobResource.uri})` : "";
						const mimeType = blobResource.mimeType ? blobResource.mimeType : "unknown type";
						formattedParts.push(
							`[Binary Content${uri}: ${mimeType}, ${blobSize} bytes]\nThe task is complete and the content accessible to the User`
						);
					}
					break;
			}
		}

		return formattedParts.join("\n");
	}

	/**
	 * Calculates the approximate size in bytes of base64-encoded data
	 */
	private static getBase64Size(base64: string): number {
		// Remove base64 header if present (e.g., data:image/png;base64,)
		const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64;

		// Calculate size: Base64 encodes 3 bytes into 4 characters
		const padding = cleanBase64.endsWith("==") ? 2 : cleanBase64.endsWith("=") ? 1 : 0;
		return Math.floor((cleanBase64.length * 3) / 4 - padding);
	}
}