import { RedmineMcpServer } from '../server.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import axios from 'axios';
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import {
ListToolsResultSchema,
CallToolResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
InitializeResultSchema
} from '@modelcontextprotocol/sdk/types.js';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('Real MCP Client Integration Tests (TDD)', () => {
let server: RedmineMcpServer;
let client: Client<any, any, any> | null = null;
let transport: Transport | null = null;
beforeAll(async () => {
// Setup mock axios for Redmine API calls
const mockAxiosInstance = {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
interceptors: {
response: {
use: jest.fn()
}
}
};
mockedAxios.create.mockReturnValue(mockAxiosInstance as any);
});
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(async () => {
if (client) {
try {
await client.close();
} catch (e) {
// Ignore close errors in tests
}
client = null;
}
if (transport) {
try {
await transport.close();
} catch (e) {
// Ignore close errors in tests
}
transport = null;
}
});
describe('MCP Protocol Communication (Red Phase - Should Fail Initially)', () => {
it('should establish MCP connection with real client', async () => {
// This test should fail initially because server startup should fail due to missing real config
await expect(async () => {
transport = new StdioClientTransport({
command: 'node',
args: ['dist/index.js'],
env: {
REDMINE_BASE_URL: 'https://invalid-test-redmine-server.example.com',
REDMINE_API_KEY: 'invalid-test-key-12345678'
}
});
client = new Client({
name: 'test-mcp-client',
version: '1.0.0'
}, {
capabilities: {}
});
await client.connect(transport);
// Try to make an actual request that should fail
const toolsRequest = {
method: 'tools/list',
params: {}
};
const tools = await client.request(toolsRequest, ListToolsResultSchema);
// If we get here, the test should fail because we expect connection to fail
expect(tools.tools).toHaveLength(8);
}).rejects.toThrow(); // Should fail - connection should not succeed with invalid config
}, 15000); // Increase timeout for subprocess startup
it('should list available tools through MCP protocol', async () => {
// This test should fail initially - no connected client
await expect(async () => {
// Assuming we have a connected client (we don't yet)
const toolsRequest = {
method: 'tools/list',
params: {}
};
const tools = await client!.request(toolsRequest, ListToolsResultSchema);
expect(tools.tools).toHaveLength(8);
expect(tools.tools.map(t => t.name)).toEqual([
'list-issues',
'create-issue',
'get-issue',
'update-issue',
'list-projects',
'get-project',
'list-users',
'get-user'
]);
}).rejects.toThrow(); // Should fail - no client connected
});
it('should execute tools through MCP protocol', async () => {
// This test should fail initially - no client connected
await expect(async () => {
const toolRequest = {
method: 'tools/call',
params: {
name: 'list-issues',
arguments: { project_id: 1 }
}
};
const result = await client!.request(toolRequest, CallToolResultSchema);
expect(result.content).toBeDefined();
expect(Array.isArray(result.content)).toBe(true);
expect(result.content[0]).toHaveProperty('type', 'text');
expect(result.content[0]).toHaveProperty('text');
expect(result.content[0].text).toContain('Test Issue via MCP');
}).rejects.toThrow(); // Should fail - no client connected
});
it('should handle MCP protocol errors gracefully', async () => {
// This test should fail initially - no client connected
const toolRequest = {
method: 'tools/call',
params: {
name: 'non-existent-tool',
arguments: {}
}
};
await expect(client!.request(toolRequest, CallToolResultSchema)).rejects.toThrow(); // Should fail - no client
});
it('should support MCP resource listing', async () => {
// This test should fail initially - no client connected and no resources implemented
const resourcesRequest = {
method: 'resources/list',
params: {}
};
await expect(client!.request(resourcesRequest, ListResourcesResultSchema)).rejects.toThrow(); // Should fail - no client
});
it('should support MCP prompt templates', async () => {
// This test should fail initially - we haven't implemented prompts
expect(async () => {
const promptsRequest = {
method: 'prompts/list',
params: {}
};
const prompts = await client!.request(promptsRequest, ListPromptsResultSchema);
expect(prompts.prompts).toBeDefined();
}).rejects.toThrow(); // Should fail - no prompts implemented
});
});
describe('Real Redmine API Integration through MCP', () => {
it('should handle real Redmine API responses through MCP client', async () => {
// This test should fail initially
expect(async () => {
// Mock real Redmine API response structure
// Note: This test is expected to fail since server is not initialized in this context
const mockAxiosInstance = (global as any).mockAxiosInstance;
if (mockAxiosInstance) {
jest.spyOn(mockAxiosInstance, 'get').mockResolvedValue({
data: {
issues: [
{
id: 123,
project: { id: 1, name: 'Test Project' },
tracker: { id: 1, name: 'Bug' },
status: { id: 1, name: 'New' },
priority: { id: 2, name: 'Normal' },
author: { id: 1, name: 'Test User' },
subject: 'Real Redmine Issue',
description: 'This is a real issue from Redmine',
created_on: '2025-01-01T00:00:00Z',
updated_on: '2025-01-01T00:00:00Z'
}
],
total_count: 1,
offset: 0,
limit: 25
}
});
}
const toolRequest = {
method: 'tools/call',
params: {
name: 'list-issues',
arguments: {
project_id: 1,
limit: 25,
offset: 0
}
}
};
const result = await client!.request(toolRequest, CallToolResultSchema);
const responseData = JSON.parse((result.content[0] as any).text);
expect(responseData.issues[0].subject).toBe('Real Redmine Issue');
expect(responseData.issues[0].project.name).toBe('Test Project');
}).rejects.toThrow(); // Should fail - no client connected
});
it('should handle Redmine API authentication through MCP', async () => {
// This test should fail initially
expect(async () => {
// Test with invalid API key
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/index.js'],
env: {
REDMINE_BASE_URL: 'https://test.redmine.org',
REDMINE_API_KEY: 'invalid-key'
}
});
client = new Client({
name: 'test-auth-client',
version: '1.0.0'
}, {
capabilities: {}
});
await client.connect(transport);
// This should trigger authentication error
const toolRequest = {
method: 'tools/call',
params: {
name: 'list-issues',
arguments: {}
}
};
await client.request(toolRequest, CallToolResultSchema);
}).rejects.toThrow('Authentication failed'); // Should fail - no proper setup
});
});
describe('MCP Protocol Compliance in Real Environment', () => {
it('should follow MCP protocol specifications for tool calls', async () => {
// This test should fail initially
expect(async () => {
const toolCall = {
name: 'create-issue',
arguments: {
project_id: 1,
subject: 'MCP Protocol Test Issue',
description: 'Created via MCP protocol'
}
};
const toolRequest = {
method: 'tools/call',
params: toolCall
};
const result = await client!.request(toolRequest, CallToolResultSchema);
// Verify MCP protocol compliance
expect(result).toHaveProperty('content');
expect(Array.isArray(result.content)).toBe(true);
expect(result.content[0]).toHaveProperty('type');
expect(result.content[0]).toHaveProperty('text');
expect(result.isError).toBeFalsy();
}).rejects.toThrow(); // Should fail - no client setup
});
it('should support MCP protocol versioning', async () => {
// This test should fail initially
expect(async () => {
const initializeRequest = {
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: {
name: 'test-client',
version: '1.0.0'
}
}
};
const serverInfo = await client!.request(initializeRequest, InitializeResultSchema);
expect((serverInfo as any).serverInfo.name).toBe('redmine-mcp-server');
expect((serverInfo as any).serverInfo.version).toBe('1.0.0');
expect((serverInfo as any).protocolVersion).toBeDefined();
}).rejects.toThrow(); // Should fail - no client setup
});
it('should handle MCP protocol capabilities negotiation', async () => {
// This test should fail initially
expect(async () => {
const initializeRequest = {
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: {
name: 'test-client',
version: '1.0.0'
}
}
};
const initResult = await client!.request(initializeRequest, InitializeResultSchema);
expect((initResult as any).capabilities.tools).toBeDefined();
expect((initResult as any).capabilities.resources).toBeDefined();
expect((initResult as any).capabilities.prompts).toBeDefined();
}).rejects.toThrow(); // Should fail - no client setup
});
});
describe('Performance Testing with Real MCP Client', () => {
it('should handle concurrent MCP tool calls efficiently', async () => {
// This test should fail initially
expect(async () => {
const promises = Array.from({ length: 10 }, (_, i) => {
const toolRequest = {
method: 'tools/call',
params: {
name: 'list-issues',
arguments: { limit: 5, offset: i * 5 }
}
};
return client!.request(toolRequest, CallToolResultSchema);
});
const startTime = performance.now();
const results = await Promise.all(promises);
const endTime = performance.now();
expect(results).toHaveLength(10);
expect(endTime - startTime).toBeLessThan(5000); // Should complete in 5 seconds
results.forEach(result => {
expect(result.content).toBeDefined();
expect(result.content[0].type).toBe('text');
});
}).rejects.toThrow(); // Should fail - no client setup
});
it('should maintain MCP connection stability under load', async () => {
// This test should fail initially
expect(async () => {
// Rapid sequential calls
for (let i = 0; i < 50; i++) {
const toolRequest = {
method: 'tools/call',
params: {
name: 'list-projects',
arguments: {}
}
};
const result = await client!.request(toolRequest, CallToolResultSchema);
expect(result.content).toBeDefined();
}
// Verify connection is still alive by making another request
const statusRequest = {
method: 'tools/list',
params: {}
};
const statusResult = await client!.request(statusRequest, ListToolsResultSchema);
expect(statusResult).toBeDefined();
}).rejects.toThrow(); // Should fail - no client setup
});
});
});