From 7ac7eae833478cbfdd668775281acf4458be644d Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 25 Mar 2026 03:51:30 +0200 Subject: [PATCH 01/12] feat: add SEA build support with reinitialization handling for transport --- .gitignore | 1 + .../e2e/cli-daemon-sea.e2e.spec.ts | 361 ++++++++++++++++++ .../demo-e2e-cli-exec/e2e/helpers/exec-cli.ts | 84 ++++ .../e2e/esm-hot-reload.e2e.spec.ts | 14 +- .../e2e/session-reconnect.e2e.spec.ts | 166 ++++++++ .../exec/__tests__/generate-cli-entry.spec.ts | 31 +- .../__tests__/generated-cli-smoke.spec.ts | 5 + .../exec/cli-runtime/generate-cli-entry.ts | 29 +- .../streamable-http-transport.spec.ts | 83 ++++ .../adapters/streamable-http-transport.ts | 24 ++ .../adapters/transport.local.adapter.ts | 16 + .../transport.streamable-http.adapter.ts | 16 + .../handle.streamable-http.reconnect.spec.ts | 145 +++++++ .../flows/handle.streamable-http.flow.ts | 12 + libs/sdk/src/transport/transport.local.ts | 8 + libs/sdk/src/transport/transport.remote.ts | 8 + libs/sdk/src/transport/transport.types.ts | 12 + 17 files changed, 1004 insertions(+), 11 deletions(-) create mode 100644 apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts diff --git a/.gitignore b/.gitignore index 04640d470..22dab542a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # compiled output dist +dist-sea tmp out-tsc diff --git a/apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts b/apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts new file mode 100644 index 000000000..f67496530 --- /dev/null +++ b/apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts @@ -0,0 +1,361 @@ +/** + * E2E tests for the SEA (Single Executable Application) CLI daemon. + * + * Tests the full production flow: build SEA binary -> install -> daemon + * start/status/stop/logs -> MCP connectivity over Unix socket. + * + * This covers the critical path that was broken when the daemon spawned + * `node -e` with external requires that couldn't resolve from the installed + * location. The fix spawns `process.execPath` (the SEA binary itself) with + * `__FRONTMCP_DAEMON_MODE=1` so all inlined code is available. + * + * If the SEA build toolchain is unavailable (e.g., missing postject), + * all tests pass trivially with a warning. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { execFileSync } from 'child_process'; +import { ensureSeaBuild, isSeaBuildAvailable, runSeaCli } from './helpers/exec-cli'; +import { McpJsonRpcClient, httpOverSocket, waitForSocket } from './helpers/mcp-client'; + +const APP_NAME = 'cli-exec-demo'; + +// State shared across the ordered test suite +let seaAvailable = false; +let homeDir: string; +let prefixDir: string; +let binDir: string; +let installedBinaryPath: string; +let socketPath: string; +let pidPath: string; +let daemonPid: number | null = null; + +/** Run the installed SEA binary with FRONTMCP_HOME isolation. */ +function runInstalledCli( + args: string[], + extraEnv?: Record, +): { stdout: string; stderr: string; exitCode: number } { + try { + const stdout = execFileSync(installedBinaryPath, args, { + timeout: 30000, + encoding: 'utf-8', + env: { ...process.env, NODE_ENV: 'test', FRONTMCP_HOME: homeDir, ...extraEnv }, + }); + return { stdout: stdout.toString(), stderr: '', exitCode: 0 }; + } catch (err: unknown) { + const error = err as { stdout?: string | Buffer; stderr?: string | Buffer; status?: number }; + return { + stdout: (error.stdout || '').toString(), + stderr: (error.stderr || '').toString(), + exitCode: error.status ?? 1, + }; + } +} + +function killByPid(pid: number): void { + try { + process.kill(pid, 'SIGTERM'); + } catch { + /* already dead */ + } +} + +async function waitForFile(filePath: string, timeoutMs = 10000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (fs.existsSync(filePath)) return true; + await new Promise((r) => setTimeout(r, 100)); + } + return false; +} + +describe('SEA CLI Daemon E2E', () => { + beforeAll(async () => { + seaAvailable = await ensureSeaBuild(); + if (!seaAvailable) { + console.warn('[e2e:sea] SEA binary not available — all daemon-sea tests will pass trivially.'); + return; + } + + // Set up isolated directories + homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontmcp-sea-daemon-')); + prefixDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontmcp-sea-install-')); + binDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontmcp-sea-bin-')); + + // Install the SEA binary to a temp prefix + const { stdout, exitCode } = runSeaCli(['install', '--prefix', prefixDir, '--bin-dir', binDir]); + if (exitCode !== 0) { + console.warn('[e2e:sea] Install failed:', stdout); + seaAvailable = false; + return; + } + + // Resolve the installed binary path + const appDir = path.join(prefixDir, 'apps', APP_NAME); + const seaBinaryInInstall = path.join(appDir, `${APP_NAME}-cli-bin`); + const symlinkPath = path.join(binDir, APP_NAME); + + if (fs.existsSync(symlinkPath)) { + installedBinaryPath = fs.realpathSync(symlinkPath); + } else if (fs.existsSync(seaBinaryInInstall)) { + installedBinaryPath = seaBinaryInInstall; + } else { + console.warn('[e2e:sea] Could not find installed SEA binary'); + seaAvailable = false; + return; + } + + // Pre-calculate daemon paths + socketPath = path.join(homeDir, 'sockets', `${APP_NAME}.sock`); + pidPath = path.join(homeDir, 'pids', `${APP_NAME}.pid`); + }, 180000); + + afterAll(async () => { + if (!seaAvailable) return; + + // Safety: kill daemon if still running + if (daemonPid) { + killByPid(daemonPid); + daemonPid = null; + } + try { + runInstalledCli(['daemon', 'stop']); + } catch { + /* ok */ + } + + await new Promise((r) => setTimeout(r, 500)); + + // Clean up temp dirs + for (const dir of [homeDir, prefixDir, binDir]) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch { + /* ok */ + } + } + }); + + // ─── Build & Install Verification ────────────────────────────────────── + + it('should have SEA CLI binary available', () => { + if (!seaAvailable) return; + expect(fs.existsSync(installedBinaryPath)).toBe(true); + const stats = fs.statSync(installedBinaryPath); + expect(stats.mode & 0o111).not.toBe(0); // executable + }); + + // ─── Daemon Lifecycle ────────────────────────────────────────────────── + + it('daemon status before start should show not running', () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'status']); + expect(exitCode).toBe(0); + expect(stdout).toContain('Not running'); + }); + + it('daemon start should succeed with PID', async () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'start']); + expect(exitCode).toBe(0); + expect(stdout).toMatch(/Daemon (started|already running)/); + expect(stdout).toMatch(/PID: \d+/); + + const pidMatch = stdout.match(/PID: (\d+)/); + if (pidMatch) { + daemonPid = parseInt(pidMatch[1], 10); + } + + // Wait for socket file to appear + if (!fs.existsSync(socketPath)) { + const appeared = await waitForFile(socketPath, 15000); + expect(appeared).toBe(true); + } + }); + + it('daemon socket should respond to health check', async () => { + if (!seaAvailable) return; + await waitForSocket(socketPath, 20000); + const response = await httpOverSocket(socketPath, { path: '/health' }); + expect(response.statusCode).toBe(200); + }); + + it('daemon status should show running after start', () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'status']); + expect(exitCode).toBe(0); + expect(stdout).toContain('Running'); + expect(stdout).toMatch(/PID: \d+/); + }); + + it('daemon double start should detect already running', () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'start']); + expect(exitCode).toBe(0); + expect(stdout).toContain('already running'); + }); + + it('PID file should have correct structure', () => { + if (!seaAvailable) return; + expect(fs.existsSync(pidPath)).toBe(true); + const pidData = JSON.parse(fs.readFileSync(pidPath, 'utf8')); + expect(pidData.pid).toEqual(expect.any(Number)); + expect(pidData.socketPath).toContain('.sock'); + expect(pidData.startedAt).toEqual(expect.any(String)); + expect(() => process.kill(pidData.pid, 0)).not.toThrow(); + }); + + it('daemon logs should not contain ZodError or MODULE_NOT_FOUND', () => { + if (!seaAvailable) return; + const logPath = path.join(homeDir, 'logs', `${APP_NAME}.log`); + if (fs.existsSync(logPath)) { + const content = fs.readFileSync(logPath, 'utf8'); + expect(content).not.toContain('ZodError'); + expect(content).not.toContain('MODULE_NOT_FOUND'); + expect(content).not.toContain('Cannot find module'); + } + }); + + it('daemon logs command should return output', () => { + if (!seaAvailable) return; + const { exitCode } = runInstalledCli(['daemon', 'logs', '-n', '20']); + expect(exitCode).toBe(0); + }); + + // ─── MCP Connectivity ────────────────────────────────────────────────── + + it('should handle MCP initialize handshake via daemon socket', async () => { + if (!seaAvailable) return; + const client = McpJsonRpcClient.forSocket(socketPath, '/'); + const result = await client.initialize(); + + expect(result.result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(client.getSessionId()).toBeDefined(); + + const initResult = result.result as Record; + expect(initResult['protocolVersion']).toBeDefined(); + expect(initResult['capabilities']).toBeDefined(); + }); + + it('should list tools via daemon socket', async () => { + if (!seaAvailable) return; + const client = McpJsonRpcClient.forSocket(socketPath, '/'); + await client.initialize(); + + const result = await client.listTools(); + expect(result.error).toBeUndefined(); + + const toolsResult = result.result as { tools: Array<{ name: string }> }; + const toolNames = toolsResult.tools.map((t) => t.name); + expect(toolNames).toContain('add'); + expect(toolNames).toContain('greet'); + expect(toolNames).toContain('transform_data'); + }); + + it('should execute tool via daemon socket', async () => { + if (!seaAvailable) return; + const client = McpJsonRpcClient.forSocket(socketPath, '/'); + await client.initialize(); + + const result = await client.callTool('add', { a: 10, b: 20 }); + expect(result.error).toBeUndefined(); + + const callResult = result.result as { content: Array<{ text: string }> }; + const text = callResult.content.map((c) => c.text).join(''); + expect(text).toContain('30'); + }); + + it('should list resources via daemon socket', async () => { + if (!seaAvailable) return; + const client = McpJsonRpcClient.forSocket(socketPath, '/'); + await client.initialize(); + + const result = await client.listResources(); + expect(result.error).toBeUndefined(); + + const resourcesResult = result.result as { resources: Array<{ name: string; uri: string }> }; + expect(resourcesResult.resources.length).toBeGreaterThan(0); + }); + + it('should list prompts via daemon socket', async () => { + if (!seaAvailable) return; + const client = McpJsonRpcClient.forSocket(socketPath, '/'); + await client.initialize(); + + const result = await client.listPrompts(); + expect(result.error).toBeUndefined(); + + const promptsResult = result.result as { prompts: Array<{ name: string }> }; + const promptNames = promptsResult.prompts.map((p) => p.name); + expect(promptNames).toContain('code-review'); + }); + + // ─── Daemon Stop & Cleanup ───────────────────────────────────────────── + + it('daemon stop should succeed', () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'stop']); + expect(exitCode).toBe(0); + expect(stdout).toContain('Daemon stopped'); + daemonPid = null; + }); + + it('daemon status after stop should show not running', () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'status']); + expect(exitCode).toBe(0); + expect(stdout).toMatch(/Not running/); + }); + + it('socket file should be removed after stop', () => { + if (!seaAvailable) return; + expect(fs.existsSync(socketPath)).toBe(false); + }); + + it('PID file should be removed after stop', () => { + if (!seaAvailable) return; + expect(fs.existsSync(pidPath)).toBe(false); + }); + + // ─── Daemon Restart ──────────────────────────────────────────────────── + + it('should start again after stop', async () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'start']); + expect(exitCode).toBe(0); + expect(stdout).toMatch(/Daemon started/); + + const pidMatch = stdout.match(/PID: (\d+)/); + if (pidMatch) { + daemonPid = parseInt(pidMatch[1], 10); + } + + if (!fs.existsSync(socketPath)) { + await waitForFile(socketPath, 15000); + } + await waitForSocket(socketPath, 20000); + }); + + it('should serve MCP after restart', async () => { + if (!seaAvailable) return; + const client = McpJsonRpcClient.forSocket(socketPath, '/'); + const initResult = await client.initialize(); + expect(initResult.result).toBeDefined(); + + const toolResult = await client.callTool('add', { a: 5, b: 7 }); + expect(toolResult.error).toBeUndefined(); + const text = (toolResult.result as { content: Array<{ text: string }> }).content.map((c) => c.text).join(''); + expect(text).toContain('12'); + }); + + it('should stop cleanly after restart', () => { + if (!seaAvailable) return; + const { stdout, exitCode } = runInstalledCli(['daemon', 'stop']); + expect(exitCode).toBe(0); + expect(stdout).toContain('Daemon stopped'); + daemonPid = null; + }); +}); diff --git a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts index 0ae04ffcf..09c56fc3c 100644 --- a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts +++ b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts @@ -1,14 +1,22 @@ import { execFileSync, spawn, ChildProcess } from 'child_process'; +import * as fs from 'fs'; import * as path from 'path'; import type { StdioOptions } from 'node:child_process'; +const APP_NAME = 'cli-exec-demo'; const FIXTURE_DIR = path.resolve(__dirname, '../../fixture'); const DIST_DIR = path.join(FIXTURE_DIR, 'dist'); const CLI_BUNDLE = path.join(DIST_DIR, 'cli-exec-demo-cli.bundle.js'); const SERVER_BUNDLE = path.join(DIST_DIR, 'cli-exec-demo.bundle.js'); const MANIFEST = path.join(DIST_DIR, 'cli-exec-demo.manifest.json'); +// SEA build support — separate output dir to avoid interfering with regular builds +const SEA_DIST_DIR = path.join(FIXTURE_DIR, 'dist-sea'); +const SEA_CLI_BINARY = path.join(SEA_DIST_DIR, `${APP_NAME}-cli-bin`); + let buildDone = false; +let seaBuildDone = false; +let seaBuildAvailable = false; export function getDistDir(): string { return DIST_DIR; @@ -119,3 +127,79 @@ export function spawnCli(args: string[], timeoutMs = 3000, extraEnv?: Record { + if (seaBuildDone) return seaBuildAvailable; + + const rootDir = path.resolve(FIXTURE_DIR, '../../../..'); + const frontmcpBin = path.join(rootDir, 'libs', 'cli', 'dist', 'src', 'core', 'cli.js'); + + console.log('[e2e:sea] Building CLI exec bundle with SEA...'); + try { + execFileSync('node', [frontmcpBin, 'build', '--exec', '--cli', '--sea', '--out-dir', 'dist-sea'], { + cwd: FIXTURE_DIR, + stdio: 'pipe', + timeout: 180000, + env: { ...process.env, NODE_ENV: 'production' }, + }); + console.log('[e2e:sea] SEA build complete.'); + } catch (err: unknown) { + const error = err as { stderr?: string | Buffer }; + console.warn( + '[e2e:sea] SEA build failed (CLI bundle may still be available):', + (error.stderr || '').toString().slice(0, 300), + ); + } + + seaBuildAvailable = fs.existsSync(SEA_CLI_BINARY); + seaBuildDone = true; + + if (seaBuildAvailable) { + console.log(`[e2e:sea] SEA CLI binary available: ${SEA_CLI_BINARY}`); + } else { + console.warn('[e2e:sea] SEA CLI binary not available — SEA daemon tests will be skipped.'); + } + + return seaBuildAvailable; +} + +/** + * Run the SEA CLI binary directly (not via node). + */ +export function runSeaCli(args: string[], extraEnv?: Record): CliResult { + try { + const stdout = execFileSync(SEA_CLI_BINARY, args, { + cwd: SEA_DIST_DIR, + timeout: 30000, + encoding: 'utf-8', + env: { ...process.env, NODE_ENV: 'test', ...extraEnv }, + }); + return { stdout: stdout.toString(), stderr: '', exitCode: 0 }; + } catch (err: unknown) { + const error = err as { stdout?: string | Buffer; stderr?: string | Buffer; status?: number }; + return { + stdout: (error.stdout || '').toString(), + stderr: (error.stderr || '').toString(), + exitCode: error.status ?? 1, + }; + } +} diff --git a/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts b/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts index ec962e7fd..cfe453205 100644 --- a/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts +++ b/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts @@ -116,10 +116,16 @@ test.describe('ESM Hot-Reload E2E', () => { }); test('detects new version and registers new tools', async ({ mcp }) => { - // Step 1: Verify initial state — only echo and add - const initialTools = await mcp.tools.list(); - const initialNames = initialTools.map((t: { name: string }) => t.name); - log('[TEST] Initial tools:', initialNames); + // Step 1: Wait for ESM tools to load (async loading after server start) + let initialTools: Array<{ name: string }> = []; + let initialNames: string[] = []; + for (let i = 0; i < 15; i++) { + initialTools = await mcp.tools.list(); + initialNames = initialTools.map((t: { name: string }) => t.name); + log('[TEST] Poll initial tools:', initialNames); + if (initialNames.includes('esm:echo')) break; + await new Promise((resolve) => setTimeout(resolve, 2000)); + } expect(initialTools).toContainTool('esm:echo'); expect(initialTools).toContainTool('esm:add'); diff --git a/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts b/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts index e9e785664..1bd46b30e 100644 --- a/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts +++ b/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts @@ -766,6 +766,172 @@ test.describe('Session Reconnect E2E', () => { expect(sse.status).toBe(404); }); }); + + // ═══════════════════════════════════════════════════════════════════ + // INITIALIZE RETRY WITH SAME SESSION (REGRESSION) + // + // These tests cover the exact production bug where a client retries + // initialize with a session whose transport is already initialized, + // causing a 400 "server already initialized" error. + // + // The scenario: DELETE → init (get session B) → stale notification + // with old session A → 404 → client retries init with B → must be 200. + // ═══════════════════════════════════════════════════════════════════ + + test.describe('Initialize retry with same session (regression #reinit)', () => { + test('exact production bug: DELETE → init → stale notification → retry initialize with same session', async ({ + server, + }) => { + // Step 1: Full initial handshake with session A + const initA = await sendInitialize(server.info.baseUrl); + expect(initA.status).toBe(200); + const sessionA = initA.sessionId; + if (!sessionA) throw new Error('Expected session A'); + + const notifA = await sendNotificationInitialized(server.info.baseUrl, sessionA); + expect(notifA.status).toBe(202); + + // Step 2: Verify session A works + const toolA = await sendToolCall(server.info.baseUrl, sessionA, 'get-session-info'); + expect(toolA.status).toBe(200); + + // Step 3: DELETE session A + const del = await sendDelete(server.info.baseUrl, sessionA); + expect(del.status).toBe(204); + + // Step 4: Initialize with stale session A → reconnect creates session B + const initB = await sendInitialize(server.info.baseUrl, sessionA); + expect(initB.status).toBe(200); + const sessionB = initB.sessionId; + if (!sessionB) throw new Error('Expected session B'); + expect(sessionB).not.toBe(sessionA); + + // Step 5: Client sends notifications/initialized with OLD session A → 404 + const staleNotif = await sendNotificationInitialized(server.info.baseUrl, sessionA); + expect(staleNotif.status).toBe(404); + + // Step 6: THE BUG — Client retries initialize with session B + // Before fix: 400 "server already initialized" + // After fix: 200 with re-initialized session + const retryInit = await sendInitialize(server.info.baseUrl, sessionB); + expect(retryInit.status).toBe(200); + + // Step 7: Complete handshake and verify tools work + const retrySessionId = retryInit.sessionId; + if (!retrySessionId) throw new Error('Expected session after retry'); + + const notifB = await sendNotificationInitialized(server.info.baseUrl, retrySessionId); + expect(notifB.status).toBe(202); + + const toolB = await sendToolCall(server.info.baseUrl, retrySessionId, 'get-session-info'); + expect(toolB.status).toBe(200); + }); + + test('multiple initialize retries on same session should all succeed', async ({ server }) => { + const init1 = await sendInitialize(server.info.baseUrl); + expect(init1.status).toBe(200); + const sessionId = init1.sessionId; + if (!sessionId) throw new Error('Expected session'); + + // Retry initialize 3 times with same session + for (let i = 0; i < 3; i++) { + const retry = await sendInitialize(server.info.baseUrl, sessionId); + expect(retry.status).toBe(200); + } + + // Session should still work after retries + const retryResult = await sendInitialize(server.info.baseUrl, sessionId); + const finalSession = retryResult.sessionId; + if (!finalSession) throw new Error('Expected final session'); + + await sendNotificationInitialized(server.info.baseUrl, finalSession); + const tool = await sendToolCall(server.info.baseUrl, finalSession, 'get-session-info'); + expect(tool.status).toBe(200); + }); + + test('retry initialize then use tools normally', async ({ server }) => { + // Initialize + const init1 = await sendInitialize(server.info.baseUrl); + expect(init1.status).toBe(200); + const s = init1.sessionId; + if (!s) throw new Error('Expected session'); + + // Retry initialize with same session + const init2 = await sendInitialize(server.info.baseUrl, s); + expect(init2.status).toBe(200); + const s2 = init2.sessionId; + if (!s2) throw new Error('Expected session after retry'); + + // Complete handshake + await sendNotificationInitialized(server.info.baseUrl, s2); + + // All standard operations should work + const list = await sendToolsList(server.info.baseUrl, s2); + expect(list.status).toBe(200); + + const tool = await sendToolCall(server.info.baseUrl, s2, 'increment-counter', { amount: 7 }); + expect(tool.status).toBe(200); + const body = tool.body as Record; + const result = body['result'] as Record; + const content = result['content'] as Array<{ text: string }>; + const output = JSON.parse(content[0].text) as { newValue: number }; + expect(output.newValue).toBe(7); + }); + + test('rapid DELETE + init + retry cycles', async ({ server }) => { + for (let cycle = 0; cycle < 3; cycle++) { + // Initialize + const init1 = await sendInitialize(server.info.baseUrl); + expect(init1.status).toBe(200); + const s = init1.sessionId; + if (!s) throw new Error(`Expected session in cycle ${cycle}`); + + // Retry initialize (simulates client retry) + const retry = await sendInitialize(server.info.baseUrl, s); + expect(retry.status).toBe(200); + + const retrySession = retry.sessionId; + if (!retrySession) throw new Error(`Expected retry session in cycle ${cycle}`); + + // Verify it works + await sendNotificationInitialized(server.info.baseUrl, retrySession); + const tool = await sendToolCall(server.info.baseUrl, retrySession, 'get-session-info'); + expect(tool.status).toBe(200); + + // DELETE before next cycle + await sendDelete(server.info.baseUrl, retrySession); + } + }); + + test('concurrent clients: one clients retry does not break another', async ({ server }) => { + // Client A initializes + const initA = await sendInitialize(server.info.baseUrl); + expect(initA.status).toBe(200); + const sA = initA.sessionId; + if (!sA) throw new Error('Expected session A'); + await sendNotificationInitialized(server.info.baseUrl, sA); + + // Client B initializes + const initB = await sendInitialize(server.info.baseUrl); + expect(initB.status).toBe(200); + const sB = initB.sessionId; + if (!sB) throw new Error('Expected session B'); + await sendNotificationInitialized(server.info.baseUrl, sB); + + // Client A retries initialize (should not affect B) + const retryA = await sendInitialize(server.info.baseUrl, sA); + expect(retryA.status).toBe(200); + + // Client B should still work fine + const toolB = await sendToolCall(server.info.baseUrl, sB, 'get-session-info'); + expect(toolB.status).toBe(200); + const bodyB = toolB.body as Record; + const resultB = bodyB['result'] as Record; + const contentB = resultB['content'] as Array<{ text: string }>; + const infoB = JSON.parse(contentB[0].text) as { hasSession: boolean }; + expect(infoB.hasSession).toBe(true); + }); + }); }); // ═══════════════════════════════════════════════════════════════════ diff --git a/libs/cli/src/commands/build/exec/__tests__/generate-cli-entry.spec.ts b/libs/cli/src/commands/build/exec/__tests__/generate-cli-entry.spec.ts index 9af71bccd..20c73d366 100644 --- a/libs/cli/src/commands/build/exec/__tests__/generate-cli-entry.spec.ts +++ b/libs/cli/src/commands/build/exec/__tests__/generate-cli-entry.spec.ts @@ -903,16 +903,39 @@ describe('generateCliEntry', () => { expect(source).toContain('runUnixSocket'); }); - it('should use node -e with runUnixSocket in selfContained/SEA mode', () => { + it('should spawn process.execPath with daemon mode env in selfContained/SEA mode', () => { const source = generateCliEntry(makeOptions({ appName: 'my-server', selfContained: true, })); - // Even in SEA mode, daemon uses node -e with runUnixSocket for compatibility - expect(source).toContain("spawn('node'"); - expect(source).toContain('runUnixSocket'); + // SEA mode: spawn self (process.execPath) instead of node -e + expect(source).toContain('process.execPath'); + expect(source).toContain('__FRONTMCP_DAEMON_MODE'); expect(source).toContain('FRONTMCP_DAEMON_SOCKET'); }); + + it('should include daemon mode handler in header for selfContained mode', () => { + const source = generateCliEntry(makeOptions({ + serverBundleFilename: 'my-app.bundle.js', + selfContained: true, + })); + // Daemon mode guard should appear before commander setup + expect(source).toContain("process.env.__FRONTMCP_DAEMON_MODE === '1'"); + expect(source).toContain('runUnixSocket'); + expect(source).toContain("require('../my-app.bundle.js')"); + expect(source).toContain("require('@frontmcp/sdk')"); + expect(source).toContain("require('reflect-metadata')"); + const daemonIdx = source.indexOf('__FRONTMCP_DAEMON_MODE'); + const commanderIdx = source.indexOf("require('commander')"); + expect(daemonIdx).toBeLessThan(commanderIdx); + }); + + it('should not include daemon mode handler in header for non-selfContained mode', () => { + const source = generateCliEntry(makeOptions({ + selfContained: false, + })); + expect(source).not.toContain('__FRONTMCP_DAEMON_MODE'); + }); }); describe('doctor command', () => { diff --git a/libs/cli/src/commands/build/exec/__tests__/generated-cli-smoke.spec.ts b/libs/cli/src/commands/build/exec/__tests__/generated-cli-smoke.spec.ts index 1eb055534..c4ffa2cb1 100644 --- a/libs/cli/src/commands/build/exec/__tests__/generated-cli-smoke.spec.ts +++ b/libs/cli/src/commands/build/exec/__tests__/generated-cli-smoke.spec.ts @@ -46,6 +46,11 @@ describe('generated CLI smoke tests', () => { expect(() => new Function(stripShebang(source))).not.toThrow(); }); + it('should produce valid JavaScript in selfContained/SEA mode', () => { + const source = generateCliEntry(makeOptions({ selfContained: true })); + expect(() => new Function(stripShebang(source))).not.toThrow(); + }); + it('should produce valid JavaScript with multiple tools', () => { const source = generateCliEntry(makeOptions({ schema: makeSchema({ diff --git a/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts b/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts index 68b42ebfd..b482a7f3d 100644 --- a/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts +++ b/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts @@ -122,7 +122,24 @@ function generateHeader( groupEntries.push(` 'System': []`); return `'use strict'; - +${selfContained ? ` +// SEA daemon mode: when spawned by 'daemon start', run the server directly +// using the inlined (bundled) server code — no external requires needed. +if (process.env.__FRONTMCP_DAEMON_MODE === '1') { + require('reflect-metadata'); + var _dMod = require(${JSON.stringify('../' + serverBundleFilename)}); + var _dSdk = require('@frontmcp/sdk'); + var _FMI = _dSdk.FrontMcpInstance || _dSdk.default.FrontMcpInstance; + var _raw = _dMod.default || _dMod; + var _cfg = (typeof _raw === 'function' && typeof Reflect !== 'undefined' && Reflect.getMetadata) + ? (Reflect.getMetadata('__frontmcp:config', _raw) || _raw) : _raw; + var _sp = process.env.FRONTMCP_DAEMON_SOCKET; + _FMI.runUnixSocket(Object.assign({}, _cfg, { socketPath: _sp })) + .then(function() { console.log('Daemon listening on ' + _sp); }) + .catch(function(e) { console.error('Daemon failed:', e); process.exit(1); }); + return; +} +` : ''} var { Command, Option } = require('commander'); var path = require('path'); var fs = require('fs'); @@ -1299,7 +1316,13 @@ daemonCmd var out = fs.openSync(logPath, 'a'); var err = fs.openSync(logPath, 'a'); - // Start the daemon using runUnixSocket via a small wrapper script +${selfContained ? ` // SEA mode: spawn the binary itself in daemon mode — all code is inlined + env.__FRONTMCP_DAEMON_MODE = '1'; + var child = spawn(process.execPath, [], { + detached: true, + stdio: ['ignore', out, err], + env: env + });` : ` // Start the daemon using runUnixSocket via a small wrapper script // Always use absolute path for the server bundle (SCRIPT_DIR resolves to __dirname at runtime) var serverBundlePath = pathMod.join(SCRIPT_DIR, ${JSON.stringify(serverBundleFilename)}); var daemonScript = 'require("reflect-metadata");' + @@ -1318,7 +1341,7 @@ daemonCmd detached: true, stdio: ['ignore', out, err], env: env - }); + });`} fs.writeFileSync(pidPath, JSON.stringify({ pid: child.pid, diff --git a/libs/sdk/src/transport/adapters/__tests__/streamable-http-transport.spec.ts b/libs/sdk/src/transport/adapters/__tests__/streamable-http-transport.spec.ts index bc7acaf5a..03b323183 100644 --- a/libs/sdk/src/transport/adapters/__tests__/streamable-http-transport.spec.ts +++ b/libs/sdk/src/transport/adapters/__tests__/streamable-http-transport.spec.ts @@ -340,6 +340,89 @@ describe('RecreateableStreamableHTTPServerTransport', () => { }); }); + describe('resetForReinitialization', () => { + it('should reset _initialized to false and sessionId to undefined', () => { + transport.setInitializationState('session-123'); + expect(transport.isInitialized).toBe(true); + + transport.resetForReinitialization(); + + const webTransport = (transport as any)._webStandardTransport; + expect(webTransport._initialized).toBe(false); + expect(webTransport.sessionId).toBeUndefined(); + }); + + it('should return isInitialized === false after reset', () => { + transport.setInitializationState('session-abc'); + expect(transport.isInitialized).toBe(true); + + transport.resetForReinitialization(); + expect(transport.isInitialized).toBe(false); + }); + + it('should handle missing _webStandardTransport gracefully', () => { + (transport as any)._webStandardTransport = undefined; + + expect(() => transport.resetForReinitialization()).not.toThrow(); + expect(transport.isInitialized).toBe(false); + }); + + it('should clear pending init state when transport not ready', () => { + (transport as any)._webStandardTransport = undefined; + transport.setInitializationState('pending-session'); + expect(transport.hasPendingInitState).toBe(true); + + transport.resetForReinitialization(); + expect(transport.hasPendingInitState).toBe(false); + }); + + it('should be safe on an already-uninitialized transport', () => { + expect(transport.isInitialized).toBe(false); + expect(() => transport.resetForReinitialization()).not.toThrow(); + expect(transport.isInitialized).toBe(false); + }); + + it('should be idempotent - multiple reset calls', () => { + transport.setInitializationState('session-xyz'); + + transport.resetForReinitialization(); + transport.resetForReinitialization(); + transport.resetForReinitialization(); + + expect(transport.isInitialized).toBe(false); + expect((transport as any)._webStandardTransport.sessionId).toBeUndefined(); + }); + + it('should not affect other transport instances', () => { + const transport2 = new RecreateableStreamableHTTPServerTransport(); + transport.setInitializationState('session-1'); + transport2.setInitializationState('session-2'); + + transport.resetForReinitialization(); + + expect(transport.isInitialized).toBe(false); + expect(transport2.isInitialized).toBe(true); + expect((transport2 as any)._webStandardTransport.sessionId).toBe('session-2'); + }); + + it('should allow full cycle: init → reset → re-init', () => { + // Phase 1: Initialize + transport.setInitializationState('session-first'); + expect(transport.isInitialized).toBe(true); + expect((transport as any)._webStandardTransport.sessionId).toBe('session-first'); + + // Phase 2: Reset + transport.resetForReinitialization(); + expect(transport.isInitialized).toBe(false); + expect((transport as any)._webStandardTransport.sessionId).toBeUndefined(); + + // Phase 3: Re-initialize with new session + transport.setInitializationState('session-second'); + expect(transport.isInitialized).toBe(true); + expect((transport as any)._webStandardTransport.sessionId).toBe('session-second'); + }); + }); + describe('edge cases', () => { it('should handle very long session IDs', () => { const longSessionId = 'a'.repeat(1000); diff --git a/libs/sdk/src/transport/adapters/streamable-http-transport.ts b/libs/sdk/src/transport/adapters/streamable-http-transport.ts index 31505a207..9b16255f6 100644 --- a/libs/sdk/src/transport/adapters/streamable-http-transport.ts +++ b/libs/sdk/src/transport/adapters/streamable-http-transport.ts @@ -143,6 +143,30 @@ export class RecreateableStreamableHTTPServerTransport extends StreamableHTTPSer this._applyInitState(webTransport, sessionId); } + /** + * Resets the transport's initialization state to allow re-initialization. + * + * This is needed when a client reconnects after terminating its session: + * the cached transport is still marked as initialized, but the client + * needs to re-initialize. Resetting _initialized and sessionId allows + * the MCP SDK to process a fresh initialize request. + * + * This is the inverse of setInitializationState(). + */ + resetForReinitialization(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const webTransport = (this as any)._webStandardTransport; + if (!webTransport) { + this._pendingInitState = undefined; + return; + } + + if ('_initialized' in webTransport) { + webTransport._initialized = false; + webTransport.sessionId = undefined; + } + } + /** * Applies initialization state to the internal transport. * @param webTransport - The internal _webStandardTransport object diff --git a/libs/sdk/src/transport/adapters/transport.local.adapter.ts b/libs/sdk/src/transport/adapters/transport.local.adapter.ts index 81e6deae7..fa6c5d0dd 100644 --- a/libs/sdk/src/transport/adapters/transport.local.adapter.ts +++ b/libs/sdk/src/transport/adapters/transport.local.adapter.ts @@ -93,6 +93,14 @@ export abstract class LocalTransportAdapter { abstract handleRequest(req: AuthenticatedServerRequest, res: ServerResponse): Promise; + /** + * Whether this transport has already been initialized via the MCP initialize handshake. + * Override in subclasses that track initialization state. + */ + get isInitialized(): boolean { + return false; + } + /** * Marks this transport as pre-initialized for session recreation. * Override in subclasses that need to set the MCP SDK's _initialized flag. @@ -101,6 +109,14 @@ export abstract class LocalTransportAdapter { // Default no-op - override in subclasses } + /** + * Resets initialization state to allow re-initialization. + * Override in subclasses that support session re-initialization. + */ + resetForReinitialization(): void { + // Default no-op - override in subclasses + } + connectServer() { const { info, apps } = this.scope.metadata; diff --git a/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts b/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts index 54cd19167..ebdbb852d 100644 --- a/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts +++ b/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts @@ -275,6 +275,22 @@ export class TransportStreamableHttpAdapter extends LocalTransportAdapter { }).toThrow('Session ID is required'); }); }); + +/** + * Tests for the re-initialization guard in onInitialize. + * + * When a client retries initialize with a session whose transport is already + * initialized (e.g., after notifications/initialized with old session got 404), + * the guard must call resetForReinitialization() before transport.initialize() + * to prevent the MCP SDK from rejecting with 400 "server already initialized". + * + * This is the exact production bug scenario: + * 1. Session A created + initialized + * 2. DELETE terminates A + * 3. Initialize with stale A → creates session B, transport B initialized + * 4. notifications/initialized with old A → 404 + * 5. Client retries initialize with B → transport B already initialized → 400 (BUG) + * 6. Fix: guard detects isInitialized, calls resetForReinitialization → 200 + */ +describe('onInitialize re-initialization guard', () => { + /** + * Simulates the re-initialization guard logic added to onInitialize + * in handle.streamable-http.flow.ts. + */ + function simulateOnInitializeGuard(params: { + transport: { isInitialized: boolean; resetForReinitialization: () => void; initialize: () => void }; + }): { resetCalled: boolean } { + const { transport } = params; + let resetCalled = false; + + // This is the exact guard logic from onInitialize: + if (transport.isInitialized) { + transport.resetForReinitialization(); + resetCalled = true; + } + + transport.initialize(); + return { resetCalled }; + } + + it('should call resetForReinitialization when transport is already initialized', () => { + const resetFn = jest.fn(); + const initFn = jest.fn(); + + const result = simulateOnInitializeGuard({ + transport: { + isInitialized: true, + resetForReinitialization: resetFn, + initialize: initFn, + }, + }); + + expect(result.resetCalled).toBe(true); + expect(resetFn).toHaveBeenCalledTimes(1); + expect(initFn).toHaveBeenCalledTimes(1); + }); + + it('should NOT call resetForReinitialization when transport is fresh', () => { + const resetFn = jest.fn(); + const initFn = jest.fn(); + + const result = simulateOnInitializeGuard({ + transport: { + isInitialized: false, + resetForReinitialization: resetFn, + initialize: initFn, + }, + }); + + expect(result.resetCalled).toBe(false); + expect(resetFn).not.toHaveBeenCalled(); + expect(initFn).toHaveBeenCalledTimes(1); + }); + + it('should handle full reconnect chain: session cleared → new session → cached transport → guard resets', () => { + // Step 1: Simulate session clearing (http.request.flow reconnect logic) + const authorization: { session?: { id: string } } = { + session: { id: 'old-terminated-session' }, + }; + authorization.session = undefined; // Cleared by reconnect logic + + // Step 2: Simulate parseInput creating new session + const newSession = { id: 'new-session-B' }; + if (!authorization.session) { + authorization.session = newSession; + } + + // Step 3: Simulate createTransporter returning cached transport (already initialized) + let transportInitialized = true; // Cached from first successful init + let transportSessionId: string | undefined = 'new-session-B'; + const resetFn = jest.fn(() => { + transportInitialized = false; + transportSessionId = undefined; + }); + const initFn = jest.fn(() => { + transportInitialized = true; + transportSessionId = newSession.id; + }); + + // Step 4: Apply the guard + const result = simulateOnInitializeGuard({ + transport: { + get isInitialized() { + return transportInitialized; + }, + resetForReinitialization: resetFn, + initialize: initFn, + }, + }); + + // Guard should have detected the initialized state and reset + expect(result.resetCalled).toBe(true); + expect(resetFn).toHaveBeenCalledTimes(1); + expect(initFn).toHaveBeenCalledTimes(1); + // After initialize, transport should be initialized again with new session + expect(transportInitialized).toBe(true); + expect(transportSessionId).toBe('new-session-B'); + }); + + it('should handle multiple retries gracefully', () => { + let initCount = 0; + // Each retry: transport starts initialized (from previous success), gets reset, re-initialized + for (let retry = 0; retry < 3; retry++) { + const resetFn = jest.fn(); + const initFn = jest.fn(() => { + initCount++; + }); + + const result = simulateOnInitializeGuard({ + transport: { + isInitialized: retry > 0, // First call is fresh, subsequent are retries + resetForReinitialization: resetFn, + initialize: initFn, + }, + }); + + if (retry > 0) { + expect(result.resetCalled).toBe(true); + } else { + expect(result.resetCalled).toBe(false); + } + expect(initFn).toHaveBeenCalledTimes(1); + } + + expect(initCount).toBe(3); + }); +}); diff --git a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts index 681a92c42..01dec2564 100644 --- a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts @@ -324,6 +324,18 @@ export default class HandleStreamableHttpFlow extends FlowBase { syncStreamableHttpAuthorizationSession(authorization, session); const transport = await transportService.createTransporter('streamable-http', token, session.id, response); + + // If the transport is already initialized, this is a retry of a successful + // initialize (e.g., client retried after its notifications/initialized with + // the old session ID was 404'd). Reset initialization state so the MCP SDK + // accepts the new initialize request instead of rejecting with 400. + if (transport.isInitialized) { + logger.info('onInitialize: transport already initialized, resetting for re-initialization', { + sessionId: session.id?.slice(0, 20), + }); + transport.resetForReinitialization(); + } + logger.info('onInitialize: transport created, calling initialize'); await transport.initialize(request, response); logger.info('onInitialize: completed successfully'); diff --git a/libs/sdk/src/transport/transport.local.ts b/libs/sdk/src/transport/transport.local.ts index 9ee9bf821..9d5c8bc35 100644 --- a/libs/sdk/src/transport/transport.local.ts +++ b/libs/sdk/src/transport/transport.local.ts @@ -72,6 +72,10 @@ export class LocalTransporter implements Transporter { } } + get isInitialized(): boolean { + return this.adapter.isInitialized; + } + /** * Marks this transport as pre-initialized for session recreation. * This is needed when recreating a transport from Redis because the @@ -81,6 +85,10 @@ export class LocalTransporter implements Transporter { this.adapter.markAsInitialized(); } + resetForReinitialization(): void { + this.adapter.resetForReinitialization(); + } + async destroy(reason: string): Promise { try { await this.adapter.destroy(reason); diff --git a/libs/sdk/src/transport/transport.remote.ts b/libs/sdk/src/transport/transport.remote.ts index a4f8b88ee..da635ae89 100644 --- a/libs/sdk/src/transport/transport.remote.ts +++ b/libs/sdk/src/transport/transport.remote.ts @@ -32,7 +32,15 @@ export class RemoteTransporter implements Transporter { throw new MethodNotImplementedError('RemoteTransporter', 'destroy'); } + get isInitialized(): boolean { + return false; + } + markAsInitialized(): void { // No-op for remote transporters - initialization state is managed on the remote node } + + resetForReinitialization(): void { + // No-op for remote transporters - initialization state is managed on the remote node + } } diff --git a/libs/sdk/src/transport/transport.types.ts b/libs/sdk/src/transport/transport.types.ts index 43c384aac..608a71aab 100644 --- a/libs/sdk/src/transport/transport.types.ts +++ b/libs/sdk/src/transport/transport.types.ts @@ -58,12 +58,24 @@ export interface Transporter { ping(timeoutMs?: number): Promise; + /** + * Whether this transport has already been initialized via the MCP initialize handshake. + */ + readonly isInitialized: boolean; + /** * Marks this transport as pre-initialized for session recreation. * This is needed when recreating a transport from Redis because the * original initialize request was processed by a different transport instance. */ markAsInitialized(): void; + + /** + * Resets initialization state to allow re-initialization. + * Used when a client retries initialize on an already-initialized transport + * (e.g., after reconnect following session termination). + */ + resetForReinitialization(): void; } export interface TransportRegistryOptions { From 53defbe335e24693f3433b8abbb9f9005010125c Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 25 Mar 2026 04:36:07 +0200 Subject: [PATCH 02/12] feat: improve file existence checks and enhance transport session handling --- .../e2e/cli-daemon-sea.e2e.spec.ts | 52 +++++++++---------- .../demo-e2e-cli-exec/e2e/helpers/exec-cli.ts | 4 +- .../e2e/esm-hot-reload.e2e.spec.ts | 2 +- .../e2e/session-reconnect.e2e.spec.ts | 10 ++-- .../adapters/streamable-http-transport.ts | 11 ++-- .../transport.streamable-http.adapter.ts | 2 +- 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts b/apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts index f67496530..b51cb0410 100644 --- a/apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts +++ b/apps/e2e/demo-e2e-cli-exec/e2e/cli-daemon-sea.e2e.spec.ts @@ -13,11 +13,11 @@ * all tests pass trivially with a warning. */ -import * as fs from 'fs'; -import * as os from 'os'; +import { realpathSync } from 'fs'; import * as path from 'path'; import { execFileSync } from 'child_process'; -import { ensureSeaBuild, isSeaBuildAvailable, runSeaCli } from './helpers/exec-cli'; +import { fileExists, mkdtemp, readFile, stat, rm } from '@frontmcp/utils'; +import { ensureSeaBuild, runSeaCli } from './helpers/exec-cli'; import { McpJsonRpcClient, httpOverSocket, waitForSocket } from './helpers/mcp-client'; const APP_NAME = 'cli-exec-demo'; @@ -65,7 +65,7 @@ function killByPid(pid: number): void { async function waitForFile(filePath: string, timeoutMs = 10000): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { - if (fs.existsSync(filePath)) return true; + if (await fileExists(filePath)) return true; await new Promise((r) => setTimeout(r, 100)); } return false; @@ -80,9 +80,9 @@ describe('SEA CLI Daemon E2E', () => { } // Set up isolated directories - homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontmcp-sea-daemon-')); - prefixDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontmcp-sea-install-')); - binDir = fs.mkdtempSync(path.join(os.tmpdir(), 'frontmcp-sea-bin-')); + homeDir = await mkdtemp(path.join(require('os').tmpdir(), 'frontmcp-sea-daemon-')); + prefixDir = await mkdtemp(path.join(require('os').tmpdir(), 'frontmcp-sea-install-')); + binDir = await mkdtemp(path.join(require('os').tmpdir(), 'frontmcp-sea-bin-')); // Install the SEA binary to a temp prefix const { stdout, exitCode } = runSeaCli(['install', '--prefix', prefixDir, '--bin-dir', binDir]); @@ -97,9 +97,9 @@ describe('SEA CLI Daemon E2E', () => { const seaBinaryInInstall = path.join(appDir, `${APP_NAME}-cli-bin`); const symlinkPath = path.join(binDir, APP_NAME); - if (fs.existsSync(symlinkPath)) { - installedBinaryPath = fs.realpathSync(symlinkPath); - } else if (fs.existsSync(seaBinaryInInstall)) { + if (await fileExists(symlinkPath)) { + installedBinaryPath = realpathSync(symlinkPath); + } else if (await fileExists(seaBinaryInInstall)) { installedBinaryPath = seaBinaryInInstall; } else { console.warn('[e2e:sea] Could not find installed SEA binary'); @@ -131,7 +131,7 @@ describe('SEA CLI Daemon E2E', () => { // Clean up temp dirs for (const dir of [homeDir, prefixDir, binDir]) { try { - fs.rmSync(dir, { recursive: true, force: true }); + await rm(dir, { recursive: true, force: true }); } catch { /* ok */ } @@ -140,10 +140,10 @@ describe('SEA CLI Daemon E2E', () => { // ─── Build & Install Verification ────────────────────────────────────── - it('should have SEA CLI binary available', () => { + it('should have SEA CLI binary available', async () => { if (!seaAvailable) return; - expect(fs.existsSync(installedBinaryPath)).toBe(true); - const stats = fs.statSync(installedBinaryPath); + expect(await fileExists(installedBinaryPath)).toBe(true); + const stats = await stat(installedBinaryPath); expect(stats.mode & 0o111).not.toBe(0); // executable }); @@ -169,7 +169,7 @@ describe('SEA CLI Daemon E2E', () => { } // Wait for socket file to appear - if (!fs.existsSync(socketPath)) { + if (!(await fileExists(socketPath))) { const appeared = await waitForFile(socketPath, 15000); expect(appeared).toBe(true); } @@ -197,21 +197,21 @@ describe('SEA CLI Daemon E2E', () => { expect(stdout).toContain('already running'); }); - it('PID file should have correct structure', () => { + it('PID file should have correct structure', async () => { if (!seaAvailable) return; - expect(fs.existsSync(pidPath)).toBe(true); - const pidData = JSON.parse(fs.readFileSync(pidPath, 'utf8')); + expect(await fileExists(pidPath)).toBe(true); + const pidData = JSON.parse(await readFile(pidPath)); expect(pidData.pid).toEqual(expect.any(Number)); expect(pidData.socketPath).toContain('.sock'); expect(pidData.startedAt).toEqual(expect.any(String)); expect(() => process.kill(pidData.pid, 0)).not.toThrow(); }); - it('daemon logs should not contain ZodError or MODULE_NOT_FOUND', () => { + it('daemon logs should not contain ZodError or MODULE_NOT_FOUND', async () => { if (!seaAvailable) return; const logPath = path.join(homeDir, 'logs', `${APP_NAME}.log`); - if (fs.existsSync(logPath)) { - const content = fs.readFileSync(logPath, 'utf8'); + if (await fileExists(logPath)) { + const content = await readFile(logPath); expect(content).not.toContain('ZodError'); expect(content).not.toContain('MODULE_NOT_FOUND'); expect(content).not.toContain('Cannot find module'); @@ -310,14 +310,14 @@ describe('SEA CLI Daemon E2E', () => { expect(stdout).toMatch(/Not running/); }); - it('socket file should be removed after stop', () => { + it('socket file should be removed after stop', async () => { if (!seaAvailable) return; - expect(fs.existsSync(socketPath)).toBe(false); + expect(await fileExists(socketPath)).toBe(false); }); - it('PID file should be removed after stop', () => { + it('PID file should be removed after stop', async () => { if (!seaAvailable) return; - expect(fs.existsSync(pidPath)).toBe(false); + expect(await fileExists(pidPath)).toBe(false); }); // ─── Daemon Restart ──────────────────────────────────────────────────── @@ -333,7 +333,7 @@ describe('SEA CLI Daemon E2E', () => { daemonPid = parseInt(pidMatch[1], 10); } - if (!fs.existsSync(socketPath)) { + if (!(await fileExists(socketPath))) { await waitForFile(socketPath, 15000); } await waitForSocket(socketPath, 20000); diff --git a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts index 09c56fc3c..5fc0eb7a2 100644 --- a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts +++ b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts @@ -1,6 +1,6 @@ import { execFileSync, spawn, ChildProcess } from 'child_process'; -import * as fs from 'fs'; import * as path from 'path'; +import { fileExists } from '@frontmcp/utils'; import type { StdioOptions } from 'node:child_process'; const APP_NAME = 'cli-exec-demo'; @@ -170,7 +170,7 @@ export async function ensureSeaBuild(): Promise { ); } - seaBuildAvailable = fs.existsSync(SEA_CLI_BINARY); + seaBuildAvailable = await fileExists(SEA_CLI_BINARY); seaBuildDone = true; if (seaBuildAvailable) { diff --git a/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts b/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts index cfe453205..59eae79e9 100644 --- a/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts +++ b/apps/e2e/demo-e2e-esm/e2e/esm-hot-reload.e2e.spec.ts @@ -123,7 +123,7 @@ test.describe('ESM Hot-Reload E2E', () => { initialTools = await mcp.tools.list(); initialNames = initialTools.map((t: { name: string }) => t.name); log('[TEST] Poll initial tools:', initialNames); - if (initialNames.includes('esm:echo')) break; + if (initialNames.includes('esm:echo') && initialNames.includes('esm:add')) break; await new Promise((resolve) => setTimeout(resolve, 2000)); } diff --git a/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts b/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts index 1bd46b30e..44987309b 100644 --- a/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts +++ b/apps/e2e/demo-e2e-transport-recreation/e2e/session-reconnect.e2e.spec.ts @@ -841,10 +841,12 @@ test.describe('Session Reconnect E2E', () => { // Session should still work after retries const retryResult = await sendInitialize(server.info.baseUrl, sessionId); + expect(retryResult.status).toBe(200); const finalSession = retryResult.sessionId; if (!finalSession) throw new Error('Expected final session'); - await sendNotificationInitialized(server.info.baseUrl, finalSession); + const notif = await sendNotificationInitialized(server.info.baseUrl, finalSession); + expect(notif.status).toBe(202); const tool = await sendToolCall(server.info.baseUrl, finalSession, 'get-session-info'); expect(tool.status).toBe(200); }); @@ -894,12 +896,14 @@ test.describe('Session Reconnect E2E', () => { if (!retrySession) throw new Error(`Expected retry session in cycle ${cycle}`); // Verify it works - await sendNotificationInitialized(server.info.baseUrl, retrySession); + const notif = await sendNotificationInitialized(server.info.baseUrl, retrySession); + expect(notif.status).toBe(202); const tool = await sendToolCall(server.info.baseUrl, retrySession, 'get-session-info'); expect(tool.status).toBe(200); // DELETE before next cycle - await sendDelete(server.info.baseUrl, retrySession); + const del = await sendDelete(server.info.baseUrl, retrySession); + expect(del.status).toBe(204); } }); diff --git a/libs/sdk/src/transport/adapters/streamable-http-transport.ts b/libs/sdk/src/transport/adapters/streamable-http-transport.ts index 9b16255f6..f2c61d4df 100644 --- a/libs/sdk/src/transport/adapters/streamable-http-transport.ts +++ b/libs/sdk/src/transport/adapters/streamable-http-transport.ts @@ -161,10 +161,15 @@ export class RecreateableStreamableHTTPServerTransport extends StreamableHTTPSer return; } - if ('_initialized' in webTransport) { - webTransport._initialized = false; - webTransport.sessionId = undefined; + if (!('_initialized' in webTransport)) { + throw new InvalidTransportSessionError( + '[RecreateableStreamableHTTPServerTransport] Expected _initialized field not found on internal transport. ' + + 'This may indicate an incompatible MCP SDK version.', + ); } + + webTransport._initialized = false; + webTransport.sessionId = undefined; } /** diff --git a/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts b/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts index ebdbb852d..2ee49be52 100644 --- a/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts +++ b/libs/sdk/src/transport/adapters/transport.streamable-http.adapter.ts @@ -276,7 +276,7 @@ export class TransportStreamableHttpAdapter extends LocalTransportAdapter Date: Wed, 25 Mar 2026 13:56:24 +0200 Subject: [PATCH 03/12] feat: unify build command by replacing --exec and --adapter flags with --target option --- docs/frontmcp/deployment/runtime-modes.mdx | 6 +- docs/frontmcp/deployment/serverless.mdx | 22 +- .../getting-started/cli-reference.mdx | 23 +- .../nx-plugin/executors/build-exec.mdx | 4 +- .../frontmcp/nx-plugin/executors/overview.mdx | 2 +- docs/frontmcp/nx-plugin/overview.mdx | 2 +- libs/cli/package.json | 3 +- .../build/__tests__/target-resolution.spec.ts | 54 + .../src/commands/build/adapters/cloudflare.ts | 2 +- .../cli/src/commands/build/adapters/lambda.ts | 2 +- .../cli/src/commands/build/adapters/vercel.ts | 2 +- libs/cli/src/commands/build/browser/index.ts | 70 ++ .../exec/__tests__/runner-script.spec.ts | 8 +- libs/cli/src/commands/build/exec/config.ts | 6 +- .../commands/build/exec/esbuild-bundler.ts | 2 +- libs/cli/src/commands/build/exec/index.ts | 2 +- .../src/commands/build/exec/runner-script.ts | 6 +- libs/cli/src/commands/build/index.ts | 77 +- libs/cli/src/commands/build/register.ts | 10 +- libs/cli/src/commands/build/sdk/index.ts | 80 ++ libs/cli/src/commands/package/install.ts | 4 +- .../src/core/__tests__/args-new-flags.spec.ts | 25 - libs/cli/src/core/__tests__/args.spec.ts | 12 - libs/cli/src/core/__tests__/bridge.spec.ts | 18 +- libs/cli/src/core/__tests__/help.spec.ts | 6 +- libs/cli/src/core/__tests__/program.spec.ts | 7 +- libs/cli/src/core/args.ts | 25 +- libs/cli/src/core/bridge.ts | 19 +- libs/cli/src/core/help.ts | 6 +- yarn.lock | 1031 +---------------- 30 files changed, 390 insertions(+), 1146 deletions(-) create mode 100644 libs/cli/src/commands/build/__tests__/target-resolution.spec.ts create mode 100644 libs/cli/src/commands/build/browser/index.ts create mode 100644 libs/cli/src/commands/build/sdk/index.ts diff --git a/docs/frontmcp/deployment/runtime-modes.mdx b/docs/frontmcp/deployment/runtime-modes.mdx index d23b9d8f3..a2f52cc59 100644 --- a/docs/frontmcp/deployment/runtime-modes.mdx +++ b/docs/frontmcp/deployment/runtime-modes.mdx @@ -271,7 +271,7 @@ export default async function handler(req, res) { npx frontmcp create my-app --target vercel # Or build existing project - frontmcp build --adapter vercel + frontmcp build --target vercel-edge # Deploy vercel deploy @@ -287,7 +287,7 @@ export default async function handler(req, res) { npm install @codegenie/serverless-express # Build and deploy - frontmcp build --adapter lambda + frontmcp build --target lambda cd ci && sam build && sam deploy ``` @@ -298,7 +298,7 @@ export default async function handler(req, res) { npx frontmcp create my-app --target cloudflare # Build and deploy - frontmcp build --adapter cloudflare + frontmcp build --target cloudflare-worker wrangler deploy ``` diff --git a/docs/frontmcp/deployment/serverless.mdx b/docs/frontmcp/deployment/serverless.mdx index a261c71e4..14b14bc0a 100644 --- a/docs/frontmcp/deployment/serverless.mdx +++ b/docs/frontmcp/deployment/serverless.mdx @@ -38,7 +38,7 @@ This generates the platform config files (`vercel.json`, `ci/template.yaml`, or ```bash # Build for Vercel - frontmcp build --adapter vercel + frontmcp build --target vercel-edge # Deploy vercel deploy @@ -51,7 +51,7 @@ This generates the platform config files (`vercel.json`, `ci/template.yaml`, or npm install @codegenie/serverless-express # Build for Lambda - frontmcp build --adapter lambda + frontmcp build --target lambda # Deploy with your preferred tool (SAM, CDK, Serverless Framework) ``` @@ -60,7 +60,7 @@ This generates the platform config files (`vercel.json`, `ci/template.yaml`, or ```bash # Build for Cloudflare Workers - frontmcp build --adapter cloudflare + frontmcp build --target cloudflare-worker # Deploy wrangler deploy @@ -82,7 +82,7 @@ For persistent session storage on Vercel, see [Vercel KV Setup](/frontmcp/deploy 1. Build your project: ```bash - frontmcp build --adapter vercel + frontmcp build --target vercel-edge ``` 2. This generates: @@ -157,7 +157,7 @@ npm run deploy # Runs: cd ci && sam build && sam deploy 1. Build your project: ```bash - frontmcp build --adapter lambda + frontmcp build --target lambda ``` 2. This generates: @@ -260,7 +260,7 @@ npm run deploy # Runs: wrangler deploy 1. Build your project: ```bash - frontmcp build --adapter cloudflare + frontmcp build --target cloudflare-worker ``` 2. This generates: @@ -361,7 +361,7 @@ RememberPlugin.init({ ┌─────────────────────────────────────────────────────────────┐ │ Build Time │ ├─────────────────────────────────────────────────────────────┤ -│ frontmcp build --adapter vercel │ +│ frontmcp build --target vercel-edge │ │ │ │ │ ├── Compiles TypeScript with --module esnext │ │ ├── Generates platform-specific index.js wrapper │ @@ -456,16 +456,16 @@ Ensure your `tsconfig.json` doesn't conflict. The CLI arguments override tsconfi frontmcp build # Build for Vercel (ESM) -frontmcp build --adapter vercel +frontmcp build --target vercel-edge # Build for AWS Lambda (ESM) -frontmcp build --adapter lambda +frontmcp build --target lambda # Build for Cloudflare Workers (CommonJS) -frontmcp build --adapter cloudflare +frontmcp build --target cloudflare-worker # Specify output directory -frontmcp build --adapter vercel --outDir build +frontmcp build --target vercel-edge --outDir build ``` ### SDK Exports diff --git a/docs/frontmcp/getting-started/cli-reference.mdx b/docs/frontmcp/getting-started/cli-reference.mdx index 9f57135ba..d0b18aae7 100644 --- a/docs/frontmcp/getting-started/cli-reference.mdx +++ b/docs/frontmcp/getting-started/cli-reference.mdx @@ -19,8 +19,8 @@ Commands for building, testing, and debugging your FrontMCP server. | -------------------- | ------------------------------------------------------------- | | `dev` | Start in development mode (tsx --watch + async type-check) | | `build` | Compile entry with TypeScript (tsc) | -| `build --exec` | Build distributable executable bundle (esbuild) | -| `build --exec --cli` | Build CLI executable with subcommands per tool | +| `build --target node` | Build distributable executable bundle (esbuild) | +| `build --target cli` | Build CLI executable with subcommands per tool | | `test` | Run E2E tests with auto-injected Jest configuration | | `init` | Create or fix a tsconfig.json suitable for FrontMCP | | `doctor` | Check Node/npm versions and tsconfig requirements. Use `--fix` to auto-repair (installs missing deps, creates app directories) | @@ -93,11 +93,10 @@ See [ESM Packages](/frontmcp/servers/esm-packages) for full documentation on ESM ### Build Options -| Option | Description | -| ---------------------- | ------------------------------------------------------------ | -| `--exec` | Build distributable executable bundle | -| `--cli` | Generate CLI with subcommands per tool (use with `--exec`) | -| `-a, --adapter ` | Deployment adapter: `node`, `vercel`, `lambda`, `cloudflare` | +| Option | Description | +| ---------------------- | -------------------------------------------------------------------------------- | +| `--target ` | Build target: `node`, `cli`, `vercel-edge`, `lambda`, `cloudflare-worker` | +| `--js` | Emit plain JavaScript bundle instead of SEA (use with `--target cli`) | ### Start Options @@ -169,15 +168,15 @@ If `git` is not installed, this step is silently skipped. ## Generated Executable CLI -When you build with `--exec --cli`, the output is a self-contained executable whose commands are auto-generated from your MCP server's tools, resources, prompts, and templates. +When you build with `--target cli`, the output is a self-contained executable whose commands are auto-generated from your MCP server's tools, resources, prompts, and templates. ### Building ```bash -frontmcp build --exec --cli +frontmcp build --target cli ``` -This produces a standalone JavaScript bundle in `dist/` that can be distributed and run with Node.js. +This produces a self-contained executable in `dist/` that can be distributed and run directly. Use `--js` to emit a plain JavaScript bundle instead. ### Global Options @@ -276,10 +275,10 @@ frontmcp dev frontmcp build --out-dir build # Build distributable executable -frontmcp build --exec +frontmcp build --target node # Build CLI executable with subcommands per tool -frontmcp build --exec --cli +frontmcp build --target cli # Run E2E tests sequentially frontmcp test --runInBand diff --git a/docs/frontmcp/nx-plugin/executors/build-exec.mdx b/docs/frontmcp/nx-plugin/executors/build-exec.mdx index 27f5fcc16..6e0ab9300 100644 --- a/docs/frontmcp/nx-plugin/executors/build-exec.mdx +++ b/docs/frontmcp/nx-plugin/executors/build-exec.mdx @@ -2,7 +2,7 @@ title: Build Exec Executor slug: nx-plugin/executors/build-exec icon: box -description: Build a distributable bundle using frontmcp build --exec +description: Build a distributable bundle using frontmcp build --target node --- Builds a distributable executable bundle using esbuild. Produces a single-file output ideal for containerized deployments. @@ -40,7 +40,7 @@ nx build-exec my-app ## CLI Mode -Pass `cli: true` (or use `frontmcp build --exec --cli`) to generate a standalone CLI executable with auto-generated subcommands for every tool, resource, prompt, and template in your MCP server. +Pass `cli: true` (or use `frontmcp build --target cli`) to generate a standalone CLI executable with auto-generated subcommands for every tool, resource, prompt, and template in your MCP server. ```json project.json { diff --git a/docs/frontmcp/nx-plugin/executors/overview.mdx b/docs/frontmcp/nx-plugin/executors/overview.mdx index e920e747a..772cde81c 100644 --- a/docs/frontmcp/nx-plugin/executors/overview.mdx +++ b/docs/frontmcp/nx-plugin/executors/overview.mdx @@ -12,7 +12,7 @@ Executors wrap FrontMCP CLI commands as Nx targets, enabling caching, dependency | Executor | CLI Command | Cacheable | Long-Running | | -------------------------------------------------------- | ----------------------- | --------- | ------------ | | [`build`](/frontmcp/nx-plugin/executors/build) | `frontmcp build` | Yes | No | -| [`build-exec`](/frontmcp/nx-plugin/executors/build-exec) | `frontmcp build --exec` | Yes | No | +| [`build-exec`](/frontmcp/nx-plugin/executors/build-exec) | `frontmcp build --target node` | Yes | No | | [`dev`](/frontmcp/nx-plugin/executors/dev) | `frontmcp dev` | No | Yes | | [`serve`](/frontmcp/nx-plugin/executors/serve) | `frontmcp start` | No | Yes | | [`test`](/frontmcp/nx-plugin/executors/test) | `frontmcp test` | Yes | No | diff --git a/docs/frontmcp/nx-plugin/overview.mdx b/docs/frontmcp/nx-plugin/overview.mdx index 679e5785b..00d83b22c 100644 --- a/docs/frontmcp/nx-plugin/overview.mdx +++ b/docs/frontmcp/nx-plugin/overview.mdx @@ -67,7 +67,7 @@ graph TD | Executor | Wraps | Cacheable | | ------------ | ----------------------- | ----------------- | | `build` | `frontmcp build` | Yes | -| `build-exec` | `frontmcp build --exec` | Yes | +| `build-exec` | `frontmcp build --target node` | Yes | | `dev` | `frontmcp dev` | No (long-running) | | `serve` | `frontmcp start` | No (long-running) | | `test` | `frontmcp test` | Yes | diff --git a/libs/cli/package.json b/libs/cli/package.json index ec7182010..ea0c377d6 100644 --- a/libs/cli/package.json +++ b/libs/cli/package.json @@ -45,7 +45,6 @@ "devDependencies": { "typescript": "^5.5.3", "tsx": "^4.20.6", - "@types/node": "^24.0.0", - "@modelcontextprotocol/inspector": "^0.21.1" + "@types/node": "^24.0.0" } } diff --git a/libs/cli/src/commands/build/__tests__/target-resolution.spec.ts b/libs/cli/src/commands/build/__tests__/target-resolution.spec.ts new file mode 100644 index 000000000..6cbaca551 --- /dev/null +++ b/libs/cli/src/commands/build/__tests__/target-resolution.spec.ts @@ -0,0 +1,54 @@ +import { toParsedArgs } from '../../../core/bridge'; + +describe('Build target resolution', () => { + describe('--target flag', () => { + it('--target cli should set buildTarget to cli', () => { + const args = toParsedArgs('build', [], { target: 'cli' }); + expect(args.buildTarget).toBe('cli'); + }); + + it('--target cli --js should set buildTarget and js', () => { + const args = toParsedArgs('build', [], { target: 'cli', js: true }); + expect(args.buildTarget).toBe('cli'); + expect(args.js).toBe(true); + }); + + it('--target node should set buildTarget to node', () => { + const args = toParsedArgs('build', [], { target: 'node' }); + expect(args.buildTarget).toBe('node'); + }); + + it('--target sdk should set buildTarget to sdk', () => { + const args = toParsedArgs('build', [], { target: 'sdk' }); + expect(args.buildTarget).toBe('sdk'); + }); + + it('--target browser should set buildTarget to browser', () => { + const args = toParsedArgs('build', [], { target: 'browser' }); + expect(args.buildTarget).toBe('browser'); + }); + + it('--target vercel-edge should set buildTarget to vercel-edge', () => { + const args = toParsedArgs('build', [], { target: 'vercel-edge' }); + expect(args.buildTarget).toBe('vercel-edge'); + }); + + it('--target lambda should set buildTarget to lambda', () => { + const args = toParsedArgs('build', [], { target: 'lambda' }); + expect(args.buildTarget).toBe('lambda'); + }); + + it('--target cloudflare-worker should set buildTarget to cloudflare-worker', () => { + const args = toParsedArgs('build', [], { target: 'cloudflare-worker' }); + expect(args.buildTarget).toBe('cloudflare-worker'); + }); + }); + + describe('non-build commands should not resolve target', () => { + it('create --target should not set buildTarget', () => { + const args = toParsedArgs('create', [], { target: 'vercel' }); + expect(args.buildTarget).toBeUndefined(); + expect(args.target).toBe('vercel'); + }); + }); +}); diff --git a/libs/cli/src/commands/build/adapters/cloudflare.ts b/libs/cli/src/commands/build/adapters/cloudflare.ts index dfa72412d..e6f5a4598 100644 --- a/libs/cli/src/commands/build/adapters/cloudflare.ts +++ b/libs/cli/src/commands/build/adapters/cloudflare.ts @@ -10,7 +10,7 @@ export const cloudflareAdapter: AdapterTemplate = { moduleFormat: 'commonjs', getEntryTemplate: (mainModulePath: string) => `// Auto-generated Cloudflare Workers entry point -// Generated by: frontmcp build --adapter cloudflare +// Generated by: frontmcp build --target cloudflare-worker process.env.FRONTMCP_SERVERLESS = '1'; require('${mainModulePath}'); diff --git a/libs/cli/src/commands/build/adapters/lambda.ts b/libs/cli/src/commands/build/adapters/lambda.ts index 8abe22791..4c931c0bb 100644 --- a/libs/cli/src/commands/build/adapters/lambda.ts +++ b/libs/cli/src/commands/build/adapters/lambda.ts @@ -27,7 +27,7 @@ process.env.FRONTMCP_SERVERLESS = '1'; `, getEntryTemplate: (mainModulePath: string) => `// Auto-generated AWS Lambda entry point -// Generated by: frontmcp build --adapter lambda +// Generated by: frontmcp build --target lambda // // IMPORTANT: This adapter requires @codegenie/serverless-express // Install it with: npm install @codegenie/serverless-express diff --git a/libs/cli/src/commands/build/adapters/vercel.ts b/libs/cli/src/commands/build/adapters/vercel.ts index beacf4b2a..14ace4f15 100644 --- a/libs/cli/src/commands/build/adapters/vercel.ts +++ b/libs/cli/src/commands/build/adapters/vercel.ts @@ -67,7 +67,7 @@ process.env.FRONTMCP_SERVERLESS = '1'; `, getEntryTemplate: (mainModulePath: string) => `// Auto-generated Vercel entry point -// Generated by: frontmcp build --adapter vercel +// Generated by: frontmcp build --target vercel-edge import './serverless-setup.js'; import '${mainModulePath}'; import { getServerlessHandlerAsync } from '@frontmcp/sdk'; diff --git a/libs/cli/src/commands/build/browser/index.ts b/libs/cli/src/commands/build/browser/index.ts new file mode 100644 index 000000000..8ca393642 --- /dev/null +++ b/libs/cli/src/commands/build/browser/index.ts @@ -0,0 +1,70 @@ +import * as path from 'path'; +import { ParsedArgs } from '../../../core/args'; +import { c } from '../../../core/colors'; +import { ensureDir } from '@frontmcp/utils'; +import { resolveEntry } from '../../../shared/fs'; + +/** + * Build a browser-compatible ESM bundle. + * + * Uses esbuild with `platform: 'browser'` which resolves conditional imports + * (`#imports` in package.json) to browser implementations automatically: + * - Crypto → @noble/hashes + @noble/ciphers (no node:crypto) + * - AsyncLocalStorage → stack-based polyfill + * - EventEmitter → Map-based polyfill + * - SSE/Express/Stdio → stubs that throw on instantiation + * + * @example + * ```bash + * frontmcp build --target browser + * ``` + */ +export async function buildBrowser(opts: ParsedArgs): Promise { + const cwd = process.cwd(); + const entry = await resolveEntry(cwd, opts.entry); + const outDir = path.resolve(cwd, opts.outDir || 'dist'); + await ensureDir(outDir); + + const pkg = require(path.join(cwd, 'package.json')); + const appName = pkg.name || path.basename(cwd); + + console.log(`${c('cyan', '[build:browser]')} entry: ${path.relative(cwd, entry)}`); + console.log(`${c('cyan', '[build:browser]')} outDir: ${path.relative(cwd, outDir)}`); + + const esbuild = await import('esbuild'); + + // Build ESM bundle for browsers + console.log(c('cyan', '[build:browser] Bundling ESM for browser...')); + await esbuild.build({ + entryPoints: [entry], + bundle: true, + platform: 'browser', + format: 'esm', + target: 'es2022', + outfile: path.join(outDir, `${appName}.browser.mjs`), + keepNames: true, + treeShaking: true, + sourcemap: true, + // Browser-safe: keep React and peer deps external (user's bundler handles them) + external: [ + 'react', 'react-dom', 'react-router', 'react-router-dom', + // Keep @frontmcp/* external — user installs them + '@frontmcp/sdk', '@frontmcp/di', '@frontmcp/utils', + '@frontmcp/auth', '@frontmcp/react', + 'reflect-metadata', + // Node.js-only — these should be tree-shaken by platform: 'browser' + // but list explicitly to avoid accidental bundling + 'better-sqlite3', 'fsevents', 'ioredis', + ...Object.keys(pkg.peerDependencies || {}), + ], + // Resolve conditional imports to browser variants + conditions: ['browser', 'import', 'default'], + define: { + 'process.env.NODE_ENV': '"production"', + }, + }); + + console.log(c('green', `\n Browser build complete:`)); + console.log(c('gray', ` ESM: ${appName}.browser.mjs`)); + console.log(c('gray', ` Source map: ${appName}.browser.mjs.map`)); +} diff --git a/libs/cli/src/commands/build/exec/__tests__/runner-script.spec.ts b/libs/cli/src/commands/build/exec/__tests__/runner-script.spec.ts index b33cf68dc..a24b69579 100644 --- a/libs/cli/src/commands/build/exec/__tests__/runner-script.spec.ts +++ b/libs/cli/src/commands/build/exec/__tests__/runner-script.spec.ts @@ -52,12 +52,12 @@ describe('runner-script', () => { expect(script).not.toContain('BUNDLE="${SCRIPT_DIR}/my-app.bundle.js"'); }); - it('should include --cli in comment when cliMode is true', () => { + it('should include --target cli in comment when cliMode is true', () => { const config: FrontmcpExecConfig = { name: 'my-app' }; const script = generateRunnerScript(config, true); expect(script).toContain('CLI Executable'); - expect(script).toContain('--cli'); + expect(script).toContain('--target cli'); }); it('should use server bundle when cliMode is false', () => { @@ -85,11 +85,11 @@ describe('runner-script', () => { expect(script).toContain('my-app-cli-bin'); }); - it('should include --sea in comment', () => { + it('should include --target node in comment for SEA mode', () => { const config: FrontmcpExecConfig = { name: 'my-app' }; const script = generateRunnerScript(config, false, true); - expect(script).toContain('--sea'); + expect(script).toContain('--target node'); expect(script).toContain('single executable'); }); diff --git a/libs/cli/src/commands/build/exec/config.ts b/libs/cli/src/commands/build/exec/config.ts index 398c1767e..c04f75390 100644 --- a/libs/cli/src/commands/build/exec/config.ts +++ b/libs/cli/src/commands/build/exec/config.ts @@ -29,10 +29,14 @@ export interface CliConfig { oauth?: OAuthConfig; } +export type ConfigBuildTarget = 'cli' | 'node' | 'sdk' | 'browser' | 'cloudflare-worker' | 'vercel-edge' | 'lambda'; + export interface FrontmcpExecConfig { name: string; version?: string; entry?: string; + /** Build target. When set, takes precedence over cli.enabled / sea.enabled. */ + target?: ConfigBuildTarget; storage?: { type: 'sqlite' | 'redis' | 'none'; required?: boolean; @@ -89,7 +93,7 @@ export async function loadExecConfig(cwd: string): Promise { const pkgPath = path.join(cwd, 'package.json'); if (!fs.existsSync(pkgPath)) { throw new Error( - 'No frontmcp.config.js/json found and no package.json. Create a frontmcp.config.js to use --exec.', + 'No frontmcp.config.js/json found and no package.json. Create a frontmcp.config.js for build targets.', ); } diff --git a/libs/cli/src/commands/build/exec/esbuild-bundler.ts b/libs/cli/src/commands/build/exec/esbuild-bundler.ts index 3a0adcfff..512e9ec75 100644 --- a/libs/cli/src/commands/build/exec/esbuild-bundler.ts +++ b/libs/cli/src/commands/build/exec/esbuild-bundler.ts @@ -46,7 +46,7 @@ export async function bundleWithEsbuild( esbuild = require('esbuild'); } catch { throw new Error( - 'esbuild is required for --exec builds. Install it: npm install -D esbuild', + 'esbuild is required for build targets. Install it: npm install -D esbuild', ); } diff --git a/libs/cli/src/commands/build/exec/index.ts b/libs/cli/src/commands/build/exec/index.ts index c8f57fd70..95ab79159 100644 --- a/libs/cli/src/commands/build/exec/index.ts +++ b/libs/cli/src/commands/build/exec/index.ts @@ -27,7 +27,7 @@ import { validateStepGraph } from './setup'; import { ensureDir, fileExists, runCmd } from '@frontmcp/utils'; import { REQUIRED_DECORATOR_FIELDS } from '../../../core/tsconfig'; -export async function buildExec(opts: ParsedArgs): Promise { +export async function buildExec(opts: ParsedArgs & { cli?: boolean; sea?: boolean }): Promise { const cwd = process.cwd(); const outDir = path.resolve(cwd, opts.outDir || 'dist'); diff --git a/libs/cli/src/commands/build/exec/runner-script.ts b/libs/cli/src/commands/build/exec/runner-script.ts index d8969e5b4..f4a697d83 100644 --- a/libs/cli/src/commands/build/exec/runner-script.ts +++ b/libs/cli/src/commands/build/exec/runner-script.ts @@ -19,7 +19,7 @@ export function generateRunnerScript(config: FrontmcpExecConfig, cliMode?: boole set -euo pipefail ${comment} -# Generated by frontmcp build --exec --sea${cliMode ? ' --cli' : ''} +# Generated by frontmcp build --target ${cliMode ? 'cli' : 'node'} SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" BINARY="\${SCRIPT_DIR}/${binary}" @@ -57,7 +57,7 @@ exec "\${BINARY}" "$@" set -euo pipefail ${comment} -# Generated by frontmcp build --exec${cliMode ? ' --cli' : ''} +# Generated by frontmcp build --target ${cliMode ? 'cli --js' : 'node'} SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" BUNDLE="${bundle}" @@ -78,7 +78,7 @@ fi # Check bundle exists if [ ! -f "\${BUNDLE}" ]; then echo "Error: Bundle not found at \${BUNDLE}" - echo "Run 'frontmcp build --exec${cliMode ? ' --cli' : ''}' to create it." + echo "Run 'frontmcp build --target ${cliMode ? 'cli --js' : 'node'}' to create it." exit 1 fi diff --git a/libs/cli/src/commands/build/index.ts b/libs/cli/src/commands/build/index.ts index 4963199fc..a9a51436a 100644 --- a/libs/cli/src/commands/build/index.ts +++ b/libs/cli/src/commands/build/index.ts @@ -79,50 +79,77 @@ async function generateAdapterFiles( } } +/** Map target names to internal adapter names. */ +const TARGET_TO_ADAPTER: Record = { + 'vercel-edge': 'vercel', + 'lambda': 'lambda', + 'cloudflare-worker': 'cloudflare', +}; + /** * Build the FrontMCP server for a specific deployment target. * - * @param opts - Build options from CLI arguments - * * @example * ```bash - * # Build for Node.js (default) - * frontmcp build - * - * # Build for Vercel - * frontmcp build --adapter vercel - * - * # Build for AWS Lambda - * frontmcp build --adapter lambda - * - * # Build for Cloudflare Workers - * frontmcp build --adapter cloudflare + * frontmcp build --target node # Node.js server bundle + * frontmcp build --target cli # CLI with SEA binary + * frontmcp build --target cli --js # CLI without SEA + * frontmcp build --target sdk # Library (CJS+ESM+types) + * frontmcp build --target browser # Browser ESM bundle + * frontmcp build --target vercel-edge # Vercel serverless + * frontmcp build --target lambda # AWS Lambda + * frontmcp build --target cloudflare-worker # Cloudflare Workers * ``` */ export async function runBuild(opts: ParsedArgs): Promise { - // Executable bundle build (esbuild single-file + scripts) - if (opts.exec) { - const { buildExec } = await import('./exec/index.js'); - return buildExec(opts); + const target = opts.buildTarget ?? 'node'; + + switch (target) { + case 'cli': { + const { buildExec } = await import('./exec/index.js'); + return buildExec({ ...opts, cli: true, sea: !opts.js } as ParsedArgs & { cli: boolean; sea: boolean }); + } + case 'node': { + const { buildExec } = await import('./exec/index.js'); + return buildExec(opts); + } + case 'sdk': { + const { buildSdk } = await import('./sdk/index.js'); + return buildSdk(opts); + } + case 'browser': { + const { buildBrowser } = await import('./browser/index.js'); + return buildBrowser(opts); + } + case 'vercel-edge': + case 'lambda': + case 'cloudflare-worker': { + const adapter = TARGET_TO_ADAPTER[target]; + return runAdapterBuild(opts, adapter); + } + default: + throw new Error(`Unknown build target: ${target}. Available: cli, node, sdk, browser, cloudflare-worker, vercel-edge, lambda`); } +} +/** + * Build using a deployment adapter (serverless platforms). + */ +async function runAdapterBuild(opts: ParsedArgs, adapter: AdapterName): Promise { const cwd = process.cwd(); const entry = await resolveEntry(cwd, opts.entry); const outDir = path.resolve(cwd, opts.outDir || 'dist'); - const adapter = (opts.adapter || 'node') as AdapterName; await ensureDir(outDir); - // Validate adapter const template = ADAPTERS[adapter]; if (!template) { const available = Object.keys(ADAPTERS).join(', '); throw new Error(`Unknown adapter: ${adapter}. Available: ${available}`); } - // Warn about experimental adapters if (adapter === 'cloudflare') { console.log( - c('yellow', '⚠️ Cloudflare Workers adapter is experimental. See docs for limitations.'), + c('yellow', 'Cloudflare Workers adapter is experimental. See docs for limitations.'), ); } @@ -130,9 +157,8 @@ export async function runBuild(opts: ParsedArgs): Promise { console.log(`${c('cyan', '[build]')} entry: ${path.relative(cwd, entry)}`); console.log(`${c('cyan', '[build]')} outDir: ${path.relative(cwd, outDir)}`); - console.log(`${c('cyan', '[build]')} adapter: ${adapter} (${moduleFormat})`); + console.log(`${c('cyan', '[build]')} target: ${adapter} (${moduleFormat})`); - // Build TypeScript compiler arguments const tsconfigPath = path.join(cwd, 'tsconfig.json'); const hasTsconfig = await fileExists(tsconfigPath); const args: string[] = ['-y', 'tsc']; @@ -151,21 +177,18 @@ export async function runBuild(opts: ParsedArgs): Promise { args.push('--target', REQUIRED_DECORATOR_FIELDS.target); } - // Always pass module format to override tsconfig args.push('--module', moduleFormat); args.push('--outDir', outDir); args.push('--skipLibCheck'); - // Run TypeScript compiler await runCmd('npx', args); - // Generate adapter-specific files if (adapter !== 'node') { console.log(c('cyan', `[build] Generating ${adapter} deployment files...`)); const entryBasename = path.basename(entry); await generateAdapterFiles(adapter, outDir, entryBasename, cwd); } - console.log(c('green', '✅ Build completed.')); + console.log(c('green', 'Build completed.')); console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`)); } diff --git a/libs/cli/src/commands/build/register.ts b/libs/cli/src/commands/build/register.ts index 9165ddcdf..1954fe5f1 100644 --- a/libs/cli/src/commands/build/register.ts +++ b/libs/cli/src/commands/build/register.ts @@ -1,16 +1,16 @@ import { Command } from 'commander'; import { toParsedArgs } from '../../core/bridge'; +const BUILD_TARGETS = ['cli', 'node', 'sdk', 'browser', 'cloudflare-worker', 'vercel-edge', 'lambda']; + export function registerBuildCommands(program: Command): void { program .command('build') - .description('Compile entry with TypeScript (tsc)') + .description('Build for a deployment target') + .option('-t, --target ', `Build target: ${BUILD_TARGETS.join(', ')}`) + .option('--js', 'Output JS bundle instead of native binary (cli target only)') .option('-o, --out-dir ', 'Output directory') .option('-e, --entry ', 'Manually specify entry file path') - .option('-a, --adapter ', 'Deployment adapter: node, vercel, lambda, cloudflare') - .option('--exec', 'Build distributable executable bundle (esbuild)') - .option('--cli', 'Generate CLI with subcommands per tool (use with --exec)') - .option('--sea', 'Build as single executable binary (use with --exec, Node.js 20.13+ required)') .action(async (options) => { options.outDir = options.outDir || 'dist'; const { runBuild } = await import('./index.js'); diff --git a/libs/cli/src/commands/build/sdk/index.ts b/libs/cli/src/commands/build/sdk/index.ts new file mode 100644 index 000000000..6d8d326ff --- /dev/null +++ b/libs/cli/src/commands/build/sdk/index.ts @@ -0,0 +1,80 @@ +import * as path from 'path'; +import { ParsedArgs } from '../../../core/args'; +import { c } from '../../../core/colors'; +import { ensureDir, runCmd } from '@frontmcp/utils'; +import { resolveEntry } from '../../../shared/fs'; + +/** + * Build a direct-client SDK library for Node.js applications. + * + * Produces CJS + ESM dual output with type declarations so the server + * can be consumed as a library dependency (e.g., `import { DirectMcpServer } from './my-mcp'`). + * + * @example + * ```bash + * frontmcp build --target sdk + * ``` + */ +export async function buildSdk(opts: ParsedArgs): Promise { + const cwd = process.cwd(); + const entry = await resolveEntry(cwd, opts.entry); + const outDir = path.resolve(cwd, opts.outDir || 'dist'); + await ensureDir(outDir); + + console.log(`${c('cyan', '[build:sdk]')} entry: ${path.relative(cwd, entry)}`); + console.log(`${c('cyan', '[build:sdk]')} outDir: ${path.relative(cwd, outDir)}`); + + // Step 1: Compile TypeScript with declaration emit + console.log(c('cyan', '[build:sdk] Compiling TypeScript...')); + const tscArgs = [ + '-y', 'tsc', + '--project', path.join(cwd, 'tsconfig.json'), + '--outDir', outDir, + '--declaration', '--declarationMap', + '--skipLibCheck', + ]; + await runCmd('npx', tscArgs); + + // Step 2: Bundle CJS with esbuild + console.log(c('cyan', '[build:sdk] Bundling CJS...')); + const esbuild = await import('esbuild'); + const pkg = require(path.join(cwd, 'package.json')); + const appName = pkg.name || path.basename(cwd); + + const sharedBuildOptions: import('esbuild').BuildOptions = { + entryPoints: [entry], + bundle: true, + platform: 'node', + target: 'node22', + keepNames: true, + treeShaking: true, + sourcemap: true, + external: [ + '@frontmcp/sdk', '@frontmcp/di', '@frontmcp/utils', + '@frontmcp/auth', '@frontmcp/adapters', '@frontmcp/plugins', + 'reflect-metadata', 'better-sqlite3', 'fsevents', + // Keep user's dependencies external + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.peerDependencies || {}), + ], + }; + + await esbuild.build({ + ...sharedBuildOptions, + format: 'cjs', + outfile: path.join(outDir, `${appName}.cjs.js`), + }); + + // Step 3: Bundle ESM + console.log(c('cyan', '[build:sdk] Bundling ESM...')); + await esbuild.build({ + ...sharedBuildOptions, + format: 'esm', + outfile: path.join(outDir, `${appName}.esm.mjs`), + }); + + console.log(c('green', `\n SDK build complete:`)); + console.log(c('gray', ` CJS: ${appName}.cjs.js`)); + console.log(c('gray', ` ESM: ${appName}.esm.mjs`)); + console.log(c('gray', ` Types: *.d.ts`)); +} diff --git a/libs/cli/src/commands/package/install.ts b/libs/cli/src/commands/package/install.ts index 3a93935ca..96c00232f 100644 --- a/libs/cli/src/commands/package/install.ts +++ b/libs/cli/src/commands/package/install.ts @@ -61,7 +61,7 @@ export async function runInstall(opts: ParsedArgs): Promise { if (fs.existsSync(configPath) || fs.existsSync(configJsonPath)) { console.log(`${c('cyan', '[install]')} no manifest found, building from config...`); - await runCmd('npx', ['frontmcp', 'build', '--exec'], { + await runCmd('npx', ['frontmcp', 'build', '--target', 'node'], { cwd: packageDir, }); manifest = findManifest(path.join(packageDir, 'dist')) || findManifest(packageDir); @@ -71,7 +71,7 @@ export async function runInstall(opts: ParsedArgs): Promise { if (!manifest) { throw new Error( 'Could not find or generate a manifest. Ensure the package has a ' + - 'frontmcp.config.js or was built with "frontmcp build --exec".', + 'frontmcp.config.js or was built with "frontmcp build --target node".', ); } diff --git a/libs/cli/src/core/__tests__/args-new-flags.spec.ts b/libs/cli/src/core/__tests__/args-new-flags.spec.ts index 9816bb1ba..f3da71117 100644 --- a/libs/cli/src/core/__tests__/args-new-flags.spec.ts +++ b/libs/cli/src/core/__tests__/args-new-flags.spec.ts @@ -1,31 +1,6 @@ import { parseArgs } from '../args'; describe('parseArgs - new flags', () => { - describe('--exec flag', () => { - it('should parse --exec flag', () => { - const result = parseArgs(['build', '--exec']); - expect(result.exec).toBe(true); - }); - - it('should not set exec when absent', () => { - const result = parseArgs(['build']); - expect(result.exec).toBeUndefined(); - }); - }); - - describe('--cli flag', () => { - it('should parse --cli flag', () => { - const result = parseArgs(['build', '--exec', '--cli']); - expect(result.cli).toBe(true); - expect(result.exec).toBe(true); - }); - - it('should not set cli when absent', () => { - const result = parseArgs(['build', '--exec']); - expect(result.cli).toBeUndefined(); - }); - }); - describe('--port / -p flag', () => { it('should parse --port with value', () => { const result = parseArgs(['start', 'my-app', '--port', '3005']); diff --git a/libs/cli/src/core/__tests__/args.spec.ts b/libs/cli/src/core/__tests__/args.spec.ts index 07890f896..aca6a8589 100644 --- a/libs/cli/src/core/__tests__/args.spec.ts +++ b/libs/cli/src/core/__tests__/args.spec.ts @@ -144,18 +144,6 @@ describe('parseArgs', () => { }); }); - describe('--adapter / -a flag', () => { - it('should parse --adapter with value', () => { - const result = parseArgs(['build', '--adapter', 'vercel']); - expect(result.adapter).toBe('vercel'); - }); - - it('should parse -a short flag with value', () => { - const result = parseArgs(['build', '-a', 'lambda']); - expect(result.adapter).toBe('lambda'); - }); - }); - describe('--verbose / -v flag', () => { it('should parse --verbose flag', () => { const result = parseArgs(['dev', '--verbose']); diff --git a/libs/cli/src/core/__tests__/bridge.spec.ts b/libs/cli/src/core/__tests__/bridge.spec.ts index 02dbd991c..a1ddf01be 100644 --- a/libs/cli/src/core/__tests__/bridge.spec.ts +++ b/libs/cli/src/core/__tests__/bridge.spec.ts @@ -28,19 +28,15 @@ describe('toParsedArgs', () => { expect(result.maxRestarts).toBe(3); }); - it('should map build command with exec/cli/outDir/adapter', () => { + it('should map build command with target/outDir', () => { const result = toParsedArgs('build', [], { - exec: true, - cli: true, + target: 'cli', outDir: 'build', - adapter: 'vercel', }); expect(result._).toEqual(['build']); - expect(result.exec).toBe(true); - expect(result.cli).toBe(true); + expect(result.buildTarget).toBe('cli'); expect(result.outDir).toBe('build'); - expect(result.adapter).toBe('vercel'); }); it('should map test command with all test options', () => { @@ -109,7 +105,7 @@ describe('toParsedArgs', () => { expect(result._).toEqual(['list']); expect(result.outDir).toBeUndefined(); expect(result.entry).toBeUndefined(); - expect(result.exec).toBeUndefined(); + expect(result.buildTarget).toBeUndefined(); }); it('should map stop command with force flag', () => { @@ -139,9 +135,9 @@ describe('toParsedArgs', () => { }); it('should not set fields for undefined option values', () => { - const result = toParsedArgs('build', [], { exec: undefined }); + const result = toParsedArgs('build', [], { target: undefined }); - expect(result.exec).toBeUndefined(); - expect('exec' in result).toBe(false); + expect(result.buildTarget).toBeUndefined(); + expect('buildTarget' in result).toBe(false); }); }); diff --git a/libs/cli/src/core/__tests__/help.spec.ts b/libs/cli/src/core/__tests__/help.spec.ts index bbecc8cca..e623add57 100644 --- a/libs/cli/src/core/__tests__/help.spec.ts +++ b/libs/cli/src/core/__tests__/help.spec.ts @@ -57,7 +57,7 @@ describe('customizeHelp', () => { const help = getHelpOutput(); expect(help).toContain('Examples'); expect(help).toContain('frontmcp dev'); - expect(help).toContain('frontmcp build --exec'); + expect(help).toContain('frontmcp build --target node'); }); it('should show subcommand help for build', () => { @@ -67,8 +67,8 @@ describe('customizeHelp', () => { let output = ''; buildCmd.configureOutput({ writeOut: (str) => (output += str) }); buildCmd.outputHelp(); - expect(output).toContain('--exec'); - expect(output).toContain('--cli'); + expect(output).toContain('--target'); + expect(output).toContain('--js'); expect(output).toContain('--out-dir'); }); }); diff --git a/libs/cli/src/core/__tests__/program.spec.ts b/libs/cli/src/core/__tests__/program.spec.ts index c06d2f995..308e30366 100644 --- a/libs/cli/src/core/__tests__/program.spec.ts +++ b/libs/cli/src/core/__tests__/program.spec.ts @@ -36,7 +36,7 @@ describe('createProgram', () => { ]); }); - it('should parse build --exec --cli options', async () => { + it('should parse build --target option', async () => { const program = createProgram(); program.exitOverride(); // Prevent the action from running (we only test parsing) @@ -46,11 +46,10 @@ describe('createProgram', () => { /* no-op */ }); - await program.parseAsync(['node', 'frontmcp', 'build', '--exec', '--cli']); + await program.parseAsync(['node', 'frontmcp', 'build', '--target', 'cli']); const opts = buildCmd.opts(); - expect(opts.exec).toBe(true); - expect(opts.cli).toBe(true); + expect(opts.target).toBe('cli'); }); it('should parse start with name and --port option', async () => { diff --git a/libs/cli/src/core/args.ts b/libs/cli/src/core/args.ts index dc3807c15..b20ed0a53 100644 --- a/libs/cli/src/core/args.ts +++ b/libs/cli/src/core/args.ts @@ -23,6 +23,7 @@ export type Command = | 'configure'; export type DeploymentAdapter = 'node' | 'vercel' | 'lambda' | 'cloudflare'; +export type BuildTarget = 'cli' | 'node' | 'sdk' | 'browser' | 'cloudflare-worker' | 'vercel-edge' | 'lambda'; export type RedisSetupOption = 'docker' | 'existing' | 'none'; export type PackageManagerOption = 'npm' | 'yarn' | 'pnpm'; @@ -36,7 +37,10 @@ export interface ParsedArgs { verbose?: boolean; timeout?: number; coverage?: boolean; - adapter?: DeploymentAdapter; + // Build --target flag (unified build target) + buildTarget?: BuildTarget; + // Build --js flag (cli target: produce JS bundle instead of SEA binary) + js?: boolean; // Create command flags yes?: boolean; target?: DeploymentAdapter; @@ -47,12 +51,6 @@ export interface ParsedArgs { socket?: string; db?: string; background?: boolean; - // Build --exec flag - exec?: boolean; - // Build --exec --cli flag - cli?: boolean; - // Build --exec --sea flag - sea?: boolean; // Process Manager flags port?: number; force?: boolean; @@ -75,7 +73,6 @@ export function parseArgs(argv: string[]): ParsedArgs { const a = argv[i]; if (a === '--out-dir' || a === '-o') out.outDir = argv[++i]; else if (a === '--entry' || a === '-e') out.entry = argv[++i]; - else if (a === '--adapter' || a === '-a') out.adapter = argv[++i] as DeploymentAdapter; else if (a === '--help' || a === '-h') out.help = true; else if (a === '--runInBand' || a === '-i') out.runInBand = true; else if (a === '--watch' || a === '-w') out.watch = true; @@ -86,8 +83,11 @@ export function parseArgs(argv: string[]): ParsedArgs { } else if (a === '--coverage' || a === '-c') out.coverage = true; // Create command flags else if (a === '--yes' || a === '-y') out.yes = true; - else if (a === '--target') out.target = argv[++i] as DeploymentAdapter; - else if (a === '--redis') out.redis = argv[++i] as RedisSetupOption; + else if (a === '--target') { + const val = argv[++i]; + out.target = val as DeploymentAdapter; + out.buildTarget = val as BuildTarget; + } else if (a === '--redis') out.redis = argv[++i] as RedisSetupOption; else if (a === '--cicd') out.cicd = true; else if (a === '--no-cicd') out.cicd = false; else if (a === '--pm') out.pm = argv[++i] as PackageManagerOption; @@ -95,10 +95,7 @@ export function parseArgs(argv: string[]): ParsedArgs { else if (a === '--socket' || a === '-s') out.socket = argv[++i]; else if (a === '--db') out.db = argv[++i]; else if (a === '--background' || a === '-b') out.background = true; - // Build --exec flag - else if (a === '--exec') out.exec = true; - else if (a === '--cli') out.cli = true; - else if (a === '--sea') out.sea = true; + else if (a === '--js') out.js = true; // Process Manager flags else if (a === '--port' || a === '-p') { const parsed = parseInt(argv[++i], 10); diff --git a/libs/cli/src/core/bridge.ts b/libs/cli/src/core/bridge.ts index 555d21b7f..bcb1f9018 100644 --- a/libs/cli/src/core/bridge.ts +++ b/libs/cli/src/core/bridge.ts @@ -1,11 +1,8 @@ -import { DeploymentAdapter, PackageManagerOption, ParsedArgs, RedisSetupOption } from './args'; +import { BuildTarget, DeploymentAdapter, PackageManagerOption, ParsedArgs, RedisSetupOption } from './args'; /** * Convert commander's parsed command name, positional arguments, and options - * into the legacy {@link ParsedArgs} shape expected by existing handlers. - * - * This is a Phase-1 bridge — it will be removed once each handler accepts - * its own typed options interface (Phase 2). + * into the {@link ParsedArgs} shape expected by existing handlers. */ export function toParsedArgs( commandName: string, @@ -17,16 +14,16 @@ export function toParsedArgs( // General if (options['outDir'] !== undefined) out.outDir = options['outDir'] as string; if (options['entry'] !== undefined) out.entry = options['entry'] as string; - if (options['adapter'] !== undefined) out.adapter = options['adapter'] as DeploymentAdapter; - // Build - if (options['exec'] !== undefined) out.exec = options['exec'] as boolean; - if (options['cli'] !== undefined) out.cli = options['cli'] as boolean; - if (options['sea'] !== undefined) out.sea = options['sea'] as boolean; + // Build --target resolution + if (commandName === 'build') { + if (options['target'] !== undefined) out.buildTarget = options['target'] as BuildTarget; + if (options['js'] !== undefined) out.js = options['js'] as boolean; + } // Create if (options['yes'] !== undefined) out.yes = options['yes'] as boolean; - if (options['target'] !== undefined) out.target = options['target'] as DeploymentAdapter; + if (commandName === 'create' && options['target'] !== undefined) out.target = options['target'] as DeploymentAdapter; if (options['redis'] !== undefined) out.redis = options['redis'] as RedisSetupOption; if (options['cicd'] !== undefined) out.cicd = options['cicd'] as boolean; if (options['pm'] !== undefined) out.pm = options['pm'] as PackageManagerOption; diff --git a/libs/cli/src/core/help.ts b/libs/cli/src/core/help.ts index d105c9754..1c546c0ec 100644 --- a/libs/cli/src/core/help.ts +++ b/libs/cli/src/core/help.ts @@ -65,9 +65,9 @@ export function customizeHelp(program: Command): void { ` ${c('bold', 'Examples')} frontmcp dev - frontmcp build --out-dir build - frontmcp build --exec - frontmcp build --exec --cli + frontmcp build --target node + frontmcp build --target cli + frontmcp build --target cli --js frontmcp test --runInBand frontmcp init frontmcp doctor diff --git a/yarn.lock b/yarn.lock index 56442d267..9c59b7595 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1513,33 +1513,6 @@ "@eslint/core" "^0.17.0" levn "^0.4.1" -"@floating-ui/core@^1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.4.tgz#4a006a6e01565c0f87ba222c317b056a2cffd2f4" - integrity sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg== - dependencies: - "@floating-ui/utils" "^0.2.10" - -"@floating-ui/dom@^1.7.5": - version "1.7.5" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.5.tgz#60bfc83a4d1275b2a90db76bf42ca2a5f2c231c2" - integrity sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg== - dependencies: - "@floating-ui/core" "^1.7.4" - "@floating-ui/utils" "^0.2.10" - -"@floating-ui/react-dom@^2.0.0": - version "2.1.7" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.7.tgz#529475cc16ee4976ba3387968117e773d9aa703e" - integrity sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg== - dependencies: - "@floating-ui/dom" "^1.7.5" - -"@floating-ui/utils@^0.2.10": - version "0.2.10" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" - integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== - "@hono/node-server@^1.19.9": version "1.19.9" resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.9.tgz#8f37119b1acf283fd3f6035f3d1356fdb97a09ac" @@ -2282,19 +2255,6 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== -"@mcp-ui/client@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@mcp-ui/client/-/client-6.1.0.tgz#8bc81d7852f0971fc621743f07782b1933288497" - integrity sha512-Wk/9uhu8xdOgHjiaEtAq2RbXn4WGstpFeJ6I71JCP7JC7MtvQB/qnEKDVGSbjwyLnIeZYMSILHf5E+57/YCftQ== - dependencies: - "@modelcontextprotocol/ext-apps" "^0.3.1" - "@modelcontextprotocol/sdk" "^1.24.0" - "@quilted/threads" "^3.1.3" - "@r2wc/react-to-web-component" "^2.0.4" - "@remote-dom/core" "^1.8.0" - "@remote-dom/react" "^1.2.2" - zod "^3.23.8" - "@mdx-js/mdx@^3.1.1": version "3.1.1" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-3.1.1.tgz#c5ffd991a7536b149e17175eee57a1a2a511c6d1" @@ -2333,127 +2293,6 @@ dependencies: langium "^4.0.0" -"@modelcontextprotocol/ext-apps@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/ext-apps/-/ext-apps-0.3.1.tgz#d0911b9d8a94f99bc84c1f43f698c47a9fee3941" - integrity sha512-Iivz2KwWK8xlRbiWwFB/C4NXqE8VJBoRCbBkJCN98ST2UbQvA6kfyebcLsypiqylJS467XOOaBcI9DeQ3t+zqA== - optionalDependencies: - "@oven/bun-darwin-aarch64" "^1.2.21" - "@oven/bun-darwin-x64" "^1.2.21" - "@oven/bun-darwin-x64-baseline" "^1.2.21" - "@oven/bun-linux-aarch64" "^1.2.21" - "@oven/bun-linux-aarch64-musl" "^1.2.21" - "@oven/bun-linux-x64" "^1.2.21" - "@oven/bun-linux-x64-baseline" "^1.2.21" - "@oven/bun-linux-x64-musl" "^1.2.21" - "@oven/bun-linux-x64-musl-baseline" "^1.2.21" - "@oven/bun-windows-x64" "^1.2.21" - "@oven/bun-windows-x64-baseline" "^1.2.21" - "@rollup/rollup-darwin-arm64" "^4.53.3" - "@rollup/rollup-darwin-x64" "^4.53.3" - "@rollup/rollup-linux-arm64-gnu" "^4.53.3" - "@rollup/rollup-linux-x64-gnu" "^4.53.3" - "@rollup/rollup-win32-arm64-msvc" "^4.53.3" - "@rollup/rollup-win32-x64-msvc" "^4.53.3" - -"@modelcontextprotocol/ext-apps@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.1.tgz#2fb809ac4582b6f5410a19f6aa9cfa73661202a9" - integrity sha512-rAPzBbB5GNgYk216paQjGKUgbNXSy/yeR95c0ni6Y4uvhWI2AeF+ztEOqQFLBMQy/MPM+02pbVK1HaQmQjMwYQ== - optionalDependencies: - "@oven/bun-darwin-aarch64" "^1.2.21" - "@oven/bun-darwin-x64" "^1.2.21" - "@oven/bun-darwin-x64-baseline" "^1.2.21" - "@oven/bun-linux-aarch64" "^1.2.21" - "@oven/bun-linux-aarch64-musl" "^1.2.21" - "@oven/bun-linux-x64" "^1.2.21" - "@oven/bun-linux-x64-baseline" "^1.2.21" - "@oven/bun-linux-x64-musl" "^1.2.21" - "@oven/bun-linux-x64-musl-baseline" "^1.2.21" - "@oven/bun-windows-x64" "^1.2.21" - "@oven/bun-windows-x64-baseline" "^1.2.21" - "@rollup/rollup-darwin-arm64" "^4.53.3" - "@rollup/rollup-darwin-x64" "^4.53.3" - "@rollup/rollup-linux-arm64-gnu" "^4.53.3" - "@rollup/rollup-linux-x64-gnu" "^4.53.3" - "@rollup/rollup-win32-arm64-msvc" "^4.53.3" - "@rollup/rollup-win32-x64-msvc" "^4.53.3" - -"@modelcontextprotocol/inspector-cli@^0.21.1": - version "0.21.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/inspector-cli/-/inspector-cli-0.21.1.tgz#933813f629462755134f3c4c115c6703fd0c8658" - integrity sha512-IuBHJXEjuprPm+YtJGunAuqS3dnDtqf5VOVkzauRGICCEbT860YnDHNhxZARo5h0iMFYwEgRJvMQQwYayEE28w== - dependencies: - "@modelcontextprotocol/sdk" "^1.25.2" - commander "^13.1.0" - express "^5.2.1" - spawn-rx "^5.1.2" - -"@modelcontextprotocol/inspector-client@^0.21.1": - version "0.21.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/inspector-client/-/inspector-client-0.21.1.tgz#dacf22b5ed3b7d68057f9c5af7be7d6ba5ff2212" - integrity sha512-IZlriZkASr0nbObb4HmNY+dw6Q2/jb126Eh2D97xasuhMZNB1TTBrIt7QHNy2TjhH/qV3acx29rgzMBIys3FAQ== - dependencies: - "@mcp-ui/client" "^6.0.0" - "@modelcontextprotocol/ext-apps" "^1.0.0" - "@modelcontextprotocol/sdk" "^1.25.2" - "@radix-ui/react-checkbox" "^1.1.4" - "@radix-ui/react-dialog" "^1.1.3" - "@radix-ui/react-icons" "^1.3.0" - "@radix-ui/react-label" "^2.1.0" - "@radix-ui/react-popover" "^1.1.3" - "@radix-ui/react-select" "^2.1.2" - "@radix-ui/react-slot" "^1.1.0" - "@radix-ui/react-switch" "^1.2.6" - "@radix-ui/react-tabs" "^1.1.1" - "@radix-ui/react-toast" "^1.2.6" - "@radix-ui/react-tooltip" "^1.1.8" - ajv "^6.12.6" - class-variance-authority "^0.7.0" - clsx "^2.1.1" - cmdk "^1.0.4" - lucide-react "^0.523.0" - pkce-challenge "^4.1.0" - prismjs "^1.30.0" - react "^18.3.1" - react-dom "^18.3.1" - react-simple-code-editor "^0.14.1" - serve-handler "^6.1.6" - tailwind-merge "^2.5.3" - zod "^3.25.76" - -"@modelcontextprotocol/inspector-server@^0.21.1": - version "0.21.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/inspector-server/-/inspector-server-0.21.1.tgz#4413cc58f6b825e9f8a7ee3f06563885f1262c1d" - integrity sha512-dNPbz37aHMkuwwr/AZCnmoigDFSCbilMHkDQ2SVt0yORCgeoIX2li2La6XbmZIKAp14sCpJCKKKpmhwJ9uRiLA== - dependencies: - "@modelcontextprotocol/sdk" "^1.25.2" - cors "^2.8.5" - express "^5.1.0" - express-rate-limit "^8.2.1" - shell-quote "^1.8.3" - shx "^0.3.4" - spawn-rx "^5.1.2" - ws "^8.18.0" - zod "^3.25.76" - -"@modelcontextprotocol/inspector@^0.21.1": - version "0.21.1" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/inspector/-/inspector-0.21.1.tgz#47829a8eac3fa7097d6881f0ff1463e97656a472" - integrity sha512-9d2zJ97Bg9hHj4Qz1b06aCmIu4/Ts84H8RqqJteORJUyxaK8DGfXQ3JoS0PsU8gHoHWabFAs4HAiy/ePU8Oa2A== - dependencies: - "@modelcontextprotocol/inspector-cli" "^0.21.1" - "@modelcontextprotocol/inspector-client" "^0.21.1" - "@modelcontextprotocol/inspector-server" "^0.21.1" - "@modelcontextprotocol/sdk" "^1.25.2" - concurrently "^9.2.0" - node-fetch "^3.3.2" - open "^10.2.0" - shell-quote "^1.8.3" - spawn-rx "^5.1.2" - ts-node "^10.9.2" - zod "^3.25.76" - "@modelcontextprotocol/sdk@1.27.1": version "1.27.1" resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz#a602cf823bf8a68e13e7112f50aeb02b09fb83b9" @@ -2477,29 +2316,6 @@ zod "^3.25 || ^4.0" zod-to-json-schema "^3.25.1" -"@modelcontextprotocol/sdk@^1.24.0", "@modelcontextprotocol/sdk@^1.25.2": - version "1.26.0" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz#5b35d73062125f126cc70b0be83cbab53bcdde74" - integrity sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg== - dependencies: - "@hono/node-server" "^1.19.9" - ajv "^8.17.1" - ajv-formats "^3.0.1" - content-type "^1.0.5" - cors "^2.8.5" - cross-spawn "^7.0.5" - eventsource "^3.0.2" - eventsource-parser "^3.0.0" - express "^5.2.1" - express-rate-limit "^8.2.1" - hono "^4.11.4" - jose "^6.1.3" - json-schema-typed "^8.0.2" - pkce-challenge "^5.0.0" - raw-body "^3.0.0" - zod "^3.25 || ^4.0" - zod-to-json-schema "^3.25.1" - "@module-federation/error-codes@0.22.0": version "0.22.0" resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.22.0.tgz#31ccc990dc240d73912ba7bd001f7e35ac751992" @@ -3163,61 +2979,6 @@ tslib "^2.3.0" yargs-parser "21.1.1" -"@oven/bun-darwin-aarch64@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.9.tgz#dadfbc44ae0b4ecdd4a631b7b4bce24906b20d3b" - integrity sha512-df7smckMWSUfaT5mzwN9Lfpd3ZGkOqo+vmQ8VV2a32gl14v6uZ/qeeo+1RlANXn8M0uzXPWWCkrKZIWSZUR0qw== - -"@oven/bun-darwin-x64-baseline@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.9.tgz#04ff8be1f7ac2b354cca8c42f04e92d624ec5894" - integrity sha512-XbhsA2XAFzvFr0vPSV6SNqGxab4xHKdPmVTLqoSHAx9tffrSq/012BDptOskulwnD+YNsrJUx2D2Ve1xvfgGcg== - -"@oven/bun-darwin-x64@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.9.tgz#5a7eea20640e0ae2b6a0f71612aa4dd4797c2e45" - integrity sha512-YiLxfsPzQqaVvT2a+nxH9do0YfUjrlxF3tKP0b1DDgvfgCcVKGsrQH3Wa82qHgL4dnT8h2bqi94JxXESEuPmcA== - -"@oven/bun-linux-aarch64-musl@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.9.tgz#16317da524b2e1c468ef796743e2531323f6c91c" - integrity sha512-t8uimCVBTw5f9K2QTZE5wN6UOrFETNrh/Xr7qtXT9nAOzaOnIFvYA+HcHbGfi31fRlCVfTxqm/EiCwJ1gEw9YQ== - -"@oven/bun-linux-aarch64@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.9.tgz#94a144e50f85e696512916176174713e16036425" - integrity sha512-VaNQTu0Up4gnwZLQ6/Hmho6jAlLxTQ1PwxEth8EsXHf82FOXXPV5OCQ6KC9mmmocjKlmWFaIGebThrOy8DUo4g== - -"@oven/bun-linux-x64-baseline@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.9.tgz#fa53f49f63dc1057f129d270b4a986a53b5a1f00" - integrity sha512-nZ12g22cy7pEOBwAxz2tp0wVqekaCn9QRKuGTHqOdLlyAqR4SCdErDvDhUWd51bIyHTQoCmj72TegGTgG0WNPw== - -"@oven/bun-linux-x64-musl-baseline@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.9.tgz#f6ddcf78c16bb7bcea5278bd5d36439bbd02555a" - integrity sha512-3FXQgtYFsT0YOmAdMcJn56pLM5kzSl6y942rJJIl5l2KummB9Ea3J/vMJMzQk7NCAGhleZGWU/pJSS/uXKGa7w== - -"@oven/bun-linux-x64-musl@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.9.tgz#1042cc4e3e543456e501b94677f463109e9dcdbb" - integrity sha512-4ZjIUgCxEyKwcKXideB5sX0KJpnHTZtu778w73VNq2uNH2fNpMZv98+DBgJyQ9OfFoRhmKn1bmLmSefvnHzI9w== - -"@oven/bun-linux-x64@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-linux-x64/-/bun-linux-x64-1.3.9.tgz#728fbb706add9f89a74dba0a33369d1f73b5f69c" - integrity sha512-oQyAW3+ugulvXTZ+XYeUMmNPR94sJeMokfHQoKwPvVwhVkgRuMhcLGV2ZesHCADVu30Oz2MFXbgdC8x4/o9dRg== - -"@oven/bun-windows-x64-baseline@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.9.tgz#b226f3b466b04e9d61309eac385b5082f7e29a9f" - integrity sha512-a/+hSrrDpMD7THyXvE2KJy1skxzAD0cnW4K1WjuI/91VqsphjNzvf5t/ZgxEVL4wb6f+hKrSJ5J3aH47zPr61g== - -"@oven/bun-windows-x64@^1.2.21": - version "1.3.9" - resolved "https://registry.yarnpkg.com/@oven/bun-windows-x64/-/bun-windows-x64-1.3.9.tgz#1580e78c73db8a211a353d1250d844e767009a8a" - integrity sha512-/d6vAmgKvkoYlsGPsRPlPmOK1slPis/F40UG02pYwypTH0wmY0smgzdFqR4YmryxFh17XrW1kITv+U99Oajk9Q== - "@parcel/watcher-android-arm64@2.5.6": version "2.5.6" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564" @@ -3472,11 +3233,6 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@preact/signals-core@^1.8.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@preact/signals-core/-/signals-core-1.13.0.tgz#ed770df2855701e7b42828fae5a348edeee9a3df" - integrity sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg== - "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -3530,428 +3286,11 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@quilted/events@^2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@quilted/events/-/events-2.1.3.tgz#ba9818a34c6f976814ce45822d660961a6f47d94" - integrity sha512-4fHaSLND8rmZ+tce9/4FNmG5UWTRpFtM54kOekf3tLON4ZLLnYzjjldELD35efd7+lT5+E3cdkacqc56d+kCrQ== - dependencies: - "@preact/signals-core" "^1.8.0" - -"@quilted/threads@^3.1.3": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@quilted/threads/-/threads-3.3.1.tgz#3be1291659204de3ded51cfbedca0138b8b453a3" - integrity sha512-0ASnjTH+hOu1Qwzi9NnsVcsbMhWVx8pEE8SXIHknqcc/1rXAU0QlKw9ARq0W43FAdzyVeuXeXtZN27ZC0iALKg== - dependencies: - "@quilted/events" "^2.1.3" - -"@r2wc/core@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@r2wc/core/-/core-1.3.0.tgz#ad3a0e1a022d7186007403b3319c2245de4db721" - integrity sha512-aPBnND92Itl+SWWbWyyxdFFF0+RqKB6dptGHEdiPB8ZvnHWHlVzOfEvbEcyUYGtB6HBdsfkVuBiaGYyBFVTzVQ== - -"@r2wc/react-to-web-component@^2.0.4": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@r2wc/react-to-web-component/-/react-to-web-component-2.1.0.tgz#1040f0945a34ccca2e223dcbb5f87edcabc97d6b" - integrity sha512-m/PzgUOEiL1HxmvfP5LgBLqB7sHeRj+d1QAeZklwS4OEI2HUU+xTpT3hhJipH5DQoFInDqDTfe0lNFFKcrqk4w== - dependencies: - "@r2wc/core" "^1.3.0" - -"@radix-ui/number@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090" - integrity sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g== - -"@radix-ui/primitive@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba" - integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg== - -"@radix-ui/react-arrow@1.1.7": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09" - integrity sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w== - dependencies: - "@radix-ui/react-primitive" "2.1.3" - -"@radix-ui/react-checkbox@^1.1.4": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz#db45ca8a6d5c056a92f74edbb564acee05318b79" - integrity sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-controllable-state" "1.2.2" - "@radix-ui/react-use-previous" "1.1.1" - "@radix-ui/react-use-size" "1.1.1" - -"@radix-ui/react-collection@1.1.7": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec" - integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-slot" "1.2.3" - -"@radix-ui/react-compose-refs@1.1.2", "@radix-ui/react-compose-refs@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" - integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== - -"@radix-ui/react-context@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" - integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== - -"@radix-ui/react-dialog@^1.1.3", "@radix-ui/react-dialog@^1.1.6": - version "1.1.15" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632" - integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-dismissable-layer" "1.1.11" - "@radix-ui/react-focus-guards" "1.1.3" - "@radix-ui/react-focus-scope" "1.1.7" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-portal" "1.1.9" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-slot" "1.2.3" - "@radix-ui/react-use-controllable-state" "1.2.2" - aria-hidden "^1.2.4" - react-remove-scroll "^2.6.3" - -"@radix-ui/react-direction@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14" - integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw== - -"@radix-ui/react-dismissable-layer@1.1.11": - version "1.1.11" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37" - integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-escape-keydown" "1.1.1" - -"@radix-ui/react-focus-guards@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f" - integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw== - -"@radix-ui/react-focus-scope@1.1.7": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d" - integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - -"@radix-ui/react-icons@^1.3.0": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441" - integrity sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g== - -"@radix-ui/react-id@1.1.1", "@radix-ui/react-id@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" - integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== - dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-label@^2.1.0": - version "2.1.8" - resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.8.tgz#d93b7c063ef2ea034df143a2464bfc0548e4b7e5" - integrity sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A== - dependencies: - "@radix-ui/react-primitive" "2.1.4" - -"@radix-ui/react-popover@^1.1.3": - version "1.1.15" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz#9c852f93990a687ebdc949b2c3de1f37cdc4c5d5" - integrity sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-dismissable-layer" "1.1.11" - "@radix-ui/react-focus-guards" "1.1.3" - "@radix-ui/react-focus-scope" "1.1.7" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-popper" "1.2.8" - "@radix-ui/react-portal" "1.1.9" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-slot" "1.2.3" - "@radix-ui/react-use-controllable-state" "1.2.2" - aria-hidden "^1.2.4" - react-remove-scroll "^2.6.3" - -"@radix-ui/react-popper@1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz#a79f39cdd2b09ab9fb50bf95250918422c4d9602" - integrity sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw== - dependencies: - "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.1.7" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-layout-effect" "1.1.1" - "@radix-ui/react-use-rect" "1.1.1" - "@radix-ui/react-use-size" "1.1.1" - "@radix-ui/rect" "1.1.1" - -"@radix-ui/react-portal@1.1.9": - version "1.1.9" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" - integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== - dependencies: - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-presence@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db" - integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-primitive@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" - integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== - dependencies: - "@radix-ui/react-slot" "1.2.3" - -"@radix-ui/react-primitive@2.1.4", "@radix-ui/react-primitive@^2.0.2": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz#2626ea309ebd63bf5767d3e7fc4081f81b993df0" - integrity sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg== - dependencies: - "@radix-ui/react-slot" "1.2.4" - -"@radix-ui/react-roving-focus@1.1.11": - version "1.1.11" - resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz#ef54384b7361afc6480dcf9907ef2fedb5080fd9" - integrity sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-collection" "1.1.7" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-direction" "1.1.1" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-controllable-state" "1.2.2" - -"@radix-ui/react-select@^2.1.2": - version "2.2.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.2.6.tgz#022cf8dab16bf05d0d1b4df9e53e4bea1b744fd9" - integrity sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ== - dependencies: - "@radix-ui/number" "1.1.1" - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-collection" "1.1.7" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-direction" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.11" - "@radix-ui/react-focus-guards" "1.1.3" - "@radix-ui/react-focus-scope" "1.1.7" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-popper" "1.2.8" - "@radix-ui/react-portal" "1.1.9" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-slot" "1.2.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-controllable-state" "1.2.2" - "@radix-ui/react-use-layout-effect" "1.1.1" - "@radix-ui/react-use-previous" "1.1.1" - "@radix-ui/react-visually-hidden" "1.2.3" - aria-hidden "^1.2.4" - react-remove-scroll "^2.6.3" - -"@radix-ui/react-slot@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" - integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - -"@radix-ui/react-slot@1.2.4", "@radix-ui/react-slot@^1.1.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz#63c0ba05fdf90cc49076b94029c852d7bac1fb83" - integrity sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - -"@radix-ui/react-switch@^1.2.6": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz#ff79acb831f0d5ea9216cfcc5b939912571358e3" - integrity sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-controllable-state" "1.2.2" - "@radix-ui/react-use-previous" "1.1.1" - "@radix-ui/react-use-size" "1.1.1" - -"@radix-ui/react-tabs@^1.1.1": - version "1.1.13" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz#3537ce379d7e7ff4eeb6b67a0973e139c2ac1f15" - integrity sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-direction" "1.1.1" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-roving-focus" "1.1.11" - "@radix-ui/react-use-controllable-state" "1.2.2" - -"@radix-ui/react-toast@^1.2.6": - version "1.2.15" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.15.tgz#746cf9a81297ddbfba214e5c81245ea3f706f876" - integrity sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-collection" "1.1.7" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-dismissable-layer" "1.1.11" - "@radix-ui/react-portal" "1.1.9" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-controllable-state" "1.2.2" - "@radix-ui/react-use-layout-effect" "1.1.1" - "@radix-ui/react-visually-hidden" "1.2.3" - -"@radix-ui/react-tooltip@^1.1.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz#3f50267e25bccfc9e20bb3036bfd9ab4c2c30c2c" - integrity sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-dismissable-layer" "1.1.11" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-popper" "1.2.8" - "@radix-ui/react-portal" "1.1.9" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-slot" "1.2.3" - "@radix-ui/react-use-controllable-state" "1.2.2" - "@radix-ui/react-visually-hidden" "1.2.3" - -"@radix-ui/react-use-callback-ref@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" - integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== - -"@radix-ui/react-use-controllable-state@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" - integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== - dependencies: - "@radix-ui/react-use-effect-event" "0.0.2" - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-use-effect-event@0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" - integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== - dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-use-escape-keydown@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" - integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== - dependencies: - "@radix-ui/react-use-callback-ref" "1.1.1" - -"@radix-ui/react-use-layout-effect@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" - integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== - -"@radix-ui/react-use-previous@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz#1a1ad5568973d24051ed0af687766f6c7cb9b5b5" - integrity sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ== - -"@radix-ui/react-use-rect@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" - integrity sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w== - dependencies: - "@radix-ui/rect" "1.1.1" - -"@radix-ui/react-use-size@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37" - integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ== - dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-visually-hidden@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz#a8c38c8607735dc9f05c32f87ab0f9c2b109efbf" - integrity sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug== - dependencies: - "@radix-ui/react-primitive" "2.1.3" - -"@radix-ui/rect@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" - integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== - "@react-leaflet/core@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-3.0.0.tgz#34ccc280ce7d8ac5c09f2b3d5fffded450bdf1a2" integrity sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ== -"@remote-dom/core@^1.7.0", "@remote-dom/core@^1.8.0": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@remote-dom/core/-/core-1.10.1.tgz#85601d31ab78320a07cbcf4aa9a98c5521e374dc" - integrity sha512-MlbUOGuHjOrB7uOkaYkIoLUG8lDK8/H1D7MHnGkgqbG6jwjwQSlGPHhbwnD6HYWsTGpAPOP02Byd8wBt9U6TEw== - dependencies: - "@remote-dom/polyfill" "^1.5.1" - htm "^3.1.1" - -"@remote-dom/polyfill@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@remote-dom/polyfill/-/polyfill-1.5.1.tgz#1a805cc8eb03f790dad43f9ad5d2e8d22df14846" - integrity sha512-eaWdIVKZpNfbqspKkRQLVxiFv/7vIw8u0FVA5oy52YANFbO/WVT0GU+PQmRt/QUSijaB36HBAqx7stjo8HGpVQ== - -"@remote-dom/react@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@remote-dom/react/-/react-1.2.2.tgz#076257c92d91ff8d9ddfd30bcf9fc7463c37db32" - integrity sha512-PkvioODONTr1M0StGDYsR4Ssf5M0Rd4+IlWVvVoK3Zrw8nr7+5mJkgNofaj/z7i8Aep78L28PCW8/WduUt4unA== - dependencies: - "@remote-dom/core" "^1.7.0" - "@types/react" "^18.0.0" - htm "^3.1.1" - "@rolldown/pluginutils@1.0.0-rc.3": version "1.0.0-rc.3" resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz#8a88cc92a0f741befc7bc109cb1a4c6b9408e1c5" @@ -3967,12 +3306,12 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz#10bd0382b73592beee6e9800a69401a29da625c4" integrity sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w== -"@rollup/rollup-darwin-arm64@4.57.1", "@rollup/rollup-darwin-arm64@^4.53.3": +"@rollup/rollup-darwin-arm64@4.57.1": version "4.57.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz#1e99ab04c0b8c619dd7bbde725ba2b87b55bfd81" integrity sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg== -"@rollup/rollup-darwin-x64@4.57.1", "@rollup/rollup-darwin-x64@^4.53.3": +"@rollup/rollup-darwin-x64@4.57.1": version "4.57.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz#69e741aeb2839d2e8f0da2ce7a33d8bd23632423" integrity sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w== @@ -3997,7 +3336,7 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz#6929f3e07be6b6da5991f63c6b68b3e473d0a65a" integrity sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw== -"@rollup/rollup-linux-arm64-gnu@4.57.1", "@rollup/rollup-linux-arm64-gnu@^4.53.3": +"@rollup/rollup-linux-arm64-gnu@4.57.1": version "4.57.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz#06e89fd4a25d21fe5575d60b6f913c0e65297bfa" integrity sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g== @@ -4042,7 +3381,7 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz#1da022ffd2d9e9f0fd8344ea49e113001fbcac64" integrity sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg== -"@rollup/rollup-linux-x64-gnu@4.57.1", "@rollup/rollup-linux-x64-gnu@^4.53.3": +"@rollup/rollup-linux-x64-gnu@4.57.1": version "4.57.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz#78c16eef9520bd10e1ea7a112593bb58e2842622" integrity sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg== @@ -4062,7 +3401,7 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz#f09921d0b2a0b60afbf3586d2a7a7f208ba6df17" integrity sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ== -"@rollup/rollup-win32-arm64-msvc@4.57.1", "@rollup/rollup-win32-arm64-msvc@^4.53.3": +"@rollup/rollup-win32-arm64-msvc@4.57.1": version "4.57.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz#08d491717135376e4a99529821c94ecd433d5b36" integrity sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ== @@ -4077,7 +3416,7 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz#b9cccef26f5e6fdc013bf3c0911a3c77428509d0" integrity sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ== -"@rollup/rollup-win32-x64-msvc@4.57.1", "@rollup/rollup-win32-x64-msvc@^4.53.3": +"@rollup/rollup-win32-x64-msvc@4.57.1": version "4.57.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz#a03348e7b559c792b6277cc58874b89ef46e1e72" integrity sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA== @@ -4898,7 +4237,7 @@ resolved "https://registry.yarnpkg.com/@types/path-browserify/-/path-browserify-1.0.3.tgz#25de712d4def94b3901f033c30d3d3bd16eba8d3" integrity sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw== -"@types/prop-types@*", "@types/prop-types@^15.7.15": +"@types/prop-types@^15.7.15": version "15.7.15" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== @@ -4923,14 +4262,6 @@ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@^18.0.0": - version "18.3.28" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.28.tgz#0a85b1a7243b4258d9f626f43797ba18eb5f8781" - integrity sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw== - dependencies: - "@types/prop-types" "*" - csstype "^3.2.2" - "@types/react@^19.0.0": version "19.2.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" @@ -5761,7 +5092,7 @@ ajv@8.17.1, ajv@^8.0.0, ajv@^8.12.0, ajv@^8.17.1, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" -ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -5857,13 +5188,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.2.4: - version "1.2.6" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" - integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== - dependencies: - tslib "^2.0.0" - aria-query@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" @@ -6290,11 +5614,6 @@ bundle-name@^4.1.0: dependencies: run-applescript "^7.0.0" -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -6397,7 +5716,7 @@ ccount@^2.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -6501,13 +5820,6 @@ cjs-module-lexer@^2.1.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" integrity sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ== -class-variance-authority@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787" - integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== - dependencies: - clsx "^2.1.1" - classcat@^5.0.3: version "5.0.5" resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" @@ -6606,16 +5918,6 @@ cluster-key-slot@^1.1.0: resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== -cmdk@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.1.1.tgz#b8524272699ccaa37aaf07f36850b376bf3d58e5" - integrity sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg== - dependencies: - "@radix-ui/react-compose-refs" "^1.1.1" - "@radix-ui/react-dialog" "^1.1.6" - "@radix-ui/react-id" "^1.1.0" - "@radix-ui/react-primitive" "^2.0.2" - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -6688,7 +5990,7 @@ commander@^10.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^13.0.0, commander@^13.1.0: +commander@^13.0.0: version "13.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46" integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw== @@ -6752,18 +6054,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concurrently@^9.2.0: - version "9.2.1" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.2.1.tgz#248ea21b95754947be2dad9c3e4b60f18ca4e44f" - integrity sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng== - dependencies: - chalk "4.1.2" - rxjs "7.8.2" - shell-quote "1.8.3" - supports-color "8.1.1" - tree-kill "1.2.2" - yargs "17.7.2" - confbox@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" @@ -6779,11 +6069,6 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== - content-disposition@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.1.tgz#a8b7bbeb2904befdfb6787e5c0c086959f605f9b" @@ -7369,11 +6654,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - data-urls@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" @@ -7399,7 +6679,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.4.3, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.3.7, debug@^4.4.0, debug@^4.4.3: +debug@4, debug@4.4.3, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -7576,11 +6856,6 @@ detect-newline@^3.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -detect-node-es@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" - integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== - detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -8281,7 +7556,7 @@ express@4.22.1, express@^4.22.1: utils-merge "1.0.1" vary "~1.1.2" -express@^5.1.0, express@^5.2.1: +express@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== @@ -8402,14 +7677,6 @@ fdir@^6.5.0: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - figures@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -8597,13 +7864,6 @@ form-data@^4.0.5, form-data@~4.0.4: hasown "^2.0.2" mime-types "^2.1.12" -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -8706,11 +7966,6 @@ get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: hasown "^2.0.2" math-intrinsics "^1.1.0" -get-nonce@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" - integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== - get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -8796,7 +8051,7 @@ glob@^10.3.10, glob@^10.4.1: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: +glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -9124,11 +8379,6 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -htm@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/htm/-/htm-3.1.1.tgz#49266582be0dc66ed2235d5ea892307cc0c24b78" - integrity sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ== - html-encoding-sniffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" @@ -9416,11 +8666,6 @@ internmap@^1.0.0: resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - interpret@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" @@ -10751,11 +9996,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lucide-react@^0.523.0: - version "0.523.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.523.0.tgz#a855a3b5b6fdcdee597ecbbd3c16141711a1d974" - integrity sha512-rUjQoy7egZT9XYVXBK1je9ckBnNp7qzRZOhLQx5RcEp2dCGlXo+mv6vf7Am4LimEcFBJIIZzSGfgTqc9QCrPSw== - lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -11532,18 +10772,6 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-types@2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34, mime-types@~2.1.35: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" @@ -11607,13 +10835,6 @@ minimatch@10.1.1: dependencies: "@isaacs/brace-expansion" "^5.0.0" -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - minimatch@7.4.6: version "7.4.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb" @@ -11628,6 +10849,13 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + minimatch@^5.0.1: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" @@ -11777,20 +11005,6 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - -node-fetch@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - node-fetch@cjs: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -12076,7 +11290,7 @@ onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: platform "^1.3.6" protobufjs "^7.2.4" -open@^10.0.3, open@^10.2.0: +open@^10.0.3: version "10.2.0" resolved "https://registry.yarnpkg.com/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c" integrity sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA== @@ -12321,11 +11535,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-is-inside@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -12344,11 +11553,6 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" - integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== - path-to-regexp@^8.0.0: version "8.3.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" @@ -12485,11 +11689,6 @@ pirates@^4.0.6, pirates@^4.0.7: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== -pkce-challenge@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-4.1.0.tgz#95027d7750c3c0f21676a345b48f481786f9acdb" - integrity sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ== - pkce-challenge@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d" @@ -12902,11 +12101,6 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prismjs@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9" - integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -13067,11 +12261,6 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== - range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -13097,7 +12286,7 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@19.2.3, react-dom@^18.3.1: +react-dom@19.2.3: version "19.2.3" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== @@ -13183,25 +12372,6 @@ react-refresh@^0.18.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== -react-remove-scroll-bar@^2.3.7: - version "2.3.8" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" - integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== - dependencies: - react-style-singleton "^2.2.2" - tslib "^2.0.0" - -react-remove-scroll@^2.6.3: - version "2.7.2" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz#6442da56791117661978ae99cd29be9026fecca0" - integrity sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q== - dependencies: - react-remove-scroll-bar "^2.3.7" - react-style-singleton "^2.2.3" - tslib "^2.1.0" - use-callback-ref "^1.3.3" - use-sidecar "^1.1.3" - react-router-dom@^7.13.1: version "7.13.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.13.1.tgz#74c045acc333ca94612b889cd1b1e1ee9534dead" @@ -13217,11 +12387,6 @@ react-router@7.13.1: cookie "^1.0.1" set-cookie-parser "^2.6.0" -react-simple-code-editor@^0.14.1: - version "0.14.1" - resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz#fd37eb3349f5def45900dd46acf296f796d81d2c" - integrity sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow== - react-smooth@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4" @@ -13231,14 +12396,6 @@ react-smooth@^4.0.4: prop-types "^15.8.1" react-transition-group "^4.4.5" -react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" - integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== - dependencies: - get-nonce "^1.0.0" - tslib "^2.0.0" - react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" @@ -13249,7 +12406,7 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@19.2.3, react@^18.3.1: +react@19.2.3: version "19.2.3" resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8" integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== @@ -13332,13 +12489,6 @@ recharts@^2.13.0: tiny-invariant "^1.3.1" victory-vendor "^36.6.8" -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== - dependencies: - resolve "^1.1.6" - rechoir@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" @@ -13577,7 +12727,7 @@ resolve.exports@2.0.3: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.11: +resolve@^1.1.7, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.11: version "1.22.11" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== @@ -13732,7 +12882,7 @@ rxjs@7.8.1: dependencies: tslib "^2.1.0" -rxjs@7.8.2, rxjs@^7.4.0, rxjs@^7.8.0, rxjs@^7.8.1: +rxjs@^7.4.0, rxjs@^7.8.0: version "7.8.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== @@ -14037,19 +13187,6 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^ dependencies: randombytes "^2.1.0" -serve-handler@^6.1.6: - version "6.1.6" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" - integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - mime-types "2.1.18" - minimatch "3.1.2" - path-is-inside "1.0.2" - path-to-regexp "3.3.0" - range-parser "1.2.0" - serve-index@^1.9.1: version "1.9.2" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.2.tgz#2988e3612106d78a5e4849ddff552ce7bd3d9bcb" @@ -14156,28 +13293,11 @@ shell-exec@1.0.2: resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756" integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg== -shell-quote@1.8.3, shell-quote@^1.8.3: +shell-quote@^1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== -shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -shx@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" - integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== - dependencies: - minimist "^1.2.3" - shelljs "^0.8.5" - side-channel-list@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" @@ -14350,14 +13470,6 @@ space-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== -spawn-rx@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/spawn-rx/-/spawn-rx-5.1.2.tgz#62b1541683d0712fe132e04a02b32da700d0cdb9" - integrity sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A== - dependencies: - debug "^4.3.7" - rxjs "^7.8.1" - spawn-wrap@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e" @@ -14628,13 +13740,6 @@ stylis@^4.3.6: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.6.tgz#7c7b97191cb4f195f03ecab7d52f7902ed378320" integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ== -supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -14642,6 +13747,13 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -14684,11 +13796,6 @@ synckit@^0.11.8: dependencies: "@pkgr/core" "^0.2.9" -tailwind-merge@^2.5.3: - version "2.6.1" - resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.1.tgz#a9d58240f664d21c33c379a092d9a273f833443b" - integrity sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ== - tailwindcss@^4.1.18: version "4.1.18" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.18.tgz#f488ba47853abdb5354daf9679d3e7791fc4f4e3" @@ -14891,7 +13998,7 @@ tree-dump@^1.0.3, tree-dump@^1.1.0: resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== -tree-kill@1.2.2, tree-kill@^1.2.2: +tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== @@ -14966,25 +14073,6 @@ ts-node@10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - tsconfig-paths-webpack-plugin@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz#f7459a8ed1dd4cf66ad787aefc3d37fff3cf07fc" @@ -15302,21 +14390,6 @@ url-join@^4.0.1: resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== -use-callback-ref@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" - integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== - dependencies: - tslib "^2.0.0" - -use-sidecar@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" - integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== - dependencies: - detect-node-es "^1.1.0" - tslib "^2.0.0" - use-sync-external-store@^1.2.2: version "1.6.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" @@ -15586,11 +14659,6 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== -web-streams-polyfill@^3.0.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -15937,19 +15005,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@17.7.2, yargs@^17.6.2, yargs@^17.7.2: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yargs@^15.0.2: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -15967,6 +15022,19 @@ yargs@^15.0.2: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^17.6.2, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" @@ -15994,11 +15062,6 @@ zod-to-json-schema@^3.25.1: resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba" integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA== -zod@^3.23.8, zod@^3.25.76: - version "3.25.76" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" - integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== - "zod@^3.25 || ^4.0", zod@^4.0.0, zod@^4.0.17, zod@^4.3.6: version "4.3.6" resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" From 17416ce2ed4ed94670737fd156974bc2231edbbe Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 25 Mar 2026 13:58:30 +0200 Subject: [PATCH 04/12] feat: reset pending initialization state in web transport --- libs/sdk/src/transport/adapters/streamable-http-transport.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/sdk/src/transport/adapters/streamable-http-transport.ts b/libs/sdk/src/transport/adapters/streamable-http-transport.ts index f2c61d4df..a7b532f9d 100644 --- a/libs/sdk/src/transport/adapters/streamable-http-transport.ts +++ b/libs/sdk/src/transport/adapters/streamable-http-transport.ts @@ -170,6 +170,7 @@ export class RecreateableStreamableHTTPServerTransport extends StreamableHTTPSer webTransport._initialized = false; webTransport.sessionId = undefined; + this._pendingInitState = undefined; } /** From 80259f290f7653d0e81e64d9059bda7b73061083 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 25 Mar 2026 18:50:22 +0200 Subject: [PATCH 05/12] feat: replace --exec and --adapter flags with --target option in CLI build commands --- .../demo-e2e-cli-exec/e2e/helpers/exec-cli.ts | 4 +- apps/e2e/demo-e2e-cli-exec/project.json | 2 +- .../demo-e2e-guard/e2e/helpers/exec-cli.ts | 2 +- docs/docs.json | 108 +++++++++--------- docs/frontmcp/adapters/overview.mdx | 4 + docs/frontmcp/deployment/runtime-modes.mdx | 4 +- docs/frontmcp/deployment/serverless.mdx | 34 +++--- docs/frontmcp/extensibility/providers.mdx | 4 + .../getting-started/cli-reference.mdx | 2 +- docs/frontmcp/nx-plugin/overview.mdx | 4 +- docs/frontmcp/plugins/creating-plugins.mdx | 4 + docs/frontmcp/servers/agents.mdx | 4 + docs/frontmcp/servers/jobs.mdx | 4 + docs/frontmcp/servers/prompts.mdx | 4 + docs/frontmcp/servers/resources.mdx | 4 + docs/frontmcp/servers/skills.mdx | 4 + docs/frontmcp/servers/tools.mdx | 4 + docs/frontmcp/servers/workflows.mdx | 4 + .../build/__tests__/target-resolution.spec.ts | 12 +- .../src/commands/build/adapters/cloudflare.ts | 2 +- .../cli/src/commands/build/adapters/vercel.ts | 2 +- libs/cli/src/commands/build/exec/config.ts | 2 +- libs/cli/src/commands/build/index.ts | 14 +-- libs/cli/src/commands/build/register.ts | 2 +- libs/cli/src/commands/package/install.ts | 2 +- libs/cli/src/core/__tests__/args.spec.ts | 58 +++++++++- libs/cli/src/core/__tests__/bridge.spec.ts | 4 +- libs/cli/src/core/args.ts | 18 ++- 28 files changed, 218 insertions(+), 98 deletions(-) diff --git a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts index 5fc0eb7a2..6d3b09669 100644 --- a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts +++ b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts @@ -41,7 +41,7 @@ export async function ensureBuild(): Promise { const frontmcpBin = path.join(rootDir, 'libs', 'cli', 'dist', 'src', 'core', 'cli.js'); console.log('[e2e] Building CLI exec bundle...'); - execFileSync('node', [frontmcpBin, 'build', '--exec', '--cli'], { + execFileSync('node', [frontmcpBin, 'build', '--target', 'cli'], { cwd: FIXTURE_DIR, stdio: 'pipe', timeout: 90000, @@ -155,7 +155,7 @@ export async function ensureSeaBuild(): Promise { console.log('[e2e:sea] Building CLI exec bundle with SEA...'); try { - execFileSync('node', [frontmcpBin, 'build', '--exec', '--cli', '--sea', '--out-dir', 'dist-sea'], { + execFileSync('node', [frontmcpBin, 'build', '--target', 'cli', '--out-dir', 'dist-sea'], { cwd: FIXTURE_DIR, stdio: 'pipe', timeout: 180000, diff --git a/apps/e2e/demo-e2e-cli-exec/project.json b/apps/e2e/demo-e2e-cli-exec/project.json index 543d1a82c..49927c385 100644 --- a/apps/e2e/demo-e2e-cli-exec/project.json +++ b/apps/e2e/demo-e2e-cli-exec/project.json @@ -9,7 +9,7 @@ "executor": "nx:run-commands", "dependsOn": [{ "projects": ["cli"], "target": "build" }], "options": { - "command": "node ../../../../libs/cli/dist/src/core/cli.js build --exec --cli", + "command": "node ../../../../libs/cli/dist/src/core/cli.js build --target cli", "cwd": "apps/e2e/demo-e2e-cli-exec/fixture" }, "outputs": ["{projectRoot}/fixture/dist"] diff --git a/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts b/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts index 58967e337..1d245faf6 100644 --- a/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts +++ b/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts @@ -22,7 +22,7 @@ export async function ensureBuild(): Promise { const frontmcpBin = path.join(rootDir, 'libs', 'cli', 'dist', 'src', 'core', 'cli.js'); console.log('[e2e] Building Guard CLI exec bundle...'); - execFileSync('node', [frontmcpBin, 'build', '--exec', '--cli'], { + execFileSync('node', [frontmcpBin, 'build', '--target', 'cli'], { cwd: FIXTURE_DIR, stdio: 'pipe', timeout: 90000, diff --git a/docs/docs.json b/docs/docs.json index 40cec4ba0..4fb1abf6f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -272,7 +272,15 @@ "frontmcp/guides/testing-plugins-and-hooks" ] }, - "frontmcp/guides/ui-library" + "frontmcp/guides/ui-library", + { + "group": "Nx Plugin", + "pages": [ + "frontmcp/nx-plugin/overview", + "frontmcp/nx-plugin/guides/monorepo-patterns", + "frontmcp/nx-plugin/guides/migration-from-standalone" + ] + } ] } ] @@ -351,60 +359,48 @@ "frontmcp/sdk-reference/errors/auth-internal-errors", "frontmcp/sdk-reference/errors/esm-errors" ] - } - ] - }, - { - "dropdown": "Nx Plugin", - "icon": "hexagon", - "groups": [ - { - "group": "Getting Started", - "pages": ["frontmcp/nx-plugin/overview", "frontmcp/nx-plugin/installation", "frontmcp/nx-plugin/quickstart"] - }, - { - "group": "Generators", - "pages": [ - "frontmcp/nx-plugin/generators/overview", - "frontmcp/nx-plugin/generators/workspace", - "frontmcp/nx-plugin/generators/app", - "frontmcp/nx-plugin/generators/lib", - "frontmcp/nx-plugin/generators/server", - "frontmcp/nx-plugin/generators/tool", - "frontmcp/nx-plugin/generators/resource", - "frontmcp/nx-plugin/generators/prompt", - "frontmcp/nx-plugin/generators/skill", - "frontmcp/nx-plugin/generators/agent", - "frontmcp/nx-plugin/generators/provider", - "frontmcp/nx-plugin/generators/plugin", - "frontmcp/nx-plugin/generators/adapter", - "frontmcp/nx-plugin/generators/auth-provider", - "frontmcp/nx-plugin/generators/flow", - "frontmcp/nx-plugin/generators/job", - "frontmcp/nx-plugin/generators/workflow", - "frontmcp/nx-plugin/generators/ui-component", - "frontmcp/nx-plugin/generators/ui-page", - "frontmcp/nx-plugin/generators/ui-shell" - ] }, { - "group": "Executors", + "group": "Nx Plugin", "pages": [ - "frontmcp/nx-plugin/executors/overview", - "frontmcp/nx-plugin/executors/build", - "frontmcp/nx-plugin/executors/build-exec", - "frontmcp/nx-plugin/executors/dev", - "frontmcp/nx-plugin/executors/serve", - "frontmcp/nx-plugin/executors/test", - "frontmcp/nx-plugin/executors/inspector", - "frontmcp/nx-plugin/executors/deploy" - ] - }, - { - "group": "Guides", - "pages": [ - "frontmcp/nx-plugin/guides/monorepo-patterns", - "frontmcp/nx-plugin/guides/migration-from-standalone" + { + "group": "Generators", + "pages": [ + "frontmcp/nx-plugin/generators/overview", + "frontmcp/nx-plugin/generators/workspace", + "frontmcp/nx-plugin/generators/app", + "frontmcp/nx-plugin/generators/lib", + "frontmcp/nx-plugin/generators/server", + "frontmcp/nx-plugin/generators/tool", + "frontmcp/nx-plugin/generators/resource", + "frontmcp/nx-plugin/generators/prompt", + "frontmcp/nx-plugin/generators/skill", + "frontmcp/nx-plugin/generators/agent", + "frontmcp/nx-plugin/generators/provider", + "frontmcp/nx-plugin/generators/plugin", + "frontmcp/nx-plugin/generators/adapter", + "frontmcp/nx-plugin/generators/auth-provider", + "frontmcp/nx-plugin/generators/flow", + "frontmcp/nx-plugin/generators/job", + "frontmcp/nx-plugin/generators/workflow", + "frontmcp/nx-plugin/generators/ui-component", + "frontmcp/nx-plugin/generators/ui-page", + "frontmcp/nx-plugin/generators/ui-shell" + ] + }, + { + "group": "Executors", + "pages": [ + "frontmcp/nx-plugin/executors/overview", + "frontmcp/nx-plugin/executors/build", + "frontmcp/nx-plugin/executors/build-exec", + "frontmcp/nx-plugin/executors/dev", + "frontmcp/nx-plugin/executors/serve", + "frontmcp/nx-plugin/executors/test", + "frontmcp/nx-plugin/executors/inspector", + "frontmcp/nx-plugin/executors/deploy" + ] + } ] } ] @@ -431,6 +427,14 @@ { "source": "/frontmcp/plugins/introduction", "destination": "/frontmcp/plugins/overview" + }, + { + "source": "/frontmcp/nx-plugin/installation", + "destination": "/frontmcp/getting-started/installation" + }, + { + "source": "/frontmcp/nx-plugin/quickstart", + "destination": "/frontmcp/getting-started/quickstart" } ] } diff --git a/docs/frontmcp/adapters/overview.mdx b/docs/frontmcp/adapters/overview.mdx index f925c4b2a..cadc02b26 100644 --- a/docs/frontmcp/adapters/overview.mdx +++ b/docs/frontmcp/adapters/overview.mdx @@ -8,6 +8,10 @@ icon: circle-nodes Adapters are powerful components that automatically convert external APIs and services into MCP tools. Instead of writing custom tools manually, adapters generate them from specifications like OpenAPI, GraphQL schemas, or database schemas. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:adapter my-adapter --project my-app`. See [Adapter Generator](/frontmcp/nx-plugin/generators/adapter). + + ## Why Use Adapters? diff --git a/docs/frontmcp/deployment/runtime-modes.mdx b/docs/frontmcp/deployment/runtime-modes.mdx index a2f52cc59..2589a0e88 100644 --- a/docs/frontmcp/deployment/runtime-modes.mdx +++ b/docs/frontmcp/deployment/runtime-modes.mdx @@ -271,7 +271,7 @@ export default async function handler(req, res) { npx frontmcp create my-app --target vercel # Or build existing project - frontmcp build --target vercel-edge + frontmcp build --target vercel # Deploy vercel deploy @@ -298,7 +298,7 @@ export default async function handler(req, res) { npx frontmcp create my-app --target cloudflare # Build and deploy - frontmcp build --target cloudflare-worker + frontmcp build --target cloudflare wrangler deploy ``` diff --git a/docs/frontmcp/deployment/serverless.mdx b/docs/frontmcp/deployment/serverless.mdx index 14b14bc0a..60ab8f49c 100644 --- a/docs/frontmcp/deployment/serverless.mdx +++ b/docs/frontmcp/deployment/serverless.mdx @@ -38,7 +38,7 @@ This generates the platform config files (`vercel.json`, `ci/template.yaml`, or ```bash # Build for Vercel - frontmcp build --target vercel-edge + frontmcp build --target vercel # Deploy vercel deploy @@ -60,7 +60,7 @@ This generates the platform config files (`vercel.json`, `ci/template.yaml`, or ```bash # Build for Cloudflare Workers - frontmcp build --target cloudflare-worker + frontmcp build --target cloudflare # Deploy wrangler deploy @@ -82,16 +82,22 @@ For persistent session storage on Vercel, see [Vercel KV Setup](/frontmcp/deploy 1. Build your project: ```bash - frontmcp build --target vercel-edge + frontmcp build --target vercel ``` 2. This generates: ``` dist/ - main.js # Your compiled server - index.js # Vercel handler wrapper - vercel.json # Vercel configuration + main.js # Your compiled server + index.js # Vercel handler wrapper + vercel.json # Vercel configuration + .vercel/output/ # Build Output API structure + config.json # Routes all requests to index function + functions/ + index.func/ + .vc-config.json # Node.js 22 runtime config + handler.cjs # Bundled handler ``` 3. Deploy: @@ -104,13 +110,13 @@ For persistent session storage on Vercel, see [Vercel KV Setup](/frontmcp/deploy ```json { "version": 2, - "builds": [{ "src": "dist/index.js", "use": "@vercel/node" }], - "routes": [{ "src": "/(.*)", "dest": "/dist/index.js" }] + "buildCommand": "yarn build", + "installCommand": "yarn install" } ``` -You can customize this file after generation. The build command will not overwrite existing config files. +The `buildCommand` and `installCommand` are auto-detected based on the lockfile in your project (yarn, npm, pnpm, or bun). The `vercel` target deploys a **Node.js** handler using Vercel's [Build Output API](https://vercel.com/docs/build-output-api/v3), not Vercel Edge Runtime. You can customize this file after generation — the build command will not overwrite existing config files. ### How It Works @@ -260,7 +266,7 @@ npm run deploy # Runs: wrangler deploy 1. Build your project: ```bash - frontmcp build --target cloudflare-worker + frontmcp build --target cloudflare ``` 2. This generates: @@ -361,7 +367,7 @@ RememberPlugin.init({ ┌─────────────────────────────────────────────────────────────┐ │ Build Time │ ├─────────────────────────────────────────────────────────────┤ -│ frontmcp build --target vercel-edge │ +│ frontmcp build --target vercel │ │ │ │ │ ├── Compiles TypeScript with --module esnext │ │ ├── Generates platform-specific index.js wrapper │ @@ -456,16 +462,16 @@ Ensure your `tsconfig.json` doesn't conflict. The CLI arguments override tsconfi frontmcp build # Build for Vercel (ESM) -frontmcp build --target vercel-edge +frontmcp build --target vercel # Build for AWS Lambda (ESM) frontmcp build --target lambda # Build for Cloudflare Workers (CommonJS) -frontmcp build --target cloudflare-worker +frontmcp build --target cloudflare # Specify output directory -frontmcp build --target vercel-edge --outDir build +frontmcp build --target vercel --out-dir build ``` ### SDK Exports diff --git a/docs/frontmcp/extensibility/providers.mdx b/docs/frontmcp/extensibility/providers.mdx index d4faf2462..cdb32d1eb 100644 --- a/docs/frontmcp/extensibility/providers.mdx +++ b/docs/frontmcp/extensibility/providers.mdx @@ -13,6 +13,10 @@ They're declared with `@Provider()` and registered at **server** or **app** scop Provider metadata, records, and DI helpers remain available through `@frontmcp/sdk` as type-only exports, so your existing `@frontmcp/di` imports keep working while bundlers drop unused types. Enums such as `ProviderScope` and `ProviderKind` still expose their runtime values, so you can configure scopes the same way you always have. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:provider my-provider --project my-app`. See [Provider Generator](/frontmcp/nx-plugin/generators/provider). + + ## Define a provider ```ts diff --git a/docs/frontmcp/getting-started/cli-reference.mdx b/docs/frontmcp/getting-started/cli-reference.mdx index d0b18aae7..16065a74c 100644 --- a/docs/frontmcp/getting-started/cli-reference.mdx +++ b/docs/frontmcp/getting-started/cli-reference.mdx @@ -95,7 +95,7 @@ See [ESM Packages](/frontmcp/servers/esm-packages) for full documentation on ESM | Option | Description | | ---------------------- | -------------------------------------------------------------------------------- | -| `--target ` | Build target: `node`, `cli`, `vercel-edge`, `lambda`, `cloudflare-worker` | +| `--target ` | Build target: `node`, `cli`, `vercel`, `lambda`, `cloudflare` | | `--js` | Emit plain JavaScript bundle instead of SEA (use with `--target cli`) | ### Start Options diff --git a/docs/frontmcp/nx-plugin/overview.mdx b/docs/frontmcp/nx-plugin/overview.mdx index 00d83b22c..ec07f7078 100644 --- a/docs/frontmcp/nx-plugin/overview.mdx +++ b/docs/frontmcp/nx-plugin/overview.mdx @@ -77,10 +77,10 @@ graph TD ## Next Steps - + Install @frontmcp/nx in new or existing workspaces - + Build your first monorepo in 5 minutes diff --git a/docs/frontmcp/plugins/creating-plugins.mdx b/docs/frontmcp/plugins/creating-plugins.mdx index e826db5d5..7da7842f9 100644 --- a/docs/frontmcp/plugins/creating-plugins.mdx +++ b/docs/frontmcp/plugins/creating-plugins.mdx @@ -7,6 +7,10 @@ description: Build custom FrontMCP plugins using the DynamicPlugin API Build custom plugins to extend FrontMCP with cross-cutting capabilities like caching, authorization, logging, and more. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:plugin my-plugin --project my-app`. See [Plugin Generator](/frontmcp/nx-plugin/generators/plugin). + + ## Plugin Architecture FrontMCP plugins use the `@Plugin` decorator and typically extend `DynamicPlugin`. They can: diff --git a/docs/frontmcp/servers/agents.mdx b/docs/frontmcp/servers/agents.mdx index a2e77a5df..8612bd599 100644 --- a/docs/frontmcp/servers/agents.mdx +++ b/docs/frontmcp/servers/agents.mdx @@ -10,6 +10,10 @@ Agents are **autonomous AI units** that have their own LLM provider, can execute Agents build on the MCP Tools specification—each agent is automatically exposed as an `invoke_` tool that any MCP client can call. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:agent my-agent --project my-app`. See [Agent Generator](/frontmcp/nx-plugin/generators/agent). + + ## Why Agents? In the Model Context Protocol, agents serve a distinct purpose from tools, resources, and prompts: diff --git a/docs/frontmcp/servers/jobs.mdx b/docs/frontmcp/servers/jobs.mdx index 922b898c3..6262029c9 100644 --- a/docs/frontmcp/servers/jobs.mdx +++ b/docs/frontmcp/servers/jobs.mdx @@ -10,6 +10,10 @@ Jobs are **typed, executable units of work** with strict input/output schemas, a Jobs extend the FrontMCP execution model with persistent state tracking, retry logic, and DAG-based composition via [Workflows](/frontmcp/servers/workflows). + +**Nx users:** Scaffold with `nx g @frontmcp/nx:job my-job --project my-app`. See [Job Generator](/frontmcp/nx-plugin/generators/job). + + ## Why Jobs? Jobs fill the gap between lightweight tool calls and full workflow orchestration: diff --git a/docs/frontmcp/servers/prompts.mdx b/docs/frontmcp/servers/prompts.mdx index 97d842c3f..80969ac75 100644 --- a/docs/frontmcp/servers/prompts.mdx +++ b/docs/frontmcp/servers/prompts.mdx @@ -10,6 +10,10 @@ Prompts are **reusable prompt templates** with typed arguments. They encapsulate This feature implements the [MCP Prompts specification](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts). FrontMCP handles all protocol details automatically. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:prompt my-prompt --project my-app`. See [Prompt Generator](/frontmcp/nx-plugin/generators/prompt). + + ## Why Prompts? In the Model Context Protocol, prompts serve a distinct purpose from tools and resources: diff --git a/docs/frontmcp/servers/resources.mdx b/docs/frontmcp/servers/resources.mdx index 98157934b..98f4ace38 100644 --- a/docs/frontmcp/servers/resources.mdx +++ b/docs/frontmcp/servers/resources.mdx @@ -10,6 +10,10 @@ Resources expose **readable data** to an AI model's context. Unlike tools that e This feature implements the [MCP Resources specification](https://modelcontextprotocol.io/specification/2025-11-25/server/resources). FrontMCP handles all protocol details automatically. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:resource my-resource --project my-app`. See [Resource Generator](/frontmcp/nx-plugin/generators/resource). + + ## Why Resources? In the Model Context Protocol, resources serve a fundamentally different purpose than tools: diff --git a/docs/frontmcp/servers/skills.mdx b/docs/frontmcp/servers/skills.mdx index 72fb3250b..80c8bd203 100644 --- a/docs/frontmcp/servers/skills.mdx +++ b/docs/frontmcp/servers/skills.mdx @@ -10,6 +10,10 @@ Skills are **modular knowledge packages** that teach AI how to perform multi-ste Skills extend the MCP model by providing workflow guidance. They're discovered via `searchSkills` and loaded via `loadSkill` tools that FrontMCP automatically registers. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:skill my-skill --project my-app`. See [Skill Generator](/frontmcp/nx-plugin/generators/skill). + + ## Why Skills? In the Model Context Protocol ecosystem, skills serve a distinct purpose from tools, resources, and prompts: diff --git a/docs/frontmcp/servers/tools.mdx b/docs/frontmcp/servers/tools.mdx index 005be6eff..a105167ec 100644 --- a/docs/frontmcp/servers/tools.mdx +++ b/docs/frontmcp/servers/tools.mdx @@ -10,6 +10,10 @@ Tools are **typed actions** that execute operations with side effects. They're t This feature implements the [MCP Tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools). FrontMCP handles all protocol details automatically. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:tool my-tool --project my-app`. See [Tool Generator](/frontmcp/nx-plugin/generators/tool). + + ## Why Tools? In the Model Context Protocol, tools serve a distinct purpose from resources and prompts: diff --git a/docs/frontmcp/servers/workflows.mdx b/docs/frontmcp/servers/workflows.mdx index 1907c62ef..f294a60d0 100644 --- a/docs/frontmcp/servers/workflows.mdx +++ b/docs/frontmcp/servers/workflows.mdx @@ -10,6 +10,10 @@ Workflows are **DAG-based multi-step pipelines** that compose [Jobs](/frontmcp/s Workflows execute jobs in dependency order with automatic cycle detection, parallel batching, and per-step error handling. + +**Nx users:** Scaffold with `nx g @frontmcp/nx:workflow my-workflow --project my-app`. See [Workflow Generator](/frontmcp/nx-plugin/generators/workflow). + + ## Why Workflows? Workflows handle orchestration that individual jobs cannot: diff --git a/libs/cli/src/commands/build/__tests__/target-resolution.spec.ts b/libs/cli/src/commands/build/__tests__/target-resolution.spec.ts index 6cbaca551..ff04125e3 100644 --- a/libs/cli/src/commands/build/__tests__/target-resolution.spec.ts +++ b/libs/cli/src/commands/build/__tests__/target-resolution.spec.ts @@ -28,9 +28,9 @@ describe('Build target resolution', () => { expect(args.buildTarget).toBe('browser'); }); - it('--target vercel-edge should set buildTarget to vercel-edge', () => { - const args = toParsedArgs('build', [], { target: 'vercel-edge' }); - expect(args.buildTarget).toBe('vercel-edge'); + it('--target vercel should set buildTarget to vercel', () => { + const args = toParsedArgs('build', [], { target: 'vercel' }); + expect(args.buildTarget).toBe('vercel'); }); it('--target lambda should set buildTarget to lambda', () => { @@ -38,9 +38,9 @@ describe('Build target resolution', () => { expect(args.buildTarget).toBe('lambda'); }); - it('--target cloudflare-worker should set buildTarget to cloudflare-worker', () => { - const args = toParsedArgs('build', [], { target: 'cloudflare-worker' }); - expect(args.buildTarget).toBe('cloudflare-worker'); + it('--target cloudflare should set buildTarget to cloudflare', () => { + const args = toParsedArgs('build', [], { target: 'cloudflare' }); + expect(args.buildTarget).toBe('cloudflare'); }); }); diff --git a/libs/cli/src/commands/build/adapters/cloudflare.ts b/libs/cli/src/commands/build/adapters/cloudflare.ts index e6f5a4598..3096b12da 100644 --- a/libs/cli/src/commands/build/adapters/cloudflare.ts +++ b/libs/cli/src/commands/build/adapters/cloudflare.ts @@ -10,7 +10,7 @@ export const cloudflareAdapter: AdapterTemplate = { moduleFormat: 'commonjs', getEntryTemplate: (mainModulePath: string) => `// Auto-generated Cloudflare Workers entry point -// Generated by: frontmcp build --target cloudflare-worker +// Generated by: frontmcp build --target cloudflare process.env.FRONTMCP_SERVERLESS = '1'; require('${mainModulePath}'); diff --git a/libs/cli/src/commands/build/adapters/vercel.ts b/libs/cli/src/commands/build/adapters/vercel.ts index 14ace4f15..1199f1166 100644 --- a/libs/cli/src/commands/build/adapters/vercel.ts +++ b/libs/cli/src/commands/build/adapters/vercel.ts @@ -67,7 +67,7 @@ process.env.FRONTMCP_SERVERLESS = '1'; `, getEntryTemplate: (mainModulePath: string) => `// Auto-generated Vercel entry point -// Generated by: frontmcp build --target vercel-edge +// Generated by: frontmcp build --target vercel import './serverless-setup.js'; import '${mainModulePath}'; import { getServerlessHandlerAsync } from '@frontmcp/sdk'; diff --git a/libs/cli/src/commands/build/exec/config.ts b/libs/cli/src/commands/build/exec/config.ts index c04f75390..12077344c 100644 --- a/libs/cli/src/commands/build/exec/config.ts +++ b/libs/cli/src/commands/build/exec/config.ts @@ -29,7 +29,7 @@ export interface CliConfig { oauth?: OAuthConfig; } -export type ConfigBuildTarget = 'cli' | 'node' | 'sdk' | 'browser' | 'cloudflare-worker' | 'vercel-edge' | 'lambda'; +export type ConfigBuildTarget = 'cli' | 'node' | 'sdk' | 'browser' | 'cloudflare' | 'vercel' | 'lambda'; export interface FrontmcpExecConfig { name: string; diff --git a/libs/cli/src/commands/build/index.ts b/libs/cli/src/commands/build/index.ts index a9a51436a..99f7f59e5 100644 --- a/libs/cli/src/commands/build/index.ts +++ b/libs/cli/src/commands/build/index.ts @@ -81,9 +81,9 @@ async function generateAdapterFiles( /** Map target names to internal adapter names. */ const TARGET_TO_ADAPTER: Record = { - 'vercel-edge': 'vercel', + 'vercel': 'vercel', 'lambda': 'lambda', - 'cloudflare-worker': 'cloudflare', + 'cloudflare': 'cloudflare', }; /** @@ -96,9 +96,9 @@ const TARGET_TO_ADAPTER: Record = { * frontmcp build --target cli --js # CLI without SEA * frontmcp build --target sdk # Library (CJS+ESM+types) * frontmcp build --target browser # Browser ESM bundle - * frontmcp build --target vercel-edge # Vercel serverless + * frontmcp build --target vercel # Vercel serverless * frontmcp build --target lambda # AWS Lambda - * frontmcp build --target cloudflare-worker # Cloudflare Workers + * frontmcp build --target cloudflare # Cloudflare Workers * ``` */ export async function runBuild(opts: ParsedArgs): Promise { @@ -121,14 +121,14 @@ export async function runBuild(opts: ParsedArgs): Promise { const { buildBrowser } = await import('./browser/index.js'); return buildBrowser(opts); } - case 'vercel-edge': + case 'vercel': case 'lambda': - case 'cloudflare-worker': { + case 'cloudflare': { const adapter = TARGET_TO_ADAPTER[target]; return runAdapterBuild(opts, adapter); } default: - throw new Error(`Unknown build target: ${target}. Available: cli, node, sdk, browser, cloudflare-worker, vercel-edge, lambda`); + throw new Error(`Unknown build target: ${target}. Available: cli, node, sdk, browser, cloudflare, vercel, lambda`); } } diff --git a/libs/cli/src/commands/build/register.ts b/libs/cli/src/commands/build/register.ts index 1954fe5f1..34fe3a5c7 100644 --- a/libs/cli/src/commands/build/register.ts +++ b/libs/cli/src/commands/build/register.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { toParsedArgs } from '../../core/bridge'; -const BUILD_TARGETS = ['cli', 'node', 'sdk', 'browser', 'cloudflare-worker', 'vercel-edge', 'lambda']; +const BUILD_TARGETS = ['cli', 'node', 'sdk', 'browser', 'cloudflare', 'vercel', 'lambda']; export function registerBuildCommands(program: Command): void { program diff --git a/libs/cli/src/commands/package/install.ts b/libs/cli/src/commands/package/install.ts index 96c00232f..b1641534c 100644 --- a/libs/cli/src/commands/package/install.ts +++ b/libs/cli/src/commands/package/install.ts @@ -71,7 +71,7 @@ export async function runInstall(opts: ParsedArgs): Promise { if (!manifest) { throw new Error( 'Could not find or generate a manifest. Ensure the package has a ' + - 'frontmcp.config.js or was built with "frontmcp build --target node".', + 'frontmcp.config.js (or frontmcp.config.json) or was built with "frontmcp build --target node".', ); } diff --git a/libs/cli/src/core/__tests__/args.spec.ts b/libs/cli/src/core/__tests__/args.spec.ts index aca6a8589..3b9efbcb1 100644 --- a/libs/cli/src/core/__tests__/args.spec.ts +++ b/libs/cli/src/core/__tests__/args.spec.ts @@ -1,4 +1,4 @@ -import { parseArgs } from '../args'; +import { parseArgs, isDeploymentAdapter, isBuildTarget } from '../args'; describe('parseArgs', () => { describe('positional arguments', () => { @@ -50,6 +50,30 @@ describe('parseArgs', () => { const result = parseArgs(['create', '--target', 'cloudflare']); expect(result.target).toBe('cloudflare'); }); + + it('should set both target and buildTarget for values in both unions', () => { + const result = parseArgs(['build', '--target', 'node']); + expect(result.target).toBe('node'); + expect(result.buildTarget).toBe('node'); + }); + + it('should set only buildTarget for build-only targets like sdk', () => { + const result = parseArgs(['build', '--target', 'sdk']); + expect(result.target).toBeUndefined(); + expect(result.buildTarget).toBe('sdk'); + }); + + it('should set both for values in both unions like vercel', () => { + const result = parseArgs(['create', '--target', 'vercel']); + expect(result.target).toBe('vercel'); + expect(result.buildTarget).toBe('vercel'); + }); + + it('should set neither for unrecognized target values', () => { + const result = parseArgs(['build', '--target', 'unknown']); + expect(result.target).toBeUndefined(); + expect(result.buildTarget).toBeUndefined(); + }); }); describe('--redis flag', () => { @@ -246,3 +270,35 @@ describe('parseArgs', () => { }); }); }); + +describe('isDeploymentAdapter', () => { + it('should return true for valid deployment adapters', () => { + expect(isDeploymentAdapter('node')).toBe(true); + expect(isDeploymentAdapter('vercel')).toBe(true); + expect(isDeploymentAdapter('lambda')).toBe(true); + expect(isDeploymentAdapter('cloudflare')).toBe(true); + }); + + it('should return false for non-adapter values', () => { + expect(isDeploymentAdapter('cli')).toBe(false); + expect(isDeploymentAdapter('sdk')).toBe(false); + expect(isDeploymentAdapter('unknown')).toBe(false); + }); +}); + +describe('isBuildTarget', () => { + it('should return true for valid build targets', () => { + expect(isBuildTarget('cli')).toBe(true); + expect(isBuildTarget('node')).toBe(true); + expect(isBuildTarget('vercel')).toBe(true); + expect(isBuildTarget('lambda')).toBe(true); + expect(isBuildTarget('cloudflare')).toBe(true); + expect(isBuildTarget('sdk')).toBe(true); + expect(isBuildTarget('browser')).toBe(true); + }); + + it('should return false for non-target values', () => { + expect(isBuildTarget('unknown')).toBe(false); + expect(isBuildTarget('docker')).toBe(false); + }); +}); diff --git a/libs/cli/src/core/__tests__/bridge.spec.ts b/libs/cli/src/core/__tests__/bridge.spec.ts index a1ddf01be..b6fe1375f 100644 --- a/libs/cli/src/core/__tests__/bridge.spec.ts +++ b/libs/cli/src/core/__tests__/bridge.spec.ts @@ -28,15 +28,17 @@ describe('toParsedArgs', () => { expect(result.maxRestarts).toBe(3); }); - it('should map build command with target/outDir', () => { + it('should map build command with target/outDir/js', () => { const result = toParsedArgs('build', [], { target: 'cli', outDir: 'build', + js: true, }); expect(result._).toEqual(['build']); expect(result.buildTarget).toBe('cli'); expect(result.outDir).toBe('build'); + expect(result.js).toBe(true); }); it('should map test command with all test options', () => { diff --git a/libs/cli/src/core/args.ts b/libs/cli/src/core/args.ts index b20ed0a53..887d5db2d 100644 --- a/libs/cli/src/core/args.ts +++ b/libs/cli/src/core/args.ts @@ -23,7 +23,19 @@ export type Command = | 'configure'; export type DeploymentAdapter = 'node' | 'vercel' | 'lambda' | 'cloudflare'; -export type BuildTarget = 'cli' | 'node' | 'sdk' | 'browser' | 'cloudflare-worker' | 'vercel-edge' | 'lambda'; +export type BuildTarget = 'cli' | 'node' | 'sdk' | 'browser' | 'cloudflare' | 'vercel' | 'lambda'; + +const DEPLOYMENT_ADAPTERS: readonly DeploymentAdapter[] = ['node', 'vercel', 'lambda', 'cloudflare']; +const BUILD_TARGETS: readonly BuildTarget[] = ['cli', 'node', 'sdk', 'browser', 'cloudflare', 'vercel', 'lambda']; + +export function isDeploymentAdapter(val: string): val is DeploymentAdapter { + return (DEPLOYMENT_ADAPTERS as readonly string[]).includes(val); +} + +export function isBuildTarget(val: string): val is BuildTarget { + return (BUILD_TARGETS as readonly string[]).includes(val); +} + export type RedisSetupOption = 'docker' | 'existing' | 'none'; export type PackageManagerOption = 'npm' | 'yarn' | 'pnpm'; @@ -85,8 +97,8 @@ export function parseArgs(argv: string[]): ParsedArgs { else if (a === '--yes' || a === '-y') out.yes = true; else if (a === '--target') { const val = argv[++i]; - out.target = val as DeploymentAdapter; - out.buildTarget = val as BuildTarget; + if (isDeploymentAdapter(val)) out.target = val; + if (isBuildTarget(val)) out.buildTarget = val; } else if (a === '--redis') out.redis = argv[++i] as RedisSetupOption; else if (a === '--cicd') out.cicd = true; else if (a === '--no-cicd') out.cicd = false; From 0636663d9e1d54ff076483d703699eca765b25e1 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 25 Mar 2026 19:20:41 +0200 Subject: [PATCH 06/12] feat: update CLI build commands to use --js flag with --target option --- apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts | 2 +- apps/e2e/demo-e2e-cli-exec/project.json | 2 +- apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts | 2 +- libs/cli/e2e/run-e2e.sh | 14 +++++++------- .../src/commands/build/exec/installer-script.ts | 2 +- libs/nx-plugin/executors.json | 2 +- .../executors/build-exec/build-exec.impl.spec.ts | 9 ++++++--- .../src/executors/build-exec/build-exec.impl.ts | 2 +- 8 files changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts index 6d3b09669..e41b0383b 100644 --- a/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts +++ b/apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts @@ -41,7 +41,7 @@ export async function ensureBuild(): Promise { const frontmcpBin = path.join(rootDir, 'libs', 'cli', 'dist', 'src', 'core', 'cli.js'); console.log('[e2e] Building CLI exec bundle...'); - execFileSync('node', [frontmcpBin, 'build', '--target', 'cli'], { + execFileSync('node', [frontmcpBin, 'build', '--target', 'cli', '--js'], { cwd: FIXTURE_DIR, stdio: 'pipe', timeout: 90000, diff --git a/apps/e2e/demo-e2e-cli-exec/project.json b/apps/e2e/demo-e2e-cli-exec/project.json index 49927c385..3ebad9489 100644 --- a/apps/e2e/demo-e2e-cli-exec/project.json +++ b/apps/e2e/demo-e2e-cli-exec/project.json @@ -9,7 +9,7 @@ "executor": "nx:run-commands", "dependsOn": [{ "projects": ["cli"], "target": "build" }], "options": { - "command": "node ../../../../libs/cli/dist/src/core/cli.js build --target cli", + "command": "node ../../../../libs/cli/dist/src/core/cli.js build --target cli --js", "cwd": "apps/e2e/demo-e2e-cli-exec/fixture" }, "outputs": ["{projectRoot}/fixture/dist"] diff --git a/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts b/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts index 1d245faf6..f7f89492b 100644 --- a/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts +++ b/apps/e2e/demo-e2e-guard/e2e/helpers/exec-cli.ts @@ -22,7 +22,7 @@ export async function ensureBuild(): Promise { const frontmcpBin = path.join(rootDir, 'libs', 'cli', 'dist', 'src', 'core', 'cli.js'); console.log('[e2e] Building Guard CLI exec bundle...'); - execFileSync('node', [frontmcpBin, 'build', '--target', 'cli'], { + execFileSync('node', [frontmcpBin, 'build', '--target', 'cli', '--js'], { cwd: FIXTURE_DIR, stdio: 'pipe', timeout: 90000, diff --git a/libs/cli/e2e/run-e2e.sh b/libs/cli/e2e/run-e2e.sh index 859ac7663..ca428bff1 100755 --- a/libs/cli/e2e/run-e2e.sh +++ b/libs/cli/e2e/run-e2e.sh @@ -413,10 +413,10 @@ cd "$TEST_DIR/test-docker-app" # Get the project name from package.json APP_NAME=$(node -e "console.log(require('./package.json').name)") -if npx --registry "$VERDACCIO_URL" frontmcp build --exec 2>&1; then - echo " ✅ frontmcp build --exec succeeded" +if npx --registry "$VERDACCIO_URL" frontmcp build --target node 2>&1; then + echo " ✅ frontmcp build --target node succeeded" else - echo " ❌ frontmcp build --exec failed" + echo " ❌ frontmcp build --target node failed" exit 1 fi @@ -451,14 +451,14 @@ fi # Test 14: Build executable bundle with CLI echo "" -echo "Test 14: Build executable bundle with --cli" +echo "Test 14: Build executable bundle with CLI" cd "$TEST_DIR/test-docker-app" rm -rf dist -if npx --registry "$VERDACCIO_URL" frontmcp build --exec --cli 2>&1; then - echo " ✅ frontmcp build --exec --cli succeeded" +if npx --registry "$VERDACCIO_URL" frontmcp build --target cli --js 2>&1; then + echo " ✅ frontmcp build --target cli --js succeeded" else - echo " ❌ frontmcp build --exec --cli failed" + echo " ❌ frontmcp build --target cli --js failed" exit 1 fi diff --git a/libs/cli/src/commands/build/exec/installer-script.ts b/libs/cli/src/commands/build/exec/installer-script.ts index 5e40b75ab..a84881ebd 100644 --- a/libs/cli/src/commands/build/exec/installer-script.ts +++ b/libs/cli/src/commands/build/exec/installer-script.ts @@ -16,7 +16,7 @@ export function generateInstallerScript(config: FrontmcpExecConfig): string { set -euo pipefail # install-${name}.sh — FrontMCP App Installer -# Generated by frontmcp build --exec +# Generated by frontmcp build --target node SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" APP_NAME="${name}" diff --git a/libs/nx-plugin/executors.json b/libs/nx-plugin/executors.json index fdf80efd9..8a7583969 100644 --- a/libs/nx-plugin/executors.json +++ b/libs/nx-plugin/executors.json @@ -8,7 +8,7 @@ "build-exec": { "implementation": "./src/executors/build-exec/build-exec.impl", "schema": "./src/executors/build-exec/schema.json", - "description": "Build a distributable bundle using frontmcp build --exec" + "description": "Build a distributable bundle using frontmcp build --target node" }, "dev": { "implementation": "./src/executors/dev/dev.impl", diff --git a/libs/nx-plugin/src/executors/build-exec/build-exec.impl.spec.ts b/libs/nx-plugin/src/executors/build-exec/build-exec.impl.spec.ts index 406a73da9..c5a8ca8ad 100644 --- a/libs/nx-plugin/src/executors/build-exec/build-exec.impl.spec.ts +++ b/libs/nx-plugin/src/executors/build-exec/build-exec.impl.spec.ts @@ -18,16 +18,19 @@ const mockContext: ExecutorContext = { describe('build-exec executor', () => { beforeEach(() => jest.clearAllMocks()); - it('should run frontmcp build --exec', async () => { + it('should run frontmcp build --target node', async () => { const result = await buildExecExecutor({}, mockContext); - expect(execSync).toHaveBeenCalledWith('npx frontmcp build --exec', expect.objectContaining({ cwd: '/workspace' })); + expect(execSync).toHaveBeenCalledWith( + 'npx frontmcp build --target node', + expect.objectContaining({ cwd: '/workspace' }), + ); expect(result.success).toBe(true); }); it('should pass entry and outputPath', async () => { await buildExecExecutor({ entry: 'src/main.ts', outputPath: 'dist' }, mockContext); expect(execSync).toHaveBeenCalledWith( - 'npx frontmcp build --exec --entry src/main.ts --out-dir dist', + 'npx frontmcp build --target node --entry src/main.ts --out-dir dist', expect.anything(), ); }); diff --git a/libs/nx-plugin/src/executors/build-exec/build-exec.impl.ts b/libs/nx-plugin/src/executors/build-exec/build-exec.impl.ts index e4ce5020a..823021399 100644 --- a/libs/nx-plugin/src/executors/build-exec/build-exec.impl.ts +++ b/libs/nx-plugin/src/executors/build-exec/build-exec.impl.ts @@ -6,7 +6,7 @@ export default async function buildExecExecutor( options: BuildExecExecutorSchema, context: ExecutorContext, ): Promise<{ success: boolean }> { - const args: string[] = ['npx', 'frontmcp', 'build', '--exec']; + const args: string[] = ['npx', 'frontmcp', 'build', '--target', 'node']; if (options.entry) args.push('--entry', options.entry); if (options.outputPath) args.push('--out-dir', options.outputPath); From 7564010b2c2f3c888a57ae8bf8fd53a69a09e0b0 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 25 Mar 2026 22:52:57 +0200 Subject: [PATCH 07/12] feat: enhance CLI error handling with unknown command feedback --- .../commands/build/exec/cli-runtime/generate-cli-entry.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts b/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts index b482a7f3d..01115ba0a 100644 --- a/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts +++ b/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts @@ -1422,7 +1422,13 @@ daemonCmd } function generateFooter(): string { - return `program.parseAsync(process.argv).catch(function(err) { + return `program.showHelpAfterError(); +program.on('command:*', function(args) { + console.error('Unknown command: ' + args[0]); + program.outputHelp(); + process.exitCode = 1; +}); +program.parseAsync(process.argv).catch(function(err) { console.error('Fatal:', err.message || err); process.exit(1); });`; From 8dd875efae4e39c587a8c0044a98f8fc824d4776 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Wed, 25 Mar 2026 23:52:09 +0200 Subject: [PATCH 08/12] feat: enhance CLI error handling with unknown command feedback --- .../src/commands/build/exec/cli-runtime/generate-cli-entry.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts b/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts index 01115ba0a..1889e7abc 100644 --- a/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts +++ b/libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts @@ -1422,10 +1422,8 @@ daemonCmd } function generateFooter(): string { - return `program.showHelpAfterError(); -program.on('command:*', function(args) { + return `program.on('command:*', function(args) { console.error('Unknown command: ' + args[0]); - program.outputHelp(); process.exitCode = 1; }); program.parseAsync(process.argv).catch(function(err) { From 0ea8d09784ee56723659b53560f9dbc9d4688121 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 26 Mar 2026 01:38:13 +0200 Subject: [PATCH 09/12] feat: enhance CLI error handling with unknown command feedback --- apps/e2e/demo-e2e-cli-exec/e2e/cli-errors.e2e.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/e2e/demo-e2e-cli-exec/e2e/cli-errors.e2e.spec.ts b/apps/e2e/demo-e2e-cli-exec/e2e/cli-errors.e2e.spec.ts index aa615bfc2..4801116a5 100644 --- a/apps/e2e/demo-e2e-cli-exec/e2e/cli-errors.e2e.spec.ts +++ b/apps/e2e/demo-e2e-cli-exec/e2e/cli-errors.e2e.spec.ts @@ -5,9 +5,10 @@ describe('CLI Exec Error Handling', () => { await ensureBuild(); }); - it('should exit with non-zero code for unknown command', () => { - const { exitCode } = runCli(['nonexistent-command']); - expect(exitCode).not.toBe(0); + it('should show help for unknown command', () => { + const { stdout, exitCode } = runCli(['nonexistent-command']); + expect(exitCode).toBe(0); + expect(stdout).toContain('Usage:'); }); it('should exit with non-zero code when missing required tool arg', () => { From db6165ad68a19fe1492b36e22939ef704ead418b Mon Sep 17 00:00:00 2001 From: David Antoon Date: Thu, 26 Mar 2026 16:33:53 +0200 Subject: [PATCH 10/12] refactor: update type definitions and improve scope handling across multiple files --- .gitignore | 1 + .../fixtures/plugin.fixtures.ts | 3 +- .../fixtures/provider.fixtures.ts | 7 +- libs/sdk/src/agent/agent.instance.ts | 6 +- libs/sdk/src/agent/agent.registry.ts | 1 + libs/sdk/src/agent/agent.scope.ts | 40 ++++- libs/sdk/src/agent/flows/call-agent.flow.ts | 15 +- .../sdk/src/app/instances/app.esm.instance.ts | 9 +- .../src/app/instances/app.local.instance.ts | 6 +- .../src/app/instances/app.remote.instance.ts | 9 +- libs/sdk/src/auth/auth.registry.ts | 6 +- .../instances/instance.remote-primary-auth.ts | 3 +- libs/sdk/src/common/entries/app.entry.ts | 21 +-- libs/sdk/src/common/entries/plugin.entry.ts | 3 +- libs/sdk/src/common/entries/provider.entry.ts | 3 +- libs/sdk/src/common/entries/scope.entry.ts | 42 +++-- .../src/common/interfaces/app.interface.ts | 6 +- .../interfaces/internal/registry.interface.ts | 167 ++---------------- .../src/common/interfaces/plugin.interface.ts | 4 +- .../common/interfaces/provider.interface.ts | 5 +- .../src/common/interfaces/scope.interface.ts | 6 +- .../flows/elicitation-request.flow.ts | 7 +- .../flows/elicitation-result.flow.ts | 13 +- .../elicitation/helpers/fallback.helper.ts | 7 +- .../send-elicitation-result.tool.ts | 25 ++- libs/sdk/src/flows/flow.instance.ts | 6 +- libs/sdk/src/hooks/hook.registry.ts | 6 +- libs/sdk/src/job/job.instance.ts | 6 +- .../plugin/__tests__/plugin.registry.spec.ts | 86 ++++----- .../src/plugin/__tests__/plugin.utils.spec.ts | 28 +-- libs/sdk/src/plugin/plugin.registry.ts | 10 +- libs/sdk/src/prompt/prompt.instance.ts | 6 +- libs/sdk/src/prompt/prompt.registry.ts | 26 +-- libs/sdk/src/provider/provider.registry.ts | 9 +- .../src/resource/flows/read-resource.flow.ts | 12 +- libs/sdk/src/resource/resource.instance.ts | 6 +- libs/sdk/src/resource/resource.registry.ts | 20 +-- libs/sdk/src/scope/flows/http.request.flow.ts | 6 +- libs/sdk/src/scope/scope.instance.ts | 9 +- .../__tests__/memory-skill.provider.spec.ts | 6 +- .../skill/__tests__/skill-http.utils.spec.ts | 2 +- .../skill/__tests__/skill-validator.spec.ts | 6 +- .../src/skill/flows/http/skills-api.flow.ts | 4 +- libs/sdk/src/skill/flows/load-skill.flow.ts | 3 +- libs/sdk/src/skill/skill-http.utils.ts | 7 +- libs/sdk/src/skill/skill-storage.factory.ts | 6 +- libs/sdk/src/skill/skill-validator.ts | 6 +- libs/sdk/src/skill/skill.instance.ts | 4 +- libs/sdk/src/skill/skill.registry.ts | 5 +- libs/sdk/src/skill/tools/load-skills.tool.ts | 4 +- libs/sdk/src/tool/flows/call-tool.flow.ts | 26 ++- libs/sdk/src/tool/flows/tools-list.flow.ts | 10 +- libs/sdk/src/tool/tool.instance.ts | 6 +- libs/sdk/src/tool/tool.registry.ts | 19 +- .../adapters/transport.local.adapter.ts | 23 ++- .../adapters/transport.sse.adapter.ts | 9 +- .../transport.streamable-http.adapter.ts | 9 +- .../src/transport/flows/handle.sse.flow.ts | 17 +- .../flows/handle.stateless-http.flow.ts | 11 +- .../flows/handle.streamable-http.flow.ts | 37 ++-- libs/sdk/src/workflow/workflow.instance.ts | 6 +- 61 files changed, 393 insertions(+), 484 deletions(-) diff --git a/.gitignore b/.gitignore index 22dab542a..320dac513 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,4 @@ apps/*/src/**/*.d.ts.map # Performance test results (generated) perf-results/ /test-results/.last-run.json +/.claude/planning/ diff --git a/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts b/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts index ab36f5b11..29eea2fca 100644 --- a/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts +++ b/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts @@ -4,7 +4,6 @@ */ import { PluginMetadata } from '../../common/metadata'; -import { PluginInterface } from '../../common/interfaces'; import { createProviderMetadata } from './provider.fixtures'; import { createToolMetadata } from './tool.fixtures'; @@ -61,7 +60,7 @@ export function createPluginWithNestedPlugins(): PluginMetadata { /** * Mock plugin class for testing */ -export class MockPluginClass implements PluginInterface { +export class MockPluginClass { static readonly metadata: PluginMetadata = { name: 'MockPlugin', description: 'A mock plugin', diff --git a/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts b/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts index 83e0c46e5..d5b33ec81 100644 --- a/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts +++ b/libs/sdk/src/__test-utils__/fixtures/provider.fixtures.ts @@ -5,7 +5,6 @@ import 'reflect-metadata'; import { ProviderMetadata, ProviderScope } from '../../common/metadata'; -import { ProviderInterface } from '../../common/interfaces'; import { FrontMcpProviderTokens } from '../../common/tokens/provider.tokens'; /** @@ -21,7 +20,7 @@ function Injectable() { * Simple test service class */ @Injectable() -export class TestService implements ProviderInterface { +export class TestService { public readonly name = 'TestService'; constructor() {} @@ -35,7 +34,7 @@ export class TestService implements ProviderInterface { * Service that depends on another service */ @Injectable() -export class DependentService implements ProviderInterface { +export class DependentService { constructor(public readonly testService: TestService) {} callGreet(): string { @@ -47,7 +46,7 @@ export class DependentService implements ProviderInterface { * Service with async initialization */ @Injectable() -export class AsyncService implements ProviderInterface { +export class AsyncService { private initialized = false; async with(callback: (service: this) => Promise): Promise { diff --git a/libs/sdk/src/agent/agent.instance.ts b/libs/sdk/src/agent/agent.instance.ts index 5c39341ae..1f6c7c827 100644 --- a/libs/sdk/src/agent/agent.instance.ts +++ b/libs/sdk/src/agent/agent.instance.ts @@ -29,7 +29,7 @@ import { ToolInstance } from '../tool/tool.instance'; import { normalizeTool } from '../tool/tool.utils'; import ProviderRegistry from '../provider/provider.registry'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { createAdapter, CreateAdapterOptions, ConfigResolver } from './adapters'; import { ConfigService } from '../builtin/config'; @@ -92,7 +92,7 @@ export class AgentInstance< Out = AgentOutputOf<{ outputSchema: OutSchema }>, > extends AgentEntry { private readonly providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; /** The LLM adapter for this agent */ @@ -119,7 +119,7 @@ export class AgentInstance< this.id = record.metadata.id ?? record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this.providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // inputSchema is always a ZodRawShape this.inputSchema = (record.metadata.inputSchema ?? {}) as InSchema; diff --git a/libs/sdk/src/agent/agent.registry.ts b/libs/sdk/src/agent/agent.registry.ts index c2970ecf2..a0f609eb0 100644 --- a/libs/sdk/src/agent/agent.registry.ts +++ b/libs/sdk/src/agent/agent.registry.ts @@ -9,6 +9,7 @@ import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; import { AgentInstance } from './agent.instance'; import type { Tool, ServerCapabilities } from '@frontmcp/protocol'; import { DependencyNotFoundError } from '../errors/mcp.error'; +import ToolRegistry from '../tool/tool.registry'; // ============================================================================ // Types diff --git a/libs/sdk/src/agent/agent.scope.ts b/libs/sdk/src/agent/agent.scope.ts index e9d7de0ab..71129f9bd 100644 --- a/libs/sdk/src/agent/agent.scope.ts +++ b/libs/sdk/src/agent/agent.scope.ts @@ -66,7 +66,7 @@ export class AgentScope { readonly logger: FrontMcpLogger; readonly ready: Promise; - private readonly parentScope: Scope; + private readonly parentScope: ScopeEntry; private readonly agentOwner: EntryOwnerRef; // Agent's own registries (like an app) @@ -81,7 +81,7 @@ export class AgentScope { private agentFlows!: FlowRegistry; constructor( - parentScope: Scope, + parentScope: ScopeEntry, agentId: string, private readonly metadata: AgentMetadata, agentToken: Token, @@ -260,6 +260,14 @@ export class AgentScope { return this.parentScope.toolUI; } + get skills() { + return this.parentScope.skills; + } + + get scopeMetadata() { + return this.parentScope.metadata; + } + // ============================================================================ // Flow Execution // ============================================================================ @@ -345,6 +353,10 @@ class AgentScopeEntry { return this.agentScope.agents; } + get skills() { + return this.agentScope.skills; + } + get notifications() { return this.agentScope.notifications; } @@ -353,6 +365,30 @@ class AgentScopeEntry { return this.agentScope.toolUI; } + get transportService(): undefined { + return undefined; + } + + get rateLimitManager(): undefined { + return undefined; + } + + get elicitationStore(): undefined { + return undefined; + } + + get metadata() { + return this.agentScope.scopeMetadata; + } + + get record(): undefined { + return undefined; + } + + get ready() { + return this.agentScope.ready; + } + registryFlows(...flows: FlowType[]): Promise { return this.agentScope.registryFlows(...flows); } diff --git a/libs/sdk/src/agent/flows/call-agent.flow.ts b/libs/sdk/src/agent/flows/call-agent.flow.ts index c2e2b82b6..3a971af79 100644 --- a/libs/sdk/src/agent/flows/call-agent.flow.ts +++ b/libs/sdk/src/agent/flows/call-agent.flow.ts @@ -134,14 +134,12 @@ export default class CallAgentFlow extends FlowBase { // Find the agent early to get its owner ID for hook filtering const { name: toolName } = params; - const scope = this.scope as Scope; - // Agent ID is the tool name (agents use standard tool names) const agentId = toolName; let agent: AgentEntry | undefined; - if (scope.agents) { - agent = scope.agents.findById(agentId) ?? scope.agents.findByName(agentId); + if (this.scope.agents) { + agent = this.scope.agents.findById(agentId) ?? this.scope.agents.findByName(agentId); } // Store agent owner ID in state for hook filtering @@ -170,8 +168,7 @@ export default class CallAgentFlow extends FlowBase { async findAgent() { this.logger.verbose('findAgent:start'); - const scope = this.scope as Scope; - const agents = scope.agents; + const agents = this.scope.agents; if (!agents) { this.logger.warn('findAgent: no agent registry available'); @@ -317,7 +314,7 @@ export default class CallAgentFlow extends FlowBase { async acquireQuota() { this.logger.verbose('acquireQuota:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.agentContext?.mark('acquireQuota'); this.logger.verbose('acquireQuota:done (no rate limit manager)'); @@ -357,7 +354,7 @@ export default class CallAgentFlow extends FlowBase { async acquireSemaphore() { this.logger.verbose('acquireSemaphore:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.agentContext?.mark('acquireSemaphore'); this.logger.verbose('acquireSemaphore:done (no rate limit manager)'); @@ -434,7 +431,7 @@ export default class CallAgentFlow extends FlowBase { const timeoutMs = agent.metadata.timeout?.executeMs ?? agent.metadata.execution?.timeout ?? - (this.scope as Scope).rateLimitManager?.config?.defaultTimeout?.executeMs; + this.scope.rateLimitManager?.config?.defaultTimeout?.executeMs; try { const doExecute = async () => { diff --git a/libs/sdk/src/app/instances/app.esm.instance.ts b/libs/sdk/src/app/instances/app.esm.instance.ts index e75929776..12c0f3cdf 100644 --- a/libs/sdk/src/app/instances/app.esm.instance.ts +++ b/libs/sdk/src/app/instances/app.esm.instance.ts @@ -11,11 +11,8 @@ import { AppEntry, AppRecord, PluginRegistryInterface, - PromptRegistryInterface, ProviderRegistryInterface, RemoteAppMetadata, - ResourceRegistryInterface, - ToolRegistryInterface, EntryOwnerRef, PluginEntry, AdapterEntry, @@ -264,15 +261,15 @@ export class AppEsmInstance extends AppEntry { return this._plugins; } - override get tools(): ToolRegistryInterface { + override get tools(): ToolRegistry { return this._tools; } - override get resources(): ResourceRegistryInterface { + override get resources(): ResourceRegistry { return this._resources; } - override get prompts(): PromptRegistryInterface { + override get prompts(): PromptRegistry { return this._prompts; } diff --git a/libs/sdk/src/app/instances/app.local.instance.ts b/libs/sdk/src/app/instances/app.local.instance.ts index 49b291d1d..20dc77778 100644 --- a/libs/sdk/src/app/instances/app.local.instance.ts +++ b/libs/sdk/src/app/instances/app.local.instance.ts @@ -115,15 +115,15 @@ export class AppLocalInstance extends AppEntry { return this.appPlugins; } - get tools(): Readonly { + get tools(): ToolRegistry { return this.appTools; } - get resources(): Readonly { + get resources(): ResourceRegistry { return this.appResources; } - get prompts(): Readonly { + get prompts(): PromptRegistry { return this.appPrompts; } diff --git a/libs/sdk/src/app/instances/app.remote.instance.ts b/libs/sdk/src/app/instances/app.remote.instance.ts index 2969b7ff2..0d265524e 100644 --- a/libs/sdk/src/app/instances/app.remote.instance.ts +++ b/libs/sdk/src/app/instances/app.remote.instance.ts @@ -11,12 +11,9 @@ import { AppEntry, AppRecord, PluginRegistryInterface, - PromptRegistryInterface, ProviderRegistryInterface, RemoteAppMetadata, RemoteAuthConfig, - ResourceRegistryInterface, - ToolRegistryInterface, EntryOwnerRef, PluginEntry, AdapterEntry, @@ -296,15 +293,15 @@ export class AppRemoteInstance extends AppEntry { return this._plugins; } - override get tools(): ToolRegistryInterface { + override get tools(): ToolRegistry { return this._tools; } - override get resources(): ResourceRegistryInterface { + override get resources(): ResourceRegistry { return this._resources; } - override get prompts(): PromptRegistryInterface { + override get prompts(): PromptRegistry { return this._prompts; } diff --git a/libs/sdk/src/auth/auth.registry.ts b/libs/sdk/src/auth/auth.registry.ts index c6976d219..74f47ed77 100644 --- a/libs/sdk/src/auth/auth.registry.ts +++ b/libs/sdk/src/auth/auth.registry.ts @@ -8,7 +8,6 @@ import { FrontMcpLogger, AuthProviderType, AuthProviderEntry, - AuthRegistryInterface, AuthProviderRecord, AuthProviderKind, EntryOwnerRef, @@ -39,10 +38,7 @@ const DEFAULT_AUTH_OPTIONS: AuthOptionsInput = { mode: 'public', }; -export class AuthRegistry - extends RegistryAbstract - implements AuthRegistryInterface -{ +export class AuthRegistry extends RegistryAbstract { private readonly primary?: FrontMcpAuth; private readonly parsedOptions: AuthOptions; private readonly logger: FrontMcpLogger; diff --git a/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts b/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts index 23556002d..8da0fcac7 100644 --- a/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts +++ b/libs/sdk/src/auth/instances/instance.remote-primary-auth.ts @@ -6,7 +6,6 @@ import WellKnownPrmFlow from '../flows/well-known.prm.flow'; import WellKnownAsFlow from '../flows/well-known.oauth-authorization-server.flow'; import WellKnownJwksFlow from '../flows/well-known.jwks.flow'; import SessionVerifyFlow from '../flows/session.verify.flow'; -import { Scope } from '../../scope'; export class RemotePrimaryAuth extends FrontMcpAuth { override ready: Promise; @@ -49,7 +48,7 @@ export class RemotePrimaryAuth extends FrontMcpAuth { return Promise.resolve(); } - private async registerAuthFlows(scope: Scope) { + private async registerAuthFlows(scope: ScopeEntry) { await scope.registryFlows( WellKnownPrmFlow /** /.well-known/oauth-protected-resource */, WellKnownAsFlow /** /.well-known/oauth-authorization-server */, diff --git a/libs/sdk/src/common/entries/app.entry.ts b/libs/sdk/src/common/entries/app.entry.ts index c52e73544..efdb3c42b 100644 --- a/libs/sdk/src/common/entries/app.entry.ts +++ b/libs/sdk/src/common/entries/app.entry.ts @@ -1,18 +1,13 @@ import { BaseEntry } from './base.entry'; import { AppRecord } from '../records'; -import { - AdapterRegistryInterface, - AppInterface, - PluginRegistryInterface, - PromptRegistryInterface, - ProviderRegistryInterface, - ResourceRegistryInterface, - ToolRegistryInterface, -} from '../interfaces'; +import { AdapterRegistryInterface, PluginRegistryInterface, ProviderRegistryInterface } from '../interfaces'; import type { SkillRegistryInterface } from '../../skill/skill.registry'; import { AppMetadata } from '../metadata'; +import type ToolRegistry from '../../tool/tool.registry'; +import type ResourceRegistry from '../../resource/resource.registry'; +import type PromptRegistry from '../../prompt/prompt.registry'; -export abstract class AppEntry extends BaseEntry { +export abstract class AppEntry extends BaseEntry { readonly id: string; /** @@ -31,11 +26,11 @@ export abstract class AppEntry extends BaseEntry { +export abstract class PluginEntry extends BaseEntry { abstract get(token: Token): T; } diff --git a/libs/sdk/src/common/entries/provider.entry.ts b/libs/sdk/src/common/entries/provider.entry.ts index ce713ada6..34b2ba635 100644 --- a/libs/sdk/src/common/entries/provider.entry.ts +++ b/libs/sdk/src/common/entries/provider.entry.ts @@ -1,8 +1,7 @@ import { BaseEntry } from './base.entry'; import type { ProviderRecord } from '../records'; -import type { ProviderInterface } from '../interfaces'; import type { ProviderMetadata } from '../metadata'; -abstract class ProviderEntry extends BaseEntry {} +abstract class ProviderEntry extends BaseEntry {} export { ProviderEntry }; diff --git a/libs/sdk/src/common/entries/scope.entry.ts b/libs/sdk/src/common/entries/scope.entry.ts index d77fc3819..0f69a50e5 100644 --- a/libs/sdk/src/common/entries/scope.entry.ts +++ b/libs/sdk/src/common/entries/scope.entry.ts @@ -2,26 +2,30 @@ import { Token, Type } from '@frontmcp/di'; import { BaseEntry } from './base.entry'; import { ScopeRecord } from '../records'; import { - ScopeInterface, ProviderRegistryInterface, - AppRegistryInterface, - AuthRegistryInterface, FrontMcpAuth, FlowInputOf, FlowOutputOf, FlowType, FrontMcpLogger, - ToolRegistryInterface, - HookRegistryInterface, - ResourceRegistryInterface, - PromptRegistryInterface, } from '../interfaces'; import { FlowName, ScopeMetadata } from '../metadata'; import { normalizeEntryPrefix, normalizeScopeBase } from '../utils'; import type { NotificationService } from '../../notification'; import type { SkillRegistryInterface } from '../../skill/skill.registry'; +import type { ToolUIRegistry } from '../../tool/ui/ui-shared'; +import type { TransportService } from '../../transport/transport.registry'; +import type { ElicitationStore } from '../../elicitation/store/elicitation.store'; +import type { GuardManager } from '@frontmcp/guard'; +import type HookRegistry from '../../hooks/hook.registry'; +import type { AuthRegistry } from '../../auth/auth.registry'; +import type AppRegistry from '../../app/app.registry'; +import type ToolRegistry from '../../tool/tool.registry'; +import type ResourceRegistry from '../../resource/resource.registry'; +import type PromptRegistry from '../../prompt/prompt.registry'; +import type AgentRegistry from '../../agent/agent.registry'; -export abstract class ScopeEntry extends BaseEntry { +export abstract class ScopeEntry extends BaseEntry { abstract readonly id: string; abstract readonly entryPath: string; abstract readonly routeBase: string; @@ -35,24 +39,34 @@ export abstract class ScopeEntry extends BaseEntry; abstract runFlow( diff --git a/libs/sdk/src/common/interfaces/app.interface.ts b/libs/sdk/src/common/interfaces/app.interface.ts index a0256d42f..d12f624f8 100644 --- a/libs/sdk/src/common/interfaces/app.interface.ts +++ b/libs/sdk/src/common/interfaces/app.interface.ts @@ -1,12 +1,8 @@ import { Type, ValueType } from '@frontmcp/di'; import { AppMetadata, RemoteAppMetadata } from '../metadata'; -/** Marker interface for FrontMCP application classes */ - -export interface AppInterface {} - export type AppValueType = ValueType & AppMetadata; // Using 'any' default to allow broad compatibility with untyped app classes // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AppType = Type | AppValueType | RemoteAppMetadata; +export type AppType = Type | AppValueType | RemoteAppMetadata; diff --git a/libs/sdk/src/common/interfaces/internal/registry.interface.ts b/libs/sdk/src/common/interfaces/internal/registry.interface.ts index 45d7bb529..ddfd51aab 100644 --- a/libs/sdk/src/common/interfaces/internal/registry.interface.ts +++ b/libs/sdk/src/common/interfaces/internal/registry.interface.ts @@ -1,25 +1,15 @@ import { Token } from '@frontmcp/di'; -import { - ScopeEntry, - FlowEntry, - AuthProviderEntry, - AppEntry, - ProviderEntry, - PluginEntry, - AdapterEntry, - PromptEntry, - ResourceEntry, - ToolEntry, - LoggerEntry, - AgentEntry, - EntryOwnerRef, - HookEntry, -} from '../../entries'; -import { FrontMcpAuth } from './primary-auth-provider.interface'; +import { ScopeEntry, FlowEntry, ProviderEntry, PluginEntry, AdapterEntry, LoggerEntry } from '../../entries'; import { FlowName } from '../../metadata'; -import { FlowCtxOf, FlowInputOf, FlowStagesOf } from '../flow.interface'; -import { HookRecord } from '../../records'; -import { ToolChangeEvent } from '../../../tool/tool.events'; + +// Import concrete registry classes using `import type` to avoid circular deps +import type HookRegistryCls from '../../../hooks/hook.registry'; +import type { AuthRegistry as AuthRegistryCls } from '../../../auth/auth.registry'; +import type AppRegistryCls from '../../../app/app.registry'; +import type ToolRegistryCls from '../../../tool/tool.registry'; +import type ResourceRegistryCls from '../../../resource/resource.registry'; +import type PromptRegistryCls from '../../../prompt/prompt.registry'; +import type AgentRegistryCls from '../../../agent/agent.registry'; export interface ScopeRegistryInterface { getScopes(): ScopeEntry[]; @@ -29,49 +19,6 @@ export interface FlowRegistryInterface { getFlows(): FlowEntry[]; } -export interface HookRegistryInterface { - /** - * used to pull hooks registered by a class and related to that class only, - * like registering hooks on specific tool execution - * @param token - */ - getClsHooks(token: Token): HookEntry[]; - - /** - * Used to pull all hooks registered to specific flow by name, - * this is used to construct the flow graph and execute hooks in order - * @param flow - */ - getFlowHooks( - flow: Name, - ): HookEntry, Name, FlowStagesOf, FlowCtxOf>[]; - - /** - * Used to pull all hooks registered to specific flow and stage by name, - * this is used to construct the flow graph and execute hooks in order - * @param flow - * @param stage - */ - getFlowStageHooks( - flow: Name, - stage: FlowStagesOf | string, - ): HookEntry, Name, FlowStagesOf, FlowCtxOf>[]; - - /** - * Used to pull hooks for a specific flow, optionally filtered by owner ID. - * Returns all hooks if no ownerId is provided, or only hooks belonging to - * the specified owner or global hooks (no owner) if ownerId is provided. - * @param flow - * @param ownerId - */ - getFlowHooksForOwner( - flow: Name, - ownerId?: string, - ): HookEntry, Name, FlowStagesOf, FlowCtxOf>[]; - - registerHooks(embedded: boolean, ...records: HookRecord[]): Promise; -} - export interface ProviderViews { /** App-wide singletons, created at boot. Immutable from invoke's POV. */ global: ReadonlyMap; @@ -93,16 +40,6 @@ export interface ProviderRegistryInterface { buildViews(session: any): Promise; } -export interface AuthRegistryInterface { - getPrimary(): FrontMcpAuth; - - getAuthProviders(): AuthProviderEntry[]; -} - -export interface AppRegistryInterface { - getApps(): AppEntry[]; -} - export interface PluginRegistryInterface { getPlugins(): PluginEntry[]; getPluginNames(): string[]; @@ -112,80 +49,10 @@ export interface AdapterRegistryInterface { getAdapters(): AdapterEntry[]; } -export interface ToolRegistryInterface { - owner: EntryOwnerRef; - - // inline tools plus discovered by nested tool registries - getTools(includeHidden?: boolean): ToolEntry[]; - - // tools appropriate for MCP listing based on client elicitation support - getToolsForListing(supportsElicitation?: boolean): ToolEntry[]; - - // inline tools only - getInlineTools(): ToolEntry[]; - - // subscribe to tool change events - subscribe( - opts: { immediate?: boolean; filter?: (i: ToolEntry) => boolean }, - cb: (evt: ToolChangeEvent) => void, - ): () => void; -} - -export interface ResourceRegistryInterface { - // owner of this registry - owner: EntryOwnerRef; - - // inline resources plus discovered by nested resource registries - getResources(includeHidden?: boolean): ResourceEntry[]; - - // resource templates - getResourceTemplates(): ResourceEntry[]; - - // inline resources only - getInlineResources(): ResourceEntry[]; - - // find a resource by URI (exact match first, then template matching) - findResourceForUri(uri: string): { instance: ResourceEntry; params: Record } | undefined; -} - -export interface PromptRegistryInterface { - // owner reference for the registry - owner: EntryOwnerRef; - - // inline prompts plus discovered by nested prompt registries - getPrompts(includeHidden?: boolean): PromptEntry[]; - - // inline prompts only - getInlinePrompts(): PromptEntry[]; - - // find a prompt by name - findByName(name: string): PromptEntry | undefined; -} - export interface LoggerRegistryInterface { getLoggers(): LoggerEntry[]; } -export interface AgentRegistryInterface { - // owner reference for the registry - owner: EntryOwnerRef; - - // all agents (inline plus discovered from nested registries) - getAgents(includeHidden?: boolean): AgentEntry[]; - - // inline agents only - getInlineAgents(): AgentEntry[]; - - // find agent by ID - findById(id: string): AgentEntry | undefined; - - // find agent by name - findByName(name: string): AgentEntry | undefined; - - // get agents visible to a specific agent - getVisibleAgentsFor(agentId: string): AgentEntry[]; -} - export type GlobalRegistryKind = 'LoggerRegistry' | 'ScopeRegistry'; export type ScopedRegistryKind = 'AppRegistry' | 'AuthRegistry' | 'FlowRegistry' | 'HookRegistry'; @@ -213,16 +80,16 @@ export type RegistryType = { LoggerRegistry: LoggerRegistryInterface; ScopeRegistry: ScopeRegistryInterface; FlowRegistry: FlowRegistryInterface; - HookRegistry: HookRegistryInterface; - AppRegistry: AppRegistryInterface; - AuthRegistry: AuthRegistryInterface; + HookRegistry: HookRegistryCls; + AppRegistry: AppRegistryCls; + AuthRegistry: AuthRegistryCls; ProviderRegistry: ProviderRegistryInterface; PluginRegistry: PluginRegistryInterface; AdapterRegistry: AdapterRegistryInterface; - ToolRegistry: ToolRegistryInterface; - ResourceRegistry: ResourceRegistryInterface; - PromptRegistry: PromptRegistryInterface; - AgentRegistry: AgentRegistryInterface; + ToolRegistry: ToolRegistryCls; + ResourceRegistry: ResourceRegistryCls; + PromptRegistry: PromptRegistryCls; + AgentRegistry: AgentRegistryCls; SkillRegistry: SkillRegistryInterface; JobRegistry: JobRegistryInterface; WorkflowRegistry: WorkflowRegistryInterface; diff --git a/libs/sdk/src/common/interfaces/plugin.interface.ts b/libs/sdk/src/common/interfaces/plugin.interface.ts index 2dac31ae1..95e86a890 100644 --- a/libs/sdk/src/common/interfaces/plugin.interface.ts +++ b/libs/sdk/src/common/interfaces/plugin.interface.ts @@ -1,13 +1,11 @@ import { Type, Token, ValueType, ClassType, FactoryType } from '@frontmcp/di'; import { PluginMetadata } from '../metadata'; -export interface PluginInterface {} - export type PluginClassType = ClassType & PluginMetadata; export type PluginValueType = ValueType & PluginMetadata; export type PluginFactoryType = FactoryType & PluginMetadata; -export type PluginType = +export type PluginType = | Type | PluginClassType | PluginValueType diff --git a/libs/sdk/src/common/interfaces/provider.interface.ts b/libs/sdk/src/common/interfaces/provider.interface.ts index fb3d35e4c..2a4d945d9 100644 --- a/libs/sdk/src/common/interfaces/provider.interface.ts +++ b/libs/sdk/src/common/interfaces/provider.interface.ts @@ -1,8 +1,6 @@ import { Type, Token, ValueType, ClassType, FactoryType, ClassToken } from '@frontmcp/di'; import { ProviderMetadata } from '../metadata'; -export interface ProviderInterface {} - export type ProviderClassType = ClassType & ProviderMetadata; export type ProviderValueType = ValueType & ProviderMetadata; export type ProviderFactoryType = FactoryType< @@ -11,8 +9,9 @@ export type ProviderFactoryType & ProviderMetadata; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type ProviderType< - Provide extends ProviderInterface = any, + Provide = any, Tokens extends readonly (ClassToken | Token)[] = readonly (ClassToken | Token)[], > = Type | ProviderClassType | ProviderValueType | ProviderFactoryType; diff --git a/libs/sdk/src/common/interfaces/scope.interface.ts b/libs/sdk/src/common/interfaces/scope.interface.ts index 4297dd78d..2af23bf9a 100644 --- a/libs/sdk/src/common/interfaces/scope.interface.ts +++ b/libs/sdk/src/common/interfaces/scope.interface.ts @@ -1,7 +1,5 @@ import { Type } from '@frontmcp/di'; -export interface ScopeInterface {} +export type ScopeType = Type; -export type ScopeType = Type; - -export { ScopeInterface as FrontMcpScopeInterface, ScopeType as FrontMcpScopeType }; +export { ScopeType as FrontMcpScopeType }; diff --git a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts index 36cf90359..04485609a 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts @@ -163,7 +163,10 @@ export default class ElicitationRequestFlow extends FlowBase { this.logger.verbose('storePendingRecord:start'); const { elicitId, sessionId, message, mode, expiresAt, requestedSchema } = this.state.required; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; + if (!store) { + throw new Error('Elicitation store not initialized'); + } const pendingRecord: PendingElicitRecord = { elicitId, @@ -175,7 +178,7 @@ export default class ElicitationRequestFlow extends FlowBase { requestedSchema, }; - await scope.elicitationStore.setPending(pendingRecord); + await store.setPending(pendingRecord); this.state.set('pendingRecord', pendingRecord); this.logger.verbose('storePendingRecord:done', { elicitId, sessionId }); diff --git a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts index b58661050..3e49354e0 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts @@ -110,9 +110,12 @@ export default class ElicitationResultFlow extends FlowBase { this.logger.verbose('lookupPending:start'); const { sessionId } = this.state.required; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; + if (!store) { + throw new Error('Elicitation store not initialized'); + } - const pendingRecord = await scope.elicitationStore.getPending(sessionId); + const pendingRecord = await store.getPending(sessionId); this.state.set('pendingRecord', pendingRecord ?? undefined); if (!pendingRecord) { @@ -191,16 +194,16 @@ export default class ElicitationResultFlow extends FlowBase { const { pendingRecord, elicitResult } = this.state; // sessionId is set in parseInput and is required by stateSchema const { sessionId } = this.state.required; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; - if (!pendingRecord || !elicitResult) { + if (!pendingRecord || !elicitResult || !store) { this.state.set('handled', false); this.logger.verbose('publishResult:skip (no pending or no result)'); return; } try { - await scope.elicitationStore.publishResult(pendingRecord.elicitId, sessionId, elicitResult); + await store.publishResult(pendingRecord.elicitId, sessionId, elicitResult); this.state.set('handled', true); this.logger.verbose('publishResult:done', { elicitId: pendingRecord.elicitId, diff --git a/libs/sdk/src/elicitation/helpers/fallback.helper.ts b/libs/sdk/src/elicitation/helpers/fallback.helper.ts index 1d885756e..2e3873716 100644 --- a/libs/sdk/src/elicitation/helpers/fallback.helper.ts +++ b/libs/sdk/src/elicitation/helpers/fallback.helper.ts @@ -147,7 +147,12 @@ export async function handleWaitingFallback( }, ttl); // Subscribe to fallback results - scope.elicitationStore + const store = scope.elicitationStore; + if (!store) { + reject(new Error('Elicitation store not initialized')); + return; + } + store .subscribeFallbackResult( error.elicitId, (result: FallbackExecutionResult) => { diff --git a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts index 038399f70..9697dc0bf 100644 --- a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts +++ b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts @@ -53,11 +53,10 @@ export class SendElicitationResultTool extends ToolContext { this.logger.info('sendElicitationResult: processing', { elicitId, action }); - // Cast scope to Scope type to access elicitationStore - const scope = this.scope as unknown as Scope; + const store = this.scope.elicitationStore; - // Guard: ensure scope and elicitationStore exist - if (!scope?.elicitationStore) { + // Guard: ensure elicitationStore is available + if (!store) { this.logger.error('sendElicitationResult: scope or elicitationStore not available'); return { content: [ @@ -71,7 +70,7 @@ export class SendElicitationResultTool extends ToolContext { } // Get pending fallback context - const pending = await scope.elicitationStore.getPendingFallback(elicitId); + const pending = await store.getPendingFallback(elicitId); if (!pending) { this.logger.warn('sendElicitationResult: no pending elicitation found', { elicitId }); return { @@ -87,7 +86,7 @@ export class SendElicitationResultTool extends ToolContext { // Check expiration if (Date.now() > pending.expiresAt) { - await scope.elicitationStore.deletePendingFallback(elicitId); + await store.deletePendingFallback(elicitId); this.logger.warn('sendElicitationResult: elicitation expired', { elicitId }); return { content: [ @@ -107,10 +106,10 @@ export class SendElicitationResultTool extends ToolContext { }; // Store resolved result for re-invocation (pass sessionId for encryption support) - await scope.elicitationStore.setResolvedResult(elicitId, elicitResult, pending.sessionId); + await store.setResolvedResult(elicitId, elicitResult, pending.sessionId); // Clean up pending fallback - await scope.elicitationStore.deletePendingFallback(elicitId); + await store.deletePendingFallback(elicitId); this.logger.info('sendElicitationResult: re-invoking original tool', { elicitId, @@ -127,7 +126,7 @@ export class SendElicitationResultTool extends ToolContext { // Re-invoke the original tool using the flow // The pre-resolved result is in the async context, so the tool's elicit() // will return it immediately instead of throwing ElicitationFallbackRequired - const toolResult = await scope.runFlowForOutput('tools:call-tool', { + const toolResult = await (this.scope as unknown as Scope).runFlowForOutput('tools:call-tool', { request: { method: 'tools/call', params: { @@ -146,26 +145,26 @@ export class SendElicitationResultTool extends ToolContext { // Publish the result to notify any waiting requests (distributed mode) // This allows the original request on a different node to receive the result - await scope.elicitationStore.publishFallbackResult(elicitId, pending.sessionId, { + await store.publishFallbackResult(elicitId, pending.sessionId, { success: true, result: toolResult, }); // Clean up resolved result - await scope.elicitationStore.deleteResolvedResult(elicitId); + await store.deleteResolvedResult(elicitId); return toolResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Publish the error to notify any waiting requests (distributed mode) - await scope.elicitationStore.publishFallbackResult(elicitId, pending.sessionId, { + await store.publishFallbackResult(elicitId, pending.sessionId, { success: false, error: errorMessage, }); // Clean up resolved result on error - await scope.elicitationStore.deleteResolvedResult(elicitId); + await store.deleteResolvedResult(elicitId); this.logger.error('sendElicitationResult: original tool failed', { elicitId, diff --git a/libs/sdk/src/flows/flow.instance.ts b/libs/sdk/src/flows/flow.instance.ts index ffbf079dd..d03b59499 100644 --- a/libs/sdk/src/flows/flow.instance.ts +++ b/libs/sdk/src/flows/flow.instance.ts @@ -15,6 +15,7 @@ import { HookEntry, HookMetadata, Reference, + ScopeEntry, ServerRequest, Token, Type, @@ -22,7 +23,6 @@ import { import ProviderRegistry from '../provider/provider.registry'; import { collectFlowHookMap, StageMap, cloneStageMap, mergeHookMetasIntoStageMap } from './flow.stages'; import { writeHttpResponse } from '../server/server.validation'; -import { Scope } from '../scope'; import HookRegistry from '../hooks/hook.registry'; import { FrontMcpContextStorage, FRONTMCP_CONTEXT } from '../context'; import { RequestContextNotAvailableError, InternalMcpError } from '../errors'; @@ -58,14 +58,14 @@ export class FlowInstance extends FlowEntry { private hooks: HookRegistry; private readonly logger: FrontMcpLogger; - constructor(scope: Scope, record: FlowRecord, deps: Set, globalProviders: ProviderRegistry) { + constructor(scope: ScopeEntry, record: FlowRecord, deps: Set, globalProviders: ProviderRegistry) { super(scope, record); this.deps = [...deps]; this.globalProviders = globalProviders; this.FlowClass = this.record.provide; this.ready = this.initialize(); this.plan = this.record.metadata.plan; - this.hooks = scope.providers.getHooksRegistry(); + this.hooks = scope.hooks; this.logger = scope.logger.child('FlowInstance'); } diff --git a/libs/sdk/src/hooks/hook.registry.ts b/libs/sdk/src/hooks/hook.registry.ts index c756ffe1b..cd798092b 100644 --- a/libs/sdk/src/hooks/hook.registry.ts +++ b/libs/sdk/src/hooks/hook.registry.ts @@ -7,7 +7,6 @@ import { FlowStagesOf, HookEntry, HookRecord, - HookRegistryInterface, HookType, ScopeEntry, Token, @@ -17,10 +16,7 @@ import ProviderRegistry from '../provider/provider.registry'; import { HookInstance } from './hook.instance'; import { UnsupportedHookOwnerKindError } from '../errors'; -export default class HookRegistry - extends RegistryAbstract - implements HookRegistryInterface -{ +export default class HookRegistry extends RegistryAbstract { scope: ScopeEntry; /** Historical records by class (kept if you still want access to raw records) */ diff --git a/libs/sdk/src/job/job.instance.ts b/libs/sdk/src/job/job.instance.ts index 563a16a33..8f7b0d0ba 100644 --- a/libs/sdk/src/job/job.instance.ts +++ b/libs/sdk/src/job/job.instance.ts @@ -6,7 +6,7 @@ import { ToolInputOf, ToolOutputOf } from '../common/decorators'; import ProviderRegistry from '../provider/provider.registry'; import { z } from 'zod'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { InvalidHookFlowError } from '../errors/mcp.error'; import { InvalidRegistryKindError, DynamicJobDirectExecutionError } from '../errors'; @@ -21,7 +21,7 @@ export class JobInstance< Out = ToolOutputOf<{ outputSchema: OutSchema }>, > extends JobEntry { private readonly _providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; constructor(record: JobRecord, providers: ProviderRegistry, owner: EntryOwnerRef) { @@ -31,7 +31,7 @@ export class JobInstance< this.name = record.metadata.id || record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // inputSchema is always a ZodRawShape this.inputSchema = (record.metadata.inputSchema ?? {}) as InSchema; diff --git a/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts b/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts index 71cc708ff..dd5491310 100644 --- a/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts +++ b/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts @@ -4,7 +4,7 @@ import 'reflect-metadata'; import PluginRegistry, { PluginScopeInfo } from '../plugin.registry'; -import { PluginInterface, FlowCtxOf } from '../../common/interfaces'; +import { FlowCtxOf } from '../../common/interfaces'; import { FrontMcpPlugin } from '../../common/decorators/plugin.decorator'; import { FlowHooksOf } from '../../common/decorators/hook.decorator'; import { createClassProvider, createValueProvider } from '../../__test-utils__/fixtures/provider.fixtures'; @@ -22,7 +22,7 @@ describe('PluginRegistry', () => { name: 'TestPlugin', description: 'A test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor() {} } @@ -41,13 +41,13 @@ describe('PluginRegistry', () => { name: 'PluginA', description: 'First plugin', }) - class PluginA implements PluginInterface {} + class PluginA {} @FrontMcpPlugin({ name: 'PluginB', description: 'Second plugin', }) - class PluginB implements PluginInterface {} + class PluginB {} const providers = await createProviderRegistryWithScope(); @@ -63,7 +63,7 @@ describe('PluginRegistry', () => { it('should register a plugin using useClass', async () => { const PLUGIN_TOKEN = Symbol('PLUGIN_TOKEN'); - class TestPluginImpl implements PluginInterface { + class TestPluginImpl { constructor() {} } @@ -145,7 +145,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides services', providers: [], }) - class PluginWithProviders implements PluginInterface { + class PluginWithProviders { get: any; getService() { @@ -174,7 +174,7 @@ describe('PluginRegistry', () => { providers: [], exports: [], }) - class PluginWithExports implements PluginInterface {} + class PluginWithExports {} const providers = await createProviderRegistryWithScope(); @@ -193,7 +193,7 @@ describe('PluginRegistry', () => { name: 'DependentPlugin', description: 'Plugin with dependencies', }) - class DependentPlugin implements PluginInterface { + class DependentPlugin { constructor() {} } @@ -213,7 +213,7 @@ describe('PluginRegistry', () => { name: 'SimplePlugin', description: 'Plugin without dependencies', }) - class SimplePlugin implements PluginInterface { + class SimplePlugin { constructor() {} } @@ -264,14 +264,14 @@ describe('PluginRegistry', () => { name: 'NestedPlugin', description: 'A nested plugin', }) - class NestedPlugin implements PluginInterface {} + class NestedPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', description: 'Parent plugin with nested plugins', plugins: [NestedPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); @@ -291,7 +291,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides tools', tools: [], }) - class PluginWithTools implements PluginInterface {} + class PluginWithTools {} const providers = await createProviderRegistryWithScope(); @@ -309,7 +309,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides resources', resources: [], }) - class PluginWithResources implements PluginInterface {} + class PluginWithResources {} const providers = await createProviderRegistryWithScope(); @@ -327,7 +327,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides prompts', prompts: [], }) - class PluginWithPrompts implements PluginInterface {} + class PluginWithPrompts {} const providers = await createProviderRegistryWithScope(); @@ -345,7 +345,7 @@ describe('PluginRegistry', () => { description: 'Plugin that provides adapters', adapters: [], }) - class PluginWithAdapters implements PluginInterface {} + class PluginWithAdapters {} const providers = await createProviderRegistryWithScope(); @@ -451,7 +451,7 @@ describe('PluginRegistry', () => { description: 'Plugin that uses get method', providers: [], }) - class PluginWithGet implements PluginInterface { + class PluginWithGet { get: any; } @@ -471,7 +471,7 @@ describe('PluginRegistry', () => { it('should handle plugins with dynamic providers', async () => { const DYNAMIC_TOKEN = Symbol('DYNAMIC'); - class DynamicPlugin implements PluginInterface { + class DynamicPlugin { get: any; } @@ -500,7 +500,7 @@ describe('PluginRegistry', () => { name: 'DefaultScopePlugin', description: 'Plugin without explicit scope', }) - class DefaultScopePlugin implements PluginInterface {} + class DefaultScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -525,7 +525,7 @@ describe('PluginRegistry', () => { description: 'Plugin with app scope', scope: 'app', }) - class AppScopePlugin implements PluginInterface {} + class AppScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -550,7 +550,7 @@ describe('PluginRegistry', () => { description: 'Plugin with server scope', scope: 'server', }) - class ServerScopePlugin implements PluginInterface {} + class ServerScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -576,7 +576,7 @@ describe('PluginRegistry', () => { description: 'Plugin with server scope', scope: 'server', }) - class ServerScopePlugin implements PluginInterface {} + class ServerScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -600,7 +600,7 @@ describe('PluginRegistry', () => { description: 'Plugin with app scope', scope: 'app', }) - class AppScopePlugin implements PluginInterface {} + class AppScopePlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -625,14 +625,14 @@ describe('PluginRegistry', () => { description: 'App scope plugin', scope: 'app', }) - class AppPlugin implements PluginInterface {} + class AppPlugin {} @FrontMcpPlugin({ name: 'ServerPlugin', description: 'Server scope plugin', scope: 'server', }) - class ServerPlugin implements PluginInterface {} + class ServerPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -658,7 +658,7 @@ describe('PluginRegistry', () => { name: 'LegacyPlugin', description: 'Plugin without scopeInfo', }) - class LegacyPlugin implements PluginInterface {} + class LegacyPlugin {} const providers = await createProviderRegistryWithScope(); @@ -674,7 +674,7 @@ describe('PluginRegistry', () => { it('should validate server scope plugin with object-based registration', async () => { const PLUGIN_TOKEN = Symbol('SERVER_PLUGIN'); - class ServerPlugin implements PluginInterface { + class ServerPlugin { get: any; } @@ -711,7 +711,7 @@ describe('PluginRegistry', () => { name: 'DecoratedServerPlugin', scope: 'app', // Decorator says app }) - class DecoratedServerPlugin implements PluginInterface {} + class DecoratedServerPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -747,7 +747,7 @@ describe('PluginRegistry', () => { name: 'DecoratedAppPlugin', scope: 'app', }) - class DecoratedAppPlugin implements PluginInterface {} + class DecoratedAppPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -785,13 +785,13 @@ describe('PluginRegistry', () => { name: 'NestedServerPlugin', scope: 'server', }) - class NestedServerPlugin implements PluginInterface {} + class NestedServerPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', plugins: [NestedServerPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -814,13 +814,13 @@ describe('PluginRegistry', () => { name: 'NestedAppPlugin', scope: 'app', }) - class NestedAppPlugin implements PluginInterface {} + class NestedAppPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', plugins: [NestedAppPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -843,13 +843,13 @@ describe('PluginRegistry', () => { name: 'NestedServerPlugin', scope: 'server', }) - class NestedServerPlugin implements PluginInterface {} + class NestedServerPlugin {} @FrontMcpPlugin({ name: 'ParentPlugin', plugins: [NestedServerPlugin], }) - class ParentPlugin implements PluginInterface {} + class ParentPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -873,19 +873,19 @@ describe('PluginRegistry', () => { name: 'DeeplyNestedServerPlugin', scope: 'server', }) - class DeeplyNestedServerPlugin implements PluginInterface {} + class DeeplyNestedServerPlugin {} @FrontMcpPlugin({ name: 'MiddlePlugin', plugins: [DeeplyNestedServerPlugin], }) - class MiddlePlugin implements PluginInterface {} + class MiddlePlugin {} @FrontMcpPlugin({ name: 'TopPlugin', plugins: [MiddlePlugin], }) - class TopPlugin implements PluginInterface {} + class TopPlugin {} const providers = await createProviderRegistryWithScope(); const ownScope = providers.get(Scope); @@ -910,7 +910,7 @@ describe('PluginRegistry', () => { name: 'AppScopePluginWithHooks', scope: 'app', }) - class AppScopePluginWithHooks implements PluginInterface { + class AppScopePluginWithHooks { @ToolHook.Will('execute') async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) { // App-scoped hook @@ -940,7 +940,7 @@ describe('PluginRegistry', () => { name: 'ServerScopePluginWithHooks', scope: 'server', }) - class ServerScopePluginWithHooks implements PluginInterface { + class ServerScopePluginWithHooks { @ToolHook.Will('execute') async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) { // Server-scoped hook @@ -970,7 +970,7 @@ describe('PluginRegistry', () => { name: 'ServerScopePluginNoParent', scope: 'server', }) - class ServerScopePluginNoParent implements PluginInterface { + class ServerScopePluginNoParent { @ToolHook.Will('execute') async beforeExecute(_ctx: FlowCtxOf<'tools:call-tool'>) { // Server-scoped hook with no parent @@ -1006,7 +1006,7 @@ describe('PluginRegistry', () => { name: 'DecoratorAppPlugin', scope: 'app', }) - class DecoratorAppPlugin implements PluginInterface { + class DecoratorAppPlugin { get: any; } @@ -1045,7 +1045,7 @@ describe('PluginRegistry', () => { name: 'DecoratorAppPlugin', scope: 'app', }) - class DecoratorAppPlugin implements PluginInterface { + class DecoratorAppPlugin { get: any; } @@ -1082,7 +1082,7 @@ describe('PluginRegistry', () => { const PLUGIN_TOKEN = Symbol('PLUGIN'); // No @FrontMcpPlugin decorator - class PlainPlugin implements PluginInterface { + class PlainPlugin { get: any; } diff --git a/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts b/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts index b96ffdc3e..9c8ec4a44 100644 --- a/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts +++ b/libs/sdk/src/plugin/__tests__/plugin.utils.spec.ts @@ -5,7 +5,7 @@ import 'reflect-metadata'; import { normalizePlugin, collectPluginMetadata, pluginDiscoveryDeps } from '../plugin.utils'; import { FrontMcpPlugin } from '../../common/decorators/plugin.decorator'; -import { PluginInterface } from '../../common/interfaces'; + import { PluginKind } from '../../common/records'; import { createValueProvider } from '../../__test-utils__/fixtures/provider.fixtures'; @@ -16,7 +16,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'A test plugin', }) - class TestPlugin implements PluginInterface {} + class TestPlugin {} const metadata = collectPluginMetadata(TestPlugin); @@ -39,7 +39,7 @@ describe('Plugin Utils', () => { description: 'Plugin with providers', providers: [], }) - class PluginWithProviders implements PluginInterface {} + class PluginWithProviders {} const metadata = collectPluginMetadata(PluginWithProviders); @@ -55,7 +55,7 @@ describe('Plugin Utils', () => { providers: [], exports: [], }) - class PluginWithExports implements PluginInterface {} + class PluginWithExports {} const metadata = collectPluginMetadata(PluginWithExports); @@ -72,7 +72,7 @@ describe('Plugin Utils', () => { resources: [], prompts: [], }) - class FullPlugin implements PluginInterface {} + class FullPlugin {} const metadata = collectPluginMetadata(FullPlugin); @@ -90,7 +90,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'A test plugin', }) - class TestPlugin implements PluginInterface {} + class TestPlugin {} const record = normalizePlugin(TestPlugin); @@ -114,7 +114,7 @@ describe('Plugin Utils', () => { it('should normalize a plugin object with useClass to CLASS kind', () => { const PLUGIN_TOKEN = Symbol('PLUGIN'); - class TestPluginImpl implements PluginInterface {} + class TestPluginImpl {} const plugin = { provide: PLUGIN_TOKEN, @@ -378,7 +378,7 @@ describe('Plugin Utils', () => { describe('CLASS Plugin Dependencies', () => { it('should return empty array for plugins without dependencies', () => { - class TestPlugin implements PluginInterface { + class TestPlugin { constructor() {} } @@ -395,7 +395,7 @@ describe('Plugin Utils', () => { }); it('should filter dependencies based on type', () => { - class TestPlugin implements PluginInterface { + class TestPlugin { constructor( public dep1: unknown, public primitive: string, @@ -425,7 +425,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'Test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor() {} } @@ -441,7 +441,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'Test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor( public dep1: unknown, public nullable: unknown, @@ -464,7 +464,7 @@ describe('Plugin Utils', () => { name: 'TestPlugin', description: 'Test plugin', }) - class TestPlugin implements PluginInterface { + class TestPlugin { constructor( public dep1: unknown, public stringDep: string, @@ -497,7 +497,7 @@ describe('Plugin Utils', () => { resources: [], prompts: [], }) - class ComplexPlugin implements PluginInterface {} + class ComplexPlugin {} const record = normalizePlugin(ComplexPlugin); @@ -518,7 +518,7 @@ describe('Plugin Utils', () => { name: 'DependentPlugin', description: 'Plugin with dependencies', }) - class DependentPlugin implements PluginInterface { + class DependentPlugin { constructor() {} } diff --git a/libs/sdk/src/plugin/plugin.registry.ts b/libs/sdk/src/plugin/plugin.registry.ts index 376155547..4fab7fdce 100644 --- a/libs/sdk/src/plugin/plugin.registry.ts +++ b/libs/sdk/src/plugin/plugin.registry.ts @@ -9,6 +9,7 @@ import { PluginRegistryInterface, PluginType, ProviderEntry, + ScopeEntry, } from '../common'; import { normalizePlugin, pluginDiscoveryDeps } from './plugin.utils'; import ProviderRegistry from '../provider/provider.registry'; @@ -19,7 +20,6 @@ import PromptRegistry from '../prompt/prompt.registry'; import SkillRegistry from '../skill/skill.registry'; import { normalizeProvider } from '../provider/provider.utils'; import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; -import { Scope } from '../scope'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { InvalidPluginScopeError, RegistryDependencyNotRegisteredError, InvalidRegistryKindError } from '../errors'; import { installContextExtensions } from '../context'; @@ -32,9 +32,9 @@ import { FrontMcpLogger } from '../common'; */ export interface PluginScopeInfo { /** The scope where the plugin is defined (app's own scope) */ - ownScope: Scope; + ownScope: ScopeEntry; /** Parent scope for non-standalone apps (gateway scope) */ - parentScope?: Scope; + parentScope?: ScopeEntry; /** Whether the app is standalone (standalone: true) */ isStandaloneApp: boolean; } @@ -58,7 +58,7 @@ export default class PluginRegistry /** skills by token */ private readonly pSkills: Map = new Map(); - private readonly scope: Scope; + private readonly scope: ScopeEntry; private readonly scopeInfo?: PluginScopeInfo; private readonly owner?: EntryOwnerRef; private readonly logger?: FrontMcpLogger; @@ -225,7 +225,7 @@ export default class PluginRegistry // Determine which scope to use for hook registration: // - scope='app' (default): register hooks to own scope (app-level) // - scope='server': register hooks to parent scope (gateway-level) if available - let targetHookScope: Scope; + let targetHookScope: ScopeEntry; if (pluginScope === 'server' && this.scopeInfo?.parentScope) { targetHookScope = this.scopeInfo.parentScope; } else { diff --git a/libs/sdk/src/prompt/prompt.instance.ts b/libs/sdk/src/prompt/prompt.instance.ts index 82c8c85f2..1b8c31a97 100644 --- a/libs/sdk/src/prompt/prompt.instance.ts +++ b/libs/sdk/src/prompt/prompt.instance.ts @@ -15,7 +15,7 @@ import { } from '../common'; import ProviderRegistry from '../provider/provider.registry'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { buildParsedPromptResult } from './prompt.utils'; import { GetPromptResult } from '@frontmcp/protocol'; @@ -23,7 +23,7 @@ import { MissingPromptArgumentError, InvalidRegistryKindError } from '../errors' export class PromptInstance extends PromptEntry { private readonly _providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; constructor(record: PromptRecord, providers: ProviderRegistry, owner: EntryOwnerRef) { @@ -33,7 +33,7 @@ export class PromptInstance extends PromptEntry { this.name = record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; this.ready = this.initialize(); } diff --git a/libs/sdk/src/prompt/prompt.registry.ts b/libs/sdk/src/prompt/prompt.registry.ts index 945d367e1..46ee97472 100644 --- a/libs/sdk/src/prompt/prompt.registry.ts +++ b/libs/sdk/src/prompt/prompt.registry.ts @@ -1,15 +1,7 @@ // file: libs/sdk/src/prompt/prompt.registry.ts import { Token, tokenName, getMetadata } from '@frontmcp/di'; -import { - EntryLineage, - EntryOwnerRef, - PromptEntry, - PromptRecord, - PromptRegistryInterface, - PromptType, - AppEntry, -} from '../common'; +import { AppEntry, EntryLineage, EntryOwnerRef, PromptEntry, PromptRecord, PromptType, ScopeEntry } from '../common'; import { PromptChangeEvent, PromptEmitter } from './prompt.events'; import ProviderRegistry from '../provider/provider.registry'; import { ensureMaxLen, sepFor } from '@frontmcp/utils'; @@ -22,7 +14,6 @@ import { DEFAULT_PROMPT_EXPORT_OPTS, PromptExportOptions, IndexedPrompt } from ' import GetPromptFlow from './flows/get-prompt.flow'; import PromptsListFlow from './flows/prompts-list.flow'; import { ServerCapabilities } from '@frontmcp/protocol'; -import { Scope } from '../scope'; import { NameDisambiguationError, EntryValidationError, @@ -33,14 +24,11 @@ import { /** Maximum attempts for name disambiguation to prevent infinite loops */ const MAX_DISAMBIGUATE_ATTEMPTS = 10000; -export default class PromptRegistry - extends RegistryAbstract< - PromptInstance, // instances map holds PromptInstance - PromptRecord, - PromptType[] - > - implements PromptRegistryInterface -{ +export default class PromptRegistry extends RegistryAbstract< + PromptInstance, // instances map holds PromptInstance + PromptRecord, + PromptType[] +> { /** Who owns this registry (used for provenance). */ owner: EntryOwnerRef; @@ -190,7 +178,7 @@ export default class PromptRegistry * Remote apps expose prompts via proxy entries that forward execution to the remote server. * This also subscribes to updates from the remote app's registry for lazy-loaded prompts. */ - private adoptPromptsFromRemoteApp(app: AppEntry, scope: Scope): void { + private adoptPromptsFromRemoteApp(app: AppEntry, scope: ScopeEntry): void { const remoteRegistry = app.prompts as PromptRegistry; // Helper to adopt/re-adopt prompts from the remote app diff --git a/libs/sdk/src/provider/provider.registry.ts b/libs/sdk/src/provider/provider.registry.ts index 674191d29..a017736e3 100644 --- a/libs/sdk/src/provider/provider.registry.ts +++ b/libs/sdk/src/provider/provider.registry.ts @@ -13,7 +13,6 @@ import { hasAsyncWith, } from '@frontmcp/di'; import { - ProviderInterface, ProviderType, ProviderRegistryInterface, ScopeEntry, @@ -575,7 +574,7 @@ export default class ProviderRegistry } getHooksRegistry() { - return this.getRegistries('HookRegistry')[0] as HookRegistry; + return this.getRegistries('HookRegistry')[0]; } // noinspection JSUnusedGlobalSymbols @@ -648,7 +647,7 @@ export default class ProviderRegistry mergeFromRegistry( providedBy: ProviderRegistry, exported: { - token: Token; + token: Token; def: ProviderRecord; /** Instance may be undefined for CONTEXT-scoped providers (built per-request) */ instance: ProviderEntry | undefined; @@ -669,7 +668,7 @@ export default class ProviderRegistry /** * Used by plugins to get the exported provider definitions. */ - getProviderInfo(token: Token) { + getProviderInfo(token: Token) { const def = this.defs.get(token); const instance = this.instances.get(token); if (!def || !instance) @@ -733,7 +732,7 @@ export default class ProviderRegistry return parent.getWithParents(token); } - getActiveScope(): Scope { + getActiveScope(): ScopeEntry { return this.getWithParents(Scope); } diff --git a/libs/sdk/src/resource/flows/read-resource.flow.ts b/libs/sdk/src/resource/flows/read-resource.flow.ts index e9782386c..8f21feef7 100644 --- a/libs/sdk/src/resource/flows/read-resource.flow.ts +++ b/libs/sdk/src/resource/flows/read-resource.flow.ts @@ -169,19 +169,21 @@ export default class ReadResourceFlow extends FlowBase { if (isUIResourceUri(uri)) { this.logger.info(`findResource: detected UI resource URI "${uri}"`); - // Get the ToolUIRegistry from the scope - const scope = this.scope as Scope; - // Get platform type: first check sessionIdPayload (detected from user-agent), // then fall back to notification service (detected from MCP clientInfo) const { sessionId, authInfo } = this.state; const platformType = authInfo?.sessionIdPayload?.platformType ?? - (sessionId ? scope.notifications.getPlatformType(sessionId) : undefined); + (sessionId ? this.scope.notifications.getPlatformType(sessionId) : undefined); this.logger.verbose(`findResource: platform type for session: ${platformType ?? 'unknown'}`); - const uiResult = handleUIResourceRead(uri, scope.toolUI, platformType); + if (!this.scope.toolUI) { + this.logger.verbose('findResource: toolUI not available, skipping UI resource handling'); + throw new ResourceNotFoundError(uri); + } + + const uiResult = handleUIResourceRead(uri, this.scope.toolUI, platformType); if (uiResult.handled) { if (uiResult.error) { diff --git a/libs/sdk/src/resource/resource.instance.ts b/libs/sdk/src/resource/resource.instance.ts index 4571cc270..fc10cf00c 100644 --- a/libs/sdk/src/resource/resource.instance.ts +++ b/libs/sdk/src/resource/resource.instance.ts @@ -18,7 +18,7 @@ import { } from '../common'; import ProviderRegistry from '../provider/provider.registry'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import { matchUriTemplate, parseUriTemplate } from '@frontmcp/utils'; import { buildResourceContent as buildParsedResourceResult } from '../utils/content.utils'; @@ -30,7 +30,7 @@ export class ResourceInstance< Out = unknown, > extends ResourceEntry { private readonly providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; /** Parsed URI template info for template resources */ @@ -43,7 +43,7 @@ export class ResourceInstance< this.name = record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this.providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // Determine if this is a template resource this.isTemplate = 'uriTemplate' in record.metadata; diff --git a/libs/sdk/src/resource/resource.registry.ts b/libs/sdk/src/resource/resource.registry.ts index 9cc719e37..2c7963c27 100644 --- a/libs/sdk/src/resource/resource.registry.ts +++ b/libs/sdk/src/resource/resource.registry.ts @@ -2,13 +2,14 @@ import { Token, tokenName, getMetadata } from '@frontmcp/di'; import { + AppEntry, EntryLineage, EntryOwnerRef, ResourceEntry, ResourceRecord, ResourceTemplateRecord, - ResourceRegistryInterface, ResourceType, + ScopeEntry, } from '../common'; import { ResourceChangeEvent, ResourceEmitter } from './resource.events'; import ProviderRegistry from '../provider/provider.registry'; @@ -23,8 +24,6 @@ import { } from './resource.utils'; import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; import { ResourceInstance } from './resource.instance'; -import { Scope } from '../scope'; -import { AppEntry } from '../common'; import { DEFAULT_RESOURCE_EXPORT_OPTS, ResourceExportOptions, IndexedResource } from './resource.types'; import ReadResourceFlow from './flows/read-resource.flow'; import ResourcesListFlow from './flows/resources-list.flow'; @@ -34,14 +33,11 @@ import UnsubscribeResourceFlow from './flows/unsubscribe-resource.flow'; import type { ServerCapabilities } from '@frontmcp/protocol'; import { NameDisambiguationError, EntryValidationError } from '../errors'; -export default class ResourceRegistry - extends RegistryAbstract< - ResourceInstance, // instances map holds ResourceInstance - ResourceRecord | ResourceTemplateRecord, - ResourceType[] - > - implements ResourceRegistryInterface -{ +export default class ResourceRegistry extends RegistryAbstract< + ResourceInstance, // instances map holds ResourceInstance + ResourceRecord | ResourceTemplateRecord, + ResourceType[] +> { /** Who owns this registry (used for provenance). */ owner: EntryOwnerRef; @@ -197,7 +193,7 @@ export default class ResourceRegistry * Remote apps expose resources via proxy entries that forward execution to the remote server. * This also subscribes to updates from the remote app's registry for lazy-loaded resources. */ - private adoptResourcesFromRemoteApp(app: AppEntry, scope: Scope): void { + private adoptResourcesFromRemoteApp(app: AppEntry, scope: ScopeEntry): void { const remoteRegistry = app.resources as ResourceRegistry; // Helper to adopt/re-adopt resources from the remote app diff --git a/libs/sdk/src/scope/flows/http.request.flow.ts b/libs/sdk/src/scope/flows/http.request.flow.ts index 8d8792c2a..2b3633eff 100644 --- a/libs/sdk/src/scope/flows/http.request.flow.ts +++ b/libs/sdk/src/scope/flows/http.request.flow.ts @@ -23,7 +23,6 @@ import { z } from 'zod'; import { sessionVerifyOutputSchema } from '../../auth/flows/session.verify.flow'; import { randomUUID } from '@frontmcp/utils'; import { SessionVerificationFailedError } from '../../errors'; -import type { Scope } from '../scope.instance'; const plan = { pre: [ @@ -158,7 +157,7 @@ export default class HttpRequestFlow extends FlowBase { @Stage('acquireQuota') async acquireQuota() { - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager?.config?.global) return; const context = this.tryGetContext(); @@ -526,7 +525,8 @@ export default class HttpRequestFlow extends FlowBase { // session persists, allowing recreation on other nodes in distributed mode. const authorization = request[ServerRequestTokens.auth] as Authorization | undefined; if (authorization?.token) { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) return; for (const protocol of ['streamable-http', 'sse'] as const) { try { await transportService.destroyTransporter(protocol, authorization.token, sessionId); diff --git a/libs/sdk/src/scope/scope.instance.ts b/libs/sdk/src/scope/scope.instance.ts index fb151852a..0fafadfcc 100644 --- a/libs/sdk/src/scope/scope.instance.ts +++ b/libs/sdk/src/scope/scope.instance.ts @@ -8,7 +8,6 @@ import { FrontMcpAuth, FrontMcpLogger, FrontMcpServer, - HookRegistryInterface, ProviderScope, ScopeEntry, ScopeRecord, @@ -43,7 +42,6 @@ import CallAgentFlow from '../agent/flows/call-agent.flow'; import PluginRegistry, { PluginScopeInfo } from '../plugin/plugin.registry'; import { ElicitationStore, createElicitationStore } from '../elicitation'; import { ElicitationRequestFlow, ElicitationResultFlow } from '../elicitation/flows'; -import { ElicitationStoreNotInitializedError } from '../errors/elicitation.error'; import { SendElicitationResultTool } from '../elicitation/send-elicitation-result.tool'; import { normalizeTool } from '../tool/tool.utils'; import { ToolInstance } from '../tool/tool.instance'; @@ -597,7 +595,7 @@ export class Scope extends ScopeEntry { return this.scopeAuth.getPrimary(); } - get hooks(): HookRegistryInterface { + get hooks(): HookRegistry { return this.scopeHooks; } @@ -678,10 +676,7 @@ export class Scope extends ScopeEntry { * * @see createElicitationStore for factory implementation details */ - get elicitationStore(): ElicitationStore { - if (!this._elicitationStore) { - throw new ElicitationStoreNotInitializedError(); - } + get elicitationStore(): ElicitationStore | undefined { return this._elicitationStore; } diff --git a/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts b/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts index 4ba1afb43..6a22b9e13 100644 --- a/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts +++ b/libs/sdk/src/skill/__tests__/memory-skill.provider.spec.ts @@ -7,7 +7,7 @@ import { MemorySkillProvider } from '../providers/memory-skill.provider'; import { SkillToolValidator } from '../skill-validator'; import { SkillContent } from '../../common/interfaces'; -import { ToolRegistryInterface } from '../../common/interfaces/internal'; +import type ToolRegistry from '../../tool/tool.registry'; import { ToolEntry } from '../../common'; // Helper to create test skills @@ -23,7 +23,7 @@ const createTestSkill = ( }); // Mock tool registry -const createMockToolRegistry = (tools: string[]): ToolRegistryInterface => +const createMockToolRegistry = (tools: string[]): ToolRegistry => ({ getTools: () => tools.map( @@ -41,7 +41,7 @@ const createMockToolRegistry = (tools: string[]): ToolRegistryInterface => getCapabilities: jest.fn(), getInlineTools: jest.fn(), owner: { kind: 'scope', id: 'test', ref: {} }, - }) as unknown as ToolRegistryInterface; + }) as unknown as ToolRegistry; describe('MemorySkillProvider', () => { let provider: MemorySkillProvider; diff --git a/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts b/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts index 463480f06..ea44d9537 100644 --- a/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts +++ b/libs/sdk/src/skill/__tests__/skill-http.utils.spec.ts @@ -62,7 +62,7 @@ function createMockSkillContent(overrides: Partial = {}): SkillCon }; } -// Mock ToolRegistryInterface for testing +// Mock ToolRegistry for testing function createMockToolRegistry( tools: Array<{ name: string; diff --git a/libs/sdk/src/skill/__tests__/skill-validator.spec.ts b/libs/sdk/src/skill/__tests__/skill-validator.spec.ts index cc715a782..7056e6790 100644 --- a/libs/sdk/src/skill/__tests__/skill-validator.spec.ts +++ b/libs/sdk/src/skill/__tests__/skill-validator.spec.ts @@ -5,7 +5,7 @@ */ import { SkillToolValidator, ToolValidationResult } from '../skill-validator'; -import { ToolRegistryInterface } from '../../common/interfaces/internal'; +import type ToolRegistry from '../../tool/tool.registry'; import { ToolEntry } from '../../common'; // Mock tool entries for testing @@ -23,7 +23,7 @@ const createMockTool = (name: string, hidden = false): ToolEntry => }) as unknown as ToolEntry; // Create a mock tool registry -const createMockToolRegistry = (visibleTools: string[], hiddenTools: string[] = []): ToolRegistryInterface => { +const createMockToolRegistry = (visibleTools: string[], hiddenTools: string[] = []): ToolRegistry => { const allToolList: ToolEntry[] = [ ...visibleTools.map((name) => createMockTool(name, false)), ...hiddenTools.map((name) => createMockTool(name, true)), @@ -41,7 +41,7 @@ const createMockToolRegistry = (visibleTools: string[], hiddenTools: string[] = getCapabilities: jest.fn(), getInlineTools: jest.fn(), owner: { kind: 'scope', id: 'test', ref: {} }, - } as unknown as ToolRegistryInterface; + } as unknown as ToolRegistry; }; describe('skill-validator', () => { diff --git a/libs/sdk/src/skill/flows/http/skills-api.flow.ts b/libs/sdk/src/skill/flows/http/skills-api.flow.ts index a4235be62..a3c1d6a19 100644 --- a/libs/sdk/src/skill/flows/http/skills-api.flow.ts +++ b/libs/sdk/src/skill/flows/http/skills-api.flow.ts @@ -18,8 +18,8 @@ import { FlowHooksOf, normalizeEntryPrefix, normalizeScopeBase, - ToolRegistryInterface, } from '../../../common'; +import type ToolRegistry from '../../../tool/tool.registry'; import { z } from 'zod'; import { skillToApiResponse, formatSkillForLLMWithSchemas } from '../../skill-http.utils'; import { formatSkillForLLM } from '../../skill.utils'; @@ -258,7 +258,7 @@ export default class SkillsApiFlow extends FlowBase { private async handleGetSkill( skillId: string, skillRegistry: SkillRegistryInterface, - toolRegistry: ToolRegistryInterface | null, + toolRegistry: ToolRegistry | null, ) { const loadResult = await skillRegistry.loadSkill(skillId); diff --git a/libs/sdk/src/skill/flows/load-skill.flow.ts b/libs/sdk/src/skill/flows/load-skill.flow.ts index 8fa9f63db..e105ff4d3 100644 --- a/libs/sdk/src/skill/flows/load-skill.flow.ts +++ b/libs/sdk/src/skill/flows/load-skill.flow.ts @@ -8,7 +8,6 @@ import { formatSkillForLLMWithSchemas } from '../skill-http.utils'; import type { SkillLoadResult } from '../skill-storage.interface'; import type { SkillSessionManager } from '../session/skill-session.manager'; import type { SkillPolicyMode, SkillActivationResult } from '../session/skill-session.types'; -import type { Scope } from '../../scope'; // Input schema matching MCP request format - supports multiple skill IDs const inputSchema = z.object({ @@ -273,7 +272,7 @@ export default class LoadSkillFlow extends FlowBase { return; } - const toolRegistry = (this.scope as Scope).tools; + const toolRegistry = this.scope.tools; const skillResults: z.infer[] = []; let totalTools = 0; let allToolsAvailable = true; diff --git a/libs/sdk/src/skill/skill-http.utils.ts b/libs/sdk/src/skill/skill-http.utils.ts index 79ebf1f96..674b07dab 100644 --- a/libs/sdk/src/skill/skill-http.utils.ts +++ b/libs/sdk/src/skill/skill-http.utils.ts @@ -9,7 +9,8 @@ * - /skills API - JSON responses */ -import type { SkillContent, SkillEntry, ToolRegistryInterface, ToolEntry } from '../common'; +import type { SkillContent, SkillEntry, ToolEntry } from '../common'; +import type ToolRegistry from '../tool/tool.registry'; import type { SkillVisibility, SkillResources } from '../common/metadata/skill.metadata'; import type { SkillRegistryInterface as SkillRegistryInterfaceType } from './skill.registry'; @@ -91,7 +92,7 @@ export function formatSkillsForLlmCompact(skills: SkillEntry[]): string { */ export async function formatSkillsForLlmFull( registry: SkillRegistryInterfaceType, - toolRegistry: ToolRegistryInterface, + toolRegistry: ToolRegistry, visibility: SkillVisibility = 'both', ): Promise { const skills = registry.getSkills(false); // Don't include hidden @@ -128,7 +129,7 @@ export function formatSkillForLLMWithSchemas( skill: SkillContent, availableTools: string[], missingTools: string[], - toolRegistry: ToolRegistryInterface, + toolRegistry: ToolRegistry, ): string { const parts: string[] = []; diff --git a/libs/sdk/src/skill/skill-storage.factory.ts b/libs/sdk/src/skill/skill-storage.factory.ts index 3b2abb8a5..341684d51 100644 --- a/libs/sdk/src/skill/skill-storage.factory.ts +++ b/libs/sdk/src/skill/skill-storage.factory.ts @@ -10,7 +10,7 @@ */ import type { FrontMcpLogger } from '../common'; -import type { ToolRegistryInterface } from '../common/interfaces/internal'; +import type ToolRegistry from '../tool/tool.registry'; import type { SkillStorageProvider, SkillStorageProviderType } from './skill-storage.interface'; import { SkillToolValidator } from './skill-validator'; import { MemorySkillProvider, MemorySkillProviderOptions } from './providers/memory-skill.provider'; @@ -127,7 +127,7 @@ export interface SkillStorageFactoryOptions { * Tool registry for validating tool references. * Required for tool validation in search results. */ - toolRegistry?: ToolRegistryInterface; + toolRegistry?: ToolRegistry; /** * Logger instance. @@ -291,7 +291,7 @@ export function createSkillStorageProvider( */ export function createMemorySkillProvider( options: { - toolRegistry?: ToolRegistryInterface; + toolRegistry?: ToolRegistry; defaultTopK?: number; defaultMinScore?: number; logger?: FrontMcpLogger; diff --git a/libs/sdk/src/skill/skill-validator.ts b/libs/sdk/src/skill/skill-validator.ts index 0beb5c99c..7749316ee 100644 --- a/libs/sdk/src/skill/skill-validator.ts +++ b/libs/sdk/src/skill/skill-validator.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/skill/skill-validator.ts -import { ToolRegistryInterface } from '../common/interfaces/internal'; +import type ToolRegistry from '../tool/tool.registry'; /** * Result of validating tool availability for a skill. @@ -34,9 +34,9 @@ export interface ToolValidationResult { * in the current scope. It categorizes tools as available, missing, or hidden. */ export class SkillToolValidator { - private readonly toolRegistry: ToolRegistryInterface; + private readonly toolRegistry: ToolRegistry; - constructor(toolRegistry: ToolRegistryInterface) { + constructor(toolRegistry: ToolRegistry) { this.toolRegistry = toolRegistry; } diff --git a/libs/sdk/src/skill/skill.instance.ts b/libs/sdk/src/skill/skill.instance.ts index d1c3535db..c1a89eee7 100644 --- a/libs/sdk/src/skill/skill.instance.ts +++ b/libs/sdk/src/skill/skill.instance.ts @@ -4,7 +4,7 @@ import { EntryOwnerRef, SkillEntry, SkillKind, SkillRecord, SkillToolRef, normal import { SkillContent } from '../common/interfaces'; import { SkillVisibility } from '../common/metadata/skill.metadata'; import ProviderRegistry from '../provider/provider.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { loadInstructions, buildSkillContent } from './skill.utils'; /** @@ -33,7 +33,7 @@ export class SkillInstance extends SkillEntry { private readonly providersRef: ProviderRegistry; /** The scope this skill operates in */ - readonly scope: Scope; + readonly scope: ScopeEntry; /** Cached instructions (loaded lazily) */ private cachedInstructions?: string; diff --git a/libs/sdk/src/skill/skill.registry.ts b/libs/sdk/src/skill/skill.registry.ts index beb37b66d..d6d4cec0a 100644 --- a/libs/sdk/src/skill/skill.registry.ts +++ b/libs/sdk/src/skill/skill.registry.ts @@ -1,7 +1,7 @@ // file: libs/sdk/src/skill/skill.registry.ts import { Token, tokenName } from '@frontmcp/di'; -import { EntryLineage, EntryOwnerRef, SkillEntry, SkillType, SkillToolValidationMode } from '../common'; +import { EntryLineage, EntryOwnerRef, ScopeEntry, SkillEntry, SkillType, SkillToolValidationMode } from '../common'; import { SkillContent } from '../common/interfaces'; import { SkillChangeEvent, SkillEmitter } from './skill.events'; import { SkillInstance, createSkillInstance } from './skill.instance'; @@ -9,7 +9,6 @@ import { normalizeSkill, skillDiscoveryDeps } from './skill.utils'; import { SkillRecord } from '../common/records'; import ProviderRegistry from '../provider/provider.registry'; import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; -import { Scope } from '../scope'; import { SkillStorageProvider, SkillSearchOptions, @@ -221,7 +220,7 @@ export default class SkillRegistry private toolValidator?: SkillToolValidator; /** The scope this registry operates in */ - readonly scope: Scope; + readonly scope: ScopeEntry; /** Registry-level options for validation behavior */ private readonly options: SkillRegistryOptions; diff --git a/libs/sdk/src/skill/tools/load-skills.tool.ts b/libs/sdk/src/skill/tools/load-skills.tool.ts index 69d28f1a0..2534ebd68 100644 --- a/libs/sdk/src/skill/tools/load-skills.tool.ts +++ b/libs/sdk/src/skill/tools/load-skills.tool.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { Tool, ToolContext } from '../../common'; import { formatSkillForLLM, generateNextSteps } from '../skill.utils'; import { formatSkillForLLMWithSchemas } from '../skill-http.utils'; -import type { ToolRegistryInterface } from '../../common'; +import type ToolRegistry from '../../tool/tool.registry'; /** * Input schema for loadSkills tool. @@ -143,7 +143,7 @@ export class LoadSkillsTool extends ToolContext { async acquireQuota() { this.logger.verbose('acquireQuota:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.toolContext?.mark('acquireQuota'); this.logger.verbose('acquireQuota:done (no rate limit manager)'); @@ -501,7 +502,7 @@ export default class CallToolFlow extends FlowBase { async acquireSemaphore() { this.logger.verbose('acquireSemaphore:start'); - const manager = (this.scope as Scope).rateLimitManager; + const manager = this.scope.rateLimitManager; if (!manager) { this.state.toolContext?.mark('acquireSemaphore'); this.logger.verbose('acquireSemaphore:done (no rate limit manager)'); @@ -573,7 +574,7 @@ export default class CallToolFlow extends FlowBase { const { tool } = this.state.required; const timeoutMs = - tool.metadata.timeout?.executeMs ?? (this.scope as Scope).rateLimitManager?.config?.defaultTimeout?.executeMs; + tool.metadata.timeout?.executeMs ?? this.scope.rateLimitManager?.config?.defaultTimeout?.executeMs; try { const doExecute = async () => { @@ -607,9 +608,12 @@ export default class CallToolFlow extends FlowBase { const authInfo = this.state.authInfo; const authInfoWithTransport = authInfo as (AuthInfo & TransportExtension) | undefined; const sessionId = authInfo?.sessionId ?? 'anonymous'; - const scope = this.scope as Scope; + const store = this.scope.elicitationStore; + if (!store) { + throw new InternalMcpError('Elicitation store not initialized'); + } - await scope.elicitationStore.setPendingFallback({ + await store.setPendingFallback({ elicitId: error.elicitId, sessionId, toolName: error.toolName, @@ -627,14 +631,14 @@ export default class CallToolFlow extends FlowBase { // 1. Transport is streamable-http (supports keeping connection open) // 2. Notifications can be delivered (session is registered in NotificationService) // Some LLMs don't support MCP notifications, so we need to fall back to fire-and-forget - if (transportType === 'streamable-http' && canDeliverNotifications(scope, sessionId)) { + if (transportType === 'streamable-http' && canDeliverNotifications(this.scope as Scope, sessionId)) { // Waiting mode: Send notification + wait for pub/sub result // This keeps the request open and returns the actual tool result // when sendElicitationResult is called this.logger.info('execute: using waiting fallback for streamable-http', { elicitId: error.elicitId, }); - const deps: FallbackHandlerDeps = { scope, sessionId, logger: this.logger }; + const deps: FallbackHandlerDeps = { scope: this.scope as Scope, sessionId, logger: this.logger }; const result = await handleWaitingFallback(deps, error); toolContext.output = result; this.logger.verbose('execute:done (elicitation waiting fallback)'); @@ -745,8 +749,7 @@ export default class CallToolFlow extends FlowBase { } try { - // Cast scope to Scope to access toolUI and notifications - const scope = this.scope as Scope; + const scope = this.scope; // Get session info for platform detection from authInfo (already in state from parseInput) const { authInfo } = this.state; @@ -786,6 +789,11 @@ export default class CallToolFlow extends FlowBase { let htmlContent: string | undefined; let uiMeta: Record = {}; + if (!scope.toolUI) { + this.logger.verbose('applyUI: toolUI not available, skipping UI rendering'); + return; + } + if (servingMode === 'static') { // For static mode: no additional rendering needed // Widget was already registered at server startup diff --git a/libs/sdk/src/tool/flows/tools-list.flow.ts b/libs/sdk/src/tool/flows/tools-list.flow.ts index 3e00136c4..9af5642d9 100644 --- a/libs/sdk/src/tool/flows/tools-list.flow.ts +++ b/libs/sdk/src/tool/flows/tools-list.flow.ts @@ -176,15 +176,12 @@ export default class ToolsListFlow extends FlowBase { const sessionId = authInfo.sessionId; - // Cast scope to access notifications service for platform detection - const scope = this.scope as Scope; - // Get platform type: first check sessionIdPayload (detected from user-agent), // then fall back to notification service (detected from MCP clientInfo), // finally default to 'unknown' const platformType: AIPlatformType = authInfo.sessionIdPayload?.platformType ?? - (sessionId ? scope.notifications?.getPlatformType(sessionId) : undefined) ?? + (sessionId ? this.scope.notifications?.getPlatformType(sessionId) : undefined) ?? 'unknown'; this.logger.verbose(`parseInput: detected platform=${platformType}`); @@ -340,9 +337,8 @@ export default class ToolsListFlow extends FlowBase { const allResolved = this.state.required.resolvedTools; const platformType = this.state.platformType ?? 'unknown'; - // Get pagination config from scope - const scope = this.scope as Scope; - const paginationConfig = scope.pagination?.tools; + // Get pagination config from scope metadata + const paginationConfig = this.scope.metadata.pagination?.tools; // Determine if pagination should apply const usePagination = this.shouldPaginate(allResolved.length, paginationConfig); diff --git a/libs/sdk/src/tool/tool.instance.ts b/libs/sdk/src/tool/tool.instance.ts index f1f30a4fa..8a1a5d185 100644 --- a/libs/sdk/src/tool/tool.instance.ts +++ b/libs/sdk/src/tool/tool.instance.ts @@ -20,7 +20,7 @@ import { import ProviderRegistry from '../provider/provider.registry'; import { z } from 'zod'; import HookRegistry from '../hooks/hook.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import { normalizeHooksFromCls } from '../hooks/hooks.utils'; import type { CallToolRequest } from '@frontmcp/protocol'; import { buildParsedToolResult } from './tool.utils'; @@ -45,7 +45,7 @@ export class ToolInstance< /** The provider registry this tool is bound to (captured at construction) */ private readonly _providers: ProviderRegistry; /** The scope this tool operates in (captured at construction from providers) */ - readonly scope: Scope; + readonly scope: ScopeEntry; /** The hook registry for this tool's scope (captured at construction) */ readonly hooks: HookRegistry; @@ -56,7 +56,7 @@ export class ToolInstance< this.name = record.metadata.id || record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; // inputSchema is always a ZodRawShape this.inputSchema = (record.metadata.inputSchema ?? {}) as InSchema; diff --git a/libs/sdk/src/tool/tool.registry.ts b/libs/sdk/src/tool/tool.registry.ts index bdf236646..f0ce8b6b6 100644 --- a/libs/sdk/src/tool/tool.registry.ts +++ b/libs/sdk/src/tool/tool.registry.ts @@ -1,5 +1,5 @@ import { Token, tokenName, getMetadata } from '@frontmcp/di'; -import { EntryLineage, EntryOwnerRef, ToolEntry, ToolRecord, ToolRegistryInterface, ToolType } from '../common'; +import { AppEntry, EntryLineage, EntryOwnerRef, ScopeEntry, ToolEntry, ToolRecord, ToolType } from '../common'; import { ToolChangeEvent, ToolEmitter } from './tool.events'; import ProviderRegistry from '../provider/provider.registry'; import { ensureMaxLen, sepFor } from '@frontmcp/utils'; @@ -12,8 +12,6 @@ import { DEFAULT_EXPORT_OPTS, ExportNameOptions, IndexedTool } from './tool.type import ToolsListFlow from './flows/tools-list.flow'; import CallToolFlow from './flows/call-tool.flow'; import { ServerCapabilities } from '@frontmcp/protocol'; -import { Scope } from '../scope'; -import { AppEntry } from '../common'; import { isSendElicitationResultTool } from '../elicitation/send-elicitation-result.tool'; import { NameDisambiguationError, @@ -22,14 +20,11 @@ import { RegistryGraphEntryNotFoundError, } from '../errors'; -export default class ToolRegistry - extends RegistryAbstract< - ToolInstance, // IMPORTANT: instances map holds ToolInstance (not the interface) - ToolRecord, - ToolType[] - > - implements ToolRegistryInterface -{ +export default class ToolRegistry extends RegistryAbstract< + ToolInstance, // IMPORTANT: instances map holds ToolInstance (not the interface) + ToolRecord, + ToolType[] +> { /** Who owns this registry (used for provenance). Optional. */ owner: EntryOwnerRef; @@ -193,7 +188,7 @@ export default class ToolRegistry * Remote apps expose tools via proxy entries that forward execution to the remote server. * This also subscribes to updates from the remote app's registry for lazy-loaded tools. */ - private adoptToolsFromRemoteApp(app: AppEntry, scope: Scope): void { + private adoptToolsFromRemoteApp(app: AppEntry, scope: ScopeEntry): void { // Validate that app.tools has the expected interface before casting // Remote apps may have different registry implementations if (!app.tools || typeof app.tools.getTools !== 'function') { diff --git a/libs/sdk/src/transport/adapters/transport.local.adapter.ts b/libs/sdk/src/transport/adapters/transport.local.adapter.ts index fa6c5d0dd..759999b09 100644 --- a/libs/sdk/src/transport/adapters/transport.local.adapter.ts +++ b/libs/sdk/src/transport/adapters/transport.local.adapter.ts @@ -290,10 +290,22 @@ export abstract class LocalTransportAdapter { * Get the elicitation store for distributed elicitation support. * Uses Redis in distributed mode, in-memory for single-node. */ - protected get elicitStore(): ElicitationStore { + protected get elicitStore(): ElicitationStore | undefined { return this.scope.elicitationStore; } + /** + * Get the elicitation store, throwing if not initialized. + * Use in contexts where elicitation is required (sendElicitRequest, cancelPendingElicit). + */ + protected requireElicitStore(): ElicitationStore { + const store = this.elicitStore; + if (!store) { + throw new Error('Elicitation store not initialized'); + } + return store; + } + /** * Cancel any pending elicitation request. * Called before sending a new elicit to enforce single-elicit-per-session. @@ -315,9 +327,12 @@ export abstract class LocalTransportAdapter { // Publish cancel to store for distributed mode (non-atomic, intentional) // In distributed mode, another node may have already processed this elicitation const sessionId = this.key.sessionId; - const pending = await this.elicitStore.getPending(sessionId); - if (pending) { - await this.elicitStore.publishResult(pending.elicitId, sessionId, { status: 'cancel' }); + const store = this.elicitStore; + if (store) { + const pending = await store.getPending(sessionId); + if (pending) { + await store.publishResult(pending.elicitId, sessionId, { status: 'cancel' }); + } } this.pendingElicit = undefined; diff --git a/libs/sdk/src/transport/adapters/transport.sse.adapter.ts b/libs/sdk/src/transport/adapters/transport.sse.adapter.ts index 2dc94a1a5..811f6e2db 100644 --- a/libs/sdk/src/transport/adapters/transport.sse.adapter.ts +++ b/libs/sdk/src/transport/adapters/transport.sse.adapter.ts @@ -121,7 +121,8 @@ export class TransportSSEAdapter extends LocalTransportAdapter ? O : unknown>( elicitId, (result) => { @@ -203,7 +204,7 @@ export class TransportSSEAdapter extends LocalTransportAdapter ? O : unknown>( elicitId, (result) => { @@ -261,7 +262,7 @@ export class TransportStreamableHttpAdapter extends LocalTransportAdapter { session = createSessionId('legacy-sse', token, { userAgent: request.headers?.['user-agent'] as string | undefined, - platformDetectionConfig: (this.scope as Scope).metadata.transport?.platformDetection, + platformDetectionConfig: this.scope.metadata.transport?.platformDetection, skillsOnlyMode, }); } @@ -156,10 +156,9 @@ export default class HandleSseFlow extends FlowBase { @Stage('router') async router() { const { request } = this.rawInput; - const scope = this.scope as Scope; const requestPath = normalizeEntryPrefix(request.path); - const prefix = normalizeEntryPrefix(scope.entryPath); - const scopePath = normalizeScopeBase(scope.routeBase); + const prefix = normalizeEntryPrefix(this.scope.entryPath); + const scopePath = normalizeScopeBase(this.scope.routeBase); const basePath = `${prefix}${scopePath}`; if (requestPath === `${basePath}/sse`) { @@ -173,7 +172,10 @@ export default class HandleSseFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'initialize', }) async onInitialize() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } const { request, response } = this.rawInput; const { token, session } = this.state.required; @@ -199,7 +201,10 @@ export default class HandleSseFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'message', }) async onMessage() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } const logger = this.scopeLogger.child('handle:legacy-sse:onMessage'); const { request, response } = this.rawInput; diff --git a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts index 506390e5f..04ebb93a6 100644 --- a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts @@ -55,7 +55,7 @@ export default class HandleStatelessHttpFlow extends FlowBase { @Stage('parseInput') async parseInput() { const { request } = this.rawInput; - const logger = (this.scope as Scope).logger.child('HandleStatelessHttpFlow'); + const logger = this.scope.logger.child('HandleStatelessHttpFlow'); // Check if we have auth info const auth = request[ServerRequestTokens.auth] as Authorization | undefined; @@ -75,7 +75,7 @@ export default class HandleStatelessHttpFlow extends FlowBase { @Stage('router') async router() { const { request } = this.rawInput; - const logger = (this.scope as Scope).logger.child('HandleStatelessHttpFlow'); + const logger = this.scope.logger.child('HandleStatelessHttpFlow'); const body = request.body as { method?: string } | undefined; const method = body?.method; @@ -95,8 +95,11 @@ export default class HandleStatelessHttpFlow extends FlowBase { @Stage('handleRequest') async handleRequest() { - const transportService = (this.scope as Scope).transportService; - const logger = (this.scope as Scope).logger.child('HandleStatelessHttpFlow'); + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } + const logger = this.scope.logger.child('HandleStatelessHttpFlow'); const { request, response } = this.rawInput; const { token, isAuthenticated, requestType } = this.state; diff --git a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts index 01dec2564..b005d1331 100644 --- a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts @@ -17,7 +17,6 @@ import { InternalMcpError } from '../../errors'; import { z } from 'zod'; import { ElicitResultSchema, RequestSchema, CallToolResultSchema } from '@frontmcp/protocol'; import type { StoredSession } from '@frontmcp/auth'; -import { Scope } from '../../scope'; import { createSessionId } from '../../auth/session/utils/session-id.utils'; import { detectSkillsOnlyMode } from '../../skill/skill-mode.utils'; import { createExtAppsMessageHandler, type ExtAppsJsonRpcRequest, type ExtAppsHostCapabilities } from '../../ext-apps'; @@ -246,7 +245,7 @@ export default class HandleStreamableHttpFlow extends FlowBase { return createSessionId('streamable-http', token, { userAgent: request.headers?.['user-agent'] as string | undefined, - platformDetectionConfig: (this.scope as Scope).metadata.transport?.platformDetection, + platformDetectionConfig: this.scope.metadata.transport?.platformDetection, skillsOnlyMode, }); }, @@ -302,8 +301,11 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'initialize', }) async onInitialize() { - const transportService = (this.scope as Scope).transportService; - const logger = (this.scope as Scope).logger.child('handle:streamable-http:onInitialize'); + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } + const logger = this.scope.logger.child('handle:streamable-http:onInitialize'); const { request, response } = this.rawInput; const { token, session } = this.state.required; @@ -357,7 +359,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'elicitResult', }) async onElicitResult() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } const logger = this.scopeLogger.child('handle:streamable-http:onElicitResult'); const { request, response } = this.rawInput; @@ -426,7 +431,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'message', }) async onMessage() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } const logger = this.scopeLogger.child('handle:streamable-http:onMessage'); const { request, response } = this.rawInput; @@ -507,7 +515,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'sseListener', }) async onSseListener() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } const logger = this.scopeLogger.child('handle:streamable-http:onSseListener'); const { request, response } = this.rawInput; @@ -557,7 +568,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { filter: ({ state: { requestType } }) => requestType === 'extApps', }) async onExtApps() { - const transportService = (this.scope as Scope).transportService; + const transportService = this.scope.transportService; + if (!transportService) { + throw new Error('Transport service not available'); + } const logger = this.scopeLogger.child('handle:streamable-http:onExtApps'); const { request, response } = this.rawInput; @@ -618,10 +632,9 @@ export default class HandleStreamableHttpFlow extends FlowBase { } // 4. Create ExtAppsMessageHandler with session context - const scope = this.scope as Scope; // Get host capabilities from scope metadata, with defaults - const configuredCapabilities = scope.metadata.extApps?.hostCapabilities; + const configuredCapabilities = this.scope.metadata.extApps?.hostCapabilities; const hostCapabilities: ExtAppsHostCapabilities = { serverToolProxy: configuredCapabilities?.serverToolProxy ?? true, logging: configuredCapabilities?.logging ?? true, @@ -631,10 +644,10 @@ export default class HandleStreamableHttpFlow extends FlowBase { const handler = createExtAppsMessageHandler({ context: { sessionId: session.id, - logger: scope.logger, + logger: this.scope.logger, callTool: async (name, args) => { // Route through CallToolFlow with session's authInfo - const result = await scope.runFlow('tools:call-tool', { + const result = await this.scope.runFlow('tools:call-tool', { request: { method: 'tools/call', params: { name, arguments: args } }, ctx: { authInfo: { diff --git a/libs/sdk/src/workflow/workflow.instance.ts b/libs/sdk/src/workflow/workflow.instance.ts index 7b380776b..86739d335 100644 --- a/libs/sdk/src/workflow/workflow.instance.ts +++ b/libs/sdk/src/workflow/workflow.instance.ts @@ -3,7 +3,7 @@ import { WorkflowEntry } from '../common/entries/workflow.entry'; import { WorkflowMetadata } from '../common/metadata/workflow.metadata'; import { WorkflowRecord } from '../common/records/workflow.record'; import ProviderRegistry from '../provider/provider.registry'; -import { Scope } from '../scope'; +import { ScopeEntry } from '../common'; import HookRegistry from '../hooks/hook.registry'; /** @@ -11,7 +11,7 @@ import HookRegistry from '../hooks/hook.registry'; */ export class WorkflowInstance extends WorkflowEntry { private readonly _providers: ProviderRegistry; - readonly scope: Scope; + readonly scope: ScopeEntry; readonly hooks: HookRegistry; constructor(record: WorkflowRecord, providers: ProviderRegistry, owner: EntryOwnerRef) { @@ -21,7 +21,7 @@ export class WorkflowInstance extends WorkflowEntry { this.name = record.metadata.id || record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this._providers.getActiveScope(); - this.hooks = this.scope.providers.getHooksRegistry(); + this.hooks = this.scope.hooks; this.ready = this.initialize(); } From 5c29be55c1e60aa9f7509d42dd59b2595316f8f7 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Fri, 27 Mar 2026 03:54:17 +0300 Subject: [PATCH 11/12] refactor: remove unused imports and improve error handling for transport service --- .../src/__test-utils__/fixtures/plugin.fixtures.ts | 1 - libs/sdk/src/agent/agent.registry.ts | 1 - libs/sdk/src/agent/agent.scope.ts | 8 ++++++++ libs/sdk/src/agent/flows/call-agent.flow.ts | 1 - libs/sdk/src/common/entries/scope.entry.ts | 6 ++++++ libs/sdk/src/common/interfaces/app.interface.ts | 4 +--- .../sdk/src/common/interfaces/provider.interface.ts | 3 +-- .../elicitation/flows/elicitation-request.flow.ts | 5 ++--- .../elicitation/flows/elicitation-result.flow.ts | 1 - libs/sdk/src/elicitation/helpers/fallback.helper.ts | 10 ++++++++-- .../src/elicitation/send-elicitation-result.tool.ts | 3 +-- libs/sdk/src/errors/index.ts | 1 + libs/sdk/src/errors/transport.errors.ts | 9 +++++++++ .../src/plugin/__tests__/plugin.registry.spec.ts | 2 +- libs/sdk/src/provider/provider.registry.ts | 1 - libs/sdk/src/resource/flows/read-resource.flow.ts | 1 - libs/sdk/src/scope/flows/http.request.flow.ts | 13 +++++++------ libs/sdk/src/transport/flows/handle.sse.flow.ts | 6 +++--- .../transport/flows/handle.stateless-http.flow.ts | 4 ++-- .../transport/flows/handle.streamable-http.flow.ts | 12 ++++++------ 20 files changed, 56 insertions(+), 36 deletions(-) diff --git a/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts b/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts index 29eea2fca..bf71c8299 100644 --- a/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts +++ b/libs/sdk/src/__test-utils__/fixtures/plugin.fixtures.ts @@ -5,7 +5,6 @@ import { PluginMetadata } from '../../common/metadata'; import { createProviderMetadata } from './provider.fixtures'; -import { createToolMetadata } from './tool.fixtures'; /** * Creates a simple plugin metadata object diff --git a/libs/sdk/src/agent/agent.registry.ts b/libs/sdk/src/agent/agent.registry.ts index a0f609eb0..c2970ecf2 100644 --- a/libs/sdk/src/agent/agent.registry.ts +++ b/libs/sdk/src/agent/agent.registry.ts @@ -9,7 +9,6 @@ import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; import { AgentInstance } from './agent.instance'; import type { Tool, ServerCapabilities } from '@frontmcp/protocol'; import { DependencyNotFoundError } from '../errors/mcp.error'; -import ToolRegistry from '../tool/tool.registry'; // ============================================================================ // Types diff --git a/libs/sdk/src/agent/agent.scope.ts b/libs/sdk/src/agent/agent.scope.ts index 71129f9bd..1cc99842e 100644 --- a/libs/sdk/src/agent/agent.scope.ts +++ b/libs/sdk/src/agent/agent.scope.ts @@ -400,4 +400,12 @@ class AgentScopeEntry { ): Promise | undefined> { return this.agentScope.runFlow(name, input, deps); } + + runFlowForOutput( + name: Name, + input: FlowInputOf, + deps?: Map, + ): Promise> { + return this.agentScope.runFlowForOutput(name, input, deps); + } } diff --git a/libs/sdk/src/agent/flows/call-agent.flow.ts b/libs/sdk/src/agent/flows/call-agent.flow.ts index 3a971af79..7a8e2393e 100644 --- a/libs/sdk/src/agent/flows/call-agent.flow.ts +++ b/libs/sdk/src/agent/flows/call-agent.flow.ts @@ -13,7 +13,6 @@ import { AgentExecutionError, RateLimitError, } from '../../errors'; -import { Scope } from '../../scope'; import { ExecutionTimeoutError, ConcurrencyLimitError, withTimeout, type SemaphoreTicket } from '@frontmcp/guard'; // ============================================================================ diff --git a/libs/sdk/src/common/entries/scope.entry.ts b/libs/sdk/src/common/entries/scope.entry.ts index 0f69a50e5..955bdd4cc 100644 --- a/libs/sdk/src/common/entries/scope.entry.ts +++ b/libs/sdk/src/common/entries/scope.entry.ts @@ -74,4 +74,10 @@ export abstract class ScopeEntry extends BaseEntry, additionalDeps?: Map, ): Promise | undefined>; + + abstract runFlowForOutput( + name: Name, + input: FlowInputOf, + additionalDeps?: Map, + ): Promise>; } diff --git a/libs/sdk/src/common/interfaces/app.interface.ts b/libs/sdk/src/common/interfaces/app.interface.ts index d12f624f8..07ead69fa 100644 --- a/libs/sdk/src/common/interfaces/app.interface.ts +++ b/libs/sdk/src/common/interfaces/app.interface.ts @@ -3,6 +3,4 @@ import { AppMetadata, RemoteAppMetadata } from '../metadata'; export type AppValueType = ValueType & AppMetadata; -// Using 'any' default to allow broad compatibility with untyped app classes -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AppType = Type | AppValueType | RemoteAppMetadata; +export type AppType = Type | AppValueType | RemoteAppMetadata; diff --git a/libs/sdk/src/common/interfaces/provider.interface.ts b/libs/sdk/src/common/interfaces/provider.interface.ts index 2a4d945d9..042d9145a 100644 --- a/libs/sdk/src/common/interfaces/provider.interface.ts +++ b/libs/sdk/src/common/interfaces/provider.interface.ts @@ -9,9 +9,8 @@ export type ProviderFactoryType & ProviderMetadata; -// eslint-disable-next-line @typescript-eslint/no-explicit-any export type ProviderType< - Provide = any, + Provide = unknown, Tokens extends readonly (ClassToken | Token)[] = readonly (ClassToken | Token)[], > = Type | ProviderClassType | ProviderValueType | ProviderFactoryType; diff --git a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts index 04485609a..cfc58c0f6 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts @@ -10,11 +10,10 @@ import { Flow, FlowBase, FlowHooksOf, FlowPlan, FlowRunOptions } from '../../common'; import { z } from 'zod'; import { randomUUID } from '@frontmcp/utils'; -import { InvalidInputError } from '../../errors'; +import { InvalidInputError, ElicitationStoreNotInitializedError } from '../../errors'; import type { ElicitMode } from '../elicitation.types'; import { DEFAULT_ELICIT_TTL } from '../elicitation.types'; import type { PendingElicitRecord } from '../store'; -import type { Scope } from '../../scope'; const inputSchema = z.object({ /** Related request ID from the transport */ @@ -165,7 +164,7 @@ export default class ElicitationRequestFlow extends FlowBase { const { elicitId, sessionId, message, mode, expiresAt, requestedSchema } = this.state.required; const store = this.scope.elicitationStore; if (!store) { - throw new Error('Elicitation store not initialized'); + throw new ElicitationStoreNotInitializedError(); } const pendingRecord: PendingElicitRecord = { diff --git a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts index 3e49354e0..180cb36b8 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts @@ -11,7 +11,6 @@ import { Flow, FlowBase, FlowHooksOf, FlowPlan, FlowRunOptions } from '../../com import { z } from 'zod'; import type { ElicitResult, ElicitStatus } from '../elicitation.types'; import type { PendingElicitRecord } from '../store'; -import type { Scope } from '../../scope'; import { InvalidInputError } from '../../errors'; import { validateElicitationContent } from '../helpers'; diff --git a/libs/sdk/src/elicitation/helpers/fallback.helper.ts b/libs/sdk/src/elicitation/helpers/fallback.helper.ts index 2e3873716..d60d41f6b 100644 --- a/libs/sdk/src/elicitation/helpers/fallback.helper.ts +++ b/libs/sdk/src/elicitation/helpers/fallback.helper.ts @@ -11,7 +11,11 @@ import type { FrontMcpLogger } from '../../common'; import type { Scope } from '../../scope'; import type { FallbackExecutionResult } from '../elicitation.types'; import { DEFAULT_FALLBACK_WAIT_TTL } from '../elicitation.types'; -import { ElicitationFallbackRequired, ElicitationSubscriptionError } from '../../errors'; +import { + ElicitationFallbackRequired, + ElicitationSubscriptionError, + ElicitationStoreNotInitializedError, +} from '../../errors'; import type { CallToolResult } from '@frontmcp/protocol'; /** @@ -149,7 +153,9 @@ export async function handleWaitingFallback( // Subscribe to fallback results const store = scope.elicitationStore; if (!store) { - reject(new Error('Elicitation store not initialized')); + resolved = true; + clearTimeout(timeoutHandle); + reject(new ElicitationStoreNotInitializedError()); return; } store diff --git a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts index 9697dc0bf..e09453565 100644 --- a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts +++ b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts @@ -16,7 +16,6 @@ import { z } from 'zod'; import type { CallToolResult } from '@frontmcp/protocol'; import { Tool, ToolContext } from '../common'; import type { ElicitResult, ElicitStatus } from './elicitation.types'; -import type { Scope } from '../scope'; const inputSchema = { elicitId: z.string().describe('The elicitation ID from the pending request'), @@ -126,7 +125,7 @@ export class SendElicitationResultTool extends ToolContext { // Re-invoke the original tool using the flow // The pre-resolved result is in the async context, so the tool's elicit() // will return it immediately instead of throwing ElicitationFallbackRequired - const toolResult = await (this.scope as unknown as Scope).runFlowForOutput('tools:call-tool', { + const toolResult = await this.scope.runFlowForOutput('tools:call-tool', { request: { method: 'tools/call', params: { diff --git a/libs/sdk/src/errors/index.ts b/libs/sdk/src/errors/index.ts index 01bc8a6df..b4e9346c7 100644 --- a/libs/sdk/src/errors/index.ts +++ b/libs/sdk/src/errors/index.ts @@ -165,6 +165,7 @@ export { TransportNotConnectedError, TransportAlreadyStartedError, UnsupportedContentTypeError, + TransportServiceNotAvailableError, } from './transport.errors'; // Export auth internal errors diff --git a/libs/sdk/src/errors/transport.errors.ts b/libs/sdk/src/errors/transport.errors.ts index 2174c41a5..117b66eac 100644 --- a/libs/sdk/src/errors/transport.errors.ts +++ b/libs/sdk/src/errors/transport.errors.ts @@ -72,3 +72,12 @@ export class UnsupportedContentTypeError extends PublicMcpError { this.contentType = contentType; } } + +/** + * Thrown when the transport service is required but not available on the scope. + */ +export class TransportServiceNotAvailableError extends InternalMcpError { + constructor() { + super('Transport service not available', 'TRANSPORT_SERVICE_NOT_AVAILABLE'); + } +} diff --git a/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts b/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts index dd5491310..e55b2919f 100644 --- a/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts +++ b/libs/sdk/src/plugin/__tests__/plugin.registry.spec.ts @@ -7,7 +7,7 @@ import PluginRegistry, { PluginScopeInfo } from '../plugin.registry'; import { FlowCtxOf } from '../../common/interfaces'; import { FrontMcpPlugin } from '../../common/decorators/plugin.decorator'; import { FlowHooksOf } from '../../common/decorators/hook.decorator'; -import { createClassProvider, createValueProvider } from '../../__test-utils__/fixtures/provider.fixtures'; +import { createClassProvider } from '../../__test-utils__/fixtures/provider.fixtures'; import { createProviderRegistryWithScope, createMockScope } from '../../__test-utils__/fixtures/scope.fixtures'; import { InvalidPluginScopeError } from '../../errors'; import { Scope } from '../../scope'; diff --git a/libs/sdk/src/provider/provider.registry.ts b/libs/sdk/src/provider/provider.registry.ts index a017736e3..13fc363ed 100644 --- a/libs/sdk/src/provider/provider.registry.ts +++ b/libs/sdk/src/provider/provider.registry.ts @@ -37,7 +37,6 @@ import { import { RegistryAbstract, RegistryBuildMapResult } from '../regsitry'; import { ProviderViews } from './provider.types'; import { Scope } from '../scope'; -import HookRegistry from '../hooks/hook.registry'; import { validateSessionId } from '../context/frontmcp-context'; import { type DistributedEnabled, shouldCacheProviders } from '../common/types/options/transport'; diff --git a/libs/sdk/src/resource/flows/read-resource.flow.ts b/libs/sdk/src/resource/flows/read-resource.flow.ts index 8f21feef7..9d645b9d5 100644 --- a/libs/sdk/src/resource/flows/read-resource.flow.ts +++ b/libs/sdk/src/resource/flows/read-resource.flow.ts @@ -12,7 +12,6 @@ import { ResourceReadError, } from '../../errors'; import { isUIResourceUri, handleUIResourceRead } from '../../tool/ui'; -import { Scope } from '../../scope'; import { FlowContextProviders } from '../../provider/flow-context-providers'; const inputSchema = z.object({ diff --git a/libs/sdk/src/scope/flows/http.request.flow.ts b/libs/sdk/src/scope/flows/http.request.flow.ts index 2b3633eff..19d943aa8 100644 --- a/libs/sdk/src/scope/flows/http.request.flow.ts +++ b/libs/sdk/src/scope/flows/http.request.flow.ts @@ -526,12 +526,13 @@ export default class HttpRequestFlow extends FlowBase { const authorization = request[ServerRequestTokens.auth] as Authorization | undefined; if (authorization?.token) { const transportService = this.scope.transportService; - if (!transportService) return; - for (const protocol of ['streamable-http', 'sse'] as const) { - try { - await transportService.destroyTransporter(protocol, authorization.token, sessionId); - } catch { - // Transport may already be evicted or not found — non-critical + if (transportService) { + for (const protocol of ['streamable-http', 'sse'] as const) { + try { + await transportService.destroyTransporter(protocol, authorization.token, sessionId); + } catch { + // Transport may already be evicted or not found — non-critical + } } } } diff --git a/libs/sdk/src/transport/flows/handle.sse.flow.ts b/libs/sdk/src/transport/flows/handle.sse.flow.ts index 24af6f49e..f94b089e8 100644 --- a/libs/sdk/src/transport/flows/handle.sse.flow.ts +++ b/libs/sdk/src/transport/flows/handle.sse.flow.ts @@ -14,7 +14,7 @@ import { validateMcpSessionHeader, } from '../../common'; import { z } from 'zod'; -import { Scope } from '../../scope'; +import { TransportServiceNotAvailableError } from '../../errors'; import { createSessionId } from '../../auth/session/utils/session-id.utils'; import { detectSkillsOnlyMode } from '../../skill/skill-mode.utils'; @@ -174,7 +174,7 @@ export default class HandleSseFlow extends FlowBase { async onInitialize() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const { request, response } = this.rawInput; @@ -203,7 +203,7 @@ export default class HandleSseFlow extends FlowBase { async onMessage() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const logger = this.scopeLogger.child('handle:legacy-sse:onMessage'); diff --git a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts index 04ebb93a6..907091575 100644 --- a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts @@ -12,7 +12,7 @@ import { } from '../../common'; import { z } from 'zod'; import { RequestSchema } from '@frontmcp/protocol'; -import { Scope } from '../../scope'; +import { TransportServiceNotAvailableError } from '../../errors'; export const plan = { pre: ['parseInput', 'router'], @@ -97,7 +97,7 @@ export default class HandleStatelessHttpFlow extends FlowBase { async handleRequest() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const logger = this.scope.logger.child('HandleStatelessHttpFlow'); const { request, response } = this.rawInput; diff --git a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts index b005d1331..05fcce6c4 100644 --- a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts @@ -13,7 +13,7 @@ import { FlowControl, validateMcpSessionHeader, } from '../../common'; -import { InternalMcpError } from '../../errors'; +import { InternalMcpError, TransportServiceNotAvailableError } from '../../errors'; import { z } from 'zod'; import { ElicitResultSchema, RequestSchema, CallToolResultSchema } from '@frontmcp/protocol'; import type { StoredSession } from '@frontmcp/auth'; @@ -303,7 +303,7 @@ export default class HandleStreamableHttpFlow extends FlowBase { async onInitialize() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const logger = this.scope.logger.child('handle:streamable-http:onInitialize'); @@ -361,7 +361,7 @@ export default class HandleStreamableHttpFlow extends FlowBase { async onElicitResult() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const logger = this.scopeLogger.child('handle:streamable-http:onElicitResult'); @@ -433,7 +433,7 @@ export default class HandleStreamableHttpFlow extends FlowBase { async onMessage() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const logger = this.scopeLogger.child('handle:streamable-http:onMessage'); @@ -517,7 +517,7 @@ export default class HandleStreamableHttpFlow extends FlowBase { async onSseListener() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const logger = this.scopeLogger.child('handle:streamable-http:onSseListener'); @@ -570,7 +570,7 @@ export default class HandleStreamableHttpFlow extends FlowBase { async onExtApps() { const transportService = this.scope.transportService; if (!transportService) { - throw new Error('Transport service not available'); + throw new TransportServiceNotAvailableError(); } const logger = this.scopeLogger.child('handle:streamable-http:onExtApps'); From 271f60612c08da68ae236b2a49057a970a7eadba Mon Sep 17 00:00:00 2001 From: David Antoon Date: Fri, 27 Mar 2026 04:37:23 +0300 Subject: [PATCH 12/12] refactor: enhance error handling by introducing ElicitationStoreNotInitializedError --- libs/sdk/src/elicitation/flows/elicitation-result.flow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts index 180cb36b8..7bfb057dc 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts @@ -11,7 +11,7 @@ import { Flow, FlowBase, FlowHooksOf, FlowPlan, FlowRunOptions } from '../../com import { z } from 'zod'; import type { ElicitResult, ElicitStatus } from '../elicitation.types'; import type { PendingElicitRecord } from '../store'; -import { InvalidInputError } from '../../errors'; +import { InvalidInputError, ElicitationStoreNotInitializedError } from '../../errors'; import { validateElicitationContent } from '../helpers'; const inputSchema = z.object({ @@ -111,7 +111,7 @@ export default class ElicitationResultFlow extends FlowBase { const { sessionId } = this.state.required; const store = this.scope.elicitationStore; if (!store) { - throw new Error('Elicitation store not initialized'); + throw new ElicitationStoreNotInitializedError(); } const pendingRecord = await store.getPending(sessionId);