@genkit-ai/mcp 1.14.1-rc.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.
Files changed (88) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +205 -0
  3. package/examples/client/index.js +36 -0
  4. package/examples/client/package.json +25 -0
  5. package/examples/server/index.js +46 -0
  6. package/examples/server/package.json +18 -0
  7. package/examples/server/prompts/port_code.prompt +13 -0
  8. package/lib/client/client.d.mts +177 -0
  9. package/lib/client/client.d.ts +177 -0
  10. package/lib/client/client.js +282 -0
  11. package/lib/client/client.js.map +1 -0
  12. package/lib/client/client.mjs +267 -0
  13. package/lib/client/client.mjs.map +1 -0
  14. package/lib/client/host.d.mts +202 -0
  15. package/lib/client/host.d.ts +202 -0
  16. package/lib/client/host.js +392 -0
  17. package/lib/client/host.js.map +1 -0
  18. package/lib/client/host.mjs +368 -0
  19. package/lib/client/host.mjs.map +1 -0
  20. package/lib/client/index.d.mts +9 -0
  21. package/lib/client/index.d.ts +9 -0
  22. package/lib/client/index.js +32 -0
  23. package/lib/client/index.js.map +1 -0
  24. package/lib/client/index.mjs +7 -0
  25. package/lib/client/index.mjs.map +1 -0
  26. package/lib/index.d.mts +12 -0
  27. package/lib/index.d.ts +12 -0
  28. package/lib/index.js +48 -0
  29. package/lib/index.js.map +1 -0
  30. package/lib/index.mjs +22 -0
  31. package/lib/index.mjs.map +1 -0
  32. package/lib/server.d.mts +188 -0
  33. package/lib/server.d.ts +188 -0
  34. package/lib/server.js +280 -0
  35. package/lib/server.js.map +1 -0
  36. package/lib/server.mjs +249 -0
  37. package/lib/server.mjs.map +1 -0
  38. package/lib/util/index.d.mts +11 -0
  39. package/lib/util/index.d.ts +11 -0
  40. package/lib/util/index.js +29 -0
  41. package/lib/util/index.js.map +1 -0
  42. package/lib/util/index.mjs +5 -0
  43. package/lib/util/index.mjs.map +1 -0
  44. package/lib/util/message.d.mts +43 -0
  45. package/lib/util/message.d.ts +43 -0
  46. package/lib/util/message.js +61 -0
  47. package/lib/util/message.js.map +1 -0
  48. package/lib/util/message.mjs +36 -0
  49. package/lib/util/message.mjs.map +1 -0
  50. package/lib/util/prompts.d.mts +45 -0
  51. package/lib/util/prompts.d.ts +45 -0
  52. package/lib/util/prompts.js +147 -0
  53. package/lib/util/prompts.js.map +1 -0
  54. package/lib/util/prompts.mjs +123 -0
  55. package/lib/util/prompts.mjs.map +1 -0
  56. package/lib/util/resource.d.mts +28 -0
  57. package/lib/util/resource.d.ts +28 -0
  58. package/lib/util/resource.js +116 -0
  59. package/lib/util/resource.js.map +1 -0
  60. package/lib/util/resource.mjs +95 -0
  61. package/lib/util/resource.mjs.map +1 -0
  62. package/lib/util/tools.d.mts +37 -0
  63. package/lib/util/tools.d.ts +37 -0
  64. package/lib/util/tools.js +120 -0
  65. package/lib/util/tools.js.map +1 -0
  66. package/lib/util/tools.mjs +95 -0
  67. package/lib/util/tools.mjs.map +1 -0
  68. package/lib/util/transport.d.mts +39 -0
  69. package/lib/util/transport.d.ts +39 -0
  70. package/lib/util/transport.js +63 -0
  71. package/lib/util/transport.js.map +1 -0
  72. package/lib/util/transport.mjs +29 -0
  73. package/lib/util/transport.mjs.map +1 -0
  74. package/package.json +57 -0
  75. package/src/client/client.ts +414 -0
  76. package/src/client/host.ts +485 -0
  77. package/src/client/index.ts +29 -0
  78. package/src/index.ts +114 -0
  79. package/src/server.ts +330 -0
  80. package/src/util/index.ts +20 -0
  81. package/src/util/message.ts +72 -0
  82. package/src/util/prompts.ts +223 -0
  83. package/src/util/resource.ts +141 -0
  84. package/src/util/tools.ts +164 -0
  85. package/src/util/transport.ts +67 -0
  86. package/tests/fakes.ts +221 -0
  87. package/tests/host_test.ts +609 -0
  88. package/tests/server_test.ts +165 -0
