diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index e06ac834d..d6a7a67a6 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -6,7 +6,6 @@ import { randomUUID } from "node:crypto"; import { hostname as getHostname } from "node:os"; import path from "node:path"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import type { Logger } from "pino"; @@ -96,7 +95,11 @@ import { AgentManager } from "./agent/agent-manager.js"; import { AgentStorage } from "./agent/agent-storage.js"; import { attachAgentStoragePersistence } from "./persistence-hooks.js"; import { createAgentMcpServer } from "./agent/mcp-server.js"; -import { createAllClients, shutdownProviders } from "./agent/provider-registry.js"; +import { + buildProviderRegistry, + createAllClients, + shutdownProviders, +} from "./agent/provider-registry.js"; import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js"; import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js"; import { FileBackedChatService } from "./chat/chat-service.js"; @@ -112,36 +115,21 @@ import { resolveDaemonVersion } from "./daemon-version.js"; import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js"; import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js"; import { isHostAllowed, type AllowedHostsConfig } from "./allowed-hosts.js"; -import { - createVoiceMcpSocketBridgeManager, - type VoiceMcpSocketBridgeManager, -} from "./voice-mcp-bridge.js"; -import { resolveVoiceMcpBridgeFromRuntime } from "./voice-mcp-bridge-command.js"; type AgentMcpTransportMap = Map; -function resolveVoiceMcpBridgeCommand( - logger: Logger, -): { command: string; baseArgs: string[] } | null { - try { - const decision = resolveVoiceMcpBridgeFromRuntime({ - bootstrapModuleUrl: import.meta.url, - execPath: process.execPath, - explicitScriptPath: process.env.PASEO_MCP_STDIO_SOCKET_BRIDGE_SCRIPT, - }); - logger.info( - { - source: decision.source, - command: decision.resolved.command, - baseArgs: decision.resolved.baseArgs, - }, - "Resolved voice MCP bridge command", - ); - return decision.resolved; - } catch (err) { - logger.warn({ err }, "Voice MCP bridge script not available — voice MCP via stdio disabled"); +function formatHostForHttpUrl(host: string): string { + return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; +} + +function createAgentMcpBaseUrl(listenTarget: ListenTarget | null): string | null { + if (!listenTarget || listenTarget.type !== "tcp") { return null; } + return new URL( + "/mcp/agents", + `http://${formatHostForHttpUrl(listenTarget.host)}:${listenTarget.port}`, + ).toString(); } export type PaseoOpenAIConfig = OpenAiSpeechProviderConfig; @@ -171,6 +159,7 @@ export type PaseoDaemonConfig = { corsAllowedOrigins: string[]; allowedHosts?: AllowedHostsConfig; mcpEnabled?: boolean; + mcpInjectIntoAgents?: boolean; staticDir: string; mcpDebug: boolean; agentClients: Partial>; @@ -360,6 +349,9 @@ export async function createPaseoDaemon( registry: agentStorage, logger, }); + const providerRegistry = buildProviderRegistry(logger, { + runtimeSettings: config.agentProviderSettings, + }); const terminalManager = createTerminalManager(); @@ -410,31 +402,9 @@ export async function createPaseoDaemon( ); logger.info({ elapsed: elapsed() }, "Preparing voice and MCP runtime"); let wsServer: VoiceAssistantWebSocketServer | null = null; - let voiceMcpBridgeManager: VoiceMcpSocketBridgeManager | null = null; - - // Create in-memory transport for Session's Agent MCP client (voice assistant tools) - const createInMemoryAgentMcpTransport = async (): Promise => { - const agentMcpServer = await createAgentMcpServer({ - agentManager, - agentStorage, - terminalManager, - paseoHome: config.paseoHome, - enableVoiceTools: false, - resolveSpeakHandler: (callerAgentId) => - wsServer?.resolveVoiceSpeakHandler(callerAgentId) ?? null, - resolveCallerContext: (callerAgentId) => - wsServer?.resolveVoiceCallerContext(callerAgentId) ?? null, - logger, - }); - - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - - await agentMcpServer.connect(serverTransport); - - return clientTransport; - }; const mcpEnabled = config.mcpEnabled ?? true; + let agentMcpBaseUrl: string | null = null; if (mcpEnabled) { const agentMcpRoute = "/mcp/agents"; const agentMcpTransports: AgentMcpTransportMap = new Map(); @@ -444,6 +414,8 @@ export async function createPaseoDaemon( agentManager, agentStorage, terminalManager, + scheduleService, + providerRegistry, paseoHome: config.paseoHome, callerAgentId, enableVoiceTools: false, @@ -554,25 +526,6 @@ export async function createPaseoDaemon( logger.info("Agent MCP HTTP endpoint disabled"); } - const voiceMcpSocketDir = path.join(config.paseoHome, "runtime", "voice-mcp"); - const voiceMcpBridgeCommand = resolveVoiceMcpBridgeCommand(logger); - voiceMcpBridgeManager = createVoiceMcpSocketBridgeManager({ - runtimeDir: voiceMcpSocketDir, - logger, - createAgentMcpServerForCaller: async (callerAgentId) => { - return createAgentMcpServer({ - agentManager, - agentStorage, - terminalManager, - paseoHome: config.paseoHome, - callerAgentId, - voiceOnly: true, - resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null, - resolveCallerContext: (agentId) => wsServer?.resolveVoiceCallerContext(agentId) ?? null, - logger, - }); - }, - }); const speechService = createSpeechService({ logger, openaiConfig: config.openai, @@ -580,55 +533,6 @@ export async function createPaseoDaemon( }); logger.info({ elapsed: elapsed() }, "Speech service created"); - wsServer = new VoiceAssistantWebSocketServer( - httpServer, - logger, - serverId, - agentManager, - agentStorage, - downloadTokenStore, - config.paseoHome, - createInMemoryAgentMcpTransport, - { allowedOrigins, allowedHosts: config.allowedHosts }, - speechService, - terminalManager, - { - voiceAgentMcpStdio: voiceMcpBridgeCommand - ? { - command: voiceMcpBridgeCommand.command, - baseArgs: [...voiceMcpBridgeCommand.baseArgs], - env: { - ELECTRON_RUN_AS_NODE: "1", - PASEO_HOME: config.paseoHome, - }, - } - : null, - ensureVoiceMcpSocketForAgent: (agentId) => - voiceMcpBridgeManager?.ensureBridgeForCaller(agentId) ?? - Promise.reject(new Error("Voice MCP bridge manager is not initialized")), - removeVoiceMcpSocketForAgent: (agentId) => - voiceMcpBridgeManager?.removeBridgeForCaller(agentId) ?? Promise.resolve(), - }, - { - finalTimeoutMs: config.dictationFinalTimeoutMs, - }, - config.agentProviderSettings, - daemonVersion, - (intent) => { - try { - config.onLifecycleIntent?.(intent); - } catch (error) { - logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent"); - } - }, - projectRegistry, - workspaceRegistry, - chatService, - loopService, - scheduleService, - checkoutDiffManager, - ); - logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening"); const start = async () => { @@ -642,6 +546,9 @@ export async function createPaseoDaemon( httpServer.off("error", onError); const logAndResolve = async () => { boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer); + const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null; + agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl; + agentManager.setMcpBaseUrl(agentMcpBaseUrl); const relayEnabled = config.relayEnabled ?? true; const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443"; const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint; @@ -660,9 +567,41 @@ export async function createPaseoDaemon( logger.info( { path: boundListenTarget.path, elapsed: elapsed() }, `Server listening on ${boundListenTarget.path}`, - ); + ); } + wsServer = new VoiceAssistantWebSocketServer( + httpServer, + logger, + serverId, + agentManager, + agentStorage, + downloadTokenStore, + config.paseoHome, + mcpBaseUrl, + { allowedOrigins, allowedHosts: config.allowedHosts }, + speechService, + terminalManager, + { + finalTimeoutMs: config.dictationFinalTimeoutMs, + }, + config.agentProviderSettings, + daemonVersion, + (intent) => { + try { + config.onLifecycleIntent?.(intent); + } catch (error) { + logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent"); + } + }, + projectRegistry, + workspaceRegistry, + chatService, + loopService, + scheduleService, + checkoutDiffManager, + ); + if (typeof process.send === "function" && process.env.PASEO_SUPERVISED === "1") { process.send({ type: "paseo:ready", @@ -681,7 +620,7 @@ export async function createPaseoDaemon( }); const url = encodeOfferToFragmentUrl({ offer, appBaseUrl }); - logger.info({ url }, "pairing_offer"); + logger.info("pairing_offer"); relayTransport?.stop().catch(() => undefined); relayTransport = startRelayTransport({ @@ -734,9 +673,6 @@ export async function createPaseoDaemon( if (wsServer) { await wsServer.close(); } - if (voiceMcpBridgeManager) { - await voiceMcpBridgeManager.stop().catch(() => undefined); - } await new Promise((resolve) => { httpServer.close(() => resolve()); }); diff --git a/packages/server/src/server/relay-transport.ts b/packages/server/src/server/relay-transport.ts index 4ee2fe7c0..4e6059200 100644 --- a/packages/server/src/server/relay-transport.ts +++ b/packages/server/src/server/relay-transport.ts @@ -149,7 +149,7 @@ export function startRelayTransport({ clearTimeout(controlReadyTimeout); controlReadyTimeout = null; } - relayLogger.info({ url, connectionId }, "relay_control_connected"); + relayLogger.info({ connectionId }, "relay_control_connected"); }; socket.on("open", () => { @@ -203,7 +203,7 @@ export function startRelayTransport({ try { socket.send(JSON.stringify({ type: "ping", ts: now })); } catch (error) { - relayLogger.warn({ err: error, url, connectionId }, "relay_control_ping_send_failed"); + relayLogger.warn({ err: error, connectionId }, "relay_control_ping_send_failed"); try { socket.terminate(); } catch { @@ -214,14 +214,14 @@ export function startRelayTransport({ try { socket.send(JSON.stringify({ type: "ping", ts: Date.now() })); } catch (error) { - relayLogger.warn({ err: error, url, connectionId }, "relay_control_ping_send_failed"); + relayLogger.warn({ err: error, connectionId }, "relay_control_ping_send_failed"); try { socket.terminate(); } catch { // ignore } } - relayLogger.debug({ url, connectionId }, "relay_control_open_waiting_for_ready"); + relayLogger.debug({ connectionId }, "relay_control_open_waiting_for_ready"); }); socket.on("close", (code, reason) => { @@ -244,7 +244,7 @@ export function startRelayTransport({ socket.on("error", (err) => { if (controlWs !== socket) return; - relayLogger.warn({ err, url, connectionId }, "relay_error"); + relayLogger.warn({ err, connectionId }, "relay_error"); // close event will schedule reconnect }); @@ -319,7 +319,7 @@ export function startRelayTransport({ const openTimeout = setTimeout(() => { if (stopped) return; if (socket.readyState === WebSocket.OPEN) return; - relayLogger.warn({ url, connectionId }, "relay_data_open_timeout_terminating"); + relayLogger.warn({ connectionId }, "relay_data_open_timeout_terminating"); try { socket.terminate(); } catch { @@ -329,7 +329,7 @@ export function startRelayTransport({ socket.on("open", () => { clearTimeout(openTimeout); - relayLogger.info({ url, connectionId }, "relay_data_connected"); + relayLogger.info({ connectionId }, "relay_data_connected"); if (attached) return; attached = true; const externalMetadata: ExternalSocketMetadata = { @@ -361,7 +361,7 @@ export function startRelayTransport({ }); socket.on("error", (err) => { - relayLogger.warn({ err, url, connectionId }, "relay_data_error"); + relayLogger.warn({ err, connectionId }, "relay_data_error"); }); };