@tambo-ai/react 0.63.0 → 0.64.0
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/dist/hooks/use-tambo-voice.js.map +1 -1
- package/dist/mcp/__tests__/mcp-hooks.test.js +479 -16
- package/dist/mcp/__tests__/mcp-hooks.test.js.map +1 -1
- package/dist/mcp/__tests__/tambo-mcp-provider.test.js +156 -0
- package/dist/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/mcp-hooks.d.ts +93 -3
- package/dist/mcp/mcp-hooks.d.ts.map +1 -1
- package/dist/mcp/mcp-hooks.js +111 -4
- package/dist/mcp/mcp-hooks.js.map +1 -1
- package/dist/mcp/tambo-mcp-provider.d.ts +16 -0
- package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/dist/mcp/tambo-mcp-provider.js +71 -7
- package/dist/mcp/tambo-mcp-provider.js.map +1 -1
- package/dist/providers/tambo-context-attachment-provider.js +2 -2
- package/dist/providers/tambo-context-attachment-provider.js.map +1 -1
- package/esm/hooks/use-tambo-voice.js.map +1 -1
- package/esm/mcp/__tests__/mcp-hooks.test.js +480 -17
- package/esm/mcp/__tests__/mcp-hooks.test.js.map +1 -1
- package/esm/mcp/__tests__/tambo-mcp-provider.test.js +156 -0
- package/esm/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
- package/esm/mcp/index.d.ts +2 -1
- package/esm/mcp/index.d.ts.map +1 -1
- package/esm/mcp/index.js +1 -1
- package/esm/mcp/index.js.map +1 -1
- package/esm/mcp/mcp-hooks.d.ts +93 -3
- package/esm/mcp/mcp-hooks.d.ts.map +1 -1
- package/esm/mcp/mcp-hooks.js +109 -4
- package/esm/mcp/mcp-hooks.js.map +1 -1
- package/esm/mcp/tambo-mcp-provider.d.ts +16 -0
- package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/esm/mcp/tambo-mcp-provider.js +71 -7
- package/esm/mcp/tambo-mcp-provider.js.map +1 -1
- package/esm/providers/tambo-context-attachment-provider.js +2 -2
- package/esm/providers/tambo-context-attachment-provider.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { TamboMcpTokenProvider } from "../../providers/tambo-mcp-token-provider"
|
|
|
6
6
|
import { TamboRegistryProvider } from "../../providers/tambo-registry-provider";
|
|
7
7
|
import { MCPTransport } from "../mcp-client";
|
|
8
8
|
import { TamboMcpProvider, useTamboMcpServers } from "../tambo-mcp-provider";
|
|
9
|
-
import { useTamboMcpPromptList } from "../mcp-hooks";
|
|
9
|
+
import { useTamboMcpPromptList, useTamboMcpResourceList, useTamboMcpResource, } from "../mcp-hooks";
|
|
10
10
|
// Mock the MCP client to avoid ES module issues
|
|
11
11
|
let createImpl = jest.fn();
|
|
12
12
|
jest.mock("../mcp-client", () => ({
|
|
@@ -102,16 +102,16 @@ describe("useTamboMcpPromptList - individual server caching", () => {
|
|
|
102
102
|
await waitFor(() => {
|
|
103
103
|
expect(capturedPrompts.length).toBe(4);
|
|
104
104
|
});
|
|
105
|
-
// Verify all prompts are present
|
|
105
|
+
// Verify all prompts are present (with prefixes since we have 2 servers)
|
|
106
106
|
const promptNames = capturedPrompts.map((p) => p.prompt.name);
|
|
107
|
-
expect(promptNames).toContain("prompt-a1");
|
|
108
|
-
expect(promptNames).toContain("prompt-a2");
|
|
109
|
-
expect(promptNames).toContain("prompt-b1");
|
|
110
|
-
expect(promptNames).toContain("prompt-b2");
|
|
107
|
+
expect(promptNames).toContain("server-a:prompt-a1");
|
|
108
|
+
expect(promptNames).toContain("server-a:prompt-a2");
|
|
109
|
+
expect(promptNames).toContain("server-b:prompt-b1");
|
|
110
|
+
expect(promptNames).toContain("server-b:prompt-b2");
|
|
111
111
|
// Verify each prompt has the correct server info
|
|
112
|
-
const promptA1 = capturedPrompts.find((p) => p.prompt.name === "prompt-a1");
|
|
112
|
+
const promptA1 = capturedPrompts.find((p) => p.prompt.name === "server-a:prompt-a1");
|
|
113
113
|
expect(promptA1?.server.url).toBe("https://server-a.example");
|
|
114
|
-
const promptB1 = capturedPrompts.find((p) => p.prompt.name === "prompt-b1");
|
|
114
|
+
const promptB1 = capturedPrompts.find((p) => p.prompt.name === "server-b:prompt-b1");
|
|
115
115
|
expect(promptB1?.server.url).toBe("https://server-b.example");
|
|
116
116
|
});
|
|
117
117
|
it("should remove prompts when a server is removed", async () => {
|
|
@@ -188,10 +188,10 @@ describe("useTamboMcpPromptList - individual server caching", () => {
|
|
|
188
188
|
expect(capturedPrompts.length).toBe(4);
|
|
189
189
|
});
|
|
190
190
|
const initialPromptNames = capturedPrompts.map((p) => p.prompt.name);
|
|
191
|
-
expect(initialPromptNames).toContain("prompt-a1");
|
|
192
|
-
expect(initialPromptNames).toContain("prompt-a2");
|
|
193
|
-
expect(initialPromptNames).toContain("prompt-b1");
|
|
194
|
-
expect(initialPromptNames).toContain("prompt-b2");
|
|
191
|
+
expect(initialPromptNames).toContain("server-a:prompt-a1");
|
|
192
|
+
expect(initialPromptNames).toContain("server-a:prompt-a2");
|
|
193
|
+
expect(initialPromptNames).toContain("server-b:prompt-b1");
|
|
194
|
+
expect(initialPromptNames).toContain("server-b:prompt-b2");
|
|
195
195
|
// Remove server B
|
|
196
196
|
rerender(React.createElement(TamboClientContext.Provider, { value: {
|
|
197
197
|
client: { baseURL: "https://api.tambo.co" },
|
|
@@ -208,6 +208,7 @@ describe("useTamboMcpPromptList - individual server caching", () => {
|
|
|
208
208
|
] },
|
|
209
209
|
React.createElement(Capture, null))))));
|
|
210
210
|
// Wait for prompts to be updated (server B prompts should disappear)
|
|
211
|
+
// When only 1 server remains, prompts should NOT be prefixed
|
|
211
212
|
await waitFor(() => {
|
|
212
213
|
expect(capturedPrompts.length).toBe(2);
|
|
213
214
|
});
|
|
@@ -216,6 +217,7 @@ describe("useTamboMcpPromptList - individual server caching", () => {
|
|
|
216
217
|
expect(updatedPromptNames).toContain("prompt-a2");
|
|
217
218
|
expect(updatedPromptNames).not.toContain("prompt-b1");
|
|
218
219
|
expect(updatedPromptNames).not.toContain("prompt-b2");
|
|
220
|
+
expect(updatedPromptNames).not.toContain("server-a:prompt-a1"); // No prefix when only 1 server
|
|
219
221
|
// Verify server B's client was closed
|
|
220
222
|
expect(clientB.close).toHaveBeenCalled();
|
|
221
223
|
});
|
|
@@ -370,10 +372,10 @@ describe("useTamboMcpPromptList - individual server caching", () => {
|
|
|
370
372
|
expect(capturedPrompts.length).toBe(1);
|
|
371
373
|
expect(mcpServersCount).toBe(2); // Both servers should be in the list
|
|
372
374
|
});
|
|
373
|
-
// Verify only server A's prompts are present
|
|
375
|
+
// Verify only server A's prompts are present (with prefix since 2 servers configured)
|
|
374
376
|
const promptNames = capturedPrompts.map((p) => p.prompt.name);
|
|
375
|
-
expect(promptNames).toContain("prompt-a");
|
|
376
|
-
expect(promptNames).not.toContain("prompt-b");
|
|
377
|
+
expect(promptNames).toContain("server-a:prompt-a");
|
|
378
|
+
expect(promptNames).not.toContain("server-b:prompt-b");
|
|
377
379
|
});
|
|
378
380
|
it("should add prompts when a new server is added", async () => {
|
|
379
381
|
const serverAPrompts = {
|
|
@@ -458,12 +460,473 @@ describe("useTamboMcpPromptList - individual server caching", () => {
|
|
|
458
460
|
] },
|
|
459
461
|
React.createElement(Capture, null))))));
|
|
460
462
|
// Wait for server B prompts to be added
|
|
463
|
+
// Now with 2 servers, prompts should be prefixed
|
|
461
464
|
await waitFor(() => {
|
|
462
465
|
expect(capturedPrompts.length).toBe(2);
|
|
463
466
|
});
|
|
464
467
|
const promptNames = capturedPrompts.map((p) => p.prompt.name);
|
|
465
|
-
expect(promptNames).toContain("prompt-a");
|
|
466
|
-
expect(promptNames).toContain("prompt-b");
|
|
468
|
+
expect(promptNames).toContain("server-a:prompt-a");
|
|
469
|
+
expect(promptNames).toContain("server-b:prompt-b");
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
describe("useTamboMcpResourceList - resource management", () => {
|
|
473
|
+
let queryClient;
|
|
474
|
+
beforeEach(() => {
|
|
475
|
+
createImpl = jest.fn();
|
|
476
|
+
queryClient = new QueryClient({
|
|
477
|
+
defaultOptions: {
|
|
478
|
+
queries: {
|
|
479
|
+
retry: false,
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
afterEach(() => {
|
|
485
|
+
queryClient.clear();
|
|
486
|
+
});
|
|
487
|
+
it("should fetch and combine resources from multiple servers", async () => {
|
|
488
|
+
// Mock two servers with different resources
|
|
489
|
+
const serverAResources = {
|
|
490
|
+
resources: [
|
|
491
|
+
{
|
|
492
|
+
uri: "file:///home/user/doc1.txt",
|
|
493
|
+
name: "Document 1",
|
|
494
|
+
mimeType: "text/plain",
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
uri: "file:///home/user/doc2.txt",
|
|
498
|
+
name: "Document 2",
|
|
499
|
+
mimeType: "text/plain",
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
const serverBResources = {
|
|
504
|
+
resources: [
|
|
505
|
+
{
|
|
506
|
+
uri: "file:///workspace/code.js",
|
|
507
|
+
name: "Code File",
|
|
508
|
+
mimeType: "text/javascript",
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
uri: "file:///workspace/README.md",
|
|
512
|
+
name: "Readme",
|
|
513
|
+
mimeType: "text/markdown",
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
const mockClientA = {
|
|
518
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
519
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
520
|
+
listResources: jest.fn().mockResolvedValue(serverAResources),
|
|
521
|
+
close: jest.fn(),
|
|
522
|
+
};
|
|
523
|
+
const mockClientB = {
|
|
524
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
525
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
526
|
+
listResources: jest.fn().mockResolvedValue(serverBResources),
|
|
527
|
+
close: jest.fn(),
|
|
528
|
+
};
|
|
529
|
+
const clientA = {
|
|
530
|
+
client: mockClientA,
|
|
531
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
532
|
+
close: jest.fn(),
|
|
533
|
+
};
|
|
534
|
+
const clientB = {
|
|
535
|
+
client: mockClientB,
|
|
536
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
537
|
+
close: jest.fn(),
|
|
538
|
+
};
|
|
539
|
+
createImpl.mockImplementation(async (url) => {
|
|
540
|
+
if (url === "https://server-a.example")
|
|
541
|
+
return clientA;
|
|
542
|
+
if (url === "https://server-b.example")
|
|
543
|
+
return clientB;
|
|
544
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
545
|
+
});
|
|
546
|
+
let capturedResources = [];
|
|
547
|
+
const Capture = () => {
|
|
548
|
+
const { data: resources } = useTamboMcpResourceList();
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
if (resources) {
|
|
551
|
+
capturedResources = resources;
|
|
552
|
+
}
|
|
553
|
+
}, [resources]);
|
|
554
|
+
return null;
|
|
555
|
+
};
|
|
556
|
+
render(React.createElement(TamboClientContext.Provider, { value: {
|
|
557
|
+
client: { baseURL: "https://api.tambo.co" },
|
|
558
|
+
queryClient,
|
|
559
|
+
isUpdatingToken: false,
|
|
560
|
+
} },
|
|
561
|
+
React.createElement(TamboRegistryProvider, null,
|
|
562
|
+
React.createElement(TamboMcpTokenProvider, null,
|
|
563
|
+
React.createElement(TamboMcpProvider, { mcpServers: [
|
|
564
|
+
{
|
|
565
|
+
url: "https://server-a.example",
|
|
566
|
+
transport: MCPTransport.SSE,
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
url: "https://server-b.example",
|
|
570
|
+
transport: MCPTransport.SSE,
|
|
571
|
+
},
|
|
572
|
+
] },
|
|
573
|
+
React.createElement(Capture, null))))));
|
|
574
|
+
// Wait for all resources to be loaded
|
|
575
|
+
await waitFor(() => {
|
|
576
|
+
expect(capturedResources.length).toBe(4);
|
|
577
|
+
});
|
|
578
|
+
// Verify all resources are present (with prefixes since we have 2 servers)
|
|
579
|
+
const resourceUris = capturedResources.map((r) => r.resource.uri);
|
|
580
|
+
expect(resourceUris).toContain("server-a:file:///home/user/doc1.txt");
|
|
581
|
+
expect(resourceUris).toContain("server-a:file:///home/user/doc2.txt");
|
|
582
|
+
expect(resourceUris).toContain("server-b:file:///workspace/code.js");
|
|
583
|
+
expect(resourceUris).toContain("server-b:file:///workspace/README.md");
|
|
584
|
+
// Verify each resource has the correct server info
|
|
585
|
+
const resource1 = capturedResources.find((r) => r.resource.uri === "server-a:file:///home/user/doc1.txt");
|
|
586
|
+
expect(resource1?.server.url).toBe("https://server-a.example");
|
|
587
|
+
const resource2 = capturedResources.find((r) => r.resource.uri === "server-b:file:///workspace/code.js");
|
|
588
|
+
expect(resource2?.server.url).toBe("https://server-b.example");
|
|
589
|
+
});
|
|
590
|
+
it("should not prefix resources when only one server exists", async () => {
|
|
591
|
+
const serverAResources = {
|
|
592
|
+
resources: [
|
|
593
|
+
{
|
|
594
|
+
uri: "file:///home/user/doc.txt",
|
|
595
|
+
name: "Document",
|
|
596
|
+
mimeType: "text/plain",
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
const mockClientA = {
|
|
601
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
602
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
603
|
+
listResources: jest.fn().mockResolvedValue(serverAResources),
|
|
604
|
+
close: jest.fn(),
|
|
605
|
+
};
|
|
606
|
+
const clientA = {
|
|
607
|
+
client: mockClientA,
|
|
608
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
609
|
+
close: jest.fn(),
|
|
610
|
+
};
|
|
611
|
+
createImpl.mockImplementation(async () => clientA);
|
|
612
|
+
let capturedResources = [];
|
|
613
|
+
const Capture = () => {
|
|
614
|
+
const { data: resources } = useTamboMcpResourceList();
|
|
615
|
+
useEffect(() => {
|
|
616
|
+
if (resources) {
|
|
617
|
+
capturedResources = resources;
|
|
618
|
+
}
|
|
619
|
+
}, [resources]);
|
|
620
|
+
return null;
|
|
621
|
+
};
|
|
622
|
+
render(React.createElement(TamboClientContext.Provider, { value: {
|
|
623
|
+
client: { baseURL: "https://api.tambo.co" },
|
|
624
|
+
queryClient,
|
|
625
|
+
isUpdatingToken: false,
|
|
626
|
+
} },
|
|
627
|
+
React.createElement(TamboRegistryProvider, null,
|
|
628
|
+
React.createElement(TamboMcpTokenProvider, null,
|
|
629
|
+
React.createElement(TamboMcpProvider, { mcpServers: [
|
|
630
|
+
{
|
|
631
|
+
url: "https://server-a.example",
|
|
632
|
+
transport: MCPTransport.SSE,
|
|
633
|
+
},
|
|
634
|
+
] },
|
|
635
|
+
React.createElement(Capture, null))))));
|
|
636
|
+
await waitFor(() => {
|
|
637
|
+
expect(capturedResources.length).toBe(1);
|
|
638
|
+
});
|
|
639
|
+
// No prefix when only 1 server
|
|
640
|
+
expect(capturedResources[0].resource.uri).toBe("file:///home/user/doc.txt");
|
|
641
|
+
});
|
|
642
|
+
it("should remove resource prefixes when a server is removed", async () => {
|
|
643
|
+
const serverAResources = {
|
|
644
|
+
resources: [
|
|
645
|
+
{
|
|
646
|
+
uri: "file:///home/user/doc1.txt",
|
|
647
|
+
name: "Document 1",
|
|
648
|
+
mimeType: "text/plain",
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
uri: "file:///home/user/doc2.txt",
|
|
652
|
+
name: "Document 2",
|
|
653
|
+
mimeType: "text/plain",
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
};
|
|
657
|
+
const serverBResources = {
|
|
658
|
+
resources: [
|
|
659
|
+
{
|
|
660
|
+
uri: "file:///workspace/code.js",
|
|
661
|
+
name: "Code File",
|
|
662
|
+
mimeType: "text/javascript",
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
};
|
|
666
|
+
const mockClientA = {
|
|
667
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
668
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
669
|
+
listResources: jest.fn().mockResolvedValue(serverAResources),
|
|
670
|
+
close: jest.fn(),
|
|
671
|
+
};
|
|
672
|
+
const mockClientB = {
|
|
673
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
674
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
675
|
+
listResources: jest.fn().mockResolvedValue(serverBResources),
|
|
676
|
+
close: jest.fn(),
|
|
677
|
+
};
|
|
678
|
+
const clientA = {
|
|
679
|
+
client: mockClientA,
|
|
680
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
681
|
+
close: jest.fn(),
|
|
682
|
+
};
|
|
683
|
+
const clientB = {
|
|
684
|
+
client: mockClientB,
|
|
685
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
686
|
+
close: jest.fn(),
|
|
687
|
+
};
|
|
688
|
+
createImpl.mockImplementation(async (url) => {
|
|
689
|
+
if (url === "https://server-a.example")
|
|
690
|
+
return clientA;
|
|
691
|
+
if (url === "https://server-b.example")
|
|
692
|
+
return clientB;
|
|
693
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
694
|
+
});
|
|
695
|
+
let capturedResources = [];
|
|
696
|
+
const Capture = () => {
|
|
697
|
+
const { data: resources } = useTamboMcpResourceList();
|
|
698
|
+
useEffect(() => {
|
|
699
|
+
if (resources) {
|
|
700
|
+
capturedResources = resources;
|
|
701
|
+
}
|
|
702
|
+
}, [resources]);
|
|
703
|
+
return null;
|
|
704
|
+
};
|
|
705
|
+
const { rerender } = render(React.createElement(TamboClientContext.Provider, { value: {
|
|
706
|
+
client: { baseURL: "https://api.tambo.co" },
|
|
707
|
+
queryClient,
|
|
708
|
+
isUpdatingToken: false,
|
|
709
|
+
} },
|
|
710
|
+
React.createElement(TamboRegistryProvider, null,
|
|
711
|
+
React.createElement(TamboMcpTokenProvider, null,
|
|
712
|
+
React.createElement(TamboMcpProvider, { mcpServers: [
|
|
713
|
+
{
|
|
714
|
+
url: "https://server-a.example",
|
|
715
|
+
transport: MCPTransport.SSE,
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
url: "https://server-b.example",
|
|
719
|
+
transport: MCPTransport.SSE,
|
|
720
|
+
},
|
|
721
|
+
] },
|
|
722
|
+
React.createElement(Capture, null))))));
|
|
723
|
+
// Wait for all resources to be loaded (prefixed)
|
|
724
|
+
await waitFor(() => {
|
|
725
|
+
expect(capturedResources.length).toBe(3);
|
|
726
|
+
});
|
|
727
|
+
const initialUris = capturedResources.map((r) => r.resource.uri);
|
|
728
|
+
expect(initialUris).toContain("server-a:file:///home/user/doc1.txt");
|
|
729
|
+
expect(initialUris).toContain("server-b:file:///workspace/code.js");
|
|
730
|
+
// Now remove server B
|
|
731
|
+
rerender(React.createElement(TamboClientContext.Provider, { value: {
|
|
732
|
+
client: { baseURL: "https://api.tambo.co" },
|
|
733
|
+
queryClient,
|
|
734
|
+
isUpdatingToken: false,
|
|
735
|
+
} },
|
|
736
|
+
React.createElement(TamboRegistryProvider, null,
|
|
737
|
+
React.createElement(TamboMcpTokenProvider, null,
|
|
738
|
+
React.createElement(TamboMcpProvider, { mcpServers: [
|
|
739
|
+
{
|
|
740
|
+
url: "https://server-a.example",
|
|
741
|
+
transport: MCPTransport.SSE,
|
|
742
|
+
},
|
|
743
|
+
] },
|
|
744
|
+
React.createElement(Capture, null))))));
|
|
745
|
+
// Wait for server B resources to be removed and prefixes stripped
|
|
746
|
+
await waitFor(() => {
|
|
747
|
+
expect(capturedResources.length).toBe(2);
|
|
748
|
+
});
|
|
749
|
+
const updatedUris = capturedResources.map((r) => r.resource.uri);
|
|
750
|
+
expect(updatedUris).toContain("file:///home/user/doc1.txt"); // No prefix
|
|
751
|
+
expect(updatedUris).toContain("file:///home/user/doc2.txt");
|
|
752
|
+
expect(updatedUris).not.toContain("server-a:file:///home/user/doc1.txt"); // No prefix when only 1 server
|
|
753
|
+
expect(updatedUris).not.toContain("server-b:file:///workspace/code.js"); // Server B removed
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
describe("useTamboMcpResource - read individual resource", () => {
|
|
757
|
+
let queryClient;
|
|
758
|
+
beforeEach(() => {
|
|
759
|
+
createImpl = jest.fn();
|
|
760
|
+
queryClient = new QueryClient({
|
|
761
|
+
defaultOptions: {
|
|
762
|
+
queries: {
|
|
763
|
+
retry: false,
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
afterEach(() => {
|
|
769
|
+
queryClient.clear();
|
|
770
|
+
});
|
|
771
|
+
it("should read a resource from a single server (unprefixed)", async () => {
|
|
772
|
+
const serverAResources = {
|
|
773
|
+
resources: [
|
|
774
|
+
{
|
|
775
|
+
uri: "file:///home/user/doc.txt",
|
|
776
|
+
name: "Document",
|
|
777
|
+
mimeType: "text/plain",
|
|
778
|
+
},
|
|
779
|
+
],
|
|
780
|
+
};
|
|
781
|
+
const resourceContents = {
|
|
782
|
+
contents: [
|
|
783
|
+
{
|
|
784
|
+
uri: "file:///home/user/doc.txt",
|
|
785
|
+
mimeType: "text/plain",
|
|
786
|
+
text: "Hello, world!",
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
};
|
|
790
|
+
const mockClientA = {
|
|
791
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
792
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
793
|
+
listResources: jest.fn().mockResolvedValue(serverAResources),
|
|
794
|
+
readResource: jest.fn().mockResolvedValue(resourceContents),
|
|
795
|
+
close: jest.fn(),
|
|
796
|
+
};
|
|
797
|
+
const clientA = {
|
|
798
|
+
client: mockClientA,
|
|
799
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
800
|
+
close: jest.fn(),
|
|
801
|
+
};
|
|
802
|
+
createImpl.mockImplementation(async () => clientA);
|
|
803
|
+
let capturedResourceData = null;
|
|
804
|
+
const Capture = () => {
|
|
805
|
+
const { data: resourceData } = useTamboMcpResource("file:///home/user/doc.txt");
|
|
806
|
+
useEffect(() => {
|
|
807
|
+
if (resourceData) {
|
|
808
|
+
capturedResourceData = resourceData;
|
|
809
|
+
}
|
|
810
|
+
}, [resourceData]);
|
|
811
|
+
return null;
|
|
812
|
+
};
|
|
813
|
+
render(React.createElement(TamboClientContext.Provider, { value: {
|
|
814
|
+
client: { baseURL: "https://api.tambo.co" },
|
|
815
|
+
queryClient,
|
|
816
|
+
isUpdatingToken: false,
|
|
817
|
+
} },
|
|
818
|
+
React.createElement(TamboRegistryProvider, null,
|
|
819
|
+
React.createElement(TamboMcpTokenProvider, null,
|
|
820
|
+
React.createElement(TamboMcpProvider, { mcpServers: [
|
|
821
|
+
{
|
|
822
|
+
url: "https://server-a.example",
|
|
823
|
+
transport: MCPTransport.SSE,
|
|
824
|
+
},
|
|
825
|
+
] },
|
|
826
|
+
React.createElement(Capture, null))))));
|
|
827
|
+
await waitFor(() => {
|
|
828
|
+
expect(capturedResourceData).not.toBeNull();
|
|
829
|
+
});
|
|
830
|
+
expect(capturedResourceData.contents[0].text).toBe("Hello, world!");
|
|
831
|
+
expect(mockClientA.readResource).toHaveBeenCalledWith({
|
|
832
|
+
uri: "file:///home/user/doc.txt",
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
it("should read a resource from multiple servers (prefixed)", async () => {
|
|
836
|
+
const serverAResources = {
|
|
837
|
+
resources: [
|
|
838
|
+
{
|
|
839
|
+
uri: "file:///home/user/doc.txt",
|
|
840
|
+
name: "Document",
|
|
841
|
+
mimeType: "text/plain",
|
|
842
|
+
},
|
|
843
|
+
],
|
|
844
|
+
};
|
|
845
|
+
const serverBResources = {
|
|
846
|
+
resources: [
|
|
847
|
+
{
|
|
848
|
+
uri: "file:///workspace/code.js",
|
|
849
|
+
name: "Code",
|
|
850
|
+
mimeType: "text/javascript",
|
|
851
|
+
},
|
|
852
|
+
],
|
|
853
|
+
};
|
|
854
|
+
const resourceContentsA = {
|
|
855
|
+
contents: [
|
|
856
|
+
{
|
|
857
|
+
uri: "file:///home/user/doc.txt",
|
|
858
|
+
mimeType: "text/plain",
|
|
859
|
+
text: "From server A",
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
};
|
|
863
|
+
const mockClientA = {
|
|
864
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
865
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
866
|
+
listResources: jest.fn().mockResolvedValue(serverAResources),
|
|
867
|
+
readResource: jest.fn().mockResolvedValue(resourceContentsA),
|
|
868
|
+
close: jest.fn(),
|
|
869
|
+
};
|
|
870
|
+
const mockClientB = {
|
|
871
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
872
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
873
|
+
listResources: jest.fn().mockResolvedValue(serverBResources),
|
|
874
|
+
close: jest.fn(),
|
|
875
|
+
};
|
|
876
|
+
const clientA = {
|
|
877
|
+
client: mockClientA,
|
|
878
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
879
|
+
close: jest.fn(),
|
|
880
|
+
};
|
|
881
|
+
const clientB = {
|
|
882
|
+
client: mockClientB,
|
|
883
|
+
listTools: jest.fn().mockResolvedValue([]),
|
|
884
|
+
close: jest.fn(),
|
|
885
|
+
};
|
|
886
|
+
createImpl.mockImplementation(async (url) => {
|
|
887
|
+
if (url === "https://server-a.example")
|
|
888
|
+
return clientA;
|
|
889
|
+
if (url === "https://server-b.example")
|
|
890
|
+
return clientB;
|
|
891
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
892
|
+
});
|
|
893
|
+
let capturedResourceData = null;
|
|
894
|
+
const Capture = () => {
|
|
895
|
+
// Request with prefix
|
|
896
|
+
const { data: resourceData } = useTamboMcpResource("server-a:file:///home/user/doc.txt");
|
|
897
|
+
useEffect(() => {
|
|
898
|
+
if (resourceData) {
|
|
899
|
+
capturedResourceData = resourceData;
|
|
900
|
+
}
|
|
901
|
+
}, [resourceData]);
|
|
902
|
+
return null;
|
|
903
|
+
};
|
|
904
|
+
render(React.createElement(TamboClientContext.Provider, { value: {
|
|
905
|
+
client: { baseURL: "https://api.tambo.co" },
|
|
906
|
+
queryClient,
|
|
907
|
+
isUpdatingToken: false,
|
|
908
|
+
} },
|
|
909
|
+
React.createElement(TamboRegistryProvider, null,
|
|
910
|
+
React.createElement(TamboMcpTokenProvider, null,
|
|
911
|
+
React.createElement(TamboMcpProvider, { mcpServers: [
|
|
912
|
+
{
|
|
913
|
+
url: "https://server-a.example",
|
|
914
|
+
transport: MCPTransport.SSE,
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
url: "https://server-b.example",
|
|
918
|
+
transport: MCPTransport.SSE,
|
|
919
|
+
},
|
|
920
|
+
] },
|
|
921
|
+
React.createElement(Capture, null))))));
|
|
922
|
+
await waitFor(() => {
|
|
923
|
+
expect(capturedResourceData).not.toBeNull();
|
|
924
|
+
});
|
|
925
|
+
expect(capturedResourceData.contents[0].text).toBe("From server A");
|
|
926
|
+
// Verify the prefix was stripped before calling the server
|
|
927
|
+
expect(mockClientA.readResource).toHaveBeenCalledWith({
|
|
928
|
+
uri: "file:///home/user/doc.txt",
|
|
929
|
+
});
|
|
467
930
|
});
|
|
468
931
|
});
|
|
469
932
|
//# sourceMappingURL=mcp-hooks.test.js.map
|