#
tokens: 6446/50000 12/12 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .prettierrc
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│   ├── constants.ts
│   ├── file-system.ts
│   ├── index.ts
│   ├── prompts.ts
│   ├── schemas.ts
│   └── types.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
build/
node_modules/

```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "singleQuote": true,
  "trailingComma": "none",
  "bracketSpacing": true,
  "proseWrap": "preserve",
  "semi": false,
  "printWidth": 80,
  "plugins": ["prettier-plugin-organize-imports"]
}

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Obsidian iCloud MCP

Connecting Obsidian Vaults that are stored in iCloud Drive to AI via the Model Context Protocol (MCP).

> [!WARNING]
> Obsidian iCloud MCP is fully tested on MacOS. If you are using Windows or Linux, please test it and let me know if it works.

## Usage with Claude Desktop

Add this to your [`claude_desktop_config.json`](https://modelcontextprotocol.io/quickstart/user):

### Debugging in Development

```json
{
  "mcpServers": {
    "obsidian-mcp": {
      "command": "node",
      "args": [
        "/path/to/obsidian-mcp/build/index.js",
        "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_1>",
        "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_2>"
      ]
    }
  }
}
```

Using [`npx @modelcontextprotocol/inspector node path/to/server/index.js arg1 arg2 arg3 arg...`](https://modelcontextprotocol.io/docs/tools/inspector) to inspect servers locally developed.

### Using in Production

```json
{
  "mcpServers": {
    "obsidian-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "obsidian-mcp",
        "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_1>",
        "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_2>"
      ]
    }
  }
}
```

```

--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------

```typescript
export const MCP_SERVER_NAME = 'obsidian-mcp'

export const MCP_SERVER_VERSION = '1.0.0'

```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
export interface Resource {
  [x: string]: unknown
  name: string
  uri: string
  description?: string
  mimeType?: string
}

export interface DirectoryNode {
  name: string
  type: 'directory'
  children: (DirectoryNode | FileNode)[]
}

export interface FileNode {
  name: string
  type: 'file'
  uri: string
  mimeType: string
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import js from '@eslint/js'
import { defineConfig } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'

export default defineConfig([
  { files: ['**/*.{js,mjs,cjs,ts}'] },
  {
    files: ['**/*.{js,mjs,cjs,ts}'],
    languageOptions: { globals: globals.browser }
  },
  {
    files: ['**/*.{js,mjs,cjs,ts}'],
    plugins: { js },
    extends: ['js/recommended']
  },
  tseslint.configs.recommended
])

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "obsidian-mcp",
  "version": "1.0.0",
  "description": "Connecting Obsidian Vaults that are stored in local to AI via the Model Context Protocol (MCP).",
  "type": "module",
  "bin": {
    "obsidian-mcp": "./build/index.js"
  },
  "scripts": {
    "dev": "tsc --watch",
    "build": "rimraf build && tsc && chmod 755 build/index.js",
    "format": "prettier ./.prettierrc -w ./src",
    "lint": "eslint --fix ./src"
  },
  "files": [
    "build"
  ],
  "keywords": [
    "Obsidian",
    "Model Context Protocol(MCP)"
  ],
  "author": "Yancey Leo <[email protected]>",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "flexsearch": "^0.8.105",
    "glob": "^11.0.1",
    "gray-matter": "^4.0.3",
    "mime": "^4.0.6",
    "remove-markdown": "^0.6.0",
    "rimraf": "^6.0.1",
    "zod": "^3.24.2",
    "zod-to-json-schema": "^3.24.5"
  },
  "devDependencies": {
    "@eslint/js": "^9.23.0",
    "@types/node": "^22.13.11",
    "eslint": "^9.23.0",
    "globals": "^16.0.0",
    "prettier": "^3.5.3",
    "prettier-plugin-organize-imports": "^4.1.0",
    "typescript": "^5.8.2",
    "typescript-eslint": "^8.28.0"
  }
}

