@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.
- package/LICENSE +203 -0
- package/README.md +205 -0
- package/examples/client/index.js +36 -0
- package/examples/client/package.json +25 -0
- package/examples/server/index.js +46 -0
- package/examples/server/package.json +18 -0
- package/examples/server/prompts/port_code.prompt +13 -0
- package/lib/client/client.d.mts +177 -0
- package/lib/client/client.d.ts +177 -0
- package/lib/client/client.js +282 -0
- package/lib/client/client.js.map +1 -0
- package/lib/client/client.mjs +267 -0
- package/lib/client/client.mjs.map +1 -0
- package/lib/client/host.d.mts +202 -0
- package/lib/client/host.d.ts +202 -0
- package/lib/client/host.js +392 -0
- package/lib/client/host.js.map +1 -0
- package/lib/client/host.mjs +368 -0
- package/lib/client/host.mjs.map +1 -0
- package/lib/client/index.d.mts +9 -0
- package/lib/client/index.d.ts +9 -0
- package/lib/client/index.js +32 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/index.mjs +7 -0
- package/lib/client/index.mjs.map +1 -0
- package/lib/index.d.mts +12 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +48 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +22 -0
- package/lib/index.mjs.map +1 -0
- package/lib/server.d.mts +188 -0
- package/lib/server.d.ts +188 -0
- package/lib/server.js +280 -0
- package/lib/server.js.map +1 -0
- package/lib/server.mjs +249 -0
- package/lib/server.mjs.map +1 -0
- package/lib/util/index.d.mts +11 -0
- package/lib/util/index.d.ts +11 -0
- package/lib/util/index.js +29 -0
- package/lib/util/index.js.map +1 -0
- package/lib/util/index.mjs +5 -0
- package/lib/util/index.mjs.map +1 -0
- package/lib/util/message.d.mts +43 -0
- package/lib/util/message.d.ts +43 -0
- package/lib/util/message.js +61 -0
- package/lib/util/message.js.map +1 -0
- package/lib/util/message.mjs +36 -0
- package/lib/util/message.mjs.map +1 -0
- package/lib/util/prompts.d.mts +45 -0
- package/lib/util/prompts.d.ts +45 -0
- package/lib/util/prompts.js +147 -0
- package/lib/util/prompts.js.map +1 -0
- package/lib/util/prompts.mjs +123 -0
- package/lib/util/prompts.mjs.map +1 -0
- package/lib/util/resource.d.mts +28 -0
- package/lib/util/resource.d.ts +28 -0
- package/lib/util/resource.js +116 -0
- package/lib/util/resource.js.map +1 -0
- package/lib/util/resource.mjs +95 -0
- package/lib/util/resource.mjs.map +1 -0
- package/lib/util/tools.d.mts +37 -0
- package/lib/util/tools.d.ts +37 -0
- package/lib/util/tools.js +120 -0
- package/lib/util/tools.js.map +1 -0
- package/lib/util/tools.mjs +95 -0
- package/lib/util/tools.mjs.map +1 -0
- package/lib/util/transport.d.mts +39 -0
- package/lib/util/transport.d.ts +39 -0
- package/lib/util/transport.js +63 -0
- package/lib/util/transport.js.map +1 -0
- package/lib/util/transport.mjs +29 -0
- package/lib/util/transport.mjs.map +1 -0
- package/package.json +57 -0
- package/src/client/client.ts +414 -0
- package/src/client/host.ts +485 -0
- package/src/client/index.ts +29 -0
- package/src/index.ts +114 -0
- package/src/server.ts +330 -0
- package/src/util/index.ts +20 -0
- package/src/util/message.ts +72 -0
- package/src/util/prompts.ts +223 -0
- package/src/util/resource.ts +141 -0
- package/src/util/tools.ts +164 -0
- package/src/util/transport.ts +67 -0
- package/tests/fakes.ts +221 -0
- package/tests/host_test.ts +609 -0
- 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
|
+
});
|