@lobu/gateway 3.0.9 → 3.0.13

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 (212) hide show
  1. package/dist/api/platform.d.ts.map +1 -1
  2. package/dist/api/platform.js +7 -26
  3. package/dist/api/platform.js.map +1 -1
  4. package/dist/auth/mcp/proxy.d.ts +14 -0
  5. package/dist/auth/mcp/proxy.d.ts.map +1 -1
  6. package/dist/auth/mcp/proxy.js +149 -13
  7. package/dist/auth/mcp/proxy.js.map +1 -1
  8. package/dist/cli/gateway.d.ts.map +1 -1
  9. package/dist/cli/gateway.js +29 -0
  10. package/dist/cli/gateway.js.map +1 -1
  11. package/dist/connections/chat-instance-manager.d.ts.map +1 -1
  12. package/dist/connections/chat-instance-manager.js +2 -1
  13. package/dist/connections/chat-instance-manager.js.map +1 -1
  14. package/dist/connections/interaction-bridge.d.ts +9 -2
  15. package/dist/connections/interaction-bridge.d.ts.map +1 -1
  16. package/dist/connections/interaction-bridge.js +121 -261
  17. package/dist/connections/interaction-bridge.js.map +1 -1
  18. package/dist/gateway/index.js +1 -1
  19. package/dist/gateway/index.js.map +1 -1
  20. package/dist/interactions.d.ts +9 -43
  21. package/dist/interactions.d.ts.map +1 -1
  22. package/dist/interactions.js +10 -52
  23. package/dist/interactions.js.map +1 -1
  24. package/dist/routes/public/agent.d.ts +4 -0
  25. package/dist/routes/public/agent.d.ts.map +1 -1
  26. package/dist/routes/public/agent.js +21 -0
  27. package/dist/routes/public/agent.js.map +1 -1
  28. package/dist/services/core-services.d.ts.map +1 -1
  29. package/dist/services/core-services.js +4 -0
  30. package/dist/services/core-services.js.map +1 -1
  31. package/package.json +9 -9
  32. package/src/__tests__/agent-config-routes.test.ts +0 -254
  33. package/src/__tests__/agent-history-routes.test.ts +0 -72
  34. package/src/__tests__/agent-routes.test.ts +0 -68
  35. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  36. package/src/__tests__/agent-settings-store.test.ts +0 -323
  37. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  38. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  39. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  40. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  41. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  42. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  43. package/src/__tests__/config-request-store.test.ts +0 -127
  44. package/src/__tests__/connection-routes.test.ts +0 -144
  45. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  46. package/src/__tests__/docker-deployment.test.ts +0 -1211
  47. package/src/__tests__/embedded-deployment.test.ts +0 -342
  48. package/src/__tests__/grant-store.test.ts +0 -148
  49. package/src/__tests__/http-proxy.test.ts +0 -281
  50. package/src/__tests__/instruction-service.test.ts +0 -37
  51. package/src/__tests__/link-buttons.test.ts +0 -112
  52. package/src/__tests__/lobu.test.ts +0 -32
  53. package/src/__tests__/mcp-config-service.test.ts +0 -347
  54. package/src/__tests__/mcp-proxy.test.ts +0 -694
  55. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  56. package/src/__tests__/model-selection.test.ts +0 -172
  57. package/src/__tests__/oauth-templates.test.ts +0 -39
  58. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  59. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  60. package/src/__tests__/provider-inheritance.test.ts +0 -212
  61. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  62. package/src/__tests__/routes/interactions.test.ts +0 -121
  63. package/src/__tests__/secret-proxy.test.ts +0 -85
  64. package/src/__tests__/session-manager.test.ts +0 -572
  65. package/src/__tests__/setup.ts +0 -133
  66. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  67. package/src/__tests__/slack-routes.test.ts +0 -161
  68. package/src/__tests__/system-config-resolver.test.ts +0 -75
  69. package/src/__tests__/system-message-limiter.test.ts +0 -89
  70. package/src/__tests__/system-skills-service.test.ts +0 -362
  71. package/src/__tests__/transcription-service.test.ts +0 -222
  72. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  73. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  74. package/src/__tests__/worker-job-router.test.ts +0 -722
  75. package/src/api/index.ts +0 -1
  76. package/src/api/platform.ts +0 -292
  77. package/src/api/response-renderer.ts +0 -157
  78. package/src/auth/agent-metadata-store.ts +0 -168
  79. package/src/auth/api-auth-middleware.ts +0 -69
  80. package/src/auth/api-key-provider-module.ts +0 -213
  81. package/src/auth/base-provider-module.ts +0 -201
  82. package/src/auth/bedrock/provider-module.ts +0 -110
  83. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  84. package/src/auth/chatgpt/device-code-client.ts +0 -218
  85. package/src/auth/chatgpt/index.ts +0 -1
  86. package/src/auth/claude/oauth-module.ts +0 -280
  87. package/src/auth/cli/token-service.ts +0 -249
  88. package/src/auth/external/client.ts +0 -560
  89. package/src/auth/external/device-code-client.ts +0 -235
  90. package/src/auth/mcp/config-service.ts +0 -420
  91. package/src/auth/mcp/proxy.ts +0 -1086
  92. package/src/auth/mcp/string-substitution.ts +0 -17
  93. package/src/auth/mcp/tool-cache.ts +0 -90
  94. package/src/auth/oauth/base-client.ts +0 -267
  95. package/src/auth/oauth/client.ts +0 -153
  96. package/src/auth/oauth/credentials.ts +0 -7
  97. package/src/auth/oauth/providers.ts +0 -69
  98. package/src/auth/oauth/state-store.ts +0 -150
  99. package/src/auth/oauth-templates.ts +0 -179
  100. package/src/auth/provider-catalog.ts +0 -220
  101. package/src/auth/provider-model-options.ts +0 -41
  102. package/src/auth/settings/agent-settings-store.ts +0 -565
  103. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  104. package/src/auth/settings/index.ts +0 -12
  105. package/src/auth/settings/model-preference-store.ts +0 -52
  106. package/src/auth/settings/model-selection.ts +0 -135
  107. package/src/auth/settings/resolved-settings-view.ts +0 -298
  108. package/src/auth/settings/template-utils.ts +0 -44
  109. package/src/auth/settings/token-service.ts +0 -88
  110. package/src/auth/system-env-store.ts +0 -98
  111. package/src/auth/user-agents-store.ts +0 -68
  112. package/src/channels/binding-service.ts +0 -214
  113. package/src/channels/index.ts +0 -4
  114. package/src/cli/gateway.ts +0 -1312
  115. package/src/cli/index.ts +0 -74
  116. package/src/commands/built-in-commands.ts +0 -80
  117. package/src/commands/command-dispatcher.ts +0 -94
  118. package/src/commands/command-reply-adapters.ts +0 -27
  119. package/src/config/file-loader.ts +0 -618
  120. package/src/config/index.ts +0 -588
  121. package/src/config/network-allowlist.ts +0 -71
  122. package/src/connections/chat-instance-manager.ts +0 -1284
  123. package/src/connections/chat-response-bridge.ts +0 -618
  124. package/src/connections/index.ts +0 -7
  125. package/src/connections/interaction-bridge.ts +0 -831
  126. package/src/connections/message-handler-bridge.ts +0 -440
  127. package/src/connections/platform-auth-methods.ts +0 -15
  128. package/src/connections/types.ts +0 -84
  129. package/src/gateway/connection-manager.ts +0 -291
  130. package/src/gateway/index.ts +0 -698
  131. package/src/gateway/job-router.ts +0 -201
  132. package/src/gateway-main.ts +0 -200
  133. package/src/index.ts +0 -41
  134. package/src/infrastructure/queue/index.ts +0 -12
  135. package/src/infrastructure/queue/queue-producer.ts +0 -148
  136. package/src/infrastructure/queue/redis-queue.ts +0 -361
  137. package/src/infrastructure/queue/types.ts +0 -133
  138. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  139. package/src/interactions/config-request-store.ts +0 -198
  140. package/src/interactions.ts +0 -363
  141. package/src/lobu.ts +0 -311
  142. package/src/metrics/prometheus.ts +0 -159
  143. package/src/modules/module-system.ts +0 -179
  144. package/src/orchestration/base-deployment-manager.ts +0 -900
  145. package/src/orchestration/deployment-utils.ts +0 -98
  146. package/src/orchestration/impl/docker-deployment.ts +0 -620
  147. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  148. package/src/orchestration/impl/index.ts +0 -8
  149. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  150. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  151. package/src/orchestration/impl/k8s/index.ts +0 -1
  152. package/src/orchestration/index.ts +0 -333
  153. package/src/orchestration/message-consumer.ts +0 -584
  154. package/src/orchestration/scheduled-wakeup.ts +0 -704
  155. package/src/permissions/approval-policy.ts +0 -36
  156. package/src/permissions/grant-store.ts +0 -219
  157. package/src/platform/file-handler.ts +0 -66
  158. package/src/platform/link-buttons.ts +0 -57
  159. package/src/platform/renderer-utils.ts +0 -44
  160. package/src/platform/response-renderer.ts +0 -84
  161. package/src/platform/unified-thread-consumer.ts +0 -194
  162. package/src/platform.ts +0 -318
  163. package/src/proxy/http-proxy.ts +0 -752
  164. package/src/proxy/proxy-manager.ts +0 -81
  165. package/src/proxy/secret-proxy.ts +0 -402
  166. package/src/proxy/token-refresh-job.ts +0 -143
  167. package/src/routes/internal/audio.ts +0 -141
  168. package/src/routes/internal/device-auth.ts +0 -652
  169. package/src/routes/internal/files.ts +0 -226
  170. package/src/routes/internal/history.ts +0 -69
  171. package/src/routes/internal/images.ts +0 -127
  172. package/src/routes/internal/interactions.ts +0 -84
  173. package/src/routes/internal/middleware.ts +0 -23
  174. package/src/routes/internal/schedule.ts +0 -226
  175. package/src/routes/internal/types.ts +0 -22
  176. package/src/routes/openapi-auto.ts +0 -239
  177. package/src/routes/public/agent-access.ts +0 -23
  178. package/src/routes/public/agent-config.ts +0 -675
  179. package/src/routes/public/agent-history.ts +0 -422
  180. package/src/routes/public/agent-schedules.ts +0 -296
  181. package/src/routes/public/agent.ts +0 -1086
  182. package/src/routes/public/agents.ts +0 -373
  183. package/src/routes/public/channels.ts +0 -191
  184. package/src/routes/public/cli-auth.ts +0 -896
  185. package/src/routes/public/connections.ts +0 -574
  186. package/src/routes/public/landing.ts +0 -16
  187. package/src/routes/public/oauth.ts +0 -147
  188. package/src/routes/public/settings-auth.ts +0 -104
  189. package/src/routes/public/slack.ts +0 -173
  190. package/src/routes/shared/agent-ownership.ts +0 -101
  191. package/src/routes/shared/token-verifier.ts +0 -34
  192. package/src/services/bedrock-model-catalog.ts +0 -217
  193. package/src/services/bedrock-openai-service.ts +0 -658
  194. package/src/services/core-services.ts +0 -1072
  195. package/src/services/image-generation-service.ts +0 -257
  196. package/src/services/instruction-service.ts +0 -318
  197. package/src/services/mcp-registry.ts +0 -94
  198. package/src/services/platform-helpers.ts +0 -287
  199. package/src/services/session-manager.ts +0 -262
  200. package/src/services/settings-resolver.ts +0 -74
  201. package/src/services/system-config-resolver.ts +0 -89
  202. package/src/services/system-skills-service.ts +0 -229
  203. package/src/services/transcription-service.ts +0 -684
  204. package/src/session.ts +0 -110
  205. package/src/spaces/index.ts +0 -1
  206. package/src/spaces/space-resolver.ts +0 -17
  207. package/src/stores/in-memory-agent-store.ts +0 -403
  208. package/src/stores/redis-agent-store.ts +0 -279
  209. package/src/utils/public-url.ts +0 -44
  210. package/src/utils/rate-limiter.ts +0 -94
  211. package/tsconfig.json +0 -33
  212. package/tsconfig.tsbuildinfo +0 -1
