@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,419 @@
1
+ /**
2
+ * MCP Protocol Scenario Tests
3
+ *
4
+ * Real-world scenario tests for Model Context Protocol (MCP).
5
+ * These tests verify client initialization, server connection, and tool usage.
6
+ *
7
+ * Run:
8
+ * node --test src/tests/scenarios/mcp-scenarios.test.js
9
+ */
10
+
11
+ import { test, describe } from 'node:test';
12
+ import assert from 'node:assert';
13
+ import { MCPClient, MCPError } from '../../mcp/client.js';
14
+
15
+ // =============================================================================
16
+ // Scenario 1: Client Initialization
17
+ // =============================================================================
18
+
19
+ describe('Scenario: MCP Client Initialization', () => {
20
+ test('Creates client with default configuration', () => {
21
+ const client = new MCPClient();
22
+
23
+ assert.strictEqual(client.isConnected, false);
24
+ assert.strictEqual(client.protocolVersion, '2024-11-05');
25
+ assert.strictEqual(client.clientInfo.name, 'aura-scout');
26
+ assert.strictEqual(client.clientInfo.version, '1.0.0');
27
+ });
28
+
29
+ test('Creates client with custom configuration', () => {
30
+ const client = new MCPClient({
31
+ timeout: 60000,
32
+ clientInfo: {
33
+ name: 'custom-shopping-agent',
34
+ version: '2.0.0',
35
+ },
36
+ });
37
+
38
+ assert.strictEqual(client.clientInfo.name, 'custom-shopping-agent');
39
+ assert.strictEqual(client.clientInfo.version, '2.0.0');
40
+ });
41
+
42
+ test('Client exposes required capabilities', () => {
43
+ const client = new MCPClient();
44
+ const caps = client.capabilities;
45
+
46
+ assert.ok(caps.tools, 'Should have tools capability');
47
+ assert.ok(caps.resources, 'Should have resources capability');
48
+ assert.ok(caps.prompts, 'Should have prompts capability');
49
+ assert.ok(caps.sampling, 'Should have sampling capability');
50
+ });
51
+
52
+ test('Client is an EventEmitter', () => {
53
+ const client = new MCPClient();
54
+ let eventFired = false;
55
+
56
+ client.on('test', () => {
57
+ eventFired = true;
58
+ });
59
+
60
+ client.emit('test');
61
+ assert.strictEqual(eventFired, true);
62
+ });
63
+ });
64
+
65
+ // =============================================================================
66
+ // Scenario 2: Tool Discovery (Offline Mode)
67
+ // =============================================================================
68
+
69
+ describe('Scenario: Tool Discovery Without Servers', () => {
70
+ test('Returns empty tools list when disconnected', async () => {
71
+ const client = new MCPClient();
72
+ const tools = await client.listAllTools();
73
+
74
+ assert.ok(Array.isArray(tools));
75
+ assert.strictEqual(tools.length, 0);
76
+ });
77
+
78
+ test('Returns empty resources list when disconnected', async () => {
79
+ const client = new MCPClient();
80
+ const resources = await client.listAllResources();
81
+
82
+ assert.ok(Array.isArray(resources));
83
+ assert.strictEqual(resources.length, 0);
84
+ });
85
+
86
+ test('Context aggregation returns empty structure when disconnected', async () => {
87
+ const client = new MCPClient();
88
+ const context = await client.aggregateContext({});
89
+
90
+ assert.deepStrictEqual(context.tools, []);
91
+ assert.deepStrictEqual(context.resources, []);
92
+ assert.deepStrictEqual(context.data, {});
93
+ });
94
+ });
95
+
96
+ // =============================================================================
97
+ // Scenario 3: Server Connection Management
98
+ // =============================================================================
99
+
100
+ describe('Scenario: Server Connection Management', () => {
101
+ test('Tracks connection status', () => {
102
+ const client = new MCPClient();
103
+
104
+ assert.strictEqual(client.isConnected, false);
105
+ assert.deepStrictEqual(client.connections, []);
106
+ });
107
+
108
+ test('Disconnect is safe when not connected', async () => {
109
+ const client = new MCPClient();
110
+
111
+ // Should not throw
112
+ await client.disconnect('non-existent-server');
113
+ assert.strictEqual(client.isConnected, false);
114
+ });
115
+
116
+ test('DisconnectAll is safe with no connections', async () => {
117
+ const client = new MCPClient();
118
+
119
+ // Should not throw
120
+ await client.disconnectAll();
121
+ assert.strictEqual(client.isConnected, false);
122
+ });
123
+
124
+ test('Connection errors are handled gracefully', async () => {
125
+ const client = new MCPClient();
126
+
127
+ await assert.rejects(
128
+ async () => {
129
+ await client.connect({
130
+ uri: 'https://invalid.mcp-server.example.com/sse',
131
+ name: 'invalid-server',
132
+ });
133
+ },
134
+ /failed|error|ENOTFOUND/i
135
+ );
136
+ });
137
+ });
138
+
139
+ // =============================================================================
140
+ // Scenario 4: Tool Calls
141
+ // =============================================================================
142
+
143
+ describe('Scenario: Tool Calls', () => {
144
+ test('Tool call fails when server not connected', async () => {
145
+ const client = new MCPClient();
146
+
147
+ await assert.rejects(
148
+ async () => {
149
+ await client.callTool('https://mcp.example.com', 'search', { query: 'test' });
150
+ },
151
+ /not connected/i
152
+ );
153
+ });
154
+
155
+ test('Resource read fails when server not connected', async () => {
156
+ const client = new MCPClient();
157
+
158
+ await assert.rejects(
159
+ async () => {
160
+ await client.readResource('https://mcp.example.com', 'resource://test');
161
+ },
162
+ /not connected/i
163
+ );
164
+ });
165
+ });
166
+
167
+ // =============================================================================
168
+ // Scenario 5: Event Handling
169
+ // =============================================================================
170
+
171
+ describe('Scenario: Event Handling', () => {
172
+ test('Emits events for connection lifecycle', () => {
173
+ const client = new MCPClient();
174
+ const events = [];
175
+
176
+ client.on('connected', (data) => events.push({ type: 'connected', data }));
177
+ client.on('disconnected', (data) => events.push({ type: 'disconnected', data }));
178
+ client.on('error', (data) => events.push({ type: 'error', data }));
179
+
180
+ // Simulate events
181
+ client.emit('connected', { uri: 'test://server' });
182
+ client.emit('error', { error: new Error('test') });
183
+ client.emit('disconnected', { uri: 'test://server' });
184
+
185
+ assert.strictEqual(events.length, 3);
186
+ assert.strictEqual(events[0].type, 'connected');
187
+ assert.strictEqual(events[1].type, 'error');
188
+ assert.strictEqual(events[2].type, 'disconnected');
189
+ });
190
+
191
+ test('Supports multiple listeners per event', () => {
192
+ const client = new MCPClient();
193
+ let count = 0;
194
+
195
+ client.on('test-event', () => count++);
196
+ client.on('test-event', () => count++);
197
+ client.on('test-event', () => count++);
198
+
199
+ client.emit('test-event');
200
+ assert.strictEqual(count, 3);
201
+ });
202
+
203
+ test('Removes listeners with off/removeListener', () => {
204
+ const client = new MCPClient();
205
+ let count = 0;
206
+
207
+ const listener = () => count++;
208
+ client.on('test-event', listener);
209
+ client.emit('test-event');
210
+ assert.strictEqual(count, 1);
211
+
212
+ client.removeListener('test-event', listener);
213
+ client.emit('test-event');
214
+ assert.strictEqual(count, 1); // Still 1, listener removed
215
+ });
216
+ });
217
+
218
+ // =============================================================================
219
+ // Scenario 6: Context Aggregation
220
+ // =============================================================================
221
+
222
+ describe('Scenario: Context Aggregation', () => {
223
+ test('Aggregates context with query parameters', async () => {
224
+ const client = new MCPClient();
225
+
226
+ const context = await client.aggregateContext({
227
+ includeResources: true,
228
+ resourcePatterns: ['calendar://', 'file://'],
229
+ });
230
+
231
+ assert.ok('tools' in context);
232
+ assert.ok('resources' in context);
233
+ assert.ok('data' in context);
234
+ });
235
+
236
+ test('Handles includeResources: false', async () => {
237
+ const client = new MCPClient();
238
+
239
+ const context = await client.aggregateContext({
240
+ includeResources: false,
241
+ });
242
+
243
+ // Resources should still exist but be empty (no servers)
244
+ assert.ok('resources' in context);
245
+ });
246
+ });
247
+
248
+ // =============================================================================
249
+ // Scenario 7: MCP Error Handling
250
+ // =============================================================================
251
+
252
+ describe('Scenario: MCP Error Handling', () => {
253
+ test('MCPError has correct structure', () => {
254
+ const error = new MCPError(-32600, 'Invalid Request', { field: 'method' });
255
+
256
+ assert.strictEqual(error.name, 'MCPError');
257
+ assert.strictEqual(error.code, -32600);
258
+ assert.strictEqual(error.message, 'Invalid Request');
259
+ assert.deepStrictEqual(error.data, { field: 'method' });
260
+ });
261
+
262
+ test('MCPError is instance of Error', () => {
263
+ const error = new MCPError(-32600, 'Test');
264
+ assert.ok(error instanceof Error);
265
+ });
266
+
267
+ test('Standard JSON-RPC error codes', () => {
268
+ const errorCodes = {
269
+ PARSE_ERROR: -32700,
270
+ INVALID_REQUEST: -32600,
271
+ METHOD_NOT_FOUND: -32601,
272
+ INVALID_PARAMS: -32602,
273
+ INTERNAL_ERROR: -32603,
274
+ };
275
+
276
+ // Verify codes are in expected range
277
+ Object.values(errorCodes).forEach(code => {
278
+ assert.ok(code >= -32700 && code <= -32600);
279
+ });
280
+ });
281
+ });
282
+
283
+ // =============================================================================
284
+ // Scenario 8: Protocol Compliance
285
+ // =============================================================================
286
+
287
+ describe('Scenario: MCP Protocol Compliance', () => {
288
+ test('Uses correct protocol version', () => {
289
+ const client = new MCPClient();
290
+ assert.strictEqual(client.protocolVersion, '2024-11-05');
291
+ });
292
+
293
+ test('JSON-RPC 2.0 message format', () => {
294
+ // Test that we understand the expected format
295
+ const request = {
296
+ jsonrpc: '2.0',
297
+ id: 1,
298
+ method: 'tools/list',
299
+ params: {},
300
+ };
301
+
302
+ assert.strictEqual(request.jsonrpc, '2.0');
303
+ assert.ok(Number.isInteger(request.id));
304
+ assert.ok(typeof request.method === 'string');
305
+ assert.ok(typeof request.params === 'object');
306
+ });
307
+
308
+ test('Notification format (no id)', () => {
309
+ const notification = {
310
+ jsonrpc: '2.0',
311
+ method: 'notifications/initialized',
312
+ params: {},
313
+ };
314
+
315
+ assert.strictEqual(notification.jsonrpc, '2.0');
316
+ assert.ok(!('id' in notification), 'Notifications should not have id');
317
+ });
318
+
319
+ test('Response format with result', () => {
320
+ const response = {
321
+ jsonrpc: '2.0',
322
+ id: 1,
323
+ result: {
324
+ tools: [
325
+ { name: 'search', description: 'Search products' },
326
+ ],
327
+ },
328
+ };
329
+
330
+ assert.strictEqual(response.jsonrpc, '2.0');
331
+ assert.ok('result' in response);
332
+ assert.ok(!('error' in response), 'Success response should not have error');
333
+ });
334
+
335
+ test('Response format with error', () => {
336
+ const errorResponse = {
337
+ jsonrpc: '2.0',
338
+ id: 1,
339
+ error: {
340
+ code: -32601,
341
+ message: 'Method not found',
342
+ },
343
+ };
344
+
345
+ assert.strictEqual(errorResponse.jsonrpc, '2.0');
346
+ assert.ok('error' in errorResponse);
347
+ assert.ok(!('result' in errorResponse), 'Error response should not have result');
348
+ });
349
+ });
350
+
351
+ // =============================================================================
352
+ // Scenario 9: Multiple Server Support (Structure Test)
353
+ // =============================================================================
354
+
355
+ describe('Scenario: Multiple Server Architecture', () => {
356
+ test('Client tracks multiple connections', () => {
357
+ const client = new MCPClient();
358
+
359
+ // Test the structure supports multiple connections
360
+ const connections = client.connections;
361
+ assert.ok(Array.isArray(connections));
362
+ });
363
+
364
+ test('ConnectAll handles multiple servers', async () => {
365
+ const client = new MCPClient();
366
+
367
+ // This will fail (no servers), but tests the interface
368
+ const results = await client.connectAll([
369
+ { uri: 'https://server1.invalid', name: 'Server 1' },
370
+ { uri: 'https://server2.invalid', name: 'Server 2' },
371
+ ]);
372
+
373
+ assert.strictEqual(results.length, 2);
374
+ results.forEach((result, i) => {
375
+ assert.ok('status' in result);
376
+ assert.ok('server' in result);
377
+ });
378
+ });
379
+ });
380
+
381
+ // =============================================================================
382
+ // Scenario 10: AURA Integration Patterns
383
+ // =============================================================================
384
+
385
+ describe('Scenario: AURA Integration Patterns', () => {
386
+ test('Scout can use MCP for external context', async () => {
387
+ // This test documents the expected integration pattern
388
+ const client = new MCPClient({
389
+ clientInfo: {
390
+ name: 'aura-scout',
391
+ version: '1.0.0',
392
+ },
393
+ });
394
+
395
+ // Scout would connect to various context servers:
396
+ // - Calendar server for scheduling
397
+ // - CRM for customer data
398
+ // - Inventory systems
399
+
400
+ // Verify client is ready for this pattern
401
+ assert.strictEqual(client.clientInfo.name, 'aura-scout');
402
+ assert.ok(client.capabilities.tools);
403
+ assert.ok(client.capabilities.resources);
404
+ });
405
+
406
+ test('Context can be passed to intent parsing', async () => {
407
+ const client = new MCPClient();
408
+
409
+ // Aggregate context (empty in offline mode)
410
+ const context = await client.aggregateContext({
411
+ includeResources: true,
412
+ });
413
+
414
+ // Context has expected shape for intent enrichment
415
+ assert.ok('tools' in context, 'Should have tools for capability awareness');
416
+ assert.ok('resources' in context, 'Should have resources for context');
417
+ assert.ok('data' in context, 'Should have data for specific lookups');
418
+ });
419
+ });