diff --git a/cli/src/client/prompts.ts b/cli/src/client/prompts.ts index 0b237496..16f31115 100644 --- a/cli/src/client/prompts.ts +++ b/cli/src/client/prompts.ts @@ -2,9 +2,12 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { McpResponse } from "./types.js"; // List available prompts -export async function listPrompts(client: Client): Promise { +export async function listPrompts( + client: Client, + _meta?: Record, +): Promise { try { - const response = await client.listPrompts(); + const response = await client.listPrompts({ _meta }); return response; } catch (error) { throw new Error( @@ -18,11 +21,13 @@ export async function getPrompt( client: Client, name: string, args?: Record, + _meta?: Record, ): Promise { try { const response = await client.getPrompt({ name, arguments: args || {}, + _meta, }); return response; diff --git a/cli/src/client/resources.ts b/cli/src/client/resources.ts index bf33d64d..a35a1a9e 100644 --- a/cli/src/client/resources.ts +++ b/cli/src/client/resources.ts @@ -2,9 +2,12 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { McpResponse } from "./types.js"; // List available resources -export async function listResources(client: Client): Promise { +export async function listResources( + client: Client, + _meta?: Record, +): Promise { try { - const response = await client.listResources(); + const response = await client.listResources({ _meta }); return response; } catch (error) { throw new Error( @@ -17,9 +20,10 @@ export async function listResources(client: Client): Promise { export async function readResource( client: Client, uri: string, + _meta?: Record, ): Promise { try { - const response = await client.readResource({ uri }); + const response = await client.readResource({ uri, _meta }); return response; } catch (error) { throw new Error( @@ -31,9 +35,10 @@ export async function readResource( // List resource templates export async function listResourceTemplates( client: Client, + _meta?: Record, ): Promise { try { - const response = await client.listResourceTemplates(); + const response = await client.listResourceTemplates({ _meta }); return response; } catch (error) { throw new Error( diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts index acdb4871..305d49bf 100644 --- a/cli/src/client/tools.ts +++ b/cli/src/client/tools.ts @@ -9,9 +9,12 @@ type JsonSchemaType = { items?: JsonSchemaType; }; -export async function listTools(client: Client): Promise { +export async function listTools( + client: Client, + _meta?: Record, +): Promise { try { - const response = await client.listTools(); + const response = await client.listTools({ _meta }); return response; } catch (error) { throw new Error( @@ -69,9 +72,10 @@ export async function callTool( client: Client, name: string, args: Record, + _meta?: Record, ): Promise { try { - const toolsResponse = await listTools(client); + const toolsResponse = await listTools(client, _meta); const tools = toolsResponse.tools as Tool[]; const tool = tools.find((t) => t.name === name); @@ -85,6 +89,7 @@ export async function callTool( const response = await client.callTool({ name: name, arguments: convertedArgs, + _meta, }); return response; } catch (error) { diff --git a/client/src/App.tsx b/client/src/App.tsx index 26eb44c5..d2bfb4b5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -422,11 +422,14 @@ const App = () => { } }; - const listResources = async () => { + const listResources = async (meta?: Record | null) => { const response = await sendMCPRequest( { method: "resources/list" as const, - params: nextResourceCursor ? { cursor: nextResourceCursor } : {}, + params: { + ...(nextResourceCursor ? { cursor: nextResourceCursor } : {}), + ...(meta && { _meta: meta }), + }, }, ListResourcesResultSchema, "resources", @@ -435,13 +438,18 @@ const App = () => { setNextResourceCursor(response.nextCursor); }; - const listResourceTemplates = async () => { + const listResourceTemplates = async ( + meta?: Record | null, + ) => { const response = await sendMCPRequest( { method: "resources/templates/list" as const, - params: nextResourceTemplateCursor - ? { cursor: nextResourceTemplateCursor } - : {}, + params: { + ...(nextResourceTemplateCursor + ? { cursor: nextResourceTemplateCursor } + : {}), + ...(meta && { _meta: meta }), + }, }, ListResourceTemplatesResultSchema, "resources", @@ -452,11 +460,14 @@ const App = () => { setNextResourceTemplateCursor(response.nextCursor); }; - const readResource = async (uri: string) => { + const readResource = async ( + uri: string, + meta?: Record | null, + ) => { const response = await sendMCPRequest( { method: "resources/read" as const, - params: { uri }, + params: { uri, ...(meta && { _meta: meta }) }, }, ReadResourceResultSchema, "resources", @@ -464,12 +475,15 @@ const App = () => { setResourceContent(JSON.stringify(response, null, 2)); }; - const subscribeToResource = async (uri: string) => { + const subscribeToResource = async ( + uri: string, + meta?: Record | null, + ) => { if (!resourceSubscriptions.has(uri)) { await sendMCPRequest( { method: "resources/subscribe" as const, - params: { uri }, + params: { uri, ...(meta && { _meta: meta }) }, }, z.object({}), "resources", @@ -480,12 +494,15 @@ const App = () => { } }; - const unsubscribeFromResource = async (uri: string) => { + const unsubscribeFromResource = async ( + uri: string, + meta?: Record | null, + ) => { if (resourceSubscriptions.has(uri)) { await sendMCPRequest( { method: "resources/unsubscribe" as const, - params: { uri }, + params: { uri, ...(meta && { _meta: meta }) }, }, z.object({}), "resources", @@ -496,11 +513,14 @@ const App = () => { } }; - const listPrompts = async () => { + const listPrompts = async (meta?: Record | null) => { const response = await sendMCPRequest( { method: "prompts/list" as const, - params: nextPromptCursor ? { cursor: nextPromptCursor } : {}, + params: { + ...(nextPromptCursor ? { cursor: nextPromptCursor } : {}), + ...(meta && { _meta: meta }), + }, }, ListPromptsResultSchema, "prompts", @@ -509,11 +529,28 @@ const App = () => { setNextPromptCursor(response.nextCursor); }; - const getPrompt = async (name: string, args: Record = {}) => { + const getPrompt = async ( + name: string, + args: Record = {}, + meta?: Record | null, + ) => { + console.log("getPrompt _meta", meta); + const requestParams: { + name: string; + arguments: Record; + _meta?: Record; + } = { name, arguments: args }; + + // Simplified condition: if meta is a non-null object, add it. + // This covers both empty and non-empty objects. + if (meta && typeof meta === "object") { + requestParams._meta = meta; + } + const response = await sendMCPRequest( { method: "prompts/get" as const, - params: { name, arguments: args }, + params: requestParams, }, GetPromptResultSchema, "prompts", @@ -521,11 +558,14 @@ const App = () => { setPromptContent(JSON.stringify(response, null, 2)); }; - const listTools = async () => { + const listTools = async (meta?: Record | null) => { const response = await sendMCPRequest( { method: "tools/list" as const, - params: nextToolCursor ? { cursor: nextToolCursor } : {}, + params: { + ...(nextToolCursor ? { cursor: nextToolCursor } : {}), + ...(meta && { _meta: meta }), + }, }, ListToolsResultSchema, "tools", @@ -536,7 +576,11 @@ const App = () => { cacheToolOutputSchemas(response.tools); }; - const callTool = async (name: string, params: Record) => { + const callTool = async ( + name: string, + params: Record, + meta?: Record | null, + ) => { try { const response = await sendMCPRequest( { @@ -545,7 +589,8 @@ const App = () => { name, arguments: params, _meta: { - progressToken: progressTokenRef.current++, + ...(meta && meta), // Spread incoming meta + progressToken: progressTokenRef.current++, // Keep existing progressToken }, }, }, @@ -767,25 +812,29 @@ const App = () => { { + listResources={( + meta?: Record | null, + ) => { clearError("resources"); - listResources(); + listResources(meta); }} clearResources={() => { setResources([]); setNextResourceCursor(undefined); }} - listResourceTemplates={() => { + listResourceTemplates={( + meta?: Record | null, + ) => { clearError("resources"); - listResourceTemplates(); + listResourceTemplates(meta); }} clearResourceTemplates={() => { setResourceTemplates([]); setNextResourceTemplateCursor(undefined); }} - readResource={(uri) => { + readResource={(uri, meta) => { clearError("resources"); - readResource(uri); + readResource(uri, meta); }} selectedResource={selectedResource} setSelectedResource={(resource) => { @@ -821,9 +870,9 @@ const App = () => { setPrompts([]); setNextPromptCursor(undefined); }} - getPrompt={(name, args) => { + getPrompt={(name, args, meta) => { clearError("prompts"); - getPrompt(name, args); + getPrompt(name, args, meta); }} selectedPrompt={selectedPrompt} setSelectedPrompt={(prompt) => { @@ -841,7 +890,7 @@ const App = () => { tools={tools} listTools={() => { clearError("tools"); - listTools(); + listTools(); // TODO: Pass meta from ToolsTab if MetaEditor is added there }} clearTools={() => { setTools([]); @@ -849,10 +898,11 @@ const App = () => { // Clear cached output schemas cacheToolOutputSchemas([]); }} - callTool={async (name, params) => { + callTool={async (name, params, meta) => { + // Accept meta here clearError("tools"); setToolResult(null); - await callTool(name, params); + await callTool(name, params, meta); // Pass meta }} selectedTool={selectedTool} setSelectedTool={(tool) => { diff --git a/client/src/components/ListPane.tsx b/client/src/components/ListPane.tsx index 81cc196d..ff68bbd4 100644 --- a/client/src/components/ListPane.tsx +++ b/client/src/components/ListPane.tsx @@ -1,8 +1,10 @@ import { Button } from "./ui/button"; +import React, { useState } from "react"; // Import React and useState +import MetaEditor from "./MetaEditor"; // Import MetaEditor type ListPaneProps = { items: T[]; - listItems: () => void; + listItems: (meta?: Record | null) => void; // Updated signature clearItems: () => void; setSelectedItem: (item: T) => void; renderItem: (item: T) => React.ReactNode; @@ -20,41 +22,52 @@ const ListPane = ({ title, buttonText, isButtonDisabled, -}: ListPaneProps) => ( -
-
-

