/** * Connection Entity Schema * * Single source of truth for connection types. * Uses snake_case field names matching the database schema directly. */ import { ToolSchema } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; /** * OAuth configuration schema for downstream MCP */ const OAuthConfigSchema = z.object({ authorizationEndpoint: z.string().url(), tokenEndpoint: z.string().url(), introspectionEndpoint: z.string().url().optional(), clientId: z.string(), clientSecret: z.string().optional(), scopes: z.array(z.string()), grantType: z.enum(["authorization_code", "client_credentials"]), }); export type OAuthConfig = z.infer; /** * JSON-Schema-safe JSON Schema object. * * The MCP SDK's ToolSchema uses z.custom() (AssertObjectSchema) for property * values inside inputSchema/outputSchema. Zod 4's toJSONSchema() cannot * represent z.custom(), throwing "Custom types cannot be represented in JSON * Schema". We override only these two fields with a safe equivalent so that * all other ToolSchema fields (name, annotations, execution, icons, _meta, …) * still flow through automatically when the MCP SDK is updated. */ const JsonSchemaObjectSchema = z .object({ type: z.literal("object"), properties: z.record(z.string(), z.unknown()).optional(), required: z.array(z.string()).optional(), }) .catchall(z.unknown()); const ToolDefinitionSchema = ToolSchema.extend({ inputSchema: JsonSchemaObjectSchema, outputSchema: JsonSchemaObjectSchema.optional(), }); export type ToolDefinition = z.infer; /** * Connection parameters - discriminated by connection_type * * HTTP/SSE/WebSocket: HTTP headers for requests * STDIO: Environment variables + command config */ const HttpConnectionParametersSchema = z.object({ headers: z.record(z.string(), z.string()).optional(), }); const StdioConnectionParametersSchema = z.object({ command: z.string().describe("Command to run (e.g., 'npx', 'node')"), args: z.array(z.string()).optional().describe("Command arguments"), cwd: z.string().optional().describe("Working directory"), envVars: z .record(z.string(), z.string()) .optional() .describe("Environment variables (encrypted in storage)"), }); export type HttpConnectionParameters = z.infer< typeof HttpConnectionParametersSchema >; export type StdioConnectionParameters = z.infer< typeof StdioConnectionParametersSchema >; export type ConnectionParameters = | HttpConnectionParameters | StdioConnectionParameters; /** * Connection entity schema - single source of truth. * Compliant with collections binding pattern. */ export const ConnectionEntitySchema = z.object({ // Base collection entity fields id: z.string().describe("Unique identifier for the connection"), title: z.string().describe("Human-readable name for the connection"), created_at: z.string().describe("When the connection was created"), updated_at: z.string().describe("When the connection was last updated"), created_by: z.string().describe("User ID who created the connection"), updated_by: z .string() .optional() .describe("User ID who last updated the connection"), // Connection-specific fields organization_id: z .string() .describe("Organization ID this connection belongs to"), description: z.string().nullable().describe("Description of the connection"), icon: z.string().nullable().describe("Icon URL for the connection"), app_name: z.string().nullable().describe("Associated app name"), app_id: z.string().nullable().describe("Associated app ID"), slug: z .string() .nullable() .optional() .describe("URL-safe slug derived from app_name, connection_url, or title"), connection_type: z .enum(["HTTP", "SSE", "Websocket", "STDIO", "VIRTUAL"]) .describe("Type of connection"), connection_url: z .string() .nullable() .describe( "URL for HTTP/SSE/WebSocket connections. virtual://$id for VIRTUAL. Null for STDIO.", ), connection_token: z .string() .nullable() .describe("Authentication token (for HTTP connections)"), connection_headers: z .union([StdioConnectionParametersSchema, HttpConnectionParametersSchema]) .nullable() .describe( "Connection parameters. HTTP: { headers }. STDIO: { command, args, cwd, envVars }", ), oauth_config: OAuthConfigSchema.nullable().describe("OAuth configuration"), // New configuration fields (snake_case) configuration_state: z .record(z.string(), z.unknown()) .nullable() .describe("Configuration state (decrypted)"), configuration_scopes: z .array(z.string()) .nullable() .optional() .describe("Configuration scopes"), metadata: z .record(z.string(), z.unknown()) .nullable() .describe("Additional metadata (includes repository info)"), tools: z .array(ToolDefinitionSchema) .nullable() .describe("Discovered tools from MCP"), bindings: z.array(z.string()).nullable().describe("Detected bindings"), status: z.enum(["active", "inactive", "error"]).describe("Current status"), }); /** * The connection entity type - use this everywhere instead of MCPConnection */ export type ConnectionEntity = z.infer; /** * Input schema for creating connections */ export const ConnectionCreateDataSchema = ConnectionEntitySchema.omit({ created_at: true, updated_at: true, created_by: true, updated_by: true, organization_id: true, tools: true, bindings: true, status: true, }) .partial({ id: true, description: true, icon: true, app_name: true, app_id: true, connection_url: true, connection_token: true, connection_headers: true, oauth_config: true, configuration_state: true, configuration_scopes: true, metadata: true, }) .extend({ icon: z.string().nullish(), }); export type ConnectionCreateData = z.infer; /** * Input schema for updating connections */ export const ConnectionUpdateDataSchema = ConnectionEntitySchema.partial(); export type ConnectionUpdateData = z.infer; /** * Type guard to check if parameters are STDIO type */ export function isStdioParameters( params: ConnectionParameters | null | undefined, ): params is StdioConnectionParameters { return params !== null && params !== undefined && "command" in params; } /** * Parse virtual MCP ID from virtual:// URL * @returns The virtual MCP ID or null if not a virtual URL */ export function parseVirtualUrl(url: string | null | undefined): string | null { if (!url || !url.startsWith("virtual://")) { return null; } return url.replace("virtual://", ""); } /** * Build virtual:// URL from virtual MCP ID */ export function buildVirtualUrl(virtualMcpId: string): string { return `virtual://${virtualMcpId}`; }