npmplus-mcp-server
Version:
Production-ready MCP server for intelligent JavaScript package management. Works with Claude, Windsurf, Cursor, VS Code, and any MCP-compatible AI editor.
508 lines (418 loc) • 15.5 kB
text/typescript
/**
* Production Integration Tests for MCP Server
*
* Tests all MCP operations through the Netlify function endpoints to ensure
* production functionality works correctly. These tests simulate real-world usage.
*/
import { describe, it, expect } from '@jest/globals';
// Import the actual Netlify function
const npmPlusMcp = require('../../netlify/functions/npmplus-mcp.cjs');
describe('MCP Production Integration Tests', () => {
const createEvent = (method: string, body?: any) => ({
httpMethod: method,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'test-client'
},
body: body ? JSON.stringify(body) : null,
queryStringParameters: null,
path: '/npmplus-mcp',
isBase64Encoded: false
});
const createContext = () => ({
functionName: 'npmplus-mcp',
functionVersion: '1',
invokedFunctionArn: 'test',
memoryLimitInMB: '128',
awsRequestId: 'test-request-id',
logGroupName: 'test-log-group',
logStreamName: 'test-stream',
identity: null,
clientContext: null
});
describe('Server Capabilities', () => {
it('should handle initialize request correctly', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'test-client', version: '1.0.0' }
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
expect(response.result.capabilities).toBeDefined();
expect(response.result.serverInfo).toBeDefined();
expect(response.result.serverInfo.name).toBe('npmplus-mcp');
expect(response.result.serverInfo.version).toBe('12.0.19');
});
it('should list all available tools', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 2,
method: 'tools/list',
params: {}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
expect(response.result.tools).toBeDefined();
expect(Array.isArray(response.result.tools)).toBe(true);
// Check that all expected tools are present
const toolNames = response.result.tools.map((tool: any) => tool.name);
const expectedTools = [
'search_packages',
'install_packages',
'update_packages',
'remove_packages',
'check_outdated',
'dependency_tree',
'check_bundle_size',
'analyze_dependencies',
'download_stats',
'audit_dependencies',
'check_vulnerability',
'list_licenses',
'check_license',
'clean_cache',
'package_info'
];
expectedTools.forEach(toolName => {
expect(toolNames).toContain(toolName);
});
});
});
describe('Search Operations', () => {
it('should search for packages successfully', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'search_packages',
arguments: {
query: 'lodash',
limit: 5
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
expect(response.result.content).toBeDefined();
expect(Array.isArray(response.result.content)).toBe(true);
expect(response.result.content.length).toBeGreaterThan(0);
const textContent = response.result.content[0].text;
expect(textContent).toContain('Search results');
}, 15000); // Allow longer timeout for network requests
it('should handle empty search results', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 4,
method: 'tools/call',
params: {
name: 'search_packages',
arguments: {
query: 'thispackagereallydoesnotexist12345',
limit: 5
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
const textContent = response.result.content[0].text;
expect(textContent).toContain('No packages found');
}, 15000);
});
describe('Package Information', () => {
it('should get package information successfully', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 5,
method: 'tools/call',
params: {
name: 'package_info',
arguments: {
packageName: 'lodash'
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
const textContent = response.result.content[0].text;
expect(textContent).toContain('Package Information');
expect(textContent).toContain('lodash');
expect(textContent).toContain('Version:');
expect(textContent).toContain('License:');
}, 15000);
it('should handle non-existent package gracefully', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 6,
method: 'tools/call',
params: {
name: 'package_info',
arguments: {
packageName: 'this-package-absolutely-does-not-exist-12345'
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
expect(response.result.isError).toBe(true);
const textContent = response.result.content[0].text;
expect(textContent).toContain('not found');
}, 15000);
});
describe('Bundle Size Analysis', () => {
it('should analyze bundle size successfully', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 7,
method: 'tools/call',
params: {
name: 'check_bundle_size',
arguments: {
packageName: 'lodash'
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
const textContent = response.result.content[0].text;
// Handle both successful analysis and API error cases
if (textContent.includes('Error: Failed to analyze bundle size')) {
// External API failure - this is expected sometimes
expect(textContent).toContain('Failed to analyze bundle size');
// The error message might not contain the package name
console.log('Bundle size API temporarily unavailable - test passed with graceful error handling');
} else {
// Successful analysis
expect(textContent).toContain('Bundle Size Analysis');
expect(textContent).toContain('lodash');
}
}, 30000);
});
describe('Download Statistics', () => {
it('should get download statistics successfully', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 8,
method: 'tools/call',
params: {
name: 'download_stats',
arguments: {
packageName: 'lodash',
period: 'last-week'
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
const textContent = response.result.content[0].text;
expect(textContent).toContain('Download Statistics');
expect(textContent).toContain('lodash');
expect(textContent).toContain('Downloads:');
}, 15000);
});
describe('License Information', () => {
it('should check package license successfully', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 9,
method: 'tools/call',
params: {
name: 'check_license',
arguments: {
packageName: 'lodash'
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
const textContent = response.result.content[0].text;
expect(textContent).toContain('License Information');
expect(textContent).toContain('lodash');
}, 15000);
});
describe('Security Operations', () => {
it('should check package vulnerability successfully', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 10,
method: 'tools/call',
params: {
name: 'check_vulnerability',
arguments: {
packageName: 'lodash'
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
const textContent = response.result.content[0].text;
expect(textContent).toContain('Vulnerability Check');
expect(textContent).toContain('lodash');
}, 15000);
});
describe('Error Handling', () => {
it('should handle invalid tool name', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 11,
method: 'tools/call',
params: {
name: 'invalid_tool_name',
arguments: {}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
expect(response.result.isError).toBe(true);
});
it('should handle malformed JSON requests', async () => {
const event = createEvent('POST');
event.body = '{ invalid json }';
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(400);
const response = JSON.parse(result.body);
expect(response.error).toBeDefined();
expect(response.error.message).toContain('Invalid JSON');
});
it('should handle missing required parameters', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 12,
method: 'tools/call',
params: {
name: 'search_packages',
arguments: {
// Missing required 'query' parameter
limit: 5
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
expect(response.result.isError).toBe(true);
});
});
describe('CORS and HTTP Methods', () => {
it('should handle CORS preflight requests', async () => {
const event = createEvent('OPTIONS');
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
expect(result.headers['Access-Control-Allow-Origin']).toBe('*');
expect(result.headers['Access-Control-Allow-Methods']).toContain('POST');
});
it('should handle GET requests for service discovery', async () => {
const event = createEvent('GET');
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.name).toBe('npmplus-mcp');
expect(response.version).toBe('12.0.19');
});
it('should reject unsupported HTTP methods', async () => {
const event = createEvent('DELETE');
const result = await npmPlusMcp.handler(event, createContext());
expect(result.statusCode).toBe(405);
const response = JSON.parse(result.body);
expect(response.error).toBeDefined();
expect(response.error.message).toContain('Method not allowed');
});
});
describe('Performance and Reliability', () => {
it('should complete requests within reasonable time', async () => {
const start = Date.now();
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 13,
method: 'tools/call',
params: {
name: 'search_packages',
arguments: {
query: 'react',
limit: 5
}
}
});
const result = await npmPlusMcp.handler(event, createContext());
const duration = Date.now() - start;
expect(result.statusCode).toBe(200);
expect(duration).toBeLessThan(15000); // Should complete within 15 seconds
}, 20000);
it('should handle concurrent requests gracefully', async () => {
const requests = Array.from({ length: 5 }, (_, i) =>
npmPlusMcp.handler(
createEvent('POST', {
jsonrpc: '2.0',
id: 100 + i,
method: 'tools/call',
params: {
name: 'package_info',
arguments: {
packageName: ['lodash', 'react', 'vue', 'angular', 'express'][i]
}
}
}),
createContext()
)
);
const results = await Promise.all(requests);
results.forEach(result => {
expect(result.statusCode).toBe(200);
const response = JSON.parse(result.body);
expect(response.result).toBeDefined();
});
}, 30000);
});
describe('Cache Functionality', () => {
it('should return consistent results for repeated requests', async () => {
const event = createEvent('POST', {
jsonrpc: '2.0',
id: 14,
method: 'tools/call',
params: {
name: 'package_info',
arguments: {
packageName: 'lodash'
}
}
});
const result1 = await npmPlusMcp.handler(event, createContext());
const result2 = await npmPlusMcp.handler(event, createContext());
expect(result1.statusCode).toBe(200);
expect(result2.statusCode).toBe(200);
const response1 = JSON.parse(result1.body);
const response2 = JSON.parse(result2.body);
// Results should be consistent
expect(response1.result.content[0].text).toBe(response2.result.content[0].text);
}, 20000);
});
});