@desplega.ai/agent-swarm 1.90.0 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/openapi.json +803 -150
- package/package.json +5 -5
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +337 -1
- package/src/be/memory/providers/sqlite-store.ts +6 -1
- package/src/be/memory/types.ts +1 -0
- package/src/be/migrations/083_script_workflows.sql +51 -0
- package/src/be/modelsdev-cache.json +42352 -38595
- package/src/be/scripts/typecheck.ts +181 -1
- package/src/be/seed-scripts/catalog/compound-insights.ts +398 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
- package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
- package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
- package/src/be/seed-scripts/catalog/tool-usage.ts +59 -0
- package/src/be/seed-scripts/index.ts +54 -0
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +17 -0
- package/src/commands/artifact.ts +3 -2
- package/src/commands/profile-sync.ts +310 -0
- package/src/commands/runner.ts +134 -3
- package/src/hooks/hook.ts +32 -9
- package/src/http/db-query.ts +20 -5
- package/src/http/index.ts +57 -0
- package/src/http/integrations.ts +6 -1
- package/src/http/mcp-bridge.ts +117 -0
- package/src/http/mcp-oauth.ts +97 -39
- package/src/http/memory.ts +5 -2
- package/src/http/openapi.ts +2 -2
- package/src/http/pages-public.ts +10 -11
- package/src/http/pages.ts +7 -11
- package/src/http/script-runs.ts +555 -0
- package/src/http/scripts.ts +24 -1
- package/src/http/utils.ts +11 -4
- package/src/jira/app.ts +2 -3
- package/src/jira/webhook-lifecycle.ts +2 -1
- package/src/linear/app.ts +2 -3
- package/src/prompts/session-templates.ts +24 -4
- package/src/providers/claude-adapter.ts +86 -13
- package/src/script-workflows/executor.ts +110 -0
- package/src/script-workflows/harness.ts +73 -0
- package/src/script-workflows/label-lint.ts +51 -0
- package/src/script-workflows/limits.ts +22 -0
- package/src/script-workflows/supervisor.ts +139 -0
- package/src/script-workflows/workflow-ctx.ts +205 -0
- package/src/scripts-runtime/executors/native.ts +1 -0
- package/src/scripts-runtime/sdk-allowlist.ts +124 -0
- package/src/scripts-runtime/swarm-sdk.ts +198 -3
- package/src/scripts-runtime/types/stdlib.d.ts +287 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +287 -0
- package/src/server.ts +2 -0
- package/src/slack/handlers.ts +11 -4
- package/src/slack/message-text.ts +98 -0
- package/src/slack/thread-buffer.ts +5 -3
- package/src/tests/claude-adapter-binary.test.ts +147 -4
- package/src/tests/claude-adapter-otel.test.ts +85 -1
- package/src/tests/db-query.test.ts +28 -0
- package/src/tests/error-tracker.test.ts +121 -0
- package/src/tests/harness-provider-resolution.test.ts +33 -0
- package/src/tests/hook-registration-nudge.test.ts +69 -0
- package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
- package/src/tests/mcp-tools.test.ts +6 -0
- package/src/tests/pages-public-html.test.ts +41 -0
- package/src/tests/pages-public-json-redirect.test.ts +37 -2
- package/src/tests/profile-sync.test.ts +282 -0
- package/src/tests/prompt-template-session.test.ts +34 -5
- package/src/tests/script-runs-http.test.ts +278 -0
- package/src/tests/script-workflows-label-lint.test.ts +43 -0
- package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
- package/src/tests/scripts-mcp-e2e.test.ts +49 -2
- package/src/tests/scripts-runtime.test.ts +33 -0
- package/src/tests/seed-scripts.test.ts +347 -2
- package/src/tests/slack-message-text.test.ts +250 -0
- package/src/tests/system-default-skills.test.ts +40 -0
- package/src/tools/create-metric.ts +2 -3
- package/src/tools/create-page.ts +3 -6
- package/src/tools/db-query.ts +16 -6
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/register-kapso-number.ts +2 -4
- package/src/tools/request-human-input.ts +2 -1
- package/src/tools/script-common.ts +2 -4
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-runs.ts +123 -0
- package/src/tools/slack-read.ts +12 -3
- package/src/tools/tool-config.ts +4 -1
- package/src/types.ts +52 -0
- package/src/utils/constants.ts +58 -8
- package/src/utils/error-tracker.ts +40 -1
- package/src/utils/internal-ai/complete-structured.ts +10 -4
- package/src/workflows/executors/raw-llm.ts +76 -59
- package/templates/skills/pages/content.md +205 -55
- package/templates/skills/script-workflows/config.json +14 -0
- package/templates/skills/script-workflows/content.md +68 -0
- package/templates/skills/swarm-scripts/content.md +45 -7
package/src/http/index.ts
CHANGED
|
@@ -21,9 +21,11 @@ import {
|
|
|
21
21
|
withRemoteContext,
|
|
22
22
|
withSpanContext,
|
|
23
23
|
} from "../otel";
|
|
24
|
+
import { startScriptRunSupervisor, stopScriptRunSupervisor } from "../script-workflows/supervisor";
|
|
24
25
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
25
26
|
import { initTelemetry, telemetry } from "../telemetry";
|
|
26
27
|
import { getApiKey } from "../utils/api-key";
|
|
28
|
+
import { getMcpBaseUrl } from "../utils/constants";
|
|
27
29
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
28
30
|
import { initWorkflows } from "../workflows";
|
|
29
31
|
import { handleActiveSessions } from "./active-sessions";
|
|
@@ -42,6 +44,7 @@ import { handleInboxState } from "./inbox-state";
|
|
|
42
44
|
import { handleIntegrations } from "./integrations";
|
|
43
45
|
import { handleKv } from "./kv";
|
|
44
46
|
import { handleMcp } from "./mcp";
|
|
47
|
+
import { handleMcpBridge } from "./mcp-bridge";
|
|
45
48
|
import { handleMcpOAuth, startMcpOAuthPendingGc, stopMcpOAuthPendingGc } from "./mcp-oauth";
|
|
46
49
|
import { handleMcpServers } from "./mcp-servers";
|
|
47
50
|
import { handleMcpUser } from "./mcp-user";
|
|
@@ -56,6 +59,7 @@ import { handlePromptTemplates } from "./prompt-templates";
|
|
|
56
59
|
import { handleRepos } from "./repos";
|
|
57
60
|
import { describeRequestRoute } from "./route-def";
|
|
58
61
|
import { handleSchedules } from "./schedules";
|
|
62
|
+
import { handleScriptRuns } from "./script-runs";
|
|
59
63
|
import { handleScripts } from "./scripts";
|
|
60
64
|
import { handleSessionData } from "./session-data";
|
|
61
65
|
import { handleSessions } from "./sessions";
|
|
@@ -96,9 +100,47 @@ const globalState = globalThis as typeof globalThis & {
|
|
|
96
100
|
__transportsUser?: Record<string, StreamableHTTPServerTransport>;
|
|
97
101
|
__sessionUsers?: Record<string, string>;
|
|
98
102
|
__sigintRegistered?: boolean;
|
|
103
|
+
__apiGcInterval?: ReturnType<typeof setInterval>;
|
|
99
104
|
__runId?: string;
|
|
100
105
|
};
|
|
101
106
|
|
|
107
|
+
const API_GC_INTERVAL_MS = 5 * 60 * 1000;
|
|
108
|
+
|
|
109
|
+
type GcCapableGlobal = typeof globalThis & { gc?: () => void };
|
|
110
|
+
|
|
111
|
+
function scheduleApiGc(reason: string): boolean {
|
|
112
|
+
const gc = (globalThis as GcCapableGlobal).gc;
|
|
113
|
+
if (typeof gc !== "function") return false;
|
|
114
|
+
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
try {
|
|
118
|
+
gc();
|
|
119
|
+
console.log(`[HTTP] Explicit GC completed after ${reason} in ${Date.now() - startedAt}ms`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.warn(`[HTTP] Explicit GC failed after ${reason}: ${err}`);
|
|
122
|
+
}
|
|
123
|
+
}, 0);
|
|
124
|
+
timer.unref?.();
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function startApiGcInterval() {
|
|
129
|
+
if (globalState.__apiGcInterval) return;
|
|
130
|
+
|
|
131
|
+
const gc = (globalThis as GcCapableGlobal).gc;
|
|
132
|
+
if (typeof gc !== "function") {
|
|
133
|
+
console.log("[HTTP] Explicit GC unavailable; start API with --expose-gc to enable sweeps");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const interval = setInterval(() => {
|
|
138
|
+
scheduleApiGc("periodic API sweep");
|
|
139
|
+
}, API_GC_INTERVAL_MS);
|
|
140
|
+
interval.unref?.();
|
|
141
|
+
globalState.__apiGcInterval = interval;
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
// Clean up previous server on hot reload
|
|
103
145
|
if (globalState.__httpServer) {
|
|
104
146
|
console.log("[HTTP] Hot reload detected, closing previous server...");
|
|
@@ -233,7 +275,9 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
233
275
|
() => handleMetrics(req, res, pathSegments, queryParams, myAgentId),
|
|
234
276
|
() => handleRepos(req, res, pathSegments, queryParams),
|
|
235
277
|
() => handleSkills(req, res, pathSegments, queryParams, myAgentId),
|
|
278
|
+
() => handleScriptRuns(req, res, pathSegments, queryParams, myAgentId),
|
|
236
279
|
() => handleScripts(req, res, pathSegments, queryParams, myAgentId),
|
|
280
|
+
() => handleMcpBridge(req, res, pathSegments, queryParams, myAgentId),
|
|
237
281
|
() => handleMcpServers(req, res, pathSegments, queryParams),
|
|
238
282
|
() => handleMcpOAuth(req, res, pathSegments, queryParams),
|
|
239
283
|
() => handleMemory(req, res, pathSegments, myAgentId),
|
|
@@ -303,6 +347,9 @@ async function shutdown() {
|
|
|
303
347
|
// Stop heartbeat triage
|
|
304
348
|
stopHeartbeat();
|
|
305
349
|
|
|
350
|
+
// Stop durable script workflow subprocesses
|
|
351
|
+
stopScriptRunSupervisor();
|
|
352
|
+
|
|
306
353
|
// Stop Slack bot
|
|
307
354
|
await stopSlackApp();
|
|
308
355
|
|
|
@@ -315,6 +362,11 @@ async function shutdown() {
|
|
|
315
362
|
// Stop MCP OAuth pending-session garbage collector
|
|
316
363
|
stopMcpOAuthPendingGc();
|
|
317
364
|
|
|
365
|
+
if (globalState.__apiGcInterval) {
|
|
366
|
+
clearInterval(globalState.__apiGcInterval);
|
|
367
|
+
delete globalState.__apiGcInterval;
|
|
368
|
+
}
|
|
369
|
+
|
|
318
370
|
// Close all active transports (SSE connections, etc.)
|
|
319
371
|
for (const [id, transport] of Object.entries(transports)) {
|
|
320
372
|
console.log(`[HTTP] Closing transport ${id}`);
|
|
@@ -349,6 +401,8 @@ if (!globalState.__runId) {
|
|
|
349
401
|
globalState.__runId = `run_${Date.now()}`;
|
|
350
402
|
}
|
|
351
403
|
|
|
404
|
+
startApiGcInterval();
|
|
405
|
+
|
|
352
406
|
// Load global swarm configs before the server starts listening so decrypt/key
|
|
353
407
|
// failures fail closed instead of leaving the runtime half-initialized.
|
|
354
408
|
let startupConfigsInjected: string[] = [];
|
|
@@ -440,6 +494,9 @@ httpServer
|
|
|
440
494
|
// Initialize workflow engine (trigger subscriptions + resume listener)
|
|
441
495
|
initWorkflows();
|
|
442
496
|
|
|
497
|
+
// Reconcile durable script workflow subprocesses
|
|
498
|
+
startScriptRunSupervisor(getMcpBaseUrl());
|
|
499
|
+
|
|
443
500
|
// Start scheduler (if enabled)
|
|
444
501
|
if (hasCapability("scheduling")) {
|
|
445
502
|
const { startScheduler } = await import("../scheduler");
|
package/src/http/integrations.ts
CHANGED
|
@@ -88,7 +88,12 @@ function resolveConfigValue(key: string): string | null {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function resolveMcpBaseUrl(): string {
|
|
91
|
-
|
|
91
|
+
// Browser-facing connect URLs: prefer the public ingress origin
|
|
92
|
+
// (PUBLIC_MCP_BASE_URL) over the internal MCP_BASE_URL so split deployments
|
|
93
|
+
// (Helm) surface a host the user's browser can actually reach. Both honor the
|
|
94
|
+
// swarm_config → env resolution order. Falls back to the localhost dev base.
|
|
95
|
+
const configured =
|
|
96
|
+
resolveConfigValue("PUBLIC_MCP_BASE_URL") ?? resolveConfigValue("MCP_BASE_URL");
|
|
92
97
|
const fallback = `http://localhost:${process.env.PORT || "3013"}`;
|
|
93
98
|
return (configured || fallback).replace(/\/+$/, "");
|
|
94
99
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createServer } from "@/server";
|
|
5
|
+
import { isSdkToolAllowed } from "../scripts-runtime/sdk-allowlist";
|
|
6
|
+
import { route } from "./route-def";
|
|
7
|
+
import { json, jsonError } from "./utils";
|
|
8
|
+
|
|
9
|
+
// Lazy singleton — created once on first bridge call to avoid boot-time cost.
|
|
10
|
+
let _bridgeServer: McpServer | null = null;
|
|
11
|
+
function getBridgeServer(): McpServer {
|
|
12
|
+
if (!_bridgeServer) {
|
|
13
|
+
_bridgeServer = createServer();
|
|
14
|
+
}
|
|
15
|
+
return _bridgeServer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RegisteredTool = {
|
|
19
|
+
handler: Function;
|
|
20
|
+
inputSchema?: unknown;
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ToolRegistry = Record<string, RegisteredTool>;
|
|
25
|
+
|
|
26
|
+
const mcpBridgeRoute = route({
|
|
27
|
+
method: "post",
|
|
28
|
+
path: "/api/mcp-bridge",
|
|
29
|
+
pattern: ["api", "mcp-bridge"],
|
|
30
|
+
summary: "Generic MCP tool proxy for the scripts SDK bridge",
|
|
31
|
+
tags: ["Scripts"],
|
|
32
|
+
body: z.object({
|
|
33
|
+
tool: z.string().min(1).max(200),
|
|
34
|
+
args: z.record(z.string(), z.unknown()).default({}),
|
|
35
|
+
}),
|
|
36
|
+
responses: {
|
|
37
|
+
200: { description: "Tool result" },
|
|
38
|
+
400: { description: "Invalid tool name or args" },
|
|
39
|
+
403: { description: "Tool not in SDK allowlist" },
|
|
40
|
+
404: { description: "Tool not found" },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export async function handleMcpBridge(
|
|
45
|
+
req: IncomingMessage,
|
|
46
|
+
res: ServerResponse,
|
|
47
|
+
pathSegments: string[],
|
|
48
|
+
_queryParams?: URLSearchParams,
|
|
49
|
+
myAgentId?: string,
|
|
50
|
+
): Promise<boolean> {
|
|
51
|
+
if (!mcpBridgeRoute.match(req.method, pathSegments)) return false;
|
|
52
|
+
|
|
53
|
+
const parsed = await mcpBridgeRoute.parse(req, res, pathSegments, new URLSearchParams());
|
|
54
|
+
if (!parsed) return true;
|
|
55
|
+
|
|
56
|
+
const { tool: toolName, args } = parsed.body;
|
|
57
|
+
|
|
58
|
+
if (!isSdkToolAllowed(toolName)) {
|
|
59
|
+
jsonError(res, `Tool '${toolName}' is not in the SDK allowlist`, 403);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const server = getBridgeServer();
|
|
64
|
+
const tools = (server as unknown as { _registeredTools: ToolRegistry })._registeredTools;
|
|
65
|
+
|
|
66
|
+
const tool = tools[toolName];
|
|
67
|
+
if (!tool) {
|
|
68
|
+
jsonError(res, `Tool '${toolName}' not found in the MCP registry`, 404);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (tool.enabled === false) {
|
|
73
|
+
jsonError(res, `Tool '${toolName}' is disabled`, 400);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const sourceTaskId = Array.isArray(req.headers["x-source-task-id"])
|
|
78
|
+
? req.headers["x-source-task-id"][0]
|
|
79
|
+
: (req.headers["x-source-task-id"] as string | undefined);
|
|
80
|
+
|
|
81
|
+
const extra = {
|
|
82
|
+
sessionId: "mcp-bridge",
|
|
83
|
+
requestInfo: {
|
|
84
|
+
headers: {
|
|
85
|
+
"x-agent-id": myAgentId ?? "",
|
|
86
|
+
...(sourceTaskId ? { "x-source-task-id": sourceTaskId } : {}),
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const result = tool.inputSchema
|
|
93
|
+
? await Promise.resolve(tool.handler(args, extra))
|
|
94
|
+
: await Promise.resolve(tool.handler(extra));
|
|
95
|
+
|
|
96
|
+
if (result && typeof result === "object" && "structuredContent" in result) {
|
|
97
|
+
json(res, result.structuredContent);
|
|
98
|
+
} else if (result && typeof result === "object" && "content" in result) {
|
|
99
|
+
const content = (result as { content: Array<{ type: string; text?: string }> }).content;
|
|
100
|
+
const text = content
|
|
101
|
+
.filter((c) => c.type === "text" && c.text)
|
|
102
|
+
.map((c) => c.text)
|
|
103
|
+
.join("\n");
|
|
104
|
+
try {
|
|
105
|
+
json(res, JSON.parse(text));
|
|
106
|
+
} catch {
|
|
107
|
+
json(res, { result: text });
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
json(res, result ?? {});
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
jsonError(res, message, 500);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
package/src/http/mcp-oauth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getMcpServerById } from "../be/db";
|
|
4
|
+
import type { McpOAuthToken } from "../be/db-queries/mcp-oauth";
|
|
4
5
|
import {
|
|
5
6
|
applyMcpOAuthRefresh,
|
|
6
7
|
consumeMcpOAuthPending,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
registerClient,
|
|
24
25
|
revokeMcpToken,
|
|
25
26
|
} from "../oauth/mcp-wrapper";
|
|
27
|
+
import { getAppUrl, getPublicMcpBaseUrl } from "../utils/constants";
|
|
26
28
|
import { route } from "./route-def";
|
|
27
29
|
import { json, jsonError } from "./utils";
|
|
28
30
|
|
|
@@ -36,12 +38,14 @@ function ssrfOptions() {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function callbackRedirectUri(): string {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// The callback route lives on the API server, so it must use the PUBLIC MCP
|
|
42
|
+
// base (externally reachable), not the dashboard APP_URL.
|
|
43
|
+
return `${getPublicMcpBaseUrl()}/api/mcp-oauth/callback`;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function dashboardBase(): string {
|
|
44
|
-
|
|
47
|
+
// getAppUrl absorbs DASHBOARD_URL as a deprecated alias.
|
|
48
|
+
return getAppUrl();
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
function defaultFinalRedirect(mcpServerId: string): string {
|
|
@@ -61,6 +65,43 @@ interface DiscoveryResult {
|
|
|
61
65
|
bearerMethodsSupported: string[] | null;
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
interface OAuthClientForAuthorize {
|
|
69
|
+
clientId: string;
|
|
70
|
+
clientSecret: string | null;
|
|
71
|
+
resourceUrl: string;
|
|
72
|
+
authorizationServerIssuer: string;
|
|
73
|
+
authorizeUrl: string;
|
|
74
|
+
tokenUrl: string;
|
|
75
|
+
revocationUrl: string | null;
|
|
76
|
+
scopes: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function splitScopes(scopes: string | null | undefined): string[] {
|
|
80
|
+
return scopes?.split(/\s+/).filter(Boolean) ?? [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function manualClientFromToken(token: McpOAuthToken | null): OAuthClientForAuthorize | null {
|
|
84
|
+
if (!token || token.clientSource !== "manual" || !token.dcrClientId) return null;
|
|
85
|
+
|
|
86
|
+
// The manual-client route validates these on write. Re-check before using the
|
|
87
|
+
// stored endpoints because /authorize redirects the browser to authorizeUrl.
|
|
88
|
+
assertUrlSafe(token.resourceUrl, ssrfOptions());
|
|
89
|
+
assertUrlSafe(token.authorizeUrl, ssrfOptions());
|
|
90
|
+
assertUrlSafe(token.tokenUrl, ssrfOptions());
|
|
91
|
+
if (token.revocationUrl) assertUrlSafe(token.revocationUrl, ssrfOptions());
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
clientId: token.dcrClientId,
|
|
95
|
+
clientSecret: token.dcrClientSecret,
|
|
96
|
+
resourceUrl: token.resourceUrl,
|
|
97
|
+
authorizationServerIssuer: token.authorizationServerIssuer,
|
|
98
|
+
authorizeUrl: token.authorizeUrl,
|
|
99
|
+
tokenUrl: token.tokenUrl,
|
|
100
|
+
revocationUrl: token.revocationUrl,
|
|
101
|
+
scopes: splitScopes(token.scope),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
async function discoverForMcp(resourceUrl: string): Promise<DiscoveryResult | null> {
|
|
65
106
|
assertUrlSafe(resourceUrl, ssrfOptions());
|
|
66
107
|
|
|
@@ -266,10 +307,10 @@ interface AuthorizeFlowQuery {
|
|
|
266
307
|
}
|
|
267
308
|
|
|
268
309
|
/**
|
|
269
|
-
*
|
|
270
|
-
* persist the pending session. Returns the provider
|
|
271
|
-
* should redirect to / respond with. On failure,
|
|
272
|
-
* and returns null.
|
|
310
|
+
* Use a stored manual client or discover metadata + DCR-register, build the
|
|
311
|
+
* authorize URL, and persist the pending session. Returns the provider
|
|
312
|
+
* `providerUrl` the caller should redirect to / respond with. On failure,
|
|
313
|
+
* writes a JSON error response and returns null.
|
|
273
314
|
*/
|
|
274
315
|
async function prepareAuthorizeFlow(
|
|
275
316
|
res: ServerResponse,
|
|
@@ -277,15 +318,26 @@ async function prepareAuthorizeFlow(
|
|
|
277
318
|
server: NonNullable<ReturnType<typeof getMcpServerById>>,
|
|
278
319
|
q: AuthorizeFlowQuery,
|
|
279
320
|
): Promise<string | null> {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
321
|
+
const userId = q.userId ?? null;
|
|
322
|
+
let client = manualClientFromToken(getMcpOAuthToken(mcpServerId, userId));
|
|
323
|
+
|
|
324
|
+
if (!client) {
|
|
325
|
+
const discovery = await discoverForMcp(server.url!);
|
|
326
|
+
if (!discovery) {
|
|
327
|
+
jsonError(res, "MCP server does not require OAuth", 400);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
285
330
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
331
|
+
if (!discovery.dcrSupported || !discovery.registrationEndpoint) {
|
|
332
|
+
jsonError(
|
|
333
|
+
res,
|
|
334
|
+
"DCR not supported — paste client_id/client_secret via POST /api/mcp-oauth/:id/manual-client first.",
|
|
335
|
+
400,
|
|
336
|
+
);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const scopes = q.scopes ? splitScopes(q.scopes) : discovery.scopes;
|
|
289
341
|
const dcr = await registerClient(discovery.registrationEndpoint, {
|
|
290
342
|
client_name: `agent-swarm (${server.name})`,
|
|
291
343
|
redirect_uris: [callbackRedirectUri()],
|
|
@@ -293,43 +345,45 @@ async function prepareAuthorizeFlow(
|
|
|
293
345
|
response_types: ["code"],
|
|
294
346
|
token_endpoint_auth_method: "client_secret_basic",
|
|
295
347
|
application_type: "web",
|
|
296
|
-
scope:
|
|
348
|
+
scope: scopes.join(" ") || undefined,
|
|
297
349
|
});
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
350
|
+
|
|
351
|
+
client = {
|
|
352
|
+
clientId: dcr.client_id,
|
|
353
|
+
clientSecret: dcr.client_secret ?? null,
|
|
354
|
+
resourceUrl: discovery.resourceUrl,
|
|
355
|
+
authorizationServerIssuer: discovery.authorizationServerIssuer,
|
|
356
|
+
authorizeUrl: discovery.authorizeUrl,
|
|
357
|
+
tokenUrl: discovery.tokenUrl,
|
|
358
|
+
revocationUrl: discovery.revocationUrl,
|
|
359
|
+
scopes,
|
|
360
|
+
};
|
|
307
361
|
}
|
|
308
362
|
|
|
309
|
-
const scopes = q.scopes ? q.scopes
|
|
363
|
+
const scopes = q.scopes ? splitScopes(q.scopes) : client.scopes;
|
|
310
364
|
|
|
311
365
|
const built = await buildAuthorizeUrl({
|
|
312
|
-
authorizeUrl:
|
|
313
|
-
tokenUrl:
|
|
314
|
-
clientId: clientId
|
|
366
|
+
authorizeUrl: client.authorizeUrl,
|
|
367
|
+
tokenUrl: client.tokenUrl,
|
|
368
|
+
clientId: client.clientId,
|
|
315
369
|
redirectUri: callbackRedirectUri(),
|
|
316
370
|
scopes,
|
|
317
|
-
resource:
|
|
371
|
+
resource: client.resourceUrl,
|
|
318
372
|
});
|
|
319
373
|
|
|
320
374
|
insertMcpOAuthPending({
|
|
321
375
|
state: built.state,
|
|
322
376
|
mcpServerId,
|
|
323
|
-
userId
|
|
377
|
+
userId,
|
|
324
378
|
codeVerifier: built.codeVerifier,
|
|
325
|
-
resourceUrl:
|
|
326
|
-
authorizationServerIssuer:
|
|
327
|
-
authorizeUrl:
|
|
328
|
-
tokenUrl:
|
|
329
|
-
revocationUrl:
|
|
379
|
+
resourceUrl: client.resourceUrl,
|
|
380
|
+
authorizationServerIssuer: client.authorizationServerIssuer,
|
|
381
|
+
authorizeUrl: client.authorizeUrl,
|
|
382
|
+
tokenUrl: client.tokenUrl,
|
|
383
|
+
revocationUrl: client.revocationUrl,
|
|
330
384
|
scopes: scopes.join(" "),
|
|
331
|
-
dcrClientId: clientId
|
|
332
|
-
dcrClientSecret: clientSecret,
|
|
385
|
+
dcrClientId: client.clientId,
|
|
386
|
+
dcrClientSecret: client.clientSecret,
|
|
333
387
|
redirectUri: callbackRedirectUri(),
|
|
334
388
|
finalRedirect: q.redirect ?? null,
|
|
335
389
|
});
|
|
@@ -390,6 +444,10 @@ export async function handleMcpOAuth(
|
|
|
390
444
|
codeVerifier: pending.codeVerifier,
|
|
391
445
|
resource: pending.resourceUrl,
|
|
392
446
|
});
|
|
447
|
+
const existing = getMcpOAuthToken(pending.mcpServerId, pending.userId);
|
|
448
|
+
const clientSource =
|
|
449
|
+
existing?.clientSource ??
|
|
450
|
+
(pending.dcrClientId ? ("dcr" as const) : ("preregistered" as const));
|
|
393
451
|
|
|
394
452
|
upsertMcpOAuthToken({
|
|
395
453
|
mcpServerId: pending.mcpServerId,
|
|
@@ -406,7 +464,7 @@ export async function handleMcpOAuth(
|
|
|
406
464
|
revocationUrl: pending.revocationUrl,
|
|
407
465
|
dcrClientId: pending.dcrClientId,
|
|
408
466
|
dcrClientSecret: pending.dcrClientSecret,
|
|
409
|
-
clientSource
|
|
467
|
+
clientSource,
|
|
410
468
|
lastRefreshedAt: new Date().toISOString(),
|
|
411
469
|
});
|
|
412
470
|
|
package/src/http/memory.ts
CHANGED
|
@@ -54,6 +54,8 @@ const searchMemory = route({
|
|
|
54
54
|
body: z.object({
|
|
55
55
|
query: z.string().min(1),
|
|
56
56
|
limit: z.number().int().min(1).max(20).default(5),
|
|
57
|
+
scope: z.enum(["agent", "swarm", "all"]).default("all"),
|
|
58
|
+
source: z.enum(["manual", "file_index", "session_summary", "task_completion"]).optional(),
|
|
57
59
|
}),
|
|
58
60
|
responses: {
|
|
59
61
|
200: { description: "Search results" },
|
|
@@ -325,7 +327,7 @@ export async function handleMemory(
|
|
|
325
327
|
const parsed = await searchMemory.parse(req, res, pathSegments, new URLSearchParams());
|
|
326
328
|
if (!parsed) return true;
|
|
327
329
|
|
|
328
|
-
const { query, limit } = parsed.body;
|
|
330
|
+
const { query, limit, scope, source } = parsed.body;
|
|
329
331
|
|
|
330
332
|
try {
|
|
331
333
|
const provider = getEmbeddingProvider();
|
|
@@ -339,8 +341,9 @@ export async function handleMemory(
|
|
|
339
341
|
|
|
340
342
|
const candidateLimit = Math.min(limit, 20) * CANDIDATE_SET_MULTIPLIER;
|
|
341
343
|
const candidates = store.search(queryEmbedding, myAgentId, {
|
|
342
|
-
scope
|
|
344
|
+
scope,
|
|
343
345
|
limit: candidateLimit,
|
|
346
|
+
source,
|
|
344
347
|
isLead: false,
|
|
345
348
|
});
|
|
346
349
|
const ranked = rerank(candidates, { limit: Math.min(limit, 20) });
|
package/src/http/openapi.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
OpenApiGeneratorV31,
|
|
5
5
|
} from "@asteasolutions/zod-to-openapi";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { getPublicMcpBaseUrl } from "../utils/constants";
|
|
7
8
|
import { routeRegistry } from "./route-def";
|
|
8
9
|
|
|
9
10
|
extendZodWithOpenApi(z);
|
|
@@ -74,8 +75,7 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
|
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const serverUrl =
|
|
78
|
-
opts.serverUrl || process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
|
|
78
|
+
const serverUrl = opts.serverUrl || getPublicMcpBaseUrl();
|
|
79
79
|
|
|
80
80
|
const generator = new OpenApiGeneratorV31(registry.definitions);
|
|
81
81
|
const doc = generator.generateDocument({
|
package/src/http/pages-public.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { z } from "zod";
|
|
|
25
25
|
import { BROWSER_SDK_JS, SWARM_UI_JS } from "../artifact-sdk/browser-sdk";
|
|
26
26
|
import { getPage, incrementPageViewCount } from "../be/db";
|
|
27
27
|
import type { Page } from "../types";
|
|
28
|
+
import { getAppUrl, getConfiguredAppUrls } from "../utils/constants";
|
|
28
29
|
import { extractAndVerifyCookie, issuePageSessionCookie } from "../utils/page-session";
|
|
29
30
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
30
31
|
import { route } from "./route-def";
|
|
@@ -159,14 +160,15 @@ function stripJsonSuffix(idSegment: string): string | null {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
* Compute the SPA base URL
|
|
163
|
-
*
|
|
164
|
-
*
|
|
163
|
+
* Compute the SPA base URL. Public JSON pages historically redirect to the
|
|
164
|
+
* local dashboard when no app URL is configured; keep that route-local
|
|
165
|
+
* fallback while still delegating `APP_URL`/`DASHBOARD_URL` resolution to the
|
|
166
|
+
* shared helper.
|
|
165
167
|
*/
|
|
168
|
+
const LOCAL_PAGE_APP_URL = "http://localhost:5274";
|
|
169
|
+
|
|
166
170
|
function getAppBaseUrl(): string {
|
|
167
|
-
|
|
168
|
-
if (env) return env.replace(/\/+$/, "");
|
|
169
|
-
return "http://localhost:5274";
|
|
171
|
+
return getAppUrl(LOCAL_PAGE_APP_URL);
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
/**
|
|
@@ -177,15 +179,12 @@ function getAppBaseUrl(): string {
|
|
|
177
179
|
*/
|
|
178
180
|
function buildCsp(): string {
|
|
179
181
|
// `frame-ancestors` lists every origin allowed to iframe `/p/:id`. We must
|
|
180
|
-
// include the SPA origin(s).
|
|
182
|
+
// include the SPA origin(s). Configured app URLs may be comma-separated so
|
|
181
183
|
// portless dev (`https://ui.swarm.localhost`), a Vite port (`http://localhost:5274`),
|
|
182
184
|
// and a tunnel/staging origin can all coexist. Additionally, in non-production
|
|
183
185
|
// we always allow `http://localhost:*` and `https://*.localhost` so swapping
|
|
184
186
|
// between Vite ports / portless dev doesn't require restarting the API.
|
|
185
|
-
const configured = (
|
|
186
|
-
.split(",")
|
|
187
|
-
.map((s) => s.trim().replace(/\/+$/, ""))
|
|
188
|
-
.filter(Boolean);
|
|
187
|
+
const configured = getConfiguredAppUrls();
|
|
189
188
|
const devFallbacks =
|
|
190
189
|
process.env.NODE_ENV === "production"
|
|
191
190
|
? []
|
package/src/http/pages.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../be/db";
|
|
15
15
|
import { snapshotPage } from "../pages/version";
|
|
16
16
|
import { type Page, PageAuthModeSchema, PageContentTypeSchema, type PageSummary } from "../types";
|
|
17
|
+
import { getAppUrl, getPublicMcpBaseUrl } from "../utils/constants";
|
|
17
18
|
import { issuePageSessionCookie } from "../utils/page-session";
|
|
18
19
|
import { route } from "./route-def";
|
|
19
20
|
import { BODY_TOO_LARGE, enforceContentLengthCap, json, jsonError } from "./utils";
|
|
@@ -280,25 +281,20 @@ function applyLaunchCors(req: IncomingMessage, res: ServerResponse): void {
|
|
|
280
281
|
|
|
281
282
|
/**
|
|
282
283
|
* Resolve the public API base URL used to build a page's `api_url` share
|
|
283
|
-
* pointer
|
|
284
|
-
*
|
|
285
|
-
*
|
|
284
|
+
* pointer (handed to a browser). Delegates to the shared
|
|
285
|
+
* {@link getPublicMcpBaseUrl} helper (trailing slashes already stripped) so
|
|
286
|
+
* callers can concatenate `/p/:id` directly.
|
|
286
287
|
*/
|
|
287
288
|
function getApiBaseUrl(): string {
|
|
288
|
-
|
|
289
|
-
if (env) return env.replace(/\/+$/, "");
|
|
290
|
-
return `http://localhost:${process.env.PORT || "3013"}`;
|
|
289
|
+
return getPublicMcpBaseUrl();
|
|
291
290
|
}
|
|
292
291
|
|
|
293
292
|
/**
|
|
294
293
|
* Resolve the SPA / dashboard base URL used to build a page's `app_url` share
|
|
295
|
-
* pointer (→ `/pages/:id`).
|
|
296
|
-
* request-human-input tool); falls back to the local dev port `5274`.
|
|
294
|
+
* pointer (→ `/pages/:id`). Delegates to the shared {@link getAppUrl} helper.
|
|
297
295
|
*/
|
|
298
296
|
function getAppBaseUrl(): string {
|
|
299
|
-
|
|
300
|
-
if (env) return env.replace(/\/+$/, "");
|
|
301
|
-
return "http://localhost:5274";
|
|
297
|
+
return getAppUrl();
|
|
302
298
|
}
|
|
303
299
|
|
|
304
300
|
/** Decorate a page row with share-URL pointers. */
|