#
tokens: 8731/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | build/
2 | node_modules/
3 | 
```

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

```
 1 | {
 2 |   "singleQuote": true,
 3 |   "trailingComma": "none",
 4 |   "bracketSpacing": true,
 5 |   "proseWrap": "preserve",
 6 |   "semi": false,
 7 |   "printWidth": 80,
 8 |   "plugins": ["prettier-plugin-organize-imports"]
 9 | }
10 | 
```

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

```markdown
 1 | # Obsidian iCloud MCP
 2 | 
 3 | Connecting Obsidian Vaults that are stored in iCloud Drive to AI via the Model Context Protocol (MCP).
 4 | 
 5 | > [!WARNING]
 6 | > 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.
 7 | 
 8 | ## Usage with Claude Desktop
 9 | 
10 | Add this to your [`claude_desktop_config.json`](https://modelcontextprotocol.io/quickstart/user):
11 | 
12 | ### Debugging in Development
13 | 
14 | ```json
15 | {
16 |   "mcpServers": {
17 |     "obsidian-mcp": {
18 |       "command": "node",
19 |       "args": [
20 |         "/path/to/obsidian-mcp/build/index.js",
21 |         "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_1>",
22 |         "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_2>"
23 |       ]
24 |     }
25 |   }
26 | }
27 | ```
28 | 
29 | 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.
30 | 
31 | ### Using in Production
32 | 
33 | ```json
34 | {
35 |   "mcpServers": {
36 |     "obsidian-mcp": {
37 |       "command": "npx",
38 |       "args": [
39 |         "-y",
40 |         "obsidian-mcp",
41 |         "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_1>",
42 |         "/Users/<USERNAME>/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/<VAULT_NAME_2>"
43 |       ]
44 |     }
45 |   }
46 | }
47 | ```
48 | 
```

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

```typescript
1 | export const MCP_SERVER_NAME = 'obsidian-mcp'
2 | 
3 | export const MCP_SERVER_VERSION = '1.0.0'
4 | 
```

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

```typescript
 1 | export interface Resource {
 2 |   [x: string]: unknown
 3 |   name: string
 4 |   uri: string
 5 |   description?: string
 6 |   mimeType?: string
 7 | }
 8 | 
 9 | export interface DirectoryNode {
10 |   name: string
11 |   type: 'directory'
12 |   children: (DirectoryNode | FileNode)[]
13 | }
14 | 
15 | export interface FileNode {
16 |   name: string
17 |   type: 'file'
18 |   uri: string
19 |   mimeType: string
20 | }
21 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": [
14 |     "src/**/*"
15 |   ],
16 |   "exclude": [
17 |     "node_modules"
18 |   ]
19 | }
```

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

```javascript
 1 | import js from '@eslint/js'
 2 | import { defineConfig } from 'eslint/config'
 3 | import globals from 'globals'
 4 | import tseslint from 'typescript-eslint'
 5 | 
 6 | export default defineConfig([
 7 |   { files: ['**/*.{js,mjs,cjs,ts}'] },
 8 |   {
 9 |     files: ['**/*.{js,mjs,cjs,ts}'],
10 |     languageOptions: { globals: globals.browser }
11 |   },
12 |   {
13 |     files: ['**/*.{js,mjs,cjs,ts}'],
14 |     plugins: { js },
15 |     extends: ['js/recommended']
16 |   },
17 |   tseslint.configs.recommended
18 | ])
19 | 
```

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

```json
 1 | {
 2 |   "name": "obsidian-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "Connecting Obsidian Vaults that are stored in local to AI via the Model Context Protocol (MCP).",
 5 |   "type": "module",
 6 |   "bin": {
 7 |     "obsidian-mcp": "./build/index.js"
 8 |   },
 9 |   "scripts": {
10 |     "dev": "tsc --watch",
11 |     "build": "rimraf build && tsc && chmod 755 build/index.js",
12 |     "format": "prettier ./.prettierrc -w ./src",
13 |     "lint": "eslint --fix ./src"
14 |   },
15 |   "files": [
16 |     "build"
17 |   ],
18 |   "keywords": [
19 |     "Obsidian",
20 |     "Model Context Protocol(MCP)"
21 |   ],
22 |   "author": "Yancey Leo <[email protected]>",
23 |   "license": "MIT",
24 |   "dependencies": {
25 |     "@modelcontextprotocol/sdk": "^1.7.0",
26 |     "flexsearch": "^0.8.105",
27 |     "glob": "^11.0.1",
28 |     "gray-matter": "^4.0.3",
29 |     "mime": "^4.0.6",
30 |     "remove-markdown": "^0.6.0",
31 |     "rimraf": "^6.0.1",
32 |     "zod": "^3.24.2",
33 |     "zod-to-json-schema": "^3.24.5"
34 |   },
35 |   "devDependencies": {
36 |     "@eslint/js": "^9.23.0",
37 |     "@types/node": "^22.13.11",
38 |     "eslint": "^9.23.0",
39 |     "globals": "^16.0.0",
40 |     "prettier": "^3.5.3",
41 |     "prettier-plugin-organize-imports": "^4.1.0",
42 |     "typescript": "^5.8.2",
43 |     "typescript-eslint": "^8.28.0"
44 |   }
45 | }
46 | 
```

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

```typescript
 1 | import { ToolSchema } from '@modelcontextprotocol/sdk/types.js'
 2 | import { z } from 'zod'
 3 | 
 4 | export const ReadFileArgsSchema = z.object({
 5 |   path: z.string()
 6 | })
 7 | 
 8 | export const ReadMultipleFilesArgsSchema = z.object({
 9 |   paths: z.array(z.string())
10 | })
11 | 
12 | export const WriteFileArgsSchema = z.object({
13 |   path: z.string(),
14 |   content: z.string()
15 | })
16 | 
17 | export const RemoveFileArgsSchema = z.object({
18 |   path: z.string()
19 | })
20 | 
21 | export const RemoveMultipleFilesArgsSchema = z.object({
22 |   paths: z.array(z.string())
23 | })
24 | 
25 | export const EditFileArgsSchema = z.object({
26 |   path: z.string(),
27 |   newText: z.string(),
28 |   dryRun: z
29 |     .boolean()
30 |     .default(false)
31 |     .describe('Preview changes before real editing.')
32 | })
33 | 
34 | export const ListDirectoryArgsSchema = z.object({
35 |   path: z.string()
36 | })
37 | 
38 | export const CreateDirectoryArgsSchema = z.object({
39 |   path: z.string()
40 | })
41 | 
42 | export const RemoveDirectoryArgsSchema = z.object({
43 |   path: z.string()
44 | })
45 | 
46 | export const RemoveMultipleDirectoryArgsSchema = z.object({
47 |   paths: z.array(z.string())
48 | })
49 | 
50 | export const MoveFileArgsSchema = z.object({
51 |   source: z.string(),
52 |   destination: z.string()
53 | })
54 | 
55 | export const FullTextSearchArgsSchema = z.object({
56 |   query: z.string()
57 | })
58 | 
59 | export type ToolInput = z.infer<typeof ToolSchema.shape.inputSchema>
60 | 
```

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

```typescript
 1 | export const readFilePrompt = (rootPaths: string[]) =>
 2 |   `Your task is to read file from ${rootPaths.join(', ')}. ` +
 3 |   'Read the complete contents of a file from the file system. ' +
 4 |   'Handles various text encodings and provides detailed error messages ' +
 5 |   'if the file cannot be read. Use this tool when you need to examine ' +
 6 |   'the contents of a single file. Only works within allowed directories.'
 7 | 
 8 | export const readMultipleFilesPrompt = () =>
 9 |   'Read the contents of multiple files simultaneously. This is more ' +
