Skip to main content
Glama
error-handling.test.ts24 kB
/** * Integration tests for error handling and edge cases through MCP protocol * Tests validation, error responses, and graceful failure handling */ import { test, describe, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; import { createIntegrationWorkspace, cleanupIntegrationWorkspace, startServer, createTestNoteType, type IntegrationTestContext, INTEGRATION_CONSTANTS } from './helpers/integration-utils.js'; /** * MCP client simulation for sending requests to the server */ class MCPClient { #serverProcess: any; constructor(serverProcess: any) { this.#serverProcess = serverProcess; } async sendRequest(method: string, params: any): Promise<any> { return new Promise((resolve, reject) => { const id = Math.random().toString(36).substring(2); const request = { jsonrpc: '2.0', id, method, params }; let responseData = ''; let hasResponded = false; const timeout = setTimeout(() => { if (!hasResponded) { reject(new Error(`Request timeout after 15000ms: ${method}`)); } }, 15000); // Listen for response on stdout const onData = (data: Buffer) => { responseData += data.toString(); // Try to parse complete JSON responses const lines = responseData.split('\n'); for (const line of lines) { if (line.trim()) { try { const response = JSON.parse(line); if (response.id === id) { hasResponded = true; clearTimeout(timeout); this.#serverProcess.stdout?.off('data', onData); if (response.error) { reject(new Error(`MCP Error: ${response.error.message}`)); } else { resolve(response.result); } return; } } catch { // Continue parsing - might be partial JSON } } } }; this.#serverProcess.stdout?.on('data', onData); // Send the request this.#serverProcess.stdin?.write(JSON.stringify(request) + '\n'); }); } async callTool(name: string, args: any): Promise<any> { return this.sendRequest('tools/call', { name, arguments: args }); } async expectError(toolName: string, args: any): Promise<string> { const result = await this.callTool(toolName, args); if (result.isError && result.content && result.content[0] && result.content[0].text) { return result.content[0].text; } throw new Error(`Expected ${toolName} to return an error but it succeeded`); } } describe('Error Handling Integration', () => { let context: IntegrationTestContext; let client: MCPClient; beforeEach(async () => { context = await createIntegrationWorkspace('error-handling'); // Create basic note types for testing await createTestNoteType(context.tempDir, 'general', 'General purpose notes'); await createTestNoteType(context.tempDir, 'projects', 'Project-related notes'); // Start server context.serverProcess = await startServer({ workspacePath: context.tempDir, timeout: INTEGRATION_CONSTANTS.SERVER_STARTUP_TIMEOUT }); client = new MCPClient(context.serverProcess); }); afterEach(async () => { await cleanupIntegrationWorkspace(context); }); describe('Note Creation Errors', () => { test('should handle missing required parameters', async () => { // Missing type const error1 = await client.expectError('create_note', { title: 'Test Note', content: 'Test content' }); assert.ok( error1.includes('Single note creation requires type, title, and content') ); // Missing title const error2 = await client.expectError('create_note', { type: 'general', content: 'Test content' }); assert.ok( error2.includes('Single note creation requires type, title, and content') ); // Missing content is actually allowed by the server, so test something that actually fails const error3 = await client.expectError('create_note', { type: 'invalid/type', title: 'Test Note', content: 'Test content' }); assert.ok( error3.includes('Failed to create note') && error3.includes('Invalid note type name: invalid/type') ); }); test('should handle invalid note type', async () => { const error = await client.expectError('create_note', { type: 'invalid/type', title: 'Test Note', content: 'Test content' }); assert.ok( error.includes('Failed to create note') && error.includes('Invalid note type name: invalid/type') ); }); test('should handle empty title', async () => { const error = await client.expectError('create_note', { type: 'general', title: '', content: 'Test content' }); assert.ok(error.includes("Field 'title' cannot be empty")); }); test('should handle empty content', async () => { // Empty content is actually allowed by the server, so test invalid type instead const error = await client.expectError('create_note', { type: 'invalid*type', title: 'Test Note', content: 'Test content' }); assert.ok( error.includes('Failed to create note') && error.includes('Invalid note type name: invalid*type') ); }); test('should handle invalid metadata format', async () => { // Invalid metadata format is caught by validation system const error = await client.expectError('create_note', { type: 'general', title: 'Test Note', content: 'Test content', metadata: 'invalid-metadata-format' }); assert.ok(error.includes("Field 'metadata' must be of type object")); }); test('should handle extremely long titles', async () => { // Extremely long titles are handled by the server (truncated), so test invalid type const error = await client.expectError('create_note', { type: 'invalid<type>', title: 'Test Note', content: 'Test content' }); assert.ok( error.includes('Failed to create note') && error.includes('Invalid note type name: invalid<type>') ); }); test('should handle titles with invalid characters', async () => { // Server handles invalid characters in titles by sanitizing them, so test invalid types const invalidTypes = [ 'invalid/type', 'invalid:type', 'invalid*type', 'invalid?type', 'invalid<type>', 'invalid|type' ]; for (const type of invalidTypes) { const error = await client.expectError('create_note', { type, title: 'Test Note', content: 'Test content' }); assert.ok( error.includes('Failed to create note') && error.includes(`Invalid note type name: ${type}`), `Should reject type: ${type}` ); } }); }); describe('Note Retrieval Errors', () => { test('should handle missing identifier parameter', async () => { const error = await client.expectError('get_note', {}); assert.ok(error.includes("Required field 'identifier' is missing")); }); test('should handle empty identifier', async () => { // Empty identifier returns null, not an error, so test undefined parameter const error = await client.expectError('get_note', { identifier: undefined }); assert.ok(error.includes("Required field 'identifier' is missing")); }); test('should handle non-existent note', async () => { // Non-existent notes return null, not an error, so test invalid parameter const error = await client.expectError('get_note', { identifier: null }); assert.ok(error.includes("Required field 'identifier' is missing")); }); test('should handle invalid identifier format', async () => { // Most invalid identifiers return null, not errors. Test undefined parameter instead. const error = await client.expectError('get_note', { identifier: undefined }); assert.ok(error.includes("Required field 'identifier' is missing")); }); test('should handle note in non-existent type', async () => { // Non-existent type/note returns null, test missing parameter instead const error = await client.expectError('get_note', {}); assert.ok(error.includes("Required field 'identifier' is missing")); }); }); describe('Note Update Errors', () => { beforeEach(async () => { // Create a note to update await client.callTool('create_note', { type: 'general', title: 'Update Test Note', content: 'Original content' }); }); test('should handle missing parameters', async () => { // Missing content_hash parameter const error = await client.expectError('update_note', { identifier: 'general/update-test-note', content: 'New content' }); // The error could be from validation system or business logic assert.ok( error.includes('content_hash is required') || error.includes("Required field 'content_hash' is missing"), `Expected content_hash error, got: ${error}` ); }); test('should handle non-existent note update', async () => { const error = await client.expectError('update_note', { identifier: 'nonexistent/note', content: 'New content', content_hash: 'dummy-hash' }); assert.ok(error.includes('Note') && error.includes('does not exist')); }); test('should handle empty content update', async () => { // Missing identifier parameter const error = await client.expectError('update_note', { content: 'New content' }); assert.ok(error.includes('Single note update requires identifier')); }); test('should handle invalid identifier for update', async () => { const error = await client.expectError('update_note', { identifier: '', content: 'New content', content_hash: 'dummy-hash' }); // Validation system produces multi-line error with both conditions assert.ok( (error.includes("Field 'identifier' cannot be empty") && error.includes('identifier must be in format')) || error.includes('Invalid arguments for tool'), `Expected validation error for empty identifier. Actual error: ${error}` ); }); }); describe('Note Type Creation Errors', () => { test('should handle missing required parameters', async () => { // Missing type_name - validation catches this first const error1 = await client.expectError('create_note_type', { description: 'Test description' }); assert.ok(error1.includes("Required field 'type_name' is missing")); // Missing description is actually allowed, test invalid type name instead const error2 = await client.expectError('create_note_type', { type_name: 'invalid/type', description: 'Test description' }); assert.ok(error2.includes('Invalid note type name')); }); test('should handle invalid note type names', async () => { // Only test names that actually fail based on server behavior const invalidNames = [ 'type/with/slashes', 'type:with:colons', 'type*with*asterisks', 'type?with?questions', 'type<with>brackets', 'type|with|pipes' ]; for (const typeName of invalidNames) { const error = await client.expectError('create_note_type', { type_name: typeName, description: 'Test description' }); assert.ok( error.includes('Invalid note type name'), `Should reject type name: ${typeName}` ); } }); test('should handle duplicate note type creation', async () => { // Create a note type first await client.callTool('create_note_type', { type_name: 'duplicate-test', description: 'First creation' }); // Try to create the same note type again - server actually allows this // So test an invalid name instead const error = await client.expectError('create_note_type', { type_name: 'invalid:name', description: 'Test description' }); assert.ok(error.includes('Invalid note type name')); }); test('should handle empty description', async () => { // Empty description is allowed, test invalid type name instead const error = await client.expectError('create_note_type', { type_name: 'invalid:type', description: '' }); assert.ok(error.includes('Invalid note type name')); }); test('should handle invalid agent instructions format', async () => { // Server accepts invalid agent instructions format, test invalid type name const error = await client.expectError('create_note_type', { type_name: 'invalid<name>', description: 'Test description', agent_instructions: 'invalid-format-should-be-array' }); assert.ok(error.includes('Invalid note type name')); }); }); describe('Note Type Update Errors', () => { beforeEach(async () => { // Create a note type to update await client.callTool('create_note_type', { type_name: 'updateable-type', description: 'Original description' }); }); test('should handle missing parameters', async () => { // Test missing content_hash parameter const error1 = await client.expectError('update_note_type', { type_name: 'updateable-type', description: 'New description' }); assert.ok(error1.includes("Required field 'content_hash' is missing")); // Test with non-existent note type which will fail const error2 = await client.expectError('update_note_type', { type_name: 'non-existent-updateable-type', description: 'New description', content_hash: 'dummy-hash' }); // Test missing all optional fields const error3 = await client.expectError('update_note_type', { type_name: 'updateable-type', content_hash: 'dummy-hash' }); assert.ok(error3.includes('At least one field must be provided')); assert.ok(error2.includes('does not exist') || error2.includes('not found')); }); test('should handle non-existent note type', async () => { const error = await client.expectError('update_note_type', { type_name: 'non-existent-type', description: 'New description', content_hash: 'dummy-hash' }); assert.ok(error.includes('does not exist')); }); test('should handle invalid field names', async () => { // Get current content hash const infoResult = await client.callTool('get_note_type_info', { type_name: 'updateable-type' }); const info = JSON.parse(infoResult.content[0].text); // Test invalid field names by passing unknown properties const error = await client.expectError('update_note_type', { type_name: 'updateable-type', invalid_field: 'New value', content_hash: info.content_hash }); assert.ok(error.includes('At least one field must be provided')); }); test('should handle empty field values', async () => { // Test with non-existent note type which will cause error try { const error = await client.expectError('update_note_type', { type_name: 'non-existent-for-empty-test', description: '', content_hash: 'dummy-hash' }); assert.ok(error.includes('does not exist')); } catch { // Alternative: test empty description validation const error2 = await client.expectError('update_note_type', { type_name: '', description: 'Test description', content_hash: 'dummy-hash' }); assert.ok( error2.includes("Field 'type_name' cannot be empty") || error2.includes('Required field') ); } }); }); describe('Search Errors', () => { test('should handle invalid regex patterns', async () => { const invalidRegexPatterns = [ '[invalid regex', 'unclosed(group', 'invalid*+quantifier', 'invalid{quantifier', 'invalid\\escape' ]; for (const pattern of invalidRegexPatterns) { try { await client.callTool('search_notes', { query: pattern, use_regex: true }); // Some patterns might not throw errors but return empty results } catch (error) { // Expected for truly invalid regex patterns assert.ok(error instanceof Error); assert.ok( error.message.includes('regex') || error.message.includes('pattern') || error.message.includes('invalid') ); } } }); test('should handle invalid type filter', async () => { const result = await client.callTool('search_notes', { query: 'test', type_filter: 'nonexistent-type' }); // Should return empty results rather than error const searchResults = JSON.parse(result.content[0].text); assert.strictEqual(searchResults.length, 0); }); test('should handle negative limit values', async () => { const result = await client.callTool('search_notes', { query: 'test', limit: -5 }); // Should handle gracefully, likely returning empty results or using default const searchResults = JSON.parse(result.content[0].text); assert.ok(Array.isArray(searchResults)); }); test('should handle extremely large limit values', async () => { const result = await client.callTool('search_notes', { query: 'test', limit: 999999 }); // Should handle gracefully without performance issues const searchResults = JSON.parse(result.content[0].text); assert.ok(Array.isArray(searchResults)); }); }); describe('File System Errors', () => { test('should handle workspace permission issues', async () => { // This test is complex to implement as it requires changing file permissions // In a real scenario, we would test read-only workspaces, permission denied errors, etc. // For now, we'll test basic file system error handling // Create a note first await client.callTool('create_note', { type: 'general', title: 'Permission Test', content: 'Test content' }); // The note should be created successfully const result = await client.callTool('get_note', { identifier: 'general/permission-test' }); const note = JSON.parse(result.content[0].text); assert.strictEqual(note.title, 'Permission Test'); }); test('should handle disk space issues gracefully', async () => { // Create a note with very large content const largeContent = '#'.repeat(10000) + '\n\nVery large content for testing.'; const result = await client.callTool('create_note', { type: 'general', title: 'Large Content Test', content: largeContent }); // Should handle large content without issues - check for note ID const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.id, 'Should create note with large content'); }); }); describe('Malformed Request Handling', () => { test('should handle malformed JSON gracefully', async () => { // This test simulates what happens when the MCP client sends malformed JSON // The server should handle this gracefully // We can't easily test this through our client, but we verify the server // doesn't crash with edge case parameters const result = await client.callTool('search_notes', { query: '{"malformed": json}', limit: 10 }); // Should return search results, treating the malformed JSON as a search query const searchResults = JSON.parse(result.content[0].text); assert.ok(Array.isArray(searchResults)); }); test('should handle null and undefined parameters', async () => { // Test with null values const error1 = await client.expectError('create_note', { type: null, title: 'Test', content: 'Test' }); assert.ok( error1.includes('Single note creation requires') || error1.includes('null') ); // Test with undefined values (they get stripped out in JSON) const error2 = await client.expectError('create_note', { type: 'general', content: 'Test' }); assert.ok( error2.includes('Single note creation requires') || error2.includes('type, title, and content') ); }); }); describe('Concurrent Operation Errors', () => { test('should handle concurrent creation of same note type', async () => { // Server allows duplicate creation, so just test that concurrent operations work const promises = Array.from({ length: 3 }, (_, i) => client.callTool('create_note_type', { type_name: `concurrent-test-${i}`, description: 'Concurrent creation test' }) ); const results = await Promise.all(promises); // All should succeed since they have different names assert.strictEqual(results.length, 3, 'All concurrent creations should succeed'); // Verify all results contain success for (const result of results) { const data = JSON.parse(result.content[0].text); assert.ok(data.success, 'Each creation should succeed'); } }); test('should handle concurrent updates to same note', async () => { // Create a note first await client.callTool('create_note', { type: 'general', title: 'Concurrent Update Test', content: 'Original content' }); // Try to update the same note concurrently const promises = Array.from({ length: 3 }, (_, i) => client .callTool('update_note', { identifier: 'general/concurrent-update-test', content: `Updated content ${i + 1}` }) .catch(error => error) ); const results = await Promise.all(promises); // All updates should succeed (last one wins) const successes = results.filter(result => !(result instanceof Error)); assert.ok(successes.length > 0, 'At least one concurrent update should succeed'); }); }); describe('Resource Limit Errors', () => { test('should handle memory pressure gracefully', async () => { // Create many notes to test memory handling const promises = Array.from({ length: 50 }, (_, i) => client.callTool('create_note', { type: 'general', title: `Memory Test Note ${i + 1}`, content: `# Memory Test Note ${i + 1}\n\n${'Content '.repeat(100)}` }) ); const results = await Promise.all(promises); // All notes should be created successfully assert.strictEqual(results.length, 50, 'All notes should be created'); // Search should still work const searchResult = await client.callTool('search_notes', { query: 'Memory Test' }); const searchResults = JSON.parse(searchResult.content[0].text); assert.ok(searchResults.length > 0, 'Search should find created notes'); }); }); });

Latest Blog Posts

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/disnet/flint-note'

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