MCP

Aegisv1

Programmatic Testing API

JavaScript/TypeScript MCP Server Testing

MCP Aegis provides a comprehensive JavaScript/TypeScript API for programmatic Model Context Protocol testing, enabling seamless integration with existing test suites and complex validation scenarios.

Getting Started

Initialize MCP Aegis in your project first:

bash
npx mcp-aegis init

Your programmatic tests can then reference the generated aegis.config.json:

javascript
import { createClient } from 'mcp-aegis';

const client = await createClient('./aegis.config.json');
await client.connect();

// List available tools
const tools = await client.listTools();
console.log('Available tools:', tools.map(t => t.name));

// Execute a tool
const result = await client.callTool('my_tool', { param: 'value' });
console.log('Result:', result.content[0].text);

// Clean up
await client.disconnect();

Alternative: Auto-Connect Helper

Instead of creating then connecting, use the connect() helper which returns a ready client:

javascript
import { connect } from 'mcp-aegis';

const client = await connect('./aegis.config.json'); // Already connected + handshake done
const tools = await client.listTools();
// ... use tools
await client.disconnect();

API Reference Overview

See the full API Reference for all methods and properties.

Main Entry Points

  • createClient(config): Creates a new MCPClient instance without connecting.
  • connect(config): Creates and automatically connects a client.

MCPClient Class Core Methods

  • async connect(): Start MCP server and perform handshake.
  • async disconnect(): Gracefully shutdown server connection.
  • async listTools(): Retrieve available tools from server.
  • async callTool(name, arguments): Execute a specific tool with arguments.
  • async sendMessage(jsonRpcMessage): Send raw JSON-RPC message to server.

Utility Methods

  • getStderr(): Retrieve current stderr buffer content.
  • clearStderr(): Clear stderr buffer.
  • clearAllBuffers(): Clear all buffers (stderr, stdout) and reset state.
  • isConnected(): Check if client is connected and handshake is completed.

Properties

  • connected: boolean - Connection status
  • config: object - Configuration used for connection
  • handshakeCompleted: boolean - MCP handshake status

Critical: Preventing Test Interference

The #1 cause of flaky programmatic tests is output/buffer state leaking between tests.

  • stderr buffer: lingering error/debug lines
  • stdout partial frames: incomplete JSON fragments still queued
  • ready/state flags: previous handshake state influencing new tests
  • pending message handlers: unresolved reads consuming the next test's response

Always include this pattern:

javascript
beforeEach(() => {
  client.clearAllBuffers();
});

Without this you'll see: isolated passes, suite failures, mismatched JSON-RPC ids, unexpected stderr.

Transport vs Logical Errors

Two distinct failure surfaces:

  • Transport / JSON-RPC error: Server responds with top-level error. callTool() throws.
  • Logical tool error: Server returns normal result with isError: true.
javascript
// Transport error
await assert.rejects(() => client.callTool('nonexistent_tool', {}), /Failed to call tool/);

// Logical error
const r = await client.callTool('validate_input', { value: '' });
if (r.isError) console.log(r.content[0].text);

Auto Config Resolution

Omit the path when your config is the default aegis.config.json:

javascript
import { connect } from 'mcp-aegis';

const client = await connect();
console.log(client.isConnected());
await client.disconnect();

Testing Frameworks Integration

MCP Aegis integrates seamlessly with Node.js built-in test runner, Jest, Mocha, and more.

javascript
import { test, describe, before, after, beforeEach } from 'node:test';
import { strict as assert } from 'node:assert';
import { createClient } from 'mcp-aegis';

