Merge branch 'fix/apps-tool-result' of https://github.com/infoxicator/inspector into fix/apps-tool-result
This commit is contained in:
commit
f470fcb3a5
2 changed files with 799 additions and 76 deletions
|
|
@ -24,6 +24,7 @@ import {
|
|||
CompatibilityCallToolResult,
|
||||
ListToolsResult,
|
||||
Tool,
|
||||
ToolAnnotations,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
Loader2,
|
||||
|
|
@ -56,6 +57,84 @@ import {
|
|||
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
|
||||
typeof (tool as { _meta?: unknown })._meta !== "undefined";
|
||||
|
||||
// Type guard to safely detect the optional annotations field
|
||||
const hasAnnotations = (
|
||||
tool: Tool,
|
||||
): tool is Tool & { annotations: ToolAnnotations } =>
|
||||
typeof (tool as { annotations?: unknown }).annotations !== "undefined" &&
|
||||
(tool as { annotations?: unknown }).annotations !== null;
|
||||
|
||||
// Helper to render annotation badges
|
||||
// Shows all 4 annotation values with their state (true/false/implied default)
|
||||
const AnnotationBadges = ({
|
||||
annotations,
|
||||
}: {
|
||||
annotations: ToolAnnotations | undefined;
|
||||
}) => {
|
||||
// Spec defaults: readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true
|
||||
const getValueAndImplied = (
|
||||
value: boolean | undefined,
|
||||
defaultValue: boolean,
|
||||
): { value: boolean; implied: boolean } => ({
|
||||
value: value ?? defaultValue,
|
||||
implied: value === undefined,
|
||||
});
|
||||
|
||||
const readOnly = getValueAndImplied(annotations?.readOnlyHint, false);
|
||||
const destructive = getValueAndImplied(annotations?.destructiveHint, true);
|
||||
const idempotent = getValueAndImplied(annotations?.idempotentHint, false);
|
||||
const openWorld = getValueAndImplied(annotations?.openWorldHint, true);
|
||||
|
||||
// Descriptions from MCP spec
|
||||
const badges = [
|
||||
{
|
||||
label: "Read-only",
|
||||
value: readOnly.value,
|
||||
implied: readOnly.implied,
|
||||
description: "Tool does not modify its environment",
|
||||
},
|
||||
{
|
||||
label: "Destructive",
|
||||
value: destructive.value,
|
||||
implied: destructive.implied,
|
||||
description:
|
||||
"Tool may perform destructive updates (delete/overwrite data)",
|
||||
},
|
||||
{
|
||||
label: "Idempotent",
|
||||
value: idempotent.value,
|
||||
implied: idempotent.implied,
|
||||
description: "Calling repeatedly with same args has no additional effect",
|
||||
},
|
||||
{
|
||||
label: "Open-world",
|
||||
value: openWorld.value,
|
||||
implied: openWorld.implied,
|
||||
description:
|
||||
"Tool may interact with external entities beyond its local environment",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{badges.map(({ label, value, implied, description }) => (
|
||||
<span
|
||||
key={label}
|
||||
title={`${description}\n\nValue: ${value ? "Yes" : "No"} (${implied ? "implied default" : "explicitly set"})`}
|
||||
className={cn(
|
||||
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border",
|
||||
"bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-600",
|
||||
implied && "border-dashed opacity-60",
|
||||
!implied && "border-solid",
|
||||
)}
|
||||
>
|
||||
{value ? "✓" : "✗"} {label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ToolsTab = ({
|
||||
tools,
|
||||
listTools,
|
||||
|
|
@ -217,6 +296,13 @@ const ToolsTab = ({
|
|||
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-48 overflow-y-auto">
|
||||
{selectedTool.description}
|
||||
</p>
|
||||
<AnnotationBadges
|
||||
annotations={
|
||||
hasAnnotations(selectedTool)
|
||||
? selectedTool.annotations
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
||||
([key, value]) => {
|
||||
// First resolve any $ref references
|
||||
|
|
|
|||
|
|
@ -159,20 +159,46 @@ export function isPropertyRequired(
|
|||
* Resolves $ref references in JSON schema
|
||||
* @param schema The schema that may contain $ref
|
||||
* @param rootSchema The root schema to resolve references against
|
||||
* @param visitedRefs Optional set of visited $ref paths to detect circular references
|
||||
* @returns The resolved schema without $ref
|
||||
*/
|
||||
export function resolveRef(
|
||||
schema: JsonSchemaType,
|
||||
rootSchema: JsonSchemaType,
|
||||
visitedRefs: Set<string> = new Set(),
|
||||
): JsonSchemaType {
|
||||
if (!schema) return schema;
|
||||
|
||||
if (!("$ref" in schema) || !schema.$ref) {
|
||||
// Recursively resolve $ref in anyOf (and other nested structures)
|
||||
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
||||
const resolvedAnyOf = schema.anyOf.map((item) => {
|
||||
if (typeof item === "object" && item !== null) {
|
||||
return resolveRef(item, rootSchema, visitedRefs);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return {
|
||||
...schema,
|
||||
anyOf: resolvedAnyOf,
|
||||
};
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
const ref = schema.$ref;
|
||||
|
||||
// Handle simple #/properties/name references
|
||||
// Handle all #/ formats (#/properties/, #/$defs/, etc.)
|
||||
if (ref.startsWith("#/")) {
|
||||
// Check for circular reference
|
||||
if (visitedRefs.has(ref)) {
|
||||
console.warn(`Circular reference detected: ${ref}`);
|
||||
return schema;
|
||||
}
|
||||
|
||||
// Add current ref to visited set
|
||||
visitedRefs.add(ref);
|
||||
|
||||
const path = ref.substring(2).split("/");
|
||||
let current: unknown = rootSchema;
|
||||
|
||||
|
|
@ -186,12 +212,16 @@ export function resolveRef(
|
|||
current = (current as Record<string, unknown>)[segment];
|
||||
} else {
|
||||
// If reference cannot be resolved, return the original schema
|
||||
visitedRefs.delete(ref); // Clean up on failure
|
||||
console.warn(`Could not resolve $ref: ${ref}`);
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
|
||||
return current as JsonSchemaType;
|
||||
const resolved = current as JsonSchemaType;
|
||||
|
||||
// Recursively resolve nested structures (anyOf, oneOf, items, properties)
|
||||
return resolveRef(resolved, rootSchema, visitedRefs);
|
||||
}
|
||||
|
||||
// For other types of references, return the original schema
|
||||
|
|
@ -205,54 +235,28 @@ export function resolveRef(
|
|||
* @returns A normalized schema or the original schema
|
||||
*/
|
||||
export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType {
|
||||
// Handle anyOf with exactly string and null (FastMCP pattern)
|
||||
// Handle anyOf with exactly 2 items (type and null) - unified handling
|
||||
// Preserves enum and other properties automatically
|
||||
if (
|
||||
schema.anyOf &&
|
||||
schema.anyOf.length === 2 &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "string") &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
|
||||
) {
|
||||
return { ...schema, type: "string", anyOf: undefined, nullable: true };
|
||||
}
|
||||
const nonNullItem = schema.anyOf.find((t) => {
|
||||
const item = t as JsonSchemaType;
|
||||
return item?.type !== "null";
|
||||
}) as JsonSchemaType;
|
||||
|
||||
// Handle anyOf with exactly boolean and null (FastMCP pattern)
|
||||
if (
|
||||
schema.anyOf &&
|
||||
schema.anyOf.length === 2 &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "boolean") &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
|
||||
) {
|
||||
return { ...schema, type: "boolean", anyOf: undefined, nullable: true };
|
||||
}
|
||||
|
||||
// Handle anyOf with exactly number and null (FastMCP pattern)
|
||||
if (
|
||||
schema.anyOf &&
|
||||
schema.anyOf.length === 2 &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "number") &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
|
||||
) {
|
||||
return { ...schema, type: "number", anyOf: undefined, nullable: true };
|
||||
}
|
||||
|
||||
// Handle anyOf with exactly integer and null (FastMCP pattern)
|
||||
if (
|
||||
schema.anyOf &&
|
||||
schema.anyOf.length === 2 &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "integer") &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
|
||||
) {
|
||||
return { ...schema, type: "integer", anyOf: undefined, nullable: true };
|
||||
}
|
||||
|
||||
// Handle anyOf with exactly array and null (FastMCP pattern)
|
||||
if (
|
||||
schema.anyOf &&
|
||||
schema.anyOf.length === 2 &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "array") &&
|
||||
schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
|
||||
) {
|
||||
return { ...schema, type: "array", anyOf: undefined, nullable: true };
|
||||
// Only process if non-null item has type or enum
|
||||
if (nonNullItem?.type || nonNullItem?.enum) {
|
||||
return {
|
||||
...schema,
|
||||
...nonNullItem,
|
||||
type: nonNullItem?.type || (nonNullItem?.enum ? "string" : undefined),
|
||||
nullable: true,
|
||||
anyOf: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array type with exactly string and null
|
||||
|
|
|
|||
Loading…
Reference in a new issue