10 |   'efficient than reading files one by one when you need to analyze ' +
11 |   "or compare multiple files. Each file's content is returned with its " +
12 |   "path as a reference. Failed reads for individual files won't stop " +
13 |   'the entire operation. Only works within allowed directories.'
14 | 
15 | export const writeFilePrompt = (rootPaths: string[]) =>
16 |   `Your task is to write file to an appropriate path under ${rootPaths.join(', ')}. ` +
17 |   "The path you'll write should follow user's instruction and make sure it hasn't been occupied." +
18 |   'Create a new file or completely overwrite an existing file with new content. ' +
19 |   'Use with caution as it will overwrite existing files without warning. ' +
20 |   'Handles text content with proper encoding. Only works within allowed directories.'
21 | 
22 | export const editFilePrompt = (rootPaths: string[]) =>
23 |   `Edit a specific file under ${rootPaths.join(', ')}. ` +
24 |   'Display the modified content to the user for review; the original file will only be updated upon user confirmation. ' +
25 |   'Only works within allowed directories.'
26 | 
27 | export const removeFilePrompt = () => ''
28 | 
29 | export const removeMultipleFilesPrompt = () => ''
30 | 
31 | export const createDirectoryPrompt = () =>
32 |   'Create a new directory or ensure a directory exists. Can create multiple ' +
33 |   'nested directories in one operation. If the directory already exists, ' +
34 |   'this operation will succeed silently. Perfect for setting up directory ' +
35 |   'structures for projects or ensuring required paths exist. Only works within allowed directories.'
36 | 
37 | export const listDirectoryPrompt = (rootPaths: string[]) =>
38 |   `Your task is to list directory under ${rootPaths.join(', ')}. ` +
39 |   'Get a detailed listing of all files and directories in a specified path. ' +
40 |   'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
41 |   'prefixes. This tool is essential for understanding directory structure and ' +
42 |   'finding specific files within a directory. Only works within allowed directories.'
43 | 
44 | export const removeDirectoryPrompt = () => ''
45 | 
46 | export const removeMultipleDirectoryPrompt = () => ''
47 | 
48 | export const moveFileDirectoryPrompt = () =>
49 |   'Move or rename files and directories. Can move files between directories ' +
50 |   'and rename them in a single operation. If the destination exists, the ' +
51 |   'operation will fail. Works across different directories and can be used ' +
52 |   'for simple renaming within the same directory. Both source and destination must be within allowed directories.'
53 | 
54 | export const fullTextSearchDirectoryPrompt = () =>
55 |   "Tokenize the user's query and the search engine tool will return relevant contents. " +
56 |   "summarized those contents based on the user's query."
57 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'
  4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListResourcesRequestSchema,
  8 |   ListToolsRequestSchema,
  9 |   ReadResourceRequestSchema
 10 | } from '@modelcontextprotocol/sdk/types.js'
 11 | import { zodToJsonSchema } from 'zod-to-json-schema'
 12 | import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js'
 13 | import {
 14 |   createDirectory,
 15 |   editFile,
 16 |   flattenDirectory,
 17 |   fullTextSearch,
 18 |   listDirectory,
 19 |   moveFile,
 20 |   readFile,
 21 |   readFileFromUri,
 22 |   readMultipleFiles,
 23 |   removeDirectory,
 24 |   removeFile,
 25 |   removeMultipleDirectory,
 26 |   removeMultipleFiles,
 27 |   writeFile
 28 | } from './file-system.js'
 29 | import {
 30 |   createDirectoryPrompt,
 31 |   editFilePrompt,
 32 |   fullTextSearchDirectoryPrompt,
 33 |   listDirectoryPrompt,
 34 |   moveFileDirectoryPrompt,
 35 |   readFilePrompt,
 36 |   readMultipleFilesPrompt,
 37 |   removeDirectoryPrompt,
 38 |   removeFilePrompt,
 39 |   removeMultipleDirectoryPrompt,
 40 |   removeMultipleFilesPrompt,
 41 |   writeFilePrompt
 42 | } from './prompts.js'
 43 | import {
 44 |   CreateDirectoryArgsSchema,
 45 |   EditFileArgsSchema,
 46 |   FullTextSearchArgsSchema,
 47 |   ListDirectoryArgsSchema,
 48 |   MoveFileArgsSchema,
 49 |   ReadFileArgsSchema,
 50 |   ReadMultipleFilesArgsSchema,
 51 |   RemoveDirectoryArgsSchema,
 52 |   RemoveFileArgsSchema,
 53 |   RemoveMultipleDirectoryArgsSchema,
 54 |   RemoveMultipleFilesArgsSchema,
 55 |   ToolInput,
 56 |   WriteFileArgsSchema
 57 | } from './schemas.js'
 58 | 
 59 | const server = new Server(
 60 |   {
 61 |     name: MCP_SERVER_NAME,
 62 |     version: MCP_SERVER_VERSION
 63 |   },
 64 |   {
 65 |     capabilities: {
 66 |       tools: {},
 67 |       resources: {},
 68 |       prompts: {}
 69 |     }
 70 |   }
 71 | )
 72 | 
 73 | const args = process.argv.slice(2)
 74 | if (args.length === 0) {
 75 |   console.error(
 76 |     `Usage: ${MCP_SERVER_NAME} <obsidian-directory> [additional-directories...]`
 77 |   )
 78 |   process.exit(1)
 79 | }
 80 | 
 81 | server.setRequestHandler(ListResourcesRequestSchema, async () => {
 82 |   const resources = (
 83 |     await Promise.all(args.map((arg) => flattenDirectory(arg)))
 84 |   ).flat()
 85 |   return {
 86 |     resources
 87 |   }
 88 | })
 89 | 
 90 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
 91 |   const content = await readFileFromUri(request.params.uri)
 92 | 
 93 |   if (content === null) throw new Error('Error reading file from URL')
 94 |   return content
 95 | })
 96 | 
 97 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 98 |   return {
 99 |     tools: [
100 |       {
101 |         name: 'read_file',
102 |         description: readFilePrompt(args),
103 |         inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
104 |       },
105 |       {
106 |         name: 'read_multiple_files',
107 |         description: readMultipleFilesPrompt(),
108 |         inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput
109 |       },
110 |       {
111 |         name: 'write_file',
112 |         description: writeFilePrompt(args),
113 |         inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
114 |       },
115 |       {
116 |         name: 'edit_file',
117 |         description: editFilePrompt(args),
118 |         inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
119 |       },
120 |       {
121 |         name: 'remove_file',
122 |         description: removeFilePrompt(),
123 |         inputSchema: zodToJsonSchema(RemoveFileArgsSchema) as ToolInput
124 |       },
125 |       {
126 |         name: 'remove_multiple_files',
127 |         description: removeMultipleFilesPrompt(),
128 |         inputSchema: zodToJsonSchema(RemoveMultipleFilesArgsSchema) as ToolInput
129 |       },
130 |       {
131 |         name: 'create_directory',
132 |         description: createDirectoryPrompt(),
133 |         inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput
134 |       },
135 |       {
136 |         name: 'list_directory',
137 |         description: listDirectoryPrompt(args),
138 |         inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
139 |       },
140 |       {
141 |         name: 'remove_directory',
142 |         description: removeDirectoryPrompt(),
143 |         inputSchema: zodToJsonSchema(RemoveDirectoryArgsSchema) as ToolInput
144 |       },
145 |       {
146 |         name: 'remove_multiple_directory',
147 |         description: removeMultipleDirectoryPrompt(),
148 |         inputSchema: zodToJsonSchema(
149 |           RemoveMultipleDirectoryArgsSchema
150 |         ) as ToolInput
151 |       },
152 |       {
153 |         name: 'move_file',
154 |         description: moveFileDirectoryPrompt(),
155 |         inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput
156 |       },
157 |       {
158 |         name: 'full_text_search',
159 |         description: fullTextSearchDirectoryPrompt(),
160 |         inputSchema: zodToJsonSchema(FullTextSearchArgsSchema) as ToolInput
161 |       }
162 |     ]
163 |   }
164 | })
165 | 
166 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
167 |   try {
168 |     const { name, arguments: args } = request.params
169 | 
170 |     switch (name) {
171 |       case 'read_file': {
172 |         return readFile(args)
173 |       }
174 | 
175 |       case 'read_multiple_files': {
176 |         return readMultipleFiles(args)
177 |       }
178 | 
179 |       case 'write_file': {
180 |         return writeFile(args)
181 |       }
182 | 
183 |       case 'edit_file': {
184 |         return editFile(args)
185 |       }
186 | 
187 |       case 'remove_file': {
188 |         return removeFile(args)
189 |       }
190 | 
191 |       case 'remove_multiple_files': {
192 |         return removeMultipleFiles(args)
193 |       }
194 | 
195 |       case 'create_directory': {
196 |         return createDirectory(args)
197 |       }
198 | 
199 |       case 'list_directory': {
200 |         return listDirectory(args)
201 |       }
202 | 
203 |       case 'remove_directory': {
204 |         return removeDirectory(args)
205 |       }
206 | 
207 |       case 'remove_multiple_directory': {
208 |         return removeMultipleDirectory(args)
209 |       }
210 | 
211 |       case 'move_file': {
212 |         return moveFile(args)
213 |       }
214 | 
215 |       case 'full_text_search': {
216 |         return fullTextSearch(args)
217 |       }
218 | 
219 |       default:
220 |         throw new Error(`Unknown tool: ${name}`)
221 |     }
222 |   } catch (error) {
223 |     const errorMessage = error instanceof Error ? error.message : String(error)
224 |     return {
225 |       content: [{ type: 'text', text: `Error: ${errorMessage}` }],
226 |       isError: true
227 |     }
228 |   }
229 | })
230 | 
231 | async function main() {
232 |   const transport = new StdioServerTransport()
233 |   await server.connect(transport)
234 | }
235 | 
236 | main().catch((error) => {
237 |   console.error('Fatal error in main():', error)
238 |   process.exit(1)
239 | })
240 | 
```

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

```typescript
  1 | // @ts-expect-error FIXME:
  2 | // It says: Could not find a declaration file for module 'flexsearch'. But after installing @type/flexsearch still doesn't work.
  3 | import flexsearch from 'flexsearch'
  4 | import fs from 'fs/promises'
  5 | import { glob } from 'glob'
  6 | import matter from 'gray-matter'
  7 | import mime from 'mime'
  8 | import path from 'path'
  9 | import removeMd from 'remove-markdown'
 10 | import { rimraf } from 'rimraf'
 11 | import { fileURLToPath } from 'url'
 12 | import {
 13 |   CreateDirectoryArgsSchema,
 14 |   EditFileArgsSchema,
 15 |   FullTextSearchArgsSchema,
 16 |   ListDirectoryArgsSchema,
 17 |   MoveFileArgsSchema,
 18 |   ReadFileArgsSchema,
 19 |   ReadMultipleFilesArgsSchema,
 20 |   RemoveDirectoryArgsSchema,
 21 |   RemoveFileArgsSchema,
 22 |   RemoveMultipleDirectoryArgsSchema,
 23 |   RemoveMultipleFilesArgsSchema,
 24 |   WriteFileArgsSchema
 25 | } from './schemas.js'
 26 | import { DirectoryNode, Resource } from './types.js'
 27 | 
 28 | export async function flattenDirectory(
 29 |   directoryPath: string
 30 | ): Promise<Resource[]> {
 31 |   const flattenedFiles: Resource[] = []
 32 | 
 33 |   async function traverseDirectory(currentPath: string, relativeDir: string) {
 34 |     try {
 35 |       const entries = await fs.readdir(currentPath, { withFileTypes: true })
 36 | 
 37 |       for (const entry of entries) {
 38 |         const fullPath = path.join(currentPath, entry.name)
 39 |         const relativeName = path.join(relativeDir, entry.name)
 40 | 
 41 |         if (entry.isFile()) {
 42 |           const fileUrl = new URL(`file://${path.resolve(fullPath)}`).toString()
 43 |           const mimeType = mime.getType(fullPath) || 'application/octet-stream'
 44 | 
 45 |           flattenedFiles.push({
 46 |             uri: fileUrl,
 47 |             name: entry.name,
 48 |             mimeType
 49 |           })
 50 |         } else if (entry.isDirectory()) {
 51 |           await traverseDirectory(fullPath, relativeName)
 52 |         }
 53 |       }
 54 |     } catch (error) {
 55 |       console.error(
 56 |         `Error reading directory ${currentPath}:`,
 57 |         error instanceof Error ? error.message : error
 58 |       )
 59 |     }
 60 |   }
 61 | 
 62 |   const absoluteDirectoryPath = path.resolve(directoryPath)
 63 |   await traverseDirectory(
 64 |     absoluteDirectoryPath,
 65 |     path.basename(absoluteDirectoryPath)
 66 |   )
 67 | 
 68 |   return flattenedFiles
 69 | }
 70 | 
 71 | export async function readFileFromUri(fileUri: string) {
 72 |   try {
 73 |     const fileUrl = new URL(fileUri)
 74 |     if (fileUrl.protocol !== 'file:') {
 75 |       throw new Error('Invalid URL protocol. Only file:// URLs are supported.')
 76 |     }
 77 |     const filePath = fileURLToPath(fileUrl)
 78 |     const content = await fs.readFile(filePath, 'utf-8')
 79 |     return {
 80 |       contents: [
 81 |         {
 82 |           uri: fileUri,
 83 |           mimeType: mime.getType(filePath) || 'application/octet-stream',
 84 |           text: content
 85 |         }
 86 |       ]
 87 |     }
 88 |   } catch (error) {
 89 |     console.error(
 90 |       `Error reading file ${fileUri}:`,
 91 |       error instanceof Error ? error.message : error
 92 |     )
 93 | 
 94 |     return null
 95 |   }
 96 | }
 97 | 
 98 | export async function getDirectoryTree(
 99 |   directoryPath: string
100 | ): Promise<DirectoryNode | null> {
101 |   async function traverseDirectory(
102 |     currentPath: string,
103 |     currentName: string
104 |   ): Promise<DirectoryNode | null> {
105 |     try {
106 |       const entries = await fs.readdir(currentPath, { withFileTypes: true })
107 |       const node: DirectoryNode = {
108 |         name: currentName,
109 |         type: 'directory',
110 |         children: []
111 |       }
112 | 
113 |       for (const entry of entries) {
114 |         const fullPath = path.join(currentPath, entry.name)
115 | 
116 |         if (entry.isFile()) {
117 |           const fileUrl = new URL(`file://${path.resolve(fullPath)}`).toString()
118 |           const mimeType = mime.getType(fullPath) || 'application/octet-stream'
119 |           node.children.push({
120 |             name: entry.name,
121 |             type: 'file',
122 |             uri: fileUrl,
123 |             mimeType
124 |           })
125 |         } else if (entry.isDirectory()) {
126 |           const childNode = await traverseDirectory(fullPath, entry.name)
127 |           if (childNode) {
128 |             node.children.push(childNode)
129 |           }
130 |         }
131 |       }
132 |       return node
133 |     } catch (error) {
134 |       console.error(
135 |         `Error reading directory ${currentPath}:`,
136 |         error instanceof Error ? error.message : error
137 |       )
138 |       return null
139 |     }
140 |   }
141 | 
142 |   try {
143 |     const absoluteDirectoryPath = path.resolve(directoryPath)
144 |     const baseName = path.basename(absoluteDirectoryPath)
145 |     const tree = await traverseDirectory(absoluteDirectoryPath, baseName)
146 |     return tree
147 |   } catch (error) {
148 |     console.error(
149 |       `Error processing directory ${directoryPath}:`,
150 |       error instanceof Error ? error.message : error
151 |     )
152 |     return null
153 |   }
154 | }
155 | 
156 | export async function getFileStats(filePath: string) {
157 |   try {
158 |     const stats = await fs.stat(filePath)
159 |     return {
160 |       size: stats.size,
161 |       created: stats.birthtime,
162 |       modified: stats.mtime,
163 |       accessed: stats.atime,
164 |       isDirectory: stats.isDirectory(),
165 |       isFile: stats.isFile(),
166 |       permissions: stats.mode.toString(8).slice(-3)
167 |     }
168 |   } catch (error) {
169 |     const errorMessage = error instanceof Error ? error.message : String(error)
170 |     return {
171 |       content: [{ type: 'text', text: `Error: ${errorMessage}` }],
172 |       isError: true
173 |     }
174 |   }
175 | }
176 | 
177 | export async function getAllMarkdownPaths(rootPaths: string[]) {
178 |   const filePaths = (
179 |     await Promise.all(rootPaths.map((rootPath) => glob(`${rootPath}/**/*.md`)))
180 |   ).flat()
181 | 
182 |   return filePaths
183 | }
184 | 
185 | export async function readMarkdown(filePath: string) {
186 |   const content = await fs.readFile(filePath, 'utf-8')
187 |   const frontMatter = matter(content)
188 | 
189 |   return {
190 |     id: filePath,
191 |     title:
192 |       (frontMatter.data.title as string | undefined) ??
193 |       path.basename(filePath, '.md'),
194 |     content: removeMd(content)
195 |   }
196 | }
197 | 
198 | export async function readAllMarkdowns(filePaths: string[]) {
199 |   const markdowns = await Promise.all(
200 |     filePaths.map((filePath) => readMarkdown(filePath))
201 |   )
202 | 
203 |   return markdowns
204 | }
205 | 
206 | export async function readFile(args?: Record<string, unknown>) {
207 |   const parsed = ReadFileArgsSchema.safeParse(args)
208 |   if (!parsed.success) {
209 |     throw new Error(`Invalid arguments for read_file: ${parsed.error}`)
210 |   }
211 | 
212 |   const content = await fs.readFile(parsed.data.path, 'utf-8')
213 |   return {
214 |     content: [{ type: 'text', text: content }]
215 |   }
216 | }
217 | 
218 | export async function readMultipleFiles(args?: Record<string, unknown>) {
219 |   const parsed = ReadMultipleFilesArgsSchema.safeParse(args)
220 |   if (!parsed.success) {
221 |     throw new Error(
222 |       `Invalid arguments for read_multiple_files: ${parsed.error}`
223 |     )
224 |   }
225 | 
226 |   const results = await Promise.all(
227 |     parsed.data.paths.map(async (filePath: string) => {
228 |       const content = await fs.readFile(filePath, 'utf-8')
229 |       return `${filePath}:\n${content}\n`
230 |     })
231 |   )
232 |   return {
233 |     content: [{ type: 'text', text: results.join('\n---\n') }]
234 |   }
235 | }
236 | 
237 | export async function writeFile(args?: Record<string, unknown>) {
238 |   const parsed = WriteFileArgsSchema.safeParse(args)
239 |   if (!parsed.success) {
240 |     throw new Error(`Invalid arguments for write_file: ${parsed.error}`)
241 |   }
242 | 
243 |   await fs.writeFile(parsed.data.path, parsed.data.content, 'utf-8')
244 |   return {
245 |     content: [
246 |       { type: 'text', text: `Successfully wrote to ${parsed.data.path}` }
247 |     ]
248 |   }
249 | }
250 | 
251 | export async function editFile(args?: Record<string, unknown>) {
252 |   const parsed = EditFileArgsSchema.safeParse(args)
253 |   if (!parsed.success) {
254 |     throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
255 |   }
256 | 
257 |   if (!parsed.data.dryRun) {
258 |     await fs.writeFile(parsed.data.path, parsed.data.newText)
259 |   }
260 | 
261 |   return {
262 |     content: [{ type: 'text', text: parsed.data.newText }]
263 |   }
264 | }
265 | 
266 | export async function removeFile(args?: Record<string, unknown>) {
267 |   const parsed = RemoveFileArgsSchema.safeParse(args)
268 |   if (!parsed.success) {
269 |     throw new Error(`Invalid arguments for remove_file: ${parsed.error}`)
270 |   }
271 |   const result = await fs.unlink(parsed.data.path)
272 |   return {
273 |     content: [{ type: 'text', text: result }]
274 |   }
275 | }
276 | 
277 | export async function removeMultipleFiles(args?: Record<string, unknown>) {
278 |   const parsed = RemoveMultipleFilesArgsSchema.safeParse(args)
279 |   if (!parsed.success) {
280 |     throw new Error(
281 |       `Invalid arguments for remove_multiple_files: ${parsed.error}`
282 |     )
283 |   }
284 |   const result = await Promise.all(
285 |     parsed.data.paths.map((path) => fs.unlink(path))
286 |   )
287 | 
288 |   return {
289 |     content: [{ type: 'text', text: result }]
290 |   }
291 | }
292 | 
293 | export async function createDirectory(args?: Record<string, unknown>) {
294 |   const parsed = CreateDirectoryArgsSchema.safeParse(args)
295 |   if (!parsed.success) {
296 |     throw new Error(`Invalid arguments for create_directory: ${parsed.error}`)
297 |   }
298 |   await fs.mkdir(parsed.data.path, { recursive: true })
299 |   return {
300 |     content: [
301 |       {
302 |         type: 'text',
303 |         text: `Successfully created directory ${parsed.data.path}`
304 |       }
305 |     ]
306 |   }
307 | }
308 | 
309 | export async function listDirectory(args?: Record<string, unknown>) {
310 |   const parsed = ListDirectoryArgsSchema.safeParse(args)
311 |   if (!parsed.success) {
312 |     throw new Error(`Invalid arguments for list_directory: ${parsed.error}`)
313 |   }
314 |   const entries = await fs.readdir(parsed.data.path, {
315 |     withFileTypes: true
316 |   })
317 |   const formatted = entries
318 |     .map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
319 |     .join('\n')
320 |   return {
321 |     content: [{ type: 'text', text: formatted }]
322 |   }
323 | }
324 | 
325 | export async function removeDirectory(args?: Record<string, unknown>) {
326 |   const parsed = RemoveDirectoryArgsSchema.safeParse(args)
327 |   if (!parsed.success) {
328 |     throw new Error(`Invalid arguments for remove_directory: ${parsed.error}`)
329 |   }
330 |   const result = await rimraf(parsed.data.path)
331 |   return {
332 |     content: [{ type: 'text', text: result }]
333 |   }
334 | }
335 | 
336 | export async function removeMultipleDirectory(args?: Record<string, unknown>) {
337 |   const parsed = RemoveMultipleDirectoryArgsSchema.safeParse(args)
338 |   if (!parsed.success) {
339 |     throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
340 |   }
341 |   const result = await Promise.all(
342 |     parsed.data.paths.map((path) => rimraf(path))
343 |   )
344 | 
345 |   return {
346 |     content: [{ type: 'text', text: result }]
347 |   }
348 | }
349 | 
350 | export async function moveFile(args?: Record<string, unknown>) {
351 |   const parsed = MoveFileArgsSchema.safeParse(args)
352 |   if (!parsed.success) {
353 |     throw new Error(`Invalid arguments for move_file: ${parsed.error}`)
354 |   }
355 |   await fs.rename(parsed.data.source, parsed.data.destination)
356 |   return {
357 |     content: [
358 |       {
359 |         type: 'text',
360 |         text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}`
361 |       }
362 |     ]
363 |   }
364 | }
365 | 
366 | // TODO: Build index phrase should be mounted on service start, rather than a single request.
367 | export async function fullTextSearch(args?: Record<string, unknown>) {
368 |   const parsed = FullTextSearchArgsSchema.safeParse(args)
369 |   if (!parsed.success) {
370 |     throw new Error(`Invalid arguments for full_text_search: ${parsed.error}`)
371 |   }
372 | 
373 |   const filePaths = await getAllMarkdownPaths(process.argv.slice(2))
374 |   const documents = await readAllMarkdowns(filePaths)
375 | 
376 |   const index = new flexsearch.Document({
377 |     document: {
378 |       id: 'id',
379 |       store: true,
380 |       index: [
381 |         {
382 |           field: 'title',
383 |           tokenize: 'forward',
384 |           encoder: flexsearch.Charset.LatinBalance
385 |         },
386 |         {
387 |           field: 'content',
388 |           tokenize: 'forward',
389 |           encoder: flexsearch.Charset.LatinBalance
390 |         }
391 |       ]
392 |     }
393 |   })
394 | 
395 |   documents.forEach((file) => {
396 |     index.add(file)
397 |   })
398 | 
399 |   const searchedIds = index.search(parsed.data.query, { limit: 5 })
400 |   const filteredDocuments = documents
401 |     .filter(({ id }) => searchedIds[0].result.includes(id))
402 |     .map((document) => document.content)
403 |   return {
404 |     content: [
405 |       {
406 |         type: 'text',
407 |         text:
408 |           filteredDocuments.length > 0
409 |             ? filteredDocuments.join('\n---\n')
410 |             : 'No matches found'
411 |       }
412 |     ]
413 |   }
414 | }
415 | 
```