import { z } from "zod"; import { mcpHealth, INITIAL_MCP_HEALTH } from "./mcp-health.ts"; import { MAX_MCP_HEADERS, mcpHeaderName } from "./mcp-headers.ts"; import { mcpCatalog, mcpCatalogRefresh } from "./mcp-catalog.ts"; export { mcpCapability, mcpCatalog, MAX_MCP_CAPABILITIES, MAX_MCP_CATALOG_BYTES, } from "./mcp-catalog.ts"; export const MCP_OPERATION_ERRORS = { mcp_headers_invalid: "Use up to 8 distinct authentication headers with nonempty printable values. Transport and protocol headers cannot be replaced.", mcp_credentials_unreadable: "Stored MCP credentials could not be read. Replace them and reconnect.", mcp_tools_invalid: "Choose valid, current MCP tools before saving.", mcp_tools_conflict: "MCP tool definitions or choices changed. Reload before trying again.", mcp_missing: "MCP connection not found", mcp_full: "MCP connection limit reached (32)", mcp_conflict: "MCP connection changed. Reload before trying again.", mcp_disabled: "Enable this MCP connection before reconnecting", } as const; export const MAX_MCP_CONNECTIONS = 32; const name = z.string().trim().min(1).max(200); const id = z.string().regex(/^mcp-[0-9a-f-]{36}$/); const revision = z.number().int().positive().max(Number.MAX_SAFE_INTEGER); const authMode = z.enum(["none", "oauth", "headers"]); const state = z.enum([ "disconnected", "connecting", "authenticating", "ready", "error", "disabled", ]); const errorCode = z.enum([ "authentication_required", "authentication_failed", "credentials_unreadable", "connection_failed", "discovery_failed", ]); const endpoint = z .string() .max(2048) .transform((value, ctx) => { try { const url = new URL(value); if ( url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.href.length > 2048 ) throw new Error(); return url.href; } catch { ctx.addIssue({ code: "custom", message: "Use an HTTPS endpoint without credentials, query parameters or a fragment", }); return z.NEVER; } }); export const mcpConnectionInput = z.strictObject({ name, endpoint, authMode, }); export const mcpConnection = mcpConnectionInput.extend({ id, revision, enabled: z.boolean(), state, health: mcpHealth.default(INITIAL_MCP_HEALTH), settingsRevision: z .number() .int() .nonnegative() .max(Number.MAX_SAFE_INTEGER) .default(0), // Observations can revoke authority without starting a new connection attempt. authorityEpoch: z .number() .int() .nonnegative() .max(Number.MAX_SAFE_INTEGER) .default(0), headerNames: z.array(mcpHeaderName).max(MAX_MCP_HEADERS).default([]), capabilities: mcpCatalog, catalogRefresh: mcpCatalogRefresh.optional(), lastError: errorCode.nullable(), createdAt: z.iso.datetime(), updatedAt: z.iso.datetime(), }); export const mcpObservation = z.strictObject({ state: z.enum(["authenticating", "ready", "error"]), capabilities: mcpCatalog, catalogRefresh: mcpCatalogRefresh.optional(), lastError: errorCode.nullable(), }); export type McpConnection = z.infer; export type McpConnectionInput = z.infer; export type McpObservation = z.infer; export function parseMcpInput(value: unknown): McpConnectionInput { const result = mcpConnectionInput.safeParse(value); if (!result.success) throw new Error( "Enter a name, HTTPS endpoint without embedded credentials, and supported authentication mode", ); return result.data; } export function validateMcpIdentity(value: unknown, version: unknown) { if (!id.safeParse(value).success || !revision.safeParse(version).success) throw new Error("Invalid MCP connection identity or revision"); }