```

--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'

export const ReadFileArgsSchema = z.object({
  path: z.string()
})

export const ReadMultipleFilesArgsSchema = z.object({
  paths: z.array(z.string())
})

export const WriteFileArgsSchema = z.object({
  path: z.string(),
  content: z.string()
})

export const RemoveFileArgsSchema = z.object({
  path: z.string()
})

export const RemoveMultipleFilesArgsSchema = z.object({
  paths: z.array(z.string())
})

export const EditFileArgsSchema = z.object({
  path: z.string(),
  newText: z.string(),
  dryRun: z
    .boolean()
    .default(false)
    .describe('Preview changes before real editing.')
})

export const ListDirectoryArgsSchema = z.object({
  path: z.string()
})

export const CreateDirectoryArgsSchema = z.object({
  path: z.string()
})

export const RemoveDirectoryArgsSchema = z.object({
  path: z.string()
})

export const RemoveMultipleDirectoryArgsSchema = z.object({
  paths: z.array(z.string())
})

export const MoveFileArgsSchema = z.object({
  source: z.string(),
  destination: z.string()
})

export const FullTextSearchArgsSchema = z.object({
  query: z.string()
})

export type ToolInput = z.infer<typeof ToolSchema.shape.inputSchema>

```

--------------------------------------------------------------------------------
/src/prompts.ts:
--------------------------------------------------------------------------------

