/** * Core Binder Types and Utilities * * This module provides the core types and utilities for the bindings system. * Bindings define standardized interfaces that integrations (MCPs) can implement. */ import type { ZodType } from "zod"; import { createMCPFetchStub, MCPClientFetchStub } from "./client/mcp"; import { ServerClient } from "./client/mcp-client"; import { MCPConnection } from "./connection"; /** * ToolBinder defines a single tool within a binding. * It specifies the tool name, input/output schemas, and whether it's optional. * * @template TName - The tool name (can be a string or RegExp for pattern matching) * @template TInput - The input type (inferred from inputSchema) * @template TReturn - The return type (inferred from outputSchema) */ export interface ToolBinder< TName extends string | RegExp = string, // biome-ignore lint/suspicious/noExplicitAny: Generic type parameter // eslint-disable-next-line @typescript-eslint/no-explicit-any TInput = any, TReturn extends object | null | boolean = object, > { /** The name of the tool (e.g., "DECO_CHAT_CHANNELS_JOIN") */ name: TName; /** Zod schema for validating tool input */ inputSchema: ZodType; /** Optional Zod schema for validating tool output */ outputSchema?: ZodType; /** * Whether this tool is optional in the binding. * If true, an implementation doesn't need to provide this tool. */ opt?: true; } /** * Binder represents a collection of tool definitions that form a binding. * A binding is like a TypeScript interface - it defines what tools must be implemented. * * @template TDefinition - Array of ToolBinder definitions * * @example * ```ts * const MY_BINDING = [{ * name: "MY_TOOL" as const, * inputSchema: z.object({ id: z.string() }), * outputSchema: z.object({ success: z.boolean() }), * }] as const satisfies Binder; * ``` */ export type Binder< TDefinition extends readonly ToolBinder[] = readonly ToolBinder[], > = TDefinition; /** * Tool with schemas for validation */ export interface ToolWithSchemas { name: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any inputSchema?: ZodType | Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any outputSchema?: ZodType | Record; } /** * Binding checker interface */ export interface BindingChecker { /** * Check if a set of tools implements the binding with full schema validation. * * Validates: * - Tool name matches (exact or regex) * - Input schema: Tool accepts what binder requires (no removals from binder to tool) * - Output schema: Tool provides what binder expects (no removals from tool to binder) * * @param tools - Array of tools with names and schemas * @returns Promise - true if all tools implement the binding correctly */ isImplementedBy: (tools: ToolWithSchemas[]) => boolean; } export const bindingClient = ( binder: TDefinition, ) => { return { ...createBindingChecker(binder), forClient: (client: ServerClient): MCPClientFetchStub => { return createMCPFetchStub({ client, }); }, forConnection: ( mcpConnection: MCPConnection, ): MCPClientFetchStub => { return createMCPFetchStub({ connection: mcpConnection, }); }, }; }; export type MCPBindingClient>> = ReturnType; /** * Creates a binding checker with full schema validation using structural subset checking. * * This performs strict compatibility checking: * - For input schemas: Validates that the tool can accept what the binder requires (binder ⊆ tool) * - For output schemas: Validates that the tool provides what the binder expects (binder ⊆ tool) * * @param binderTools - The binding definition to check against * @returns A binding checker with an async isImplementedBy method * * @example * ```ts * const checker = createBindingChecker(MY_BINDING); * const isCompatible = await checker.isImplementedBy(availableTools); * ``` */ export function createBindingChecker( binderTools: TDefinition, ): BindingChecker { return { isImplementedBy: (tools: ToolWithSchemas[]): boolean => { for (const binderTool of binderTools) { // Find matching tool by name (exact or regex) const pattern = typeof binderTool.name === "string" ? new RegExp(`^${binderTool.name}$`) : binderTool.name; const matchedTool = tools.find((t) => pattern.test(t.name)); // Skip optional tools that aren't present if (!matchedTool && binderTool.opt) { continue; } // Required tool not found if (!matchedTool) { return false; } // FIXME @mcandeia Zod to JSONSchema converstion is creating inconsistent schemas } return true; }, }; } /** * Generic connection type for binding checking. * Any connection object with a tools array can be checked. */ export interface ConnectionForBinding { tools?: Array<{ name: string; inputSchema?: Record; outputSchema?: Record; }> | null; } /** * Checks if a connection implements a binding by validating its tools. * Used by plugin layouts to filter connections by binding. */ export function connectionImplementsBinding( connection: ConnectionForBinding, binding: Binder, ): boolean { const tools = connection.tools; if (!tools || tools.length === 0) { return false; } // Prepare tools for checker (only input schema, skip output for detection) const toolsForChecker = tools.map((t) => ({ name: t.name, inputSchema: t.inputSchema, })); // Create binding checker without output schemas const bindingForChecker = binding.map((b) => ({ name: b.name, inputSchema: b.inputSchema, opt: b.opt, })); const checker = createBindingChecker(bindingForChecker); return checker.isImplementedBy(toolsForChecker); }