Skip to main content
Glama
server.test.ts7.75 kB
import fs, { copyFile } from "node:fs/promises"; import path from "node:path"; import { Client } from "@modelcontextprotocol/sdk/client"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import type { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from "vitest"; import { type ZodSchema, z } from "zod"; import { OrganizeAttachmentsResultSchema } from "../src/tools/organize_attachments/params"; import { type ListAllDocumentsData, listAllDocumentsDataSchema, } from "../src/tools/vault/types/list_all"; import { readSpecificFileDocumentData } from "../src/tools/vault/types/read_specific"; import { DocumentSchema, SearchSuccessSchema, } from "../src/tools/vault/types/search"; import { FrontMatterSchema } from "../src/utils/processor/types"; import demo_data from "./assets/demo_data"; const TEST_VAULT_PATH = path.join(process.cwd(), "test-vault"); async function parseAndValidateResponse<T extends ZodSchema>( response: CompatibilityCallToolResult, schema: T, ): Promise<z.infer<T>> { expect(response.isError).toBe(false); const responseContent = response.content as { type: string; text: unknown }[]; const responseText = JSON.parse(responseContent[0].text as string); const parsed = schema.safeParse(responseText); if (!parsed.success) { console.error("Schema validation failed:", parsed.error.format()); throw new Error("Response schema validation failed"); } return parsed.data; } describe("Obsidian MCP Server E2E Tests", () => { let mcpClient: Client; let transport: StdioClientTransport; beforeAll(async () => { await fs.mkdir(TEST_VAULT_PATH, { recursive: true }); mcpClient = new Client({ name: "test-client", version: "1.0.0" }); transport = new StdioClientTransport({ command: "node", args: ["build/index.js"], env: { ...process.env, VAULT_DIR_PATH: TEST_VAULT_PATH, NODE_ENV: "test", }, }); await mcpClient.connect(transport); }); afterAll(async () => { if (mcpClient) { await mcpClient.close(); } await fs.rm(TEST_VAULT_PATH, { recursive: true, force: true }); }); beforeEach(async () => { const files = await fs.readdir(TEST_VAULT_PATH); await Promise.all( files.map((file) => fs.rm(path.join(TEST_VAULT_PATH, file), { recursive: true, force: true, }), ), ); for (const { title, tags, content } of demo_data) { const { text } = content; const tagsYaml = tags.map((tag) => ` - ${tag}`).join("\n"); const frontmatter = `---\ntitle: ${title}\ntags:\n${tagsYaml}\n---\n\n`; const fileName = `${title.replace(/[/\\?%*:|"<>]/g, "-")}.md`; const filePath = path.join(TEST_VAULT_PATH, fileName); await fs.writeFile(filePath, frontmatter + text); } }); afterEach(async () => {}); test("서버에 등록된 모든 도구 목록을 가져올 수 있다", async () => { const toolsResult = await mcpClient.listTools(); const toolNames = toolsResult.tools.map((tool) => tool.name); const expectedTools = [ "vault", "create_document_with_properties", "write_property", "generate_property", "organize_attachments", ]; expect(toolNames).toEqual(expect.arrayContaining(expectedTools)); expect(toolNames.length).toBe(expectedTools.length); }); test("vault의 read 액션은 적절하게 문서를 읽어올 수 있는가?", async () => { const ABSOLUTE_PATH = path.join( TEST_VAULT_PATH, "Getting Started with Obsidian MCP Server.md", ); const RELATIVE_PATH = "Getting Started with Obsidian MCP Server.md"; const absoulteResponse = await mcpClient.callTool({ name: "vault", arguments: { action: "read", filename: ABSOLUTE_PATH }, }); const relativeResponse = await mcpClient.callTool({ name: "vault", arguments: { action: "read", filename: RELATIVE_PATH }, }); expect(absoulteResponse.isError).toBe(false); expect(relativeResponse.isError).toBe(false); const absoulteData = await parseAndValidateResponse( absoulteResponse, readSpecificFileDocumentData, ); const relativeData = await parseAndValidateResponse( relativeResponse, readSpecificFileDocumentData, ); expect(absoulteData.contentLength).toBeGreaterThan(0); expect(relativeData.contentLength).toBeGreaterThan(0); expect(absoulteData.contentLength).toBe(relativeData.contentLength); expect(absoulteData.filename).toBe(relativeData.filename); expect(absoulteData.metadata).toEqual(relativeData.metadata); expect(absoulteData.content).toEqual(relativeData.content); }); test("list_all 도구는 vault의 모든 문서 목록을 반환한다", async () => { const response = await mcpClient.callTool({ name: "vault", arguments: { action: "list_all" }, }); const data = (await parseAndValidateResponse( response, listAllDocumentsDataSchema, )) as ListAllDocumentsData; expect(data.vault_overview.total_documents).toBe(demo_data.length); expect(data.documents.length).toBe(demo_data.length); const sortedDocuments = [...data.documents].sort((a, b) => (a.metadata.title || "").localeCompare(b.metadata.title || ""), ); const sortedDemoData = [...demo_data].sort((a, b) => a.title.localeCompare(b.title), ); for (let i = 0; i < sortedDemoData.length; i++) { const demo = sortedDemoData[i]; expect(sortedDocuments[i].metadata.title).toBe(demo.title); expect(sortedDocuments[i].metadata.tags).toEqual(demo.tags); } }); test('search 도구는 "Test Note" 키워드를 기반으로 문서를 찾을 수 있다', async () => { const searchQuery = "Getting Started with Obsidian MCP Server"; const response = await mcpClient.callTool({ name: "vault", arguments: { action: "search", keyword: searchQuery, includeContent: true, }, }); const ProcessedFrontMatterSchema = FrontMatterSchema.extend({ title: z.string(), tags: z.array(z.string()), }); const ProcessedDocumentSchema = DocumentSchema.extend({ metadata: ProcessedFrontMatterSchema, }); const ProcessedSearchSuccessSchema = SearchSuccessSchema.extend({ documents: z.array(ProcessedDocumentSchema), }); const data = await parseAndValidateResponse( response, ProcessedSearchSuccessSchema, ); expect(data.query).toBe(searchQuery); expect(data.found).toBe(1); expect(data.documents.length).toBe(1); const doc = data.documents[0]; expect(doc.filename).toBe(`${searchQuery}.md`); expect(doc.metadata.tags).toEqual(["guide", "initial"]); expect( "excerpt" in doc.content ? doc.content.excerpt : doc.content.preview, ).toBeDefined(); }); test("organize_attachments 도구는 문서의 이미지 파일을 정리할 수 있다", async () => { const sourceImagePath = path.join( process.cwd(), "tests", "assets", "demo_img.png", ); const destinationImagePath = path.join(TEST_VAULT_PATH, "demo_img.png"); await copyFile(sourceImagePath, destinationImagePath); const response = await mcpClient.callTool({ name: "organize_attachments", arguments: { keyword: "Test Note", destination: "images", useTitleAsFolderName: true, }, }); const data = await parseAndValidateResponse( response, OrganizeAttachmentsResultSchema, ); const detail = data.details.find((d) => d.document.includes("Test Note.md"), ); expect(detail?.status).toBe("success"); expect(detail?.movedFiles).toBe(1); expect(detail?.targetDirectory).toBe("images/Test Note"); const movedImagePath = path.join( TEST_VAULT_PATH, "images", "Test Note", "demo_img.png", ); const movedImageStat = await fs.stat(movedImagePath); expect(movedImageStat.isFile()).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/sunub/obsidian-mcp-server'

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