```typescript
export const readFilePrompt = (rootPaths: string[]) =>
  `Your task is to read file from ${rootPaths.join(', ')}. ` +
  'Read the complete contents of a file from the file system. ' +
  'Handles various text encodings and provides detailed error messages ' +
  'if the file cannot be read. Use this tool when you need to examine ' +
  'the contents of a single file. Only works within allowed directories.'

export const readMultipleFilesPrompt = () =>
  'Read the contents of multiple files simultaneously. This is more ' +
  'efficient than reading files one by one when you need to analyze ' +
  "or compare multiple files. Each file's content is returned with its " +
  "path as a reference. Failed reads for individual files won't stop " +
  'the entire operation. Only works within allowed directories.'

export const writeFilePrompt = (rootPaths: string[]) =>
  `Your task is to write file to an appropriate path under ${rootPaths.join(', ')}. ` +
  "The path you'll write should follow user's instruction and make sure it hasn't been occupied." +
  'Create a new file or completely overwrite an existing file with new content. ' +
  'Use with caution as it will overwrite existing files without warning. ' +
  'Handles text content with proper encoding. Only works within allowed directories.'

export const editFilePrompt = (rootPaths: string[]) =>
  `Edit a specific file under ${rootPaths.join(', ')}. ` +
  'Display the modified content to the user for review; the original file will only be updated upon user confirmation. ' +
  'Only works within allowed directories.'

export const removeFilePrompt = () => ''

export const removeMultipleFilesPrompt = () => ''

export const createDirectoryPrompt = () =>
  'Create a new directory or ensure a directory exists. Can create multiple ' +
  'nested directories in one operation. If the directory already exists, ' +
  'this operation will succeed silently. Perfect for setting up directory ' +
  'structures for projects or ensuring required paths exist. Only works within allowed directories.'

export const listDirectoryPrompt = (rootPaths: string[]) =>
  `Your task is to list directory under ${rootPaths.join(', ')}. ` +
  'Get a detailed listing of all files and directories in a specified path. ' +
  'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
  'prefixes. This tool is essential for understanding directory structure and ' +
  'finding specific files within a directory. Only works within allowed directories.'

export const removeDirectoryPrompt = () => ''

export const removeMultipleDirectoryPrompt = () => ''

export const moveFileDirectoryPrompt = () =>
  'Move or rename files and directories. Can move files between directories ' +
  'and rename them in a single operation. If the destination exists, the ' +
  'operation will fail. Works across different directories and can be used ' +
  'for simple renaming within the same directory. Both source and destination must be within allowed directories.'

export const fullTextSearchDirectoryPrompt = () =>
  "Tokenize the user's query and the search engine tool will return relevant contents. " +
  "summarized those contents based on the user's query."

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js'
import {
  createDirectory,
  editFile,
  flattenDirectory,
  fullTextSearch,
  listDirectory,
  moveFile,
  readFile,
  readFileFromUri,
  readMultipleFiles,
  removeDirectory,
  removeFile,
  removeMultipleDirectory,
  removeMultipleFiles,
  writeFile
} from './file-system.js'
import {
  createDirectoryPrompt,
  editFilePrompt,
  fullTextSearchDirectoryPrompt,
  listDirectoryPrompt,
  moveFileDirectoryPrompt,
  readFilePrompt,
  readMultipleFilesPrompt,
  removeDirectoryPrompt,
  removeFilePrompt,
  removeMultipleDirectoryPrompt,
  removeMultipleFilesPrompt,
  writeFilePrompt
} from './prompts.js'
import {
  CreateDirectoryArgsSchema,
  EditFileArgsSchema,
  FullTextSearchArgsSchema,
  ListDirectoryArgsSchema,
  MoveFileArgsSchema,
  ReadFileArgsSchema,
  ReadMultipleFilesArgsSchema,
  RemoveDirectoryArgsSchema,
  RemoveFileArgsSchema,
  RemoveMultipleDirectoryArgsSchema,
  RemoveMultipleFilesArgsSchema,
  ToolInput,
  WriteFileArgsSchema
} from './schemas.js'

const server = new Server(
  {
    name: MCP_SERVER_NAME,
    version: MCP_SERVER_VERSION
  },
  {
    capabilities: {
      tools: {},
      resources: {},
      prompts: {}
    }
  }
)

const args = process.argv.slice(2)
if (args.length === 0) {
  console.error(
    `Usage: ${MCP_SERVER_NAME} <obsidian-directory> [additional-directories...]`
  )
  process.exit(1)
}

server.setRequestHandler(ListResourcesRequestSchema, async () => {
  const resources = (
    await Promise.all(args.map((arg) => flattenDirectory(arg)))
  ).flat()
  return {
    resources
  }
})

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const content = await readFileFromUri(request.params.uri)

  if (content === null) throw new Error('Error reading file from URL')
  return content
})

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'read_file',
        description: readFilePrompt(args),
        inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
      },
      {
        name: 'read_multiple_files',
        description: readMultipleFilesPrompt(),
        inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput
      },
      {
        name: 'write_file',
        description: writeFilePrompt(args),
        inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
      },
      {
        name: 'edit_file',
        description: editFilePrompt(args),
        inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
      },
      {
        name: 'remove_file',
        description: removeFilePrompt(),
        inputSchema: zodToJsonSchema(RemoveFileArgsSchema) as ToolInput
      },
      {
        name: 'remove_multiple_files',
        description: removeMultipleFilesPrompt(),
        inputSchema: zodToJsonSchema(RemoveMultipleFilesArgsSchema) as ToolInput
      },
      {
        name: 'create_directory',
        description: createDirectoryPrompt(),
        inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput
      },
      {
        name: 'list_directory',
        description: listDirectoryPrompt(args),
        inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
      },
      {
        name: 'remove_directory',
        description: removeDirectoryPrompt(),
        inputSchema: zodToJsonSchema(RemoveDirectoryArgsSchema) as ToolInput
      },
      {
        name: 'remove_multiple_directory',
        description: removeMultipleDirectoryPrompt(),
        inputSchema: zodToJsonSchema(
          RemoveMultipleDirectoryArgsSchema
        ) as ToolInput
      },
      {
        name: 'move_file',
        description: moveFileDirectoryPrompt(),
        inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput
      },
      {
        name: 'full_text_search',
        description: fullTextSearchDirectoryPrompt(),
        inputSchema: zodToJsonSchema(FullTextSearchArgsSchema) as ToolInput
      }
    ]
  }
})

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params

    switch (name) {
      case 'read_file': {
        return readFile(args)
      }

      case 'read_multiple_files': {
        return readMultipleFiles(args)
      }

      case 'write_file': {
        return writeFile(args)
      }

      case 'edit_file': {
        return editFile(args)
      }

      case 'remove_file': {
        return removeFile(args)
      }

      case 'remove_multiple_files': {
        return removeMultipleFiles(args)
      }

      case 'create_directory': {
        return createDirectory(args)
      }

      case 'list_directory': {
        return listDirectory(args)
      }

      case 'remove_directory': {
        return removeDirectory(args)
      }

      case 'remove_multiple_directory': {
        return removeMultipleDirectory(args)
      }

      case 'move_file': {
        return moveFile(args)
      }

      case 'full_text_search': {
        return fullTextSearch(args)
      }

      default:
        throw new Error(`Unknown tool: ${name}`)
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error)
    return {
      content: [{ type: 'text', text: `Error: ${errorMessage}` }],
      isError: true
    }
  }
})

async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
}

main().catch((error) => {
  console.error('Fatal error in main():', error)
  process.exit(1)
})

```

