@lobu/gateway 3.0.5 → 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/agent-config-routes.test.ts +254 -0
  3. package/src/__tests__/agent-history-routes.test.ts +72 -0
  4. package/src/__tests__/agent-routes.test.ts +68 -0
  5. package/src/__tests__/agent-schedules-routes.test.ts +59 -0
  6. package/src/__tests__/agent-settings-store.test.ts +323 -0
  7. package/src/__tests__/chat-instance-manager-slack.test.ts +204 -0
  8. package/src/__tests__/chat-response-bridge.test.ts +131 -0
  9. package/src/__tests__/config-memory-plugins.test.ts +92 -0
  10. package/src/__tests__/config-request-store.test.ts +127 -0
  11. package/src/__tests__/connection-routes.test.ts +144 -0
  12. package/src/__tests__/core-services-store-selection.test.ts +92 -0
  13. package/src/__tests__/docker-deployment.test.ts +1211 -0
  14. package/src/__tests__/embedded-deployment.test.ts +342 -0
  15. package/src/__tests__/grant-store.test.ts +148 -0
  16. package/src/__tests__/http-proxy.test.ts +281 -0
  17. package/src/__tests__/instruction-service.test.ts +37 -0
  18. package/src/__tests__/link-buttons.test.ts +112 -0
  19. package/src/__tests__/lobu.test.ts +32 -0
  20. package/src/__tests__/mcp-config-service.test.ts +347 -0
  21. package/src/__tests__/mcp-proxy.test.ts +696 -0
  22. package/src/__tests__/message-handler-bridge.test.ts +17 -0
  23. package/src/__tests__/model-selection.test.ts +172 -0
  24. package/src/__tests__/oauth-templates.test.ts +39 -0
  25. package/src/__tests__/platform-adapter-slack-send.test.ts +114 -0
  26. package/src/__tests__/platform-helpers-model-resolution.test.ts +253 -0
  27. package/src/__tests__/provider-inheritance.test.ts +212 -0
  28. package/src/__tests__/routes/cli-auth.test.ts +337 -0
  29. package/src/__tests__/routes/interactions.test.ts +121 -0
  30. package/src/__tests__/secret-proxy.test.ts +85 -0
  31. package/src/__tests__/session-manager.test.ts +572 -0
  32. package/src/__tests__/setup.ts +133 -0
  33. package/src/__tests__/skill-and-mcp-registry.test.ts +203 -0
  34. package/src/__tests__/slack-routes.test.ts +161 -0
  35. package/src/__tests__/system-config-resolver.test.ts +75 -0
  36. package/src/__tests__/system-message-limiter.test.ts +89 -0
  37. package/src/__tests__/system-skills-service.test.ts +362 -0
  38. package/src/__tests__/transcription-service.test.ts +222 -0
  39. package/src/__tests__/utils/rate-limiter.test.ts +102 -0
  40. package/src/__tests__/worker-connection-manager.test.ts +497 -0
  41. package/src/__tests__/worker-job-router.test.ts +722 -0
  42. package/src/api/index.ts +1 -0
  43. package/src/api/platform.ts +292 -0
  44. package/src/api/response-renderer.ts +157 -0
  45. package/src/auth/agent-metadata-store.ts +168 -0
  46. package/src/auth/api-auth-middleware.ts +69 -0
  47. package/src/auth/api-key-provider-module.ts +213 -0
  48. package/src/auth/base-provider-module.ts +201 -0
  49. package/src/auth/chatgpt/chatgpt-oauth-module.ts +185 -0
  50. package/src/auth/chatgpt/device-code-client.ts +218 -0
  51. package/src/auth/chatgpt/index.ts +1 -0
  52. package/src/auth/claude/oauth-module.ts +280 -0
  53. package/src/auth/cli/token-service.ts +249 -0
  54. package/src/auth/external/client.ts +560 -0
  55. package/src/auth/external/device-code-client.ts +225 -0
  56. package/src/auth/mcp/config-service.ts +392 -0
  57. package/src/auth/mcp/proxy.ts +1088 -0
  58. package/src/auth/mcp/string-substitution.ts +17 -0
  59. package/src/auth/mcp/tool-cache.ts +90 -0
  60. package/src/auth/oauth/base-client.ts +267 -0
  61. package/src/auth/oauth/client.ts +153 -0
  62. package/src/auth/oauth/credentials.ts +7 -0
  63. package/src/auth/oauth/providers.ts +69 -0
  64. package/src/auth/oauth/state-store.ts +150 -0
  65. package/src/auth/oauth-templates.ts +179 -0
  66. package/src/auth/provider-catalog.ts +220 -0
  67. package/src/auth/provider-model-options.ts +41 -0
  68. package/src/auth/settings/agent-settings-store.ts +565 -0
  69. package/src/auth/settings/auth-profiles-manager.ts +216 -0
  70. package/src/auth/settings/index.ts +12 -0
  71. package/src/auth/settings/model-preference-store.ts +52 -0
  72. package/src/auth/settings/model-selection.ts +135 -0
  73. package/src/auth/settings/resolved-settings-view.ts +298 -0
  74. package/src/auth/settings/template-utils.ts +44 -0
  75. package/src/auth/settings/token-service.ts +88 -0
  76. package/src/auth/system-env-store.ts +98 -0
  77. package/src/auth/user-agents-store.ts +68 -0
  78. package/src/channels/binding-service.ts +214 -0
  79. package/src/channels/index.ts +4 -0
  80. package/src/cli/gateway.ts +1304 -0
  81. package/src/cli/index.ts +74 -0
  82. package/src/commands/built-in-commands.ts +80 -0
  83. package/src/commands/command-dispatcher.ts +94 -0
  84. package/src/commands/command-reply-adapters.ts +27 -0
  85. package/src/config/file-loader.ts +618 -0
  86. package/src/config/index.ts +588 -0
  87. package/src/config/network-allowlist.ts +71 -0
  88. package/src/connections/chat-instance-manager.ts +1284 -0
  89. package/src/connections/chat-response-bridge.ts +618 -0
  90. package/src/connections/index.ts +7 -0
  91. package/src/connections/interaction-bridge.ts +831 -0
  92. package/src/connections/message-handler-bridge.ts +415 -0
  93. package/src/connections/platform-auth-methods.ts +15 -0
  94. package/src/connections/types.ts +84 -0
  95. package/src/gateway/connection-manager.ts +291 -0
  96. package/src/gateway/index.ts +700 -0
  97. package/src/gateway/job-router.ts +201 -0
  98. package/src/gateway-main.ts +200 -0
  99. package/src/index.ts +41 -0
  100. package/src/infrastructure/queue/index.ts +12 -0
  101. package/src/infrastructure/queue/queue-producer.ts +148 -0
  102. package/src/infrastructure/queue/redis-queue.ts +361 -0
  103. package/src/infrastructure/queue/types.ts +133 -0
  104. package/src/infrastructure/redis/system-message-limiter.ts +94 -0
  105. package/src/interactions/config-request-store.ts +198 -0
  106. package/src/interactions.ts +363 -0
  107. package/src/lobu.ts +311 -0
  108. package/src/metrics/prometheus.ts +159 -0
  109. package/src/modules/module-system.ts +179 -0
  110. package/src/orchestration/base-deployment-manager.ts +900 -0
  111. package/src/orchestration/deployment-utils.ts +98 -0
  112. package/src/orchestration/impl/docker-deployment.ts +620 -0
  113. package/src/orchestration/impl/embedded-deployment.ts +268 -0
  114. package/src/orchestration/impl/index.ts +8 -0
  115. package/src/orchestration/impl/k8s/deployment.ts +1061 -0
  116. package/src/orchestration/impl/k8s/helpers.ts +610 -0
  117. package/src/orchestration/impl/k8s/index.ts +1 -0
  118. package/src/orchestration/index.ts +333 -0
  119. package/src/orchestration/message-consumer.ts +584 -0
  120. package/src/orchestration/scheduled-wakeup.ts +704 -0
  121. package/src/permissions/approval-policy.ts +36 -0
  122. package/src/permissions/grant-store.ts +219 -0
  123. package/src/platform/file-handler.ts +66 -0
  124. package/src/platform/link-buttons.ts +57 -0
  125. package/src/platform/renderer-utils.ts +44 -0
  126. package/src/platform/response-renderer.ts +84 -0
  127. package/src/platform/unified-thread-consumer.ts +187 -0
  128. package/src/platform.ts +318 -0
  129. package/src/proxy/http-proxy.ts +752 -0
  130. package/src/proxy/proxy-manager.ts +81 -0
  131. package/src/proxy/secret-proxy.ts +402 -0
  132. package/src/proxy/token-refresh-job.ts +143 -0
  133. package/src/routes/internal/audio.ts +141 -0
  134. package/src/routes/internal/device-auth.ts +566 -0
  135. package/src/routes/internal/files.ts +226 -0
  136. package/src/routes/internal/history.ts +69 -0
  137. package/src/routes/internal/images.ts +127 -0
  138. package/src/routes/internal/interactions.ts +84 -0
  139. package/src/routes/internal/middleware.ts +23 -0
  140. package/src/routes/internal/schedule.ts +226 -0
  141. package/src/routes/internal/types.ts +22 -0
  142. package/src/routes/openapi-auto.ts +239 -0
  143. package/src/routes/public/agent-access.ts +23 -0
  144. package/src/routes/public/agent-config.ts +675 -0
  145. package/src/routes/public/agent-history.ts +422 -0
  146. package/src/routes/public/agent-schedules.ts +296 -0
  147. package/src/routes/public/agent.ts +1086 -0
  148. package/src/routes/public/agents.ts +373 -0
  149. package/src/routes/public/channels.ts +191 -0
  150. package/src/routes/public/cli-auth.ts +883 -0
  151. package/src/routes/public/connections.ts +574 -0
  152. package/src/routes/public/landing.ts +16 -0
  153. package/src/routes/public/oauth.ts +147 -0
  154. package/src/routes/public/settings-auth.ts +104 -0
  155. package/src/routes/public/slack.ts +173 -0
  156. package/src/routes/shared/agent-ownership.ts +101 -0
  157. package/src/routes/shared/token-verifier.ts +34 -0
  158. package/src/services/core-services.ts +1053 -0
  159. package/src/services/image-generation-service.ts +257 -0
  160. package/src/services/instruction-service.ts +318 -0
  161. package/src/services/mcp-registry.ts +94 -0
  162. package/src/services/platform-helpers.ts +287 -0
  163. package/src/services/session-manager.ts +262 -0
  164. package/src/services/settings-resolver.ts +74 -0
  165. package/src/services/system-config-resolver.ts +90 -0
  166. package/src/services/system-skills-service.ts +229 -0
  167. package/src/services/transcription-service.ts +684 -0
  168. package/src/session.ts +110 -0
  169. package/src/spaces/index.ts +1 -0
  170. package/src/spaces/space-resolver.ts +17 -0
  171. package/src/stores/in-memory-agent-store.ts +403 -0
  172. package/src/stores/redis-agent-store.ts +279 -0
  173. package/src/utils/public-url.ts +44 -0
  174. package/src/utils/rate-limiter.ts +94 -0
  175. package/tsconfig.json +33 -0
