Skip to main content

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

  1. Add your MCP server in Admin Portal
  2. Click "Test Connection"
  3. Verify health check passes
  4. 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

  1. Open your application with Diosc chat
  2. Type a message that should trigger your tool
  3. Verify the AI uses the correct tool
  4. 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