@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.
- package/dist/app/application.d.ts +5 -0
- package/dist/app/application.js +55 -4
- package/dist/app/lifecycle.js +12 -0
- package/dist/app/workspace-access.d.ts +17 -4
- package/dist/core/operations/types.d.ts +1 -1
- package/dist/core/operations/types.js +1 -1
- package/dist/infrastructure/database/agent-mcp-settings-repository.d.ts +12 -0
- package/dist/infrastructure/database/agent-mcp-settings-repository.js +55 -0
- package/dist/infrastructure/database/app-database.js +3 -3
- package/dist/infrastructure/database/migrate.d.ts +2 -0
- package/dist/infrastructure/database/migrate.js +83 -0
- package/dist/infrastructure/database/schema.d.ts +3 -1
- package/dist/infrastructure/database/schema.js +35 -6
- package/dist/infrastructure/database/workspace-repository.d.ts +28 -0
- package/dist/infrastructure/database/workspace-repository.js +126 -0
- package/dist/mcp/custom/manager.d.ts +3 -1
- package/dist/mcp/custom/manager.js +37 -2
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +4 -1
- package/dist/mcp/server/plugin-mcp-registrar.js +7 -2
- package/dist/mcp/server/tool-control.d.ts +6 -1
- package/dist/mcp/server/tool-control.js +11 -2
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/plugins/agent/custom-mcp-store.d.ts +17 -0
- package/dist/plugins/agent/custom-mcp-store.js +38 -0
- package/dist/plugins/agent/mcp-gateway.d.ts +16 -0
- package/dist/plugins/agent/mcp-gateway.js +110 -0
- package/dist/plugins/agent/mcp.js +3 -2
- package/dist/plugins/agent/plugin.js +1 -1
- package/dist/plugins/agent/provider.d.ts +1 -0
- package/dist/plugins/agent/providers/codex/launch.d.ts +17 -0
- package/dist/plugins/agent/providers/codex/launch.js +24 -0
- package/dist/plugins/agent/providers/codex/provider.d.ts +3 -1
- package/dist/plugins/agent/providers/codex/provider.js +8 -2
- package/dist/plugins/agent/providers/codex/rpc.d.ts +7 -1
- package/dist/plugins/agent/providers/codex/rpc.js +28 -3
- package/dist/plugins/agent/providers/index.d.ts +2 -1
- package/dist/plugins/agent/providers/index.js +5 -2
- package/dist/plugins/agent/repository.js +10 -7
- package/dist/plugins/agent/runtime-files.d.ts +4 -4
- package/dist/plugins/agent/runtime-files.js +18 -14
- package/dist/plugins/agent/runtime.d.ts +2 -0
- package/dist/plugins/agent/runtime.js +52 -13
- package/dist/plugins/agent/types.d.ts +3 -2
- package/dist/plugins/cloud/v2/tunnel-client.js +1 -7
- package/dist/plugins/plugin-manager.d.ts +6 -1
- package/dist/plugins/plugin-manager.js +9 -2
- package/dist/plugins/types.d.ts +4 -0
- package/dist/plugins/web/http/agent-api-router.js +15 -1
- package/dist/plugins/web/http/git-api-router.js +13 -13
- package/dist/plugins/web/http/rtc-api-router.js +3 -0
- package/dist/plugins/web/http/runtime-api-router.js +22 -0
- package/dist/plugins/web/http/workspace-api-router.js +63 -12
- package/dist/plugins/web/plugin.js +1 -1
- package/dist/plugins/web/runtime.d.ts +3 -1
- package/dist/plugins/web/runtime.js +5 -1
- package/dist/plugins/workspace/mcp.js +20 -15
- package/dist/plugins/workspace/plugin.js +1 -1
- package/dist/plugins/workspace/workspace-root-policy.d.ts +0 -1
- package/dist/plugins/workspace/workspace-root-policy.js +25 -19
- package/dist/plugins/workspace/workspace-service.d.ts +20 -8
- package/dist/plugins/workspace/workspace-service.js +270 -64
- package/dist/web/assets/{AgentView-CMxIn3KB.css → AgentView-BqAfmgc4.css} +1 -1
- package/dist/web/assets/AgentView-tr6COjRg.js +4 -0
- package/dist/web/assets/{CloudView-CXuTK7Of.js → CloudView-D-fYLw_O.js} +1 -1
- 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
- package/dist/web/assets/ComputerView-B25C_C_h.js +1 -0
- package/dist/web/assets/ComputerView-vSSsexBY.css +1 -0
- package/dist/web/assets/{MarkdownContent-D4KYOn-f.js → MarkdownContent-BuDRrZ6c.js} +1 -1
- package/dist/web/assets/McpToolsView-D0G_HACK.css +1 -0
- package/dist/web/assets/McpToolsView-s0-Qvg2g.js +2 -0
- package/dist/web/assets/OperationsView-B_0SN40Y.js +4 -0
- package/dist/web/assets/{ProcessesView-NUAplTg4.js → ProcessesView-B2i-O0AL.js} +3 -3
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DwnqcvSQ.js +1 -0
- package/dist/web/assets/{SystemLogsView-DGW_4nS0.js → SystemLogsView-BR4FZoAh.js} +1 -1
- package/dist/web/assets/{TerminalView-DCqHCfb5.js → TerminalView-2Z1blITD.js} +1 -1
- package/dist/web/assets/VSelect-CHI_FOMl.js +1 -0
- package/dist/web/assets/VSheet-CKEhsY-7.js +1 -0
- package/dist/web/assets/VSpacer-FbdDPUGx.js +1 -0
- package/dist/web/assets/{VSwitch-o-WPjZs0.js → VSwitch-o9DuRTKF.js} +1 -1
- package/dist/web/assets/{VTabs-D2y6M66D.js → VTabs-BSfSKBtT.js} +1 -1
- package/dist/web/assets/{VTextarea-DFSNyIwH.js → VTextarea-D_kPhUVU.js} +1 -1
- package/dist/web/assets/WorkspaceDownloadPreview-BoALqL1M.js +1 -0
- package/dist/web/assets/WorkspaceDownloadPreview-dFslZB0W.css +1 -0
- package/dist/web/assets/{WorkspaceMediaPreview-tkpcPK82.js → WorkspaceMediaPreview-qjt04eze.js} +3 -3
- package/dist/web/assets/{WorkspacePdfPreview-PqIFAx2J.js → WorkspacePdfPreview-BrUJpmNs.js} +1 -1
- package/dist/web/assets/WorkspacesView-CHXmDTlX.js +2 -0
- package/dist/web/assets/WorkspacesView-Dt24eGQ4.css +1 -0
- package/dist/web/assets/index-9DLDvF8M.js +19 -0
- package/dist/web/assets/{useMasterDetailFocus-DgNjKho5.js → useMasterDetailFocus-Chh0cM6g.js} +1 -1
- package/dist/web/assets/useOperationLabels-DF67nIF0.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/transport-sw.js +43 -15
- package/package.json +1 -1
- package/dist/web/assets/AgentView-XEf1AUcH.js +0 -4
- package/dist/web/assets/ComputerView-Bz2Wsz2X.css +0 -1
- package/dist/web/assets/ComputerView-Cxn7zhrw.js +0 -1
- package/dist/web/assets/McpToolsView-DdYOoY9_.css +0 -1
- package/dist/web/assets/McpToolsView-XLtFPY_M.js +0 -2
- package/dist/web/assets/OperationsView-DRx2Qq8W.js +0 -4
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-BYoHRX9h.js +0 -1
- package/dist/web/assets/VSelect-Dla-KtWu.js +0 -1
- package/dist/web/assets/VSheet-B9d-Gdzq.js +0 -1
- package/dist/web/assets/VSpacer--LXbKJE2.js +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-DaFQwUIa.js +0 -1
- package/dist/web/assets/WorkspacesView-ClA6xcjn.css +0 -1
- package/dist/web/assets/WorkspacesView-DkGvIFCb.js +0 -2
- package/dist/web/assets/index-CacDJSDn.js +0 -19
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
68
|
-
args: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|