describe('MCP Server Tests', () => {
  let client;

  before(async () => {
    client = await createClient('./aegis.config.json');
    await client.connect();
  });

  after(async () => {
    if (client && client.connected) {
                                  </div>
      await client.disconnect();
    }
  });

  beforeEach(() => {
    // CRITICAL: Prevents buffer leaking between tests
    client.clearAllBuffers();
  });

  test('should list available tools', async () => {
    const tools = await client.listTools();
    assert.ok(Array.isArray(tools), 'Tools should be an array');
    assert.ok(tools.length > 0, 'Should have at least one tool');
  });

  test('should execute a tool correctly', async () => {
    const result = await client.callTool('hello', { name: 'Node.js' });
    assert.ok(result.content[0].text.includes('Hello, Node.js'));
    assert.strictEqual(result.isError, undefined, 'Should not be an error');
  });
});

Run tests with: node --test tests/mcp.test.js

Jest Integration

javascript
import { createClient } from 'mcp-aegis';

describe('MCP Server Integration', () => {
  let client;

  beforeAll(async () => {
    client = await createClient('./aegis.config.json');
    await client.connect();
  }, 10000); // 10 second timeout for server startup

  afterAll(async () => {
    if (client?.connected) {
      await client.disconnect();
    }
  });

  beforeEach(() => {
    // CRITICAL: Prevents buffer leaking between tests
    client.clearAllBuffers();
  });

  test('should validate tool schemas', async () => {
    const tools = await client.listTools();
    
    tools.forEach(tool => {
      expect(tool.name).toMatch(/^[a-z][a-z0-9_]*$/); // snake_case
      expect(tool.description).toBeTruthy();
      expect(tool.inputSchema).toHaveProperty('type', 'object');
    });
  });

  test('should handle tool execution errors', async () => {
    await expect(
      client.callTool('nonexistent_tool', {})
    ).rejects.toThrow(/Failed to call tool/);
  });
});

Mocha Integration

javascript
import { expect } from 'chai';
import { createClient } from 'mcp-aegis';

describe('MCP Server Tests', function() {
  let client;

  before(async function() {
    this.timeout(10000);
    client = await createClient('./aegis.config.json');
    await client.connect();
  });

  after(async function() {
    if (client?.connected) {
      await client.disconnect();
    }
  });

  beforeEach(function() {
    // CRITICAL: Prevents buffer leaking between tests
    client.clearAllBuffers();
  });

  it('should perform tool operations', async function() {
    const result = await client.callTool('calculator', {
      operation: 'add',
      a: 15,
      b: 27
    });

    expect(result.content).to.be.an('array');
    expect(result.content[0].text).to.include('42');
    expect(result.isError).to.be.undefined;
  });
});

Detailed API Methods

Connection Management

javascript
// Create client without connecting
const client = await createClient('./config.json');
console.log('Connected:', client.connected); // false

// Connect and perform handshake
await client.connect();
console.log('Connected:', client.connected); // true
console.log('Handshake:', client.handshakeCompleted); // true

// Check connection status
const isReady = client.isConnected();
console.log('Client ready:', isReady);

// Graceful disconnect
await client.disconnect();

Tool Operations

javascript
// List all available tools
const tools = await client.listTools();
tools.forEach(tool => {
  console.log(`Tool: ${tool.name}`);
  console.log(`Description: ${tool.description}`);
  console.log(`Schema:`, tool.inputSchema);
});

// Execute a tool
const result = await client.callTool('calculator', {
  operation: 'add',
  a: 15,
  b: 27
});

console.log('Content:', result.content);
console.log('Error:', result.isError);

Stderr Management

javascript
// Clear buffers before operation (recommended)
client.clearAllBuffers();

// Perform operation
await client.callTool('my_tool', {});

// Check for stderr output
const stderr = client.getStderr();
if (stderr.trim()) {
  console.warn('Stderr output:', stderr);
}

Raw JSON-RPC Messaging

javascript
// Send custom JSON-RPC message
const response = await client.sendMessage({
  jsonrpc: "2.0",
  id: "custom-1",
  method: "tools/list",
  params: {}
});

console.log('Raw response:', response);

Advanced Patterns

Dynamic Test Generation

