Skip to main content
Glama

Scratchpad MCP

by pc035860
update-scratchpad.test.ts21.8 kB
/** * Update Scratchpad Tool - End-to-End MCP Integration Tests * * Tests the update-scratchpad MCP tool directly to ensure proper functionality, * parameter validation, and response formatting for all four editing modes. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { ScratchpadDatabase } from '../src/database/index.js'; import { createWorkflowTool, createScratchpadTool, getScratchpadTool, enhancedUpdateScratchpadTool, type CreateWorkflowArgs, type CreateScratchpadArgs, type GetScratchpadArgs, type EnhancedUpdateScratchpadArgs, } from '../src/tools/index.js'; import { validateEnhancedUpdateScratchpadArgs } from '../src/server-helpers.js'; /** * Test helper class for Enhanced Update Scratchpad MCP tool */ class EnhancedUpdateTestHelper { private db: ScratchpadDatabase; // Tool handlers private createWorkflow: ReturnType<typeof createWorkflowTool>; private createScratchpad: ReturnType<typeof createScratchpadTool>; private getScratchpad: ReturnType<typeof getScratchpadTool>; private enhancedUpdate: ReturnType<typeof enhancedUpdateScratchpadTool>; constructor() { this.db = new ScratchpadDatabase({ filename: ':memory:' }); // Initialize tool handlers this.createWorkflow = createWorkflowTool(this.db); this.createScratchpad = createScratchpadTool(this.db); this.getScratchpad = getScratchpadTool(this.db); this.enhancedUpdate = enhancedUpdateScratchpadTool(this.db); } // Tool wrapper methods with error handling async callCreateWorkflow(args: CreateWorkflowArgs) { try { return await this.createWorkflow(args); } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error' }; } } async callCreateScratchpad(args: CreateScratchpadArgs) { try { return await this.createScratchpad(args); } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error' }; } } async callGetScratchpad(args: GetScratchpadArgs) { try { return await this.getScratchpad(args); } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error' }; } } async callEnhancedUpdate(args: EnhancedUpdateScratchpadArgs) { try { // First validate parameters like the real MCP server does const validatedArgs = validateEnhancedUpdateScratchpadArgs(args); return await this.enhancedUpdate(validatedArgs); } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error' }; } } getDatabase(): ScratchpadDatabase { return this.db; } close(): void { this.db.close(); } } describe('Enhanced Update Scratchpad - MCP Integration Tests', () => { let helper: EnhancedUpdateTestHelper; let workflowId: string; let scratchpadId: string; beforeEach(async () => { helper = new EnhancedUpdateTestHelper(); // Create test workflow const workflowResult = await helper.callCreateWorkflow({ name: 'Enhanced Update Test Workflow', description: 'Test workflow for enhanced update functionality', }); expect(workflowResult).not.toHaveProperty('error'); workflowId = workflowResult.workflow.id; // Create test scratchpad const scratchpadResult = await helper.callCreateScratchpad({ workflow_id: workflowId, title: 'Test Scratchpad for Enhanced Updates', content: 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5', include_content: true, }); expect(scratchpadResult).not.toHaveProperty('error'); scratchpadId = scratchpadResult.scratchpad.id; }); afterEach(() => { helper.close(); }); describe('Mode 1: replace - Complete Replacement', () => { it('should replace entire content successfully', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: 'Completely new content\nWith multiple lines', include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result).toHaveProperty('scratchpad'); expect(result).toHaveProperty('message'); expect(result).toHaveProperty('operation_details'); expect(result.scratchpad.content).toBe('Completely new content\nWith multiple lines'); expect(result.operation_details.mode).toBe('replace'); expect(result.operation_details.lines_affected).toBe(2); expect(result.operation_details.previous_size_bytes).toBe(34); // Original content size expect(result.operation_details.size_change_bytes).toBeGreaterThan(0); // Verify via get const getResult = await helper.callGetScratchpad({ id: scratchpadId }); expect(getResult).not.toHaveProperty('error'); expect(getResult.scratchpad.content).toBe('Completely new content\nWith multiple lines'); }); it('should handle empty content replacement', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: '', include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe(''); expect(result.operation_details.lines_affected).toBe(0); expect(result.operation_details.size_change_bytes).toBeLessThan(0); }); it('should reject extra parameters for replace mode', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: 'New content', line_number: 2, // Should not be allowed for replace mode } as any); expect(result).toHaveProperty('error'); expect(result.error).toContain('unexpected parameters'); }); }); describe('Mode 2: insert_at_line - Line Number Insertion', () => { it('should insert content at beginning (line 1)', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'New first line', line_number: 1, include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('New first line\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5'); expect(result.operation_details.mode).toBe('insert_at_line'); expect(result.operation_details.lines_affected).toBe(1); expect(result.operation_details.insertion_point).toBe(1); }); it('should insert content at middle (line 3)', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'Inserted at line 3', line_number: 3, include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('Line 1\nLine 2\nInserted at line 3\nLine 3\nLine 4\nLine 5'); expect(result.operation_details.insertion_point).toBe(3); }); it('should insert content at end (beyond last line)', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'Inserted at end', line_number: 10, // Beyond the 5 existing lines include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nInserted at end'); expect(result.operation_details.insertion_point).toBe(6); // Actual insertion point }); it('should handle multi-line insertion', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'First new line\nSecond new line', line_number: 2, include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('Line 1\nFirst new line\nSecond new line\nLine 2\nLine 3\nLine 4\nLine 5'); expect(result.operation_details.lines_affected).toBe(2); }); it('should reject invalid line_number', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'Test', line_number: 0, // Invalid: must be >= 1 }); expect(result).toHaveProperty('error'); expect(result.error).toContain('line_number must be >= 1'); }); it('should reject missing line_number', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'Test', // missing line_number } as any); expect(result).toHaveProperty('error'); expect(result.error).toContain('line_number is required'); }); }); describe('Mode 3: replace_lines - Range Replacement', () => { it('should replace single line', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace_lines', content: 'Replaced line 2', start_line: 2, end_line: 2, include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('Line 1\nReplaced line 2\nLine 3\nLine 4\nLine 5'); expect(result.operation_details.mode).toBe('replace_lines'); expect(result.operation_details.lines_affected).toBe(1); expect(result.operation_details.replaced_range).toEqual({ start_line: 2, end_line: 2, }); }); it('should replace multiple lines', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace_lines', content: 'New line 2-3\nAnother new line', start_line: 2, end_line: 3, include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('Line 1\nNew line 2-3\nAnother new line\nLine 4\nLine 5'); expect(result.operation_details.lines_affected).toBe(2); expect(result.operation_details.replaced_range).toEqual({ start_line: 2, end_line: 3, }); }); it('should replace with different line count', async () => { // Replace 2 lines with 1 line const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace_lines', content: 'Single replacement line', start_line: 3, end_line: 4, include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('Line 1\nLine 2\nSingle replacement line\nLine 5'); expect(result.operation_details.lines_affected).toBe(1); // Final line count }); it('should handle replacing entire content', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace_lines', content: 'Only line', start_line: 1, end_line: 5, include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toBe('Only line'); expect(result.operation_details.lines_affected).toBe(1); }); it('should reject invalid range (start > end)', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace_lines', content: 'Test', start_line: 3, end_line: 2, // Invalid: start > end }); expect(result).toHaveProperty('error'); expect(result.error).toContain('start_line must be <= end_line'); }); it('should reject missing range parameters', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace_lines', content: 'Test', start_line: 2, // missing end_line } as any); expect(result).toHaveProperty('error'); expect(result.error).toContain('end_line is required'); }); it('should handle out of bounds ranges gracefully', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace_lines', content: 'Beyond bounds replacement', start_line: 10, end_line: 15, // Way beyond the 5 existing lines include_content: true, }); expect(result).not.toHaveProperty('error'); // Should append at the end expect(result.scratchpad.content).toBe('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nBeyond bounds replacement'); }); }); describe('Mode 4: append_section - Markdown Section Appending', () => { beforeEach(async () => { // Create a markdown-structured scratchpad for section testing const markdownContent = `# Project Overview This is the project overview section. ## Features - Feature 1 - Feature 2 ## Implementation Notes Some implementation details here. ## Conclusion Final thoughts.`; const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: markdownContent, }); expect(result).not.toHaveProperty('error'); }); it('should append to existing section', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'append_section', content: '- Feature 3\n- Feature 4', section_marker: '## Features', include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toContain('- Feature 1\n- Feature 2'); expect(result.scratchpad.content).toContain('- Feature 3\n- Feature 4'); expect(result.operation_details.mode).toBe('append_section'); expect(result.operation_details.lines_affected).toBe(2); }); it('should handle section marker not found (append at end)', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'append_section', content: 'New section content', section_marker: '## Non-existent Section', include_content: true, }); expect(result).not.toHaveProperty('error'); expect(result.scratchpad.content).toContain('Final thoughts.'); expect(result.scratchpad.content).toContain('New section content'); // Should be appended at the very end expect(result.scratchpad.content.endsWith('New section content')).toBe(true); }); it('should handle multiple same markers (append to first)', async () => { // First add a duplicate section await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: `# Project Overview This is the project overview section. ## Features - Feature 1 - Feature 2 ## Implementation Notes Some implementation details here. ## Features - Duplicate features section - Another feature ## Conclusion Final thoughts.`, }); const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'append_section', content: '- Additional feature', section_marker: '## Features', include_content: true, }); expect(result).not.toHaveProperty('error'); // Should append to the first Features section, not the second one const content = result.scratchpad.content; const firstFeaturesIndex = content.indexOf('## Features'); const addedFeatureIndex = content.indexOf('- Additional feature'); const secondFeaturesIndex = content.indexOf('## Features', firstFeaturesIndex + 1); expect(addedFeatureIndex).toBeGreaterThan(firstFeaturesIndex); expect(addedFeatureIndex).toBeLessThan(secondFeaturesIndex); }); it('should reject missing section_marker', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'append_section', content: 'Test content', // missing section_marker } as any); expect(result).toHaveProperty('error'); expect(result.error).toContain('section_marker is required'); }); it('should handle empty section_marker', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'append_section', content: 'Test content', section_marker: '', }); expect(result).toHaveProperty('error'); expect(result.error).toContain('section_marker is required'); }); }); describe('Parameter Validation and Error Handling', () => { it('should reject invalid scratchpad id', async () => { const result = await helper.callEnhancedUpdate({ id: 'invalid-scratchpad-id', mode: 'replace', content: 'Test content', }); expect(result).toHaveProperty('error'); expect(result.error).toMatch(/scratchpad.*not found/i); }); it('should reject missing required parameters', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', // missing content } as any); expect(result).toHaveProperty('error'); expect(result.error).toContain('content'); }); it('should reject invalid mode', async () => { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'invalid_mode' as any, content: 'Test content', }); expect(result).toHaveProperty('error'); expect(result.error).toContain('mode must be one of'); }); it('should handle include_content parameter correctly', async () => { // Test include_content: false const result1 = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: 'New content without return', include_content: false, }); expect(result1).not.toHaveProperty('error'); expect(result1.scratchpad.content).toBeUndefined(); expect(result1.message).toContain('Content not included in response'); // Test include_content: true (explicit) const result2 = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: 'New content with return', include_content: true, }); expect(result2).not.toHaveProperty('error'); expect(result2.scratchpad.content).toBe('New content with return'); // Test default behavior (should include content) const result3 = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: 'Default behavior content', }); expect(result3).not.toHaveProperty('error'); expect(result3.scratchpad.content).toBe('Default behavior content'); }); }); describe('Integration with Existing Tools', () => { it('should work seamlessly with get-scratchpad tool', async () => { // Update using enhanced tool const updateResult = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'Inserted via enhanced tool', line_number: 3, }); expect(updateResult).not.toHaveProperty('error'); // Verify with get-scratchpad tool const getResult = await helper.callGetScratchpad({ id: scratchpadId }); expect(getResult).not.toHaveProperty('error'); expect(getResult.scratchpad.content).toContain('Inserted via enhanced tool'); }); it('should maintain database consistency', async () => { const initialGet = await helper.callGetScratchpad({ id: scratchpadId }); expect(initialGet).not.toHaveProperty('error'); const initialSize = initialGet.scratchpad.size_bytes; // Perform multiple updates await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'append_section', content: 'Added content 1', section_marker: 'Line 3', // Use existing content as marker }); await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: 'Added content 2', line_number: 1, }); const finalGet = await helper.callGetScratchpad({ id: scratchpadId }); expect(finalGet).not.toHaveProperty('error'); expect(finalGet.scratchpad.size_bytes).toBeGreaterThan(initialSize); expect(finalGet.scratchpad.content).toContain('Added content 1'); expect(finalGet.scratchpad.content).toContain('Added content 2'); }); }); describe('Performance and Limits', () => { it('should handle large content updates efficiently', async () => { const largeContent = 'Large line content '.repeat(1000); // ~20KB content const startTime = Date.now(); const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'replace', content: largeContent, }); const endTime = Date.now(); expect(result).not.toHaveProperty('error'); expect(endTime - startTime).toBeLessThan(100); // Should complete in <100ms expect(result.operation_details.size_change_bytes).toBeGreaterThan(18000); }); it('should handle many small updates efficiently', async () => { const startTime = Date.now(); // Perform 10 small updates for (let i = 0; i < 10; i++) { const result = await helper.callEnhancedUpdate({ id: scratchpadId, mode: 'insert_at_line', content: `Update ${i + 1}`, line_number: i + 1, }); expect(result).not.toHaveProperty('error'); } const endTime = Date.now(); expect(endTime - startTime).toBeLessThan(500); // 10 updates in <500ms }); }); });

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/pc035860/scratchpad-mcp'

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