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