--------------------------------------------------------------------------------
/src/file-system.ts:
--------------------------------------------------------------------------------

```typescript
// @ts-expect-error FIXME:
// It says: Could not find a declaration file for module 'flexsearch'. But after installing @type/flexsearch still doesn't work.
import flexsearch from 'flexsearch'
import fs from 'fs/promises'
import { glob } from 'glob'
import matter from 'gray-matter'
import mime from 'mime'
import path from 'path'
import removeMd from 'remove-markdown'
import { rimraf } from 'rimraf'
import { fileURLToPath } from 'url'
import {
  CreateDirectoryArgsSchema,
  EditFileArgsSchema,
  FullTextSearchArgsSchema,
  ListDirectoryArgsSchema,
  MoveFileArgsSchema,
  ReadFileArgsSchema,
  ReadMultipleFilesArgsSchema,
  RemoveDirectoryArgsSchema,
  RemoveFileArgsSchema,
  RemoveMultipleDirectoryArgsSchema,
  RemoveMultipleFilesArgsSchema,
  WriteFileArgsSchema
} from './schemas.js'
import { DirectoryNode, Resource } from './types.js'

export async function flattenDirectory(
  directoryPath: string
): Promise<Resource[]> {
  const flattenedFiles: Resource[] = []

  async function traverseDirectory(currentPath: string, relativeDir: string) {
    try {
      const entries = await fs.readdir(currentPath, { withFileTypes: true })

      for (const entry of entries) {
        const fullPath = path.join(currentPath, entry.name)
        const relativeName = path.join(relativeDir, entry.name)

        if (entry.isFile()) {
          const fileUrl = new URL(`file://${path.resolve(fullPath)}`).toString()
          const mimeType = mime.getType(fullPath) || 'application/octet-stream'

          flattenedFiles.push({
            uri: fileUrl,
            name: entry.name,
            mimeType
          })
        } else if (entry.isDirectory()) {
          await traverseDirectory(fullPath, relativeName)
        }
      }
    } catch (error) {
      console.error(
        `Error reading directory ${currentPath}:`,
        error instanceof Error ? error.message : error
      )
    }
  }

  const absoluteDirectoryPath = path.resolve(directoryPath)
  await traverseDirectory(
    absoluteDirectoryPath,
    path.basename(absoluteDirectoryPath)
  )

  return flattenedFiles
}

export async function readFileFromUri(fileUri: string) {
  try {
    const fileUrl = new URL(fileUri)
    if (fileUrl.protocol !== 'file:') {
      throw new Error('Invalid URL protocol. Only file:// URLs are supported.')
    }
    const filePath = fileURLToPath(fileUrl)
    const content = await fs.readFile(filePath, 'utf-8')
    return {
      contents: [
        {
          uri: fileUri,
          mimeType: mime.getType(filePath) || 'application/octet-stream',
          text: content
        }
      ]
    }
  } catch (error) {
    console.error(
      `Error reading file ${fileUri}:`,
      error instanceof Error ? error.message : error
    )

    return null
  }
}

