# 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'
}
]
}
}
```