mcp-local-dev
by txbm
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { McpServer } from '../../server/mcp-server.js';
import { SummarizationService } from '../../services/summarization.js';
import { ErrorCode, McpError, ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { SummarizationModel } from '../../types/models.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import path from 'path';
// Simple mock model for testing
class MockModel implements SummarizationModel {
async initialize(): Promise<void> {}
async summarize(content: string, type: string): Promise<string> {
// For directory listings, return the actual content
if (type === 'directory listing') {
return content;
}
return `Summarized ${type}`;
}
async cleanup(): Promise<void> {}
}
describe('McpServer', () => {
let server: McpServer;
let summarizationService: SummarizationService;
let mockServer: {
handlers: Map<string, Function>;
setRequestHandler: (schema: any, handler: Function) => void;
};
beforeEach(async () => {
// Setup mock server
mockServer = {
handlers: new Map(),
setRequestHandler: function(schema: any, handler: Function) {
const method = schema === ListToolsRequestSchema ? 'list_tools' :
schema === CallToolRequestSchema ? 'call_tool' :
'unknown';
this.handlers.set(method, handler);
}
};
// Mock Server constructor
jest.spyOn(Server.prototype, 'setRequestHandler')
.mockImplementation((schema, handler) => mockServer.setRequestHandler(schema, handler));
jest.spyOn(Server.prototype, 'connect').mockResolvedValue();
jest.spyOn(Server.prototype, 'close').mockResolvedValue();
// Create services
summarizationService = new SummarizationService(new MockModel(), {
model: { apiKey: 'test-key' },
charThreshold: 100,
cacheMaxAge: 1000
});
server = new McpServer(summarizationService);
await server.start(); // This will register the handlers
});
afterEach(async () => {
await server.cleanup();
});
const getHandler = (name: string): Function => {
const handler = mockServer.handlers.get(name);
if (!handler) {
throw new Error(`Handler not found: ${name}`);
}
return handler;
};
describe('Tool Registration', () => {
it('should register all tools on server initialization', async () => {
const response = await getHandler('list_tools')({ method: 'list_tools' }, {});
expect(response.tools).toHaveLength(5);
expect(response.tools.map((t: { name: string }) => t.name)).toEqual([
'summarize_command',
'summarize_files',
'summarize_directory',
'summarize_text',
'get_full_content'
]);
});
});
describe('Tool Execution', () => {
const callTool = async (name: string, args: any) => {
return getHandler('call_tool')({
method: 'call_tool',
params: { name, arguments: args }
}, {});
};
describe('summarize_command', () => {
it('should execute and return command output', async () => {
const response = await callTool('summarize_command', {
command: 'echo "Hello, World!"'
});
expect(response.content[0].text).toBe('Hello, World!');
});
it('should include stderr in output when present', async () => {
const response = await callTool('summarize_command', {
command: 'echo "output" && echo "error" >&2'
});
expect(response.content[0].text).toBe('output\nError: error');
});
});
describe('summarize_files', () => {
const testFilesDir = path.join(process.cwd(), 'src', '__tests__', 'test-files');
it('should read and return file contents', async () => {
const response = await callTool('summarize_files', {
paths: ['test1.txt'],
cwd: testFilesDir
});
expect(response.content[0].text).toContain('This is test file 1');
});
it('should handle multiple files', async () => {
const response = await callTool('summarize_files', {
paths: [
'test1.txt',
'test2.txt'
],
cwd: testFilesDir
});
expect(response.content[0].text).toContain('This is test file 1');
expect(response.content[0].text).toContain('This is test file 2');
});
});
describe('summarize_directory', () => {
const testFilesDir = path.join(process.cwd(), 'src', '__tests__', 'test-files');
it('should list directory contents', async () => {
const response = await callTool('summarize_directory', {
cwd: testFilesDir,
path: '.'
});
// Check for the presence of both test files in the output
const text = response.content[0].text;
expect(text).toContain('test1.txt');
expect(text).toContain('test2.txt');
// Verify it's a proper directory listing
expect(text).toMatch(/Summary \(full content ID: [\w-]+\):/);
});
});
describe('summarize_text', () => {
it('should summarize long text', async () => {
const longText = 'a'.repeat(200);
const response = await callTool('summarize_text', {
content: longText,
type: 'test'
});
expect(response.content[0].text).toContain('Summarized test');
});
it('should return original text if short', async () => {
const shortText = 'short text';
const response = await callTool('summarize_text', {
content: shortText,
type: 'test'
});
expect(response.content[0].text).toBe(shortText);
});
});
describe('get_full_content', () => {
it('should throw error for invalid ID', async () => {
await expect(callTool('get_full_content', {
id: 'invalid'
})).rejects.toThrow('Content not found or expired');
});
});
});
describe('Error Handling', () => {
const callTool = async (name: string, args: any) => {
return getHandler('call_tool')({
method: 'call_tool',
params: { name, arguments: args }
}, {});
};
it('should handle missing tool name', async () => {
await expect(getHandler('call_tool')({
method: 'call_tool',
params: { arguments: {} }
}, {})).rejects.toThrow('Missing tool name');
});
it('should handle missing arguments', async () => {
await expect(getHandler('call_tool')({
method: 'call_tool',
params: { name: 'summarize_text' }
}, {})).rejects.toThrow('Missing arguments');
});
it('should handle unknown tool', async () => {
await expect(callTool('unknown_tool', {}))
.rejects.toThrow('Unknown tool: unknown_tool');
});
it('should handle invalid arguments', async () => {
await expect(callTool('summarize_text', { invalid: 'args' }))
.rejects.toThrow('Invalid arguments for summarize_text');
});
it('should handle command execution errors', async () => {
await expect(callTool('summarize_command', {
command: 'nonexistentcommand'
})).rejects.toThrow('Error in summarize_command');
});
it('should handle file read errors', async () => {
await expect(callTool('summarize_files', {
paths: ['nonexistent.txt'],
cwd: process.cwd()
})).rejects.toThrow('Error in summarize_files');
});
it('should require cwd parameter for summarize_files', async () => {
await expect(callTool('summarize_files', {
paths: ['test1.txt']
})).rejects.toThrow('Invalid arguments for summarize_files');
});
it('should require cwd parameter for summarize_directory', async () => {
await expect(callTool('summarize_directory', {
path: '.'
})).rejects.toThrow('Invalid arguments for summarize_directory');
});
it('should handle invalid cwd path', async () => {
await expect(callTool('summarize_files', {
paths: ['test1.txt'],
cwd: '/nonexistent/directory'
})).rejects.toThrow('Error in summarize_files');
await expect(callTool('summarize_directory', {
path: '.',
cwd: '/nonexistent/directory'
})).rejects.toThrow('Error in summarize_directory');
});
});
});