// types.ts - Core type definitions import type { Transport as McpTransport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { TextContent, ImageContent } from '@earendil-works/pi-ai'; import type { UiStreamMode } from './_ui-stream-types.ts'; export type Transport = McpTransport; /** Versioned shared-event-bus channel for read-only MCP runtime snapshots. */ export const MCP_STATUS_EVENT = 'pi-mcp-adapter/status/v1'; export const MCP_STATUS_SNAPSHOT_VERSION = 1 as const; export type McpServerRuntimeStatus = 'connected' | 'cached' | 'failed' | 'needs-auth' | 'not-connected' | 'disabled'; export interface McpServerStatusSnapshot { readonly name: string; readonly status: McpServerRuntimeStatus; readonly toolCount: number; readonly resourceCount?: number; readonly failedAgoSeconds?: number; readonly disabled: boolean; } export interface McpStatusSnapshot { readonly version: typeof MCP_STATUS_SNAPSHOT_VERSION; readonly servers: ReadonlyArray; readonly totalTools: number; readonly totalResources: number; readonly connectedCount: number; readonly disabledCount: number; } // Tool definition from MCP server export interface McpTool { name: string; title?: string; description?: string; inputSchema?: unknown; // JSON Schema _meta?: Record; } // Resource definition from MCP server export interface McpResource { uri: string; name: string; description?: string; mimeType?: string; _meta?: Record; } export interface McpPromptArgument { name: string; description?: string; required?: boolean; } export interface McpPrompt { name: string; title?: string; description?: string; arguments?: McpPromptArgument[]; _meta?: Record; } export interface UiResourceMeta { csp?: UiResourceCsp; permissions?: UiResourcePermissions; domain?: string; prefersBorder?: boolean; } export interface UiResourceContent { uri: string; html: string; mimeType?: string; meta: UiResourceMeta; } export interface UiProxyRequestBody { token: string; params: TParams; } export interface UiProxyResult> { ok: boolean; result?: T; error?: string; } export interface UiResourceCsp { resourceDomains?: string[]; connectDomains?: string[]; frameDomains?: string[]; baseUriDomains?: string[]; } export interface UiResourcePermissions { camera?: {}; microphone?: {}; geolocation?: {}; clipboardWrite?: {}; } export interface UiToolInfo { id?: string | number; tool: { name: string; description?: string; inputSchema?: unknown; }; } export interface UiHostContext { toolInfo?: UiToolInfo; theme?: 'light' | 'dark'; styles?: Record; displayMode?: UiDisplayMode; availableDisplayModes?: UiDisplayMode[]; containerDimensions?: { width?: number; maxWidth?: number; height?: number; maxHeight?: number; }; [key: string]: unknown; } export type UiDisplayMode = 'inline' | 'fullscreen' | 'pip'; // Re-export stream types from the shared lightweight module. // This allows the example package to import stream schemas without pulling the full types.ts dependency graph. export { UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_RESULT_PATCH_METHOD, SERVER_STREAM_RESULT_PATCH_METHOD, UI_STREAM_STRUCTURED_CONTENT_KEY, uiStreamModeSchema, visualizationStreamPhaseSchema, visualizationStreamFrameTypeSchema, visualizationStreamStatusSchema, uiStreamHostContextSchema, visualizationStreamEnvelopeSchema, uiStreamCallToolResultSchema, uiStreamResultPatchNotificationSchema, serverStreamResultPatchNotificationSchema, getUiStreamHostContext, getVisualizationStreamEnvelope, type UiStreamMode, type VisualizationStreamPhase, type VisualizationStreamFrameType, type VisualizationStreamStatus, type UiStreamHostContext, type VisualizationStreamEnvelope, type UiStreamCallToolResult, type UiStreamResultPatchNotification, type ServerStreamResultPatchNotification, type UiStreamSummary, } from './_ui-stream-types.ts'; export interface UiMessageParams { role?: string; content?: unknown[]; type?: 'prompt' | 'notify' | 'intent' | 'message'; message?: string; prompt?: string; intent?: string; params?: Record; [key: string]: unknown; } /** * Extract prompt text from either legacy MCP UI message shapes or native AppBridge user messages. */ export function extractUiPromptText(params: UiMessageParams): string | undefined { if (params.type === 'prompt' || params.prompt) { const prompt = params.prompt ?? String(params.message ?? ''); return prompt || undefined; } if (params.role === 'user' && Array.isArray(params.content)) { const text = params.content .map((block) => block && typeof block === 'object' && 'text' in block ? String((block as { text?: unknown }).text ?? '') : '' ) .filter(Boolean) .join('\n\n'); return text || undefined; } return undefined; } /** * Structured UI handoff recovered from a canonical prompt envelope. */ export interface UiPromptHandoff { intent: string; params: Record; raw: string; } /** * Parse a canonical named UI handoff encoded as `intent\n{json}`. */ export function parseUiPromptHandoff(prompt: string): UiPromptHandoff | undefined { const newlineIndex = prompt.indexOf('\n'); if (newlineIndex <= 0) { return undefined; } const intent = prompt.slice(0, newlineIndex).trim(); const payloadText = prompt.slice(newlineIndex + 1).trim(); if (!intent || !payloadText) { return undefined; } if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(intent)) { return undefined; } try { const parsed = JSON.parse(payloadText); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { return undefined; } return { intent, params: parsed as Record, raw: prompt, }; } catch { return undefined; } } /** * Accumulated messages from a UI session. * Collected during the session and available when it ends. */ export interface UiSessionMessages { prompts: string[]; notifications: string[]; intents: Array<{ intent: string; params?: Record }>; } export interface UiModelContextParams { content?: unknown[]; structuredContent?: Record; [key: string]: unknown; } export interface UiOpenLinkResult { isError?: boolean; [key: string]: unknown; } export interface UiDisplayModeRequest { mode?: UiDisplayMode; } export interface UiDisplayModeResult { mode: UiDisplayMode; [key: string]: unknown; } // Content types from MCP export interface McpContent { type: 'text' | 'image' | 'audio' | 'resource' | 'resource_link'; text?: string; data?: string; mimeType?: string; resource?: { uri: string; text?: string; blob?: string; }; uri?: string; name?: string; description?: string; } // Pi content block type export type ContentBlock = TextContent | ImageContent; // OAuth configuration (SDK handles auto-discovery and dynamic registration) export interface OAuthConfig { /** OAuth grant type (defaults to authorization_code) */ grantType?: 'authorization_code' | 'client_credentials'; /** Pre-registered client ID (optional, dynamic registration used if not provided) */ clientId?: string; /** Client secret for confidential clients */ clientSecret?: string; /** Requested OAuth scopes */ scope?: string; /** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */ authorizationParams?: Record; /** Exact authorization-code redirect URI for pre-registered clients */ redirectUri?: string; /** Client display name for dynamic registration */ clientName?: string; /** Client homepage URI for dynamic registration */ clientUri?: string; } // Server configuration export interface ServerEntry { command?: string; args?: string[]; /** Explicit rmcp-mux Unix-domain socket path. Mutually exclusive with command and url. */ socket?: string; env?: Record; cwd?: string; // HTTP fields url?: string; headers?: Record; /** * Authentication type: * - 'oauth' - Use OAuth 2.1 (auto-discovers endpoints, supports dynamic client registration) * - 'bearer' - Use static Bearer token * - false - Disable authentication * If not specified and url is present, OAuth will be auto-detected unless custom headers are configured */ auth?: 'oauth' | 'bearer' | false; bearerToken?: string; bearerTokenEnv?: string; /** * OAuth configuration (optional). * If not provided, the SDK will attempt dynamic client registration. * Set to false to explicitly disable OAuth for this server. */ oauth?: OAuthConfig | false; lifecycle?: 'keep-alive' | 'lazy' | 'lazy-keep-alive' | 'eager'; idleTimeout?: number; // minutes, overrides global setting requestTimeoutMs?: number; // milliseconds, overrides global request timeout when > 0 // Resource handling exposeResources?: boolean; // Direct tool registration directTools?: boolean | string[]; // Override settings.toolPrefix for this server. toolPrefix?: ToolPrefix; // Include/exclude specific MCP tools/resources by original or prefixed name includeTools?: string[]; excludeTools?: string[]; // Debug debug?: boolean; // Show server stderr (default: false) /** Enable metadata-only JSONL protocol tracing for this server. */ trace?: boolean; // Keep configuration visible without allowing connections or execution. disabled?: boolean; } /** Only the literal boolean `true` disables a server. */ export function isServerDisabled(definition: ServerEntry | undefined): boolean { return definition?.disabled === true; } // Output guard tuning (settings.outputGuard object form) export interface McpOutputGuardSettings { /** Maximum inline MCP text output bytes before truncation/spill-to-disk. Defaults to 51200 (50 KiB). */ maxBytes?: number; /** Maximum inline MCP text output lines before truncation/spill-to-disk. Defaults to 2000. */ maxLines?: number; /** Maximum details.mcpResult JSON bytes kept raw; larger results are summarized and spilled to disk. Defaults to 16384 (16 KiB). */ detailsMaxBytes?: number; } // Settings export type ToolPrefix = 'server' | 'none' | 'short' | 'mcp'; export type McpFooterStatus = 'full' | 'compact' | 'off'; export interface McpTraceSettings { /** Enable tracing for all servers unless a server sets trace to false. */ enabled?: boolean; /** JSONL destination; relative paths are resolved from the session cwd. */ file?: string; /** Maximum per-session trace file size in bytes. */ maxBytes?: number; /** Maximum events retained in the per-session trace file. */ maxEvents?: number; } export interface McpSettings { toolPrefix?: ToolPrefix; /** Show the plug prefix in MCP status and connection text (default: true). Set to false to disable it. */ showStatusIcon?: boolean; /** Footer status verbosity: full details, compact connected/enabled count, or no footer status. Defaults to full. */ mcpFooterStatus?: McpFooterStatus; idleTimeout?: number; // minutes, default 10, 0 to disable requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0 directTools?: boolean; disableProxyTool?: boolean; autoAuth?: boolean; sampling?: boolean; samplingAutoApprove?: boolean; elicitation?: boolean; /** * Guard oversized MCP tool/resource output before it is returned to the model. * Defaults to true (50 KiB / 2,000 lines inline text, 16 KiB details.mcpResult). * Set to false to restore raw MCP output behavior, or pass an object to tune * the limits. Env kill switch: MCP_OUTPUT_GUARD=0. */ outputGuard?: boolean | McpOutputGuardSettings; /** * Opt-in metadata-only MCP protocol tracing. Payloads, prompts, tool * arguments/results, authorization data, and URLs are never persisted. */ trace?: McpTraceSettings; /** * Number of direct tools at which point a console warning is emitted. * Defaults to 500. The original pi-mcp-adapter had this hardcoded at 75. */ directToolsAdvisoryThreshold?: number; /** * Message returned in tool results when a server needs (re-)authentication. * "${server}" is substituted with the server name. Defaults to a TUI * instruction when unset. */ authRequiredMessage?: string; /** * Legacy OAuth tokens.json import directory. * Relative paths are resolved from the project root (cwd). * Takes precedence over the agent's mcp-oauth/ legacy import directory but * can still be overridden by the MCP_OAUTH_DIR env variable. * * Persistent OAuth credentials are stored in the operating system credential * store, not this directory. Existing plaintext tokens.json files found here * are imported once and removed. */ oauthDir?: string; } // Root config export interface McpConfig { mcpServers: Record; settings?: McpSettings; } export interface McpAdapterOptions { config?: McpConfig; configPath?: string; } // Alias for clarity export type ServerDefinition = ServerEntry; export interface ToolMetadata { name: string; // Prefixed tool name (e.g., "xcodebuild_list_sims") originalName: string; // Original MCP tool name (e.g., "list_sims") description: string; resourceUri?: string; // For resource tools: the URI to read uiResourceUri?: string; // For app-enabled tools: the UI resource URI inputSchema?: unknown; // JSON Schema for parameters (stored for describe/errors) uiStreamMode?: UiStreamMode; } export interface PromptMetadata { serverName: string; originalName: string; commandName: string; title?: string; description: string; arguments: McpPromptArgument[]; } export interface DirectToolSpec { serverName: string; originalName: string; prefixedName: string; description: string; inputSchema?: unknown; resourceUri?: string; uiResourceUri?: string; uiStreamMode?: UiStreamMode; } export interface ServerProvenance { path: string; kind: 'user' | 'project' | 'import'; importKind?: string; } export interface McpAuthResult { ok: boolean; message?: string; } export interface CachedTool { name: string; description?: string; inputSchema?: unknown; uiResourceUri?: string; uiStreamMode?: 'eager' | 'stream-first'; } export interface CachedResource { uri: string; name: string; description?: string; } export interface CachedPrompt { name: string; title?: string; description?: string; arguments?: { name: string; description?: string; required?: boolean }[]; } export interface ServerCacheEntry { configHash: string; tools: CachedTool[]; resources: CachedResource[]; prompts?: CachedPrompt[]; instructions?: string; cachedAt: number; } export interface MetadataCache { version: number; servers: Record; } export interface McpPanelCallbacks { reconnect: (serverName: string) => Promise; canAuthenticate: (serverName: string) => boolean; authenticate: (serverName: string) => Promise; getConnectionStatus: (serverName: string) => 'connected' | 'idle' | 'failed' | 'needs-auth' | 'disabled'; getFailureMessage?: (serverName: string) => string | null; refreshCacheAfterReconnect: (serverName: string) => ServerCacheEntry | null; } export interface McpPanelResult { changes: Map; canceled: boolean; } /** * Get server prefix based on tool prefix mode. */ export function getServerPrefix(serverName: string, mode: ToolPrefix): string { if (mode === 'none') { return ''; } if (mode === 'short') { let short = serverName.replace(/-?mcp$/i, '').replaceAll('-', '_'); if (!short) { short = 'mcp'; } return short; } if (mode === 'mcp') { return `mcp__${serverName.replaceAll('-', '_')}`; } return serverName.replaceAll('-', '_'); } /** * Format a tool name with server prefix. * With prefix "none" the command is bare and dash-separated * (e.g. pix_frontend_vanilla_reactive -> `pix-frontend-vanilla-reactive`). */ export function formatToolName(toolName: string, serverName: string, prefix: ToolPrefix): string { const p = getServerPrefix(serverName, prefix); const sanitized = toolName.replaceAll('.', '_'); if (!p) { return sanitized.replaceAll('_', '-'); } // "none": bare dash-separated command return `${p}_${sanitized}`; } export function resolveToolPrefix(definition?: Pick, globalPrefix?: ToolPrefix): ToolPrefix { return definition?.toolPrefix ?? globalPrefix ?? 'server'; } export function sanitizePromptName(name: string): string { const cleaned = name.replaceAll(/[^A-Za-z0-9_-]+/g, '_').replaceAll(/^[_-]+|[_-]+$/g, ''); if (!cleaned) { return 'prompt'; } return /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned; } /** * Format an MCP prompt as a slash command. With prefix "none" the command is * the bare sanitized prompt name (e.g. `pix-code-review`); every other mode * keeps the legacy `mcp____` shape. */ export function formatPromptCommandName(promptName: string, serverName: string, prefix: ToolPrefix): string { const serverPart = getServerPrefix(serverName, prefix); const sanitized = sanitizePromptName(promptName); if (!serverPart) { return sanitized; } // "none": bare prompt command return `mcp__${serverPart}__${sanitized}`; } function normalizeToolName(value: string): string { return value.replaceAll('-', '_'); } function getToolNameCandidates(toolName: string, serverName: string, prefix: ToolPrefix): Set { return new Set([ normalizeToolName(toolName), normalizeToolName(formatToolName(toolName, serverName, prefix)), normalizeToolName(formatToolName(toolName, serverName, 'server')), normalizeToolName(formatToolName(toolName, serverName, 'short')), normalizeToolName(formatToolName(toolName, serverName, 'mcp')), ]); } function globToRegExp(pattern: string): RegExp { const escaped = pattern .replaceAll(/[.+^${}()|[\]\\]/g, '\\$&') .replaceAll('*', '.*') .replaceAll('?', '.'); return new RegExp(`^${escaped}$`); } function matchesToolPattern(candidates: Set, patterns?: unknown): boolean { if (!Array.isArray(patterns) || patterns.length === 0) { return false; } for (const pattern of patterns) { if (typeof pattern !== 'string') { continue; } const normalized = normalizeToolName(pattern); if (!normalized.includes('*') && !normalized.includes('?') && candidates.has(normalized)) { return true; } if ( (normalized.includes('*') || normalized.includes('?')) && [...candidates].some((candidate) => globToRegExp(normalized).test(candidate)) ) { return true; } } return false; } export function isToolIncluded( toolName: string, serverName: string, prefix: ToolPrefix, includeTools?: unknown ): boolean { if (!Array.isArray(includeTools) || includeTools.length === 0) { return true; } return matchesToolPattern(getToolNameCandidates(toolName, serverName, prefix), includeTools); } export function isToolExcluded( toolName: string, serverName: string, prefix: ToolPrefix, excludeTools?: unknown ): boolean { return matchesToolPattern(getToolNameCandidates(toolName, serverName, prefix), excludeTools); } export function isToolAllowed( toolName: string, serverName: string, prefix: ToolPrefix, includeTools?: unknown, excludeTools?: unknown ): boolean { return ( isToolIncluded(toolName, serverName, prefix, includeTools) && !isToolExcluded(toolName, serverName, prefix, excludeTools) ); }