@@ -1,1086 +0,0 @@
1
- import dns from "node:dns/promises";
2
- import { createLogger, verifyWorkerToken } from "@lobu/core";
3
- import type { Context } from "hono";
4
- import { Hono } from "hono";
5
- import type { IMessageQueue } from "../../infrastructure/queue";
6
- import { requiresToolApproval } from "../../permissions/approval-policy";
7
- import type { GrantStore } from "../../permissions/grant-store";
8
- import {
9
- getStoredCredential,
10
- refreshCredential,
11
- tryCompletePendingDeviceAuth,
12
- } from "../../routes/internal/device-auth";
13
- import type { CachedMcpServer, McpTool, McpToolCache } from "./tool-cache";
14
-
15
- const logger = createLogger("mcp-proxy");
16
-
17
- const MAX_BODY_SIZE = 1024 * 1024; // 1MB
18
-
19
- /**
20
- * Check whether a resolved IP address belongs to a reserved/internal range.
21
- */
22
- function isReservedIp(ip: string): boolean {
23
- // IPv6 loopback
24
- if (ip === "::1") return true;
25
-
26
- // IPv6 unique local (fc00::/7)
27
- if (/^f[cd]/i.test(ip)) return true;
28
-
29
- // IPv4
30
- const parts = ip.split(".").map(Number);
31
- if (parts.length === 4) {
32
- const [a, b] = parts as [number, number, number, number];
33
- // 127.0.0.0/8
34
- if (a === 127) return true;
35
- // 10.0.0.0/8
36
- if (a === 10) return true;
37
- // 172.16.0.0/12
38
- if (a === 172 && b >= 16 && b <= 31) return true;
39
- // 192.168.0.0/16
40
- if (a === 192 && b === 168) return true;
41
- // 169.254.0.0/16 (link-local)
42
- if (a === 169 && b === 254) return true;
43
- }
44
-
45
- return false;
46
- }
47
-
48
- /**
49
- * Resolve a URL's hostname and check whether it points to an internal/reserved network.
50
- */
51
- async function isInternalUrl(url: string): Promise<boolean> {
52
- try {
53
- const parsed = new URL(url);
54
- const hostname = parsed.hostname;
55
-
56
- // Check if hostname is already an IP literal
57
- if (isReservedIp(hostname)) return true;
58
-
59
- // Resolve hostname to IP addresses
60
- const addresses = await dns.resolve4(hostname).catch(() => [] as string[]);
61
- const addresses6 = await dns.resolve6(hostname).catch(() => [] as string[]);
62
-
63
- for (const addr of [...addresses, ...addresses6]) {
64
- if (isReservedIp(addr)) return true;
65
- }
66
-
67
- return false;
68
- } catch {
69
- // If URL parsing fails, block it
70
- return true;
71
- }
72
- }
73
-
74
- interface JsonRpcResponse {
75
- jsonrpc: string;
76
- id: unknown;
77
- result?: {
78
- tools?: McpTool[];
79
- content?: unknown[];
80
- isError?: boolean;
81
- };
82
- error?: { code: number; message: string };
83
- }
84
-
85
- interface HttpMcpServerConfig {
86
- id: string;
87
- upstreamUrl: string;
88
- oauth?: import("@lobu/core").McpOAuthConfig;
89
- inputs?: unknown[];
90
- headers?: Record<string, string>;
91
- }
92
-
93
- interface McpConfigSource {
94
- getHttpServer(
95
- id: string,
96
- agentId?: string
97
- ): Promise<HttpMcpServerConfig | undefined>;
98
- getAllHttpServers(
99
- agentId?: string
100
- ): Promise<Map<string, HttpMcpServerConfig>>;
101
- }
102
-
103
- function authenticateRequest(
104
- c: Context
105
- ): { tokenData: any; token: string } | null {
106
- const sessionToken = extractSessionToken(c);
107
- if (!sessionToken) return null;
108
-
109
- const tokenData = verifyWorkerToken(sessionToken);
110
- if (!tokenData) return null;
111
-
112
- return { tokenData, token: sessionToken };
113
- }
114
-
115
- function extractSessionToken(c: Context): string | null {
116
- const authHeader = c.req.header("authorization");
117
- if (authHeader?.startsWith("Bearer ")) {
118
- return authHeader.substring(7);
119
- }
120
-
121
- const tokenFromQuery = c.req.query("workerToken");
122
- if (typeof tokenFromQuery === "string") {
123
- return tokenFromQuery;
124
- }
125
-
126
- return null;
127
- }
128
-
129
- export class McpProxy {
130
- private readonly SESSION_TTL_SECONDS = 30 * 60; // 30 minutes
131
- private readonly redisClient: any;
132
- private app: Hono;
133
- private toolCache?: McpToolCache;
134
-
135
- constructor(
136
- private readonly configService: McpConfigSource,
137
- queue: IMessageQueue,
138
- toolCache?: McpToolCache,
139
- private readonly grantStore?: GrantStore
140
- ) {
141
- this.redisClient = queue.getRedisClient();
142
- this.toolCache = toolCache;
143
- this.app = new Hono();
144
- this.setupRoutes();
145
- logger.debug("MCP proxy initialized");
146
- }
147
-
148
- getApp(): Hono {
149
- return this.app;
150
- }
151
-
152
- /**
153
- * Check if this request is an MCP proxy request (has X-Mcp-Id header)
154
- * Used by gateway to determine if root path requests should be handled by MCP proxy
155
- */
156
- isMcpRequest(c: Context): boolean {
157
- return !!c.req.header("x-mcp-id");
158
- }
159
-
160
- /**
161
- * Fetch tools and instructions for a specific MCP server.
162
- * Performs MCP initialize handshake first to capture server instructions,
163
- * then fetches tool list.
164
- */
165
- async fetchToolsForMcp(
166
- mcpId: string,
167
- agentId: string,
168
- tokenData: any
169
- ): Promise<{ tools: McpTool[]; instructions?: string }> {
170
- if (this.toolCache) {
171
- const cached = await this.toolCache.getServerInfo(mcpId, agentId);
172
- if (cached) return cached;
173
- }
174
-
175
- const httpServer = await this.configService.getHttpServer(mcpId, agentId);
176
- if (!httpServer) {
177
- return { tools: [] };
178
- }
179
-
180
- const userId = tokenData?.userId;
181
-
182
- try {
183
- // Clear any stale session before fresh tool discovery
184
- const sessionKey = `mcp:session:${agentId}:${mcpId}`;
185
- await this.redisClient.del(sessionKey).catch(() => {
186
- /* noop */
187
- });
188
-
189
- // Step 1: Send initialize to capture server instructions
190
- let instructions: string | undefined;
191
- try {
192
- const initBody = JSON.stringify({
193
- jsonrpc: "2.0",
194
- method: "initialize",
195
- params: {
196
- protocolVersion: "2024-11-05",
197
- capabilities: {},
198
- clientInfo: { name: "lobu-gateway", version: "1.0.0" },
199
- },
200
- id: 0,
201
- });
202
-
203
- const initResponse = await this.sendUpstreamRequest(
204
- httpServer,
205
- agentId,
206
- mcpId,
207
- "POST",
208
- initBody,
209
- userId
210
- );
211
-
212
- const initData = (await initResponse.json()) as {
213
- result?: { instructions?: string };
214
- error?: { code: number; message: string };
215
- };
216
-
217
- if (initData?.result?.instructions) {
218
- instructions = initData.result.instructions;
219
- logger.info("Captured MCP server instructions", {
220
- mcpId,
221
- length: instructions.length,
222
- });
223
- }
224
-
225
- // Step 2: Send initialized notification (required by MCP spec)
226
- const notifyBody = JSON.stringify({
227
- jsonrpc: "2.0",
228
- method: "notifications/initialized",
229
- });
230
- await this.sendUpstreamRequest(
231
- httpServer,
232
- agentId,
233
- mcpId,
234
- "POST",
235
- notifyBody,
236
- userId
237
- ).catch(() => {
238
- /* noop */
239
- });
240
- } catch (initError) {
241
- logger.warn("MCP initialize failed (continuing with tools/list)", {
242
- mcpId,
243
- error:
244
- initError instanceof Error ? initError.message : String(initError),
245
- });
246
- }
247
-
248
- // Step 3: Fetch tools list
249
- const jsonRpcBody = JSON.stringify({
250
- jsonrpc: "2.0",
251
- method: "tools/list",
252
- params: {},
253
- id: 1,
254
- });
255
-
256
- const response = await this.sendUpstreamRequest(
257
- httpServer,
258
- agentId,
259
- mcpId,
260
- "POST",
261
- jsonRpcBody,
262
- userId
263
- );
264
-
265
- const data = (await response.json()) as JsonRpcResponse;
266
- const tools: McpTool[] = data?.result?.tools || [];
267
-
268
- const serverInfo: CachedMcpServer = { tools, instructions };
269
- if (this.toolCache && tools.length > 0) {
270
- await this.toolCache.setServerInfo(mcpId, serverInfo, agentId);
271
- }
272
-
273
- return serverInfo;
274
- } catch (error) {
275
- logger.warn("Failed to fetch tools for MCP, retrying once", {
276
- mcpId,
277
- error: error instanceof Error ? error.message : String(error),
278
- });
279
-
280
- // Retry once after a short delay (upstream may still be starting)
281
- await new Promise((r) => setTimeout(r, 2000));
282
- try {
283
- const retryBody = JSON.stringify({
284
- jsonrpc: "2.0",
285
- method: "tools/list",
286
- params: {},
287
- id: 1,
288
- });
289
- const retryResponse = await this.sendUpstreamRequest(
290
- httpServer,
291
- agentId,
292
- mcpId,
293
- "POST",
294
- retryBody,
295
- userId
296
- );
297
- const retryData = (await retryResponse.json()) as JsonRpcResponse;
298
- const retryTools: McpTool[] = retryData?.result?.tools || [];
299
- if (retryTools.length > 0) {
300
- const serverInfo: CachedMcpServer = { tools: retryTools };
301
- if (this.toolCache) {
302
- await this.toolCache.setServerInfo(mcpId, serverInfo, agentId);
303
- }
304
- logger.info("Retry succeeded for MCP tool fetch", {
305
- mcpId,
306
- toolCount: retryTools.length,
307
- });
308
- return serverInfo;
309
- }
310
- } catch (retryError) {
311
- logger.error("Retry also failed for MCP tool fetch", {
312
- mcpId,
313
- error:
314
- retryError instanceof Error
315
- ? retryError.message
316
- : String(retryError),
317
- });
318
- }
319
- return { tools: [] };
320
- }
321
- }
322
-
323
- private setupRoutes() {
324
- // REST API endpoints for curl-based tool access (registered BEFORE catch-all)
325
- this.app.get("/tools", (c) => this.handleListAllTools(c));
326
- this.app.get("/:mcpId/tools", (c) => this.handleListTools(c));
327
- this.app.post("/:mcpId/tools/:toolName", (c) => this.handleCallTool(c));
328
-
329
- // Legacy endpoints (if needed for other MCP transports)
330
- this.app.all("/register", (c) => this.handleProxyRequest(c));
331
- this.app.all("/message", (c) => this.handleProxyRequest(c));
332
-
333
- // Path-based routes (for SSE or other transports)
334
- this.app.all("/:mcpId", (c) => this.handleProxyRequest(c));
335
- this.app.all("/:mcpId/*", (c) => this.handleProxyRequest(c));
336
- }
337
-
338
- private async handleListTools(c: Context): Promise<Response> {
339
- const mcpId = c.req.param("mcpId");
340
- if (!mcpId) return c.json({ error: "Missing MCP server id" }, 400);
341
- const auth = authenticateRequest(c);
342
- if (!auth) return c.json({ error: "Invalid authentication token" }, 401);
343
-
344
- const agentId = auth.tokenData.agentId || auth.tokenData.userId;
345
- const requesterUserId = auth.tokenData.userId;
346
- if (!agentId || !requesterUserId) {
347
- return c.json({ error: "Invalid authentication token" }, 401);
348
- }
349
- const httpServer = await this.configService.getHttpServer(mcpId, agentId);
350
- if (!httpServer) {
351
- return c.json({ error: `MCP server '${mcpId}' not found` }, 404);
352
- }
353
-
354
- // Check cache
355
- if (this.toolCache) {
356
- const cached = await this.toolCache.get(mcpId, agentId);
357
- if (cached) return c.json({ tools: cached });
358
- }
359
-
360
- try {
361
- const jsonRpcBody = JSON.stringify({
362
- jsonrpc: "2.0",
363
- method: "tools/list",
364
- params: {},
365
- id: 1,
366
- });
367
-
368
- const response = await this.sendUpstreamRequest(
369
- httpServer,
370
- agentId,
371
- mcpId,
372
- "POST",
373
- jsonRpcBody,
374
- requesterUserId
375
- );
376
-
377
- const data = (await response.json()) as JsonRpcResponse;
378
- if (data?.error) {
379
- logger.error("Upstream returned JSON-RPC error", {
380
- mcpId,
381
- error: data.error,
382
- });
383
- return c.json({ error: data.error.message || "Upstream error" }, 502);
384
- }
385
-
386
- const tools: McpTool[] = data?.result?.tools || [];
387
-
388
- // Cache result
389
- if (this.toolCache && tools.length > 0) {
390
- await this.toolCache.set(mcpId, tools, agentId);
391
- }
392
-
393
- return c.json({ tools });
394
- } catch (error) {
395
- logger.error("Failed to list tools", { mcpId, error });
396
- return c.json(
397
- {
398
- error: `Failed to connect to MCP '${mcpId}': ${error instanceof Error ? error.message : "Unknown error"}`,
399
- },
400
- 502
401
- );
402
- }
403
- }
404
-
405
- private async handleCallTool(c: Context): Promise<Response> {
406
- const mcpId = c.req.param("mcpId");
407
- const toolName = c.req.param("toolName");
408
- if (!mcpId || !toolName) {
409
- return c.json({ error: "Missing MCP server id or tool name" }, 400);
410
- }
411
- const auth = authenticateRequest(c);
412
- if (!auth) return c.json({ error: "Invalid authentication token" }, 401);
413
-
414
- const agentId = auth.tokenData.agentId || auth.tokenData.userId;
415
- const requesterUserId = auth.tokenData.userId;
416
- if (!agentId || !requesterUserId) {
417
- return c.json({ error: "Invalid authentication token" }, 401);
418
- }
419
- const httpServer = await this.configService.getHttpServer(mcpId, agentId);
420
- if (!httpServer) {
421
- return c.json({ error: `MCP server '${mcpId}' not found` }, 404);
422
- }
423
-
424
- // Check tool approval based on annotations and grants.
425
- if (this.grantStore) {
426
- const { found, annotations } = await this.getToolAnnotations(
427
- mcpId,
428
- toolName,
429
- agentId,
430
- auth.tokenData
431
- );
432
- if (found && requiresToolApproval(annotations)) {
433
- const pattern = `/mcp/${mcpId}/tools/${toolName}`;
434
- const hasGrant = await this.grantStore.hasGrant(agentId, pattern);
435
- if (!hasGrant) {
436
- logger.info("Tool call blocked: requires approval", {
437
- agentId,
438
- mcpId,
439
- toolName,
440
- pattern,
441
- });
442
- return c.json(
443
- {
444
- content: [
445
- {
446
- type: "text",
447
- text: `Tool call requires approval. Request access approval in chat for: ${mcpId} → ${toolName}`,
448
- },
449
- ],
450
- isError: true,
451
- },
452
- 403
453
- );
454
- }
455
- }
456
- }
457
-
458
- let toolArguments: Record<string, unknown> = {};
459
- try {
460
- const body = await c.req.text();
461
- if (body) {
462
- if (body.length > MAX_BODY_SIZE) {
463
- return c.json({ error: "Request body too large" }, 413);
464
- }
465
- toolArguments = JSON.parse(body);
466
- }
467
- } catch {
468
- return c.json({ error: "Invalid JSON body" }, 400);
469
- }
470
-
471
- try {
472
- const jsonRpcBody = JSON.stringify({
473
- jsonrpc: "2.0",
474
- method: "tools/call",
475
- params: { name: toolName, arguments: toolArguments },
476
- id: 1,
477
- });
478
-
479
- let response = await this.sendUpstreamRequest(
480
- httpServer,
481
- agentId,
482
- mcpId,
483
- "POST",
484
- jsonRpcBody,
485
- requesterUserId
486
- );
487
-
488
- let data = (await response.json()) as JsonRpcResponse;
489
-
490
- // Re-initialize session and retry on "Server not initialized"
491
- if (data?.error && /not initialized/i.test(data.error.message || "")) {
492
- logger.info("MCP session expired, re-initializing before retry", {
493
- mcpId,
494
- toolName,
495
- });
496
- await this.reinitializeSession(
497
- httpServer,
498
- agentId,
499
- mcpId,
500
- requesterUserId
501
- );
502
-
503
- response = await this.sendUpstreamRequest(
504
- httpServer,
505
- agentId,
506
- mcpId,
507
- "POST",
508
- jsonRpcBody,
509
- requesterUserId
510
- );
511
- data = (await response.json()) as JsonRpcResponse;
512
- }
513
-
514
- if (data?.error) {
515
- const errorMsg =
516
- data.error.message ||
517
- (typeof data.error === "string" ? data.error : "Upstream error");
518
- logger.error("Upstream returned JSON-RPC error on tool call", {
519
- mcpId,
520
- toolName,
521
- error: data.error,
522
- });
523
-
524
- // Detect auth errors — auto-start device-code auth flow
525
- if (/unauthorized|unauthenticated|forbidden/i.test(errorMsg)) {
526
- const autoAuthResult = await this.tryAutoDeviceAuth(
527
- mcpId,
528
- agentId,
529
- requesterUserId
530
- );
531
- if (autoAuthResult) {
532
- return c.json(
533
- {
534
- content: [
535
- {
536
- type: "text",
537
- text: autoAuthResult,
538
- },
539
- ],
540
- isError: true,
541
- },
542
- 200
543
- );
544
- }
545
- return c.json(
546
- {
547
- content: [
548
- {
549
- type: "text",
550
- text: `Authentication required for ${mcpId}. Call owletto_login to authenticate.`,
551
- },
552
- ],
553
- isError: true,
554
- },
555
- 200
556
- );
557
- }
558
-
559
- return c.json(
560
- {
561
- content: [],
562
- isError: true,
563
- error: errorMsg,
564
- },
565
- 502
566
- );
567
- }
568
-
569
- const result = data?.result || {};
570
- return c.json({
571
- content: result.content || [],
572
- isError: result.isError || false,
573
- });
574
- } catch (error) {
575
- logger.error("Failed to call tool", { mcpId, toolName, error });
576
- return c.json(
577
- {
578
- content: [],
579
- isError: true,
580
- error: `Failed to connect to MCP '${mcpId}': ${error instanceof Error ? error.message : "Unknown error"}`,
581
- },
582
- 502
583
- );
584
- }
585
- }
586
-
587
- private async handleListAllTools(c: Context): Promise<Response> {
588
- const auth = authenticateRequest(c);
589
- if (!auth) return c.json({ error: "Invalid authentication token" }, 401);
590
-
591
- const agentId = auth.tokenData.agentId || auth.tokenData.userId;
592
-
593
- const allHttpServers = await this.configService.getAllHttpServers(agentId);
594
- const allMcpIds = Array.from(allHttpServers.keys());
595
-
596
- const mcpServers: Record<string, { tools: McpTool[] }> = {};
597
-
598
- // Fetch tools in parallel, tolerate failures
599
- const results = await Promise.allSettled(
600
- allMcpIds.map(async (mcpId) => {
601
- const { tools } = await this.fetchToolsForMcp(
602
- mcpId,
603
- agentId,
604
- auth.tokenData
605
- );
606
- return { mcpId, tools };
607
- })
608
- );
609
-
610
- for (const result of results) {
611
- if (result.status === "fulfilled" && result.value.tools.length > 0) {
612
- mcpServers[result.value.mcpId] = { tools: result.value.tools };
613
- }
614
- }
615
-
616
- return c.json({ mcpServers });
617
- }
618
-
619
- private async handleProxyRequest(c: Context): Promise<Response> {
620
- const mcpId = c.req.param("mcpId") || c.req.header("x-mcp-id");
621
- const sessionToken = extractSessionToken(c);
622
-
623
- logger.info("Handling MCP proxy request", {
624
- method: c.req.method,
625
- path: c.req.path,
626
- mcpId,
627
- hasSessionToken: !!sessionToken,
628
- });
629
-
630
- if (!mcpId) {
631
- return this.sendJsonRpcError(c, -32600, "Missing MCP ID");
632
- }
633
-
634
- if (!sessionToken) {
635
- return this.sendJsonRpcError(c, -32600, "Missing authentication token");
636
- }
637
-
638
- const tokenData = verifyWorkerToken(sessionToken);
639
- if (!tokenData) {
640
- return this.sendJsonRpcError(c, -32600, "Invalid authentication token");
641
- }
642
-
643
- const agentId = tokenData.agentId || tokenData.userId;
644
- const httpServer = await this.configService.getHttpServer(mcpId!, agentId);
645
-
646
- if (!httpServer) {
647
- return this.sendJsonRpcError(
648
- c,
649
- -32601,
650
- `MCP server '${mcpId}' not found`
651
- );
652
- }
653
-
654
- try {
655
- return await this.forwardRequest(
656
- c,
657
- httpServer,
658
- agentId,
659
- mcpId!,
660
- tokenData.userId
661
- );
662
- } catch (error) {
663
- logger.error("Failed to proxy MCP request", { error, mcpId });
664
- return this.sendJsonRpcError(
665
- c,
666
- -32603,
667
- `Failed to connect to MCP '${mcpId}': ${error instanceof Error ? error.message : "Unknown error"}`
668
- );
669
- }
670
- }
671
-
672
- private async getToolAnnotations(
673
- mcpId: string,
674
- toolName: string,
675
- agentId: string,
676
- tokenData: any
677
- ): Promise<{ found: boolean; annotations?: McpTool["annotations"] }> {
678
- let tools: McpTool[] | null = null;
679
- if (this.toolCache) {
680
- tools = await this.toolCache.get(mcpId, agentId);
681
- }
682
-
683
- if (!tools) {
684
- const result = await this.fetchToolsForMcp(mcpId, agentId, tokenData);
685
- tools = result.tools;
686
- }
687
-
688
- if (tools.length === 0) {
689
- return { found: false };
690
- }
691
-
692
- const tool = tools.find((t) => t.name === toolName);
693
- return { found: true, annotations: tool?.annotations };
694
- }
695
-
696
- private buildUpstreamHeaders(
697
- sessionId: string | null,
698
- configHeaders?: Record<string, string>,
699
- credentialToken?: string
700
- ): Record<string, string> {
701
- const headers: Record<string, string> = {
702
- "Content-Type": "application/json",
703
- Accept: "application/json",
704
- };
705
-
706
- // Merge custom headers from server config (e.g. static auth tokens)
707
- if (configHeaders) {
708
- for (const [key, value] of Object.entries(configHeaders)) {
709
- headers[key] = value;
710
- }
711
- }
712
-
713
- // Per-user credential takes precedence over config headers for Authorization
714
- if (credentialToken) {
715
- headers.Authorization = `Bearer ${credentialToken}`;
716
- }
717
-
718
- if (sessionId) {
719
- headers["Mcp-Session-Id"] = sessionId;
720
- }
721
-
722
- return headers;
723
- }
724
-
725
- private async resolveCredentialToken(
726
- agentId: string,
727
- userId: string,
728
- mcpId: string
729
- ): Promise<string | null> {
730
- const credential = await getStoredCredential(
731
- this.redisClient,
732
- agentId,
733
- userId,
734
- mcpId
735
- );
736
- if (!credential) {
737
- // No stored credential — check if there's a pending device-auth to complete
738
- return tryCompletePendingDeviceAuth(
739
- this.redisClient,
740
- agentId,
741
- userId,
742
- mcpId
743
- );
744
- }
745
-
746
- // Check if token is still valid (5 minute buffer)
747
- if (credential.expiresAt > Date.now() + 5 * 60 * 1000) {
748
- return credential.accessToken;
749
- }
750
-
751
- // Token expired or expiring soon — refresh
752
- const refreshed = await refreshCredential(
753
- this.redisClient,
754
- agentId,
755
- userId,
756
- mcpId,
757
- credential
758
- );
759
- return refreshed?.accessToken ?? null;
760
- }
761
-
762
- private async sendUpstreamRequest(
763
- httpServer: HttpMcpServerConfig,
764
- agentId: string,
765
- mcpId: string,
766
- method: string,
767
- body?: string,
768
- userId?: string
769
- ): Promise<Response> {
770
- const sessionKey = `mcp:session:${agentId}:${mcpId}`;
771
- const sessionId = await this.getSession(sessionKey);
772
-
773
- // Look up per-user credential for this MCP
774
- let credentialToken: string | undefined;
775
- if (userId) {
776
- const token = await this.resolveCredentialToken(agentId, userId, mcpId);
777
- if (token) credentialToken = token;
778
- }
779
-
780
- // SSRF protection: block requests to internal networks
781
- if (await isInternalUrl(httpServer.upstreamUrl)) {
782
- logger.warn("Blocked SSRF attempt to internal URL", {
783
- url: httpServer.upstreamUrl,
784
- mcpId,
785
- agentId,
786
- });
787
- return new Response(
788
- JSON.stringify({
789
- jsonrpc: "2.0",
790
- id: null,
791
- error: {
792
- code: -32600,
793
- message: "Upstream URL resolves to a blocked internal network",
794
- },
795
- }),
796
- { status: 403, headers: { "Content-Type": "application/json" } }
797
- );
798
- }
799
-
800
- const headers = this.buildUpstreamHeaders(
801
- sessionId,
802
- httpServer.headers,
803
- credentialToken
804
- );
805
-
806
- const response = await fetch(httpServer.upstreamUrl, {
807
- method,
808
- headers,
809
- body: body || undefined,
810
- });
811
-
812
- // Track session
813
- const newSessionId = response.headers.get("Mcp-Session-Id");
814
- if (newSessionId) {
815
- await this.setSession(sessionKey, newSessionId);
816
- }
817
-
818
- return response;
819
- }
820
-
821
- private async forwardRequest(
822
- c: Context,
823
- httpServer: HttpMcpServerConfig,
824
- agentId: string,
825
- mcpId: string,
826
- userId?: string
827
- ): Promise<Response> {
828
- // SSRF protection: block requests to internal networks
829
- if (await isInternalUrl(httpServer.upstreamUrl)) {
830
- logger.warn("Blocked SSRF attempt to internal URL", {
831
- url: httpServer.upstreamUrl,
832
- mcpId,
833
- agentId,
834
- });
835
- return this.sendJsonRpcError(
836
- c,
837
- -32600,
838
- "Upstream URL resolves to a blocked internal network"
839
- );
840
- }
841
-
842
- const sessionKey = `mcp:session:${agentId}:${mcpId}`;
843
- let sessionId = await this.getSession(sessionKey);
844
-
845
- const bodyText = await this.getRequestBodyAsText(c);
846
-
847
- // Body size validation
848
- if (bodyText.length > MAX_BODY_SIZE) {
849
- logger.warn("Request body too large", {
850
- mcpId,
851
- agentId,
852
- size: bodyText.length,
853
- });
854
- return new Response("Request body too large", { status: 413 });
855
- }
856
-
857
- // If no active session exists, re-initialize before forwarding
858
- if (!sessionId && c.req.method === "POST") {
859
- try {
860
- await this.reinitializeSession(httpServer, agentId, mcpId, userId);
861
- sessionId = await this.getSession(sessionKey);
862
- } catch (error) {
863
- logger.warn("Pre-emptive MCP re-initialization failed", {
864
- mcpId,
865
- error: error instanceof Error ? error.message : String(error),
866
- });
867
- }
868
- }
869
-
870
- logger.info("Proxying MCP request", {
871
- mcpId,
872
- agentId,
873
- method: c.req.method,
874
- hasSession: !!sessionId,
875
- bodyLength: bodyText.length,
876
- });
877
-
878
- // Look up per-user credential for this MCP
879
- let credentialToken: string | undefined;
880
- if (userId) {
881
- const token = await this.resolveCredentialToken(agentId, userId, mcpId);
882
- if (token) credentialToken = token;
883
- }
884
-
885
- const headers = this.buildUpstreamHeaders(
886
- sessionId,
887
- httpServer.headers,
888
- credentialToken
889
- );
890
-
891
- const response = await fetch(httpServer.upstreamUrl, {
892
- method: c.req.method,
893
- headers,
894
- body: bodyText || undefined,
895
- });
896
-
897
- const newSessionId = response.headers.get("Mcp-Session-Id");
898
- if (newSessionId) {
899
- await this.setSession(sessionKey, newSessionId);
900
- logger.debug("Stored MCP session ID", {
901
- mcpId,
902
- agentId,
903
- sessionId: newSessionId,
904
- });
905
- }
906
-
907
- const responseHeaders = new Headers();
908
- const contentType = response.headers.get("content-type");
909
- if (contentType) {
910
- responseHeaders.set("Content-Type", contentType);
911
- }
912
- if (newSessionId) {
913
- responseHeaders.set("Mcp-Session-Id", newSessionId);
914
- }
915
-
916
- return new Response(response.body, {
917
- status: response.status,
918
- headers: responseHeaders,
919
- });
920
- }
921
-
922
- private async getRequestBodyAsText(c: Context): Promise<string> {
923
- if (c.req.method === "GET" || c.req.method === "HEAD") {
924
- return "";
925
- }
926
-
927
- try {
928
- return await c.req.text();
929
- } catch {
930
- return "";
931
- }
932
- }
933
-
934
- /**
935
- * Re-initialize an MCP session by sending initialize + notifications/initialized.
936
- * Called when upstream returns "Server not initialized" (stale session).
937
- */
938
- private async reinitializeSession(
939
- httpServer: HttpMcpServerConfig,
940
- agentId: string,
941
- mcpId: string,
942
- userId?: string
943
- ): Promise<void> {
944
- // Clear stale session
945
- const sessionKey = `mcp:session:${agentId}:${mcpId}`;
946
- await this.redisClient.del(sessionKey).catch(() => {
947
- /* noop */
948
- });
949
-
950
- // Send initialize
951
- const initBody = JSON.stringify({
952
- jsonrpc: "2.0",
953
- method: "initialize",
954
- params: {
955
- protocolVersion: "2024-11-05",
956
- capabilities: {},
957
- clientInfo: { name: "lobu-gateway", version: "1.0.0" },
958
- },
959
- id: 0,
960
- });
961
-
962
- const initResponse = await this.sendUpstreamRequest(
963
- httpServer,
964
- agentId,
965
- mcpId,
966
- "POST",
967
- initBody,
968
- userId
969
- );
970
-
971
- await initResponse.json(); // consume response
972
-
973
- // Send notifications/initialized
974
- const notifyBody = JSON.stringify({
975
- jsonrpc: "2.0",
976
- method: "notifications/initialized",
977
- });
978
- await this.sendUpstreamRequest(
979
- httpServer,
980
- agentId,
981
- mcpId,
982
- "POST",
983
- notifyBody,
984
- userId
985
- ).catch(() => {
986
- /* noop */
987
- });
988
-
989
- logger.info("Re-initialized MCP session", { mcpId, agentId });
990
- }
991
-
992
- /**
993
- * Auto-start device-code auth when an MCP upstream returns an auth error.
994
- * Returns a user-facing message with the verification URL, or null on failure.
995
- */
996
- private async tryAutoDeviceAuth(
997
- mcpId: string,
998
- agentId: string,
999
- userId: string
1000
- ): Promise<string | null> {
1001
- try {
1002
- const { startDeviceAuth } = await import(
1003
- "../../routes/internal/device-auth"
1004
- );
1005
-
1006
- // Check if a device auth flow is already pending (avoid duplicate starts)
1007
- const pendingKey = `device-auth:${agentId}:${userId}:${mcpId}`;
1008
- const pending = await this.redisClient.get(pendingKey);
1009
- if (pending) {
1010
- // Return the existing pending flow's info instead of starting a new one
1011
- return JSON.stringify({
1012
- status: "login_required",
1013
- message:
1014
- "Authentication is required. A login flow is already in progress. STOP calling tools and tell the user to complete login in their browser. Do NOT retry this tool call.",
1015
- note: "Do NOT call any owletto tools until the user completes login.",
1016
- });
1017
- }
1018
-
1019
- const result = await startDeviceAuth(
1020
- this.redisClient,
1021
- this.configService as any,
1022
- mcpId,
1023
- agentId,
1024
- userId
1025
- );
1026
- if (!result) return null;
1027
- const url = result.verificationUriComplete || result.verificationUri;
1028
- return JSON.stringify({
1029
- status: "login_required",
1030
- message:
1031
- "Authentication is required. STOP calling tools and show the user this login link and code. Do NOT retry this tool call — wait for the user to complete login first.",
1032
- verification_url: url,
1033
- user_code: result.userCode,
1034
- expires_in_seconds: result.expiresIn,
1035
- });
1036
- } catch (error) {
1037
- logger.warn("Auto device-auth failed", {
1038
- mcpId,
1039
- error: error instanceof Error ? error.message : String(error),
1040
- });
1041
- return null;
1042
- }
1043
- }
1044
-
1045
- private async getSession(key: string): Promise<string | null> {
1046
- try {
1047
- const sessionId = await this.redisClient.get(key);
1048
- if (sessionId) {
1049
- await this.redisClient.expire(key, this.SESSION_TTL_SECONDS);
1050
- }
1051
- return sessionId;
1052
- } catch (error) {
1053
- logger.error("Failed to get MCP session from Redis", { key, error });
1054
- return null;
1055
- }
1056
- }
1057
-
1058
- private async setSession(key: string, sessionId: string): Promise<void> {
1059
- try {
1060
- await this.redisClient.set(
1061
- key,
1062
- sessionId,
1063
- "EX",
1064
- this.SESSION_TTL_SECONDS
1065
- );
1066
- } catch (error) {
1067
- logger.error("Failed to store MCP session in Redis", { key, error });
1068
- }
1069
- }
1070
-
1071
- private sendJsonRpcError(
1072
- c: Context,
1073
- code: number,
1074
- message: string,
1075
- id: any = null
1076
- ): Response {
1077
- return c.json(
1078
- {
1079
- jsonrpc: "2.0",
1080
- id,
1081
- error: { code, message },
1082
- },
1083
- 200
1084
- );
1085
- }
1086
- }