@@ -0,0 +1,225 @@
1
+ import { BaseOAuth2Client } from "../oauth/base-client";
2
+ import type { OAuthCredentials } from "../oauth/credentials";
3
+
4
+ export const DEVICE_CODE_GRANT_TYPE =
5
+ "urn:ietf:params:oauth:grant-type:device_code";
6
+
7
+ export interface DeviceCodeClientConfig {
8
+ clientId: string;
9
+ clientSecret?: string;
10
+ tokenUrl: string;
11
+ deviceAuthorizationUrl: string;
12
+ scope: string;
13
+ tokenEndpointAuthMethod?:
14
+ | "none"
15
+ | "client_secret_post"
16
+ | "client_secret_basic";
17
+ }
18
+
19
+ export interface DeviceAuthorizationStartResult {
20
+ deviceAuthId: string;
21
+ userCode: string;
22
+ verificationUri: string;
23
+ verificationUriComplete?: string;
24
+ interval: number;
25
+ expiresIn: number;
26
+ }
27
+
28
+ export type DeviceAuthorizationPollResult =
29
+ | {
30
+ status: "pending";
31
+ interval?: number;
32
+ }
33
+ | {
34
+ status: "complete";
35
+ credentials: OAuthCredentials;
36
+ }
37
+ | {
38
+ status: "error";
39
+ error: string;
40
+ errorCode?: string;
41
+ };
42
+
43
+ interface DeviceAuthorizationResponse {
44
+ device_code: string;
45
+ user_code: string;
46
+ verification_uri: string;
47
+ verification_uri_complete?: string;
48
+ expires_in: number;
49
+ interval?: number;
50
+ }
51
+
52
+ interface DeviceTokenSuccessResponse {
53
+ access_token: string;
54
+ refresh_token?: string;
55
+ token_type?: string;
56
+ expires_in?: number;
57
+ scope?: string;
58
+ }
59
+
60
+ interface DeviceTokenErrorResponse {
61
+ error: string;
62
+ error_description?: string;
63
+ }
64
+
65
+ export class GenericDeviceCodeClient extends BaseOAuth2Client {
66
+ constructor(private readonly config: DeviceCodeClientConfig) {
67
+ super("external-device-code-client");
68
+ }
69
+
70
+ async requestDeviceCode(): Promise<DeviceAuthorizationStartResult> {
71
+ const { headers, body } = this.buildAuthenticatedFormBody({
72
+ client_id: this.config.clientId,
73
+ scope: this.config.scope,
74
+ });
75
+
76
+ const response = await fetch(this.config.deviceAuthorizationUrl, {
77
+ method: "POST",
78
+ headers,
79
+ body: body.toString(),
80
+ });
81
+
82
+ const data = await this.parseJsonResponse<
83
+ DeviceAuthorizationResponse | DeviceTokenErrorResponse
84
+ >(response);
85
+
86
+ if (!response.ok || "error" in data) {
87
+ throw new Error(
88
+ this.formatOAuthError("Device authorization failed", data)
89
+ );
90
+ }
91
+
92
+ return {
93
+ deviceAuthId: data.device_code,
94
+ userCode: data.user_code,
95
+ verificationUri: data.verification_uri,
96
+ verificationUriComplete: data.verification_uri_complete,
97
+ interval: Math.max(data.interval ?? 5, 1),
98
+ expiresIn: data.expires_in,
99
+ };
100
+ }
101
+
102
+ async pollForToken(
103
+ deviceAuthId: string,
104
+ intervalSeconds?: number
105
+ ): Promise<DeviceAuthorizationPollResult> {
106
+ const { headers, body } = this.buildAuthenticatedFormBody({
107
+ grant_type: DEVICE_CODE_GRANT_TYPE,
108
+ device_code: deviceAuthId,
109
+ client_id: this.config.clientId,
110
+ });
111
+
112
+ const response = await fetch(this.config.tokenUrl, {
113
+ method: "POST",
114
+ headers,
115
+ body: body.toString(),
116
+ });
117
+
118
+ const data = await this.parseJsonResponse<
119
+ DeviceTokenSuccessResponse | DeviceTokenErrorResponse
120
+ >(response);
121
+
122
+ if (!response.ok || "error" in data) {
123
+ if ("error" in data) {
124
+ if (data.error === "authorization_pending") {
125
+ return { status: "pending", interval: intervalSeconds };
126
+ }
127
+ if (data.error === "slow_down") {
128
+ return {
129
+ status: "pending",
130
+ interval: Math.max((intervalSeconds ?? 5) + 5, 1),
131
+ };
132
+ }
133
+ if (data.error === "expired_token" || data.error === "access_denied") {
134
+ return {
135
+ status: "error",
136
+ error: data.error_description || data.error,
137
+ errorCode: data.error,
138
+ };
139
+ }
140
+ }
141
+
142
+ throw new Error(
143
+ this.formatOAuthError("Device token polling failed", data)
144
+ );
145
+ }
146
+
147
+ const credentials = this.buildCredentials(data);
148
+ return {
149
+ status: "complete",
150
+ credentials,
151
+ };
152
+ }
153
+
154
+ private buildCredentials(
155
+ tokenData: DeviceTokenSuccessResponse
156
+ ): OAuthCredentials {
157
+ return {
158
+ accessToken: tokenData.access_token,
159
+ refreshToken: tokenData.refresh_token,
160
+ tokenType: tokenData.token_type || "Bearer",
161
+ expiresAt:
162
+ this.calculateExpiresAt(tokenData.expires_in) ?? Date.now() + 3_600_000,
163
+ scopes: this.parseScopes(tokenData.scope),
164
+ };
165
+ }
166
+
167
+ private buildAuthenticatedFormBody(params: Record<string, string>): {
168
+ headers: Record<string, string>;
169
+ body: URLSearchParams;
170
+ } {
171
+ const body = new URLSearchParams(params);
172
+ const headers: Record<string, string> = {
173
+ Accept: "application/json",
174
+ "Content-Type": "application/x-www-form-urlencoded",
175
+ };
176
+ const authMethod = this.config.tokenEndpointAuthMethod || "none";
177
+
178
+ if (authMethod === "client_secret_basic") {
179
+ headers.Authorization = `Basic ${Buffer.from(
180
+ `${this.config.clientId}:${this.config.clientSecret || ""}`
181
+ ).toString("base64")}`;
182
+ } else if (
183
+ authMethod === "client_secret_post" &&
184
+ this.config.clientSecret
185
+ ) {
186
+ body.set("client_secret", this.config.clientSecret);
187
+ }
188
+
189
+ return { headers, body };
190
+ }
191
+
192
+ private async parseJsonResponse<T>(response: Response): Promise<T> {
193
+ const contentType = response.headers.get("content-type") || "";
194
+ if (contentType.includes("application/json")) {
195
+ return (await response.json()) as T;
196
+ }
197
+
198
+ const text = await response.text();
199
+ try {
200
+ return JSON.parse(text) as T;
201
+ } catch {
202
+ return { error: text || response.statusText } as T;
203
+ }
204
+ }
205
+
206
+ private formatOAuthError(prefix: string, data: unknown): string {
207
+ if (data && typeof data === "object" && "error" in data) {
208
+ const record = data as {
209
+ error?: unknown;
210
+ error_description?: unknown;
211
+ };
212
+ const error =
213
+ typeof record.error === "string" ? record.error : "unknown_error";
214
+ const description =
215
+ typeof record.error_description === "string"
216
+ ? record.error_description
217
+ : undefined;
218
+ return description
219
+ ? `${prefix}: ${error} - ${description}`
220
+ : `${prefix}: ${error}`;
221
+ }
222
+
223
+ return prefix;
224
+ }
225
+ }
@@ -0,0 +1,392 @@
1
+ import { createLogger, verifyWorkerToken } from "@lobu/core";
2
+ import type { SystemConfigResolver } from "../../services/system-config-resolver";
3
+ import type { AgentSettingsStore } from "../settings/agent-settings-store";
4
+
5
+ const logger = createLogger("mcp-config-service");
6
+
7
+ interface McpInput {
8
+ type: "promptString";
9
+ id: string;
10
+ description: string;
11
+ }
12
+
13
+ interface HttpMcpServerConfig {
14
+ id: string;
15
+ upstreamUrl: string;
16
+ oauth?: unknown;
17
+ inputs?: McpInput[];
18
+ headers?: Record<string, string>;
19
+ loginUrl?: string;
20
+ resource?: string;
21
+ }
22
+
23
+ interface WorkerMcpConfig {
24
+ mcpServers: Record<string, any>;
25
+ }
26
+
27
+ interface McpStatus {
28
+ id: string;
29
+ name: string;
30
+ requiresAuth: boolean;
31
+ requiresInput: boolean;
32
+ }
33
+
34
+ interface LoadedConfig {
35
+ rawServers: Record<string, any>;
36
+ httpServers: Map<string, HttpMcpServerConfig>;
37
+ }
38
+
39
+ interface McpConfigServiceOptions {
40
+ agentSettingsStore?: AgentSettingsStore;
41
+ configResolver?: SystemConfigResolver;
42
+ }
43
+
44
+ export class McpConfigService {
45
+ private cache?: LoadedConfig;
46
+ private agentSettingsStore?: AgentSettingsStore;
47
+ private configResolver?: SystemConfigResolver;
48
+
49
+ constructor(options: McpConfigServiceOptions = {}) {
50
+ this.agentSettingsStore = options.agentSettingsStore;
51
+ this.configResolver = options.configResolver;
52
+ logger.debug(`McpConfigService initialized`);
53
+ }
54
+
55
+ /**
56
+ * Register additional global MCP servers (e.g. from system skills).
57
+ */
58
+ registerGlobalServers(servers: Record<string, any>): void {
59
+ if (!this.cache) {
60
+ this.cache = {
61
+ rawServers: {},
62
+ httpServers: new Map(),
63
+ };
64
+ }
65
+
66
+ const normalized = normalizeConfig({ mcpServers: servers });
67
+ for (const [id, raw] of Object.entries(normalized.rawServers)) {
68
+ if (this.cache.rawServers[id]) continue;
69
+ this.cache.rawServers[id] = raw;
70
+ }
71
+ for (const [id, http] of normalized.httpServers) {
72
+ if (this.cache.httpServers.has(id)) continue;
73
+ this.cache.httpServers.set(id, http);
74
+ }
75
+
76
+ logger.info(
77
+ `Registered ${Object.keys(servers).length} global MCP(s) from system skills: ${Object.keys(servers).join(", ")}`
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Return MCP config tailored for a worker request.
83
+ */
84
+ async getWorkerConfig(options: {
85
+ baseUrl: string;
86
+ workerToken: string;
87
+ deploymentName?: string;
88
+ }): Promise<WorkerMcpConfig> {
89
+ const { baseUrl, workerToken } = options;
90
+ const config = await this.loadConfig();
91
+ const workerConfig: WorkerMcpConfig = { mcpServers: {} };
92
+
93
+ const tokenData = verifyWorkerToken(workerToken);
94
+ if (!tokenData) {
95
+ logger.warn("Failed to verify worker token");
96
+ return workerConfig;
97
+ }
98
+
99
+ const { userId, agentId } = tokenData;
100
+ const effectiveAgentId = agentId || userId;
101
+ logger.info(`Building MCP config for user ${userId}`);
102
+
103
+ // Process global MCPs
104
+ for (const [id, serverConfig] of Object.entries(config.rawServers)) {
105
+ const cloned = cloneConfig(serverConfig);
106
+ const httpServer = config.httpServers.get(id);
107
+
108
+ if (httpServer) {
109
+ logger.info(`Configuring global MCP ${id}: baseUrl=${baseUrl}`);
110
+ cloned.url = baseUrl;
111
+ cloned.type = "sse";
112
+ cloned.headers = mergeHeaders(cloned.headers, workerToken, id);
113
+ logger.info(
114
+ `Including global MCP ${id} with URL=${cloned.url} and X-Mcp-Id header`
115
+ );
116
+ }
117
+
118
+ workerConfig.mcpServers[id] = cloned;
119
+ }
120
+
121
+ // Merge per-agent MCPs from live agent settings
122
+ const agentSettingsMcpServers =
123
+ (await this.getAgentMcpServers(effectiveAgentId)) || {};
124
+ for (const [id, serverConfig] of Object.entries(agentSettingsMcpServers)) {
125
+ if (workerConfig.mcpServers[id]) {
126
+ logger.warn(
127
+ `Per-agent MCP ${id} skipped - global MCP with same ID exists`
128
+ );
129
+ continue;
130
+ }
131
+
132
+ const cloned = cloneConfig(serverConfig);
133
+
134
+ if (cloned.enabled === false) {
135
+ logger.debug(`Skipping disabled per-agent MCP ${id}`);
136
+ continue;
137
+ }
138
+
139
+ if (cloned.url) {
140
+ logger.info(`Configuring per-agent HTTP MCP ${id}: baseUrl=${baseUrl}`);
141
+ cloned.originalUrl = cloned.url;
142
+ cloned.url = baseUrl;
143
+ cloned.type = "sse";
144
+ cloned.headers = mergeHeaders(cloned.headers, workerToken, id);
145
+ cloned.perAgent = true;
146
+ logger.info(`Including per-agent HTTP MCP ${id}`);
147
+ } else if (cloned.command) {
148
+ logger.info(`Including per-agent stdio MCP ${id}: ${cloned.command}`);
149
+ }
150
+
151
+ workerConfig.mcpServers[id] = cloned;
152
+ }
153
+
154
+ if (Object.keys(agentSettingsMcpServers).length > 0) {
155
+ logger.info(
156
+ `Merged ${Object.keys(agentSettingsMcpServers).length} per-agent MCPs from settings for agent ${effectiveAgentId}`
157
+ );
158
+ }
159
+
160
+ logger.info(
161
+ `Returning worker config with ${Object.keys(workerConfig.mcpServers).length} MCPs for user ${userId}:`,
162
+ {
163
+ mcpIds: Object.keys(workerConfig.mcpServers),
164
+ configs: Object.entries(workerConfig.mcpServers).map(([id, cfg]) => ({
165
+ id,
166
+ type: cfg.type,
167
+ hasUrl: !!cfg.url,
168
+ hasCommand: !!cfg.command,
169
+ perAgent: cfg.perAgent || false,
170
+ })),
171
+ }
172
+ );
173
+
174
+ return workerConfig;
175
+ }
176
+
177
+ /**
178
+ * Get status of all MCPs for a specific agent
179
+ */
180
+ async getMcpStatus(agentId: string): Promise<McpStatus[]> {
181
+ const httpServers = await this.getAllHttpServers(agentId);
182
+ const statuses: McpStatus[] = [];
183
+
184
+ for (const [id, httpServer] of httpServers) {
185
+ const hasOAuth = !!httpServer.oauth;
186
+ const hasLoginUrl = !!httpServer.loginUrl;
187
+ const requiresAuth = hasOAuth || hasLoginUrl;
188
+ const requiresInput = !!(
189
+ httpServer.inputs && httpServer.inputs.length > 0
190
+ );
191
+
192
+ statuses.push({
193
+ id,
194
+ name: id,
195
+ requiresAuth,
196
+ requiresInput,
197
+ });
198
+ }
199
+
200
+ return statuses;
201
+ }
202
+
203
+ /**
204
+ * Get HTTP proxy metadata for a specific MCP server.
205
+ */
206
+ async getHttpServer(
207
+ id: string,
208
+ agentId?: string
209
+ ): Promise<HttpMcpServerConfig | undefined> {
210
+ const httpServers = await this.getAllHttpServers(agentId);
211
+ return httpServers.get(id);
212
+ }
213
+
214
+ /**
215
+ * Get all HTTP proxy metadata for all MCP servers.
216
+ */
217
+ async getAllHttpServers(
218
+ agentId?: string
219
+ ): Promise<Map<string, HttpMcpServerConfig>> {
220
+ const config = await this.loadConfig();
221
+ const merged = new Map(config.httpServers);
222
+
223
+ if (agentId) {
224
+ const agentMcpServers = await this.getAgentMcpServers(agentId);
225
+ for (const [id, serverConfig] of Object.entries(agentMcpServers)) {
226
+ if (merged.has(id)) continue;
227
+ const httpServer = toHttpServerConfig(id, serverConfig);
228
+ if (httpServer) {
229
+ merged.set(id, httpServer);
230
+ }
231
+ }
232
+ }
233
+
234
+ return merged;
235
+ }
236
+
237
+ /**
238
+ * Return global MCP server configs in settings-compatible format.
239
+ */
240
+ async getGlobalMcpServers(): Promise<
241
+ Record<string, { url?: string; type?: "sse" | "stdio" }>
242
+ > {
243
+ const config = await this.loadConfig();
244
+ const result: Record<string, { url?: string; type?: "sse" | "stdio" }> = {};
245
+ for (const [id, raw] of Object.entries(config.rawServers)) {
246
+ const type = raw.type === "stdio" ? ("stdio" as const) : ("sse" as const);
247
+ result[id] = { url: raw.url, type };
248
+ }
249
+ return result;
250
+ }
251
+
252
+ private async getAgentMcpServers(
253
+ agentId: string
254
+ ): Promise<Record<string, any>> {
255
+ if (!this.agentSettingsStore) {
256
+ return {};
257
+ }
258
+
259
+ try {
260
+ const settings = await this.agentSettingsStore.getSettings(agentId);
261
+ return settings?.mcpServers || {};
262
+ } catch (error) {
263
+ logger.warn(`Failed to load per-agent MCP settings for ${agentId}`, {
264
+ error,
265
+ });
266
+ return {};
267
+ }
268
+ }
269
+
270
+ private async loadConfig(): Promise<LoadedConfig> {
271
+ if (!this.cache) {
272
+ let globalMcpServers: Record<string, any> = {};
273
+ if (this.configResolver) {
274
+ globalMcpServers = await this.configResolver.getGlobalMcpServers();
275
+ }
276
+ const normalized = normalizeConfig({ mcpServers: globalMcpServers });
277
+ this.cache = normalized;
278
+ }
279
+ return this.cache;
280
+ }
281
+ }
282
+
283
+ function normalizeConfig(config: { mcpServers: Record<string, any> }) {
284
+ const rawServers: Record<string, any> = {};
285
+ const httpServers = new Map<string, HttpMcpServerConfig>();
286
+
287
+ for (const [id, serverConfig] of Object.entries(config.mcpServers)) {
288
+ if (!serverConfig || typeof serverConfig !== "object") {
289
+ continue;
290
+ }
291
+
292
+ const cloned = cloneConfig(serverConfig);
293
+ rawServers[id] = cloned;
294
+
295
+ if (typeof cloned.url === "string" && isHttpUrl(cloned.url)) {
296
+ httpServers.set(id, {
297
+ id,
298
+ upstreamUrl: cloned.url,
299
+ oauth:
300
+ cloned.oauth && typeof cloned.oauth === "object"
301
+ ? cloned.oauth
302
+ : undefined,
303
+ inputs: Array.isArray(cloned.inputs)
304
+ ? cloned.inputs.filter(
305
+ (input: any) =>
306
+ input &&
307
+ typeof input === "object" &&
308
+ input.type === "promptString"
309
+ )
310
+ : undefined,
311
+ headers:
312
+ cloned.headers && typeof cloned.headers === "object"
313
+ ? cloned.headers
314
+ : undefined,
315
+ loginUrl:
316
+ typeof cloned.loginUrl === "string" ? cloned.loginUrl : undefined,
317
+ resource:
318
+ typeof cloned.resource === "string" ? cloned.resource : undefined,
319
+ });
320
+ }
321
+ }
322
+
323
+ return { rawServers, httpServers };
324
+ }
325
+
326
+ function toHttpServerConfig(
327
+ id: string,
328
+ serverConfig: any
329
+ ): HttpMcpServerConfig | null {
330
+ if (!serverConfig || typeof serverConfig !== "object") {
331
+ return null;
332
+ }
333
+
334
+ if (serverConfig.enabled === false) {
335
+ return null;
336
+ }
337
+
338
+ const cloned = cloneConfig(serverConfig);
339
+ if (typeof cloned.url !== "string" || !isHttpUrl(cloned.url)) {
340
+ return null;
341
+ }
342
+
343
+ return {
344
+ id,
345
+ upstreamUrl: cloned.url,
346
+ oauth:
347
+ cloned.oauth && typeof cloned.oauth === "object"
348
+ ? cloned.oauth
349
+ : undefined,
350
+ inputs: Array.isArray(cloned.inputs)
351
+ ? cloned.inputs.filter(
352
+ (input: any) =>
353
+ input && typeof input === "object" && input.type === "promptString"
354
+ )
355
+ : undefined,
356
+ headers:
357
+ cloned.headers && typeof cloned.headers === "object"
358
+ ? cloned.headers
359
+ : undefined,
360
+ loginUrl: typeof cloned.loginUrl === "string" ? cloned.loginUrl : undefined,
361
+ };
362
+ }
363
+
364
+ function cloneConfig(config: any) {
365
+ return JSON.parse(JSON.stringify(config));
366
+ }
367
+
368
+ function isHttpUrl(candidate: string): boolean {
369
+ return candidate.startsWith("http://") || candidate.startsWith("https://");
370
+ }
371
+
372
+ function mergeHeaders(
373
+ existingHeaders: unknown,
374
+ workerToken: string,
375
+ mcpId: string
376
+ ): Record<string, string> {
377
+ const normalized: Record<string, string> = {};
378
+
379
+ if (existingHeaders && typeof existingHeaders === "object") {
380
+ for (const [key, value] of Object.entries(existingHeaders as any)) {
381
+ if (typeof value === "string") {
382
+ normalized[key] = value;
383
+ } else if (value != null) {
384
+ normalized[key] = String(value);
385
+ }
386
+ }
387
+ }
388
+
389
+ normalized.Authorization = `Bearer ${workerToken}`;
390
+ normalized["X-Mcp-Id"] = mcpId;
391
+ return normalized;
392
+ }