export async function getDirectoryTree(
  directoryPath: string
): Promise<DirectoryNode | null> {
  async function traverseDirectory(
    currentPath: string,
    currentName: string
  ): Promise<DirectoryNode | null> {
    try {
      const entries = await fs.readdir(currentPath, { withFileTypes: true })
      const node: DirectoryNode = {
        name: currentName,
        type: 'directory',
        children: []
      }

      for (const entry of entries) {
        const fullPath = path.join(currentPath, entry.name)

        if (entry.isFile()) {
          const fileUrl = new URL(`file://${path.resolve(fullPath)}`).toString()
          const mimeType = mime.getType(fullPath) || 'application/octet-stream'
          node.children.push({
            name: entry.name,
            type: 'file',
            uri: fileUrl,
            mimeType
          })
        } else if (entry.isDirectory()) {
          const childNode = await traverseDirectory(fullPath, entry.name)
          if (childNode) {
            node.children.push(childNode)
          }
        }
      }
      return node
    } catch (error) {
      console.error(
        `Error reading directory ${currentPath}:`,
        error instanceof Error ? error.message : error
      )
      return null
    }
  }

  try {
    const absoluteDirectoryPath = path.resolve(directoryPath)
    const baseName = path.basename(absoluteDirectoryPath)
    const tree = await traverseDirectory(absoluteDirectoryPath, baseName)
    return tree
  } catch (error) {
    console.error(
      `Error processing directory ${directoryPath}:`,
      error instanceof Error ? error.message : error
    )
    return null
  }
}

export async function getFileStats(filePath: string) {
  try {
    const stats = await fs.stat(filePath)
    return {
      size: stats.size,
      created: stats.birthtime,
      modified: stats.mtime,
      accessed: stats.atime,
      isDirectory: stats.isDirectory(),
      isFile: stats.isFile(),
      permissions: stats.mode.toString(8).slice(-3)
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error)
    return {
      content: [{ type: 'text', text: `Error: ${errorMessage}` }],
      isError: true
    }
  }
}

export async function getAllMarkdownPaths(rootPaths: string[]) {
  const filePaths = (
    await Promise.all(rootPaths.map((rootPath) => glob(`${rootPath}/**/*.md`)))
  ).flat()

  return filePaths
}

export async function readMarkdown(filePath: string) {
  const content = await fs.readFile(filePath, 'utf-8')
  const frontMatter = matter(content)

  return {
    id: filePath,
    title:
      (frontMatter.data.title as string | undefined) ??
      path.basename(filePath, '.md'),
    content: removeMd(content)
  }
}

export async function readAllMarkdowns(filePaths: string[]) {
  const markdowns = await Promise.all(
    filePaths.map((filePath) => readMarkdown(filePath))
  )

  return markdowns
}

export async function readFile(args?: Record<string, unknown>) {
  const parsed = ReadFileArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for read_file: ${parsed.error}`)
  }

  const content = await fs.readFile(parsed.data.path, 'utf-8')
  return {
    content: [{ type: 'text', text: content }]
  }
}

export async function readMultipleFiles(args?: Record<string, unknown>) {
  const parsed = ReadMultipleFilesArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(
      `Invalid arguments for read_multiple_files: ${parsed.error}`
    )
  }

  const results = await Promise.all(
    parsed.data.paths.map(async (filePath: string) => {
      const content = await fs.readFile(filePath, 'utf-8')
      return `${filePath}:\n${content}\n`
    })
  )
  return {
    content: [{ type: 'text', text: results.join('\n---\n') }]
  }
}

export async function writeFile(args?: Record<string, unknown>) {
  const parsed = WriteFileArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for write_file: ${parsed.error}`)
  }

  await fs.writeFile(parsed.data.path, parsed.data.content, 'utf-8')
  return {
    content: [
      { type: 'text', text: `Successfully wrote to ${parsed.data.path}` }
    ]
  }
}

