@axiom-lattice/gateway 2.1.34 → 2.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/gateway",
3
- "version": "2.1.34",
3
+ "version": "2.1.36",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -36,9 +36,9 @@
36
36
  "pino-roll": "^3.1.0",
37
37
  "redis": "^5.0.1",
38
38
  "uuid": "^9.0.1",
39
- "@axiom-lattice/core": "2.1.29",
40
- "@axiom-lattice/protocols": "2.1.16",
41
- "@axiom-lattice/queue-redis": "1.0.15"
39
+ "@axiom-lattice/core": "2.1.30",
40
+ "@axiom-lattice/protocols": "2.1.17",
41
+ "@axiom-lattice/queue-redis": "1.0.16"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/jest": "^29.5.14",
@@ -0,0 +1,691 @@
1
+ /**
2
+ * MCP Server Config Controller
3
+ * Handles MCP server configuration CRUD operations with automatic tool registration
4
+ */
5
+
6
+ import { FastifyRequest, FastifyReply } from "fastify";
7
+ import {
8
+ getStoreLattice,
9
+ mcpManager,
10
+ toolLatticeManager,
11
+ } from "@axiom-lattice/core";
12
+ import type {
13
+ McpServerConfigStore,
14
+ McpServerConfigEntry,
15
+ CreateMcpServerConfigRequest,
16
+ UpdateMcpServerConfigRequest,
17
+ McpServerConfig,
18
+ McpTool,
19
+ } from "@axiom-lattice/protocols";
20
+ import { randomUUID } from "crypto";
21
+
22
+ /**
23
+ * Get tenant ID from request headers
24
+ */
25
+ function getTenantId(request: FastifyRequest): string {
26
+ return (request.headers["x-tenant-id"] as string) || "default";
27
+ }
28
+
29
+ /**
30
+ * MCP server config list response
31
+ */
32
+ interface McpServerConfigListResponse {
33
+ success: boolean;
34
+ message: string;
35
+ data: {
36
+ records: McpServerConfigEntry[];
37
+ total: number;
38
+ };
39
+ }
40
+
41
+ /**
42
+ * MCP server config response
43
+ */
44
+ interface McpServerConfigResponse {
45
+ success: boolean;
46
+ message: string;
47
+ data?: McpServerConfigEntry;
48
+ }
49
+
50
+ /**
51
+ * Test connection response
52
+ */
53
+ interface TestConnectionResponse {
54
+ success: boolean;
55
+ message: string;
56
+ data?: {
57
+ connected: boolean;
58
+ latency?: number;
59
+ error?: string;
60
+ };
61
+ }
62
+
63
+ /**
64
+ * List tools response
65
+ */
66
+ interface ListToolsResponse {
67
+ success: boolean;
68
+ message: string;
69
+ data?: {
70
+ tools: McpTool[];
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Get all MCP server configs for a tenant
76
+ */
77
+ export async function getMcpServerConfigList(
78
+ request: FastifyRequest,
79
+ reply: FastifyReply
80
+ ): Promise<McpServerConfigListResponse> {
81
+ const tenantId = getTenantId(request);
82
+
83
+ try {
84
+ const storeLattice = getStoreLattice("default", "mcp");
85
+ const store: McpServerConfigStore = storeLattice.store;
86
+
87
+ const configs = await store.getAllConfigs(tenantId);
88
+
89
+ return {
90
+ success: true,
91
+ message: "MCP server configurations retrieved successfully",
92
+ data: {
93
+ records: configs,
94
+ total: configs.length,
95
+ },
96
+ };
97
+ } catch (error) {
98
+ console.error("Failed to get MCP server configs:", error);
99
+ return {
100
+ success: false,
101
+ message: "Failed to retrieve MCP server configurations",
102
+ data: {
103
+ records: [],
104
+ total: 0,
105
+ },
106
+ };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get MCP server config by key
112
+ */
113
+ export async function getMcpServerConfig(
114
+ request: FastifyRequest,
115
+ reply: FastifyReply
116
+ ): Promise<McpServerConfigResponse> {
117
+ const tenantId = getTenantId(request);
118
+ const { key } = request.params as { key: string };
119
+
120
+ try {
121
+ const storeLattice = getStoreLattice("default", "mcp");
122
+ const store: McpServerConfigStore = storeLattice.store;
123
+
124
+ const config = await store.getConfigByKey(tenantId, key);
125
+
126
+ if (!config) {
127
+ return {
128
+ success: false,
129
+ message: "MCP server configuration not found",
130
+ };
131
+ }
132
+
133
+ return {
134
+ success: true,
135
+ message: "MCP server configuration retrieved successfully",
136
+ data: config,
137
+ };
138
+ } catch (error) {
139
+ console.error("Failed to get MCP server config:", error);
140
+ return {
141
+ success: false,
142
+ message: "Failed to retrieve MCP server configuration",
143
+ };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Create new MCP server config
149
+ */
150
+ export async function createMcpServerConfig(
151
+ request: FastifyRequest,
152
+ reply: FastifyReply
153
+ ): Promise<McpServerConfigResponse> {
154
+ const tenantId = getTenantId(request);
155
+ const body = request.body as CreateMcpServerConfigRequest & {
156
+ id?: string;
157
+ };
158
+
159
+ try {
160
+ const storeLattice = getStoreLattice("default", "mcp");
161
+ const store: McpServerConfigStore = storeLattice.store;
162
+
163
+ // Check if key already exists
164
+ const existing = await store.getConfigByKey(tenantId, body.key);
165
+ if (existing) {
166
+ reply.code(409);
167
+ return {
168
+ success: false,
169
+ message: "MCP server configuration with this key already exists",
170
+ };
171
+ }
172
+
173
+ const id = body.id || randomUUID();
174
+ const config = await store.createConfig(tenantId, id, body);
175
+
176
+ // Auto-connect and register tools
177
+ try {
178
+ await connectAndRegisterTools(config);
179
+ // Update status to connected
180
+ await store.updateConfig(tenantId, id, { status: "connected" });
181
+ config.status = "connected";
182
+ } catch (error) {
183
+ console.warn("Failed to auto-connect MCP server:", error);
184
+ // Update status to error but don't fail the creation
185
+ await store.updateConfig(tenantId, id, { status: "error" });
186
+ config.status = "error";
187
+ }
188
+
189
+ reply.code(201);
190
+ return {
191
+ success: true,
192
+ message: "MCP server configuration created successfully",
193
+ data: config,
194
+ };
195
+ } catch (error) {
196
+ console.error("Failed to create MCP server config:", error);
197
+ return {
198
+ success: false,
199
+ message: "Failed to create MCP server configuration",
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Update MCP server config
206
+ */
207
+ export async function updateMcpServerConfig(
208
+ request: FastifyRequest,
209
+ reply: FastifyReply
210
+ ): Promise<McpServerConfigResponse> {
211
+ const tenantId = getTenantId(request);
212
+ const { key } = request.params as { key: string };
213
+ const updates = request.body as Partial<UpdateMcpServerConfigRequest>;
214
+
215
+ try {
216
+ const storeLattice = getStoreLattice("default", "mcp");
217
+ const store: McpServerConfigStore = storeLattice.store;
218
+
219
+ const existing = await store.getConfigByKey(tenantId, key);
220
+ if (!existing) {
221
+ reply.code(404);
222
+ return {
223
+ success: false,
224
+ message: "MCP server configuration not found",
225
+ };
226
+ }
227
+
228
+ // If config is being updated, we need to reconnect
229
+ const shouldReconnect = updates.config !== undefined;
230
+
231
+ const updated = await store.updateConfig(tenantId, existing.id, updates);
232
+
233
+ if (!updated) {
234
+ return {
235
+ success: false,
236
+ message: "Failed to update MCP server configuration",
237
+ };
238
+ }
239
+
240
+ // Reconnect and re-register tools if config changed
241
+ if (shouldReconnect) {
242
+ try {
243
+ // Disconnect existing connection if any
244
+ if (mcpManager.hasServer(key)) {
245
+ await mcpManager.removeServer(key);
246
+ }
247
+ // Reconnect with new config
248
+ await connectAndRegisterTools(updated);
249
+ await store.updateConfig(tenantId, existing.id, { status: "connected" });
250
+ updated.status = "connected";
251
+ } catch (error) {
252
+ console.warn("Failed to reconnect MCP server:", error);
253
+ await store.updateConfig(tenantId, existing.id, { status: "error" });
254
+ updated.status = "error";
255
+ }
256
+ }
257
+
258
+ return {
259
+ success: true,
260
+ message: "MCP server configuration updated successfully",
261
+ data: updated,
262
+ };
263
+ } catch (error) {
264
+ console.error("Failed to update MCP server config:", error);
265
+ return {
266
+ success: false,
267
+ message: "Failed to update MCP server configuration",
268
+ };
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Delete MCP server config by key or id
274
+ */
275
+ export async function deleteMcpServerConfig(
276
+ request: FastifyRequest,
277
+ reply: FastifyReply
278
+ ): Promise<McpServerConfigResponse> {
279
+ const tenantId = getTenantId(request);
280
+ const { keyOrId } = request.params as { keyOrId: string };
281
+
282
+ try {
283
+ const storeLattice = getStoreLattice("default", "mcp");
284
+ const store: McpServerConfigStore = storeLattice.store;
285
+
286
+ // Try to find by key first, then by id
287
+ let config = await store.getConfigByKey(tenantId, keyOrId);
288
+ let configKey = keyOrId;
289
+
290
+ if (!config) {
291
+ // Try to find by id
292
+ config = await store.getConfigById(tenantId, keyOrId);
293
+ if (config) {
294
+ configKey = config.key;
295
+ }
296
+ }
297
+
298
+ if (!config) {
299
+ reply.code(404);
300
+ return {
301
+ success: false,
302
+ message: "MCP server configuration not found",
303
+ };
304
+ }
305
+
306
+ // Disconnect and remove from MCP manager
307
+ try {
308
+ if (mcpManager.hasServer(configKey)) {
309
+ await mcpManager.removeServer(configKey);
310
+ }
311
+ } catch (error) {
312
+ console.warn("Failed to remove from MCP manager:", error);
313
+ }
314
+
315
+ const deleted = await store.deleteConfig(tenantId, config.id);
316
+
317
+ if (!deleted) {
318
+ return {
319
+ success: false,
320
+ message: "Failed to delete MCP server configuration",
321
+ };
322
+ }
323
+
324
+ return {
325
+ success: true,
326
+ message: "MCP server configuration deleted successfully",
327
+ };
328
+ } catch (error) {
329
+ console.error("Failed to delete MCP server config:", error);
330
+ return {
331
+ success: false,
332
+ message: "Failed to delete MCP server configuration",
333
+ };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Test MCP server connection
339
+ */
340
+ export async function testMcpServerConnection(
341
+ request: FastifyRequest,
342
+ reply: FastifyReply
343
+ ): Promise<TestConnectionResponse> {
344
+ const tenantId = getTenantId(request);
345
+ const { key } = request.params as { key: string };
346
+
347
+ try {
348
+ const storeLattice = getStoreLattice("default", "mcp");
349
+ const store: McpServerConfigStore = storeLattice.store;
350
+
351
+ const config = await store.getConfigByKey(tenantId, key);
352
+ if (!config) {
353
+ reply.code(404);
354
+ return {
355
+ success: false,
356
+ message: "MCP server configuration not found",
357
+ };
358
+ }
359
+
360
+ const startTime = Date.now();
361
+
362
+ // Test connection by trying to list tools
363
+ try {
364
+ // Create temporary connection
365
+ const testKey = `__test_${key}_${Date.now()}`;
366
+ const connection = convertToConnection(config.config);
367
+ mcpManager.addServer(testKey, connection);
368
+
369
+ await mcpManager.connect();
370
+ const tools = await mcpManager.getAllTools();
371
+ const latency = Date.now() - startTime;
372
+
373
+ // Cleanup
374
+ await mcpManager.removeServer(testKey);
375
+
376
+ return {
377
+ success: true,
378
+ message: "Connection test successful",
379
+ data: {
380
+ connected: true,
381
+ latency,
382
+ },
383
+ };
384
+ } catch (error) {
385
+ return {
386
+ success: true,
387
+ message: "Connection test failed",
388
+ data: {
389
+ connected: false,
390
+ error: error instanceof Error ? error.message : "Unknown error",
391
+ },
392
+ };
393
+ }
394
+ } catch (error) {
395
+ console.error("Failed to test MCP server connection:", error);
396
+ return {
397
+ success: false,
398
+ message: "Failed to test MCP server connection",
399
+ data: {
400
+ connected: false,
401
+ error: error instanceof Error ? error.message : "Unknown error",
402
+ },
403
+ };
404
+ }
405
+ }
406
+
407
+ /**
408
+ * List available tools from an MCP server
409
+ */
410
+ export async function listMcpServerTools(
411
+ request: FastifyRequest,
412
+ reply: FastifyReply
413
+ ): Promise<ListToolsResponse> {
414
+ const tenantId = getTenantId(request);
415
+ const { key } = request.params as { key: string };
416
+
417
+ try {
418
+ const storeLattice = getStoreLattice("default", "mcp");
419
+ const store: McpServerConfigStore = storeLattice.store;
420
+
421
+ const config = await store.getConfigByKey(tenantId, key);
422
+ if (!config) {
423
+ reply.code(404);
424
+ return {
425
+ success: false,
426
+ message: "MCP server configuration not found",
427
+ };
428
+ }
429
+
430
+ // Ensure server is connected
431
+ if (!mcpManager.hasServer(key)) {
432
+ await connectAndRegisterTools(config);
433
+ }
434
+
435
+ const tools = await mcpManager.getAllTools();
436
+
437
+ return {
438
+ success: true,
439
+ message: "Tools retrieved successfully",
440
+ data: {
441
+ tools,
442
+ },
443
+ };
444
+ } catch (error) {
445
+ console.error("Failed to list MCP tools:", error);
446
+ return {
447
+ success: false,
448
+ message: "Failed to retrieve tools",
449
+ };
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Connect to MCP server and register tools
455
+ */
456
+ export async function connectMcpServer(
457
+ request: FastifyRequest,
458
+ reply: FastifyReply
459
+ ): Promise<McpServerConfigResponse> {
460
+ const tenantId = getTenantId(request);
461
+ const { key } = request.params as { key: string };
462
+
463
+ try {
464
+ const storeLattice = getStoreLattice("default", "mcp");
465
+ const store: McpServerConfigStore = storeLattice.store;
466
+
467
+ const config = await store.getConfigByKey(tenantId, key);
468
+ if (!config) {
469
+ reply.code(404);
470
+ return {
471
+ success: false,
472
+ message: "MCP server configuration not found",
473
+ };
474
+ }
475
+
476
+ await connectAndRegisterTools(config);
477
+
478
+ // Update status
479
+ const updated = await store.updateConfig(tenantId, config.id, {
480
+ status: "connected",
481
+ });
482
+
483
+ return {
484
+ success: true,
485
+ message: "MCP server connected successfully",
486
+ data: updated || config,
487
+ };
488
+ } catch (error) {
489
+ console.error("Failed to connect MCP server:", error);
490
+
491
+ // Update status to error
492
+ const storeLattice = getStoreLattice("default", "mcp");
493
+ const store: McpServerConfigStore = storeLattice.store;
494
+ const config = await store.getConfigByKey(tenantId, key);
495
+ if (config) {
496
+ await store.updateConfig(tenantId, config.id, { status: "error" });
497
+ }
498
+
499
+ return {
500
+ success: false,
501
+ message: `Failed to connect MCP server: ${
502
+ error instanceof Error ? error.message : "Unknown error"
503
+ }`,
504
+ };
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Disconnect from MCP server
510
+ */
511
+ export async function disconnectMcpServer(
512
+ request: FastifyRequest,
513
+ reply: FastifyReply
514
+ ): Promise<McpServerConfigResponse> {
515
+ const tenantId = getTenantId(request);
516
+ const { key } = request.params as { key: string };
517
+
518
+ try {
519
+ const storeLattice = getStoreLattice("default", "mcp");
520
+ const store: McpServerConfigStore = storeLattice.store;
521
+
522
+ const config = await store.getConfigByKey(tenantId, key);
523
+ if (!config) {
524
+ reply.code(404);
525
+ return {
526
+ success: false,
527
+ message: "MCP server configuration not found",
528
+ };
529
+ }
530
+
531
+ // Disconnect from MCP manager
532
+ if (mcpManager.hasServer(key)) {
533
+ await mcpManager.removeServer(key);
534
+ }
535
+
536
+ // Update status
537
+ const updated = await store.updateConfig(tenantId, config.id, {
538
+ status: "disconnected",
539
+ });
540
+
541
+ return {
542
+ success: true,
543
+ message: "MCP server disconnected successfully",
544
+ data: updated || config,
545
+ };
546
+ } catch (error) {
547
+ console.error("Failed to disconnect MCP server:", error);
548
+ return {
549
+ success: false,
550
+ message: "Failed to disconnect MCP server",
551
+ };
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Test MCP server connection and list tools without saving config
557
+ * Used for multi-step configuration flow
558
+ */
559
+ export async function testMcpServerTools(
560
+ request: FastifyRequest,
561
+ reply: FastifyReply
562
+ ): Promise<ListToolsResponse> {
563
+ const body = request.body as {
564
+ config: McpServerConfig;
565
+ };
566
+
567
+ try {
568
+ if (!body.config) {
569
+ reply.code(400);
570
+ return {
571
+ success: false,
572
+ message: "config is required",
573
+ };
574
+ }
575
+
576
+ const testKey = `__test_${Date.now()}`;
577
+
578
+ // Create temporary connection
579
+ const connection = convertToConnection(body.config);
580
+ mcpManager.addServer(testKey, connection);
581
+
582
+ await mcpManager.connect();
583
+ const tools = await mcpManager.getAllTools();
584
+
585
+ // Cleanup
586
+ await mcpManager.removeServer(testKey);
587
+
588
+ return {
589
+ success: true,
590
+ message: "Tools retrieved successfully",
591
+ data: {
592
+ tools,
593
+ },
594
+ };
595
+ } catch (error) {
596
+ console.error("Failed to test MCP server tools:", error);
597
+ return {
598
+ success: false,
599
+ message: `Failed to retrieve tools: ${
600
+ error instanceof Error ? error.message : String(error)
601
+ }`,
602
+ };
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Convert McpServerConfig to Connection format compatible with @langchain/mcp-adapters
608
+ */
609
+ function convertToConnection(config: McpServerConfig): any {
610
+ const baseConfig = {
611
+ env: config.env,
612
+ };
613
+
614
+ if (config.transport === "stdio") {
615
+ return {
616
+ ...baseConfig,
617
+ transport: "stdio" as const,
618
+ command: config.command!,
619
+ args: config.args || [],
620
+ };
621
+ } else {
622
+ // For HTTP/SSE transports
623
+ return {
624
+ ...baseConfig,
625
+ transport: config.transport === "streamable_http" ? "http" : config.transport,
626
+ url: config.url!,
627
+ };
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Helper function to connect to MCP server and register selected tools
633
+ */
634
+ async function connectAndRegisterTools(
635
+ config: McpServerConfigEntry
636
+ ): Promise<void> {
637
+ // Add server to MCP manager
638
+ const connection = convertToConnection(config.config);
639
+ mcpManager.addServer(config.key, connection);
640
+
641
+ // Connect to server
642
+ await mcpManager.connect();
643
+
644
+ // Get all available tools
645
+ const allTools = await mcpManager.getAllTools();
646
+
647
+ // Filter to only selected tools
648
+ const selectedTools = allTools.filter((tool) =>
649
+ config.selectedTools.includes(tool.name)
650
+ );
651
+
652
+ // Register selected tools to ToolLattice
653
+ for (const tool of selectedTools) {
654
+ toolLatticeManager.registerExistingTool(tool.name, tool);
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Register MCP server config routes
660
+ */
661
+ export function registerMcpServerConfigRoutes(app: any): void {
662
+ // Get all configs
663
+ app.get("/api/mcp-servers", getMcpServerConfigList);
664
+
665
+ // Get config by key
666
+ app.get("/api/mcp-servers/:key", getMcpServerConfig);
667
+
668
+ // Create config
669
+ app.post("/api/mcp-servers", createMcpServerConfig);
670
+
671
+ // Update config
672
+ app.put("/api/mcp-servers/:key", updateMcpServerConfig);
673
+
674
+ // Delete config by key or id
675
+ app.delete("/api/mcp-servers/:keyOrId", deleteMcpServerConfig);
676
+
677
+ // Test connection
678
+ app.post("/api/mcp-servers/:key/test", testMcpServerConnection);
679
+
680
+ // List available tools
681
+ app.get("/api/mcp-servers/:key/tools", listMcpServerTools);
682
+
683
+ // Connect to server
684
+ app.post("/api/mcp-servers/:key/connect", connectMcpServer);
685
+
686
+ // Disconnect from server
687
+ app.post("/api/mcp-servers/:key/disconnect", disconnectMcpServer);
688
+
689
+ // Test tools without saving config (for multi-step config flow)
690
+ app.post("/api/mcp-servers/test-tools", testMcpServerTools);
691
+ }