@zhanguym/chatroom 1.2.0 → 1.3.3

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.
Files changed (110) hide show
  1. package/dist/app/application.d.ts +5 -0
  2. package/dist/app/application.js +55 -4
  3. package/dist/app/lifecycle.js +12 -0
  4. package/dist/app/workspace-access.d.ts +17 -4
  5. package/dist/core/operations/types.d.ts +1 -1
  6. package/dist/core/operations/types.js +1 -1
  7. package/dist/infrastructure/database/agent-mcp-settings-repository.d.ts +12 -0
  8. package/dist/infrastructure/database/agent-mcp-settings-repository.js +55 -0
  9. package/dist/infrastructure/database/app-database.js +3 -3
  10. package/dist/infrastructure/database/migrate.d.ts +2 -0
  11. package/dist/infrastructure/database/migrate.js +83 -0
  12. package/dist/infrastructure/database/schema.d.ts +3 -1
  13. package/dist/infrastructure/database/schema.js +35 -6
  14. package/dist/infrastructure/database/workspace-repository.d.ts +28 -0
  15. package/dist/infrastructure/database/workspace-repository.js +126 -0
  16. package/dist/mcp/custom/manager.d.ts +3 -1
  17. package/dist/mcp/custom/manager.js +37 -2
  18. package/dist/mcp/server/plugin-mcp-registrar.d.ts +4 -1
  19. package/dist/mcp/server/plugin-mcp-registrar.js +7 -2
  20. package/dist/mcp/server/tool-control.d.ts +6 -1
  21. package/dist/mcp/server/tool-control.js +11 -2
  22. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  23. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  24. package/dist/plugins/agent/custom-mcp-store.d.ts +17 -0
  25. package/dist/plugins/agent/custom-mcp-store.js +38 -0
  26. package/dist/plugins/agent/mcp-gateway.d.ts +16 -0
  27. package/dist/plugins/agent/mcp-gateway.js +110 -0
  28. package/dist/plugins/agent/mcp.js +3 -2
  29. package/dist/plugins/agent/plugin.js +1 -1
  30. package/dist/plugins/agent/provider.d.ts +1 -0
  31. package/dist/plugins/agent/providers/codex/launch.d.ts +17 -0
  32. package/dist/plugins/agent/providers/codex/launch.js +24 -0
  33. package/dist/plugins/agent/providers/codex/provider.d.ts +3 -1
  34. package/dist/plugins/agent/providers/codex/provider.js +8 -2
  35. package/dist/plugins/agent/providers/codex/rpc.d.ts +7 -1
  36. package/dist/plugins/agent/providers/codex/rpc.js +28 -3
  37. package/dist/plugins/agent/providers/index.d.ts +2 -1
  38. package/dist/plugins/agent/providers/index.js +5 -2
  39. package/dist/plugins/agent/repository.js +10 -7
  40. package/dist/plugins/agent/runtime-files.d.ts +4 -4
  41. package/dist/plugins/agent/runtime-files.js +18 -14
  42. package/dist/plugins/agent/runtime.d.ts +2 -0
  43. package/dist/plugins/agent/runtime.js +52 -13
  44. package/dist/plugins/agent/types.d.ts +3 -2
  45. package/dist/plugins/cloud/v2/tunnel-client.js +1 -7
  46. package/dist/plugins/plugin-manager.d.ts +6 -1
  47. package/dist/plugins/plugin-manager.js +9 -2
  48. package/dist/plugins/types.d.ts +4 -0
  49. package/dist/plugins/web/http/agent-api-router.js +15 -1
  50. package/dist/plugins/web/http/git-api-router.js +13 -13
  51. package/dist/plugins/web/http/rtc-api-router.js +3 -0
  52. package/dist/plugins/web/http/runtime-api-router.js +22 -0
  53. package/dist/plugins/web/http/workspace-api-router.js +63 -12
  54. package/dist/plugins/web/plugin.js +1 -1
  55. package/dist/plugins/web/runtime.d.ts +3 -1
  56. package/dist/plugins/web/runtime.js +5 -1
  57. package/dist/plugins/workspace/mcp.js +20 -15
  58. package/dist/plugins/workspace/plugin.js +1 -1
  59. package/dist/plugins/workspace/workspace-root-policy.d.ts +0 -1
  60. package/dist/plugins/workspace/workspace-root-policy.js +25 -19
  61. package/dist/plugins/workspace/workspace-service.d.ts +20 -8
  62. package/dist/plugins/workspace/workspace-service.js +270 -64
  63. package/dist/web/assets/{AgentView-CMxIn3KB.css → AgentView-BqAfmgc4.css} +1 -1
  64. package/dist/web/assets/AgentView-tr6COjRg.js +4 -0
  65. package/dist/web/assets/{CloudView-CXuTK7Of.js → CloudView-D-fYLw_O.js} +1 -1
  66. package/dist/web/assets/{CodeViewer.vue_vue_type_script_setup_true_lang-DK6xGH2G.js → CodeViewer.vue_vue_type_script_setup_true_lang-BtA5aWrS.js} +1 -1
  67. package/dist/web/assets/ComputerView-B25C_C_h.js +1 -0
  68. package/dist/web/assets/ComputerView-vSSsexBY.css +1 -0
  69. package/dist/web/assets/{MarkdownContent-D4KYOn-f.js → MarkdownContent-BuDRrZ6c.js} +1 -1
  70. package/dist/web/assets/McpToolsView-D0G_HACK.css +1 -0
  71. package/dist/web/assets/McpToolsView-s0-Qvg2g.js +2 -0
  72. package/dist/web/assets/OperationsView-B_0SN40Y.js +4 -0
  73. package/dist/web/assets/{ProcessesView-NUAplTg4.js → ProcessesView-B2i-O0AL.js} +3 -3
  74. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DwnqcvSQ.js +1 -0
  75. package/dist/web/assets/{SystemLogsView-DGW_4nS0.js → SystemLogsView-BR4FZoAh.js} +1 -1
  76. package/dist/web/assets/{TerminalView-DCqHCfb5.js → TerminalView-2Z1blITD.js} +1 -1
  77. package/dist/web/assets/VSelect-CHI_FOMl.js +1 -0
  78. package/dist/web/assets/VSheet-CKEhsY-7.js +1 -0
  79. package/dist/web/assets/VSpacer-FbdDPUGx.js +1 -0
  80. package/dist/web/assets/{VSwitch-o-WPjZs0.js → VSwitch-o9DuRTKF.js} +1 -1
  81. package/dist/web/assets/{VTabs-D2y6M66D.js → VTabs-BSfSKBtT.js} +1 -1
  82. package/dist/web/assets/{VTextarea-DFSNyIwH.js → VTextarea-D_kPhUVU.js} +1 -1
  83. package/dist/web/assets/WorkspaceDownloadPreview-BoALqL1M.js +1 -0
  84. package/dist/web/assets/WorkspaceDownloadPreview-dFslZB0W.css +1 -0
  85. package/dist/web/assets/{WorkspaceMediaPreview-tkpcPK82.js → WorkspaceMediaPreview-qjt04eze.js} +3 -3
  86. package/dist/web/assets/{WorkspacePdfPreview-PqIFAx2J.js → WorkspacePdfPreview-BrUJpmNs.js} +1 -1
  87. package/dist/web/assets/WorkspacesView-CHXmDTlX.js +2 -0
  88. package/dist/web/assets/WorkspacesView-Dt24eGQ4.css +1 -0
  89. package/dist/web/assets/index-9DLDvF8M.js +19 -0
  90. package/dist/web/assets/{useMasterDetailFocus-DgNjKho5.js → useMasterDetailFocus-Chh0cM6g.js} +1 -1
  91. package/dist/web/assets/useOperationLabels-DF67nIF0.js +1 -0
  92. package/dist/web/index.html +1 -1
  93. package/dist/web/transport-sw.js +43 -15
  94. package/package.json +1 -1
  95. package/dist/web/assets/AgentView-XEf1AUcH.js +0 -4
  96. package/dist/web/assets/ComputerView-Bz2Wsz2X.css +0 -1
  97. package/dist/web/assets/ComputerView-Cxn7zhrw.js +0 -1
  98. package/dist/web/assets/McpToolsView-DdYOoY9_.css +0 -1
  99. package/dist/web/assets/McpToolsView-XLtFPY_M.js +0 -2
  100. package/dist/web/assets/OperationsView-DRx2Qq8W.js +0 -4
  101. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-BYoHRX9h.js +0 -1
  102. package/dist/web/assets/VSelect-Dla-KtWu.js +0 -1
  103. package/dist/web/assets/VSheet-B9d-Gdzq.js +0 -1
  104. package/dist/web/assets/VSpacer--LXbKJE2.js +0 -1
  105. package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +0 -1
  106. package/dist/web/assets/WorkspaceDownloadPreview-DaFQwUIa.js +0 -1
  107. package/dist/web/assets/WorkspacesView-ClA6xcjn.css +0 -1
  108. package/dist/web/assets/WorkspacesView-DkGvIFCb.js +0 -2
  109. package/dist/web/assets/index-CacDJSDn.js +0 -19
  110. package/dist/web/assets/useOperationLabels-BNiE-bRq.js +0 -1