export async function editFile(args?: Record<string, unknown>) {
  const parsed = EditFileArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
  }

  if (!parsed.data.dryRun) {
    await fs.writeFile(parsed.data.path, parsed.data.newText)
  }

  return {
    content: [{ type: 'text', text: parsed.data.newText }]
  }
}

export async function removeFile(args?: Record<string, unknown>) {
  const parsed = RemoveFileArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for remove_file: ${parsed.error}`)
  }
  const result = await fs.unlink(parsed.data.path)
  return {
    content: [{ type: 'text', text: result }]
  }
}

export async function removeMultipleFiles(args?: Record<string, unknown>) {
  const parsed = RemoveMultipleFilesArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(
      `Invalid arguments for remove_multiple_files: ${parsed.error}`
    )
  }
  const result = await Promise.all(
    parsed.data.paths.map((path) => fs.unlink(path))
  )

  return {
    content: [{ type: 'text', text: result }]
  }
}

export async function createDirectory(args?: Record<string, unknown>) {
  const parsed = CreateDirectoryArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for create_directory: ${parsed.error}`)
  }
  await fs.mkdir(parsed.data.path, { recursive: true })
  return {
    content: [
      {
        type: 'text',
        text: `Successfully created directory ${parsed.data.path}`
      }
    ]
  }
}

export async function listDirectory(args?: Record<string, unknown>) {
  const parsed = ListDirectoryArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for list_directory: ${parsed.error}`)
  }
  const entries = await fs.readdir(parsed.data.path, {
    withFileTypes: true
  })
  const formatted = entries
    .map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
    .join('\n')
  return {
    content: [{ type: 'text', text: formatted }]
  }
}

export async function removeDirectory(args?: Record<string, unknown>) {
  const parsed = RemoveDirectoryArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for remove_directory: ${parsed.error}`)
  }
  const result = await rimraf(parsed.data.path)
  return {
    content: [{ type: 'text', text: result }]
  }
}

export async function removeMultipleDirectory(args?: Record<string, unknown>) {
  const parsed = RemoveMultipleDirectoryArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
  }
  const result = await Promise.all(
    parsed.data.paths.map((path) => rimraf(path))
  )

  return {
    content: [{ type: 'text', text: result }]
  }
}

export async function moveFile(args?: Record<string, unknown>) {
  const parsed = MoveFileArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for move_file: ${parsed.error}`)
  }
  await fs.rename(parsed.data.source, parsed.data.destination)
  return {
    content: [
      {
        type: 'text',
        text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}`
      }
    ]
  }
}

// TODO: Build index phrase should be mounted on service start, rather than a single request.
export async function fullTextSearch(args?: Record<string, unknown>) {
  const parsed = FullTextSearchArgsSchema.safeParse(args)
  if (!parsed.success) {
    throw new Error(`Invalid arguments for full_text_search: ${parsed.error}`)
  }

  const filePaths = await getAllMarkdownPaths(process.argv.slice(2))
  const documents = await readAllMarkdowns(filePaths)

  const index = new flexsearch.Document({
    document: {
      id: 'id',
      store: true,
      index: [
        {
          field: 'title',
          tokenize: 'forward',
          encoder: flexsearch.Charset.LatinBalance
        },
        {
          field: 'content',
          tokenize: 'forward',
          encoder: flexsearch.Charset.LatinBalance
        }
      ]
    }
  })

  documents.forEach((file) => {
    index.add(file)
  })

  const searchedIds = index.search(parsed.data.query, { limit: 5 })
  const filteredDocuments = documents
    .filter(({ id }) => searchedIds[0].result.includes(id))
    .map((document) => document.content)
  return {
    content: [
      {
        type: 'text',
        text:
          filteredDocuments.length > 0
            ? filteredDocuments.join('\n---\n')
            : 'No matches found'
      }
    ]
  }
}

```