{title}

-
-
- - -
- {items.map((item, index) => ( -
setSelectedItem(item)} - > - {renderItem(item)} -
- ))} +}: ListPaneProps) => { + const [metaValue, setMetaValue] = useState | null>( + null, + ); + + return ( +
+
+

{title}

+
+
+ + + +
+ {items.map((item, index) => ( +
setSelectedItem(item)} + > + {renderItem(item)} +
+ ))} +
-
-); + ); +}; export default ListPane; diff --git a/client/src/components/MetaEditor.tsx b/client/src/components/MetaEditor.tsx new file mode 100644 index 00000000..89623e84 --- /dev/null +++ b/client/src/components/MetaEditor.tsx @@ -0,0 +1,86 @@ +import React, { useState, useEffect } from "react"; +import JsonEditor from "./JsonEditor"; +import { Button } from "./ui/button"; // Import Button +import { cn } from "@/lib/utils"; +import { Minus, Plus } from "lucide-react"; + +interface MetaEditorProps { + onChange: (value: Record | null) => void; + initialCollapsed?: boolean; + initialValue?: Record; // Default for the editor's content +} + +const MetaEditor: React.FC = ({ + onChange, + initialCollapsed = true, + initialValue = {}, // This ensures the editor has a default of {} +}) => { + const [isCollapsed, setIsCollapsed] = useState(initialCollapsed); + const [jsonString, setJsonString] = useState(() => { + try { + return JSON.stringify(initialValue, null, 2); + } catch { + return "{}"; + } + }); + const [parseError, setParseError] = useState(null); + + useEffect(() => { + if (isCollapsed) { + onChange(null); + } else { + try { + const parsedJson = JSON.parse(jsonString); + onChange(parsedJson); + setParseError(null); + } catch (e) { + onChange(null); + } + } + }, [isCollapsed, jsonString, onChange]); + + const handleToggleCollapse = () => { + setIsCollapsed((prevCollapsed) => !prevCollapsed); + // onChange will be handled by the useEffect above based on the new isCollapsed state + }; + + const handleEditorChange = (newJsonString: string) => { + console.log("change"); + setJsonString(newJsonString); + try { + JSON.parse(newJsonString); + setParseError(null); // Clear error if current input is valid + } catch (e) { + setParseError("Invalid JSON format."); // Set error for JsonEditor to display + } + }; + + return ( +
+ + {!isCollapsed && ( + + )} +
+ ); +}; + +export default MetaEditor; diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index 66c15d8f..dd95faf7 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -14,6 +14,7 @@ import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import { useCompletionState } from "@/lib/hooks/useCompletionState"; import JsonView from "./JsonView"; +import MetaEditor from "./MetaEditor"; // Import MetaEditor export type Prompt = { name: string; @@ -39,9 +40,13 @@ const PromptsTab = ({ error, }: { prompts: Prompt[]; - listPrompts: () => void; + listPrompts: (meta?: Record | null) => void; // Updated signature clearPrompts: () => void; - getPrompt: (name: string, args: Record) => void; + getPrompt: ( + name: string, + args: Record, + _meta: Record | null, + ) => void; // Updated signature selectedPrompt: Prompt | null; setSelectedPrompt: (prompt: Prompt | null) => void; handleCompletion: ( @@ -55,6 +60,9 @@ const PromptsTab = ({ error: string | null; }) => { const [promptArgs, setPromptArgs] = useState>({}); + const [metaValue, setMetaValue] = useState | null>( + null, + ); // State for MetaEditor const { completions, clearCompletions, requestCompletions } = useCompletionState(handleCompletion, completionsSupported); @@ -79,7 +87,8 @@ const PromptsTab = ({ const handleGetPrompt = () => { if (selectedPrompt) { - getPrompt(selectedPrompt.name, promptArgs); + console.log(metaValue); + getPrompt(selectedPrompt.name, promptArgs, metaValue); // Pass metaValue } }; @@ -154,6 +163,11 @@ const PromptsTab = ({ )}
))} + diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 8ec3f91b..d7279c2f 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -41,11 +41,11 @@ const ResourcesTab = ({ }: { resources: Resource[]; resourceTemplates: ResourceTemplate[]; - listResources: () => void; + listResources: (meta?: Record | null) => void; clearResources: () => void; - listResourceTemplates: () => void; + listResourceTemplates: (meta?: Record | null) => void; clearResourceTemplates: () => void; - readResource: (uri: string) => void; + readResource: (uri: string, meta?: Record) => void; selectedResource: Resource | null; setSelectedResource: (resource: Resource | null) => void; handleCompletion: ( diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 8a7f6578..172f1d4b 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -18,6 +18,7 @@ import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; import ToolResults from "./ToolResults"; +import MetaEditor from "./MetaEditor"; // Import MetaEditor const ToolsTab = ({ tools, @@ -30,9 +31,13 @@ const ToolsTab = ({ nextCursor, }: { tools: Tool[]; - listTools: () => void; + listTools: (meta?: Record | null) => void; // Updated signature clearTools: () => void; - callTool: (name: string, params: Record) => Promise; + callTool: ( + name: string, + params: Record, + meta: Record | null, + ) => Promise; // Updated signature selectedTool: Tool | null; setSelectedTool: (tool: Tool | null) => void; toolResult: CompatibilityCallToolResult | null; @@ -42,6 +47,9 @@ const ToolsTab = ({ const [params, setParams] = useState>({}); const [isToolRunning, setIsToolRunning] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); + const [metaValue, setMetaValue] = useState | null>( + null, + ); // State for MetaEditor useEffect(() => { const params = Object.entries( @@ -230,11 +238,16 @@ const ToolsTab = ({
)} +