diff --git a/cli/src/client/prompts.ts b/cli/src/client/prompts.ts index 870146178..45e269f27 100644 --- a/cli/src/client/prompts.ts +++ b/cli/src/client/prompts.ts @@ -12,9 +12,14 @@ type JsonValue = | { [key: string]: JsonValue }; // List available prompts -export async function listPrompts(client: Client): Promise { +export async function listPrompts( + client: Client, + metaData?: Record, +): Promise { try { - const response = await client.listPrompts(); + const params = + metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {}; + const response = await client.listPrompts(params); return response; } catch (error) { throw new Error( @@ -28,6 +33,7 @@ export async function getPrompt( client: Client, name: string, args?: Record, + metaData?: Record, ): Promise { try { // Convert all arguments to strings for prompt arguments @@ -44,10 +50,16 @@ export async function getPrompt( } } - const response = await client.getPrompt({ + const params: any = { name, arguments: stringArgs, - }); + }; + + if (metaData && Object.keys(metaData).length > 0) { + params._meta = metaData; + } + + const response = await client.getPrompt(params); return response; } catch (error) { diff --git a/cli/src/client/resources.ts b/cli/src/client/resources.ts index bf33d64d2..d530bbc27 100644 --- a/cli/src/client/resources.ts +++ b/cli/src/client/resources.ts @@ -2,9 +2,14 @@ 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, + metaData?: Record, +): Promise { try { - const response = await client.listResources(); + const params = + metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {}; + const response = await client.listResources(params); return response; } catch (error) { throw new Error( @@ -17,9 +22,14 @@ export async function listResources(client: Client): Promise { export async function readResource( client: Client, uri: string, + metaData?: Record, ): Promise { try { - const response = await client.readResource({ uri }); + const params: any = { uri }; + if (metaData && Object.keys(metaData).length > 0) { + params._meta = metaData; + } + const response = await client.readResource(params); return response; } catch (error) { throw new Error( @@ -31,9 +41,12 @@ export async function readResource( // List resource templates export async function listResourceTemplates( client: Client, + metaData?: Record, ): Promise { try { - const response = await client.listResourceTemplates(); + const params = + metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {}; + const response = await client.listResourceTemplates(params); return response; } catch (error) { throw new Error( diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts index 9da0b4d9f..df2634a7e 100644 --- a/cli/src/client/tools.ts +++ b/cli/src/client/tools.ts @@ -19,9 +19,14 @@ type JsonSchemaType = { items?: JsonSchemaType; }; -export async function listTools(client: Client): Promise { +export async function listTools( + client: Client, + metaData?: Record, +): Promise { try { - const response = await client.listTools(); + const params = + metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {}; + const response = await client.listTools(params); return response; } catch (error) { throw new Error( @@ -82,9 +87,11 @@ export async function callTool( client: Client, name: string, args: Record, + generalMetaData?: Record, + toolSpecificMetaData?: Record, ): Promise { try { - const toolsResponse = await listTools(client); + const toolsResponse = await listTools(client, generalMetaData); const tools = toolsResponse.tools as Tool[]; const tool = tools.find((t) => t.name === name); @@ -106,9 +113,23 @@ export async function callTool( } } + // Merge general metadata with tool-specific metadata + // Tool-specific metadata takes precedence over general metadata + let mergedMeta: Record | undefined; + if (generalMetaData || toolSpecificMetaData) { + mergedMeta = { + ...(generalMetaData || {}), + ...(toolSpecificMetaData || {}), + }; + } + const response = await client.callTool({ name: name, arguments: convertedArgs, + _meta: + mergedMeta && Object.keys(mergedMeta).length > 0 + ? mergedMeta + : undefined, }); return response; } catch (error) { diff --git a/cli/src/index.ts b/cli/src/index.ts index a7c5896d7..e9f60ebdc 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -42,8 +42,10 @@ type Args = { logLevel?: LogLevel; toolName?: string; toolArg?: Record; + toolMeta?: Record; transport?: "sse" | "stdio" | "http"; headers?: Record; + metaData?: Record; }; function createTransportOptions( @@ -121,7 +123,7 @@ async function callMethod(args: Args): Promise { // Tools methods if (args.method === "tools/list") { - result = await listTools(client); + result = await listTools(client, args.metaData); } else if (args.method === "tools/call") { if (!args.toolName) { throw new Error( @@ -129,11 +131,17 @@ async function callMethod(args: Args): Promise { ); } - result = await callTool(client, args.toolName, args.toolArg || {}); + result = await callTool( + client, + args.toolName, + args.toolArg || {}, + args.metaData, + args.toolMeta, + ); } // Resources methods else if (args.method === "resources/list") { - result = await listResources(client); + result = await listResources(client, args.metaData); } else if (args.method === "resources/read") { if (!args.uri) { throw new Error( @@ -141,13 +149,13 @@ async function callMethod(args: Args): Promise { ); } - result = await readResource(client, args.uri); + result = await readResource(client, args.uri, args.metaData); } else if (args.method === "resources/templates/list") { - result = await listResourceTemplates(client); + result = await listResourceTemplates(client, args.metaData); } // Prompts methods else if (args.method === "prompts/list") { - result = await listPrompts(client); + result = await listPrompts(client, args.metaData); } else if (args.method === "prompts/get") { if (!args.promptName) { throw new Error( @@ -155,7 +163,12 @@ async function callMethod(args: Args): Promise { ); } - result = await getPrompt(client, args.promptName, args.promptArgs || {}); + result = await getPrompt( + client, + args.promptName, + args.promptArgs || {}, + args.metaData, + ); } // Logging methods else if (args.method === "logging/setLevel") { @@ -327,6 +340,8 @@ function parseArgs(): Args { const options = program.opts() as Omit & { header?: Record; + meta?: Record; + toolMeta?: Record; }; let remainingArgs = program.args; @@ -344,6 +359,22 @@ function parseArgs(): Args { target: finalArgs, ...options, headers: options.header, // commander.js uses 'header' field, map to 'headers' + metaData: options.meta + ? Object.fromEntries( + Object.entries(options.meta).map(([key, value]) => [ + key, + String(value), + ]), + ) + : undefined, + toolMeta: options.toolMeta + ? Object.fromEntries( + Object.entries(options.toolMeta).map(([key, value]) => [ + key, + String(value), + ]), + ) + : undefined, }; } diff --git a/client/src/App.tsx b/client/src/App.tsx index b7fc607b0..7f5314711 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -47,6 +47,7 @@ import { Hash, Key, MessageSquare, + Settings, } from "lucide-react"; import { z } from "zod"; @@ -80,6 +81,7 @@ import { CustomHeaders, migrateFromLegacyAuth, } from "./lib/types/customHeaders"; +import MetaDataTab from "./components/MetaDataTab"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; @@ -195,9 +197,27 @@ const App = () => { const [authState, setAuthState] = useState(EMPTY_DEBUGGER_STATE); + // Meta data state - persisted in localStorage + const [metaData, setMetaData] = useState>(() => { + const savedMetaData = localStorage.getItem("lastMetaData"); + if (savedMetaData) { + try { + return JSON.parse(savedMetaData); + } catch (error) { + console.warn("Failed to parse saved meta data:", error); + } + } + return {}; + }); + const updateAuthState = (updates: Partial) => { setAuthState((prev) => ({ ...prev, ...updates })); }; + + const handleMetaDataChange = (newMetaData: Record) => { + setMetaData(newMetaData); + localStorage.setItem("lastMetaData", JSON.stringify(newMetaData)); + }; const nextRequestId = useRef(0); const rootsRef = useRef([]); @@ -299,6 +319,7 @@ const App = () => { }, getRoots: () => rootsRef.current, defaultLoggingLevel: logLevel, + metaData, }); useEffect(() => { @@ -775,7 +796,11 @@ const App = () => { cacheToolOutputSchemas(response.tools); }; - const callTool = async (name: string, params: Record) => { + const callTool = async ( + name: string, + params: Record, + meta?: Record, + ) => { lastToolCallOriginTabRef.current = currentTabRef.current; try { @@ -785,15 +810,21 @@ const App = () => { ? cleanParams(params, tool.inputSchema as JsonSchemaType) : params; + // Merge general metadata with tool-specific metadata + // Tool-specific metadata takes precedence over general metadata + const mergedMeta = { + ...metaData, // General metadata first + progressToken: progressTokenRef.current++, + ...(meta ?? {}), // Tool-specific metadata overrides + }; + const response = await sendMCPRequest( { method: "tools/call" as const, params: { name, arguments: cleanedParams, - _meta: { - progressToken: progressTokenRef.current++, - }, + _meta: mergedMeta, }, }, CompatibilityCallToolResultSchema, @@ -989,6 +1020,10 @@ const App = () => { Auth + + + Meta Data +
@@ -1099,10 +1134,14 @@ const App = () => { setNextToolCursor(undefined); cacheToolOutputSchemas([]); }} - callTool={async (name, params) => { + callTool={async ( + name: string, + params: Record, + meta?: Record, + ) => { clearError("tools"); setToolResult(null); - await callTool(name, params); + await callTool(name, params, meta); }} selectedTool={selectedTool} setSelectedTool={(tool) => { @@ -1145,6 +1184,10 @@ const App = () => { onRootsChange={handleRootsChange} /> + )}
diff --git a/client/src/components/MetaDataTab.tsx b/client/src/components/MetaDataTab.tsx new file mode 100644 index 000000000..c4cc6d214 --- /dev/null +++ b/client/src/components/MetaDataTab.tsx @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import { TabsContent } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Trash2, Plus } from "lucide-react"; + +interface MetaDataEntry { + key: string; + value: string; +} + +interface MetaDataTabProps { + metaData: Record; + onMetaDataChange: (metaData: Record) => void; +} + +const MetaDataTab: React.FC = ({ + metaData, + onMetaDataChange, +}) => { + const [entries, setEntries] = useState(() => { + return Object.entries(metaData).map(([key, value]) => ({ key, value })); + }); + + const addEntry = () => { + setEntries([...entries, { key: "", value: "" }]); + }; + + const removeEntry = (index: number) => { + const newEntries = entries.filter((_, i) => i !== index); + setEntries(newEntries); + updateMetaData(newEntries); + }; + + const updateEntry = ( + index: number, + field: "key" | "value", + value: string, + ) => { + const newEntries = [...entries]; + newEntries[index][field] = value; + setEntries(newEntries); + updateMetaData(newEntries); + }; + + const updateMetaData = (newEntries: MetaDataEntry[]) => { + const metaDataObject: Record = {}; + newEntries.forEach(({ key, value }) => { + if (key.trim() && value.trim()) { + metaDataObject[key.trim()] = value.trim(); + } + }); + onMetaDataChange(metaDataObject); + }; + + return ( + +
+
+
+

Meta Data

+

+ Key-value pairs that will be included in all MCP requests +

+
+ +
+ +
+ {entries.map((entry, index) => ( +
+
+ + updateEntry(index, "key", e.target.value)} + /> +
+
+ + updateEntry(index, "value", e.target.value)} + /> +
+ +
+ ))} +
+ + {entries.length === 0 && ( +
+

+ No meta data entries. Click "Add Entry" to add key-value pairs. +

+
+ )} +
+
+ ); +}; + +export default MetaDataTab; diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx index 6479b5fbb..64798cd9d 100644 --- a/client/src/components/ToolResults.tsx +++ b/client/src/components/ToolResults.tsx @@ -156,7 +156,7 @@ const ToolResults = ({ )} {structuredResult._meta && (
-
Meta:
+
Meta Schema:
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 0123296af..04d298e13 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -53,7 +53,11 @@ const ToolsTab = ({ tools: Tool[]; listTools: () => void; clearTools: () => void; - callTool: (name: string, params: Record) => Promise; + callTool: ( + name: string, + params: Record, + meta?: Record, + ) => Promise; selectedTool: Tool | null; setSelectedTool: (tool: Tool | null) => void; toolResult: CompatibilityCallToolResult | null; @@ -66,6 +70,9 @@ const ToolsTab = ({ const [isToolRunning, setIsToolRunning] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); const [isMetaExpanded, setIsMetaExpanded] = useState(false); + const [metaEntries, setMetaEntries] = useState< + { id: string; key: string; value: string }[] + >([]); const [hasValidationErrors, setHasValidationErrors] = useState(false); const formRefs = useRef>({}); const { toast } = useToast(); @@ -297,6 +304,102 @@ const ToolsTab = ({ ); }, )} +
+
+

Meta:

+ +
+ {metaEntries.length === 0 ? ( +

+ No meta pairs. +

+ ) : ( +
+ {metaEntries.map((entry, index) => ( +
+ + { + const value = e.target.value; + setMetaEntries((prev) => + prev.map((m, i) => + i === index ? { ...m, key: value } : m, + ), + ); + }} + className="h-8 flex-1" + /> + + { + const value = e.target.value; + setMetaEntries((prev) => + prev.map((m, i) => + i === index ? { ...m, value } : m, + ), + ); + }} + className="h-8 flex-1" + /> + +
+ ))} +
+ )} +
{selectedTool.outputSchema && (
@@ -338,7 +441,7 @@ const ToolsTab = ({ selectedTool._meta && (
-

Meta:

+

Meta Schema:

)} -
- + try { + setIsToolRunning(true); + const meta = metaEntries.reduce>( + (acc, { key, value }) => { + if (key.trim() !== "") acc[key] = value; + return acc; + }, + {}, + ); + await callTool( + selectedTool.name, + params, + Object.keys(meta).length ? meta : undefined, + ); + } finally { + setIsToolRunning(false); + } + }} + disabled={isToolRunning || hasValidationErrors} + > + {isToolRunning ? ( + <> + + Running... + + ) : ( + <> + + Run Tool + + )} + +