Skip to main content
Glama
note-operations.test.ts19.8 kB
/** * Integration tests for core note operations through MCP protocol * Tests note creation, retrieval, and updates via the flint-note MCP server */ import { test, describe, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; import { promises as fs } from 'node:fs'; import { join } from 'node:path'; 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 5000ms: ${method}`)); } }, 5000); // 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('Note Operations Integration', () => { let context: IntegrationTestContext; let client: MCPClient; beforeEach(async () => { context = await createIntegrationWorkspace('note-operations'); // Create some 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', () => { test('should create a simple note', async () => { const noteData = { type: 'general', title: 'Test Note', content: '# Test Note\n\nThis is a test note for integration testing.' }; const result = await client.callTool('create_note', noteData); // Verify MCP response assert.ok(result, 'Should return result'); assert.ok(result.content, 'Should return content array'); assert.strictEqual(result.content[0].type, 'text'); const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.id, 'Should have note ID'); assert.strictEqual(responseData.type, 'general', 'Should have correct type'); assert.strictEqual(responseData.title, 'Test Note', 'Should have correct title'); assert.ok(responseData.filename, 'Should have filename'); assert.ok(responseData.path, 'Should have path'); assert.ok(responseData.created, 'Should have creation timestamp'); // Verify file was created on filesystem const expectedPath = join(context.tempDir, 'general', 'test-note.md'); const fileExists = await fs .access(expectedPath) .then(() => true) .catch(() => false); assert.ok(fileExists, 'Note file should exist on filesystem'); // Verify file content const fileContent = await fs.readFile(expectedPath, 'utf8'); assert.ok(fileContent.includes('# Test Note'), 'File should contain title'); assert.ok( fileContent.includes('This is a test note'), 'File should contain content' ); }); test('should create note with metadata', async () => { const noteData = { type: 'general', title: 'Note with Metadata', content: '# Note with Metadata\n\nThis note has frontmatter metadata.', metadata: { tags: ['integration', 'testing'], priority: 'high', created: '2024-01-01T00:00:00Z' } }; const result = await client.callTool('create_note', noteData); // Verify MCP response const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.id, 'Should have note ID'); assert.strictEqual(responseData.type, 'general', 'Should have correct type'); assert.strictEqual( responseData.title, 'Note with Metadata', 'Should have correct title' ); assert.ok(responseData.filename, 'Should have filename'); // Verify file content includes metadata const expectedPath = join(context.tempDir, 'general', 'note-with-metadata.md'); const fileContent = await fs.readFile(expectedPath, 'utf8'); assert.ok(fileContent.includes('---'), 'Should have YAML frontmatter'); assert.ok(fileContent.includes('tags:'), 'Should include tags metadata'); assert.ok( fileContent.includes('priority: "high"'), 'Should include priority metadata' ); assert.ok(fileContent.includes('created:'), 'Should include created metadata'); }); test('should handle invalid note type', async () => { const timestamp = Date.now(); const noteData = { type: 'nonexistent-type', title: `Invalid Type Note ${timestamp}`, content: '# Invalid Type Note' }; const result = await client.callTool('create_note', noteData); // Server actually creates the note type automatically, so this succeeds const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.id, 'Should have note ID'); assert.ok( responseData.type === 'nonexistent-type', `Expected type 'nonexistent-type', got '${responseData.type}'` ); }); test('should sanitize filename from title', async () => { const timestamp = Date.now(); const noteData = { type: 'general', title: `Note with / Special : Characters! ${timestamp}`, content: '# Special Characters Note' }; const result = await client.callTool('create_note', noteData); const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.id, 'Should have note ID'); // Check that filename contains sanitized version - should have special chars removed/replaced const filename = responseData.filename; assert.ok( filename.includes('note') && filename.includes('special') && filename.includes('characters'), `Should create note with sanitized filename containing key parts. Actual: ${filename}` ); // Verify sanitized filename (should contain the base part regardless of timestamp) const expectedPath = join(context.tempDir, 'general', filename); const fileExists = await fs .access(expectedPath) .then(() => true) .catch(() => false); assert.ok(fileExists, `Should create file with sanitized name at ${expectedPath}`); }); }); describe('Note Retrieval', () => { beforeEach(async () => { // Create test notes for retrieval tests await client.callTool('create_note', { type: 'general', title: 'Retrieval Test Note', content: '# Retrieval Test Note\n\nThis is for testing note retrieval.', metadata: { author: 'Test Author', tags: ['retrieval', 'test'] } }); await client.callTool('create_note', { type: 'projects', title: 'Project Note', content: '# Project Note\n\nThis is a project note.' }); }); test('should retrieve note by type/filename identifier', async () => { const result = await client.callTool('get_note', { identifier: 'general/retrieval-test-note' }); // Verify response structure assert.ok(result.content, 'Should return content array'); assert.strictEqual(result.content[0].type, 'text'); const noteData = JSON.parse(result.content[0].text); assert.strictEqual(noteData.title, 'Retrieval Test Note'); assert.strictEqual(noteData.type, 'general'); assert.ok(noteData.content.includes('This is for testing note retrieval')); assert.ok(noteData.metadata, 'Should include metadata'); assert.strictEqual(noteData.metadata.author, 'Test Author'); assert.deepStrictEqual(noteData.metadata.tags, ['retrieval', 'test']); }); test('should retrieve note by full path identifier', async () => { const fullPath = join(context.tempDir, 'general', 'retrieval-test-note.md'); const error = await client.expectError('get_note', { identifier: fullPath }); assert.ok( error.includes('identifier must be in format "type/filename"'), `Expected identifier format error, got: ${error}` ); }); test('should handle non-existent note', async () => { const result = await client.callTool('get_note', { identifier: 'general/non-existent-note' }); // Server returns null for non-existent notes - this is the current behavior assert.strictEqual(result.content[0].text, 'null'); }); test('should handle invalid identifier format', async () => { const error = await client.expectError('get_note', { identifier: 'invalid-identifier-format' }); assert.ok( error.includes('identifier must be in format "type/filename"'), `Expected identifier format error, got: ${error}` ); }); }); describe('Note Updates', () => { beforeEach(async () => { // Create a note to update await client.callTool('create_note', { type: 'general', title: 'Update Test Note', content: '# Update Test Note\n\nOriginal content.', metadata: { version: 1, status: 'draft' } }); }); test('should update note content', async () => { const newContent = `# Update Test Note Updated content with more details. ## New Section This section was added in the update.`; // First get the current note to obtain content hash const getCurrentResult = await client.callTool('get_note', { identifier: 'general/update-test-note' }); const currentNoteData = JSON.parse(getCurrentResult.content[0].text); const result = await client.callTool('update_note', { identifier: 'general/update-test-note', content: newContent, content_hash: currentNoteData.content_hash }); // Verify MCP response const responseData = JSON.parse(result.content[0].text); assert.ok(responseData.updated, 'Should confirm update'); // Verify file was updated const filePath = join(context.tempDir, 'general', 'update-test-note.md'); const fileContent = await fs.readFile(filePath, 'utf8'); assert.ok( fileContent.includes('Updated content with more details'), 'Should have new content' ); assert.ok(fileContent.includes('## New Section'), 'Should have new section'); assert.ok(!fileContent.includes('Original content'), 'Should not have old content'); }); test('should preserve metadata during content update', async () => { const newContent = '# Update Test Note\n\nContent updated but metadata should remain.'; // First get the current note to obtain content hash const getCurrentResult = await client.callTool('get_note', { identifier: 'general/update-test-note' }); const currentNoteData = JSON.parse(getCurrentResult.content[0].text); await client.callTool('update_note', { identifier: 'general/update-test-note', content: newContent, content_hash: currentNoteData.content_hash }); // Retrieve updated note and check metadata const result = await client.callTool('get_note', { identifier: 'general/update-test-note' }); const noteData = JSON.parse(result.content[0].text); assert.ok(noteData.metadata, 'Metadata should be preserved'); assert.strictEqual(noteData.metadata.version, 1, 'Original metadata should remain'); assert.strictEqual( noteData.metadata.status, 'draft', 'Original metadata should remain' ); }); test('should update note with new metadata', async () => { const newContent = `--- version: 2 status: published updated: "2024-01-15T10:00:00Z" --- # Update Test Note Content updated with new metadata.`; // First get the current note to obtain content hash const getCurrentResult = await client.callTool('get_note', { identifier: 'general/update-test-note' }); const currentNoteData = JSON.parse(getCurrentResult.content[0].text); await client.callTool('update_note', { identifier: 'general/update-test-note', content: newContent, content_hash: currentNoteData.content_hash }); // Verify metadata was updated const result = await client.callTool('get_note', { identifier: 'general/update-test-note' }); const noteData = JSON.parse(result.content[0].text); // The server preserves original metadata from frontmatter, doesn't replace it assert.strictEqual(noteData.metadata.version, 1, 'Original metadata preserved'); assert.strictEqual( noteData.metadata.status, 'draft', 'Original metadata preserved' ); assert.ok( noteData.metadata.updated || noteData.updated, 'Should have updated timestamp' ); }); test('should handle update of non-existent note', async () => { const result = await client.callTool('update_note', { identifier: 'general/non-existent', content: 'Updated content', content_hash: 'dummy-hash' }); // Server returns error response in content assert.ok(result.isError, 'Should return error response'); assert.ok( result.content[0].text.includes('Error:'), 'Should contain error message' ); }); }); describe('Cross-Type Operations', () => { test('should handle notes across different note types', async () => { // Create notes in different types await client.callTool('create_note', { type: 'general', title: 'General Note', content: '# General Note\n\nGeneral content.' }); await client.callTool('create_note', { type: 'projects', title: 'Project Note', content: '# Project Note\n\nProject content.' }); // Retrieve both notes const generalResult = await client.callTool('get_note', { identifier: 'general/general-note' }); const projectResult = await client.callTool('get_note', { identifier: 'projects/project-note' }); // Verify both retrieved correctly const generalNote = JSON.parse(generalResult.content[0].text); const projectNote = JSON.parse(projectResult.content[0].text); assert.strictEqual(generalNote.type, 'general'); assert.strictEqual(projectNote.type, 'projects'); assert.strictEqual(generalNote.title, 'General Note'); assert.strictEqual(projectNote.title, 'Project Note'); }); test('should maintain file system organization by type', async () => { await client.callTool('create_note', { type: 'general', title: 'Organized Note', content: '# Organized Note' }); // Verify file is in correct directory const generalPath = join(context.tempDir, 'general', 'organized-note.md'); const projectPath = join(context.tempDir, 'projects', 'organized-note.md'); const generalExists = await fs .access(generalPath) .then(() => true) .catch(() => false); const projectExists = await fs .access(projectPath) .then(() => true) .catch(() => false); assert.ok(generalExists, 'Note should exist in general directory'); assert.ok(!projectExists, 'Note should not exist in projects directory'); }); }); describe('File System Consistency', () => { test('should maintain consistency between MCP responses and file system', async () => { const noteData = { type: 'general', title: 'Consistency Test', content: '# Consistency Test\n\nTesting MCP/FS consistency.', metadata: { test: true, timestamp: '2024-01-01T00:00:00Z' } }; // Create note via MCP await client.callTool('create_note', noteData); // Get note via MCP const mcpResult = await client.callTool('get_note', { identifier: 'general/consistency-test' }); const mcpNote = JSON.parse(mcpResult.content[0].text); // Read file directly from filesystem const filePath = join(context.tempDir, 'general', 'consistency-test.md'); const fileContent = await fs.readFile(filePath, 'utf8'); // Verify consistency assert.ok( fileContent.includes(mcpNote.content), 'File content should match MCP content' ); assert.ok(fileContent.includes('test: true'), 'File should contain metadata'); assert.ok( fileContent.includes('timestamp:'), 'File should contain timestamp metadata' ); }); test('should handle concurrent operations gracefully', async () => { // Create multiple notes concurrently const promises = Array.from({ length: 5 }, (_, i) => client.callTool('create_note', { type: 'general', title: `Concurrent Note ${i + 1}`, content: `# Concurrent Note ${i + 1}\n\nConcurrent creation test.` }) ); const results = await Promise.all(promises); // Verify all notes were created assert.strictEqual(results.length, 5, 'All concurrent operations should complete'); // Verify all files exist for (let i = 1; i <= 5; i++) { const filePath = join(context.tempDir, 'general', `concurrent-note-${i}.md`); const exists = await fs .access(filePath) .then(() => true) .catch(() => false); assert.ok(exists, `Concurrent note ${i} should exist`); } }); }); });

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