import type { IReq_ControllerMcpBrowserAction, IReq_ControllerMcpProjectCreate, IReq_ControllerMcpModelList, IReq_ControllerMcpSessionCreate, IReq_ControllerMcpSessionModelUpdate, IReq_ControllerMcpSessionRename, IReq_ControllerMcpSessionArchive, IReq_ControllerMcpSessionAbort, IReq_ControllerMcpResourceList, IReq_ControllerMcpResourceCreate, IReq_ControllerMcpResourceRename, IReq_ControllerMcpResourceAttach, IReq_ControllerMcpResourceDetach, IReq_ControllerMcpResourceStart, IReq_ControllerMcpResourceStop, IReq_ControllerMcpResourceRetire, IReq_ControllerMcpContextResolve, TControllerMcpWorkspaceMethod, TControllerMcpWorkspaceRequest, TControllerMcpWorkspaceResponse, } from '../ts_interfaces/mcpworkspacerequests.js'; import * as plugins from './plugins.js'; import { controllerFailureJournal } from './classes.failurejournal.js'; import { controllerMcpMethods, controllerOperationFailedErrorCode, controllerMcpTypedRequestMaximumBodyBytes, controllerMcpTypedRequestPath, controllerMcpTypedResponseMaximumBytes, type IReq_ControllerMcpProjectsList, type IReq_ControllerMcpSessionRead, type IReq_ControllerMcpSessionScratchpadRead, type IReq_ControllerMcpSessionScratchpadUpdate, type IReq_ControllerMcpSessionSend, type IReq_ControllerMcpSessionsList, type IReq_ControllerMcpStatus, type IReq_ControllerMcpSettingsGet, type IReq_ControllerMcpSettingsUpdate, type IReq_ControllerMcpAuthSwitch, type IControllerMcpCaller, type TControllerMcpMethod, type TControllerOperationPrincipal, } from '../ts_interfaces/index.js'; import type { IAGLHomePaths } from './classes.aglhome.js'; import { ControllerMcpDescriptorPublisher, type IControllerMcpDescriptor, } from './classes.mcpdescriptor.js'; import type { IControllerProcessIdentity } from './classes.processinspection.js'; import { assertControllerMcpRuntimeSupported } from './classes.mcpplatform.js'; import { assertControllerMcpWireCaller, assertControllerMcpWireRequest, } from './classes.mcpvalidation.js'; import { anonymousControllerMcpCaller, runWithControllerMcpCaller, } from './classes.mcpcallerregistry.js'; export interface IControllerMcpPrivateApi { getSettings(signal: AbortSignal): Promise; updateSettings(request: IReq_ControllerMcpSettingsUpdate['request'], signal: AbortSignal): Promise; authswitch(request: IReq_ControllerMcpAuthSwitch['request'], signal: AbortSignal): Promise; workspace: { [TMethod in TControllerMcpWorkspaceMethod]: ( requestArg: TControllerMcpWorkspaceRequest, signalArg: AbortSignal, ) => Promise>; }; status( principalArg: TControllerOperationPrincipal, signalArg: AbortSignal, ): Promise; listProjects( principalArg: TControllerOperationPrincipal, signalArg: AbortSignal, ): Promise; listSessions( principalArg: TControllerOperationPrincipal, requestArg: IReq_ControllerMcpSessionsList['request'], signalArg: AbortSignal, ): Promise; readSession( principalArg: TControllerOperationPrincipal, requestArg: IReq_ControllerMcpSessionRead['request'], signalArg: AbortSignal, ): Promise; sendSession( principalArg: TControllerOperationPrincipal, requestArg: IReq_ControllerMcpSessionSend['request'], signalArg: AbortSignal, ): Promise; readSessionScratchpad( principalArg: TControllerOperationPrincipal, requestArg: IReq_ControllerMcpSessionScratchpadRead['request'], signalArg: AbortSignal, ): Promise; updateSessionScratchpad( principalArg: TControllerOperationPrincipal, requestArg: IReq_ControllerMcpSessionScratchpadUpdate['request'], signalArg: AbortSignal, ): Promise; } export interface IControllerMcpHostOptions { controllerPort: number; lifecycleGeneration: string; processIdentity: IControllerProcessIdentity; api: IControllerMcpPrivateApi; paths?: IAGLHomePaths; /** Resolves a presented caller credential to the chat it was minted for. */ resolveCaller?: (credentialArg: string | undefined) => IControllerMcpCaller; } class ControllerMcpBoundedTypedRouter extends plugins.typedrequest.TypedRouter { constructor( private readonly resolveCaller: ( credentialArg: string | undefined, ) => IControllerMcpCaller = () => anonymousControllerMcpCaller, ) { super(); } public override async routeAndAddResponse< T extends plugins.typedrequestInterfaces.ITypedRequest = plugins.typedrequestInterfaces.ITypedRequest, >( typedRequestArg: T, optionsArg: plugins.typedrequest.ITypedRouterRouteOptions = {}, ): Promise { // The envelope is the only place the caller credential exists; typed handlers never see it. // Resolving here once and running the handler inside the caller context keeps authorization // out of every handler signature and needs no per-request bookkeeping to clean up. const wireCaller = assertControllerMcpWireCaller( (typedRequestArg as unknown as Record).caller, ); const caller = this.resolveCaller(wireCaller?.credential); const response = await runWithControllerMcpCaller( caller, async () => super.routeAndAddResponse(typedRequestArg, optionsArg), ); if (!response) return response; // A credential is never reflected back: the response carries exactly its documented keys. delete (response as unknown as Record).caller; let exceedsLimit = false; try { exceedsLimit = Buffer.byteLength(JSON.stringify(response), 'utf8') > controllerMcpTypedResponseMaximumBytes; } catch { exceedsLimit = true; } if (!exceedsLimit) return response; response.response = {}; response.error = { text: 'The private AGL controller response exceeded its transfer limit.', data: { code: 'response_limit' }, }; if (response.correlation) response.correlation.phase = 'response'; delete response.localData; delete response.serverData; return response; } } /** * Reduces a controller failure to what an MCP caller may see. The caller is an agent, not the * owner: it gets the same opaque reference the owner UI resolves, never the internal text. */ const safeTypedResponseError = ( errorArg: unknown, methodArg: string, ): plugins.typedrequest.TypedResponseError => { if (errorArg instanceof plugins.typedrequest.TypedResponseError) { const code = errorArg.errorData?.code; const reference = typeof errorArg.errorData?.reference === 'string' ? errorArg.errorData.reference : undefined; if (code === controllerOperationFailedErrorCode) { // The controller journaled this one already; recording it again would duplicate the cause. return new plugins.typedrequest.TypedResponseError( 'The private AGL controller operation failed.', { code: 'request_failed', ...(reference === undefined ? {} : { reference }) }, ); } if (typeof code === 'string' && /^[a-z0-9_]{1,64}$/.test(code)) { const messages: Record = { controller_unavailable: 'The controller is not ready to accept this operation.', operation_limit: 'The controller is busy. Try again after current operations settle.', rate_limited: 'The authenticated operation rate limit was reached.', project_not_found: 'The requested project is unavailable.', session_not_found: 'The requested session is unavailable.', ambiguous_session: 'The native ID exists on multiple connections; select its qualified AGL ID.', model_not_found: 'The selected model is unavailable.', browser_image_limit: 'The screenshot exceeds 512 KiB; request JPEG with lower quality.', invalid_request: 'The private controller request is invalid.', concurrent_change: 'The resource or scratchpad changed since it was read.', outcome_unknown: 'The operation outcome is unknown.', }; return new plugins.typedrequest.TypedResponseError( messages[code] ?? 'The private AGL controller rejected the request.', { code }, ); } } const journalReference = controllerFailureJournal.record({ operation: methodArg, cause: errorArg, }); console.error(`Private AGL MCP request failed (${methodArg}, ref ${journalReference}):`, errorArg); return new plugins.typedrequest.TypedResponseError( 'The private AGL controller operation failed.', { code: 'request_failed', reference: journalReference }, ); }; export class ControllerMcpHost { private readonly descriptorPublisher: ControllerMcpDescriptorPublisher; private typedServer?: plugins.typedserver.TypedServer; private publishedDescriptor?: IControllerMcpDescriptor; private startCalled = false; private admissionOpen = false; private closePromise?: Promise; private closed = false; constructor(private readonly options: IControllerMcpHostOptions) { this.descriptorPublisher = new ControllerMcpDescriptorPublisher({ controllerPort: options.controllerPort, ...(options.paths === undefined ? {} : { paths: options.paths }), }); } public get privatePort(): number | undefined { return this.publishedDescriptor?.privatePort; } public get descriptor(): IControllerMcpDescriptor | undefined { return this.publishedDescriptor === undefined ? undefined : { ...this.publishedDescriptor }; } public async start(): Promise { if (this.startCalled || this.closed || this.closePromise) { throw new Error('The private AGL controller host is one-shot.'); } assertControllerMcpRuntimeSupported(); this.startCalled = true; const router = new ControllerMcpBoundedTypedRouter( this.options.resolveCaller ?? (() => anonymousControllerMcpCaller), ); this.registerHandlers(router); const typedServer = new plugins.typedserver.TypedServer({ port: 0, listenHostname: '127.0.0.1', cors: false, connectionTimeout: 30_000, headersTimeout: 5_000, requestTimeout: 30_000, cleanupTimeoutMs: 5_000, surfaces: [{ name: 'agl-mcp-private', match: { hostnames: ['127.0.0.1'], pathPrefixes: [controllerMcpTypedRequestPath], }, httpTypedRouter: router, typedRequestPath: controllerMcpTypedRequestPath, typedRequestMaxBodyBytes: controllerMcpTypedRequestMaximumBodyBytes, includeBuiltinTypedHandlers: false, serveDir: null, spaFallback: false, healthzEndpoint: false, cors: false, noCache: true, requestAdmission: (contextArg) => this.admitHttpRequest(contextArg), typedRequestAdmission: (requestArg, contextArg) => { if (!this.admissionOpen || !this.authenticated(contextArg.headers)) { return new Response('Service Unavailable', { status: 503 }); } try { assertControllerMcpWireRequest(requestArg); return true; } catch { return new Response('Invalid request', { status: 400 }); } }, securityHeaders: { xFrameOptions: 'DENY', xContentTypeOptions: true, referrerPolicy: 'no-referrer', crossOriginOpenerPolicy: 'same-origin', crossOriginResourcePolicy: 'same-origin', permissionsPolicy: { camera: [], microphone: [], geolocation: [], }, }, }], }); this.typedServer = typedServer; let startupError: unknown; try { await typedServer.start(); if (this.closePromise || this.closed) { throw new Error('Private controller startup was cancelled by shutdown.'); } const privatePort = typedServer.listeningPort; if ( typeof privatePort !== 'number' || !Number.isSafeInteger(privatePort) || privatePort < 1 || privatePort > 65_535 ) { throw new Error('The private listener did not publish a valid port.'); } this.publishedDescriptor = await this.descriptorPublisher.publish({ lifecycleGeneration: this.options.lifecycleGeneration, privatePort, processIdentity: this.options.processIdentity, }); if (this.closePromise || this.closed) { throw new Error('Private controller startup was cancelled by shutdown.'); } return; } catch (errorArg) { startupError = errorArg; } let cleanupError: unknown; try { await this.close(); if (this.publishedDescriptor || this.descriptorPublisher.descriptor || this.typedServer) { await this.close(); } } catch (errorArg) { cleanupError = errorArg; } if (cleanupError !== undefined) { throw new AggregateError( [startupError, cleanupError], 'Private controller startup and cleanup failed.', ); } throw startupError; } public openAdmission(): void { if (!this.typedServer || !this.publishedDescriptor || this.closed || this.closePromise) { throw new Error('The private AGL controller host is not ready.'); } this.admissionOpen = true; } public beginShutdown(): void { this.admissionOpen = false; } public close(): Promise { this.beginShutdown(); if (this.closePromise) return this.closePromise; if ( this.closed && !this.publishedDescriptor && !this.descriptorPublisher.descriptor && !this.typedServer ) return Promise.resolve(); let finalPromise!: Promise; const operation = Promise.resolve().then(async () => { const errors: unknown[] = []; try { await this.descriptorPublisher.removeOwned(); this.publishedDescriptor = undefined; } catch (errorArg) { errors.push(errorArg); } if (this.typedServer) { const typedServer = this.typedServer; try { await typedServer.stop(); if (this.typedServer === typedServer) this.typedServer = undefined; } catch (errorArg) { errors.push(errorArg); } } if (errors.length === 1) throw errors[0]; if (errors.length > 1) { throw new AggregateError(errors, 'Private controller cleanup failed.'); } this.closed = true; }); finalPromise = operation.finally(() => { if (this.closePromise === finalPromise) this.closePromise = undefined; }); this.closePromise = finalPromise; return finalPromise; } private authenticated(headersArg: Headers): boolean { const authorization = headersArg.get('authorization'); const token = this.publishedDescriptor?.token; if (!authorization || !token || !authorization.startsWith('Bearer ')) return false; const supplied = Buffer.from(authorization.slice('Bearer '.length), 'utf8'); const expected = Buffer.from(token, 'utf8'); return supplied.byteLength === expected.byteLength && plugins.crypto.timingSafeEqual(supplied, expected); } private admitHttpRequest( contextArg: Parameters>[0], ): boolean | Response { const privatePort = this.publishedDescriptor?.privatePort ?? this.typedServer?.listeningPort; if (!this.admissionOpen || privatePort === undefined) { return new Response('Service Unavailable', { status: 503 }); } const url = contextArg.url; if ( contextArg.method !== 'POST' || url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || url.port !== String(privatePort) || url.pathname !== controllerMcpTypedRequestPath || url.search !== '' || url.hash !== '' || url.username !== '' || url.password !== '' || contextArg.headers.get('host') !== `127.0.0.1:${privatePort}` ) { return new Response('Not Found', { status: 404 }); } if (contextArg.headers.get('content-type') !== 'application/json') { return new Response('Unsupported Media Type', { status: 415 }); } if (!this.authenticated(contextArg.headers)) { return new Response('Unauthorized', { status: 401 }); } return true; } private registerHandlers(routerArg: ControllerMcpBoundedTypedRouter): void { const run = async ( methodArg: TControllerMcpMethod, operationArg: () => Promise, ): Promise => { try { return await operationArg(); } catch (errorArg) { throw safeTypedResponseError(errorArg, methodArg); } }; routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.controller.settings.get', async (_request, tools) => run('agl.mcp.controller.settings.get', () => this.options.api.getSettings(tools?.abortSignal ?? new AbortController().signal)), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.controller.settings.update', async (request, tools) => run('agl.mcp.controller.settings.update', () => this.options.api.updateSettings(request, tools?.abortSignal ?? new AbortController().signal)), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.project.create', async (requestArg, toolsArg) => run('agl.mcp.project.create', () => this.options.api.workspace['agl.mcp.project.create']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.model.list', async (requestArg, toolsArg) => run('agl.mcp.model.list', () => this.options.api.workspace['agl.mcp.model.list']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.session.create', async (requestArg, toolsArg) => run('agl.mcp.session.create', () => this.options.api.workspace['agl.mcp.session.create']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.session.model.update', async (requestArg, toolsArg) => run('agl.mcp.session.model.update', () => this.options.api.workspace['agl.mcp.session.model.update']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.session.rename', async (requestArg, toolsArg) => run('agl.mcp.session.rename', () => this.options.api.workspace['agl.mcp.session.rename']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.session.archive', async (requestArg, toolsArg) => run('agl.mcp.session.archive', () => this.options.api.workspace['agl.mcp.session.archive']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.session.abort', async (requestArg, toolsArg) => run('agl.mcp.session.abort', () => this.options.api.workspace['agl.mcp.session.abort']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.list', async (requestArg, toolsArg) => run('agl.mcp.resource.list', () => this.options.api.workspace['agl.mcp.resource.list']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.create', async (requestArg, toolsArg) => run('agl.mcp.resource.create', () => this.options.api.workspace['agl.mcp.resource.create']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.rename', async (requestArg, toolsArg) => run('agl.mcp.resource.rename', () => this.options.api.workspace['agl.mcp.resource.rename']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.attach', async (requestArg, toolsArg) => run('agl.mcp.resource.attach', () => this.options.api.workspace['agl.mcp.resource.attach']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.detach', async (requestArg, toolsArg) => run('agl.mcp.resource.detach', () => this.options.api.workspace['agl.mcp.resource.detach']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.start', async (requestArg, toolsArg) => run('agl.mcp.resource.start', () => this.options.api.workspace['agl.mcp.resource.start']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.stop', async (requestArg, toolsArg) => run('agl.mcp.resource.stop', () => this.options.api.workspace['agl.mcp.resource.stop']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.resource.retire', async (requestArg, toolsArg) => run('agl.mcp.resource.retire', () => this.options.api.workspace['agl.mcp.resource.retire']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.browser.action', async (requestArg, toolsArg) => run('agl.mcp.browser.action', () => this.options.api.workspace['agl.mcp.browser.action']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.context.resolve', async (requestArg, toolsArg) => run('agl.mcp.context.resolve', () => this.options.api.workspace['agl.mcp.context.resolve']( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( controllerMcpMethods[0], async (_requestArg, toolsArg) => run(controllerMcpMethods[0], () => this.options.api.status( 'mcp', toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( 'agl.mcp.authswitch.request', async (requestArg, toolsArg) => run('agl.mcp.authswitch.request', () => this.options.api.authswitch( requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( controllerMcpMethods[1], async (_requestArg, toolsArg) => run(controllerMcpMethods[1], () => this.options.api.listProjects( 'mcp', toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( controllerMcpMethods[2], async (requestArg, toolsArg) => run(controllerMcpMethods[2], () => this.options.api.listSessions( 'mcp', requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( controllerMcpMethods[3], async (requestArg, toolsArg) => run(controllerMcpMethods[3], () => this.options.api.readSession( 'mcp', requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler( controllerMcpMethods[4], async (requestArg, toolsArg) => run(controllerMcpMethods[4], () => this.options.api.sendSession( 'mcp', requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), )); routerArg.addTypedHandler( new plugins.typedrequest.TypedHandler( controllerMcpMethods[5], async (requestArg, toolsArg) => run(controllerMcpMethods[5], () => this.options.api.readSessionScratchpad( 'mcp', requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), ), ); routerArg.addTypedHandler( new plugins.typedrequest.TypedHandler( controllerMcpMethods[6], async (requestArg, toolsArg) => run(controllerMcpMethods[6], () => this.options.api.updateSessionScratchpad( 'mcp', requestArg, toolsArg?.abortSignal ?? new AbortController().signal, )), ), ); } }