Spaces:
Paused
Paused
| import React from "react"; | |
| import { ColumnDef } from "@tanstack/react-table"; | |
| import { MCPTool, InputSchema } from "./types"; | |
| import { Button } from "@tremor/react" | |
| export const columns: ColumnDef<MCPTool>[] = [ | |
| { | |
| accessorKey: "mcp_info.server_name", | |
| header: "Provider", | |
| cell: ({ row }) => { | |
| const serverName = row.original.mcp_info.server_name; | |
| const logoUrl = row.original.mcp_info.logo_url; | |
| return ( | |
| <div className="flex items-center space-x-2"> | |
| {logoUrl && ( | |
| <img | |
| src={logoUrl} | |
| alt={`${serverName} logo`} | |
| className="h-5 w-5 object-contain" | |
| /> | |
| )} | |
| <span className="font-medium">{serverName}</span> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| { | |
| accessorKey: "name", | |
| header: "Tool Name", | |
| cell: ({ row }) => { | |
| const name = row.getValue("name") as string; | |
| return ( | |
| <div> | |
| <span className="font-mono text-sm">{name}</span> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| { | |
| accessorKey: "description", | |
| header: "Description", | |
| cell: ({ row }) => { | |
| const description = row.getValue("description") as string; | |
| return ( | |
| <div className="max-w-md"> | |
| <span className="text-sm text-gray-700">{description}</span> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| { | |
| id: "actions", | |
| header: "Actions", | |
| cell: ({ row }) => { | |
| const tool = row.original; | |
| return ( | |
| <div className="flex items-center space-x-2"> | |
| <Button | |
| size="xs" | |
| variant="light" | |
| className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]" | |
| onClick={() => { | |
| if (typeof row.original.onToolSelect === 'function') { | |
| row.original.onToolSelect(tool); | |
| } | |
| }} | |
| > | |
| Test Tool | |
| </Button> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| ]; | |
| // Tool Panel component to display when a tool is selected | |
| export function ToolTestPanel({ | |
| tool, | |
| onSubmit, | |
| isLoading, | |
| result, | |
| error, | |
| onClose | |
| }: { | |
| tool: MCPTool; | |
| onSubmit: (args: Record<string, any>) => void; | |
| isLoading: boolean; | |
| result: any | null; | |
| error: Error | null; | |
| onClose: () => void; | |
| }) { | |
| const [formState, setFormState] = React.useState<Record<string, any>>({}); | |
| // Create a placeholder schema if we only have the "tool_input_schema" string | |
| const schema: InputSchema = React.useMemo(() => { | |
| if (typeof tool.inputSchema === 'string') { | |
| // Default schema with a single text field | |
| return { | |
| type: "object", | |
| properties: { | |
| input: { | |
| type: "string", | |
| description: "Input for this tool" | |
| } | |
| }, | |
| required: ["input"] | |
| }; | |
| } | |
| return tool.inputSchema as InputSchema; | |
| }, [tool.inputSchema]); | |
| const handleInputChange = (key: string, value: any) => { | |
| setFormState(prev => ({ | |
| ...prev, | |
| [key]: value | |
| })); | |
| }; | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| onSubmit(formState); | |
| }; | |
| return ( | |
| <div className="bg-white rounded-lg shadow-lg border p-6 max-w-4xl w-full"> | |
| <div className="flex justify-between items-start mb-4"> | |
| <div> | |
| <h2 className="text-xl font-bold">Test Tool: <span className="font-mono">{tool.name}</span></h2> | |
| <p className="text-gray-600">{tool.description}</p> | |
| <p className="text-sm text-gray-500 mt-1">Provider: {tool.mcp_info.server_name}</p> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="p-1 rounded-full hover:bg-gray-200" | |
| > | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| width="20" | |
| height="20" | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| stroke="currentColor" | |
| strokeWidth="2" | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| > | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {/* Form Section */} | |
| <div className="bg-gray-50 p-4 rounded-lg"> | |
| <h3 className="font-medium mb-4">Input Parameters</h3> | |
| <form onSubmit={handleSubmit}> | |
| {typeof tool.inputSchema === 'string' ? ( | |
| <div className="mb-4"> | |
| <p className="text-xs text-gray-500 mb-1">This tool uses a dynamic input schema.</p> | |
| <div className="mb-4"> | |
| <label className="block text-sm font-medium text-gray-700 mb-1"> | |
| Input <span className="text-red-500">*</span> | |
| </label> | |
| <input | |
| type="text" | |
| value={formState.input || ""} | |
| onChange={(e) => handleInputChange("input", e.target.value)} | |
| required | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" | |
| /> | |
| </div> | |
| </div> | |
| ) : ( | |
| Object.entries(schema.properties).map(([key, prop]) => ( | |
| <div key={key} className="mb-4"> | |
| <label className="block text-sm font-medium text-gray-700 mb-1"> | |
| {key}{" "} | |
| {schema.required?.includes(key) && ( | |
| <span className="text-red-500">*</span> | |
| )} | |
| </label> | |
| {prop.description && ( | |
| <p className="text-xs text-gray-500 mb-1">{prop.description}</p> | |
| )} | |
| {/* Render appropriate input based on type */} | |
| {prop.type === "string" && ( | |
| <input | |
| type="text" | |
| value={formState[key] || ""} | |
| onChange={(e) => handleInputChange(key, e.target.value)} | |
| required={schema.required?.includes(key)} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" | |
| /> | |
| )} | |
| {prop.type === "number" && ( | |
| <input | |
| type="number" | |
| value={formState[key] || ""} | |
| onChange={(e) => handleInputChange(key, parseFloat(e.target.value))} | |
| required={schema.required?.includes(key)} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" | |
| /> | |
| )} | |
| {prop.type === "boolean" && ( | |
| <div className="flex items-center"> | |
| <input | |
| type="checkbox" | |
| checked={formState[key] || false} | |
| onChange={(e) => handleInputChange(key, e.target.checked)} | |
| className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" | |
| /> | |
| <span className="ml-2 text-sm text-gray-600">Enable</span> | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| <div className="mt-6"> | |
| <Button | |
| type="submit" | |
| disabled={isLoading} | |
| className="w-full px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white" | |
| > | |
| {isLoading ? "Calling..." : "Call Tool"} | |
| </Button> | |
| </div> | |
| </form> | |
| </div> | |
| {/* Result Section */} | |
| <div className="bg-gray-50 p-4 rounded-lg overflow-auto max-h-[500px]"> | |
| <h3 className="font-medium mb-4">Result</h3> | |
| {isLoading && ( | |
| <div className="flex justify-center items-center py-8"> | |
| <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-700"></div> | |
| </div> | |
| )} | |
| {error && ( | |
| <div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-md"> | |
| <p className="font-medium">Error</p> | |
| <pre className="mt-2 text-xs overflow-auto whitespace-pre-wrap">{error.message}</pre> | |
| </div> | |
| )} | |
| {result && !isLoading && !error && ( | |
| <div> | |
| {result.map((content: any, idx: number) => ( | |
| <div key={idx} className="mb-4"> | |
| {content.type === "text" && ( | |
| <div className="bg-white border p-3 rounded-md"> | |
| <p className="whitespace-pre-wrap text-sm">{content.text}</p> | |
| </div> | |
| )} | |
| {content.type === "image" && content.url && ( | |
| <div className="bg-white border p-3 rounded-md"> | |
| <img src={content.url} alt="Tool result" className="max-w-full h-auto rounded" /> | |
| </div> | |
| )} | |
| {content.type === "embedded_resource" && ( | |
| <div className="bg-white border p-3 rounded-md"> | |
| <p className="text-sm font-medium">Embedded Resource</p> | |
| <p className="text-xs text-gray-500">Type: {content.resource_type}</p> | |
| {content.url && ( | |
| <a | |
| href={content.url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-sm text-blue-600 hover:underline" | |
| > | |
| View Resource | |
| </a> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| <div className="mt-2"> | |
| <details className="text-xs"> | |
| <summary className="cursor-pointer text-gray-500 hover:text-gray-700">Raw JSON Response</summary> | |
| <pre className="mt-2 bg-gray-100 p-2 rounded-md overflow-auto max-h-[300px]"> | |
| {JSON.stringify(result, null, 2)} | |
| </pre> | |
| </details> | |
| </div> | |
| </div> | |
| )} | |
| {!result && !isLoading && !error && ( | |
| <div className="text-center py-8 text-gray-500"> | |
| <p>The result will appear here after you call the tool.</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |