Added general meta data section to add pairs for all requests
This commit is contained in:
parent
78c56b350b
commit
842aa3c8ca
8 changed files with 835 additions and 25 deletions
|
|
@ -12,9 +12,14 @@ type JsonValue =
|
||||||
| { [key: string]: JsonValue };
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
// List available prompts
|
// List available prompts
|
||||||
export async function listPrompts(client: Client): Promise<McpResponse> {
|
export async function listPrompts(
|
||||||
|
client: Client,
|
||||||
|
metaData?: Record<string, string>,
|
||||||
|
): Promise<McpResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await client.listPrompts();
|
const params =
|
||||||
|
metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {};
|
||||||
|
const response = await client.listPrompts(params);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -28,6 +33,7 @@ export async function getPrompt(
|
||||||
client: Client,
|
client: Client,
|
||||||
name: string,
|
name: string,
|
||||||
args?: Record<string, JsonValue>,
|
args?: Record<string, JsonValue>,
|
||||||
|
metaData?: Record<string, string>,
|
||||||
): Promise<McpResponse> {
|
): Promise<McpResponse> {
|
||||||
try {
|
try {
|
||||||
// Convert all arguments to strings for prompt arguments
|
// 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,
|
name,
|
||||||
arguments: stringArgs,
|
arguments: stringArgs,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (metaData && Object.keys(metaData).length > 0) {
|
||||||
|
params._meta = metaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.getPrompt(params);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,14 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { McpResponse } from "./types.js";
|
import { McpResponse } from "./types.js";
|
||||||
|
|
||||||
// List available resources
|
// List available resources
|
||||||
export async function listResources(client: Client): Promise<McpResponse> {
|
export async function listResources(
|
||||||
|
client: Client,
|
||||||
|
metaData?: Record<string, string>,
|
||||||
|
): Promise<McpResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await client.listResources();
|
const params =
|
||||||
|
metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {};
|
||||||
|
const response = await client.listResources(params);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -17,9 +22,14 @@ export async function listResources(client: Client): Promise<McpResponse> {
|
||||||
export async function readResource(
|
export async function readResource(
|
||||||
client: Client,
|
client: Client,
|
||||||
uri: string,
|
uri: string,
|
||||||
|
metaData?: Record<string, string>,
|
||||||
): Promise<McpResponse> {
|
): Promise<McpResponse> {
|
||||||
try {
|
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;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -31,9 +41,12 @@ export async function readResource(
|
||||||
// List resource templates
|
// List resource templates
|
||||||
export async function listResourceTemplates(
|
export async function listResourceTemplates(
|
||||||
client: Client,
|
client: Client,
|
||||||
|
metaData?: Record<string, string>,
|
||||||
): Promise<McpResponse> {
|
): Promise<McpResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await client.listResourceTemplates();
|
const params =
|
||||||
|
metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {};
|
||||||
|
const response = await client.listResourceTemplates(params);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,14 @@ type JsonSchemaType = {
|
||||||
items?: JsonSchemaType;
|
items?: JsonSchemaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function listTools(client: Client): Promise<McpResponse> {
|
export async function listTools(
|
||||||
|
client: Client,
|
||||||
|
metaData?: Record<string, string>,
|
||||||
|
): Promise<McpResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await client.listTools();
|
const params =
|
||||||
|
metaData && Object.keys(metaData).length > 0 ? { _meta: metaData } : {};
|
||||||
|
const response = await client.listTools(params);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -82,9 +87,11 @@ export async function callTool(
|
||||||
client: Client,
|
client: Client,
|
||||||
name: string,
|
name: string,
|
||||||
args: Record<string, JsonValue>,
|
args: Record<string, JsonValue>,
|
||||||
|
generalMetaData?: Record<string, string>,
|
||||||
|
toolSpecificMetaData?: Record<string, string>,
|
||||||
): Promise<McpResponse> {
|
): Promise<McpResponse> {
|
||||||
try {
|
try {
|
||||||
const toolsResponse = await listTools(client);
|
const toolsResponse = await listTools(client, generalMetaData);
|
||||||
const tools = toolsResponse.tools as Tool[];
|
const tools = toolsResponse.tools as Tool[];
|
||||||
const tool = tools.find((t) => t.name === name);
|
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<string, string> | undefined;
|
||||||
|
if (generalMetaData || toolSpecificMetaData) {
|
||||||
|
mergedMeta = {
|
||||||
|
...(generalMetaData || {}),
|
||||||
|
...(toolSpecificMetaData || {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const response = await client.callTool({
|
const response = await client.callTool({
|
||||||
name: name,
|
name: name,
|
||||||
arguments: convertedArgs,
|
arguments: convertedArgs,
|
||||||
|
_meta:
|
||||||
|
mergedMeta && Object.keys(mergedMeta).length > 0
|
||||||
|
? mergedMeta
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,10 @@ type Args = {
|
||||||
logLevel?: LogLevel;
|
logLevel?: LogLevel;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
toolArg?: Record<string, JsonValue>;
|
toolArg?: Record<string, JsonValue>;
|
||||||
|
toolMeta?: Record<string, string>;
|
||||||
transport?: "sse" | "stdio" | "http";
|
transport?: "sse" | "stdio" | "http";
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
metaData?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createTransportOptions(
|
function createTransportOptions(
|
||||||
|
|
@ -121,7 +123,7 @@ async function callMethod(args: Args): Promise<void> {
|
||||||
|
|
||||||
// Tools methods
|
// Tools methods
|
||||||
if (args.method === "tools/list") {
|
if (args.method === "tools/list") {
|
||||||
result = await listTools(client);
|
result = await listTools(client, args.metaData);
|
||||||
} else if (args.method === "tools/call") {
|
} else if (args.method === "tools/call") {
|
||||||
if (!args.toolName) {
|
if (!args.toolName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -129,11 +131,17 @@ async function callMethod(args: Args): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await callTool(client, args.toolName, args.toolArg || {});
|
result = await callTool(
|
||||||
|
client,
|
||||||
|
args.toolName,
|
||||||
|
args.toolArg || {},
|
||||||
|
args.metaData,
|
||||||
|
args.toolMeta,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Resources methods
|
// Resources methods
|
||||||
else if (args.method === "resources/list") {
|
else if (args.method === "resources/list") {
|
||||||
result = await listResources(client);
|
result = await listResources(client, args.metaData);
|
||||||
} else if (args.method === "resources/read") {
|
} else if (args.method === "resources/read") {
|
||||||
if (!args.uri) {
|
if (!args.uri) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -141,13 +149,13 @@ async function callMethod(args: Args): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await readResource(client, args.uri);
|
result = await readResource(client, args.uri, args.metaData);
|
||||||
} else if (args.method === "resources/templates/list") {
|
} else if (args.method === "resources/templates/list") {
|
||||||
result = await listResourceTemplates(client);
|
result = await listResourceTemplates(client, args.metaData);
|
||||||
}
|
}
|
||||||
// Prompts methods
|
// Prompts methods
|
||||||
else if (args.method === "prompts/list") {
|
else if (args.method === "prompts/list") {
|
||||||
result = await listPrompts(client);
|
result = await listPrompts(client, args.metaData);
|
||||||
} else if (args.method === "prompts/get") {
|
} else if (args.method === "prompts/get") {
|
||||||
if (!args.promptName) {
|
if (!args.promptName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -155,7 +163,12 @@ async function callMethod(args: Args): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await getPrompt(client, args.promptName, args.promptArgs || {});
|
result = await getPrompt(
|
||||||
|
client,
|
||||||
|
args.promptName,
|
||||||
|
args.promptArgs || {},
|
||||||
|
args.metaData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Logging methods
|
// Logging methods
|
||||||
else if (args.method === "logging/setLevel") {
|
else if (args.method === "logging/setLevel") {
|
||||||
|
|
@ -327,6 +340,8 @@ function parseArgs(): Args {
|
||||||
|
|
||||||
const options = program.opts() as Omit<Args, "target"> & {
|
const options = program.opts() as Omit<Args, "target"> & {
|
||||||
header?: Record<string, string>;
|
header?: Record<string, string>;
|
||||||
|
meta?: Record<string, JsonValue>;
|
||||||
|
toolMeta?: Record<string, JsonValue>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let remainingArgs = program.args;
|
let remainingArgs = program.args;
|
||||||
|
|
@ -344,6 +359,22 @@ function parseArgs(): Args {
|
||||||
target: finalArgs,
|
target: finalArgs,
|
||||||
...options,
|
...options,
|
||||||
headers: options.header, // commander.js uses 'header' field, map to 'headers'
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import {
|
||||||
Hash,
|
Hash,
|
||||||
Key,
|
Key,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -80,6 +81,7 @@ import {
|
||||||
CustomHeaders,
|
CustomHeaders,
|
||||||
migrateFromLegacyAuth,
|
migrateFromLegacyAuth,
|
||||||
} from "./lib/types/customHeaders";
|
} from "./lib/types/customHeaders";
|
||||||
|
import MetaDataTab from "./components/MetaDataTab";
|
||||||
|
|
||||||
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
||||||
|
|
||||||
|
|
@ -195,9 +197,27 @@ const App = () => {
|
||||||
const [authState, setAuthState] =
|
const [authState, setAuthState] =
|
||||||
useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
|
useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
|
||||||
|
|
||||||
|
// Meta data state - persisted in localStorage
|
||||||
|
const [metaData, setMetaData] = useState<Record<string, string>>(() => {
|
||||||
|
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<AuthDebuggerState>) => {
|
const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
|
||||||
setAuthState((prev) => ({ ...prev, ...updates }));
|
setAuthState((prev) => ({ ...prev, ...updates }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMetaDataChange = (newMetaData: Record<string, string>) => {
|
||||||
|
setMetaData(newMetaData);
|
||||||
|
localStorage.setItem("lastMetaData", JSON.stringify(newMetaData));
|
||||||
|
};
|
||||||
const nextRequestId = useRef(0);
|
const nextRequestId = useRef(0);
|
||||||
const rootsRef = useRef<Root[]>([]);
|
const rootsRef = useRef<Root[]>([]);
|
||||||
|
|
||||||
|
|
@ -299,6 +319,7 @@ const App = () => {
|
||||||
},
|
},
|
||||||
getRoots: () => rootsRef.current,
|
getRoots: () => rootsRef.current,
|
||||||
defaultLoggingLevel: logLevel,
|
defaultLoggingLevel: logLevel,
|
||||||
|
metaData,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -789,16 +810,21 @@ const App = () => {
|
||||||
? cleanParams(params, tool.inputSchema as JsonSchemaType)
|
? cleanParams(params, tool.inputSchema as JsonSchemaType)
|
||||||
: params;
|
: 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(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "tools/call" as const,
|
method: "tools/call" as const,
|
||||||
params: {
|
params: {
|
||||||
name,
|
name,
|
||||||
arguments: cleanedParams,
|
arguments: cleanedParams,
|
||||||
_meta: {
|
_meta: mergedMeta,
|
||||||
progressToken: progressTokenRef.current++,
|
|
||||||
...(meta ?? {}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CompatibilityCallToolResultSchema,
|
CompatibilityCallToolResultSchema,
|
||||||
|
|
@ -994,6 +1020,10 @@ const App = () => {
|
||||||
<Key className="w-4 h-4 mr-2" />
|
<Key className="w-4 h-4 mr-2" />
|
||||||
Auth
|
Auth
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="metadata">
|
||||||
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
|
Meta Data
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|
@ -1154,6 +1184,10 @@ const App = () => {
|
||||||
onRootsChange={handleRootsChange}
|
onRootsChange={handleRootsChange}
|
||||||
/>
|
/>
|
||||||
<AuthDebuggerWrapper />
|
<AuthDebuggerWrapper />
|
||||||
|
<MetaDataTab
|
||||||
|
metaData={metaData}
|
||||||
|
onMetaDataChange={handleMetaDataChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
121
client/src/components/MetaDataTab.tsx
Normal file
121
client/src/components/MetaDataTab.tsx
Normal file
|
|
@ -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<string, string>;
|
||||||
|
onMetaDataChange: (metaData: Record<string, string>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetaDataTab: React.FC<MetaDataTabProps> = ({
|
||||||
|
metaData,
|
||||||
|
onMetaDataChange,
|
||||||
|
}) => {
|
||||||
|
const [entries, setEntries] = useState<MetaDataEntry[]>(() => {
|
||||||
|
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<string, string> = {};
|
||||||
|
newEntries.forEach(({ key, value }) => {
|
||||||
|
if (key.trim() && value.trim()) {
|
||||||
|
metaDataObject[key.trim()] = value.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onMetaDataChange(metaDataObject);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContent value="metadata">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Meta Data</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Key-value pairs that will be included in all MCP requests
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={addEntry} size="sm">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Add Entry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{entries.map((entry, index) => (
|
||||||
|
<div key={index} className="flex items-center space-x-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label htmlFor={`key-${index}`} className="sr-only">
|
||||||
|
Key
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`key-${index}`}
|
||||||
|
placeholder="Key"
|
||||||
|
value={entry.key}
|
||||||
|
onChange={(e) => updateEntry(index, "key", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label htmlFor={`value-${index}`} className="sr-only">
|
||||||
|
Value
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`value-${index}`}
|
||||||
|
placeholder="Value"
|
||||||
|
value={entry.value}
|
||||||
|
onChange={(e) => updateEntry(index, "value", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeEntry(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{entries.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mb-4">
|
||||||
|
No meta data entries. Click "Add Entry" to add key-value pairs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MetaDataTab;
|
||||||
558
client/src/components/__tests__/MetaDataTab.test.tsx
Normal file
558
client/src/components/__tests__/MetaDataTab.test.tsx
Normal file
|
|
@ -0,0 +1,558 @@
|
||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import MetaDataTab from "../MetaDataTab";
|
||||||
|
import { Tabs } from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
describe("MetaDataTab", () => {
|
||||||
|
const defaultProps = {
|
||||||
|
metaData: {},
|
||||||
|
onMetaDataChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMetaDataTab = (props = {}) => {
|
||||||
|
return render(
|
||||||
|
<Tabs defaultValue="metadata">
|
||||||
|
<MetaDataTab {...defaultProps} {...props} />
|
||||||
|
</Tabs>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Initial Rendering", () => {
|
||||||
|
it("should render the metadata tab with title and description", () => {
|
||||||
|
renderMetaDataTab();
|
||||||
|
|
||||||
|
expect(screen.getByText("Meta Data")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
"Key-value pairs that will be included in all MCP requests",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render Add Entry button", () => {
|
||||||
|
renderMetaDataTab();
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
expect(addButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show empty state message when no entries exist", () => {
|
||||||
|
renderMetaDataTab();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'No meta data entries. Click "Add Entry" to add key-value pairs.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show empty state message when entries exist", () => {
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText(
|
||||||
|
'No meta data entries. Click "Add Entry" to add key-value pairs.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Initial Data Handling", () => {
|
||||||
|
it("should initialize with existing metadata", () => {
|
||||||
|
const initialMetaData = {
|
||||||
|
API_KEY: "test-key",
|
||||||
|
VERSION: "1.0.0",
|
||||||
|
};
|
||||||
|
|
||||||
|
renderMetaDataTab({ metaData: initialMetaData });
|
||||||
|
|
||||||
|
expect(screen.getByDisplayValue("API_KEY")).toBeInTheDocument();
|
||||||
|
expect(screen.getByDisplayValue("test-key")).toBeInTheDocument();
|
||||||
|
expect(screen.getByDisplayValue("VERSION")).toBeInTheDocument();
|
||||||
|
expect(screen.getByDisplayValue("1.0.0")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render multiple entries in correct order", () => {
|
||||||
|
const initialMetaData = {
|
||||||
|
FIRST: "first-value",
|
||||||
|
SECOND: "second-value",
|
||||||
|
THIRD: "third-value",
|
||||||
|
};
|
||||||
|
|
||||||
|
renderMetaDataTab({ metaData: initialMetaData });
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
expect(keyInputs).toHaveLength(3);
|
||||||
|
expect(valueInputs).toHaveLength(3);
|
||||||
|
|
||||||
|
// Check that entries are rendered in the order they appear in the object
|
||||||
|
const entries = Object.entries(initialMetaData);
|
||||||
|
entries.forEach(([key, value], index) => {
|
||||||
|
expect(keyInputs[index]).toHaveValue(key);
|
||||||
|
expect(valueInputs[index]).toHaveValue(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Adding Entries", () => {
|
||||||
|
it("should add a new empty entry when Add Entry button is clicked", () => {
|
||||||
|
renderMetaDataTab();
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
expect(keyInputs).toHaveLength(1);
|
||||||
|
expect(valueInputs).toHaveLength(1);
|
||||||
|
expect(keyInputs[0]).toHaveValue("");
|
||||||
|
expect(valueInputs[0]).toHaveValue("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add multiple entries when Add Entry button is clicked multiple times", () => {
|
||||||
|
renderMetaDataTab();
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
expect(keyInputs).toHaveLength(3);
|
||||||
|
expect(valueInputs).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hide empty state message after adding first entry", () => {
|
||||||
|
renderMetaDataTab();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'No meta data entries. Click "Add Entry" to add key-value pairs.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText(
|
||||||
|
'No meta data entries. Click "Add Entry" to add key-value pairs.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Removing Entries", () => {
|
||||||
|
it("should render remove button for each entry", () => {
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1", key2: "value2" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeButtons = screen.getAllByRole("button", { name: "" }); // Trash icon buttons have no text
|
||||||
|
expect(removeButtons).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove entry when remove button is clicked", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1", key2: "value2" },
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeButtons = screen.getAllByRole("button", { name: "" });
|
||||||
|
fireEvent.click(removeButtons[0]);
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({ key2: "value2" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove correct entry when multiple entries exist", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: {
|
||||||
|
FIRST: "first-value",
|
||||||
|
SECOND: "second-value",
|
||||||
|
THIRD: "third-value",
|
||||||
|
},
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeButtons = screen.getAllByRole("button", { name: "" });
|
||||||
|
fireEvent.click(removeButtons[1]); // Remove second entry
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
FIRST: "first-value",
|
||||||
|
THIRD: "third-value",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show empty state message after removing all entries", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1" },
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeButton = screen.getByRole("button", { name: "" });
|
||||||
|
fireEvent.click(removeButton);
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Editing Entries", () => {
|
||||||
|
it("should update key when key input is changed", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { oldKey: "value1" },
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyInput = screen.getByDisplayValue("oldKey");
|
||||||
|
fireEvent.change(keyInput, { target: { value: "newKey" } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({ newKey: "value1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update value when value input is changed", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "oldValue" },
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const valueInput = screen.getByDisplayValue("oldValue");
|
||||||
|
fireEvent.change(valueInput, { target: { value: "newValue" } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({ key1: "newValue" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle editing multiple entries independently", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: {
|
||||||
|
key1: "value1",
|
||||||
|
key2: "value2",
|
||||||
|
},
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
// Edit first entry key
|
||||||
|
fireEvent.change(keyInputs[0], { target: { value: "newKey1" } });
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
newKey1: "value1",
|
||||||
|
key2: "value2",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear mock to test second edit independently
|
||||||
|
onMetaDataChange.mockClear();
|
||||||
|
|
||||||
|
// Edit second entry value
|
||||||
|
fireEvent.change(valueInputs[1], { target: { value: "newValue2" } });
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
newKey1: "value1",
|
||||||
|
key2: "newValue2",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Data Validation and Trimming", () => {
|
||||||
|
it("should trim whitespace from keys and values", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInput = screen.getByPlaceholderText("Key");
|
||||||
|
const valueInput = screen.getByPlaceholderText("Value");
|
||||||
|
|
||||||
|
fireEvent.change(keyInput, { target: { value: " trimmedKey " } });
|
||||||
|
fireEvent.change(valueInput, { target: { value: " trimmedValue " } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
trimmedKey: "trimmedValue",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should exclude entries with empty keys or values after trimming", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
// First entry: valid key and value
|
||||||
|
fireEvent.change(keyInputs[0], { target: { value: "validKey" } });
|
||||||
|
fireEvent.change(valueInputs[0], { target: { value: "validValue" } });
|
||||||
|
|
||||||
|
// Second entry: empty key (should be excluded)
|
||||||
|
fireEvent.change(keyInputs[1], { target: { value: "" } });
|
||||||
|
fireEvent.change(valueInputs[1], { target: { value: "someValue" } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
validKey: "validValue",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should exclude entries with whitespace-only keys or values", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
// First entry: valid key and value
|
||||||
|
fireEvent.change(keyInputs[0], { target: { value: "validKey" } });
|
||||||
|
fireEvent.change(valueInputs[0], { target: { value: "validValue" } });
|
||||||
|
|
||||||
|
// Second entry: whitespace-only key (should be excluded)
|
||||||
|
fireEvent.change(keyInputs[1], { target: { value: " " } });
|
||||||
|
fireEvent.change(valueInputs[1], { target: { value: "someValue" } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
validKey: "validValue",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed valid and invalid entries", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
// First entry: valid
|
||||||
|
fireEvent.change(keyInputs[0], { target: { value: "key1" } });
|
||||||
|
fireEvent.change(valueInputs[0], { target: { value: "value1" } });
|
||||||
|
|
||||||
|
// Second entry: empty key (invalid)
|
||||||
|
fireEvent.change(keyInputs[1], { target: { value: "" } });
|
||||||
|
fireEvent.change(valueInputs[1], { target: { value: "value2" } });
|
||||||
|
|
||||||
|
// Third entry: valid
|
||||||
|
fireEvent.change(keyInputs[2], { target: { value: "key3" } });
|
||||||
|
fireEvent.change(valueInputs[2], { target: { value: "value3" } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
key1: "value1",
|
||||||
|
key3: "value3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Input Accessibility", () => {
|
||||||
|
it("should have proper labels for screen readers", () => {
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyLabel = screen.getByLabelText("Key", { selector: "input" });
|
||||||
|
const valueLabel = screen.getByLabelText("Value", { selector: "input" });
|
||||||
|
|
||||||
|
expect(keyLabel).toBeInTheDocument();
|
||||||
|
expect(valueLabel).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have unique IDs for each input pair", () => {
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: {
|
||||||
|
key1: "value1",
|
||||||
|
key2: "value2",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
expect(keyInputs[0]).toHaveAttribute("id", "key-0");
|
||||||
|
expect(keyInputs[1]).toHaveAttribute("id", "key-1");
|
||||||
|
expect(valueInputs[0]).toHaveAttribute("id", "value-0");
|
||||||
|
expect(valueInputs[1]).toHaveAttribute("id", "value-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have proper placeholder text", () => {
|
||||||
|
renderMetaDataTab();
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInput = screen.getByPlaceholderText("Key");
|
||||||
|
const valueInput = screen.getByPlaceholderText("Value");
|
||||||
|
|
||||||
|
expect(keyInput).toHaveAttribute("placeholder", "Key");
|
||||||
|
expect(valueInput).toHaveAttribute("placeholder", "Value");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Edge Cases", () => {
|
||||||
|
it("should handle special characters in keys and values", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInput = screen.getByPlaceholderText("Key");
|
||||||
|
const valueInput = screen.getByPlaceholderText("Value");
|
||||||
|
|
||||||
|
fireEvent.change(keyInput, {
|
||||||
|
target: { value: "key-with-special@chars!" },
|
||||||
|
});
|
||||||
|
fireEvent.change(valueInput, {
|
||||||
|
target: { value: "value with spaces & symbols $%^" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
"key-with-special@chars!": "value with spaces & symbols $%^",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle unicode characters", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInput = screen.getByPlaceholderText("Key");
|
||||||
|
const valueInput = screen.getByPlaceholderText("Value");
|
||||||
|
|
||||||
|
fireEvent.change(keyInput, { target: { value: "🔑_key" } });
|
||||||
|
fireEvent.change(valueInput, { target: { value: "值_value_🎯" } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
"🔑_key": "值_value_🎯",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle very long keys and values", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInput = screen.getByPlaceholderText("Key");
|
||||||
|
const valueInput = screen.getByPlaceholderText("Value");
|
||||||
|
|
||||||
|
const longKey = "A".repeat(100);
|
||||||
|
const longValue = "B".repeat(500);
|
||||||
|
|
||||||
|
fireEvent.change(keyInput, { target: { value: longKey } });
|
||||||
|
fireEvent.change(valueInput, { target: { value: longValue } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
[longKey]: longValue,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle duplicate keys by keeping the last one", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({ onMetaDataChange });
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: /add entry/i });
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
const keyInputs = screen.getAllByPlaceholderText("Key");
|
||||||
|
const valueInputs = screen.getAllByPlaceholderText("Value");
|
||||||
|
|
||||||
|
// Set same key for both entries
|
||||||
|
fireEvent.change(keyInputs[0], { target: { value: "duplicateKey" } });
|
||||||
|
fireEvent.change(valueInputs[0], { target: { value: "firstValue" } });
|
||||||
|
|
||||||
|
fireEvent.change(keyInputs[1], { target: { value: "duplicateKey" } });
|
||||||
|
fireEvent.change(valueInputs[1], { target: { value: "secondValue" } });
|
||||||
|
|
||||||
|
// The second value should overwrite the first
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({
|
||||||
|
duplicateKey: "secondValue",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Integration with Parent Component", () => {
|
||||||
|
it("should not call onMetaDataChange when component mounts with existing data", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1" },
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onMetaDataChange).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call onMetaDataChange only when user makes changes", () => {
|
||||||
|
const onMetaDataChange = jest.fn();
|
||||||
|
renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1" },
|
||||||
|
onMetaDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not be called on mount
|
||||||
|
expect(onMetaDataChange).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Should be called when user changes value
|
||||||
|
const valueInput = screen.getByDisplayValue("value1");
|
||||||
|
fireEvent.change(valueInput, { target: { value: "newValue" } });
|
||||||
|
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onMetaDataChange).toHaveBeenCalledWith({ key1: "newValue" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should maintain internal state when props change (component doesn't sync with prop changes)", () => {
|
||||||
|
const { rerender } = renderMetaDataTab({
|
||||||
|
metaData: { key1: "value1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByDisplayValue("key1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByDisplayValue("value1")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Rerender with different props - component should maintain its internal state
|
||||||
|
// This is the intended behavior since useState initializer only runs once
|
||||||
|
rerender(
|
||||||
|
<Tabs defaultValue="metadata">
|
||||||
|
<MetaDataTab
|
||||||
|
metaData={{ key2: "value2", key3: "value3" }}
|
||||||
|
onMetaDataChange={jest.fn()}
|
||||||
|
/>
|
||||||
|
</Tabs>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The component should still show the original values since it maintains internal state
|
||||||
|
expect(screen.getByDisplayValue("key1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByDisplayValue("value1")).toBeInTheDocument();
|
||||||
|
// The new prop values should not be displayed
|
||||||
|
expect(screen.queryByDisplayValue("key2")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByDisplayValue("value2")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -79,6 +79,7 @@ interface UseConnectionOptions {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
getRoots?: () => any[];
|
getRoots?: () => any[];
|
||||||
defaultLoggingLevel?: LoggingLevel;
|
defaultLoggingLevel?: LoggingLevel;
|
||||||
|
metaData?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useConnection({
|
export function useConnection({
|
||||||
|
|
@ -97,6 +98,7 @@ export function useConnection({
|
||||||
onElicitationRequest,
|
onElicitationRequest,
|
||||||
getRoots,
|
getRoots,
|
||||||
defaultLoggingLevel,
|
defaultLoggingLevel,
|
||||||
|
metaData = {},
|
||||||
}: UseConnectionOptions) {
|
}: UseConnectionOptions) {
|
||||||
const [connectionStatus, setConnectionStatus] =
|
const [connectionStatus, setConnectionStatus] =
|
||||||
useState<ConnectionStatus>("disconnected");
|
useState<ConnectionStatus>("disconnected");
|
||||||
|
|
@ -153,6 +155,20 @@ export function useConnection({
|
||||||
try {
|
try {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
// Add metadata to the request if available, but skip for tool calls
|
||||||
|
// as they handle metadata merging separately
|
||||||
|
const shouldAddGeneralMeta =
|
||||||
|
request.method !== "tools/call" && Object.keys(metaData).length > 0;
|
||||||
|
const requestWithMeta = shouldAddGeneralMeta
|
||||||
|
? {
|
||||||
|
...request,
|
||||||
|
params: {
|
||||||
|
...request.params,
|
||||||
|
_meta: metaData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: request;
|
||||||
|
|
||||||
// prepare MCP Client request options
|
// prepare MCP Client request options
|
||||||
const mcpRequestOptions: RequestOptions = {
|
const mcpRequestOptions: RequestOptions = {
|
||||||
signal: options?.signal ?? abortController.signal,
|
signal: options?.signal ?? abortController.signal,
|
||||||
|
|
@ -181,13 +197,17 @@ export function useConnection({
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await mcpClient.request(request, schema, mcpRequestOptions);
|
response = await mcpClient.request(
|
||||||
|
requestWithMeta,
|
||||||
|
schema,
|
||||||
|
mcpRequestOptions,
|
||||||
|
);
|
||||||
|
|
||||||
pushHistory(request, response);
|
pushHistory(requestWithMeta, response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
pushHistory(request, { error: errorMessage });
|
pushHistory(requestWithMeta, { error: errorMessage });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue