@zhanguym/chatroom 1.1.0 → 1.2.0-beta.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/dist/app/application.d.ts +2 -0
- package/dist/app/application.js +7 -1
- package/dist/app/lifecycle.js +6 -0
- package/dist/auth/auth-service.js +5 -1
- package/dist/core/runtime/child-environment.js +3 -2
- package/dist/infrastructure/database/custom-mcp-repository.d.ts +9 -0
- package/dist/infrastructure/database/custom-mcp-repository.js +37 -0
- package/dist/infrastructure/database/mcp-tool-settings-repository.d.ts +1 -0
- package/dist/infrastructure/database/mcp-tool-settings-repository.js +6 -0
- package/dist/infrastructure/database/schema.d.ts +1 -1
- package/dist/infrastructure/database/schema.js +9 -0
- package/dist/infrastructure/http/http-server.js +7 -1
- package/dist/mcp/custom/manager.d.ts +54 -0
- package/dist/mcp/custom/manager.js +494 -0
- package/dist/mcp/custom/oauth.d.ts +38 -0
- package/dist/mcp/custom/oauth.js +116 -0
- package/dist/mcp/custom/types.d.ts +42 -0
- package/dist/mcp/custom/types.js +1 -0
- package/dist/mcp/server/create-mcp-server.d.ts +2 -1
- package/dist/mcp/server/create-mcp-server.js +4 -3
- package/dist/mcp/server/tool-control.d.ts +5 -0
- package/dist/mcp/server/tool-control.js +17 -0
- package/dist/native/linux/chatroom-computer-helper +0 -0
- 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/cloud/tunnel-client.d.ts +2 -84
- package/dist/plugins/cloud/tunnel-client.js +2 -426
- package/dist/plugins/cloud/v2/stream.d.ts +48 -0
- package/dist/plugins/cloud/v2/stream.js +156 -0
- package/dist/plugins/cloud/v2/tunnel-client.d.ts +45 -0
- package/dist/plugins/cloud/v2/tunnel-client.js +426 -0
- package/dist/plugins/computer/computer-schemas.d.ts +8 -8
- package/dist/plugins/process/backend.d.ts +3 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +2 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.js +5 -2
- package/dist/plugins/process/infrastructure/terminal-shell.d.ts +4 -0
- package/dist/plugins/process/infrastructure/terminal-shell.js +10 -0
- package/dist/plugins/process/process-supervisor.d.ts +6 -0
- package/dist/plugins/process/process-supervisor.js +26 -0
- package/dist/plugins/process/types.d.ts +2 -0
- package/dist/plugins/types.d.ts +2 -0
- package/dist/plugins/web/api-types.d.ts +2 -0
- package/dist/plugins/web/http/api-router.d.ts +2 -1
- package/dist/plugins/web/http/api-router.js +8 -1
- package/dist/plugins/web/http/custom-mcp-api-router.d.ts +6 -0
- package/dist/plugins/web/http/custom-mcp-api-router.js +114 -0
- package/dist/plugins/web/http/logs-api-router.js +2 -2
- package/dist/plugins/web/http/process-api-router.js +48 -0
- package/dist/plugins/web/http/rtc-api-router.d.ts +4 -0
- package/dist/plugins/web/http/rtc-api-router.js +316 -0
- package/dist/plugins/web/http/runtime-api-router.js +2 -2
- package/dist/plugins/web/http/terminal-api-router.d.ts +4 -0
- package/dist/plugins/web/http/terminal-api-router.js +132 -0
- package/dist/plugins/web/plugin.js +1 -1
- package/dist/plugins/web/runtime.d.ts +3 -1
- package/dist/plugins/web/runtime.js +3 -1
- package/dist/web/assets/AgentView-BEA5mQVv.js +4 -0
- package/dist/web/assets/{AgentView-BGLIBoxF.css → AgentView-CMxIn3KB.css} +1 -1
- package/dist/web/assets/CloudView-BGcVSE22.js +1 -0
- package/dist/web/assets/CloudView-Bgp5G965.css +1 -0
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-DfPmlGC0.js +3 -0
- package/dist/web/assets/ComputerView-Bz2Wsz2X.css +1 -0
- package/dist/web/assets/ComputerView-D6_NSXKb.js +1 -0
- package/dist/web/assets/{MarkdownContent-CkjR5fs7.js → MarkdownContent-DCkAALOF.js} +2 -2
- package/dist/web/assets/McpToolsView--8UloObb.js +2 -0
- package/dist/web/assets/McpToolsView-DdYOoY9_.css +1 -0
- package/dist/web/assets/OperationsView-CqgAPnSL.js +4 -0
- package/dist/web/assets/ProcessesView-BmNdR3C0.css +1 -0
- package/dist/web/assets/ProcessesView-DC0tjiYU.js +5 -0
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-D_LKEtqT.js +1 -0
- package/dist/web/assets/SystemLogsView-COvwamo5.css +1 -0
- package/dist/web/assets/SystemLogsView-DkTeUr2k.js +1 -0
- package/dist/web/assets/TerminalView-Coze7IMj.css +1 -0
- package/dist/web/assets/TerminalView-DEP1U0aW.js +16 -0
- package/dist/web/assets/VSelect-B5-t612C.js +1 -0
- package/dist/web/assets/VSheet-CNayLacf.js +1 -0
- package/dist/web/assets/VSpacer-BYe3Z99W.js +1 -0
- package/dist/web/assets/VSwitch-cVRCnipJ.js +1 -0
- package/dist/web/assets/VTabs-HKX286IT.js +1 -0
- package/dist/web/assets/VTextarea-ypCBuKI-.js +1 -0
- package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +1 -0
- package/dist/web/assets/WorkspaceDownloadPreview-qwbTkU59.js +1 -0
- package/dist/web/assets/WorkspaceMediaPreview-D28Yf2yb.js +4 -0
- package/dist/web/assets/{WorkspacePdfPreview-CO-ZtDHG.js → WorkspacePdfPreview-BfZSPcR-.js} +2 -2
- package/dist/web/assets/WorkspacesView-CsCGtk5H.js +2 -0
- package/dist/web/assets/index-BCUxnASh.css +1 -0
- package/dist/web/assets/index-CFhV1jOj.js +19 -0
- package/dist/web/assets/{useMasterDetailFocus-D1mQ8Qjk.js → useMasterDetailFocus-CMJXBPPp.js} +1 -1
- package/dist/web/assets/useOperationLabels-DSbMa9Eb.js +1 -0
- package/dist/web/index.html +2 -2
- package/dist/web/transport-sw.js +417 -0
- package/package.json +10 -4
- package/scripts/ensure-pty-helper.mjs +28 -0
- package/dist/web/assets/AgentView-Dd6zCbr-.js +0 -4
- package/dist/web/assets/CloudView-DVVvwuWW.css +0 -1
- package/dist/web/assets/CloudView-DquY1HDZ.js +0 -1
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-ekOImmBP.js +0 -3
- package/dist/web/assets/ComputerView-DWzfSYJ7.css +0 -1
- package/dist/web/assets/ComputerView-RD2gMxJZ.js +0 -1
- package/dist/web/assets/McpToolsView-Dg2-iaDP.js +0 -1
- package/dist/web/assets/McpToolsView-DjxPbTHj.css +0 -1
- package/dist/web/assets/OperationsView-C8yCX7WZ.js +0 -4
- package/dist/web/assets/ProcessesView-C834LC-f.js +0 -4
- package/dist/web/assets/ProcessesView-Ce6iVwjA.css +0 -1
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-By2gilBw.js +0 -1
- package/dist/web/assets/SystemLogsView-B6KcZv1J.js +0 -1
- package/dist/web/assets/SystemLogsView-CofP4Jvi.css +0 -1
- package/dist/web/assets/VSelect-BtAJX-XJ.js +0 -1
- package/dist/web/assets/VSpacer-M8bpkhTH.js +0 -1
- package/dist/web/assets/VSwitch-C0VTnwLJ.js +0 -1
- package/dist/web/assets/VTabs-g61mykvY.js +0 -1
- package/dist/web/assets/VTextarea-CRYQYRqg.js +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-BrESF-82.js +0 -1
- package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +0 -1
- package/dist/web/assets/WorkspaceMediaPreview-HYajqgZ3.js +0 -4
- package/dist/web/assets/WorkspacesView-BJCnzBLj.js +0 -2
- package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
- package/dist/web/assets/index-CZ-4tJlF.css +0 -1
- package/dist/web/assets/index-u-aADQP-.js +0 -19
- package/dist/web/assets/useOperationLabels-BjPIVnJW.js +0 -1
- package/dist/web/assets/utils-rtPwWNel.js +0 -1
- /package/dist/web/assets/{CodeViewer-CH1DEJge.css → VSheet-CH1DEJge.css} +0 -0
|
@@ -12,8 +12,8 @@ export class PtyProcessBackend {
|
|
|
12
12
|
const env = Object.fromEntries(Object.entries(options.env).filter((entry) => typeof entry[1] === "string"));
|
|
13
13
|
const processHandle = pty.spawn(command, args, {
|
|
14
14
|
name: env.TERM ?? "xterm-256color",
|
|
15
|
-
cols: 120,
|
|
16
|
-
rows: 30,
|
|
15
|
+
cols: options.cols ?? 120,
|
|
16
|
+
rows: options.rows ?? 30,
|
|
17
17
|
cwd: options.cwd,
|
|
18
18
|
env,
|
|
19
19
|
});
|
|
@@ -31,6 +31,9 @@ class PtyBackendProcess {
|
|
|
31
31
|
write(data) {
|
|
32
32
|
this.processHandle.write(data);
|
|
33
33
|
}
|
|
34
|
+
resize(cols, rows) {
|
|
35
|
+
this.processHandle.resize(cols, rows);
|
|
36
|
+
}
|
|
34
37
|
kill(signal = "SIGTERM") {
|
|
35
38
|
if (process.platform === "win32") {
|
|
36
39
|
this.processHandle.kill();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Select the OS default interactive shell for a local terminal session. */
|
|
2
|
+
export declare function defaultTerminalShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
|
|
3
|
+
/** macOS Terminal starts an interactive login shell; match its PATH initialization. */
|
|
4
|
+
export declare function defaultTerminalArgs(platform?: NodeJS.Platform): string[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Select the OS default interactive shell for a local terminal session. */
|
|
2
|
+
export function defaultTerminalShell(platform = process.platform, env = process.env) {
|
|
3
|
+
return platform === "win32"
|
|
4
|
+
? env.ComSpec || env.COMSPEC || "cmd.exe"
|
|
5
|
+
: env.SHELL || "/bin/sh";
|
|
6
|
+
}
|
|
7
|
+
/** macOS Terminal starts an interactive login shell; match its PATH initialization. */
|
|
8
|
+
export function defaultTerminalArgs(platform = process.platform) {
|
|
9
|
+
return platform === "darwin" ? ["-l"] : [];
|
|
10
|
+
}
|
|
@@ -27,6 +27,12 @@ export declare class ProcessSupervisor {
|
|
|
27
27
|
list(): ProcessSnapshot[];
|
|
28
28
|
summaries(): ProcessSummary[];
|
|
29
29
|
write(processId: ProcessId, data: string): ProcessSnapshot;
|
|
30
|
+
/** Subscribe to raw PTY output; terminal reconnects start from the snapshot. */
|
|
31
|
+
subscribeOutput(processId: ProcessId, listener: (chunk: Buffer | null, stream?: "stdout" | "stderr") => void): {
|
|
32
|
+
snapshot: ProcessSnapshot;
|
|
33
|
+
unsubscribe: () => void;
|
|
34
|
+
};
|
|
35
|
+
resize(processId: ProcessId, cols: number, rows: number): void;
|
|
30
36
|
kill(processId: ProcessId, force?: boolean): ProcessSnapshot;
|
|
31
37
|
shutdown(): Promise<void>;
|
|
32
38
|
private onOutput;
|
|
@@ -36,6 +36,7 @@ export class ProcessSupervisor {
|
|
|
36
36
|
args,
|
|
37
37
|
cwd: request.cwd,
|
|
38
38
|
pty: request.pty ?? false,
|
|
39
|
+
...(request.pty ? { cols: request.cols, rows: request.rows } : {}),
|
|
39
40
|
timeoutMs: request.timeoutMs ?? this.defaultTimeoutMs,
|
|
40
41
|
env: request.env ?? {},
|
|
41
42
|
},
|
|
@@ -47,6 +48,8 @@ export class ProcessSupervisor {
|
|
|
47
48
|
backend = await (request.pty ? this.backends.pty : this.backends.pipe).start(request.command, args, {
|
|
48
49
|
cwd: request.cwd,
|
|
49
50
|
env: childEnvironment(request.env),
|
|
51
|
+
...(request.cols !== undefined ? { cols: request.cols } : {}),
|
|
52
|
+
...(request.rows !== undefined ? { rows: request.rows } : {}),
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
catch (error) {
|
|
@@ -81,6 +84,7 @@ export class ProcessSupervisor {
|
|
|
81
84
|
timeout: null,
|
|
82
85
|
forceTimeout: null,
|
|
83
86
|
outputEventTimer: null,
|
|
87
|
+
outputListeners: new Set(),
|
|
84
88
|
timedOut: false,
|
|
85
89
|
operationId,
|
|
86
90
|
settled,
|
|
@@ -129,6 +133,23 @@ export class ProcessSupervisor {
|
|
|
129
133
|
item.backend.write(data);
|
|
130
134
|
return this.snapshotOf(processId, item);
|
|
131
135
|
}
|
|
136
|
+
/** Subscribe to raw PTY output; terminal reconnects start from the snapshot. */
|
|
137
|
+
subscribeOutput(processId, listener) {
|
|
138
|
+
const item = this.require(processId);
|
|
139
|
+
item.outputListeners.add(listener);
|
|
140
|
+
return {
|
|
141
|
+
snapshot: this.snapshotOf(processId, item),
|
|
142
|
+
unsubscribe: () => item.outputListeners.delete(listener),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
resize(processId, cols, rows) {
|
|
146
|
+
const item = this.requireRunning(processId);
|
|
147
|
+
if (!item.request.pty || !item.backend.resize)
|
|
148
|
+
throw new ChatRoomError("UNSUPPORTED", "Not a resizable PTY");
|
|
149
|
+
if (![cols, rows].every((value) => Number.isInteger(value) && value >= 2 && value <= 500))
|
|
150
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid terminal dimensions");
|
|
151
|
+
item.backend.resize(cols, rows);
|
|
152
|
+
}
|
|
132
153
|
kill(processId, force = false) {
|
|
133
154
|
const item = this.requireRunning(processId);
|
|
134
155
|
item.backend.kill(force ? "SIGKILL" : "SIGTERM");
|
|
@@ -161,6 +182,8 @@ export class ProcessSupervisor {
|
|
|
161
182
|
if (!item || item.state !== "running")
|
|
162
183
|
return;
|
|
163
184
|
(stream === "stdout" ? item.stdout : item.stderr).append(chunk);
|
|
185
|
+
for (const listener of item.outputListeners)
|
|
186
|
+
listener(chunk, stream);
|
|
164
187
|
if (!item.outputEventTimer) {
|
|
165
188
|
item.outputEventTimer = setTimeout(() => {
|
|
166
189
|
item.outputEventTimer = null;
|
|
@@ -183,6 +206,9 @@ export class ProcessSupervisor {
|
|
|
183
206
|
item.exitCode = exitCode;
|
|
184
207
|
item.signal = signal;
|
|
185
208
|
item.state = signal ? "killed" : exitCode === 0 ? "exited" : "failed";
|
|
209
|
+
for (const listener of item.outputListeners)
|
|
210
|
+
listener(null);
|
|
211
|
+
item.outputListeners.clear();
|
|
186
212
|
const snapshot = this.snapshotOf(processId, item);
|
|
187
213
|
this.operations.finish(item.operationId, exitCode === 0 && !signal ? "success" : signal ? "cancelled" : "error", {
|
|
188
214
|
stdout: snapshot.stdout,
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AppDatabase } from "#infrastructure/database/app-database";
|
|
|
3
3
|
import type { PluginMcpRegistrar } from "#mcp/server/plugin-mcp-registrar";
|
|
4
4
|
import type { OperationLog } from "#operations/operation-log";
|
|
5
5
|
import type { McpToolControl } from "#mcp/server/tool-control";
|
|
6
|
+
import type { CustomMcpManager } from "#mcp/custom/manager";
|
|
6
7
|
import type { RuntimeEventBus } from "#app/event-bus";
|
|
7
8
|
import type { ExternalAccessRegistry } from "#app/external-access-registry";
|
|
8
9
|
import type { LogWriter } from "#core/logging/types";
|
|
@@ -12,6 +13,7 @@ export interface PluginContext {
|
|
|
12
13
|
database: AppDatabase;
|
|
13
14
|
operations: OperationLog;
|
|
14
15
|
mcpTools: McpToolControl;
|
|
16
|
+
customMcp: CustomMcpManager;
|
|
15
17
|
events: RuntimeEventBus;
|
|
16
18
|
externalAccess: ExternalAccessRegistry;
|
|
17
19
|
services: ServiceRegistry;
|
|
@@ -2,6 +2,7 @@ import type { Operation as DomainOperation } from "#core/operations/types";
|
|
|
2
2
|
import type { GitBranch, GitChange, GitCommit, GitDiff, GitStatus } from "#app/git-access";
|
|
3
3
|
import type { ProcessSnapshot, ProcessSummary } from "#plugins/process/types";
|
|
4
4
|
import type { AgentAttachment, AgentHistory, AgentHistoryUpdate, AgentInteractionResponse, AgentItem, AgentModel, AgentProviderId, AgentProviderDetails, AgentReviewTarget, AgentProviderStatus, AgentSession, AgentTurn, AgentTurnSource } from "#plugins/agent/types";
|
|
5
|
+
import type { CustomMcpServerSummary } from "#mcp/custom/types";
|
|
5
6
|
import type { McpToolSummary as DomainMcpToolSummary } from "#mcp/server/tool-control";
|
|
6
7
|
import type { CloudServiceId, CloudStatus as DomainCloudStatus } from "#plugins/cloud/types";
|
|
7
8
|
import type { ComputerDisplay, ComputerPermission, ComputerStatus } from "#plugins/computer/types";
|
|
@@ -46,6 +47,7 @@ export interface UpdateStatus {
|
|
|
46
47
|
releaseUrl: string | null;
|
|
47
48
|
}
|
|
48
49
|
export type McpToolSummary = DomainMcpToolSummary;
|
|
50
|
+
export type { CustomMcpServerSummary };
|
|
49
51
|
export type CloudService = CloudServiceId;
|
|
50
52
|
export type CloudStatus = DomainCloudStatus;
|
|
51
53
|
export interface WorkspaceFileContent {
|
|
@@ -6,7 +6,8 @@ import type { PasskeyService } from "#auth/passkey-service";
|
|
|
6
6
|
import type { IngressPolicy } from "#auth/ingress-policy";
|
|
7
7
|
import type { CloudController } from "#plugins/cloud/controller";
|
|
8
8
|
import type { LogService } from "#core/logging/types";
|
|
9
|
-
|
|
9
|
+
import type { ChatRoomConfig } from "#config/types";
|
|
10
|
+
export declare function createApiRouter(application: WebRuntime, eventBus: RuntimeEventBus, auth: AuthService, passkeys: PasskeyService, ingress: IngressPolicy, cloud: CloudController, logs: LogService, config: ChatRoomConfig, runtimeStatus: () => {
|
|
10
11
|
version: string;
|
|
11
12
|
mcpRequests: number;
|
|
12
13
|
uptimeMinutes: number;
|
|
@@ -2,20 +2,26 @@ import { Router } from "express";
|
|
|
2
2
|
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
3
3
|
import { boundedIntegerQuery, requireString, } from "#presentation/http/http-utils";
|
|
4
4
|
import { createProcessApiRouter } from "./process-api-router.js";
|
|
5
|
+
import { createTerminalApiRouter } from "./terminal-api-router.js";
|
|
5
6
|
import { createWorkspaceApiRouter } from "./workspace-api-router.js";
|
|
6
7
|
import { createGitApiRouter } from "./git-api-router.js";
|
|
7
8
|
import { createCloudApiRouter } from "./cloud-api-router.js";
|
|
8
9
|
import { createComputerApiRouter } from "./computer-api-router.js";
|
|
9
10
|
import { apiAuthentication, createPrivateAuthApiRouter, createPublicAuthApiRouter, } from "./auth-api-router.js";
|
|
11
|
+
import { createCustomMcpApiRouter, createCustomMcpOAuthCallbackRouter, } from "./custom-mcp-api-router.js";
|
|
10
12
|
import { createRuntimeApiRouter } from "./runtime-api-router.js";
|
|
11
13
|
import { createLogsApiRouter } from "./logs-api-router.js";
|
|
12
14
|
import { createAgentApiRouter } from "./agent-api-router.js";
|
|
13
|
-
|
|
15
|
+
import { createRtcApiRouter } from "./rtc-api-router.js";
|
|
16
|
+
export function createApiRouter(application, eventBus, auth, passkeys, ingress, cloud, logs, config, runtimeStatus) {
|
|
14
17
|
const router = Router();
|
|
15
18
|
router.use(createPublicAuthApiRouter(auth, passkeys, ingress, logs));
|
|
19
|
+
router.use(createCustomMcpOAuthCallbackRouter(application.customMcp));
|
|
16
20
|
router.use(apiAuthentication(auth, ingress));
|
|
17
21
|
router.use(createPrivateAuthApiRouter(passkeys, ingress, logs));
|
|
22
|
+
router.use(createRtcApiRouter(config));
|
|
18
23
|
router.use(createRuntimeApiRouter(application, eventBus, runtimeStatus));
|
|
24
|
+
router.use(createCustomMcpApiRouter(application.customMcp, ingress));
|
|
19
25
|
router.use(createLogsApiRouter(logs));
|
|
20
26
|
router.get("/operations", (req, res) => {
|
|
21
27
|
res.json(application.operations.list({
|
|
@@ -49,6 +55,7 @@ export function createApiRouter(application, eventBus, auth, passkeys, ingress,
|
|
|
49
55
|
router.use(createWorkspaceApiRouter(application));
|
|
50
56
|
router.use(createGitApiRouter(application));
|
|
51
57
|
router.use(createProcessApiRouter(application));
|
|
58
|
+
router.use(createTerminalApiRouter(application));
|
|
52
59
|
router.use(createAgentApiRouter(application.agents));
|
|
53
60
|
router.use(createComputerApiRouter(application.computer, application.operations, ingress));
|
|
54
61
|
router.use(createCloudApiRouter(cloud, application.operations));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import type { IngressPolicy } from "#auth/ingress-policy";
|
|
3
|
+
import type { CustomMcpManager } from "#mcp/custom/manager";
|
|
4
|
+
export declare function createCustomMcpApiRouter(manager: CustomMcpManager, ingress: IngressPolicy): Router;
|
|
5
|
+
/** Public OAuth callback: the random state is single-use and scoped to an initiated login. */
|
|
6
|
+
export declare function createCustomMcpOAuthCallbackRouter(manager: CustomMcpManager): Router;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
3
|
+
import { bodyRecord, requireString } from "#presentation/http/http-utils";
|
|
4
|
+
function settings(body, partial = false) {
|
|
5
|
+
const config = {};
|
|
6
|
+
const str = (key) => {
|
|
7
|
+
if (typeof body[key] !== "string")
|
|
8
|
+
throw new ChatRoomError("INVALID_INPUT", key + " must be a string");
|
|
9
|
+
return body[key];
|
|
10
|
+
};
|
|
11
|
+
if (!partial || "name" in body)
|
|
12
|
+
config.name = str("name").trim();
|
|
13
|
+
if (!partial || "type" in body) {
|
|
14
|
+
const type = str("type");
|
|
15
|
+
if (type !== "stdio" && type !== "http")
|
|
16
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported MCP transport");
|
|
17
|
+
config.type = type;
|
|
18
|
+
}
|
|
19
|
+
for (const key of ["command", "cwd", "url"])
|
|
20
|
+
if (key in body)
|
|
21
|
+
config[key] = str(key);
|
|
22
|
+
if ("args" in body) {
|
|
23
|
+
if (!Array.isArray(body.args) ||
|
|
24
|
+
body.args.some((value) => typeof value !== "string"))
|
|
25
|
+
throw new ChatRoomError("INVALID_INPUT", "args must be an array of strings");
|
|
26
|
+
config.args = body.args;
|
|
27
|
+
}
|
|
28
|
+
for (const key of ["env", "headers"]) {
|
|
29
|
+
if (!(key in body))
|
|
30
|
+
continue;
|
|
31
|
+
const data = body[key];
|
|
32
|
+
if (!data ||
|
|
33
|
+
typeof data !== "object" ||
|
|
34
|
+
Array.isArray(data) ||
|
|
35
|
+
Object.entries(data).some(([name, value]) => !name.trim() || typeof value !== "string"))
|
|
36
|
+
throw new ChatRoomError("INVALID_INPUT", key + " must be a string map");
|
|
37
|
+
config[key] = data;
|
|
38
|
+
}
|
|
39
|
+
if ("authMode" in body) {
|
|
40
|
+
const value = str("authMode");
|
|
41
|
+
if (!["auto", "none", "headers"].includes(value))
|
|
42
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported MCP authentication");
|
|
43
|
+
config.authMode = value;
|
|
44
|
+
}
|
|
45
|
+
if (!partial || "enabled" in body) {
|
|
46
|
+
if (typeof body.enabled !== "boolean")
|
|
47
|
+
throw new ChatRoomError("INVALID_INPUT", "enabled must be a boolean");
|
|
48
|
+
config.enabled = body.enabled;
|
|
49
|
+
}
|
|
50
|
+
return config;
|
|
51
|
+
}
|
|
52
|
+
export function createCustomMcpApiRouter(manager, ingress) {
|
|
53
|
+
const router = Router();
|
|
54
|
+
router.get("/mcp/servers", (_req, res) => res.json(manager.list()));
|
|
55
|
+
router.post("/mcp/servers", async (req, res) => {
|
|
56
|
+
const config = settings(bodyRecord(req.body));
|
|
57
|
+
res.status(201).json(await manager.create(config));
|
|
58
|
+
});
|
|
59
|
+
router.post("/mcp/servers/test", async (req, res) => {
|
|
60
|
+
const config = settings(bodyRecord(req.body));
|
|
61
|
+
res.json(await manager.test(config));
|
|
62
|
+
});
|
|
63
|
+
router.post("/mcp/servers/:id/test", async (req, res) => {
|
|
64
|
+
res.json(await manager.testStored(requireString(req.params.id, "id")));
|
|
65
|
+
});
|
|
66
|
+
router.patch("/mcp/servers/:id", async (req, res) => {
|
|
67
|
+
const id = requireString(req.params.id, "id");
|
|
68
|
+
res.json(await manager.update(id, settings(bodyRecord(req.body), true)));
|
|
69
|
+
});
|
|
70
|
+
router.delete("/mcp/servers/:id", async (req, res) => {
|
|
71
|
+
await manager.remove(requireString(req.params.id, "id"));
|
|
72
|
+
res.json({ ok: true });
|
|
73
|
+
});
|
|
74
|
+
router.post("/mcp/servers/:id/authorize", async (req, res) => {
|
|
75
|
+
const id = requireString(req.params.id, "id");
|
|
76
|
+
const origin = ingress.expectedWebOrigin(req) ??
|
|
77
|
+
`http://${req.get("host") ?? "localhost:8765"}`;
|
|
78
|
+
res.json(await manager.authorize(id, origin + "/api/mcp/oauth/callback"));
|
|
79
|
+
});
|
|
80
|
+
router.post("/mcp/servers/:id/revoke", async (req, res) => {
|
|
81
|
+
await manager.revokeAuthorization(requireString(req.params.id, "id"));
|
|
82
|
+
res.json({ ok: true });
|
|
83
|
+
});
|
|
84
|
+
router.post("/mcp/servers/:id/refresh", async (req, res) => {
|
|
85
|
+
res.json(await manager.refresh(requireString(req.params.id, "id")));
|
|
86
|
+
});
|
|
87
|
+
return router;
|
|
88
|
+
}
|
|
89
|
+
/** Public OAuth callback: the random state is single-use and scoped to an initiated login. */
|
|
90
|
+
export function createCustomMcpOAuthCallbackRouter(manager) {
|
|
91
|
+
const router = Router();
|
|
92
|
+
router.get("/mcp/oauth/callback", async (req, res) => {
|
|
93
|
+
const query = new URLSearchParams();
|
|
94
|
+
for (const key of ["state", "code", "iss", "error"])
|
|
95
|
+
if (typeof req.query[key] === "string")
|
|
96
|
+
query.set(key, req.query[key]);
|
|
97
|
+
let success = false;
|
|
98
|
+
try {
|
|
99
|
+
await manager.completeAuthorization(query);
|
|
100
|
+
success = true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Never expose OAuth codes, provider errors, or credentials in this page.
|
|
104
|
+
}
|
|
105
|
+
res.setHeader("Cache-Control", "no-store");
|
|
106
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'; frame-ancestors 'none'");
|
|
107
|
+
res.status(success ? 200 : 400).type("html")
|
|
108
|
+
.send(`<!doctype html><html lang="en"><meta charset="utf-8"><title>ChatRoom MCP</title>
|
|
109
|
+
<body><p>${success ? "Authorization completed. You may close this window." : "Authorization failed. Return to ChatRoom and retry."}</p>
|
|
110
|
+
<script>window.opener?.postMessage({type:"chatroom:mcp-oauth-completed",success:${success}},window.location.origin);window.close();</script>
|
|
111
|
+
</body></html>`);
|
|
112
|
+
});
|
|
113
|
+
return router;
|
|
114
|
+
}
|
|
@@ -16,7 +16,7 @@ export function createLogsApiRouter(logs) {
|
|
|
16
16
|
...(modules?.length ? { modules: new Set(modules) } : {}),
|
|
17
17
|
}));
|
|
18
18
|
}));
|
|
19
|
-
router.get("/logs/stream", (
|
|
19
|
+
router.get("/logs/stream", (_req, res) => {
|
|
20
20
|
res.status(200);
|
|
21
21
|
res.setHeader("Content-Type", "text/event-stream");
|
|
22
22
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
@@ -28,7 +28,7 @@ export function createLogsApiRouter(logs) {
|
|
|
28
28
|
});
|
|
29
29
|
const keepalive = setInterval(() => res.write(": keepalive\n\n"), 15000);
|
|
30
30
|
keepalive.unref();
|
|
31
|
-
|
|
31
|
+
res.on("close", () => {
|
|
32
32
|
clearInterval(keepalive);
|
|
33
33
|
unsubscribe();
|
|
34
34
|
});
|
|
@@ -6,6 +6,54 @@ export function createProcessApiRouter(application) {
|
|
|
6
6
|
router.get("/processes/:processId/summary", (req, res) => {
|
|
7
7
|
res.json(application.getProcessSummary(requireString(req.params.processId, "processId")));
|
|
8
8
|
});
|
|
9
|
+
router.get("/processes/:processId/stream", (req, res) => {
|
|
10
|
+
const processId = requireString(req.params.processId, "processId");
|
|
11
|
+
// Validate and attach before flushing headers: invalid IDs return JSON 404.
|
|
12
|
+
let closed = false;
|
|
13
|
+
const write = (type, data) => {
|
|
14
|
+
if (closed || res.destroyed)
|
|
15
|
+
return;
|
|
16
|
+
if (res.writableLength > 512 * 1024) {
|
|
17
|
+
res.destroy(new Error("Process output stream is too slow"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
21
|
+
};
|
|
22
|
+
const { snapshot, unsubscribe } = application.processes.subscribeOutput(processId, (chunk, stream) => {
|
|
23
|
+
if (!chunk) {
|
|
24
|
+
const { stdout: _stdout, stderr: _stderr, ...status } = application.getProcess(processId);
|
|
25
|
+
write("exit", status);
|
|
26
|
+
res.end();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
for (let offset = 0; offset < chunk.length; offset += 16 * 1024)
|
|
30
|
+
write("output", {
|
|
31
|
+
stream: stream ?? "stdout",
|
|
32
|
+
bytes: chunk
|
|
33
|
+
.subarray(offset, offset + 16 * 1024)
|
|
34
|
+
.toString("base64"),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
res.status(200);
|
|
39
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
40
|
+
res.setHeader("Cache-Control", "private, no-store, no-transform");
|
|
41
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
42
|
+
res.flushHeaders();
|
|
43
|
+
write("snapshot", snapshot);
|
|
44
|
+
if (snapshot.state !== "running") {
|
|
45
|
+
res.end();
|
|
46
|
+
unsubscribe();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const heartbeat = setInterval(() => write("ping", null), 15_000);
|
|
50
|
+
heartbeat.unref();
|
|
51
|
+
res.on("close", () => {
|
|
52
|
+
closed = true;
|
|
53
|
+
clearInterval(heartbeat);
|
|
54
|
+
unsubscribe();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
9
57
|
router.get("/processes/:processId", (req, res) => {
|
|
10
58
|
res.json(application.getProcess(requireString(req.params.processId, "processId")));
|
|
11
59
|
});
|