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";};
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";
};