UNPKG

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
/** * 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); }); });