Testing MCP Servers
This guide covers how to test your MCP server before and after connecting it to Diosc.
Testing Levels
Level 1: Unit Testing
Test your tool handlers in isolation.
Testing Tool Logic
// tools/orders.test.ts
import { searchOrders, getOrder, updateOrderStatus } from './orders';
describe('searchOrders', () => {
const mockApi = jest.fn();
beforeEach(() => {
mockApi.mockReset();
});
it('builds correct API query from args', async () => {
mockApi.mockResolvedValue({ orders: [] });
await searchOrders(
{ status: 'pending', customer: 'John' },
{ 'Authorization': 'Bearer token' },
mockApi
);
expect(mockApi).toHaveBeenCalledWith(
'/orders?status=pending&customer=John',
{ 'Authorization': 'Bearer token' }
);
});
it('formats response correctly', async () => {
mockApi.mockResolvedValue({
orders: [{ id: '123', status: 'pending', total: 99.99 }]
});
const result = await searchOrders({}, {}, mockApi);
expect(result.content[0].text).toContain('123');
expect(result.content[0].text).toContain('pending');
});
it('handles empty results', async () => {
mockApi.mockResolvedValue({ orders: [] });
const result = await searchOrders({}, {}, mockApi);
expect(result.content[0].text).toContain('No orders found');
});
it('handles API errors', async () => {
mockApi.mockRejectedValue({ status: 500, message: 'Internal error' });
const result = await searchOrders({}, {}, mockApi);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('error');
});
});
Testing Auth Forwarding
describe('auth forwarding', () => {
it('forwards Authorization header', async () => {
const mockFetch = jest.fn().mockResolvedValue({ ok: true, json: () => ({}) });
global.fetch = mockFetch;
await callApi('/orders', {
'Authorization': 'Bearer user-token-123',
'X-Tenant-Id': 'acme'
});
expect(mockFetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer user-token-123',
'X-Tenant-Id': 'acme'
})
})
);
});
it('handles 401 responses', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 401,
text: () => 'Unauthorized'
});
const result = await callApi('/orders', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('session');
});
});
Level 2: Protocol Testing
Test MCP message handling.
Testing Tool Discovery
# Using curl
curl -X POST http://localhost:3000/messages \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' \
| jq '.result.tools'
Expected response:
{
"tools": [
{
"name": "search_orders",
"description": "Search for orders...",
"inputSchema": { ... }
}
]
}
Testing Tool Calls
# Valid call
curl -X POST http://localhost:3000/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-token" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_orders",
"arguments": {"status": "pending"}
}
}'
# Invalid tool
curl -X POST http://localhost:3000/messages \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "nonexistent_tool",
"arguments": {}
}
}'
Automated Protocol Tests
// protocol.test.ts
import request from 'supertest';
import app from './server';
describe('MCP Protocol', () => {
describe('tools/list', () => {
it('returns list of tools', async () => {
const response = await request(app)
.post('/messages')
.send({
jsonrpc: '2.0',
id: 1,
method: 'tools/list'
});
expect(response.body.result.tools).toBeInstanceOf(Array);
expect(response.body.result.tools.length).toBeGreaterThan(0);
// Check tool structure
const tool = response.body.result.tools[0];
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('inputSchema');
});
});
describe('tools/call', () => {
it('returns result for valid call', async () => {
const response = await request(app)
.post('/messages')
.set('Authorization', 'Bearer test-token')
.send({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'search_orders',
arguments: { status: 'pending' }
}
});
expect(response.body.result.content).toBeInstanceOf(Array);
expect(response.body.result.content[0].type).toBe('text');
});
it('returns error for unknown tool', async () => {
const response = await request(app)
.post('/messages')
.send({
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'unknown_tool',
arguments: {}
}
});
expect(response.body.error).toBeDefined();
expect(response.body.error.code).toBe(-32601);
});
});
});
Level 3: Integration Testing
Test your MCP server with Diosc Hub.
Using Diosc Admin Portal
- Add your MCP server in Admin Portal
- Click "Test Connection"
- Verify health check passes
- View available tools
Screenshot: Testing MCP connection in Admin Portal
Using Diosc CLI (if available)
# Test MCP server directly
diosc mcp test --url http://localhost:3000/sse
# List tools
diosc mcp tools --url http://localhost:3000/sse
# Call a tool
diosc mcp call search_orders --args '{"status":"pending"}' \
--url http://localhost:3000/sse \
--auth "Bearer test-token"
Simulating Diosc Requests
Create a test script that mimics Diosc Hub:
// integration-test.ts
import EventSource from 'eventsource';
async function testMcpServer(serverUrl: string) {
console.log('Connecting to SSE...');
const es = new EventSource(`${serverUrl}/sse`);
let messagesEndpoint: string;
return new Promise((resolve, reject) => {
es.addEventListener('endpoint', async (event) => {
messagesEndpoint = event.data;
console.log('Got endpoint:', messagesEndpoint);
// Test tools/list
console.log('\nTesting tools/list...');
const listResponse = await fetch(`${serverUrl}${messagesEndpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list'
})
});
if (!listResponse.ok) {
throw new Error(`tools/list failed: ${listResponse.status}`);
}
console.log('✓ tools/list succeeded');
// Test tool call
console.log('\nTesting tools/call...');
const callResponse = await fetch(`${serverUrl}${messagesEndpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'search_orders',
arguments: { status: 'pending' }
}
})
});
if (!callResponse.ok) {
throw new Error(`tools/call failed: ${callResponse.status}`);
}
console.log('✓ tools/call succeeded');
es.close();
resolve(true);
});
es.onerror = (error) => {
es.close();
reject(new Error('SSE connection failed'));
};
});
}
testMcpServer('http://localhost:3000')
.then(() => console.log('\n✓ All tests passed'))
.catch(err => console.error('\n✗ Test failed:', err.message));
Level 4: End-to-End Testing
Test the complete flow from user message to tool execution.
Manual E2E Testing
- Open your application with Diosc chat
- Type a message that should trigger your tool
- Verify the AI uses the correct tool
- Check the response
Example conversation:
User: "Show me all pending orders"
[AI reasoning: I should use search_orders with status=pending]
AI: "I found 3 pending orders:
- Order #12345: 2 items, $99.00
- Order #12346: 1 item, $49.00
- Order #12347: 5 items, $299.00"
Automated E2E Testing
// e2e.test.ts
import { DioscTestClient } from './test-utils';
describe('Orders Assistant E2E', () => {
let client: DioscTestClient;
beforeAll(async () => {
client = await DioscTestClient.connect({
hubUrl: process.env.DIOSC_HUB_URL,
apiKey: process.env.DIOSC_API_KEY,
auth: { Authorization: 'Bearer test-token' }
});
});
afterAll(async () => {
await client.disconnect();
});
it('searches orders when asked', async () => {
const response = await client.send('Show me pending orders');
// Verify tool was called
expect(response.toolCalls).toContainEqual(
expect.objectContaining({
name: 'search_orders',
arguments: { status: 'pending' }
})
);
// Verify response mentions orders
expect(response.content).toMatch(/order/i);
});
it('handles no results gracefully', async () => {
const response = await client.send(
'Find orders for customer NonexistentCustomer123'
);
expect(response.content).toMatch(/no orders found/i);
});
it('respects user permissions', async () => {
// Test with restricted user token
const restrictedClient = await DioscTestClient.connect({
hubUrl: process.env.DIOSC_HUB_URL,
apiKey: process.env.DIOSC_API_KEY,
auth: { Authorization: 'Bearer restricted-user-token' }
});
const response = await restrictedClient.send('Delete order #12345');
expect(response.content).toMatch(/permission|not allowed/i);
await restrictedClient.disconnect();
});
});
Health Check Testing
# Basic health check
curl http://localhost:3000/health
# Expected: {"status": "healthy", ...}
# Simulate unhealthy state (if your server supports it)
curl http://localhost:3000/health?simulate=unhealthy
# Expected: {"status": "unhealthy", ...}
Health Check Test Cases
describe('Health endpoint', () => {
it('returns healthy when all dependencies are up', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('healthy');
});
it('returns unhealthy when database is down', async () => {
mockDatabase.disconnect();
const response = await request(app).get('/health');
expect(response.status).toBe(503);
expect(response.body.status).toBe('unhealthy');
expect(response.body.error).toContain('database');
});
});
Checklist Before Production
- All unit tests pass
- Protocol tests pass (tools/list, tools/call)
- Health endpoint works
- Auth forwarding tested with real tokens
- Error responses are user-friendly
- Connection to Diosc Hub works
- E2E flow tested
- Performance under load tested
- Logging is appropriate (no sensitive data)
Next Steps
- Diosc Requirements - Server requirements
- Admin Portal: MCP Servers - Registering your server
- Troubleshooting - Common issues and solutions