包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。 排除 node_modules、npm 缓存、sessions 等运行时数据。
217 lines
6.9 KiB
TypeScript
217 lines
6.9 KiB
TypeScript
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
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<string, unknown>;
|
|
|
|
if (s.type === "object" && s.properties && typeof s.properties === "object" && !Array.isArray(s.properties)) {
|
|
const props = s.properties as Record<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>, 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<string, unknown>)) {
|
|
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<string, unknown>;
|
|
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, unknown>): 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<string, unknown>): 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)}]`);
|
|
}
|
|
}
|