javascript
describe('Generated Tool Tests', () => {
  let client;
  let tools;

  before(async () => {
    client = await createClient('./config.json');
    await client.connect();
    tools = await client.listTools();
  });

  after(async () => {
    await client?.disconnect();
  });

  beforeEach(() => {
    // CRITICAL: Prevents buffer leaking between tests
    client.clearAllBuffers();
  });

  // Dynamically generate tests for each tool
  tools?.forEach(tool => {
    test(`should execute ${tool.name} successfully`, async () => {
      // Generate basic test arguments based on schema
      const args = generateTestArgs(tool.inputSchema);
      
      const result = await client.callTool(tool.name, args);
      
      assert.ok(result.content, `${tool.name} should return content`);
      assert.ok(!result.isError, `${tool.name} should not error with valid args`);
    });
  });
});

function generateTestArgs(schema) {
  const args = {};
  
  if (schema?.properties) {
    for (const [key, prop] of Object.entries(schema.properties)) {
      if (prop.type === 'string') {
        args[key] = 'test_value';
      } else if (prop.type === 'number') {
        args[key] = 42;
      } else if (prop.type === 'boolean') {
        args[key] = true;
      }
    }
  }
  
  return args;
}

Performance Testing

javascript
describe('Performance Tests', () => {
  let client;

  before(async () => {
    client = await createClient('./config.json');
    await client.connect();
  });

  after(async () => {
    await client?.disconnect();
  });

  beforeEach(() => {
    // CRITICAL: Prevents buffer leaking between tests
    client.clearAllBuffers();
  });

  test('should handle concurrent tool calls', async () => {
    const startTime = Date.now();
    
    // Execute 10 concurrent tool calls
    const promises = Array.from({ length: 10 }, (_, i) => 
      client.callTool('calculator', { operation: 'add', a: i, b: 1 })
    );
    
    const results = await Promise.all(promises);
    const duration = Date.now() - startTime;
    
    assert.equal(results.length, 10);
    assert.ok(duration < 5000, `Should complete within 5 seconds, took ${duration}ms`);
    
    results.forEach((result, i) => {
      assert.ok(result.content[0].text.includes(`${i + 1}`));
    });
  });
});

Note: The startupTimeout in config only governs initial server readiness. Performance assertions like duration < 5000 are application-level expectations—you may tune them per environment (CI vs local).

Error Handling Patterns

javascript
describe('Error Handling', () => {
  let client;

  before(async () => {
    client = await createClient('./config.json');
    await client.connect();
  });

  after(async () => {
    await client?.disconnect();
  });

  beforeEach(() => {
    // CRITICAL: Prevents buffer leaking between tests
    client.clearAllBuffers();
  });

  test('should handle connection errors gracefully', async () => {
    // Disconnect client
    await client.disconnect();
    
    // Attempt to use disconnected client
    await assert.rejects(
      async () => await client.listTools(),
      /Client not connected/
    );
  });

  test('should handle tool execution errors', async () => {
    const result = await client.callTool('invalid_tool', {});
    
    assert.strictEqual(result.isError, true);
    assert.ok(result.content[0].text.includes('Unknown tool'));
  });

  test('should handle malformed arguments', async () => {
    try {
      await client.callTool('calculator', {
        operation: 'invalid_op',
        a: 'not_a_number',
        b: null
      });
    } catch (error) {
      assert.ok(error.message.includes('Invalid arguments'));
    }
  });
});

Best Practices

  • CRITICAL: Always clear buffers in beforeEach: Use client.clearAllBuffers() (recommended) or client.clearStderr() (minimum) in beforeEach() hooks to prevent buffer leaking between tests
  • Always use before/after hooks: Ensure proper setup and cleanup
  • Check connection status: Use client.isConnected() before operations
  • Handle timeouts: Set appropriate timeouts for server startup
  • Test both success and error scenarios: Validate error handling
  • Use stderr monitoring: Clear and check stderr for unexpected output
  • Validate tool schemas: Ensure tools have proper schema definitions
  • Test concurrent operations: Verify server can handle multiple requests
  • Generate dynamic tests: Create tests based on available tools
  • Monitor performance: Include timing assertions for critical operations