@@ -76,14 +76,16 @@ export class CustomMcpManager {
76
76
  repository;
77
77
  control;
78
78
  operations;
79
+ operationSource;
79
80
  oauth;
80
81
  authorizationFlows = new Map();
81
82
  entries = new Map();
82
83
  closed = false;
83
- constructor(repository, control, operations, dataDir) {
84
+ constructor(repository, control, operations, dataDir, operationSource = "mcp") {
84
85
  this.repository = repository;
85
86
  this.control = control;
86
87
  this.operations = operations;
88
+ this.operationSource = operationSource;
87
89
  this.oauth = dataDir ? new CustomMcpOAuthStore(dataDir) : null;
88
90
  for (const config of repository.list())
89
91
  this.entries.set(config.id, newEntry(config));
@@ -94,6 +96,7 @@ export class CustomMcpManager {
94
96
  void this.connect(entry);
95
97
  }
96
98
  list() {
99
+ this.syncRepository();
97
100
  return [...this.entries.values()].map((entry) => ({
98
101
  id: entry.config.id,
99
102
  name: entry.config.name,
@@ -277,6 +280,7 @@ export class CustomMcpManager {
277
280
  (Object.keys(config.headers ?? {}).length ? "headers" : "auto"));
278
281
  }
279
282
  registerMcp(server) {
283
+ this.syncRepository();
280
284
  for (const entry of this.entries.values()) {
281
285
  if (!entry.config.enabled || entry.status !== "connected")
282
286
  continue;
@@ -308,7 +312,7 @@ export class CustomMcpManager {
308
312
  const client = entry.client;
309
313
  const operation = this.operations.start({
310
314
  pluginId: "custom-mcp",
311
- source: "mcp",
315
+ source: this.operationSource,
312
316
  action: entry.config.name + "/" + name,
313
317
  input: { serverId: id, toolName: name },
314
318
  });
@@ -475,6 +479,37 @@ export class CustomMcpManager {
475
479
  entry.config.enabled = false;
476
480
  await this.disconnect(entry);
477
481
  }
482
+ syncRepository() {
483
+ if (this.closed)
484
+ return;
485
+ const configs = this.repository.list();
486
+ const ids = new Set(configs.map((config) => config.id));
487
+ for (const [id, previous] of this.entries) {
488
+ if (ids.has(id))
489
+ continue;
490
+ this.entries.delete(id);
491
+ this.repository.delete(id);
492
+ void this.dispose(previous);
493
+ }
494
+ for (const config of configs) {
495
+ const existing = this.entries.get(config.id);
496
+ if (!existing) {
497
+ const entry = newEntry(config);
498
+ this.entries.set(config.id, entry);
499
+ if (config.enabled)
500
+ void this.connect(entry);
501
+ continue;
502
+ }
503
+ if (JSON.stringify(existing.config) === JSON.stringify(config))
504
+ continue;
505
+ const replacement = newEntry(config);
506
+ this.entries.set(config.id, replacement);
507
+ void this.dispose(existing).then(() => {
508
+ if (config.enabled && this.entries.get(config.id) === replacement)
509
+ return this.connect(replacement);
510
+ });
511
+ }
512
+ }
478
513
  require(id) {
479
514
  const entry = this.entries.get(id);
480
515
  if (!entry)
@@ -25,7 +25,10 @@ export declare class PluginMcpRegistrar {
25
25
  private readonly operations;
26
26
  private readonly pluginId;
27
27
  private readonly toolControl;
28
- constructor(server: McpServer | null, operations: OperationLog, pluginId: string, toolControl: McpToolControl);
28
+ private readonly options;
29
+ constructor(server: McpServer | null, operations: OperationLog, pluginId: string, toolControl: McpToolControl, options?: {
30
+ agentMode?: boolean;
31
+ });
29
32
  registerTool<InputSchema extends StandardSchemaWithJSON, OutputSchema extends StandardSchemaWithJSON>(name: string, config: PluginToolConfig<InputSchema, OutputSchema>, handler: (input: PluginToolInput<InputSchema>, execution: PluginToolExecution) => Promise<unknown> | unknown): void;
30
33
  }
31
34
  export {};
@@ -5,11 +5,13 @@ export class PluginMcpRegistrar {
5
5
  operations;
6
6
  pluginId;
7
7
  toolControl;
8
- constructor(server, operations, pluginId, toolControl) {
8
+ options;
9
+ constructor(server, operations, pluginId, toolControl, options = {}) {
9
10
  this.server = server;
10
11
  this.operations = operations;
11
12
  this.pluginId = pluginId;
12
13
  this.toolControl = toolControl;
14
+ this.options = options;
13
15
  }
14
16
  registerTool(name, config, handler) {
15
17
  this.toolControl.define({
@@ -17,6 +19,9 @@ export class PluginMcpRegistrar {
17
19
  pluginId: this.pluginId,
18
20
  title: config.title,
19
21
  description: config.description,
22
+ ...(this.options.agentMode
23
+ ? { agentAllowed: this.pluginId !== "agent" }
24
+ : {}),
20
25
  });
21
26
  if (!this.server)
22
27
  return;
@@ -24,7 +29,7 @@ export class PluginMcpRegistrar {
24
29
  const callback = mcpTool(async (input) => {
25
30
  const operation = this.operations.start({
26
31
  pluginId: this.pluginId,
27
- source: "mcp",
32
+ source: this.options.agentMode ? "agent_mcp" : "mcp",
28
33
  action: typeof action === "function" ? action(input) : action,
29
34
  input: audit?.input ? audit.input(input) : input,
30
35
  ...operationReferences(input),
@@ -10,6 +10,7 @@ export interface McpToolSummary {
10
10
  title: string;
11
11
  description: string;
12
12
  enabled: boolean;
13
+ agentAllowed?: boolean;
13
14
  }
14
15
  interface McpToolDefinition {
15
16
  name: string;
@@ -17,17 +18,21 @@ interface McpToolDefinition {
17
18
  title: string;
18
19
  description: string;
19
20
  defaultEnabled?: boolean;
21
+ /** Prevent a gateway from exposing management tools to the agent itself. */
22
+ agentAllowed?: boolean;
20
23
  }
21
24
  export declare class McpToolControl {
22
25
  private readonly store;
26
+ private readonly defaultEnabled;
23
27
  private readonly disabled;
24
28
  private readonly configured;
25
29
  private readonly definitions;
26
30
  private readonly handles;
27
- constructor(store: McpToolSettingStore);
31
+ constructor(store: McpToolSettingStore, defaultEnabled?: boolean);
28
32
  define(definition: McpToolDefinition): void;
29
33
  undefine(name: string): void;
30
34
  isEnabled(name: string): boolean;
35
+ definition(name: string): Readonly<McpToolDefinition> | null;
31
36
  attach(name: string, tool: RegisteredTool): void;
32
37
  list(): McpToolSummary[];
33
38
  setEnabled(name: string, enabled: boolean): McpToolSummary;
@@ -1,12 +1,14 @@
1
1
  import { ChatRoomError } from "#core/errors/chatroom-error";
2
2
  export class McpToolControl {
3
3
  store;
4
+ defaultEnabled;
4
5
  disabled = new Set();
5
6
  configured = new Set();
6
7
  definitions = new Map();
7
8
  handles = new Map();
8
- constructor(store) {
9
+ constructor(store, defaultEnabled = true) {
9
10
  this.store = store;
11
+ this.defaultEnabled = defaultEnabled;
10
12
  for (const name of store.disabledTools())
11
13
  this.disabled.add(name);
12
14
  for (const name of store.configuredTools?.() ?? [])
@@ -17,7 +19,9 @@ export class McpToolControl {
17
19
  if (existing && existing.pluginId !== definition.pluginId)
18
20
  throw new Error(`MCP tool already belongs to another plugin: ${definition.name}`);
19
21
  this.definitions.set(definition.name, definition);
20
- if (definition.defaultEnabled === false &&
22
+ if ((definition.defaultEnabled === false ||
23
+ !this.defaultEnabled ||
24
+ definition.agentAllowed === false) &&
21
25
  !this.configured.has(definition.name))
22
26
  this.setEnabled(definition.name, false);
23
27
  }
@@ -31,6 +35,9 @@ export class McpToolControl {
31
35
  isEnabled(name) {
32
36
  return this.definitions.has(name) && !this.disabled.has(name);
33
37
  }
38
+ definition(name) {
39
+ return this.definitions.get(name) ?? null;
40
+ }
34
41
  attach(name, tool) {
35
42
  let tools = this.handles.get(name);
36
43
  if (!tools) {
@@ -59,6 +66,8 @@ export class McpToolControl {
59
66
  const definition = this.definitions.get(name);
60
67
  if (!definition)
61
68
  throw new ChatRoomError("NOT_FOUND", `Unknown MCP tool: ${name}`);
69
+ if (enabled && definition.agentAllowed === false)
70
+ throw new ChatRoomError("FORBIDDEN", `Tool is not available to Agent MCP: ${name}`);
62
71
  if (enabled === !this.disabled.has(name))
63
72
  return { ...definition, enabled };
64
73
  this.store.setEnabled(name, enabled);
@@ -0,0 +1,17 @@
1
+ import type { CustomMcpRepository } from "#infrastructure/database/custom-mcp-repository";
2
+ import type { AgentMcpSettingsRepository } from "#infrastructure/database/agent-mcp-settings-repository";
3
+ import type { CustomMcpConfig, CustomMcpStore } from "#mcp/custom/types";
4
+ /**
5
+ * Shares server definitions and credentials with ChatRoom MCP, but owns the
6
+ * Agent-specific enabled state. Updating an Agent toggle never changes the
7
+ * enabled column in custom_mcp_servers.
8
+ */
9
+ export declare class AgentCustomMcpStore implements CustomMcpStore {
10
+ private readonly servers;
11
+ private readonly settings;
12
+ constructor(servers: CustomMcpRepository, settings: AgentMcpSettingsRepository);
13
+ list(): CustomMcpConfig[];
14
+ save(config: CustomMcpConfig): void;
15
+ delete(id: string): void;
16
+ private serverSettingId;
17
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shares server definitions and credentials with ChatRoom MCP, but owns the
3
+ * Agent-specific enabled state. Updating an Agent toggle never changes the
4
+ * enabled column in custom_mcp_servers.
5
+ */
6
+ export class AgentCustomMcpStore {
7
+ servers;
8
+ settings;
9
+ constructor(servers, settings) {
10
+ this.servers = servers;
11
+ this.settings = settings;
12
+ }
13
+ list() {
14
+ return this.servers.list().map((server) => ({
15
+ ...server,
16
+ enabled: this.settings.serverEnabled(this.serverSettingId(server.id)),
17
+ }));
18
+ }
19
+ save(config) {
20
+ const existing = this.servers
21
+ .list()
22
+ .find((server) => server.id === config.id);
23
+ this.servers.save({
24
+ ...config,
25
+ // Keep the ChatRoom MCP preference independent from the Agent preference.
26
+ enabled: existing?.enabled ?? false,
27
+ });
28
+ this.settings.setServerEnabled(this.serverSettingId(config.id), config.enabled);
29
+ }
30
+ delete(id) {
31
+ // Agent settings may be cleared independently; the shared server definition
32
+ // belongs to the ChatRoom MCP configuration and is never deleted here.
33
+ this.settings.deleteServerSetting(this.serverSettingId(id));
34
+ }
35
+ serverSettingId(id) {
36
+ return "custom:" + id;
37
+ }
38
+ }
@@ -0,0 +1,16 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ export interface AgentMcpConnection {
3
+ url: string;
4
+ token: string;
5
+ }
6
+ export declare class AgentMcpGateway {
7
+ private readonly createServerForRequest;
8
+ private server;
9
+ private readonly token;
10
+ private connectionValue;
11
+ constructor(createServerForRequest: () => McpServer);
12
+ connection(): AgentMcpConnection;
13
+ start(): Promise<void>;
14
+ close(): Promise<void>;
15
+ }
16
+ export declare function createAgentMcpServer(register: (server: McpServer) => void): McpServer;
@@ -0,0 +1,110 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
4
+ import { toNodeHandler } from "@modelcontextprotocol/node";
5
+ import { CHATROOM_VERSION } from "#core/runtime/identity";
6
+ export class AgentMcpGateway {
7
+ createServerForRequest;
8
+ server = null;
9
+ token = randomBytes(32).toString("base64url");
10
+ connectionValue = null;
11
+ constructor(createServerForRequest) {
12
+ this.createServerForRequest = createServerForRequest;
13
+ }
14
+ connection() {
15
+ if (!this.connectionValue)
16
+ throw new Error("Agent MCP gateway has not started");
17
+ return { ...this.connectionValue };
18
+ }
19
+ async start() {
20
+ if (this.server)
21
+ return;
22
+ const handler = createMcpHandler(() => this.createServerForRequest(), {
23
+ legacy: "stateless",
24
+ responseMode: "auto",
25
+ });
26
+ const nodeHandler = toNodeHandler(handler);
27
+ const server = createServer((req, res) => {
28
+ const host = req.headers.host;
29
+ let hostName = "";
30
+ try {
31
+ hostName = new URL("http://" + (host ?? "")).hostname;
32
+ }
33
+ catch {
34
+ /* reject below */
35
+ }
36
+ if (!["localhost", "127.0.0.1", "[::1]", "::1"].includes(hostName)) {
37
+ res.writeHead(403).end();
38
+ return;
39
+ }
40
+ const origin = req.headers.origin;
41
+ if (origin) {
42
+ let originHost = "";
43
+ try {
44
+ originHost = new URL(origin).hostname;
45
+ }
46
+ catch {
47
+ /* reject below */
48
+ }
49
+ if (!["localhost", "127.0.0.1", "[::1]", "::1"].includes(originHost)) {
50
+ res.writeHead(403).end();
51
+ return;
52
+ }
53
+ }
54
+ if (req.url !== "/mcp") {
55
+ res.writeHead(404).end();
56
+ return;
57
+ }
58
+ const authorization = req.headers.authorization ?? "";
59
+ if (authorization !== "Bearer " + this.token) {
60
+ res
61
+ .writeHead(401, {
62
+ "WWW-Authenticate": "Bearer",
63
+ "Cache-Control": "no-store",
64
+ })
65
+ .end();
66
+ return;
67
+ }
68
+ void nodeHandler(req, res);
69
+ });
70
+ this.server = server;
71
+ try {
72
+ await new Promise((resolve, reject) => {
73
+ server.once("error", reject);
74
+ server.listen(0, "127.0.0.1", () => {
75
+ server.off("error", reject);
76
+ resolve();
77
+ });
78
+ });
79
+ const address = server.address();
80
+ if (!address || typeof address === "string")
81
+ throw new Error("Agent MCP gateway did not bind a TCP port");
82
+ this.connectionValue = {
83
+ url: `http://127.0.0.1:${address.port}/mcp`,
84
+ token: this.token,
85
+ };
86
+ }
87
+ catch (error) {
88
+ this.server = null;
89
+ server.close();
90
+ throw error;
91
+ }
92
+ }
93
+ async close() {
94
+ const server = this.server;
95
+ this.server = null;
96
+ this.connectionValue = null;
97
+ if (!server?.listening)
98
+ return;
99
+ server.closeAllConnections();
100
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
101
+ }
102
+ }
103
+ export function createAgentMcpServer(register) {
104
+ const server = new McpServer({
105
+ name: "chatroom-agent-mcp",
106
+ version: CHATROOM_VERSION,
107
+ });
108
+ register(server);
109
+ return server;
110
+ }
@@ -79,7 +79,8 @@ const capabilities = z
79
79
  .nullable();
80
80
  const session = z.object({
81
81
  id: z.string(),
82
- workspaceRoot: z.string(),
82
+ workspaceId: z.string(),
83
+ workspaceName: z.string(),
83
84
  provider,
84
85
  ...settings,
85
86
  tokenUsage: tokenUsage.nullable(),
@@ -354,7 +355,7 @@ export function registerAgentTools(mcp, agents) {
354
355
  title: "Create Agent session",
355
356
  description: "Create an Agent session for a ChatRoom workspace using a registered provider. Choose settings from agent_models and agent_providers capabilities; omitted or null settings use provider/model defaults.",
356
357
  inputSchema: z.object({
357
- workspaceRoot: z.string().min(1),
358
+ workspaceId: z.string().min(1),
358
359
  provider,
359
360
  model: settings.model.default(null),
360
361
  reasoningEffort: settings.reasoningEffort.default(null),
@@ -11,7 +11,7 @@ export function createAgentPlugin() {
11
11
  return {
12
12
  id: "agent",
13
13
  async activate(context) {
14
- runtime = new AgentRuntime(new AgentRepository(context.database), context.services.require(WorkspaceAccessToken), context.events, createAgentProviders(context.logs));
14
+ runtime = new AgentRuntime(new AgentRepository(context.database), context.services.require(WorkspaceAccessToken), context.events, createAgentProviders(context.logs, context.agentMcp));
15
15
  await runtime.cleanupOrphanAttachments();
16
16
  attachmentCleanupTimer = setInterval(() => {
17
17
  void runtime?.cleanupOrphanAttachments().catch(() => undefined);
@@ -17,6 +17,7 @@ export interface AgentProvider {
17
17
  createSession(context: AgentProviderContext): Promise<ProviderSession>;
18
18
  resumeSession(context: AgentProviderContext, providerSessionId: string): Promise<ProviderSession>;
19
19
  close(): Promise<void>;
20
+ refreshMcpServers?(): Promise<void>;
20
21
  }
21
22
  export interface ProviderSession {
22
23
  readonly providerSessionId: string;
@@ -0,0 +1,17 @@
1
+ interface CodexLaunch {
2
+ command: string;
3
+ args: string[];
4
+ }
5
+ interface CodexLaunchOptions {
6
+ platform?: NodeJS.Platform;
7
+ path?: string;
8
+ nodeExecutable?: string;
9
+ exists?: (path: string) => boolean;
10
+ }
11
+ /**
12
+ * npm exposes codex.cmd on Windows, which child_process.spawn cannot execute
13
+ * without a shell. Launch the installed JS entry point directly with Node.
14
+ * Prefer an actual executable when one is present earlier on PATH.
15
+ */
16
+ export declare function resolveCodexLaunch({ platform, path, nodeExecutable, exists, }?: CodexLaunchOptions): CodexLaunch;
17
+ export {};
@@ -0,0 +1,24 @@
1
+ import { existsSync } from "node:fs";
2
+ import { win32 } from "node:path";
3
+ /**
4
+ * npm exposes codex.cmd on Windows, which child_process.spawn cannot execute
5
+ * without a shell. Launch the installed JS entry point directly with Node.
6
+ * Prefer an actual executable when one is present earlier on PATH.
7
+ */
8
+ export function resolveCodexLaunch({ platform = process.platform, path = process.env.PATH ?? "", nodeExecutable = process.execPath, exists = existsSync, } = {}) {
9
+ if (platform !== "win32")
10
+ return { command: "codex", args: [] };
11
+ for (const entry of path.split(";")) {
12
+ const directory = entry.trim().replace(/^"|"$/g, "");
13
+ if (!directory)
14
+ continue;
15
+ const executable = win32.join(directory, "codex.exe");
16
+ if (exists(executable))
17
+ return { command: executable, args: [] };
18
+ const npmShim = win32.join(directory, "codex.cmd");
19
+ const npmEntry = win32.join(directory, "node_modules", "@openai", "codex", "bin", "codex.js");
20
+ if (exists(npmShim) && exists(npmEntry))
21
+ return { command: nodeExecutable, args: [npmEntry] };
22
+ }
23
+ return { command: "codex", args: [] };
24
+ }
@@ -1,6 +1,7 @@
1
1
  import type { LogWriter } from "#core/logging/types";
2
2
  import type { AgentProvider, AgentProviderContext, ProviderSession } from "#plugins/agent/provider";
3
3
  import type { AgentModel, AgentProviderProbe, AgentProviderDetails } from "#plugins/agent/types";
4
+ import { type CodexMcpConnection } from "./rpc.js";
4
5
  export declare class CodexProvider implements AgentProvider {
5
6
  private readonly logs;
6
7
  readonly id = "codex";
@@ -73,12 +74,13 @@ export declare class CodexProvider implements AgentProvider {
73
74
  }];
74
75
  private readonly client;
75
76
  private readonly modelNames;
76
- constructor(logs: LogWriter);
77
+ constructor(logs: LogWriter, mcpConnection?: CodexMcpConnection);
77
78
  probe(): Promise<AgentProviderProbe>;
78
79
  details(): Promise<AgentProviderDetails>;
79
80
  listModels(): Promise<AgentModel[]>;
80
81
  createSession(context: AgentProviderContext): Promise<ProviderSession>;
81
82
  resumeSession(context: AgentProviderContext, providerSessionId: string): Promise<ProviderSession>;
82
83
  private resolveModel;
84
+ refreshMcpServers(): Promise<void>;
83
85
  close(): Promise<void>;
84
86
  }
@@ -88,9 +88,12 @@ export class CodexProvider {
88
88
  ];
89
89
  client;
90
90
  modelNames = new Map();
91
- constructor(logs) {
91
+ constructor(logs, mcpConnection = {
92
+ url: "http://127.0.0.1:1/mcp",
93
+ token: "test-only-token",
94
+ }) {
92
95
  this.logs = logs;
93
- this.client = new CodexRpcClient(logs);
96
+ this.client = new CodexRpcClient(logs, mcpConnection);
94
97
  }
95
98
  async probe() {
96
99
  try {
@@ -219,6 +222,9 @@ export class CodexProvider {
219
222
  throw new Error("Unknown Codex model: " + modelId);
220
223
  return model.model;
221
224
  }
225
+ async refreshMcpServers() {
226
+ await this.client.reloadMcpConfiguration();
227
+ }
222
228
  close() {
223
229
  return this.client.close();
224
230
  }
@@ -10,8 +10,13 @@ export interface CodexServerRequest extends CodexNotification {
10
10
  interface InitializeResult {
11
11
  userAgent: string;
12
12
  }
13
+ export interface CodexMcpConnection {
14
+ url: string;
15
+ token: string;
16
+ }
13
17
  export declare class CodexRpcClient {
14
18
  private readonly logs;
19
+ private readonly mcpConnection;
15
20
  private process;
16
21
  private readonly pending;
17
22
  private readonly notificationListeners;
@@ -20,9 +25,10 @@ export declare class CodexRpcClient {
20
25
  private nextId;
21
26
  private starting;
22
27
  private initialized;
23
- constructor(logs: LogWriter);
28
+ constructor(logs: LogWriter, mcpConnection?: CodexMcpConnection);
24
29
  ensureStarted(): Promise<InitializeResult>;
25
30
  request<T>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;
31
+ reloadMcpConfiguration(): Promise<void>;
26
32
  notify(method: string, params?: unknown): void;
27
33
  respond(id: CodexRequestId, result: unknown): void;
28
34
  respondError(id: CodexRequestId, code: number, message: string): void;
@@ -1,8 +1,10 @@
1
1
  import { LineProcess } from "#core/runtime/line-process";
2
2
  import { CHATROOM_VERSION } from "#core/runtime/identity";
3
+ import { resolveCodexLaunch } from "./launch.js";
3
4
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
4
5
  export class CodexRpcClient {
5
6
  logs;
7
+ mcpConnection;
6
8
  process = null;
7
9
  pending = new Map();
8
10
  notificationListeners = new Set();
@@ -11,8 +13,12 @@ export class CodexRpcClient {
11
13
  nextId = 1;
12
14
  starting = null;
13
15
  initialized = null;
14
- constructor(logs) {
16
+ constructor(logs, mcpConnection = {
17
+ url: "http://127.0.0.1:1/mcp",
18
+ token: "test-only-token",
19
+ }) {
15
20
  this.logs = logs;
21
+ this.mcpConnection = mcpConnection;
16
22
  }
17
23
  async ensureStarted() {
18
24
  if (this.initialized)
@@ -31,6 +37,13 @@ export class CodexRpcClient {
31
37
  await this.ensureStarted();
32
38
  return this.requestReady(method, params, timeoutMs);
33
39
  }
40
+ async reloadMcpConfiguration() {
41
+ // Before Codex starts, the MCP config passed on its command line will be
42
+ // picked up by the initial connection; avoid spawning Codex just for UI toggles.
43
+ if (!this.initialized)
44
+ return;
45
+ await this.requestReady("config/mcpServer/reload", {});
46
+ }
34
47
  notify(method, params) {
35
48
  this.write(params === undefined ? { method } : { method, params });
36
49
  }
@@ -62,10 +75,22 @@ export class CodexRpcClient {
62
75
  process.forceKill();
63
76
  }
64
77
  async start() {
78
+ const launch = resolveCodexLaunch();
65
79
  let process;
66
80
  process = new LineProcess({
67
- command: "codex",
68
- args: ["app-server", "--listen", "stdio://", "-c", "mcp_servers={}"],
81
+ command: launch.command,
82
+ args: [
83
+ ...launch.args,
84
+ "app-server",
85
+ "--listen",
86
+ "stdio://",
87
+ "-c",
88
+ "mcp_servers.chatroom_agent.url=" +
89
+ JSON.stringify(this.mcpConnection.url),
90
+ "-c",
91
+ 'mcp_servers.chatroom_agent.bearer_token_env_var="CHATROOM_AGENT_MCP_TOKEN"',
92
+ ],
93
+ env: { CHATROOM_AGENT_MCP_TOKEN: this.mcpConnection.token },
69
94
  onStdout: (line) => this.handleLine(line),
70
95
  onStderr: (line) => {
71
96
  const message = line.trim();
@@ -1,3 +1,4 @@
1
1
  import type { LogWriter } from "#core/logging/types";
2
2
  import type { AgentProvider } from "../provider.js";
3
- export declare function createAgentProviders(logs: LogWriter): AgentProvider[];
3
+ import type { AgentMcpConnection } from "../mcp-gateway.js";
4
+ export declare function createAgentProviders(logs: LogWriter, mcpConnection?: AgentMcpConnection): AgentProvider[];
@@ -1,4 +1,7 @@
1
1
  import { CodexProvider } from "./codex/provider.js";
2
- export function createAgentProviders(logs) {
3
- return [new CodexProvider(logs)];
2
+ export function createAgentProviders(logs, mcpConnection = {
3
+ url: "http://127.0.0.1:1/mcp",
4
+ token: "test-only-token",
5
+ }) {
6
+ return [new CodexProvider(logs, mcpConnection)];
4
7
  }