/** * Photon MCP Server * * Wraps a .photon.ts file as an MCP server using @modelcontextprotocol/sdk * Supports both stdio and SSE transports */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { PhotonLoader } from './loader.js'; import { PhotonClassExtended } from '@portel/photon-core'; import type { Marketplace, PhotonMetadata } from './marketplace-manager.js'; import { Logger, LoggerOptions } from './shared/logger.js'; import { type ChannelPermissionResponse } from './channel-manager.js'; export declare class HotReloadDisabledError extends Error { constructor(message: string); } export type TransportType = 'stdio' | 'sse'; /** * UI format types for client compatibility * - 'sep-1865': SEP-1865 standard (Anthropic + OpenAI unified format) * - 'photon': Legacy Photon format * - 'none': Text-only, no UI support */ export type UIFormat = 'sep-1865' | 'none'; export interface UnresolvedPhoton { name: string; workingDir: string; sources: Array<{ marketplace: Marketplace; metadata?: PhotonMetadata; }>; recommendation?: string; } export interface PhotonServerOptions { filePath: string; devMode?: boolean; transport?: TransportType; port?: number; logOptions?: LoggerOptions; unresolvedPhoton?: UnresolvedPhoton; /** Working directory override (base dir for state/config/cache) */ workingDir?: string; /** Pre-imported module (for compiled binaries — skips file I/O and compilation) */ preloadedModule?: { default: any; middleware?: any[]; }; /** Embedded source code (for compiled binaries — used for metadata extraction) */ embeddedSource?: string; /** Pre-loaded @photon dependency modules (for compiled binaries) */ preloadedDependencies?: Map; /** Embedded frontend assets (for compiled binaries built with --with-app) */ embeddedAssets?: { indexHtml: string; bundleJs: string; }; /** Embedded @ui HTML templates: photonName → { assetId → html content } */ embeddedUITemplates?: Record>; /** * Embedded asset tree for `//assets/**` (v1.29 Track E). * Shape: photonName → { relativePath → utf-8 content }. Consumed by the * directory-style serving path so SPA chunks (sibling JS/CSS next to a * declared @ui index.html) resolve in standalone binaries without a * filesystem lookup. */ embeddedAssetTree?: Record>; /** Channel mode — declares channel capabilities for target clients */ channelMode?: boolean; /** Channel name — becomes the MCP server name and */ channelName?: string; /** Target channel protocols, e.g. ['claude']. Determines which capabilities to declare. */ channelTargets?: string[]; /** Channel instructions — goes into Claude's system prompt */ channelInstructions?: string; } export type { ChannelPermissionRequest, ChannelPermissionResponse } from './channel-manager.js'; export declare class PhotonServer { private loader; private mcp; private server; private taskExecutor; private options; private mcpClientFactory; private httpServer; private sseSessions; private devMode; private hotReloadDisabled; private lastReloadError?; private statusClients; private channelManager; private daemonName; /** Tracked instance name for daemon drift recovery (STDIO path) */ private daemonInstanceName?; /** Tracked instance names per SSE session for daemon drift recovery */ private sseInstanceNames; /** * Track C closure: per-claim instance pool. Populated lazily on first * request from a new authenticated caller when the photon declares * `@stateful` + `@auth`. Without this, every authenticated caller * shares `this.mcp.instance` — meaning two users on the standalone * HTTP server race on the same `this.tasks` / `this.memory`. * * Daemon path doesn't need this — it has its own per-instance loader * (`session-manager.ts`). Cloudflare doesn't need it either — the * outer Worker selects a DO per claim before the request arrives. * This pool is the standalone HTTP server's equivalent. */ private instancePool; /** * True iff the loaded photon declares both `@stateful` and `@auth` — * the only shape that actually wants per-caller state isolation. For * everything else, instance routing is a no-op and `this.mcp` is used * directly. */ private requiresInstanceRouting; /** Whether client capabilities have been logged (one-time on first tools/list) */ private clientCapabilitiesLogged; /** Client capability detection and negotiation */ private capabilityNegotiator; /** Write queue for serialized STDIO notifications (prevents interleaving from concurrent generators) */ private _notifyQueue; /** Compatibility alias for tests that seed raw capabilities directly on PhotonServer */ rawClientCapabilities: WeakMap, Record>; /** Resource listing, reading, and asset serving */ private resourceServer; /** * Server-wide registry of `resources/subscribe` state. The STDIO server * registers one persistent sink; each SSE session registers a per-session * sink at connect and clears its subscriptions at close. */ private subscriptions; /** Stable sink for the STDIO server, registered once on first subscribe. */ private stdioSink?; /** * Per-server roots cache. Populated on `oninitialized` if the client * declared the `roots` capability, refreshed on * `notifications/roots/list_changed`. Read at tool-call time and threaded * into ALS so `this.roots` resolves synchronously inside photon code. * * WeakMap-keyed so dead session servers stop holding cache entries * automatically when SSE sessions disconnect. */ private rootsByServer; private currentStatus; private logger; /** Get the loaded photon (available after start()) */ getLoadedPhoton(): PhotonClassExtended | null; /** Get the loader instance (for scheduler registration in compiled binaries) */ getLoader(): PhotonLoader; constructor(options: PhotonServerOptions); createScopedLogger(scope: string): Logger; getLogger(): Logger; private log; /** * Send a permission response back to the client. * Delegates to ChannelManager. */ respondToPermission(response: ChannelPermissionResponse): void; /** * Log client identity and capabilities for debugging tier detection */ private logClientCapabilities; /** * Send a notification through the STDIO write queue. * Serializes writes so concurrent generators don't interleave JSON on stdout. */ private queueNotification; /** * Create an MCP-aware input provider for generator ask yields * * Uses MCP elicitInput() when client supports elicitation, * otherwise falls back to readline prompts. */ private createMCPInputProvider; /** * Create an MCP-aware sampling provider for `this.sample()`. * * When the client advertises the `sampling` capability, this returns * a provider that forwards to `server.createMessage(...)`. When the * client doesn't, the provider throws — matching what the user wired * into Photon base's `this.sample()` (explicit failure rather than * silent fallback to a canned string). */ private createMCPSamplingProvider; /** * Build MCP elicit request params from a Photon ask yield */ private buildElicitParams; /** * Extract value from elicitation response content */ private extractElicitValue; /** * Get default value for an ask when elicitation is not available or declined */ private getDefaultForAsk; /** Cache for @choice-from resolved values: key = "toolName.field", value = { values, resolvedAt } */ private choiceFromCache; private static readonly CHOICE_FROM_CACHE_TTL; /** * Resolve all x-choiceFrom fields in a tool's inputSchema by calling the referenced tool. * Mutates the inputSchema in-place (sets enum from tool results). */ private resolveChoiceFromFields; private handleListTools; /** * Resolve which photon instance handles this call. For `@stateful` + * `@auth` photons we pull claims from the request's `authInfo.extra` * (populated by `BeamCompatTransport` from the HTTP headers), look up * the binding rule from the `@auth` directive, and lazy-load a fresh * photon instance keyed by that claim value. Subsequent calls from * the same caller reuse the cached instance so `this.memory` / * `this.tasks` persist across requests within their per-user scope. * * Returns `this.mcp` (the shared singleton) when the photon doesn't * declare per-caller routing, when claims are missing, or when the * binding rule yields no instance name. The fallback is intentional: * unauthenticated requests still execute, they just share the default * instance — same as a v1.28 photon would. */ private resolveInstanceMcp; private handleCallTool; private handleListPrompts; private handleGetPrompt; /** * Unified tools/call handling for every MCP transport. * * Both setupHandlers (STDIO) and setupSessionHandlers (SSE) delegate here * so @async fire-and-forget, task-mode dispatch, config elicitation retry, * and error formatting behave identically regardless of how the request * arrived. Session-specific routing (which server gets notifications, * which instance name applies) comes exclusively from ctx. */ private handleCallToolRequest; /** * Set up MCP protocol handlers (STDIO transport) */ private setupHandlers; /** * Wire up `roots/list` discovery + `notifications/roots/list_changed` * refresh for one Server instance. Called once for the STDIO server and * once per SSE session. * * Eager fetch on initialize keeps `this.roots` synchronous inside photon * code — clients that don't declare the capability skip the fetch * entirely. The cache is server-scoped (per-session for SSE) so two * clients with different working directories don't see each other's * roots. */ private setupRootsForServer; private refreshRootsCache; /** * Format tool result as text */ private formatResult; /** * Format template result to MCP prompt response */ private formatTemplateResult; /** * Compatibility wrappers for tests and older internal call sites after * resource helpers moved into ResourceServer. */ isUriTemplate(uri: string): boolean; matchUriPattern(pattern: string, uri: string): boolean; parseUriParams(pattern: string, uri: string): Record; formatStaticResult(result: any, mimeType?: string): any; clientSupportsUI(server: Server): boolean; buildUIToolMeta(uiId: string): Record; /** * Format error for AI consumption * Provides structured, actionable error messages */ private formatError; /** * Build placeholder tools from unresolved photon manifest metadata */ private buildPlaceholderTools; /** * Resolve an unresolved photon (deferred conflict resolution) * * If the client supports elicitation, presents marketplace choices. * Otherwise, auto-picks the recommendation. */ private resolveUnresolvedPhoton; /** * Download a photon from a marketplace source, save to workingDir, and load it */ private downloadAndLoadPhoton; /** * Attempt config elicitation to resolve missing env vars, then retry tool call */ private attemptConfigElicitation; /** * Initialize and start the server */ start(): Promise; /** * Start server with stdio transport. * @param webPort - when set, start a companion HTTP server on this port * to serve @get/@post web routes for stateful photons. */ private startStdio; /** * Dispatch an incoming HTTP request to the photon's @get/@post web routes. * Used by the companion HTTP server that runs alongside STDIO stateful photons. */ private handleWebRoute; private serveTopLevelUiAsset; /** * Start server with SSE transport (HTTP) */ private startSSE; /** * List all photons in the .photon directory */ private listAllPhotons; /** * Generate playground HTML for interactive testing */ private getPlaygroundHTML; /** * Handle new SSE connection */ private handleSSEConnection; /** * Handle incoming SSE message */ private handleSSEMessage; /** * Set up handlers for a session-specific MCP server * This duplicates handlers from the main server to each session */ private setupSessionHandlers; /** * Stop the server */ stop(): Promise; private buildStatusSnapshot; private handleStatusStream; private broadcastReloadStatus; private pushStatusUpdate; /** * Reload the MCP file (for dev mode hot reload) */ private reloadFailureCount; private readonly MAX_RELOAD_FAILURES; private reloadRetryTimeout?; reload(): Promise; /** * Send list_changed notifications to inform client that tools/prompts/resources changed * Used after hot reload to tell clients (like Claude Desktop) to refresh */ private notifyListsChanged; } //# sourceMappingURL=server.d.ts.map