zerogterm 0.9.0-alpha.1 → 0.9.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,7 @@ See the project walkthrough on [YouTube](https://youtu.be/4aJZCxLHD14).
38
38
 
39
39
  ## Release status
40
40
 
41
- ZeroG Terminal is currently a public alpha. The current release is `0.9.0-alpha.1`; the version history is tracked in [versions.txt](versions.txt). The running version is shown beside the wordmark in the title bar, read from the app itself rather than written into the interface, so it is accurate in a packaged build too.
41
+ ZeroG Terminal is currently a public alpha. The current release is `0.9.1-alpha.1`; the version history is tracked in [versions.txt](versions.txt). The running version is shown beside the wordmark in the title bar, read from the app itself rather than written into the interface, so it is accurate in a packaged build too.
42
42
 
43
43
  The GitHub Releases page provides a Windows x64 installer and portable executable for each desktop release. These alpha builds are intended for early adopters and testing rather than production use. The npm package remains available for developers who prefer to launch ZeroG from Node.js.
44
44
 
@@ -396,6 +396,59 @@ npm install --global zerogterm
396
396
  zerogterm
397
397
  ```
398
398
 
399
+ ## Connecting an AI agent through MCP
400
+
401
+ ZeroG Terminal can expose the running desktop instance as a local MCP server. This
402
+ lets an MCP-capable AI agent inspect workspaces and sessions, restore or create
403
+ project workspaces, open SSH sessions, and request commands for your approval.
404
+
405
+ 1. Start ZeroG Terminal with MCP enabled:
406
+
407
+ ```bash
408
+ ZEROG_MCP_ENABLED=1 npx zerogterm
409
+ ```
410
+
411
+ On Windows PowerShell, use:
412
+
413
+ ```powershell
414
+ $env:ZEROG_MCP_ENABLED = "1"
415
+ npx zerogterm
416
+ ```
417
+
418
+ 2. Open **Settings → AI & voice → MCP** in ZeroG Terminal. Copy the displayed
419
+ endpoint and bearer token into your MCP client. The default endpoint is
420
+ `http://127.0.0.1:60056/mcp`.
421
+
422
+ MCP clients that support Streamable HTTP generally need the equivalent of:
423
+
424
+ ```json
425
+ {
426
+ "url": "http://127.0.0.1:60056/mcp",
427
+ "headers": {
428
+ "Authorization": "Bearer <token-from-zero-g-settings>"
429
+ }
430
+ }
431
+ ```
432
+
433
+ The exact configuration key differs between clients; use its Streamable HTTP
434
+ or remote MCP server setting rather than a stdio command. The token belongs to
435
+ the current ZeroG process, is held only in memory, and must be copied again
436
+ after ZeroG restarts. Never put it in source control, prompts, issue reports,
437
+ or shared configuration.
438
+
439
+ 3. Ask the agent to call `zerog_get_connection_status`, then
440
+ `zerog_acquire_control` with a client ID and the capabilities it needs. Read
441
+ capabilities require no write access; workspace changes and command requests
442
+ require the corresponding lease capabilities. The lease is single-client and
443
+ expires unless renewed.
444
+
445
+ Every command request is shown in ZeroG Terminal for exact user approval before
446
+ anything is written to a shell. Rejection, cancellation, takeover, credential
447
+ prompts, uncertain prompts, and unsupported command shapes fail closed. MCP does
448
+ not receive terminal output, credentials, or keystroke-level control. See the
449
+ [full MCP protocol and security guide](docs/mcp.md) for tools, limits, and
450
+ troubleshooting.
451
+
399
452
  ## Development
400
453
 
401
454
  ```bash
@@ -381,61 +381,64 @@ ipcMain.handle('mcp:execution:cancel', (_event, requestId) => {
381
381
  win?.webContents.send('mcp:execution', result);
382
382
  return result;
383
383
  });
384
- ipcMain.handle('mcp:start', async () => {
385
- mcpHost ??= new McpServerHost({
386
- control: mcpControl,
387
- providers: {
388
- listWorkspaces: () => workspaceStore.load(),
389
- listSessions: () => service.list(),
390
- createWorkspace,
391
- restoreWorkspace,
392
- createSshSession: async (request) => {
393
- const session = await service.createSsh(request.target, request.name);
394
- const attached = service.attach(session.id, (data) => {
395
- const requestId = mcpActiveExecutions.get(session.id);
396
- if (requestId) {
397
- const output = `${mcpExecutionOutput.get(requestId) ?? ''}${data}`.slice(0, 16 * 1024);
398
- mcpExecutionOutput.set(requestId, output);
399
- if ((output.match(/ExitCode=\d+/g) ?? []).length >= 2) {
400
- const result = mcpExecutions.finishRunning(requestId, 'completed', output, false, 'Command completed.');
401
- clearMcpExecution(requestId, session.id);
402
- win?.webContents.send('mcp:execution', result);
403
- }
384
+ function createMcpProviders() {
385
+ return {
386
+ listWorkspaces: () => workspaceStore.load(),
387
+ listSessions: () => service.list(),
388
+ createWorkspace,
389
+ restoreWorkspace,
390
+ createSshSession: async (request) => {
391
+ const session = await service.createSsh(request.target, request.name);
392
+ const attached = service.attach(session.id, (data) => {
393
+ const requestId = mcpActiveExecutions.get(session.id);
394
+ if (requestId) {
395
+ const output = `${mcpExecutionOutput.get(requestId) ?? ''}${data}`.slice(0, 16 * 1024);
396
+ mcpExecutionOutput.set(requestId, output);
397
+ if ((output.match(/ExitCode=\d+/g) ?? []).length >= 2) {
398
+ const result = mcpExecutions.finishRunning(requestId, 'completed', output, false, 'Command completed.');
399
+ clearMcpExecution(requestId, session.id);
400
+ win?.webContents.send('mcp:execution', result);
404
401
  }
405
- win?.webContents.send('terminal:data', session.id, data);
406
- }, (message) => win?.webContents.send('terminal:status', session.id, message));
407
- win?.webContents.send('mcp:ssh-session-created', { session: attached });
408
- return attached;
409
- },
410
- createProjectWorkspace,
411
- requestCommand: async (request) => {
412
- const sessions = await service.list();
413
- const session = sessions.find((item) => item.id === request.sessionId);
414
- if (!session)
415
- throw new Error('Unknown session.');
416
- const result = mcpExecutions.create({ clientId: request.clientId, sessionId: request.sessionId, sessionKind: session.kind, command: request.command, policy: { allowRemoteSessions: session.kind === 'ssh', ...(request.timeoutMs === undefined ? {} : { maxRuntimeMs: request.timeoutMs }), ...(request.outputBytes === undefined ? {} : { maxOutputBytes: request.outputBytes }) } });
417
- const safeRemote = session.kind === 'ssh' && isSafeRemoteCommand(result.command);
418
- if (safeRemote) {
419
- const approved = mcpExecutions.approveAutomatically(result.requestId);
420
- mcpAudit.record({ at: Date.now(), requestId: approved.requestId, clientId: approved.clientId, capability: 'session:execute', sessionId: approved.sessionId, state: 'approved', command: approved.displayCommand });
421
- await runMcpExecution(approved.requestId);
422
- return mcpExecutions.get(approved.requestId, request.clientId);
423
402
  }
424
- mcpAudit.record({ at: Date.now(), requestId: result.requestId, clientId: result.clientId, capability: 'session:execute', sessionId: result.sessionId, state: 'requested', command: result.displayCommand });
425
- win?.webContents.send('mcp:execution', result);
426
- return result;
427
- },
428
- getCommandResult: async (request) => mcpExecutions.get(request.requestId, request.clientId),
429
- cancelCommand: async (request) => {
430
- const current = mcpExecutions.get(request.requestId, request.clientId);
431
- const result = mcpExecutions.cancel(request.requestId, request.clientId);
432
- const session = 'sessionId' in current ? (await service.list()).find((item) => item.id === current.sessionId) : undefined;
433
- if (session?.kind === 'local')
434
- service.write(session.id, '\u0003');
435
- clearMcpExecution(request.requestId);
436
- return result;
403
+ win?.webContents.send('terminal:data', session.id, data);
404
+ }, (message) => win?.webContents.send('terminal:status', session.id, message));
405
+ win?.webContents.send('mcp:ssh-session-created', { session: attached });
406
+ return attached;
407
+ },
408
+ createProjectWorkspace,
409
+ requestCommand: async (request) => {
410
+ const sessions = await service.list();
411
+ const session = sessions.find((item) => item.id === request.sessionId);
412
+ if (!session)
413
+ throw new Error('Unknown session.');
414
+ const result = mcpExecutions.create({ clientId: request.clientId, sessionId: request.sessionId, sessionKind: session.kind, command: request.command, policy: { allowRemoteSessions: session.kind === 'ssh', ...(request.timeoutMs === undefined ? {} : { maxRuntimeMs: request.timeoutMs }), ...(request.outputBytes === undefined ? {} : { maxOutputBytes: request.outputBytes }) } });
415
+ const safeRemote = session.kind === 'ssh' && isSafeRemoteCommand(result.command);
416
+ if (safeRemote) {
417
+ const approved = mcpExecutions.approveAutomatically(result.requestId);
418
+ mcpAudit.record({ at: Date.now(), requestId: approved.requestId, clientId: approved.clientId, capability: 'session:execute', sessionId: approved.sessionId, state: 'approved', command: approved.displayCommand });
419
+ await runMcpExecution(approved.requestId);
420
+ return mcpExecutions.get(approved.requestId, request.clientId);
437
421
  }
422
+ mcpAudit.record({ at: Date.now(), requestId: result.requestId, clientId: result.clientId, capability: 'session:execute', sessionId: result.sessionId, state: 'requested', command: result.displayCommand });
423
+ win?.webContents.send('mcp:execution', result);
424
+ return result;
438
425
  },
426
+ getCommandResult: async (request) => mcpExecutions.get(request.requestId, request.clientId),
427
+ cancelCommand: async (request) => {
428
+ const current = mcpExecutions.get(request.requestId, request.clientId);
429
+ const result = mcpExecutions.cancel(request.requestId, request.clientId);
430
+ const session = 'sessionId' in current ? (await service.list()).find((item) => item.id === current.sessionId) : undefined;
431
+ if (session?.kind === 'local')
432
+ service.write(session.id, '\u0003');
433
+ clearMcpExecution(request.requestId);
434
+ return result;
435
+ }
436
+ };
437
+ }
438
+ ipcMain.handle('mcp:start', async () => {
439
+ mcpHost ??= new McpServerHost({
440
+ control: mcpControl,
441
+ providers: createMcpProviders(),
439
442
  version: app.getVersion()
440
443
  });
441
444
  return mcpHost.start();
@@ -707,58 +710,7 @@ app.whenReady().then(() => {
707
710
  if (process.env.ZEROG_MCP_ENABLED === '1') {
708
711
  mcpHost ??= new McpServerHost({
709
712
  control: mcpControl,
710
- providers: {
711
- listWorkspaces: () => workspaceStore.load(),
712
- listSessions: () => service.list(),
713
- createWorkspace,
714
- restoreWorkspace,
715
- createSshSession: async (request) => {
716
- const session = await service.createSsh(request.target, request.name);
717
- const attached = service.attach(session.id, (data) => {
718
- const requestId = mcpActiveExecutions.get(session.id);
719
- if (requestId) {
720
- const output = `${mcpExecutionOutput.get(requestId) ?? ''}${data}`.slice(0, 16 * 1024);
721
- mcpExecutionOutput.set(requestId, output);
722
- if ((output.match(/ExitCode=\d+/g) ?? []).length >= 2) {
723
- const result = mcpExecutions.finishRunning(requestId, 'completed', output, false, 'Command completed.');
724
- clearMcpExecution(requestId, session.id);
725
- win?.webContents.send('mcp:execution', result);
726
- }
727
- }
728
- win?.webContents.send('terminal:data', session.id, data);
729
- }, (message) => win?.webContents.send('terminal:status', session.id, message));
730
- win?.webContents.send('mcp:ssh-session-created', { session: attached });
731
- return attached;
732
- },
733
- createProjectWorkspace,
734
- requestCommand: async (request) => {
735
- const sessions = await service.list();
736
- const session = sessions.find((item) => item.id === request.sessionId);
737
- if (!session)
738
- throw new Error('Unknown session.');
739
- const result = mcpExecutions.create({ clientId: request.clientId, sessionId: request.sessionId, sessionKind: session.kind, command: request.command, policy: { allowRemoteSessions: session.kind === 'ssh', ...(request.timeoutMs === undefined ? {} : { maxRuntimeMs: request.timeoutMs }), ...(request.outputBytes === undefined ? {} : { maxOutputBytes: request.outputBytes }) } });
740
- const safeRemote = session.kind === 'ssh' && isSafeRemoteCommand(result.command);
741
- if (safeRemote) {
742
- const approved = mcpExecutions.approveAutomatically(result.requestId);
743
- mcpAudit.record({ at: Date.now(), requestId: approved.requestId, clientId: approved.clientId, capability: 'session:execute', sessionId: approved.sessionId, state: 'approved', command: approved.displayCommand });
744
- await runMcpExecution(approved.requestId);
745
- return mcpExecutions.get(approved.requestId, request.clientId);
746
- }
747
- mcpAudit.record({ at: Date.now(), requestId: result.requestId, clientId: result.clientId, capability: 'session:execute', sessionId: result.sessionId, state: 'requested', command: result.displayCommand });
748
- win?.webContents.send('mcp:execution', result);
749
- return result;
750
- },
751
- getCommandResult: async (request) => mcpExecutions.get(request.requestId, request.clientId),
752
- cancelCommand: async (request) => {
753
- const current = mcpExecutions.get(request.requestId, request.clientId);
754
- const result = mcpExecutions.cancel(request.requestId, request.clientId);
755
- const session = 'sessionId' in current ? (await service.list()).find((item) => item.id === current.sessionId) : undefined;
756
- if (session?.kind === 'local')
757
- service.write(session.id, '\u0003');
758
- clearMcpExecution(request.requestId);
759
- return result;
760
- }
761
- },
713
+ providers: createMcpProviders(),
762
714
  version: app.getVersion()
763
715
  });
764
716
  void mcpHost.start();