import { getToolUiResourceUri } from "./ext-apps-bridge.ts"; import type { McpExtensionState } from "./state.ts"; import type { ToolMetadata, McpTool, McpResource, ServerEntry } from "./types.ts"; import { formatToolName, isToolExcluded } from "./types.ts"; import { resourceNameToToolName } from "./resource-tools.ts"; import { extractToolUiStreamMode } from "./utils.ts"; export function buildToolMetadata( tools: McpTool[], resources: McpResource[], definition: ServerEntry, serverName: string, prefix: "server" | "none" | "short" ): { metadata: ToolMetadata[]; failedTools: string[] } { const metadata: ToolMetadata[] = []; const failedTools: string[] = []; for (const tool of tools) { if (!tool?.name) { failedTools.push("(unnamed)"); continue; } if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) { continue; } let uiResourceUri: string | undefined; try { uiResourceUri = getToolUiResourceUri({ _meta: tool._meta }); } catch { failedTools.push(tool.name); } metadata.push({ name: formatToolName(tool.name, serverName, prefix), originalName: tool.name, description: tool.description ?? "", inputSchema: tool.inputSchema, uiResourceUri, uiStreamMode: extractToolUiStreamMode(tool._meta), }); } if (definition.exposeResources !== false) { for (const resource of resources) { const baseName = `get_${resourceNameToToolName(resource.name)}`; if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) { continue; } metadata.push({ name: formatToolName(baseName, serverName, prefix), originalName: baseName, description: resource.description ?? `Read resource: ${resource.uri}`, resourceUri: resource.uri, }); } } return { metadata, failedTools }; } export function getToolNames(state: McpExtensionState, serverName: string): string[] { return state.toolMetadata.get(serverName)?.map(m => m.name) ?? []; } export function totalToolCount(state: McpExtensionState): number { let count = 0; for (const metadata of state.toolMetadata.values()) { count += metadata.length; } return count; } export function findToolByName(metadata: ToolMetadata[] | undefined, toolName: string): ToolMetadata | undefined { if (!metadata) return undefined; const exact = metadata.find(m => m.name === toolName); if (exact) return exact; const normalized = toolName.replace(/-/g, "_"); return metadata.find(m => m.name.replace(/-/g, "_") === normalized); } export function formatSchema(schema: unknown, indent = " "): string { if (!schema || typeof schema !== "object" || Array.isArray(schema)) { return `${indent}(no schema)`; } const s = schema as Record; if (s.type === "object" && s.properties && typeof s.properties === "object" && !Array.isArray(s.properties)) { const props = s.properties as Record; const required = Array.isArray(s.required) ? s.required.filter((name): name is string => typeof name === "string") : []; if (Object.keys(props).length === 0) { return `${indent}(no parameters)`; } const lines: string[] = []; for (const [name, propSchema] of Object.entries(props)) { lines.push(...formatProperty(name, propSchema, required.includes(name), indent)); } return lines.join("\n"); } const lines = formatNestedSchema(s, indent); if (lines.length > 0) { return lines.join("\n"); } const typeStr = formatType(s); if (typeStr) { return `${indent}(${typeStr})`; } return `${indent}(complex schema)`; } function formatProperty(name: string, schema: unknown, required: boolean, indent: string): string[] { if (!schema || typeof schema !== "object" || Array.isArray(schema)) { return [`${indent}${name}${required ? " *required*" : ""}`]; } const s = schema as Record; const parts = [`${indent}${name}`]; const typeStr = formatType(s); if (typeStr) parts.push(`(${typeStr})`); if (required) parts.push("*required*"); appendSchemaAnnotations(parts, s); return [parts.join(" "), ...formatNestedSchema(s, `${indent} `)]; } function formatNestedSchema(schema: Record, indent: string): string[] { const lines: string[] = []; if (Array.isArray(schema.anyOf)) { lines.push(...formatVariants("anyOf", schema.anyOf, indent)); } if (Array.isArray(schema.oneOf)) { lines.push(...formatVariants("oneOf", schema.oneOf, indent)); } if (schema.items !== undefined) { lines.push(...formatProperty("items", schema.items, false, indent)); } if (schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)) { const required = Array.isArray(schema.required) ? schema.required.filter((name): name is string => typeof name === "string") : []; for (const [name, propSchema] of Object.entries(schema.properties as Record)) { lines.push(...formatProperty(name, propSchema, required.includes(name), indent)); } } return lines; } function formatVariants(keyword: "anyOf" | "oneOf", variants: unknown[], indent: string): string[] { const lines = [`${indent}${keyword}:`]; for (const variant of variants) { if (!variant || typeof variant !== "object" || Array.isArray(variant)) { lines.push(`${indent} - ${JSON.stringify(variant)}`); continue; } const s = variant as Record; const typeStr = formatType(s) || "schema"; const parts = [`${indent} - ${typeStr}`]; appendSchemaAnnotations(parts, s); lines.push(parts.join(" ")); lines.push(...formatNestedSchema(s, `${indent} `)); } return lines; } function formatType(schema: Record): string { if (Object.hasOwn(schema, "const")) { return `const ${JSON.stringify(schema.const)}`; } if (Array.isArray(schema.enum)) { return `enum: ${schema.enum.map(v => JSON.stringify(v)).join(", ")}`; } if (Array.isArray(schema.type)) { return schema.type.map(type => String(type)).join(" | "); } if (schema.type) { return String(schema.type); } if (schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)) { return "object"; } if (schema.items !== undefined) { return "array"; } return ""; } function appendSchemaAnnotations(parts: string[], schema: Record): void { if (schema.description && typeof schema.description === "string") { parts.push(`- ${schema.description}`); } for (const key of ["minLength", "maxLength", "minimum", "maximum", "minItems", "maxItems", "format", "pattern"] as const) { if (schema[key] !== undefined) { parts.push(`[${key}: ${JSON.stringify(schema[key])}]`); } } if (schema.default !== undefined) { parts.push(`[default: ${JSON.stringify(schema.default)}]`); } }