Skip to content

Commit aa82c43

Browse files
mcp: tool results now properly rendered (#1988)
* mcp: tool results now properly rendered - Text content: <pre> + monospace (terminal-style) - Image content: base64 decoded as <img> Conforms to MCP spec 2025-06-18 * fix: wrap long lines in tool result text output * mcp: smaller monospace font for tool results
1 parent 6879250 commit aa82c43

File tree

1 file changed

+79
-10
lines changed

1 file changed

+79
-10
lines changed

src/lib/components/chat/ToolUpdate.svelte

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
let animation: Animation | undefined = $state(undefined);
3939
let showingLoadingBar = $state(false);
4040
41+
type ToolOutput = Record<string, unknown>;
42+
type McpImageContent = {
43+
type: "image";
44+
data: string;
45+
mimeType: string;
46+
};
47+
4148
const formatValue = (value: unknown): string => {
4249
if (value == null) return "";
4350
if (typeof value === "object") {
@@ -50,6 +57,47 @@
5057
return String(value);
5158
};
5259
60+
const getOutputText = (output: ToolOutput): string | undefined => {
61+
const maybeText = output["text"];
62+
if (typeof maybeText !== "string") return undefined;
63+
return maybeText;
64+
};
65+
66+
const isImageBlock = (value: unknown): value is McpImageContent => {
67+
if (typeof value !== "object" || value === null) return false;
68+
const obj = value as Record<string, unknown>;
69+
return (
70+
obj["type"] === "image" &&
71+
typeof obj["data"] === "string" &&
72+
typeof obj["mimeType"] === "string"
73+
);
74+
};
75+
76+
const getImageBlocks = (output: ToolOutput): McpImageContent[] => {
77+
const blocks = output["content"];
78+
if (!Array.isArray(blocks)) return [];
79+
return blocks.filter(isImageBlock);
80+
};
81+
82+
const getMetadataEntries = (output: ToolOutput): Array<[string, unknown]> => {
83+
return Object.entries(output).filter(
84+
([key, value]) => value != null && key !== "content" && key !== "text"
85+
);
86+
};
87+
88+
interface ParsedToolOutput {
89+
text?: string;
90+
images: McpImageContent[];
91+
metadata: Array<[string, unknown]>;
92+
}
93+
94+
const parseToolOutputs = (outputs: ToolOutput[]): ParsedToolOutput[] =>
95+
outputs.map((output) => ({
96+
text: getOutputText(output),
97+
images: getImageBlocks(output),
98+
metadata: getMetadataEntries(output),
99+
}));
100+
53101
$effect(() => {
54102
if (!toolError && !toolDone && loading && loadingBarEl && eta) {
55103
loadingBarEl.classList.remove("hidden");
@@ -201,18 +249,39 @@
201249
<h3 class="text-sm">Result</h3>
202250
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
203251
</div>
204-
<ul class="py-1 text-sm">
205-
{#each update.result.outputs as output}
206-
{#each Object.entries(output) as [key, value]}
207-
{#if value != null && key !== "content"}
208-
<li>
209-
<span class="font-semibold">{key}</span>:
210-
<span class="whitespace-pre-wrap">{formatValue(value)}</span>
211-
</li>
252+
<div class="py-1 text-sm">
253+
{#each parseToolOutputs(update.result.outputs) as parsedOutput}
254+
<div class="space-y-2 py-2 first:pt-0 last:pb-0">
255+
{#if parsedOutput.text}
256+
<!-- prettier-ignore -->
257+
<pre class="whitespace-pre-wrap break-all font-mono text-xs">{parsedOutput.text}</pre>
258+
{/if}
259+
260+
{#if parsedOutput.images.length > 0}
261+
<div class="flex flex-wrap gap-2">
262+
{#each parsedOutput.images as image, imageIndex}
263+
<img
264+
alt={`Tool result image ${imageIndex + 1}`}
265+
class="max-h-60 rounded border border-gray-200 dark:border-gray-800"
266+
src={`data:${image.mimeType};base64,${image.data}`}
267+
/>
268+
{/each}
269+
</div>
270+
{/if}
271+
272+
{#if parsedOutput.metadata.length > 0}
273+
<ul class="space-y-1">
274+
{#each parsedOutput.metadata as [key, value]}
275+
<li>
276+
<span class="font-semibold">{key}</span>:
277+
<span class="whitespace-pre-wrap">{formatValue(value)}</span>
278+
</li>
279+
{/each}
280+
</ul>
212281
{/if}
213-
{/each}
282+
</div>
214283
{/each}
215-
</ul>
284+
</div>
216285
{:else if isMessageToolResultUpdate(update) && update.result.status === ToolResultStatus.Error && update.result.display}
217286
<div class="mt-1 flex items-center gap-2 opacity-80">
218287
<h3 class="text-sm text-red-600 dark:text-red-400">Error</h3>

0 commit comments

Comments
 (0)