Add Node.js MCP server with 5 vault tools: - open_note / read_note: read markdown file content - create_note: create new note with frontmatter and title - search_notes: search by title or content substring - append_to_note: append text to existing note Uses @modelcontextprotocol/sdk with stdio transport. VAULT_PATH env var configures the vault directory. 10 passing tests covering all operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.4 KiB
JavaScript
116 lines
3.4 KiB
JavaScript
import { describe, it, before, after } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import os from 'node:os'
|
|
import { readNote, createNote, searchNotes, appendToNote, findMarkdownFiles } from './vault.js'
|
|
|
|
let tmpDir
|
|
|
|
before(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'laputa-mcp-test-'))
|
|
|
|
// Create test vault structure
|
|
await fs.mkdir(path.join(tmpDir, 'project'), { recursive: true })
|
|
await fs.mkdir(path.join(tmpDir, 'note'), { recursive: true })
|
|
|
|
await fs.writeFile(path.join(tmpDir, 'project', 'test-project.md'), `---
|
|
title: Test Project
|
|
is_a: Project
|
|
status: Active
|
|
---
|
|
|
|
# Test Project
|
|
|
|
This is a test project for the MCP server.
|
|
`)
|
|
|
|
await fs.writeFile(path.join(tmpDir, 'note', 'daily-log.md'), `---
|
|
title: Daily Log
|
|
is_a: Note
|
|
---
|
|
|
|
# Daily Log
|
|
|
|
Today I worked on the MCP server implementation.
|
|
`)
|
|
})
|
|
|
|
after(async () => {
|
|
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
})
|
|
|
|
describe('findMarkdownFiles', () => {
|
|
it('should find all .md files recursively', async () => {
|
|
const files = await findMarkdownFiles(tmpDir)
|
|
assert.equal(files.length, 2)
|
|
assert.ok(files.some(f => f.endsWith('test-project.md')))
|
|
assert.ok(files.some(f => f.endsWith('daily-log.md')))
|
|
})
|
|
})
|
|
|
|
describe('readNote', () => {
|
|
it('should read a note by relative path', async () => {
|
|
const content = await readNote(tmpDir, 'project/test-project.md')
|
|
assert.ok(content.includes('Test Project'))
|
|
assert.ok(content.includes('is_a: Project'))
|
|
})
|
|
|
|
it('should throw for missing notes', async () => {
|
|
await assert.rejects(
|
|
() => readNote(tmpDir, 'nonexistent.md'),
|
|
{ code: 'ENOENT' }
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('createNote', () => {
|
|
it('should create a note with frontmatter', async () => {
|
|
const absPath = await createNote(tmpDir, 'note/new-note.md', 'My New Note', { is_a: 'Note' })
|
|
assert.ok(absPath.endsWith('new-note.md'))
|
|
|
|
const content = await fs.readFile(absPath, 'utf-8')
|
|
assert.ok(content.includes('title: My New Note'))
|
|
assert.ok(content.includes('is_a: Note'))
|
|
assert.ok(content.includes('# My New Note'))
|
|
})
|
|
|
|
it('should create parent directories', async () => {
|
|
const absPath = await createNote(tmpDir, 'deep/nested/dir/note.md', 'Deep Note')
|
|
const content = await fs.readFile(absPath, 'utf-8')
|
|
assert.ok(content.includes('# Deep Note'))
|
|
})
|
|
})
|
|
|
|
describe('searchNotes', () => {
|
|
it('should find notes matching title', async () => {
|
|
const results = await searchNotes(tmpDir, 'Test Project')
|
|
assert.ok(results.length >= 1)
|
|
assert.equal(results[0].title, 'Test Project')
|
|
})
|
|
|
|
it('should find notes matching content', async () => {
|
|
const results = await searchNotes(tmpDir, 'MCP server')
|
|
assert.ok(results.length >= 1)
|
|
})
|
|
|
|
it('should return empty for no matches', async () => {
|
|
const results = await searchNotes(tmpDir, 'xyzzy-nonexistent-12345')
|
|
assert.equal(results.length, 0)
|
|
})
|
|
|
|
it('should respect limit', async () => {
|
|
const results = await searchNotes(tmpDir, 'note', 1)
|
|
assert.ok(results.length <= 1)
|
|
})
|
|
})
|
|
|
|
describe('appendToNote', () => {
|
|
it('should append text to a note', async () => {
|
|
await appendToNote(tmpDir, 'note/daily-log.md', '## Evening Update\nFinished testing.')
|
|
const content = await readNote(tmpDir, 'note/daily-log.md')
|
|
assert.ok(content.includes('## Evening Update'))
|
|
assert.ok(content.includes('Finished testing.'))
|
|
})
|
|
})
|