Skip to main content
Glama

Motion.dev MCP Server

claude-code-integration.test.ts20.5 kB
/** * End-to-end tests for Claude Code client integration * These tests simulate the actual integration with Claude Code client */ import { spawn, ChildProcess } from 'child_process'; import { createReadStream, createWriteStream } from 'fs'; import { Readable, Writable } from 'stream'; import path from 'path'; interface MCPMessage { jsonrpc: '2.0'; id?: string | number; method?: string; params?: any; result?: any; error?: any; } describe('Claude Code Integration E2E Tests', () => { let serverProcess: ChildProcess; let serverStdin: Writable; let serverStdout: Readable; let messageId: number = 1; beforeAll(async () => { // Start the MCP server process const serverPath = path.join(__dirname, '../../dist/index.js'); serverProcess = spawn('node', [serverPath], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NODE_ENV: 'test' } }); serverStdin = serverProcess.stdin!; serverStdout = serverProcess.stdout!; // Wait for server to be ready await new Promise((resolve) => { setTimeout(resolve, 2000); }); }); afterAll(async () => { if (serverProcess) { serverProcess.kill('SIGTERM'); // Wait for graceful shutdown await new Promise((resolve) => { serverProcess.on('exit', resolve); setTimeout(() => { serverProcess.kill('SIGKILL'); resolve(undefined); }, 5000); }); } }); /** * Send a JSON-RPC message to the server */ const sendMessage = async (message: MCPMessage): Promise<MCPMessage> => { return new Promise((resolve, reject) => { const messageStr = JSON.stringify(message) + '\n'; const timeout = setTimeout(() => { reject(new Error('Response timeout')); }, 10000); const onData = (data: Buffer) => { try { const response = JSON.parse(data.toString().trim()); clearTimeout(timeout); serverStdout.off('data', onData); resolve(response); } catch (error) { // Might be partial data, wait for more } }; serverStdout.on('data', onData); serverStdin.write(messageStr); }); }; describe('Server Initialization', () => { test('should handle initialize request', async () => { const initMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: { roots: { listChanged: true }, sampling: {} }, clientInfo: { name: 'claude-code', version: '1.0.0' } } }; const response = await sendMessage(initMessage); expect(response.jsonrpc).toBe('2.0'); expect(response.id).toBe(initMessage.id); expect(response.result).toBeDefined(); expect(response.result.capabilities).toBeDefined(); expect(response.result.capabilities.tools).toBe(true); expect(response.result.capabilities.resources).toBe(true); expect(response.result.serverInfo).toBeDefined(); expect(response.result.serverInfo.name).toBe('motion-dev-mcp'); }); test('should handle initialized notification', async () => { const initializedMessage: MCPMessage = { jsonrpc: '2.0', method: 'initialized', params: {} }; // This is a notification, no response expected serverStdin.write(JSON.stringify(initializedMessage) + '\n'); // Wait a bit to ensure no error occurs await new Promise(resolve => setTimeout(resolve, 1000)); }); }); describe('Tool Operations', () => { test('should list available tools', async () => { const toolsListMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/list', params: {} }; const response = await sendMessage(toolsListMessage); expect(response.result).toBeDefined(); expect(response.result.tools).toBeDefined(); expect(Array.isArray(response.result.tools)).toBe(true); expect(response.result.tools.length).toBeGreaterThan(0); // Check that required tools are present const toolNames = response.result.tools.map((tool: any) => tool.name); expect(toolNames).toContain('get_motion_docs'); expect(toolNames).toContain('generate_motion_component'); expect(toolNames).toContain('search_motion_docs'); expect(toolNames).toContain('create_animation_sequence'); expect(toolNames).toContain('optimize_motion_code'); }); test('should call get_motion_docs tool', async () => { const toolCallMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'get_motion_docs', arguments: { url: 'https://motion.dev/docs/react' } } }; const response = await sendMessage(toolCallMessage); expect(response.result).toBeDefined(); expect(response.result.content).toBeDefined(); expect(Array.isArray(response.result.content)).toBe(true); if (response.result.content.length > 0) { expect(response.result.content[0]).toHaveProperty('type'); expect(response.result.content[0]).toHaveProperty('text'); } }); test('should call generate_motion_component tool', async () => { const toolCallMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'generate_motion_component', arguments: { pattern: 'fade-in', framework: 'react', componentName: 'FadeInButton', typescript: true } } }; const response = await sendMessage(toolCallMessage); expect(response.result).toBeDefined(); expect(response.result.content).toBeDefined(); expect(response.result.content[0].type).toBe('text'); const generatedCode = response.result.content[0].text; expect(generatedCode).toContain('import React'); expect(generatedCode).toContain('import { motion }'); expect(generatedCode).toContain('FadeInButton'); expect(generatedCode).toContain('export default'); }); test('should call search_motion_docs tool', async () => { const toolCallMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'search_motion_docs', arguments: { query: 'animation', limit: 5 } } }; const response = await sendMessage(toolCallMessage); expect(response.result).toBeDefined(); expect(response.result.content).toBeDefined(); expect(Array.isArray(response.result.content)).toBe(true); }); test('should handle invalid tool call', async () => { const invalidToolCallMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'non_existent_tool', arguments: {} } }; const response = await sendMessage(invalidToolCallMessage); expect(response.error).toBeDefined(); expect(response.error.code).toBeDefined(); expect(response.error.message).toContain('Unknown tool'); }); test('should validate tool parameters', async () => { const invalidParamsMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'get_motion_docs', arguments: { // Missing required URL parameter } } }; const response = await sendMessage(invalidParamsMessage); expect(response.result.isError).toBe(true); expect(response.result.content[0].text).toContain('Missing required parameter'); }); }); describe('Resource Operations', () => { test('should list available resources', async () => { const resourcesListMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'resources/list', params: {} }; const response = await sendMessage(resourcesListMessage); expect(response.result).toBeDefined(); expect(response.result.resources).toBeDefined(); expect(Array.isArray(response.result.resources)).toBe(true); expect(response.result.resources.length).toBeGreaterThan(0); // Check that required resources are present const resourceUris = response.result.resources.map((resource: any) => resource.uri); expect(resourceUris).toContain('motion://docs/react'); expect(resourceUris).toContain('motion://docs/vue'); expect(resourceUris).toContain('motion://docs/js'); expect(resourceUris).toContain('motion://examples'); }); test('should read a resource', async () => { const resourceReadMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'resources/read', params: { uri: 'motion://docs/react' } }; const response = await sendMessage(resourceReadMessage); expect(response.result).toBeDefined(); expect(response.result.contents).toBeDefined(); expect(Array.isArray(response.result.contents)).toBe(true); if (response.result.contents.length > 0) { expect(response.result.contents[0]).toHaveProperty('uri'); expect(response.result.contents[0]).toHaveProperty('mimeType'); expect(response.result.contents[0]).toHaveProperty('text'); } }); test('should handle invalid resource URI', async () => { const invalidResourceMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'resources/read', params: { uri: 'invalid://resource' } }; const response = await sendMessage(invalidResourceMessage); expect(response.error).toBeDefined(); expect(response.error.message).toContain('Unknown resource'); }); }); describe('Complex Workflows', () => { test('should handle documentation search and code generation workflow', async () => { // Step 1: Search for React animation documentation const searchMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'search_motion_docs', arguments: { query: 'React fade animation', framework: 'react', limit: 3 } } }; const searchResponse = await sendMessage(searchMessage); expect(searchResponse.result).toBeDefined(); // Step 2: Generate a component based on the search results const generateMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'generate_motion_component', arguments: { pattern: 'fade-in', framework: 'react', componentName: 'SearchBasedComponent', typescript: true } } }; const generateResponse = await sendMessage(generateMessage); expect(generateResponse.result).toBeDefined(); expect(generateResponse.result.content[0].text).toContain('SearchBasedComponent'); // Step 3: Optimize the generated code const optimizeMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'optimize_motion_code', arguments: { code: generateResponse.result.content[0].text, framework: 'react', optimizations: ['performance', 'accessibility'] } } }; const optimizeResponse = await sendMessage(optimizeMessage); expect(optimizeResponse.result).toBeDefined(); }); test('should handle cross-framework conversion workflow', async () => { // Step 1: Generate React component const generateReactMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'generate_motion_component', arguments: { pattern: 'slide-up', framework: 'react', componentName: 'SlideUpCard' } } }; const reactResponse = await sendMessage(generateReactMessage); expect(reactResponse.result).toBeDefined(); // Step 2: Convert React to Vue const convertMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'convert_between_frameworks', arguments: { code: reactResponse.result.content[0].text, sourceFramework: 'react', targetFramework: 'vue', componentName: 'VueSlideUpCard' } } }; const vueResponse = await sendMessage(convertMessage); expect(vueResponse.result).toBeDefined(); expect(vueResponse.result.content[0].text).toContain('<template>'); expect(vueResponse.result.content[0].text).toContain('VueSlideUpCard'); }); test('should handle animation sequence creation', async () => { const sequenceMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'create_animation_sequence', arguments: { framework: 'react', steps: [ { selector: '.header', animation: { y: [-50, 0], opacity: [0, 1] }, timing: { duration: 0.6, delay: 0 } }, { selector: '.content', animation: { x: [-100, 0], opacity: [0, 1] }, timing: { duration: 0.8, delay: 0.3 } } ], stagger: { each: 0.1, from: 'first' }, componentName: 'SequentialAnimation' } } }; const response = await sendMessage(sequenceMessage); expect(response.result).toBeDefined(); expect(response.result.content[0].text).toContain('SequentialAnimation'); expect(response.result.content[0].text).toContain('staggerChildren'); }); }); describe('Error Handling and Edge Cases', () => { test('should handle malformed JSON gracefully', async () => { const malformedJSON = '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"'; // Missing closing brace return new Promise<void>((resolve) => { const onData = (data: Buffer) => { const response = data.toString(); // Server should either ignore malformed JSON or send an error response // Either way, it shouldn't crash serverStdout.off('data', onData); resolve(); }; serverStdout.on('data', onData); serverStdin.write(malformedJSON + '\n'); // Timeout to ensure server doesn't hang setTimeout(() => { serverStdout.off('data', onData); resolve(); }, 2000); }); }); test('should handle concurrent requests', async () => { const requests: Promise<MCPMessage>[] = []; // Send 5 concurrent requests for (let i = 0; i < 5; i++) { const request: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'generate_motion_component', arguments: { pattern: 'fade-in', framework: 'react', componentName: `ConcurrentComponent${i}` } } }; requests.push(sendMessage(request)); } const responses = await Promise.all(requests); responses.forEach((response, index) => { expect(response.result).toBeDefined(); expect(response.result.content[0].text).toContain(`ConcurrentComponent${index}`); }); }); test('should handle large payloads', async () => { // Generate a large custom animation object const largeAnimation = { keyframes: Array.from({ length: 100 }, (_, i) => ({ offset: i / 99, opacity: Math.sin(i / 10), scale: 1 + Math.sin(i / 5) * 0.1, rotate: i * 3.6 })) }; const largePayloadMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/call', params: { name: 'generate_motion_component', arguments: { pattern: 'custom', framework: 'react', customAnimation: largeAnimation, componentName: 'LargeAnimationComponent' } } }; const response = await sendMessage(largePayloadMessage); expect(response.result).toBeDefined(); }); test('should maintain performance under load', async () => { const startTime = Date.now(); const requests: Promise<MCPMessage>[] = []; // Send 20 requests rapidly for (let i = 0; i < 20; i++) { const request: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/list', params: {} }; requests.push(sendMessage(request)); } await Promise.all(requests); const endTime = Date.now(); const totalTime = endTime - startTime; // Should complete within reasonable time (less than 10 seconds for 20 requests) expect(totalTime).toBeLessThan(10000); }); }); describe('Server Health and Monitoring', () => { test('should respond to health check requests', async () => { // This would be a custom health check tool if implemented // For now, we'll test that the server is responsive const healthMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/list', params: {} }; const startTime = Date.now(); const response = await sendMessage(healthMessage); const responseTime = Date.now() - startTime; expect(response.result).toBeDefined(); expect(responseTime).toBeLessThan(2000); // Should respond within 2 seconds }); test('should handle graceful shutdown signal', async () => { // Send SIGTERM to test graceful shutdown // Note: This will terminate the server, so it should be the last test // or we need to restart the server afterwards const healthMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/list', params: {} }; // Ensure server is responsive before shutdown const response = await sendMessage(healthMessage); expect(response.result).toBeDefined(); // The server shutdown will be handled in afterAll }, 10000); // Longer timeout for shutdown test }); describe('Protocol Compliance', () => { test('should follow JSON-RPC 2.0 specification', async () => { const message: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/list', params: {} }; const response = await sendMessage(message); expect(response.jsonrpc).toBe('2.0'); expect(response.id).toBe(message.id); expect(response.result || response.error).toBeDefined(); expect(response.result && response.error).toBeFalsy(); // Should not have both }); test('should handle notifications (no response expected)', async () => { const notification: MCPMessage = { jsonrpc: '2.0', method: 'notifications/initialized', params: {} }; // Send notification (no ID, so no response expected) serverStdin.write(JSON.stringify(notification) + '\n'); // Wait a bit to ensure server processes it without issues await new Promise(resolve => setTimeout(resolve, 1000)); // Server should still be responsive const testMessage: MCPMessage = { jsonrpc: '2.0', id: messageId++, method: 'tools/list', params: {} }; const response = await sendMessage(testMessage); expect(response.result).toBeDefined(); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Abhishekrajpurohit/motion-dev-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server