@aura-labs-ai/scout 0.2.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/README.md +237 -0
- package/bin/scout-cli.js +325 -0
- package/examples/basic-purchase.js +112 -0
- package/examples/protocol-integration.js +294 -0
- package/package.json +63 -0
- package/src/activity.js +420 -0
- package/src/ap2/mandates.js +366 -0
- package/src/client.js +217 -0
- package/src/errors.js +66 -0
- package/src/index.js +655 -0
- package/src/intent-session.js +233 -0
- package/src/key-manager.js +204 -0
- package/src/key-manager.test.js +293 -0
- package/src/mcp/client.js +458 -0
- package/src/session.js +603 -0
- package/src/tap/visa.js +345 -0
- package/src/tests/README.md +157 -0
- package/src/tests/ap2-mandates.test.js +413 -0
- package/src/tests/intent-session.test.js +292 -0
- package/src/tests/mcp-client.test.js +224 -0
- package/src/tests/ping.test.js +282 -0
- package/src/tests/run-protocol-tests.js +57 -0
- package/src/tests/scenarios/ap2-scenarios.test.js +422 -0
- package/src/tests/scenarios/index.js +60 -0
- package/src/tests/scenarios/integration-scenarios.test.js +514 -0
- package/src/tests/scenarios/mcp-scenarios.test.js +419 -0
- package/src/tests/scenarios/tap-scenarios.test.js +461 -0
- package/src/tests/visa-tap.test.js +376 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client for AURA Scout SDK
|
|
3
|
+
*
|
|
4
|
+
* Implements Model Context Protocol client functionality,
|
|
5
|
+
* allowing Scouts to connect to MCP servers for additional
|
|
6
|
+
* context (tools, databases, calendars, etc.)
|
|
7
|
+
*
|
|
8
|
+
* @see https://modelcontextprotocol.io/specification
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MCP Client - connects to MCP servers
|
|
15
|
+
*/
|
|
16
|
+
export class MCPClient extends EventEmitter {
|
|
17
|
+
#connections = new Map();
|
|
18
|
+
#config;
|
|
19
|
+
#clientInfo;
|
|
20
|
+
|
|
21
|
+
// MCP Protocol version
|
|
22
|
+
static PROTOCOL_VERSION = '2024-11-05';
|
|
23
|
+
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.#config = {
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
retryAttempts: 3,
|
|
29
|
+
retryDelay: 1000,
|
|
30
|
+
...config,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.#clientInfo = config.clientInfo || {
|
|
34
|
+
name: 'aura-scout',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get client info
|
|
41
|
+
*/
|
|
42
|
+
get clientInfo() {
|
|
43
|
+
return this.#clientInfo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get protocol version
|
|
48
|
+
*/
|
|
49
|
+
get protocolVersion() {
|
|
50
|
+
return MCPClient.PROTOCOL_VERSION;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if any servers are connected
|
|
55
|
+
*/
|
|
56
|
+
get isConnected() {
|
|
57
|
+
return this.#connections.size > 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get supported capabilities
|
|
62
|
+
*/
|
|
63
|
+
get capabilities() {
|
|
64
|
+
return {
|
|
65
|
+
tools: { listChanged: true },
|
|
66
|
+
resources: { subscribe: false, listChanged: true },
|
|
67
|
+
prompts: { listChanged: true },
|
|
68
|
+
sampling: {},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Connect to an MCP server
|
|
74
|
+
* @param {Object} server - Server configuration
|
|
75
|
+
* @param {string} server.uri - MCP server URI (mcp://host or https://host)
|
|
76
|
+
* @param {string} server.name - Human-readable server name
|
|
77
|
+
* @param {Object} server.auth - Optional authentication
|
|
78
|
+
*/
|
|
79
|
+
async connect(server) {
|
|
80
|
+
const { uri, name, auth } = server;
|
|
81
|
+
|
|
82
|
+
if (this.#connections.has(uri)) {
|
|
83
|
+
return this.#connections.get(uri);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const connection = new MCPConnection({
|
|
87
|
+
uri,
|
|
88
|
+
name,
|
|
89
|
+
auth,
|
|
90
|
+
timeout: this.#config.timeout,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await connection.initialize();
|
|
95
|
+
this.#connections.set(uri, connection);
|
|
96
|
+
this.emit('connected', { uri, name });
|
|
97
|
+
return connection;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.emit('error', { uri, error });
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Connect to multiple MCP servers
|
|
106
|
+
*/
|
|
107
|
+
async connectAll(servers) {
|
|
108
|
+
const results = await Promise.allSettled(
|
|
109
|
+
servers.map(server => this.connect(server))
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return results.map((result, i) => ({
|
|
113
|
+
server: servers[i],
|
|
114
|
+
status: result.status,
|
|
115
|
+
connection: result.status === 'fulfilled' ? result.value : null,
|
|
116
|
+
error: result.status === 'rejected' ? result.reason : null,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Disconnect from an MCP server
|
|
122
|
+
*/
|
|
123
|
+
async disconnect(uri) {
|
|
124
|
+
const connection = this.#connections.get(uri);
|
|
125
|
+
if (connection) {
|
|
126
|
+
await connection.close();
|
|
127
|
+
this.#connections.delete(uri);
|
|
128
|
+
this.emit('disconnected', { uri });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Disconnect from all servers
|
|
134
|
+
*/
|
|
135
|
+
async disconnectAll() {
|
|
136
|
+
const uris = Array.from(this.#connections.keys());
|
|
137
|
+
await Promise.all(uris.map(uri => this.disconnect(uri)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get all available tools from connected servers
|
|
142
|
+
*/
|
|
143
|
+
async listAllTools() {
|
|
144
|
+
const allTools = [];
|
|
145
|
+
|
|
146
|
+
for (const [uri, connection] of this.#connections) {
|
|
147
|
+
try {
|
|
148
|
+
const tools = await connection.listTools();
|
|
149
|
+
allTools.push(...tools.map(tool => ({
|
|
150
|
+
...tool,
|
|
151
|
+
serverUri: uri,
|
|
152
|
+
serverName: connection.name,
|
|
153
|
+
})));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.emit('error', { uri, error, operation: 'listTools' });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return allTools;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Call a tool on a specific server
|
|
164
|
+
*/
|
|
165
|
+
async callTool(serverUri, toolName, args) {
|
|
166
|
+
const connection = this.#connections.get(serverUri);
|
|
167
|
+
if (!connection) {
|
|
168
|
+
throw new Error(`Not connected to server: ${serverUri}`);
|
|
169
|
+
}
|
|
170
|
+
return connection.callTool(toolName, args);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get all available resources from connected servers
|
|
175
|
+
*/
|
|
176
|
+
async listAllResources() {
|
|
177
|
+
const allResources = [];
|
|
178
|
+
|
|
179
|
+
for (const [uri, connection] of this.#connections) {
|
|
180
|
+
try {
|
|
181
|
+
const resources = await connection.listResources();
|
|
182
|
+
allResources.push(...resources.map(resource => ({
|
|
183
|
+
...resource,
|
|
184
|
+
serverUri: uri,
|
|
185
|
+
serverName: connection.name,
|
|
186
|
+
})));
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.emit('error', { uri, error, operation: 'listResources' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return allResources;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read a resource from a specific server
|
|
197
|
+
*/
|
|
198
|
+
async readResource(serverUri, resourceUri) {
|
|
199
|
+
const connection = this.#connections.get(serverUri);
|
|
200
|
+
if (!connection) {
|
|
201
|
+
throw new Error(`Not connected to server: ${serverUri}`);
|
|
202
|
+
}
|
|
203
|
+
return connection.readResource(resourceUri);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Aggregate context from all connected servers
|
|
208
|
+
* Useful for providing rich context to intent parsing
|
|
209
|
+
*/
|
|
210
|
+
async aggregateContext(query = {}) {
|
|
211
|
+
const context = {
|
|
212
|
+
tools: [],
|
|
213
|
+
resources: [],
|
|
214
|
+
data: {},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Gather tools
|
|
218
|
+
context.tools = await this.listAllTools();
|
|
219
|
+
|
|
220
|
+
// Gather resources if requested
|
|
221
|
+
if (query.includeResources !== false) {
|
|
222
|
+
context.resources = await this.listAllResources();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Gather specific data if query provided
|
|
226
|
+
if (query.resourcePatterns) {
|
|
227
|
+
for (const pattern of query.resourcePatterns) {
|
|
228
|
+
const matches = context.resources.filter(r =>
|
|
229
|
+
r.uri.includes(pattern) || r.name?.includes(pattern)
|
|
230
|
+
);
|
|
231
|
+
for (const match of matches) {
|
|
232
|
+
try {
|
|
233
|
+
const data = await this.readResource(match.serverUri, match.uri);
|
|
234
|
+
context.data[match.uri] = data;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Skip resources that fail to read
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return context;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get connections() {
|
|
246
|
+
return Array.from(this.#connections.entries()).map(([uri, conn]) => ({
|
|
247
|
+
uri,
|
|
248
|
+
name: conn.name,
|
|
249
|
+
status: conn.status,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Individual MCP Server Connection
|
|
256
|
+
* Handles JSON-RPC 2.0 communication
|
|
257
|
+
*/
|
|
258
|
+
class MCPConnection extends EventEmitter {
|
|
259
|
+
#uri;
|
|
260
|
+
#name;
|
|
261
|
+
#auth;
|
|
262
|
+
#timeout;
|
|
263
|
+
#status = 'disconnected';
|
|
264
|
+
#capabilities = null;
|
|
265
|
+
#messageId = 0;
|
|
266
|
+
|
|
267
|
+
constructor({ uri, name, auth, timeout }) {
|
|
268
|
+
super();
|
|
269
|
+
this.#uri = uri;
|
|
270
|
+
this.#name = name || uri;
|
|
271
|
+
this.#auth = auth;
|
|
272
|
+
this.#timeout = timeout;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get name() {
|
|
276
|
+
return this.#name;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get status() {
|
|
280
|
+
return this.#status;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get capabilities() {
|
|
284
|
+
return this.#capabilities;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Initialize connection to MCP server
|
|
289
|
+
*/
|
|
290
|
+
async initialize() {
|
|
291
|
+
this.#status = 'connecting';
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// Send initialize request
|
|
295
|
+
const response = await this.#request('initialize', {
|
|
296
|
+
protocolVersion: '2024-11-05',
|
|
297
|
+
capabilities: {
|
|
298
|
+
tools: {},
|
|
299
|
+
resources: { subscribe: false },
|
|
300
|
+
},
|
|
301
|
+
clientInfo: {
|
|
302
|
+
name: 'aura-scout',
|
|
303
|
+
version: '0.1.0',
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
this.#capabilities = response.capabilities;
|
|
308
|
+
this.#status = 'connected';
|
|
309
|
+
|
|
310
|
+
// Send initialized notification
|
|
311
|
+
await this.#notify('notifications/initialized', {});
|
|
312
|
+
|
|
313
|
+
return response;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
this.#status = 'error';
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* List available tools
|
|
322
|
+
*/
|
|
323
|
+
async listTools() {
|
|
324
|
+
const response = await this.#request('tools/list', {});
|
|
325
|
+
return response.tools || [];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Call a tool
|
|
330
|
+
*/
|
|
331
|
+
async callTool(name, args) {
|
|
332
|
+
const response = await this.#request('tools/call', {
|
|
333
|
+
name,
|
|
334
|
+
arguments: args,
|
|
335
|
+
});
|
|
336
|
+
return response;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* List available resources
|
|
341
|
+
*/
|
|
342
|
+
async listResources() {
|
|
343
|
+
const response = await this.#request('resources/list', {});
|
|
344
|
+
return response.resources || [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Read a resource
|
|
349
|
+
*/
|
|
350
|
+
async readResource(uri) {
|
|
351
|
+
const response = await this.#request('resources/read', { uri });
|
|
352
|
+
return response.contents;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Close connection
|
|
357
|
+
*/
|
|
358
|
+
async close() {
|
|
359
|
+
this.#status = 'disconnected';
|
|
360
|
+
// Clean up any persistent connections
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Send JSON-RPC request
|
|
365
|
+
*/
|
|
366
|
+
async #request(method, params) {
|
|
367
|
+
const id = ++this.#messageId;
|
|
368
|
+
|
|
369
|
+
const message = {
|
|
370
|
+
jsonrpc: '2.0',
|
|
371
|
+
id,
|
|
372
|
+
method,
|
|
373
|
+
params,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
return this.#send(message);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Send JSON-RPC notification (no response expected)
|
|
381
|
+
*/
|
|
382
|
+
async #notify(method, params) {
|
|
383
|
+
const message = {
|
|
384
|
+
jsonrpc: '2.0',
|
|
385
|
+
method,
|
|
386
|
+
params,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return this.#send(message, false);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Send message to server
|
|
394
|
+
*/
|
|
395
|
+
async #send(message, expectResponse = true) {
|
|
396
|
+
const url = this.#uri.replace(/^mcp:\/\//, 'https://');
|
|
397
|
+
|
|
398
|
+
const headers = {
|
|
399
|
+
'Content-Type': 'application/json',
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
if (this.#auth?.type === 'bearer') {
|
|
403
|
+
headers['Authorization'] = `Bearer ${this.#auth.token}`;
|
|
404
|
+
} else if (this.#auth?.type === 'api-key') {
|
|
405
|
+
headers['X-API-Key'] = this.#auth.key;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const controller = new AbortController();
|
|
409
|
+
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const response = await fetch(url, {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
headers,
|
|
415
|
+
body: JSON.stringify(message),
|
|
416
|
+
signal: controller.signal,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
clearTimeout(timeoutId);
|
|
420
|
+
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
throw new Error(`MCP server error: ${response.status} ${response.statusText}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!expectResponse) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const result = await response.json();
|
|
430
|
+
|
|
431
|
+
if (result.error) {
|
|
432
|
+
throw new MCPError(result.error.code, result.error.message, result.error.data);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return result.result;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
clearTimeout(timeoutId);
|
|
438
|
+
if (error.name === 'AbortError') {
|
|
439
|
+
throw new Error(`MCP request timeout after ${this.#timeout}ms`);
|
|
440
|
+
}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* MCP Protocol Error
|
|
448
|
+
*/
|
|
449
|
+
export class MCPError extends Error {
|
|
450
|
+
constructor(code, message, data) {
|
|
451
|
+
super(message);
|
|
452
|
+
this.name = 'MCPError';
|
|
453
|
+
this.code = code;
|
|
454
|
+
this.data = data;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export default MCPClient;
|