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:
npx mcp-aegis initYour programmatic tests can then reference the generated aegis.config.json:
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:
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 newMCPClientinstance 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 statusconfig:object- Configuration used for connectionhandshakeCompleted: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:
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
resultwithisError: true.
// 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:
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.
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
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
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
// 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
// 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
// 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
// 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
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
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
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) orclient.clearStderr()(minimum) inbeforeEach()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