@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.
@@ -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;