@@ -0,0 +1,609 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as assert from 'assert';
18
+ import { Genkit, genkit, ToolAction } from 'genkit';
19
+ import { logger } from 'genkit/logging';
20
+ import { afterEach, beforeEach, describe, it } from 'node:test';
21
+ import { createMcpHost, GenkitMcpHost } from '../src/index.js';
22
+ import { defineEchoModel, FakeTransport } from './fakes.js';
23
+
24
+ logger.setLogLevel('debug');
25
+
26
+ describe('createMcpHost', () => {
27
+ let ai: Genkit;
28
+
29
+ beforeEach(async () => {
30
+ ai = genkit({});
31
+ defineEchoModel(ai);
32
+ });
33
+
34
+ describe('host', () => {
35
+ let fakeTransport1: FakeTransport;
36
+ let fakeTransport2: FakeTransport;
37
+ let clientHost: GenkitMcpHost;
38
+
39
+ beforeEach(() => {
40
+ clientHost = createMcpHost({
41
+ name: 'test-mcp-host',
42
+ });
43
+
44
+ fakeTransport1 = new FakeTransport();
45
+ fakeTransport1.tools.push({
46
+ name: 'testTool1',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ foo: {
51
+ type: 'string',
52
+ },
53
+ },
54
+ required: ['foo'],
55
+ additionalProperties: true,
56
+ $schema: 'http://json-schema.org/draft-07/schema#',
57
+ },
58
+ description: 'test tool 1',
59
+ });
60
+
61
+ fakeTransport2 = new FakeTransport();
62
+ fakeTransport2.tools.push({
63
+ name: 'testTool2',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ foo: {
68
+ type: 'string',
69
+ },
70
+ },
71
+ required: ['foo'],
72
+ additionalProperties: true,
73
+ $schema: 'http://json-schema.org/draft-07/schema#',
74
+ },
75
+ description: 'test tool 2',
76
+ });
77
+ });
78
+
79
+ afterEach(async () => {
80
+ await clientHost?.close();
81
+ });
82
+
83
+ it('should dynamically connect clients', async () => {
84
+ // no server connected, no tools
85
+ assert.deepStrictEqual(
86
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
87
+ []
88
+ );
89
+
90
+ // connect fakeTransport1
91
+ await clientHost.connect('test-mcp-host1', {
92
+ transport: fakeTransport1,
93
+ });
94
+
95
+ assert.deepStrictEqual(
96
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
97
+ ['test-mcp-host1/testTool1']
98
+ );
99
+
100
+ // connect fakeTransport2
101
+ await clientHost.connect('test-mcp-host2', {
102
+ transport: fakeTransport2,
103
+ });
104
+
105
+ assert.deepStrictEqual(
106
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
107
+ ['test-mcp-host1/testTool1', 'test-mcp-host2/testTool2']
108
+ );
109
+
110
+ // disable
111
+ await clientHost.disable('test-mcp-host1');
112
+
113
+ assert.deepStrictEqual(
114
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
115
+ ['test-mcp-host2/testTool2']
116
+ );
117
+
118
+ // reconnect
119
+ await clientHost.enable('test-mcp-host1');
120
+
121
+ assert.deepStrictEqual(
122
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
123
+ ['test-mcp-host1/testTool1', 'test-mcp-host2/testTool2']
124
+ );
125
+
126
+ // disconnect
127
+ await clientHost.disconnect('test-mcp-host1');
128
+
129
+ assert.deepStrictEqual(
130
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
131
+ ['test-mcp-host2/testTool2']
132
+ );
133
+ });
134
+
135
+ it('updated roots', async () => {
136
+ // no server connected, no tools
137
+ assert.deepStrictEqual(
138
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
139
+ []
140
+ );
141
+
142
+ // connect fakeTransport1
143
+ await clientHost.connect('test-mcp-host1', {
144
+ transport: fakeTransport1,
145
+ roots: [
146
+ {
147
+ uri: `file:///foo`,
148
+ name: 'foo',
149
+ },
150
+ ],
151
+ });
152
+
153
+ // MCP communicates roots async...
154
+ await new Promise((r) => setTimeout(r, 10));
155
+
156
+ assert.deepStrictEqual(fakeTransport1.roots, [
157
+ {
158
+ name: 'foo',
159
+ uri: 'file:///foo',
160
+ },
161
+ ]);
162
+
163
+ await clientHost.getClient('test-mcp-host1').updateRoots([
164
+ {
165
+ uri: `file:///bar`,
166
+ name: 'bar',
167
+ },
168
+ ]);
169
+ // MCP communicates roots async...
170
+ await new Promise((r) => setTimeout(r, 10));
171
+
172
+ assert.deepStrictEqual(fakeTransport1.roots, [
173
+ {
174
+ name: 'bar',
175
+ uri: 'file:///bar',
176
+ },
177
+ ]);
178
+ });
179
+ });
180
+
181
+ describe('tools', () => {
182
+ let fakeTransport: FakeTransport;
183
+ let clientHost: GenkitMcpHost;
184
+
185
+ beforeEach(() => {
186
+ fakeTransport = new FakeTransport();
187
+ clientHost = createMcpHost({
188
+ name: 'test-mcp-host',
189
+ mcpServers: {
190
+ 'test-server': {
191
+ transport: fakeTransport,
192
+ },
193
+ },
194
+ });
195
+
196
+ fakeTransport.tools.push({
197
+ name: 'testTool',
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {
201
+ foo: {
202
+ type: 'string',
203
+ },
204
+ },
205
+ required: ['foo'],
206
+ additionalProperties: true,
207
+ $schema: 'http://json-schema.org/draft-07/schema#',
208
+ },
209
+ description: 'test tool',
210
+ });
211
+ });
212
+
213
+ afterEach(() => {
214
+ clientHost?.close();
215
+ });
216
+
217
+ it('should list tools', async () => {
218
+ assert.deepStrictEqual(
219
+ (await clientHost.getActiveTools(ai)).map((t) => t.__action.name),
220
+ ['test-server/testTool']
221
+ );
222
+ });
223
+
224
+ it('should call the tool', async () => {
225
+ fakeTransport.callToolResult = {
226
+ content: [
227
+ {
228
+ type: 'text',
229
+ text: 'yep {"foo":"bar"}',
230
+ },
231
+ ],
232
+ };
233
+
234
+ const tool: ToolAction = (await clientHost.getActiveTools(ai))[0];
235
+ const response = await tool(
236
+ {
237
+ foo: 'bar',
238
+ },
239
+ { context: { mcp: { _meta: { soMeta: true } } } }
240
+ );
241
+ assert.deepStrictEqual(response, 'yep {"foo":"bar"}{"soMeta":true}');
242
+ });
243
+
244
+ it('should call the tool with _meta', async () => {
245
+ fakeTransport.callToolResult = {
246
+ content: [
247
+ {
248
+ type: 'text',
249
+ text: 'yep {"foo":"bar"}',
250
+ },
251
+ ],
252
+ };
253
+
254
+ const tool = (await clientHost.getActiveTools(ai))[0];
255
+ const response = await tool({
256
+ foo: 'bar',
257
+ });
258
+ assert.deepStrictEqual(response, 'yep {"foo":"bar"}');
259
+ });
260
+ });
261
+
262
+ describe('prompts', () => {
263
+ let fakeTransport: FakeTransport;
264
+ let clientHost: GenkitMcpHost;
265
+
266
+ beforeEach(() => {
267
+ fakeTransport = new FakeTransport();
268
+
269
+ clientHost = createMcpHost({
270
+ name: 'test-mcp-host',
271
+ mcpServers: {
272
+ 'test-server': {
273
+ transport: fakeTransport,
274
+ },
275
+ },
276
+ });
277
+
278
+ // Note: fakeTransport.prompts.push({ name: 'testPrompt' }); is moved to specific tests
279
+ fakeTransport.getPromptResult = {
280
+ messages: [
281
+ {
282
+ role: 'user',
283
+ content: {
284
+ type: 'text',
285
+ text: 'prompt says: hello',
286
+ },
287
+ },
288
+ ],
289
+ };
290
+ });
291
+
292
+ afterEach(() => {
293
+ clientHost?.close();
294
+ });
295
+
296
+ it('should list active prompts', async () => {
297
+ // Initially no prompts
298
+ assert.deepStrictEqual(await clientHost.getActivePrompts(ai), []);
299
+
300
+ // Add a prompt to the first transport
301
+ fakeTransport.prompts.push({
302
+ name: 'testPrompt1',
303
+ arguments: [
304
+ {
305
+ name: 'foo',
306
+ description: 'foo arg',
307
+ required: false,
308
+ },
309
+ ],
310
+ description: 'descr',
311
+ _meta: { foo: true },
312
+ });
313
+ let activePrompts = await clientHost.getActivePrompts(ai);
314
+ assert.strictEqual(activePrompts.length, 1);
315
+ assert.deepStrictEqual(await activePrompts[0].render(), {
316
+ messages: [
317
+ {
318
+ role: 'user',
319
+ content: [
320
+ {
321
+ text: 'prompt says: hello',
322
+ },
323
+ ],
324
+ },
325
+ ],
326
+ });
327
+
328
+ // Add a second transport with another prompt
329
+ const fakeTransport2 = new FakeTransport();
330
+ fakeTransport2.prompts.push({
331
+ name: 'testPrompt2',
332
+ });
333
+ await clientHost.connect('test-server-2', {
334
+ transport: fakeTransport2,
335
+ });
336
+
337
+ activePrompts = await clientHost.getActivePrompts(ai);
338
+ assert.deepStrictEqual(activePrompts[0].ref.metadata, {
339
+ arguments: [
340
+ {
341
+ description: 'foo arg',
342
+ name: 'foo',
343
+ required: false,
344
+ },
345
+ ],
346
+ description: 'descr',
347
+ mcp: { _meta: { foo: true } },
348
+ });
349
+ assert.deepStrictEqual(
350
+ activePrompts.map((p) => p.ref.name),
351
+ ['testPrompt1', 'testPrompt2']
352
+ );
353
+
354
+ // Disable the first server
355
+ await clientHost.disable('test-server');
356
+ activePrompts = await clientHost.getActivePrompts(ai);
357
+ assert.deepStrictEqual(
358
+ activePrompts.map((p) => p.ref.name),
359
+ ['testPrompt2']
360
+ );
361
+
362
+ // Enable the first server again
363
+ await clientHost.enable('test-server');
364
+ activePrompts = await clientHost.getActivePrompts(ai);
365
+ assert.deepStrictEqual(
366
+ activePrompts.map((p) => p.ref.name),
367
+ ['testPrompt1', 'testPrompt2']
368
+ );
369
+ });
370
+
371
+ it('should execute prompt', async () => {
372
+ fakeTransport.prompts.push({
373
+ name: 'testPrompt',
374
+ });
375
+ const prompt = await clientHost.getPrompt(
376
+ ai,
377
+ 'test-server',
378
+ 'testPrompt',
379
+ { model: 'echoModel', config: { temperature: 11 } }
380
+ );
381
+ assert.ok(prompt);
382
+ const { text } = await prompt({
383
+ input: 'hello',
384
+ });
385
+
386
+ assert.strictEqual(
387
+ text,
388
+ 'Echo: prompt says: hello; config: {"temperature":11}'
389
+ );
390
+ });
391
+
392
+ it('should render prompt', async () => {
393
+ fakeTransport.prompts.push({
394
+ name: 'testPrompt',
395
+ });
396
+ const prompt = await clientHost.getPrompt(
397
+ ai,
398
+ 'test-server',
399
+ 'testPrompt',
400
+ { model: 'echoModel', config: { temperature: 11 } }
401
+ );
402
+ assert.ok(prompt);
403
+ const request = await prompt.render({
404
+ input: 'hello',
405
+ });
406
+
407
+ assert.deepStrictEqual(request.messages, [
408
+ { role: 'user', content: [{ text: 'prompt says: hello' }] },
409
+ ]);
410
+ });
411
+
412
+ it('should render prompt with _meta', async () => {
413
+ fakeTransport.prompts.push({
414
+ name: 'testPrompt',
415
+ });
416
+ const prompt = await clientHost.getPrompt(
417
+ ai,
418
+ 'test-server',
419
+ 'testPrompt',
420
+ { model: 'echoModel', config: { temperature: 11 } }
421
+ );
422
+ assert.ok(prompt);
423
+ const request = await prompt.render(
424
+ {
425
+ input: 'hello',
426
+ },
427
+ { context: { mcp: { _meta: { soMeta: true } } } }
428
+ );
429
+
430
+ assert.deepStrictEqual(request.messages, [
431
+ { role: 'user', content: [{ text: 'prompt says: hello' }] },
432
+ { role: 'model', content: [{ text: '{"soMeta":true}' }] },
433
+ ]);
434
+ });
435
+
436
+ it('should stream prompt', async () => {
437
+ fakeTransport.prompts.push({
438
+ name: 'testPrompt',
439
+ });
440
+ const prompt = await clientHost.getPrompt(
441
+ ai,
442
+ 'test-server',
443
+ 'testPrompt',
444
+ { model: 'echoModel', config: { temperature: 11 } }
445
+ );
446
+ assert.ok(prompt);
447
+ const { stream, response } = prompt.stream({
448
+ input: 'hello',
449
+ });
450
+
451
+ const chunks = [] as string[];
452
+ for await (const chunk of stream) {
453
+ chunks.push(chunk.text);
454
+ }
455
+
456
+ assert.deepStrictEqual(chunks, ['3', '2', '1']);
457
+ assert.strictEqual(
458
+ (await response).text,
459
+ 'Echo: prompt says: hello; config: {"temperature":11}'
460
+ );
461
+ });
462
+ });
463
+
464
+ describe('resources', () => {
465
+ let fakeTransport: FakeTransport;
466
+ let clientHost: GenkitMcpHost;
467
+
468
+ beforeEach(() => {
469
+ fakeTransport = new FakeTransport();
470
+
471
+ clientHost = createMcpHost({
472
+ name: 'test-mcp-host',
473
+ mcpServers: {
474
+ 'test-server': {
475
+ transport: fakeTransport,
476
+ },
477
+ },
478
+ });
479
+ });
480
+
481
+ afterEach(() => {
482
+ clientHost?.close();
483
+ });
484
+
485
+ it('should list active resources', async () => {
486
+ // Initially no prompts
487
+ assert.deepStrictEqual(await clientHost.getActiveResources(ai), []);
488
+
489
+ // Add a prompt to the first transport
490
+ fakeTransport.resources.push({
491
+ name: 'testResource1',
492
+ uri: 'test://resource/1',
493
+ description: 'test resource 1',
494
+ _meta: { foo: true },
495
+ });
496
+ fakeTransport.resourceTemplates.push({
497
+ name: 'testResourceTmpl',
498
+ uriTemplate: 'test://resource/{id}',
499
+ description: 'test resource template',
500
+ _meta: { foo: true },
501
+ });
502
+ let activeResources = await clientHost.getActiveResources(ai);
503
+ assert.strictEqual(activeResources.length, 2);
504
+
505
+ // Add a second transport with another prompt
506
+ const fakeTransport2 = new FakeTransport();
507
+ fakeTransport2.resources.push({
508
+ name: 'testResource2',
509
+ uri: 'test://resource/2',
510
+ description: 'test resource 2',
511
+ _meta: { foo: true },
512
+ });
513
+ await clientHost.connect('test-server-2', {
514
+ transport: fakeTransport2,
515
+ });
516
+
517
+ activeResources = await clientHost.getActiveResources(ai);
518
+ assert.deepStrictEqual(activeResources[0].__action.metadata, {
519
+ type: 'resource',
520
+ dynamic: true,
521
+ resource: {
522
+ template: undefined,
523
+ uri: 'test://resource/1',
524
+ },
525
+ mcp: { _meta: { foo: true } },
526
+ });
527
+ assert.deepStrictEqual(
528
+ activeResources.map((p) => p.__action.name),
529
+ [
530
+ 'test-server/testResource1',
531
+ 'test-server/testResourceTmpl',
532
+ 'test-server-2/testResource2',
533
+ ]
534
+ );
535
+
536
+ // Disable the first server
537
+ await clientHost.disable('test-server');
538
+ activeResources = await clientHost.getActiveResources(ai);
539
+ assert.deepStrictEqual(
540
+ activeResources.map((p) => p.__action.name),
541
+ ['test-server-2/testResource2']
542
+ );
543
+
544
+ // Enable the first server again
545
+ await clientHost.enable('test-server');
546
+ activeResources = await clientHost.getActiveResources(ai);
547
+ assert.deepStrictEqual(
548
+ activeResources.map((p) => p.__action.name),
549
+ [
550
+ 'test-server/testResource1',
551
+ 'test-server/testResourceTmpl',
552
+ 'test-server-2/testResource2',
553
+ ]
554
+ );
555
+ });
556
+
557
+ it('should render resource', async () => {
558
+ fakeTransport.resources.push({
559
+ name: 'testResource1',
560
+ uri: 'test://resource/1',
561
+ description: 'test resource 1',
562
+ _meta: { foo: true },
563
+ });
564
+ fakeTransport.readResourceResult = {
565
+ contents: [
566
+ {
567
+ uri: 'test://resource/1',
568
+ text: 'text resource',
569
+ },
570
+ {
571
+ uri: 'test://resource/1',
572
+ blob: 'UmVzb3VyY2UgMjogVGhpcyBpcyBhIGJhc2U2NCBibG9i',
573
+ mimeType: 'application/png',
574
+ },
575
+ ],
576
+ };
577
+ const prompt = (await clientHost.getActiveResources(ai))[0];
578
+ assert.ok(prompt);
579
+
580
+ const response = await prompt.attach(ai.registry)({
581
+ uri: 'test://resource/1',
582
+ });
583
+
584
+ assert.deepStrictEqual(response, {
585
+ content: [
586
+ {
587
+ text: 'text resource',
588
+ metadata: {
589
+ resource: {
590
+ uri: 'test://resource/1',
591
+ },
592
+ },
593
+ },
594
+ {
595
+ media: {
596
+ contentType: 'application/png',
597
+ url: 'data:application/png;base64,UmVzb3VyY2UgMjogVGhpcyBpcyBhIGJhc2U2NCBibG9i',
598
+ },
599
+ metadata: {
600
+ resource: {
601
+ uri: 'test://resource/1',
602
+ },
603
+ },
604
+ },
605
+ ],
606
+ });
607
+ });
608
+ });
609
+ });