Files
sproutclaw-data/agent/extensions/pi-mcp-adapter/tool-metadata.ts
shumengya 50edff80f5 feat: 导出 SproutClaw .sproutclaw 配置
包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。
排除 node_modules、npm 缓存、sessions 等运行时数据。
2026-06-26 15:48:56 +08:00

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)}]`);
}
}