import { join } from "node:path"; import { makeSqliteAppsStore } from "../backing/sqlite-apps-store"; import { makeAppsHttpRoutes } from "../http/routes"; import { registerAppsMcp, type McpServerLike } from "../mcp/register"; import type { ClientResolver } from "./bindings"; import { makeSelfHostAppsRuntime, type SelfHostAppsRuntime } from "./self-host-runtime"; import { appsPlugin, type AppsPluginOptions } from "./apps-plugin"; // --------------------------------------------------------------------------- // One-call self-host wiring for the apps subsystem: build the runtime over the // five seam backings rooted at a data dir, a SQLite-backed metadata store, and // a ClientResolver; return the HTTP route handler, an MCP registrar, the // configured plugin, and a close hook. The self-host app calls this at boot and // mounts the pieces. // --------------------------------------------------------------------------- export interface SelfHostAppsOptions { /** Data dir root for artifacts / scope-db / workflows / metadata. */ readonly dataDir: string; /** Routes bound integration calls to real APIs (the executor invoke path). * The boot-time default (used when no per-request `makeResolver` is given). */ readonly resolver: ClientResolver; /** The single scope this self-host instance serves (single-tenant). */ readonly scope: string; /** Optional binding resolution for the plugin's catalog invoke path. */ readonly resolveBindings?: AppsPluginOptions["resolveBindings"]; /** Optional per-request resolver factory for the catalog invoke path (built * from the invoking executor context). */ readonly makeResolver?: AppsPluginOptions["makeResolver"]; /** Authenticate the apps HTTP surface (publish/invoke/workflows/ui/SSE). The * host threads its identity check here so this package doesn't import the * host's auth stack. Omit to leave the surface open (tests). */ readonly authenticate?: (request: Request) => Promise; /** Absolute base URL (e.g. `https://host`) used to build the HTTP fallback URL * for `apps_open_ui` when the MCP client can't render UI inline. Omit to * serve no fallback URL (the tool returns `fallback_unavailable`). */ readonly webBaseUrl?: string; /** Lazy variant of `webBaseUrl` for hosts whose base URL is resolved AFTER the * subsystem singleton is built (self-host: config.ts builds it, app.ts sets * the URL). Read per registration; takes precedence over `webBaseUrl`. */ readonly webBaseUrlFn?: () => string | undefined; } export interface SelfHostApps { readonly runtime: SelfHostAppsRuntime["runtime"]; /** Extension route: `{ path: "/api/apps/*", handler }`. */ readonly http: ReturnType; /** Register the apps MCP tools/resources onto an McpServer. */ readonly registerMcp: (server: McpServerLike) => void; /** The configured source plugin (add to the executor plugin tuple). */ readonly plugin: ReturnType; readonly close: () => Promise; } export const makeSelfHostApps = (options: SelfHostAppsOptions): SelfHostApps => { const store = makeSqliteAppsStore({ path: join(options.dataDir, "apps", "metadata.db") }); const host = makeSelfHostAppsRuntime({ dataDir: options.dataDir, store, resolver: options.resolver, }); const http = makeAppsHttpRoutes({ runtime: host.runtime, authenticate: options.authenticate, }); const plugin = appsPlugin({ runtime: host.runtime, resolveBindings: options.resolveBindings, makeResolver: options.makeResolver, }); return { runtime: host.runtime, http, registerMcp: (server) => registerAppsMcp(server, { runtime: host.runtime, scope: options.scope, // Read the base URL lazily so a host that resolves it after building the // subsystem singleton still gets a working fallback URL. uiDocumentUrl: (scope, name) => { const base = options.webBaseUrlFn?.() ?? options.webBaseUrl; if (!base) return undefined; return `${base.replace(/\/$/, "")}/api/apps/${encodeURIComponent( scope, )}/ui/${encodeURIComponent(name)}?document=html`; }, }), plugin, close: host.close, }; };