Support structured tool results

This commit is contained in:
Justin Spahr-Summers
2024-11-07 15:17:18 +00:00
parent f3406ca43d
commit 193032533b
2 changed files with 57 additions and 16 deletions

View File

@@ -1,7 +1,7 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { import {
CallToolResultSchema, CompatibilityCallToolResultSchema,
ClientRequest, ClientRequest,
CreateMessageRequestSchema, CreateMessageRequestSchema,
CreateMessageResult, CreateMessageResult,
@@ -19,6 +19,7 @@ import {
Root, Root,
ServerNotification, ServerNotification,
Tool, Tool,
CompatibilityCallToolResult,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
@@ -44,7 +45,7 @@ import {
FolderTree, FolderTree,
} from "lucide-react"; } from "lucide-react";
import { AnyZodObject } from "zod"; import { ZodType } from "zod";
import "./App.css"; import "./App.css";
import ConsoleTab from "./components/ConsoleTab"; import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History"; import HistoryAndNotifications from "./components/History";
@@ -69,7 +70,8 @@ const App = () => {
const [prompts, setPrompts] = useState<Prompt[]>([]); const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>(""); const [promptContent, setPromptContent] = useState<string>("");
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);
const [toolResult, setToolResult] = useState<string>(""); const [toolResult, setToolResult] =
useState<CompatibilityCallToolResult | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [command, setCommand] = useState<string>(() => { const [command, setCommand] = useState<string>(() => {
return ( return (
@@ -150,7 +152,7 @@ const App = () => {
]); ]);
}; };
const makeRequest = async <T extends AnyZodObject>( const makeRequest = async <T extends ZodType<object>>(
request: ClientRequest, request: ClientRequest,
schema: T, schema: T,
) => { ) => {
@@ -254,9 +256,9 @@ const App = () => {
}, },
}, },
}, },
CallToolResultSchema, CompatibilityCallToolResultSchema,
); );
setToolResult(JSON.stringify(response.toolResult, null, 2)); setToolResult(response);
}; };
const handleRootsChange = async () => { const handleRootsChange = async () => {
@@ -444,7 +446,7 @@ const App = () => {
selectedTool={selectedTool} selectedTool={selectedTool}
setSelectedTool={(tool) => { setSelectedTool={(tool) => {
setSelectedTool(tool); setSelectedTool(tool);
setToolResult(""); setToolResult(null);
}} }}
toolResult={toolResult} toolResult={toolResult}
nextCursor={nextToolCursor} nextCursor={nextToolCursor}

View File

@@ -8,6 +8,8 @@ import { AlertCircle, Send } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import ListPane from "./ListPane"; import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
const ToolsTab = ({ const ToolsTab = ({
tools, tools,
listTools, listTools,
@@ -23,12 +25,56 @@ const ToolsTab = ({
callTool: (name: string, params: Record<string, unknown>) => void; callTool: (name: string, params: Record<string, unknown>) => void;
selectedTool: Tool | null; selectedTool: Tool | null;
setSelectedTool: (tool: Tool) => void; setSelectedTool: (tool: Tool) => void;
toolResult: string; toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"]; nextCursor: ListToolsResult["nextCursor"];
error: string | null; error: string | null;
}) => { }) => {
const [params, setParams] = useState<Record<string, unknown>>({}); const [params, setParams] = useState<Record<string, unknown>>({});
const renderToolResult = () => {
if (!toolResult) return null;
if ("content" in toolResult) {
return (
<>
<h4 className="font-semibold mb-2">
Tool Result: {toolResult.isError ? "Error" : "Success"}
</h4>
{toolResult.content.map((item, index) => (
<div key={index} className="mb-2">
{item.type === "text" && (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{item.text}
</pre>
)}
{item.type === "image" && (
<img
src={`data:${item.mimeType};base64,${item.data}`}
alt="Tool result image"
className="max-w-full h-auto"
/>
)}
{item.type === "resource" && (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(item.resource, null, 2)}
</pre>
)}
</div>
))}
</>
);
} else if ("toolResult" in toolResult) {
return (
<>
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(toolResult.toolResult, null, 2)}
</pre>
</>
);
}
};
return ( return (
<TabsContent value="tools" className="grid grid-cols-2 gap-4"> <TabsContent value="tools" className="grid grid-cols-2 gap-4">
<ListPane <ListPane
@@ -100,14 +146,7 @@ const ToolsTab = ({
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
Run Tool Run Tool
</Button> </Button>
{toolResult && ( {toolResult && renderToolResult()}
<>
<h4 className="font-semibold mb-2">Tool Result:</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{toolResult}
</pre>
</>
)}
</div> </div>
) : ( ) : (
<Alert> <Alert>