Skip to main content
Glama
tools.test.ts10 kB
import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest'; import {type McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; import * as fs from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; import {registerAll} from './index.js'; describe('tool registration', () => { let server: McpServer; let registeredTools: Map<string, {meta: unknown; handler: (args: unknown) => Promise<unknown>}>; beforeEach(() => { registeredTools = new Map(); server = { registerTool: vi.fn((name: string, meta: unknown, handler: (args: unknown) => Promise<unknown>) => { registeredTools.set(name, {meta, handler}); }), } as unknown as McpServer; registerAll(server); }); it('registers all expected tools', () => { const expectedTools = ['view', 'create', 'str_replace', 'insert']; for (const toolName of expectedTools) { expect(registeredTools.has(toolName), `Tool ${toolName} should be registered`).toBe(true); } }); it('all tools have title and description', () => { for (const [name, tool] of registeredTools) { const meta = tool.meta as {title?: string; description?: string}; expect(meta.title, `Tool ${name} should have a title`).toBeDefined(); expect(meta.description, `Tool ${name} should have a description`).toBeDefined(); expect(meta.title!.length, `Tool ${name} title should not be empty`).toBeGreaterThan(0); expect(meta.description!.length, `Tool ${name} description should not be empty`).toBeGreaterThan(0); } }); it('all tools have input schema', () => { for (const [name, tool] of registeredTools) { const meta = tool.meta as {inputSchema?: unknown}; expect(meta.inputSchema, `Tool ${name} should have inputSchema`).toBeDefined(); } }); it('view tool is marked read-only', () => { const tool = registeredTools.get('view'); const meta = tool!.meta as {annotations?: {readOnlyHint?: boolean}}; expect(meta.annotations?.readOnlyHint).toBe(true); }); it('mutating tools are marked destructive', () => { const mutatingTools = ['create', 'str_replace', 'insert']; for (const toolName of mutatingTools) { const tool = registeredTools.get(toolName); const meta = tool!.meta as {annotations?: {destructiveHint?: boolean}}; expect(meta.annotations?.destructiveHint, `Tool ${toolName} should be destructive`).toBe(true); } }); }); describe('view tool', () => { let handler: (args: {path: string; view_range?: [number, number]}) => Promise<unknown>; let tmpDir: string; beforeEach(async () => { const registeredTools = new Map<string, {meta: unknown; handler: typeof handler}>(); const server = { registerTool: vi.fn((name: string, meta: unknown, h: typeof handler) => { registeredTools.set(name, {meta, handler: h}); }), } as unknown as McpServer; registerAll(server); handler = registeredTools.get('view')!.handler; // Create temp directory for tests tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'filesystem-mcp-test-')); }); afterEach(async () => { await fs.rm(tmpDir, {recursive: true, force: true}); }); it('reads file with line numbers', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'line1\nline2\nline3\n'); const result = await handler({path: testFile}) as {structuredContent: {type: string; content: string; totalLines: number}}; expect(result.structuredContent.type).toBe('file'); expect(result.structuredContent.totalLines).toBe(4); // includes trailing empty line expect(result.structuredContent.content).toContain('line1'); expect(result.structuredContent.content).toContain('line2'); }); it('reads file with view_range', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'line1\nline2\nline3\nline4\nline5\n'); const result = await handler({path: testFile, view_range: [2, 4]}) as {structuredContent: {content: string; viewedRange: [number, number]}}; expect(result.structuredContent.viewedRange).toEqual([2, 4]); expect(result.structuredContent.content).toContain('line2'); expect(result.structuredContent.content).toContain('line3'); expect(result.structuredContent.content).toContain('line4'); expect(result.structuredContent.content).not.toContain('line1'); expect(result.structuredContent.content).not.toContain('line5'); }); it('lists directory contents', async () => { await fs.writeFile(path.join(tmpDir, 'file1.txt'), 'content'); await fs.mkdir(path.join(tmpDir, 'subdir')); await fs.writeFile(path.join(tmpDir, 'subdir', 'file2.txt'), 'content'); const result = await handler({path: tmpDir}) as {structuredContent: {type: string; entries: string[]}}; expect(result.structuredContent.type).toBe('directory'); expect(result.structuredContent.entries).toContain('file1.txt'); expect(result.structuredContent.entries).toContain('subdir/'); }); }); describe('create tool', () => { let handler: (args: {path: string; content: string}) => Promise<unknown>; let tmpDir: string; beforeEach(async () => { const registeredTools = new Map<string, {meta: unknown; handler: typeof handler}>(); const server = { registerTool: vi.fn((name: string, meta: unknown, h: typeof handler) => { registeredTools.set(name, {meta, handler: h}); }), } as unknown as McpServer; registerAll(server); handler = registeredTools.get('create')!.handler; tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'filesystem-mcp-test-')); }); afterEach(async () => { await fs.rm(tmpDir, {recursive: true, force: true}); }); it('creates a new file', async () => { const testFile = path.join(tmpDir, 'new.txt'); const result = await handler({path: testFile, content: 'hello world'}) as {structuredContent: {success: boolean}}; expect(result.structuredContent.success).toBe(true); const content = await fs.readFile(testFile, 'utf-8'); expect(content).toBe('hello world'); }); it('creates parent directories', async () => { const testFile = path.join(tmpDir, 'nested', 'dir', 'file.txt'); await handler({path: testFile, content: 'content'}); const content = await fs.readFile(testFile, 'utf-8'); expect(content).toBe('content'); }); it('overwrites existing file', async () => { const testFile = path.join(tmpDir, 'existing.txt'); await fs.writeFile(testFile, 'old content'); await handler({path: testFile, content: 'new content'}); const content = await fs.readFile(testFile, 'utf-8'); expect(content).toBe('new content'); }); }); describe('str_replace tool', () => { let handler: (args: {path: string; old_str: string; new_str?: string}) => Promise<unknown>; let tmpDir: string; beforeEach(async () => { const registeredTools = new Map<string, {meta: unknown; handler: typeof handler}>(); const server = { registerTool: vi.fn((name: string, meta: unknown, h: typeof handler) => { registeredTools.set(name, {meta, handler: h}); }), } as unknown as McpServer; registerAll(server); handler = registeredTools.get('str_replace')!.handler; tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'filesystem-mcp-test-')); }); afterEach(async () => { await fs.rm(tmpDir, {recursive: true, force: true}); }); it('replaces unique string', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'hello world'); await handler({path: testFile, old_str: 'world', new_str: 'universe'}); const content = await fs.readFile(testFile, 'utf-8'); expect(content).toBe('hello universe'); }); it('deletes string when new_str omitted', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'hello world'); await handler({path: testFile, old_str: ' world'}); const content = await fs.readFile(testFile, 'utf-8'); expect(content).toBe('hello'); }); it('throws for non-unique string', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'hello hello'); await expect(handler({path: testFile, old_str: 'hello', new_str: 'hi'})).rejects.toThrow('appears 2 times'); }); it('throws for string not found', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'hello world'); await expect(handler({path: testFile, old_str: 'xyz', new_str: 'abc'})).rejects.toThrow('not found'); }); }); describe('insert tool', () => { let handler: (args: {path: string; insert_line: number; insert_text: string}) => Promise<unknown>; let tmpDir: string; beforeEach(async () => { const registeredTools = new Map<string, {meta: unknown; handler: typeof handler}>(); const server = { registerTool: vi.fn((name: string, meta: unknown, h: typeof handler) => { registeredTools.set(name, {meta, handler: h}); }), } as unknown as McpServer; registerAll(server); handler = registeredTools.get('insert')!.handler; tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'filesystem-mcp-test-')); }); afterEach(async () => { await fs.rm(tmpDir, {recursive: true, force: true}); }); it('inserts at beginning (line 0)', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'line1\nline2\n'); await handler({path: testFile, insert_line: 0, insert_text: 'line0\n'}); const content = await fs.readFile(testFile, 'utf-8'); expect(content).toBe('line0\nline1\nline2\n'); }); it('inserts after line N', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'line1\nline2\nline3\n'); await handler({path: testFile, insert_line: 2, insert_text: 'inserted\n'}); const content = await fs.readFile(testFile, 'utf-8'); expect(content).toBe('line1\nline2\ninserted\nline3\n'); }); it('throws for line beyond file', async () => { const testFile = path.join(tmpDir, 'test.txt'); await fs.writeFile(testFile, 'line1\n'); await expect(handler({path: testFile, insert_line: 100, insert_text: 'x\n'})).rejects.toThrow('beyond file length'); }); });

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/domdomegg/filesystem-mcp'

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