import * as plugins from './mcp.plugins.js'; import { commitinfo } from './00_commitinfo_data.js'; import { controllerMcpCallerCredentialEnvironmentVariable } from '../ts_interfaces/index.js'; import { defaultControllerPort } from './classes.config.js'; import { assertControllerMcpRuntimeSupported } from './classes.mcpplatform.js'; import { AglMcpTools } from './classes.mcptools.js'; interface ITstaskMcpToolComponent { register(serverArg: InstanceType): unknown; beginShutdown(): void; close(): Promise; } interface ITstaskMcpModule { TstaskMcpToolComponent: new () => ITstaskMcpToolComponent; } export interface IAglMcpServerOptions { controllerPort: number; /** * Read from the environment, never from argv: an argument would be visible in every process * listing on the machine. Absent when the host was not launched by AGL. */ callerCredential?: string; terminateProcess?: (exitCodeArg: number) => void; } export const resolveAglMcpCallerCredential = ( environmentArg: NodeJS.ProcessEnv = process.env, ): string | undefined => { const credential = environmentArg[controllerMcpCallerCredentialEnvironmentVariable]; return credential === undefined || !/^[A-Za-z0-9_-]{43}$/.test(credential) ? undefined : credential; }; const parsePort = (valueArg: string, sourceArg: string): number => { if (!/^[1-9][0-9]{0,4}$/.test(valueArg)) { throw new Error(`${sourceArg} must be an integer between 1 and 65535.`); } const port = Number(valueArg); if (!Number.isSafeInteger(port) || port > 65_535) { throw new Error(`${sourceArg} must be an integer between 1 and 65535.`); } return port; }; export const resolveAglMcpControllerPort = ( argvArg: readonly string[] = process.argv.slice(2), environmentArg: NodeJS.ProcessEnv = process.env, ): number => { let commandLinePort: number | undefined; for (let index = 0; index < argvArg.length; index += 1) { const argument = argvArg[index]; let rawPort: string | undefined; if (argument === '--port') { rawPort = argvArg[index + 1]; index += 1; } else if (argument.startsWith('--port=')) { rawPort = argument.slice('--port='.length); } else { throw new Error(`Unknown agl mcp argument: ${argument}`); } if (commandLinePort !== undefined) { throw new Error('The agl mcp --port option may only be specified once.'); } commandLinePort = parsePort(rawPort ?? '', '--port'); } if (commandLinePort !== undefined) return commandLinePort; const environmentPort = environmentArg.AGL_CONTROLLER_PORT; return environmentPort === undefined ? defaultControllerPort : parsePort(environmentPort, 'AGL_CONTROLLER_PORT'); }; const loadTstaskMcpTools = async (): Promise => { let loadedModule: unknown; try { loadedModule = await plugins.loadTstaskMcpModule(); } catch (errorArg) { const missingPackage = errorArg instanceof Error ? /^Cannot find package '([^']+)' imported from /u.exec(errorArg.message)?.[1] : undefined; if ( (errorArg as NodeJS.ErrnoException).code !== 'ERR_MODULE_NOT_FOUND' || missingPackage !== plugins.tstaskMcpPackageName ) throw errorArg; throw new Error( 'agl mcp requires the optional @modelprofile.com/mcp-tstask package on this platform.', { cause: errorArg }, ); } if ( loadedModule === null || typeof loadedModule !== 'object' || typeof Reflect.get(loadedModule, 'TstaskMcpToolComponent') !== 'function' ) { throw new Error( 'The installed @modelprofile.com/mcp-tstask package does not expose TstaskMcpToolComponent.', ); } const Constructor = Reflect.get( loadedModule, 'TstaskMcpToolComponent', ) as ITstaskMcpModule['TstaskMcpToolComponent']; return new Constructor(); }; export class AglMcpServer { public readonly aglTools: AglMcpTools; public readonly crossHarnessTools = new plugins.CrossHarnessMcpToolRegistrar(); public readonly systemTools = new plugins.SystemMcpTools(); private tstaskTools?: ITstaskMcpToolComponent; private activeServer?: InstanceType; private activeTransport?: plugins.Transport; private stdinEndHandler?: () => void; private stdinEndSource?: NodeJS.ReadableStream; private signalHandlers = new Map void>(); private closePromise?: Promise; private processClosePromise?: Promise; private startCalled = false; private closing = false; private closed = false; private transportClosed = false; private transportCloseNeedsRetry = false; private readonly terminateProcess: (exitCodeArg: number) => void; constructor(private readonly options: IAglMcpServerOptions) { this.aglTools = new AglMcpTools({ controllerPort: options.controllerPort, ...(options.callerCredential === undefined ? {} : { callerCredential: options.callerCredential }), }); this.terminateProcess = options.terminateProcess ?? ((exitCodeArg) => { process.exit(exitCodeArg); }); } public async start( transportArg: plugins.Transport = new plugins.StdioServerTransport(), stdinArg: NodeJS.ReadableStream = process.stdin, ): Promise { if (this.startCalled || this.closing || this.closed) { throw new Error('agl mcp server instances are one-shot.'); } assertControllerMcpRuntimeSupported(); this.startCalled = true; const tstaskTools = await loadTstaskMcpTools(); if (this.closing || this.closed) { tstaskTools.beginShutdown(); await tstaskTools.close(); throw new Error('agl mcp startup was cancelled by shutdown.'); } this.tstaskTools = tstaskTools; const server = new plugins.McpServer( { name: 'mcp-agl', version: commitinfo.version, }, { instructions: [ 'Use AGL tools to inspect and operate the verified local AGL controller.', 'session_send is a direct operation and never reads or mutates browser draft state.', 'Never retry session_send automatically when its outcome is unknown.', 'Use tstask tools only for explicit durable-task requests and follow their authority rules.', ].join(' '), }, ); this.activeServer = server; this.activeTransport = transportArg; server.server.onclose = () => this.triggerSdkClose(); try { this.crossHarnessTools.register(server); this.systemTools.register(server); this.aglTools.register(server); tstaskTools.register(server); this.installProcessCloseHandlers(stdinArg); await server.connect(transportArg); } catch (errorArg) { let cleanupError: unknown; try { await this.close(); } catch (caughtError) { cleanupError = caughtError; } if (cleanupError !== undefined) { throw new AggregateError( [errorArg, cleanupError], 'agl mcp startup and cleanup failed.', ); } throw errorArg; } } public close(): Promise { if (this.closePromise) return this.closePromise; if ( this.closed && !this.activeServer && !this.activeTransport && !this.tstaskTools ) return Promise.resolve(); this.closing = true; this.tstaskTools?.beginShutdown(); this.aglTools.beginShutdown(); let finalPromise!: Promise; const operation = Promise.resolve().then(async () => { const errors: unknown[] = []; const server = this.activeServer; const transport = this.activeTransport; if (!this.transportClosed) { try { if (this.transportCloseNeedsRetry || !server) { await transport?.close(); } else { await server.close(); } if (server?.isConnected()) { throw new Error('The shared MCP transport remained connected after close.'); } this.transportClosed = true; this.transportCloseNeedsRetry = false; this.removeProcessCloseHandlers(); } catch (errorArg) { this.transportCloseNeedsRetry = true; errors.push(errorArg); } } for (const closeOperation of [ () => this.aglTools.close(), () => this.tstaskTools?.close() ?? Promise.resolve(), () => this.systemTools.close(), () => this.crossHarnessTools.close(), ]) { try { await closeOperation(); } catch (errorArg) { errors.push(errorArg); } } if (errors.length === 1) throw errors[0]; if (errors.length > 1) { throw new AggregateError(errors, 'agl mcp shutdown failed.'); } this.activeServer = undefined; this.activeTransport = undefined; this.tstaskTools = undefined; this.closed = true; }); finalPromise = operation.finally(() => { if (this.closePromise === finalPromise) this.closePromise = undefined; }); this.closePromise = finalPromise; return finalPromise; } private installProcessCloseHandlers(stdinArg: NodeJS.ReadableStream): void { this.stdinEndSource = stdinArg; this.stdinEndHandler = () => this.triggerProcessClose(); stdinArg.once('end', this.stdinEndHandler); stdinArg.once('close', this.stdinEndHandler); stdinArg.once('error', this.stdinEndHandler); for (const signal of ['SIGINT', 'SIGTERM'] as const) { const handler = (): void => this.triggerProcessClose(); this.signalHandlers.set(signal, handler); process.once(signal, handler); } } private removeProcessCloseHandlers(): void { if (this.stdinEndHandler) { this.stdinEndSource?.off('end', this.stdinEndHandler); this.stdinEndSource?.off('close', this.stdinEndHandler); this.stdinEndSource?.off('error', this.stdinEndHandler); } this.stdinEndHandler = undefined; this.stdinEndSource = undefined; for (const [signal, handler] of this.signalHandlers) { process.removeListener(signal, handler); } this.signalHandlers.clear(); } private triggerSdkClose(): void { if (this.closing || this.closed || this.processClosePromise) return; this.triggerProcessClose(); } private triggerProcessClose(): void { if (this.processClosePromise) return; const operation = (async () => { let firstError: unknown; try { await this.close(); return; } catch (errorArg) { firstError = errorArg; } try { await this.close(); } catch (errorArg) { throw new AggregateError( [firstError, errorArg], 'agl mcp process shutdown cleanup failed after retry.', ); } })(); this.processClosePromise = operation; void operation.catch(() => { this.terminateProcess(1); }); } } export const runMcpAglCli = async ( argvArg: readonly string[] = process.argv.slice(2), ): Promise => { try { const controllerPort = resolveAglMcpControllerPort(argvArg); const callerCredential = resolveAglMcpCallerCredential(); await new AglMcpServer({ controllerPort, ...(callerCredential === undefined ? {} : { callerCredential }), }).start(); } catch (errorArg) { const message = errorArg instanceof Error ? errorArg.message : 'agl mcp startup failed.'; process.stderr.write(`agl mcp: ${message}\n`); process.exitCode = 1; } };