170 lines · 5.3 KB · TypeScript
RawDownload
import { Option, Schema } from "effect"; import { McpToolAnnotations } from "./types"; // ---------------------------------------------------------------------------// Output types// --------------------------------------------------------------------------- export interface McpToolManifestEntry {  readonly toolId: string;  readonly toolName: string;  readonly description: string | null;  readonly inputSchema?: unknown;  readonly outputSchema?: unknown;  readonly annotations?: McpToolAnnotations;} export interface McpServerMetadata {  readonly name: string | null;  readonly version: string | null;  /** The server's `instructions` from initialize, when it sent any. */  readonly instructions: string | null;} export interface McpToolManifest {  readonly server: McpServerMetadata | null;  readonly tools: readonly McpToolManifestEntry[];} // ---------------------------------------------------------------------------// Schemas// --------------------------------------------------------------------------- const ListedTool = Schema.Struct({  name: Schema.String,  description: Schema.optional(Schema.NullOr(Schema.String)),  inputSchema: Schema.optional(Schema.Unknown),  parameters: Schema.optional(Schema.Unknown),  outputSchema: Schema.optional(Schema.Unknown),  annotations: Schema.optional(McpToolAnnotations),}); const ListToolsResult = Schema.Struct({  tools: Schema.Array(ListedTool),}); // One page of a paginated `tools/list` response. Entries stay opaque here so a// page with foreign tool shapes still pages correctly; per-entry decoding// happens in `extractManifestFromListToolsResult` over the merged list.const ListToolsPage = Schema.Struct({  tools: Schema.Array(Schema.Unknown),  nextCursor: Schema.optional(Schema.NullOr(Schema.String)),}); export interface McpListToolsPage {  readonly tools: readonly unknown[];  readonly nextCursor?: string | null;} const ServerInfo = Schema.Struct({  name: Schema.optional(Schema.String),  version: Schema.optional(Schema.String),}); const decodeListToolsResult = Schema.decodeUnknownOption(ListToolsResult);const decodeListToolsPageOption = Schema.decodeUnknownOption(ListToolsPage);const decodeServerInfo = Schema.decodeUnknownOption(ServerInfo); export const decodeListToolsPage = (value: unknown): Option.Option<McpListToolsPage> =>  decodeListToolsPageOption(value); // ---------------------------------------------------------------------------// Tool ID sanitization// --------------------------------------------------------------------------- const sanitize = (value: string): string => {  const s = value    .trim()    .toLowerCase()    .replace(/[^a-z0-9]+/g, "_")    .replace(/^_+|_+$/g, "");  return s || "tool";}; const uniqueId = (value: string, seen: Map<string, number>): string => {  const base = sanitize(value);  const n = (seen.get(base) ?? 0) + 1;  seen.set(base, n);  return n === 1 ? base : `${base}_${n}`;}; // ---------------------------------------------------------------------------// Public API// --------------------------------------------------------------------------- export const joinToolPath = (namespace: string | undefined, toolId: string): string =>  namespace?.trim() ? `${namespace}.${toolId}` : toolId; export const extractManifestFromListToolsResult = (  listToolsResult: unknown,  metadata?: { serverInfo?: unknown; instructions?: string | undefined },): McpToolManifest => {  const seen = new Map<string, number>();   const listed = decodeListToolsResult(listToolsResult).pipe(    Option.map((result) => result.tools),    Option.getOrElse(() => []),  );   const server = decodeServerInfo(metadata?.serverInfo).pipe(    Option.map(      (info): McpServerMetadata => ({        name: info.name ?? null,        version: info.version ?? null,        instructions: metadata?.instructions ?? null,      }),    ),    Option.getOrNull,  );   const tools = listed.flatMap((tool): McpToolManifestEntry[] => {    const toolName = tool.name.trim();    if (!toolName) return [];     return [      {        toolId: uniqueId(toolName, seen),        toolName,        description: tool.description ?? null,        inputSchema: tool.inputSchema ?? tool.parameters,        outputSchema: tool.outputSchema,        annotations: tool.annotations,      },    ];  });   return { server, tools };}; // ---------------------------------------------------------------------------// Namespace derivation// --------------------------------------------------------------------------- const slugify = (value: string): string =>  value    .toLowerCase()    .replace(/[^a-z0-9]+/g, "_")    .replace(/^_+|_+$/g, ""); const hostnameOf = (url: string): string | null => {  if (!URL.canParse(url)) return null;  return new URL(url).hostname;}; const basenameOf = (path: string): string => path.trim().split(/[\\/]/).pop() ?? path.trim(); export const deriveMcpNamespace = (input: {  name?: string | null;  endpoint?: string | null;  command?: string | null;}): string => {  if (input.name?.trim()) return slugify(input.name) || "mcp";   const fromEndpoint = input.endpoint?.trim() ? hostnameOf(input.endpoint) : null;  if (fromEndpoint) return slugify(fromEndpoint) || "mcp";   if (input.command?.trim()) return slugify(basenameOf(input.command)) || "mcp";   return "mcp";};