Skip to main content
Glama
server.test.ts25.4 kB
/** * Unit tests for MCP Server tool registration and schema validation * These test the server logic in isolation without external services */ import { describe, expect, test } from "bun:test"; import { z } from "zod"; describe("MCP Server", () => { // Memory type enum matching server definition const MemoryTypeEnum = z.enum([ "decision", "solution", "pattern", "architecture", "note", ]); describe("tool registry", () => { const REGISTERED_TOOLS = [ "doclea_store", "doclea_search", "doclea_get", "doclea_update", "doclea_delete", "doclea_commit_message", "doclea_pr_description", "doclea_changelog", "doclea_expertise", "doclea_suggest_reviewers", "doclea_init", "doclea_import", ] as const; test("defines 12 tools", () => { expect(REGISTERED_TOOLS).toHaveLength(12); }); test("all tool names follow doclea_ prefix convention", () => { for (const tool of REGISTERED_TOOLS) { expect(tool.startsWith("doclea_")).toBe(true); } }); test("tool names are unique", () => { const unique = new Set(REGISTERED_TOOLS); expect(unique.size).toBe(REGISTERED_TOOLS.length); }); describe("tool categorization", () => { const MEMORY_TOOLS = [ "doclea_store", "doclea_search", "doclea_get", "doclea_update", "doclea_delete", ]; const GIT_TOOLS = [ "doclea_commit_message", "doclea_pr_description", "doclea_changelog", ]; const EXPERTISE_TOOLS = ["doclea_expertise", "doclea_suggest_reviewers"]; const BOOTSTRAP_TOOLS = ["doclea_init", "doclea_import"]; test("has 5 memory tools", () => { expect(MEMORY_TOOLS).toHaveLength(5); }); test("has 3 git tools", () => { expect(GIT_TOOLS).toHaveLength(3); }); test("has 2 expertise tools", () => { expect(EXPERTISE_TOOLS).toHaveLength(2); }); test("has 2 bootstrap tools", () => { expect(BOOTSTRAP_TOOLS).toHaveLength(2); }); test("categories cover all tools", () => { const all = [ ...MEMORY_TOOLS, ...GIT_TOOLS, ...EXPERTISE_TOOLS, ...BOOTSTRAP_TOOLS, ]; expect(all).toHaveLength(REGISTERED_TOOLS.length); }); }); }); describe("doclea_store schema", () => { const storeSchema = z.object({ type: MemoryTypeEnum, title: z.string(), content: z.string(), summary: z.string().optional(), importance: z.number().min(0).max(1).optional(), tags: z.array(z.string()).optional(), relatedFiles: z.array(z.string()).optional(), gitCommit: z.string().optional(), sourcePr: z.string().optional(), experts: z.array(z.string()).optional(), }); test("validates complete input", () => { const input = { type: "decision", title: "Use Bun runtime", content: "We decided to use Bun for faster builds", summary: "Bun for performance", importance: 0.8, tags: ["runtime", "performance"], relatedFiles: ["package.json"], gitCommit: "abc123", sourcePr: "#42", experts: ["alice@example.com"], }; expect(storeSchema.safeParse(input).success).toBe(true); }); test("validates minimal input", () => { const input = { type: "note", title: "Quick note", content: "Something to remember", }; expect(storeSchema.safeParse(input).success).toBe(true); }); test("rejects missing type", () => { const input = { title: "Test", content: "Content" }; expect(storeSchema.safeParse(input).success).toBe(false); }); test("rejects missing title", () => { const input = { type: "decision", content: "Content" }; expect(storeSchema.safeParse(input).success).toBe(false); }); test("rejects missing content", () => { const input = { type: "decision", title: "Test" }; expect(storeSchema.safeParse(input).success).toBe(false); }); test("rejects invalid type", () => { const input = { type: "invalid", title: "Test", content: "Content" }; expect(storeSchema.safeParse(input).success).toBe(false); }); test("rejects importance below 0", () => { const input = { type: "decision", title: "Test", content: "Content", importance: -0.1, }; expect(storeSchema.safeParse(input).success).toBe(false); }); test("rejects importance above 1", () => { const input = { type: "decision", title: "Test", content: "Content", importance: 1.1, }; expect(storeSchema.safeParse(input).success).toBe(false); }); test("accepts importance at boundaries", () => { expect( storeSchema.safeParse({ type: "note", title: "T", content: "C", importance: 0, }).success, ).toBe(true); expect( storeSchema.safeParse({ type: "note", title: "T", content: "C", importance: 1, }).success, ).toBe(true); }); test("rejects non-array tags", () => { const input = { type: "decision", title: "Test", content: "Content", tags: "not-array", }; expect(storeSchema.safeParse(input).success).toBe(false); }); test("accepts empty arrays", () => { const input = { type: "decision", title: "Test", content: "Content", tags: [], relatedFiles: [], experts: [], }; expect(storeSchema.safeParse(input).success).toBe(true); }); }); describe("doclea_search schema", () => { const searchSchema = z.object({ query: z.string(), type: MemoryTypeEnum.optional(), tags: z.array(z.string()).optional(), minImportance: z.number().min(0).max(1).optional(), limit: z.number().min(1).max(50).optional(), }); test("validates query-only input", () => { const input = { query: "authentication" }; expect(searchSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { query: "database decisions", type: "decision", tags: ["database", "sql"], minImportance: 0.5, limit: 20, }; expect(searchSchema.safeParse(input).success).toBe(true); }); test("rejects missing query", () => { const input = { type: "decision" }; expect(searchSchema.safeParse(input).success).toBe(false); }); test("rejects limit below 1", () => { const input = { query: "test", limit: 0 }; expect(searchSchema.safeParse(input).success).toBe(false); }); test("rejects limit above 50", () => { const input = { query: "test", limit: 51 }; expect(searchSchema.safeParse(input).success).toBe(false); }); test("accepts limit at boundaries", () => { expect(searchSchema.safeParse({ query: "t", limit: 1 }).success).toBe( true, ); expect(searchSchema.safeParse({ query: "t", limit: 50 }).success).toBe( true, ); }); }); describe("doclea_get schema", () => { const getSchema = z.object({ id: z.string(), }); test("validates id input", () => { const input = { id: "mem_abc123" }; expect(getSchema.safeParse(input).success).toBe(true); }); test("rejects missing id", () => { const input = {}; expect(getSchema.safeParse(input).success).toBe(false); }); test("rejects non-string id", () => { const input = { id: 123 }; expect(getSchema.safeParse(input).success).toBe(false); }); }); describe("doclea_update schema", () => { const updateSchema = z.object({ id: z.string(), type: MemoryTypeEnum.optional(), title: z.string().optional(), content: z.string().optional(), summary: z.string().optional(), importance: z.number().min(0).max(1).optional(), tags: z.array(z.string()).optional(), relatedFiles: z.array(z.string()).optional(), }); test("validates id-only input", () => { const input = { id: "mem_abc123" }; expect(updateSchema.safeParse(input).success).toBe(true); }); test("validates partial update", () => { const input = { id: "mem_abc123", title: "Updated title", importance: 0.9, }; expect(updateSchema.safeParse(input).success).toBe(true); }); test("validates full update", () => { const input = { id: "mem_abc123", type: "solution", title: "New title", content: "New content", summary: "New summary", importance: 0.7, tags: ["new", "tags"], relatedFiles: ["new/file.ts"], }; expect(updateSchema.safeParse(input).success).toBe(true); }); test("rejects missing id", () => { const input = { title: "Test" }; expect(updateSchema.safeParse(input).success).toBe(false); }); }); describe("doclea_delete schema", () => { const deleteSchema = z.object({ id: z.string(), }); test("validates id input", () => { const input = { id: "mem_abc123" }; expect(deleteSchema.safeParse(input).success).toBe(true); }); test("rejects missing id", () => { const input = {}; expect(deleteSchema.safeParse(input).success).toBe(false); }); }); describe("doclea_commit_message schema", () => { const commitSchema = z.object({ diff: z.string().optional(), projectPath: z.string().optional(), }); test("validates empty input", () => { const input = {}; expect(commitSchema.safeParse(input).success).toBe(true); }); test("validates diff input", () => { const input = { diff: "--- a/file.ts\n+++ b/file.ts\n@@ ..." }; expect(commitSchema.safeParse(input).success).toBe(true); }); test("validates with projectPath", () => { const input = { projectPath: "/home/user/project" }; expect(commitSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { diff: "diff content", projectPath: "/project", }; expect(commitSchema.safeParse(input).success).toBe(true); }); }); describe("doclea_pr_description schema", () => { const prSchema = z.object({ branch: z.string().optional(), base: z.string().optional(), projectPath: z.string().optional(), }); test("validates empty input", () => { const input = {}; expect(prSchema.safeParse(input).success).toBe(true); }); test("validates branch input", () => { const input = { branch: "feature/new-feature" }; expect(prSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { branch: "feature/test", base: "develop", projectPath: "/project", }; expect(prSchema.safeParse(input).success).toBe(true); }); }); describe("doclea_changelog schema", () => { const changelogSchema = z.object({ fromRef: z.string(), toRef: z.string().optional(), projectPath: z.string().optional(), format: z.enum(["markdown", "json"]).optional(), audience: z.enum(["developers", "users"]).optional(), }); test("validates fromRef-only input", () => { const input = { fromRef: "v1.0.0" }; expect(changelogSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { fromRef: "v1.0.0", toRef: "v2.0.0", projectPath: "/project", format: "markdown", audience: "users", }; expect(changelogSchema.safeParse(input).success).toBe(true); }); test("rejects missing fromRef", () => { const input = { toRef: "v2.0.0" }; expect(changelogSchema.safeParse(input).success).toBe(false); }); test("rejects invalid format", () => { const input = { fromRef: "v1.0.0", format: "html" }; expect(changelogSchema.safeParse(input).success).toBe(false); }); test("rejects invalid audience", () => { const input = { fromRef: "v1.0.0", audience: "managers" }; expect(changelogSchema.safeParse(input).success).toBe(false); }); }); describe("doclea_expertise schema", () => { const expertiseSchema = z.object({ path: z.string().optional(), projectPath: z.string().optional(), depth: z.number().min(1).max(5).optional(), includeStale: z.boolean().optional(), busFactorThreshold: z.number().min(50).max(100).optional(), }); test("validates empty input", () => { const input = {}; expect(expertiseSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { path: "src/", projectPath: "/project", depth: 3, includeStale: false, busFactorThreshold: 70, }; expect(expertiseSchema.safeParse(input).success).toBe(true); }); test("rejects depth below 1", () => { const input = { depth: 0 }; expect(expertiseSchema.safeParse(input).success).toBe(false); }); test("rejects depth above 5", () => { const input = { depth: 6 }; expect(expertiseSchema.safeParse(input).success).toBe(false); }); test("rejects busFactorThreshold below 50", () => { const input = { busFactorThreshold: 49 }; expect(expertiseSchema.safeParse(input).success).toBe(false); }); test("rejects busFactorThreshold above 100", () => { const input = { busFactorThreshold: 101 }; expect(expertiseSchema.safeParse(input).success).toBe(false); }); test("accepts boundaries", () => { expect( expertiseSchema.safeParse({ depth: 1, busFactorThreshold: 50 }).success, ).toBe(true); expect( expertiseSchema.safeParse({ depth: 5, busFactorThreshold: 100 }) .success, ).toBe(true); }); }); describe("doclea_suggest_reviewers schema", () => { const reviewersSchema = z.object({ files: z.array(z.string()), projectPath: z.string().optional(), excludeAuthors: z.array(z.string()).optional(), limit: z.number().min(1).max(10).optional(), }); test("validates files-only input", () => { const input = { files: ["src/index.ts"] }; expect(reviewersSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { files: ["src/index.ts", "src/utils.ts"], projectPath: "/project", excludeAuthors: ["pr-author@example.com"], limit: 5, }; expect(reviewersSchema.safeParse(input).success).toBe(true); }); test("rejects missing files", () => { const input = { limit: 3 }; expect(reviewersSchema.safeParse(input).success).toBe(false); }); test("rejects empty files array", () => { // Empty array is technically valid for z.array() const input = { files: [] }; expect(reviewersSchema.safeParse(input).success).toBe(true); }); test("rejects limit below 1", () => { const input = { files: ["file.ts"], limit: 0 }; expect(reviewersSchema.safeParse(input).success).toBe(false); }); test("rejects limit above 10", () => { const input = { files: ["file.ts"], limit: 11 }; expect(reviewersSchema.safeParse(input).success).toBe(false); }); }); describe("doclea_init schema", () => { const initSchema = z.object({ projectPath: z.string().optional(), scanGit: z.boolean().optional(), scanDocs: z.boolean().optional(), scanCode: z.boolean().optional(), scanCommits: z.number().min(10).max(2000).optional(), dryRun: z.boolean().optional(), }); test("validates empty input", () => { const input = {}; expect(initSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { projectPath: "/project", scanGit: true, scanDocs: false, scanCode: true, scanCommits: 1000, dryRun: true, }; expect(initSchema.safeParse(input).success).toBe(true); }); test("rejects scanCommits below 10", () => { const input = { scanCommits: 9 }; expect(initSchema.safeParse(input).success).toBe(false); }); test("rejects scanCommits above 2000", () => { const input = { scanCommits: 2001 }; expect(initSchema.safeParse(input).success).toBe(false); }); test("accepts scanCommits at boundaries", () => { expect(initSchema.safeParse({ scanCommits: 10 }).success).toBe(true); expect(initSchema.safeParse({ scanCommits: 2000 }).success).toBe(true); }); }); describe("doclea_import schema", () => { const importSchema = z.object({ source: z.enum(["markdown", "adr"]), path: z.string(), projectPath: z.string().optional(), recursive: z.boolean().optional(), dryRun: z.boolean().optional(), }); test("validates minimal input", () => { const input = { source: "markdown", path: "docs/" }; expect(importSchema.safeParse(input).success).toBe(true); }); test("validates complete input", () => { const input = { source: "adr", path: "docs/adr", projectPath: "/project", recursive: true, dryRun: false, }; expect(importSchema.safeParse(input).success).toBe(true); }); test("rejects missing source", () => { const input = { path: "docs/" }; expect(importSchema.safeParse(input).success).toBe(false); }); test("rejects missing path", () => { const input = { source: "markdown" }; expect(importSchema.safeParse(input).success).toBe(false); }); test("rejects invalid source", () => { const input = { source: "json", path: "docs/" }; expect(importSchema.safeParse(input).success).toBe(false); }); }); describe("response format", () => { interface MCPResponse { content: Array<{ type: string; text: string }>; isError?: boolean; } function createSuccessResponse(data: unknown): MCPResponse { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } function createErrorResponse(message: string): MCPResponse { return { content: [{ type: "text", text: message }], isError: true, }; } function createMultiPartResponse(parts: string[]): MCPResponse { return { content: parts.map((text) => ({ type: "text", text })), }; } test("success response has content array", () => { const response = createSuccessResponse({ id: "mem_123" }); expect(Array.isArray(response.content)).toBe(true); }); test("success response content has text type", () => { const response = createSuccessResponse({ id: "mem_123" }); expect(response.content[0].type).toBe("text"); }); test("success response serializes data as JSON", () => { const data = { id: "mem_123", title: "Test" }; const response = createSuccessResponse(data); expect(JSON.parse(response.content[0].text)).toEqual(data); }); test("error response has isError flag", () => { const response = createErrorResponse("Not found"); expect(response.isError).toBe(true); }); test("error response contains message", () => { const response = createErrorResponse("Memory not found"); expect(response.content[0].text).toBe("Memory not found"); }); test("multi-part response has multiple content items", () => { const response = createMultiPartResponse([ "Summary text", "---", '{"data": true}', ]); expect(response.content).toHaveLength(3); }); test("all content items have type text", () => { const response = createMultiPartResponse(["A", "B", "C"]); for (const item of response.content) { expect(item.type).toBe("text"); } }); }); describe("default values", () => { test("doclea_store defaults importance to 0.5", () => { const args = { importance: undefined }; const defaulted = args.importance ?? 0.5; expect(defaulted).toBe(0.5); }); test("doclea_store defaults arrays to empty", () => { const args = { tags: undefined, relatedFiles: undefined, experts: undefined, }; expect(args.tags ?? []).toEqual([]); expect(args.relatedFiles ?? []).toEqual([]); expect(args.experts ?? []).toEqual([]); }); test("doclea_search defaults limit to 10", () => { const args = { limit: undefined }; expect(args.limit ?? 10).toBe(10); }); test("doclea_pr_description defaults base to main", () => { const args = { base: undefined }; expect(args.base ?? "main").toBe("main"); }); test("doclea_changelog defaults toRef to HEAD", () => { const args = { toRef: undefined }; expect(args.toRef ?? "HEAD").toBe("HEAD"); }); test("doclea_changelog defaults format to markdown", () => { const args = { format: undefined }; expect(args.format ?? "markdown").toBe("markdown"); }); test("doclea_changelog defaults audience to developers", () => { const args = { audience: undefined }; expect(args.audience ?? "developers").toBe("developers"); }); test("doclea_expertise defaults depth to 2", () => { const args = { depth: undefined }; expect(args.depth ?? 2).toBe(2); }); test("doclea_expertise defaults includeStale to true", () => { const args = { includeStale: undefined }; expect(args.includeStale ?? true).toBe(true); }); test("doclea_expertise defaults busFactorThreshold to 80", () => { const args = { busFactorThreshold: undefined }; expect(args.busFactorThreshold ?? 80).toBe(80); }); test("doclea_suggest_reviewers defaults excludeAuthors to empty", () => { const args = { excludeAuthors: undefined }; expect(args.excludeAuthors ?? []).toEqual([]); }); test("doclea_suggest_reviewers defaults limit to 3", () => { const args = { limit: undefined }; expect(args.limit ?? 3).toBe(3); }); test("doclea_init defaults scanGit to true", () => { const args = { scanGit: undefined }; expect(args.scanGit ?? true).toBe(true); }); test("doclea_init defaults scanDocs to true", () => { const args = { scanDocs: undefined }; expect(args.scanDocs ?? true).toBe(true); }); test("doclea_init defaults scanCode to true", () => { const args = { scanCode: undefined }; expect(args.scanCode ?? true).toBe(true); }); test("doclea_init defaults scanCommits to 500", () => { const args = { scanCommits: undefined }; expect(args.scanCommits ?? 500).toBe(500); }); test("doclea_init defaults dryRun to false", () => { const args = { dryRun: undefined }; expect(args.dryRun ?? false).toBe(false); }); test("doclea_import defaults recursive to true", () => { const args = { recursive: undefined }; expect(args.recursive ?? true).toBe(true); }); test("doclea_import defaults dryRun to false", () => { const args = { dryRun: undefined }; expect(args.dryRun ?? false).toBe(false); }); }); describe("memory type enum", () => { test("has 5 valid types", () => { const types = MemoryTypeEnum.options; expect(types).toHaveLength(5); }); test("includes decision", () => { expect(MemoryTypeEnum.safeParse("decision").success).toBe(true); }); test("includes solution", () => { expect(MemoryTypeEnum.safeParse("solution").success).toBe(true); }); test("includes pattern", () => { expect(MemoryTypeEnum.safeParse("pattern").success).toBe(true); }); test("includes architecture", () => { expect(MemoryTypeEnum.safeParse("architecture").success).toBe(true); }); test("includes note", () => { expect(MemoryTypeEnum.safeParse("note").success).toBe(true); }); test("rejects invalid type", () => { expect(MemoryTypeEnum.safeParse("invalid").success).toBe(false); }); test("rejects empty string", () => { expect(MemoryTypeEnum.safeParse("").success).toBe(false); }); test("rejects null", () => { expect(MemoryTypeEnum.safeParse(null).success).toBe(false); }); }); describe("server configuration", () => { const SERVER_CONFIG = { name: "doclea-mcp", version: "0.0.1", }; test("has correct server name", () => { expect(SERVER_CONFIG.name).toBe("doclea-mcp"); }); test("has version", () => { expect(SERVER_CONFIG.version).toBeDefined(); }); test("version follows semver format", () => { expect(/^\d+\.\d+\.\d+/.test(SERVER_CONFIG.version)).toBe(true); }); }); });

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/docleaai/doclea-mcp'

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