@lobu/gateway 3.0.9 → 3.0.12

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 (210) 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/interactions.d.ts +9 -43
  19. package/dist/interactions.d.ts.map +1 -1
  20. package/dist/interactions.js +10 -52
  21. package/dist/interactions.js.map +1 -1
  22. package/dist/routes/public/agent.d.ts +4 -0
  23. package/dist/routes/public/agent.d.ts.map +1 -1
  24. package/dist/routes/public/agent.js +21 -0
  25. package/dist/routes/public/agent.js.map +1 -1
  26. package/dist/services/core-services.d.ts.map +1 -1
  27. package/dist/services/core-services.js +4 -0
  28. package/dist/services/core-services.js.map +1 -1
  29. package/package.json +9 -9
  30. package/src/__tests__/agent-config-routes.test.ts +0 -254
  31. package/src/__tests__/agent-history-routes.test.ts +0 -72
  32. package/src/__tests__/agent-routes.test.ts +0 -68
  33. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  34. package/src/__tests__/agent-settings-store.test.ts +0 -323
  35. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  36. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  37. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  38. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  39. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  40. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  41. package/src/__tests__/config-request-store.test.ts +0 -127
  42. package/src/__tests__/connection-routes.test.ts +0 -144
  43. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  44. package/src/__tests__/docker-deployment.test.ts +0 -1211
  45. package/src/__tests__/embedded-deployment.test.ts +0 -342
  46. package/src/__tests__/grant-store.test.ts +0 -148
  47. package/src/__tests__/http-proxy.test.ts +0 -281
  48. package/src/__tests__/instruction-service.test.ts +0 -37
  49. package/src/__tests__/link-buttons.test.ts +0 -112
  50. package/src/__tests__/lobu.test.ts +0 -32
  51. package/src/__tests__/mcp-config-service.test.ts +0 -347
  52. package/src/__tests__/mcp-proxy.test.ts +0 -694
  53. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  54. package/src/__tests__/model-selection.test.ts +0 -172
  55. package/src/__tests__/oauth-templates.test.ts +0 -39
  56. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  57. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  58. package/src/__tests__/provider-inheritance.test.ts +0 -212
  59. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  60. package/src/__tests__/routes/interactions.test.ts +0 -121
  61. package/src/__tests__/secret-proxy.test.ts +0 -85
  62. package/src/__tests__/session-manager.test.ts +0 -572
  63. package/src/__tests__/setup.ts +0 -133
  64. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  65. package/src/__tests__/slack-routes.test.ts +0 -161
  66. package/src/__tests__/system-config-resolver.test.ts +0 -75
  67. package/src/__tests__/system-message-limiter.test.ts +0 -89
  68. package/src/__tests__/system-skills-service.test.ts +0 -362
  69. package/src/__tests__/transcription-service.test.ts +0 -222
  70. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  71. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  72. package/src/__tests__/worker-job-router.test.ts +0 -722
  73. package/src/api/index.ts +0 -1
  74. package/src/api/platform.ts +0 -292
  75. package/src/api/response-renderer.ts +0 -157
  76. package/src/auth/agent-metadata-store.ts +0 -168
  77. package/src/auth/api-auth-middleware.ts +0 -69
  78. package/src/auth/api-key-provider-module.ts +0 -213
  79. package/src/auth/base-provider-module.ts +0 -201
  80. package/src/auth/bedrock/provider-module.ts +0 -110
  81. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  82. package/src/auth/chatgpt/device-code-client.ts +0 -218
  83. package/src/auth/chatgpt/index.ts +0 -1
  84. package/src/auth/claude/oauth-module.ts +0 -280
  85. package/src/auth/cli/token-service.ts +0 -249
  86. package/src/auth/external/client.ts +0 -560
  87. package/src/auth/external/device-code-client.ts +0 -235
  88. package/src/auth/mcp/config-service.ts +0 -420
  89. package/src/auth/mcp/proxy.ts +0 -1086
  90. package/src/auth/mcp/string-substitution.ts +0 -17
  91. package/src/auth/mcp/tool-cache.ts +0 -90
  92. package/src/auth/oauth/base-client.ts +0 -267
  93. package/src/auth/oauth/client.ts +0 -153
  94. package/src/auth/oauth/credentials.ts +0 -7
  95. package/src/auth/oauth/providers.ts +0 -69
  96. package/src/auth/oauth/state-store.ts +0 -150
  97. package/src/auth/oauth-templates.ts +0 -179
  98. package/src/auth/provider-catalog.ts +0 -220
  99. package/src/auth/provider-model-options.ts +0 -41
  100. package/src/auth/settings/agent-settings-store.ts +0 -565
  101. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  102. package/src/auth/settings/index.ts +0 -12
  103. package/src/auth/settings/model-preference-store.ts +0 -52
  104. package/src/auth/settings/model-selection.ts +0 -135
  105. package/src/auth/settings/resolved-settings-view.ts +0 -298
  106. package/src/auth/settings/template-utils.ts +0 -44
  107. package/src/auth/settings/token-service.ts +0 -88
  108. package/src/auth/system-env-store.ts +0 -98
  109. package/src/auth/user-agents-store.ts +0 -68
  110. package/src/channels/binding-service.ts +0 -214
  111. package/src/channels/index.ts +0 -4
  112. package/src/cli/gateway.ts +0 -1312
  113. package/src/cli/index.ts +0 -74
  114. package/src/commands/built-in-commands.ts +0 -80
  115. package/src/commands/command-dispatcher.ts +0 -94
  116. package/src/commands/command-reply-adapters.ts +0 -27
  117. package/src/config/file-loader.ts +0 -618
  118. package/src/config/index.ts +0 -588
  119. package/src/config/network-allowlist.ts +0 -71
  120. package/src/connections/chat-instance-manager.ts +0 -1284
  121. package/src/connections/chat-response-bridge.ts +0 -618
  122. package/src/connections/index.ts +0 -7
  123. package/src/connections/interaction-bridge.ts +0 -831
  124. package/src/connections/message-handler-bridge.ts +0 -440
  125. package/src/connections/platform-auth-methods.ts +0 -15
  126. package/src/connections/types.ts +0 -84
  127. package/src/gateway/connection-manager.ts +0 -291
  128. package/src/gateway/index.ts +0 -698
  129. package/src/gateway/job-router.ts +0 -201
  130. package/src/gateway-main.ts +0 -200
  131. package/src/index.ts +0 -41
  132. package/src/infrastructure/queue/index.ts +0 -12
  133. package/src/infrastructure/queue/queue-producer.ts +0 -148
  134. package/src/infrastructure/queue/redis-queue.ts +0 -361
  135. package/src/infrastructure/queue/types.ts +0 -133
  136. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  137. package/src/interactions/config-request-store.ts +0 -198
  138. package/src/interactions.ts +0 -363
  139. package/src/lobu.ts +0 -311
  140. package/src/metrics/prometheus.ts +0 -159
  141. package/src/modules/module-system.ts +0 -179
  142. package/src/orchestration/base-deployment-manager.ts +0 -900
  143. package/src/orchestration/deployment-utils.ts +0 -98
  144. package/src/orchestration/impl/docker-deployment.ts +0 -620
  145. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  146. package/src/orchestration/impl/index.ts +0 -8
  147. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  148. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  149. package/src/orchestration/impl/k8s/index.ts +0 -1
  150. package/src/orchestration/index.ts +0 -333
  151. package/src/orchestration/message-consumer.ts +0 -584
  152. package/src/orchestration/scheduled-wakeup.ts +0 -704
  153. package/src/permissions/approval-policy.ts +0 -36
  154. package/src/permissions/grant-store.ts +0 -219
  155. package/src/platform/file-handler.ts +0 -66
  156. package/src/platform/link-buttons.ts +0 -57
  157. package/src/platform/renderer-utils.ts +0 -44
  158. package/src/platform/response-renderer.ts +0 -84
  159. package/src/platform/unified-thread-consumer.ts +0 -194
  160. package/src/platform.ts +0 -318
  161. package/src/proxy/http-proxy.ts +0 -752
  162. package/src/proxy/proxy-manager.ts +0 -81
  163. package/src/proxy/secret-proxy.ts +0 -402
  164. package/src/proxy/token-refresh-job.ts +0 -143
  165. package/src/routes/internal/audio.ts +0 -141
  166. package/src/routes/internal/device-auth.ts +0 -652
  167. package/src/routes/internal/files.ts +0 -226
  168. package/src/routes/internal/history.ts +0 -69
  169. package/src/routes/internal/images.ts +0 -127
  170. package/src/routes/internal/interactions.ts +0 -84
  171. package/src/routes/internal/middleware.ts +0 -23
  172. package/src/routes/internal/schedule.ts +0 -226
  173. package/src/routes/internal/types.ts +0 -22
  174. package/src/routes/openapi-auto.ts +0 -239
  175. package/src/routes/public/agent-access.ts +0 -23
  176. package/src/routes/public/agent-config.ts +0 -675
  177. package/src/routes/public/agent-history.ts +0 -422
  178. package/src/routes/public/agent-schedules.ts +0 -296
  179. package/src/routes/public/agent.ts +0 -1086
  180. package/src/routes/public/agents.ts +0 -373
  181. package/src/routes/public/channels.ts +0 -191
  182. package/src/routes/public/cli-auth.ts +0 -896
  183. package/src/routes/public/connections.ts +0 -574
  184. package/src/routes/public/landing.ts +0 -16
  185. package/src/routes/public/oauth.ts +0 -147
  186. package/src/routes/public/settings-auth.ts +0 -104
  187. package/src/routes/public/slack.ts +0 -173
  188. package/src/routes/shared/agent-ownership.ts +0 -101
  189. package/src/routes/shared/token-verifier.ts +0 -34
  190. package/src/services/bedrock-model-catalog.ts +0 -217
  191. package/src/services/bedrock-openai-service.ts +0 -658
  192. package/src/services/core-services.ts +0 -1072
  193. package/src/services/image-generation-service.ts +0 -257
  194. package/src/services/instruction-service.ts +0 -318
  195. package/src/services/mcp-registry.ts +0 -94
  196. package/src/services/platform-helpers.ts +0 -287
  197. package/src/services/session-manager.ts +0 -262
  198. package/src/services/settings-resolver.ts +0 -74
  199. package/src/services/system-config-resolver.ts +0 -89
  200. package/src/services/system-skills-service.ts +0 -229
  201. package/src/services/transcription-service.ts +0 -684
  202. package/src/session.ts +0 -110
  203. package/src/spaces/index.ts +0 -1
  204. package/src/spaces/space-resolver.ts +0 -17
  205. package/src/stores/in-memory-agent-store.ts +0 -403
  206. package/src/stores/redis-agent-store.ts +0 -279
  207. package/src/utils/public-url.ts +0 -44
  208. package/src/utils/rate-limiter.ts +0 -94
  209. package/tsconfig.json +0 -33
  210. 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
- }