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

```
├── .gitignore
├── CHANGELOG.md
├── index.ts
├── package.json
├── README.md
└── tsconfig.json
```

# Files

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

```
Sample

# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
package-lock.json
yarn.lock
.pnp.*

# Environment variables
.env
.env.local
.env.*.local

# Build output
dist/
build/
out/
.next/
.nuxt/
.output/

# Coverage directory
coverage/

# Cache & Logs
.cache/
.temp/
logs
*.log

# Editor directories and files
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sublime-workspace
*.sublime-project

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# Typescript
*.tsbuildinfo
next-env.d.ts
```

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

```markdown
# MCP XMind Server
[![smithery badge](https://smithery.ai/badge/@41px/mcp-xmind)](https://smithery.ai/server/@41px/mcp-xmind)

A Model Context Protocol server for analyzing and querying XMind mind maps. This tool provides powerful capabilities for searching, extracting, and analyzing content from XMind files.

## Features

- 🔍 Smart fuzzy search across mind maps
- 📝 Task management and tracking
- 🌲 Hierarchical content navigation
- 🔗 Link and reference extraction
- 📊 Multi-file analysis
- 🏷️ Label and tag support
- 📂 Directory scanning
- 🔒 Secure directory access

## Installation

### Installing via Smithery

To install XMind Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@41px/mcp-xmind):

```bash
npx -y @smithery/cli install @41px/mcp-xmind --client claude
```

### Manual Installation
```bash
npm install @modelcontextprotocol/sdk adm-zip zod
npm install --save-dev typescript @types/node
```

## Usage

### Starting the Server

```bash
node dist/index.js <allowed-directory> [additional-directories...]
```

### Available Tools

1. **read_xmind**
   - Parse and analyze XMind files
   - Extract complete mind map structure

2. **get_todo_tasks**
   - Extract and analyze TODO tasks
   - Include task context and hierarchy

3. **list_xmind_directory**
   - Recursively scan for XMind files
   - Filter and organize results

4. **read_multiple_xmind_files**
   - Process multiple files simultaneously
   - Compare and analyze across files

5. **search_xmind_files**
   - Search files by name patterns
   - Recursive directory scanning

6. **extract_node**
   - Smart fuzzy path matching
   - Ranked search results
   - Complete subtree extraction

7. **extract_node_by_id**
   - Direct node access by ID
   - Fast and precise retrieval

8. **search_nodes**
   - Multi-criteria content search
   - Configurable search fields

## Examples

### Search for Nodes
```json
{
    "name": "search_nodes",
    "arguments": {
        "path": "/path/to/file.xmind",
        "query": "project",
        "searchIn": ["title", "notes"],
        "caseSensitive": false
    }
}
```

### Extract Node
```json
{
    "name": "extract_node",
    "arguments": {
        "path": "/path/to/file.xmind",
        "searchQuery": "Feature > API"
    }
}
```

### List Tasks
```json
{
    "name": "get_todo_tasks",
    "arguments": {
        "path": "/path/to/file.xmind"
    }
}
```

## Configuration

### Development Configuration

Example `claude_desktop_config.json` for development:

```json
{
  "xmind": {
    "command": "node",
    "args": [
      "/Users/alex/Src/mcp-xmind/dist/index.js",
      "/Users/alex/XMind"
    ]
  }
}
```

### Production Configuration

Example `claude_desktop_config.json` for production using npmjs:

```json
{
  "xmind": {
    "command": "npx",
    "args": [
      "-y",
      "@41px/mcp-xmind",
      "/Users/alex/XMind"
    ]
  }
}
```

## Security

- Only allows access to specified directories
- Path normalization and validation
- Error handling for invalid access attempts

## Development

### Building
```bash
npm run build
```

### Type Checking
```bash
npm run type-check
```

### MCP Inspector

```bash
npx @modelcontextprotocol/inspector node dist/index.js /Users/alex/XMind
```

```

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

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

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

## [1.1.1] - 2024-01-20

### Added
- Support for node relationships
- Enhanced search with task status filtering
- Improved callouts support

### Changed
- Removed get_todo_tasks in favor of search_nodes with status filter
- Optimized file searching
- Improved tool descriptions

### Fixed
- Fixed relationship parsing in content.json
- Better file path handling

## [1.0.0] - 2024-01-19

### Added
- Initial release
- Basic XMind file support
- Node and task extraction
- File searching capabilities

```

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

```json
{
  "name": "@41px/mcp-xmind",
  "version": "1.1.1",
  "description": "MCP server for XMind",
  "license": "MIT",
  "author": "Alexandre Peyroux <[email protected]>",
  "type": "module",
  "bin": {
    "mcp-server-xmind": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "prepare": "npm run build",
    "watch": "tsc --watch"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0",
    "adm-zip": "^0.5.16",
    "diff": "^5.1.0",
    "glob": "^10.3.10",
    "minimatch": "^10.0.1",
    "zod": "^3.24.1",
    "zod-to-json-schema": "^3.24.1"
  },
  "devDependencies": {
    "@types/adm-zip": "^0.5.7",
    "@types/diff": "^5.0.9",
    "@types/minimatch": "^5.1.2",
    "@types/node": "^20.17.10",
    "shx": "^0.3.4",
    "typescript": "^5.7.2"
  }
}

```

--------------------------------------------------------------------------------
/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,
    ListToolsRequestSchema,
    ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs/promises";
import path from "path";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import AdmZip from 'adm-zip';

// Command line argument parsing
const args = process.argv.slice(2);
if (args.length === 0) {
    console.error("Usage: mcp-server-xmind <allowed-directory> [additional-directories...]");
    process.exit(1);
}

// Store allowed directories in normalized form
const allowedDirectories = args.map(dir =>
    path.normalize(path.resolve(dir)).toLowerCase()
);

// Validate that all directories exist and are accessible
await Promise.all(args.map(async (dir) => {
    try {
        const stats = await fs.stat(dir);
        if (!stats.isDirectory()) {
            console.error(`Error: ${dir} is not a directory`);
            process.exit(1);
        }
    } catch (error) {
        console.error(`Error accessing directory ${dir}:`, error);
        process.exit(1);
    }
}));

// Ajouter après la définition des allowedDirectories
function isPathAllowed(filePath: string): boolean {
    const normalizedPath = path.normalize(path.resolve(filePath)).toLowerCase();
    return allowedDirectories.some(dir => normalizedPath.startsWith(dir));
}

// XMind Interfaces
interface XMindNode {
    title: string;
    id?: string;
    children?: XMindNode[];
    taskStatus?: 'done' | 'todo';
    notes?: {
        content?: string;
    };
    href?: string;
    labels?: string[];
    sheetTitle?: string;
    callouts?: {
        title: string;
    }[];
    relationships?: XMindRelationship[];
}

interface XMindTopic {
    id: string;
    title: string;
    children?: {
        attached: XMindTopic[];
        callout?: XMindTopic[];
    };
    extensions?: Array<{
        provider: string;
        content: {
            status: 'done' | 'todo';
        };
    }>;
    notes?: {
        plain?: {
            content: string;
        };
        realHTML?: {
            content: string;
        };
    };
    href?: string;
    labels?: string[];
}

interface XMindRelationship {
    id: string;
    end1Id: string;
    end2Id: string;
    title?: string;
}

// Class XMindParser
class XMindParser {
    private filePath: string;

    constructor(filePath: string) {
        const resolvedPath = path.resolve(filePath);
        if (!isPathAllowed(resolvedPath)) {
            throw new Error(`Access denied: ${filePath} is not in an allowed directory`);
        }
        this.filePath = resolvedPath;
    }

    public async parse(): Promise<XMindNode[]> {
        const contentJson = this.extractContentJson();
        return this.parseContentJson(contentJson);
    }

    private extractContentJson(): string {
        try {
            const zip = new AdmZip(this.filePath);
            const contentEntry = zip.getEntry("content.json");
            if (!contentEntry) {
                throw new Error("content.json not found in XMind file");
            }
            return zip.readAsText(contentEntry);
        } catch (error) {
            throw new Error(`Failed to extract content.json: ${error}`);
        }
    }

    private parseContentJson(jsonContent: string): Promise<XMindNode[]> {
        try {
            const content = JSON.parse(jsonContent);
            const allNodes = content.map((sheet: { 
                rootTopic: XMindTopic; 
                title?: string;
                relationships?: XMindRelationship[];
            }) => {
                const rootNode = this.processNode(sheet.rootTopic, sheet.title || "Untitled Map");
                // Ajouter les relations au nœud racine
                if (sheet.relationships) {
                    rootNode.relationships = sheet.relationships;
                }
                return rootNode;
            });
            return Promise.resolve(allNodes);
        } catch (error) {
            return Promise.reject(`Failed to parse JSON content: ${error}`);
        }
    }

    private processNode(node: XMindTopic, sheetTitle?: string): XMindNode {
        const processedNode: XMindNode = {
            title: node.title,
            id: node.id,
            sheetTitle: sheetTitle || "Untitled Map"
        };

        // Handle links, labels and callouts
        if (node.href) processedNode.href = node.href;
        if (node.labels) processedNode.labels = node.labels;
        if (node.children?.callout) {
            processedNode.callouts = node.children.callout.map(callout => ({
                title: callout.title
            }));
        }

        // Handle notes and callouts
        if (node.notes?.plain?.content) {
            processedNode.notes = {};

            // Process main note content
            if (node.notes?.plain?.content) {
                processedNode.notes.content = node.notes.plain.content;
            }
        }

        // Handle task status
        if (node.extensions) {
            const taskExtension = node.extensions.find((ext) =>
                ext.provider === 'org.xmind.ui.task' && ext.content?.status
            );
            if (taskExtension) {
                processedNode.taskStatus = taskExtension.content.status;
            }
        }

        // Process regular children
        if (node.children?.attached) {
            processedNode.children = node.children.attached.map(child =>
                this.processNode(child, sheetTitle)
            );
        }

        return processedNode;
    }
}

function getNodePath(node: XMindNode, parents: string[] = []): string {
    return parents.length > 0 ? `${parents.join(' > ')} > ${node.title}` : node.title;
}

// Schema definitions
const ReadXMindArgsSchema = z.object({
    path: z.string(),
});

const ListXMindDirectoryArgsSchema = z.object({
    directory: z.string().optional(),
});

const ReadMultipleXMindArgsSchema = z.object({
    paths: z.array(z.string()),
});

const SearchXMindFilesSchema = z.object({
    pattern: z.string(),
    directory: z.string().optional(),
});

// Modifier le schéma pour refléter la nouvelle approche
const ExtractNodeArgsSchema = z.object({
    path: z.string(),
    searchQuery: z.string(), // Renommé de nodePath à searchQuery
});

const ExtractNodeByIdArgsSchema = z.object({
    path: z.string(),
    nodeId: z.string(),
});

const SearchNodesArgsSchema = z.object({
    path: z.string(),
    query: z.string(),
    searchIn: z.array(z.enum(['title', 'notes', 'labels', 'callouts', 'tasks'])).optional(),
    caseSensitive: z.boolean().optional(),
    taskStatus: z.enum(['todo', 'done']).optional(), // Ajout du filtre de statut de tâche
});

interface MultipleXMindResult {
    filePath: string;
    content: XMindNode[];
    error?: string;
}

async function readMultipleXMindFiles(paths: string[]): Promise<MultipleXMindResult[]> {
    const results: MultipleXMindResult[] = [];

    for (const filePath of paths) {
        if (!isPathAllowed(filePath)) {
            results.push({
                filePath,
                content: [],
                error: `Access denied: ${filePath} is not in an allowed directory`
            });
            continue;
        }
        try {
            const parser = new XMindParser(filePath);
            const content = await parser.parse();
            results.push({ filePath, content });
        } catch (error) {
            results.push({
                filePath,
                content: [],
                error: error instanceof Error ? error.message : String(error)
            });
        }
    }

    return results;
}

// Function to list XMind files
async function listXMindFiles(directory?: string): Promise<string[]> {
    const files: string[] = [];
    const dirsToScan = directory
        ? [path.normalize(path.resolve(directory))]
        : allowedDirectories;

    for (const dir of dirsToScan) {
        // Check if directory is allowed
        const normalizedDir = dir.toLowerCase();
        if (!allowedDirectories.some(allowed => normalizedDir.startsWith(allowed))) {
            continue; // Skip unauthorized directories
        }

        async function scanDirectory(currentDir: string) {
            try {
                const entries = await fs.readdir(currentDir, { withFileTypes: true });
                for (const entry of entries) {
                    const fullPath = path.join(currentDir, entry.name);
                    if (entry.isDirectory()) {
                        await scanDirectory(fullPath);
                    } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.xmind')) {
                        files.push(fullPath);
                    }
                }
            } catch (error) {
                console.error(`Warning: Error scanning directory ${currentDir}:`, error);
                // Continue scanning other directories even if one fails
            }
        }

        await scanDirectory(dir);
    }

    return files;
}

// Add before server setup
async function searchInXMindContent(filePath: string, searchText: string): Promise<boolean> {
    try {
        const zip = new AdmZip(filePath);
        const contentEntry = zip.getEntry("content.json");
        if (!contentEntry) return false;

        const content = zip.readAsText(contentEntry);
        return content.toLowerCase().includes(searchText.toLowerCase());
    } catch (error) {
        console.error(`Error reading XMind file ${filePath}:`, error);
        return false;
    }
}

// Modification de la fonction searchXMindFiles
async function searchXMindFiles(pattern: string): Promise<string[]> {
    const matches: string[] = [];
    const contentMatches: string[] = [];
    const searchPattern = pattern.toLowerCase();

    async function searchInDirectory(currentDir: string) {
        try {
            const entries = await fs.readdir(currentDir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(currentDir, entry.name);

                if (entry.isDirectory()) {
                    const normalizedPath = path.normalize(fullPath).toLowerCase();
                    if (allowedDirectories.some(allowed => normalizedPath.startsWith(allowed))) {
                        await searchInDirectory(fullPath);
                    }
                } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.xmind')) {
                    const searchableText = [
                        entry.name.toLowerCase(),
                        path.basename(entry.name, '.xmind').toLowerCase(),
                        fullPath.toLowerCase()
                    ];

                    if (searchPattern === '' || 
                        searchableText.some(text => text.includes(searchPattern))) {
                        matches.push(fullPath);
                    } else {
                        // Si le pattern n'est pas trouvé dans le nom, chercher dans le contenu
                        if (await searchInXMindContent(fullPath, searchPattern)) {
                            contentMatches.push(fullPath);
                        }
                    }
                }
            }
        } catch (error) {
            console.error(`Warning: Error searching directory ${currentDir}:`, error);
        }
    }

    await Promise.all(allowedDirectories.map(dir => searchInDirectory(dir)));

    // Combiner et trier les résultats
    const allMatches = [
        ...matches.sort((a, b) => path.basename(a).localeCompare(path.basename(b))),
        ...contentMatches.sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
    ];

    return allMatches;
}

interface NodeSearchResult {
    found: boolean;
    node?: XMindNode;
    error?: string;
}

function findNodeByPath(node: XMindNode, searchPath: string[]): NodeSearchResult {
    if (searchPath.length === 0 || !searchPath[0]) {
        return { found: true, node };
    }

    const currentSearch = searchPath[0].toLowerCase();

    if (!node.children) {
        return {
            found: false,
            error: `Node "${node.title}" has no children, cannot find "${currentSearch}"`
        };
    }

    const matchingChild = node.children.find(
        child => child.title.toLowerCase() === currentSearch
    );

    if (!matchingChild) {
        return {
            found: false,
            error: `Could not find child "${currentSearch}" in node "${node.title}"`
        };
    }

    return findNodeByPath(matchingChild, searchPath.slice(1));
}

interface NodeMatch {
    id: string;
    title: string;
    path: string;
    sheet: string;
    matchedIn: string[];
    notes?: string;
    labels?: string[];
    callouts?: {
        title: string;
    }[];
    taskStatus?: 'todo' | 'done';
}

interface SearchResult {
    query: string;
    matches: NodeMatch[];
    totalMatches: number;
    searchedIn: string[];
}

// Ajouter la fonction de recherche de nœuds
function searchNodes(
    node: XMindNode,
    query: string,
    options: {
        searchIn?: string[],
        caseSensitive?: boolean,
        taskStatus?: 'todo' | 'done'
    } = {},
    parents: string[] = []
): NodeMatch[] {
    const matches: NodeMatch[] = [];
    const searchQuery = options.caseSensitive ? query : query.toLowerCase();
    const searchFields = options.searchIn || ['title', 'notes', 'labels', 'callouts', 'tasks'];

    const matchedIn: string[] = [];

    // Fonction helper pour la recherche de texte sécurisée
    const matchesText = (text: string | undefined): boolean => {
        if (!text) return false;
        const searchIn = options.caseSensitive ? text : text.toLowerCase();
        return searchIn.includes(searchQuery);
    };

    // Vérification du statut de tâche si spécifié
    if (options.taskStatus && node.taskStatus) {
        if (node.taskStatus !== options.taskStatus) {
            // Si le statut ne correspond pas, ignorer ce nœud
            return [];
        }
    }

    // Vérifier chaque champ configuré
    if (searchFields.includes('title') && matchesText(node.title)) {
        matchedIn.push('title');
    }
    if (searchFields.includes('notes') && node.notes?.content && matchesText(node.notes.content)) {
        matchedIn.push('notes');
    }
    if (searchFields.includes('labels') && node.labels?.some(label => matchesText(label))) {
        matchedIn.push('labels');
    }
    if (searchFields.includes('callouts') && node.callouts?.some(callout => matchesText(callout.title))) {
        matchedIn.push('callouts');
    }
    if (searchFields.includes('tasks') && node.taskStatus) {
        matchedIn.push('tasks');
    }

    // Si on a trouvé des correspondances ou si c'est une tâche correspondante, ajouter ce nœud
    const shouldIncludeNode = matchedIn.length > 0 || 
        (options.taskStatus && node.taskStatus === options.taskStatus);

    if (shouldIncludeNode && node.id) {
        matches.push({
            id: node.id,
            title: node.title,
            path: getNodePath(node, parents),
            sheet: node.sheetTitle || 'Untitled Map',
            matchedIn,
            notes: node.notes?.content,
            labels: node.labels,
            callouts: node.callouts,
            taskStatus: node.taskStatus // Ajout du statut de tâche dans les résultats
        });
    }

    // Rechercher récursivement dans les enfants
    if (node.children) {
        const currentPath = [...parents, node.title];
        node.children.forEach(child => {
            matches.push(...searchNodes(child, query, options, currentPath));
        });
    }

    return matches;
}

// Modifier la fonction de récupération d'un nœud pour utiliser l'ID
function findNodeById(node: XMindNode, searchId: string): NodeSearchResult {
    if (node.id === searchId) {
        return { found: true, node };
    }

    if (!node.children) {
        return { found: false };
    }

    for (const child of node.children) {
        const result = findNodeById(child, searchId);
        if (result.found) {
            return result;
        }
    }

    return { found: false };
}

// Nouvelle interface pour les résultats de recherche de chemin
interface PathSearchResult {
    found: boolean;
    nodes: Array<{
        node: XMindNode;
        matchConfidence: number;
        path: string;
    }>;
    error?: string;
}

// Nouvelle fonction de recherche de nœuds par chemin approximatif
function findNodesbyFuzzyPath(
    node: XMindNode,
    searchQuery: string,
    parents: string[] = [],
    threshold: number = 0.5
): PathSearchResult['nodes'] {
    const results: PathSearchResult['nodes'] = [];
    const currentPath = getNodePath(node, parents);

    // Fonction helper pour calculer la pertinence
    function calculateRelevance(nodePath: string, query: string): number {
        const pathLower = nodePath.toLowerCase();
        const queryLower = query.toLowerCase();

        // Score plus élevé pour une correspondance exacte
        if (pathLower.includes(queryLower)) {
            return 1.0;
        }

        // Score basé sur les mots correspondants
        const pathWords = pathLower.split(/[\s>]+/);
        const queryWords = queryLower.split(/[\s>]+/);

        const matchingWords = queryWords.filter(word =>
            pathWords.some(pathWord => pathWord.includes(word))
        );

        return matchingWords.length / queryWords.length;
    }

    // Vérifier le nœud courant
    const confidence = calculateRelevance(currentPath, searchQuery);
    if (confidence > threshold) {
        results.push({
            node,
            matchConfidence: confidence,
            path: currentPath
        });
    }

    // Rechercher récursivement dans les enfants
    if (node.children) {
        const newParents = [...parents, node.title];
        node.children.forEach(child => {
            results.push(...findNodesbyFuzzyPath(child, searchQuery, newParents, threshold));
        });
    }

    return results;
}

// Server setup
const server = new Server(
    {
        name: "xmind-analysis-server",
        version: "1.0.0",
    },
    {
        capabilities: {
            tools: {},
        },
    }
);

// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
        tools: [
            {
                name: "read_xmind",
                description: `Parse and analyze XMind files with multiple capabilities:
                - Extract complete mind map structure in JSON format
                - Include all relationships between nodes with their IDs and titles
                - Extract callouts attached to topics
                - Generate text or markdown summaries
                - Search for specific content
                - Get hierarchical path to any node
                - Filter content by labels, task status, or node depth
                - Extract all URLs and external references
                - Analyze relationships and connections between topics
                Input: File path to .xmind file
                Output: JSON structure containing nodes, relationships, and callouts`,
                inputSchema: zodToJsonSchema(ReadXMindArgsSchema),
            },
            {
                name: "list_xmind_directory",
                description: `Comprehensive XMind file discovery and analysis tool:
                - Recursively scan directories for .xmind files
                - Filter files by creation/modification date
                - Search for files containing specific content
                - Group files by project or category
                - Detect duplicate mind maps
                - Generate directory statistics and summaries
                - Verify file integrity and structure
                - Monitor changes in mind map files
                Input: Directory path to scan
                Output: List of XMind files with optional metadata`,
                inputSchema: zodToJsonSchema(ListXMindDirectoryArgsSchema),
            },
            {
                name: "read_multiple_xmind_files",
                description: `Advanced multi-file analysis and correlation tool:
                - Process multiple XMind files simultaneously
                - Compare content across different mind maps
                - Identify common themes and patterns
                - Merge related content from different files
                - Generate cross-reference reports
                - Find content duplications across files
                - Create consolidated summaries
                - Track changes across multiple versions
                - Generate comparative analysis
                Input: Array of file paths to .xmind files
                Output: Combined analysis results in JSON format with per-file details`,
                inputSchema: zodToJsonSchema(ReadMultipleXMindArgsSchema),
            },
            {
                name: "search_xmind_files",
                description: `Advanced file search tool with recursive capabilities:
                - Search for files and directories by partial name matching
                - Case-insensitive pattern matching
                - Searches through all subdirectories recursively
                - Returns full paths to all matching items
                - Includes both files and directories in results
                - Safe searching within allowed directories only
                - Handles special characters in names
                - Continues searching even if some directories are inaccessible
                Input: {
                    directory: Starting directory path,
                    pattern: Search text to match in names
                }
                Output: Array of full paths to matching items`,
                inputSchema: zodToJsonSchema(SearchXMindFilesSchema),
            },
            {
                name: "extract_node",
                description: `Smart node extraction with fuzzy path matching:
                - Flexible search using partial or complete node paths
                - Returns multiple matching nodes ranked by relevance
                - Supports approximate matching for better results
                - Includes full context and hierarchy information
                - Returns complete subtree for each match
                - Best tool for exploring and navigating complex mind maps
                - Perfect for finding nodes when exact path is unknown
                Usage examples:
                - "Project > Backend" : finds nodes in any path containing these terms
                - "Feature API" : finds nodes containing these words in any order
                Input: {
                    path: Path to .xmind file,
                    searchQuery: Text to search in node paths (flexible matching)
                }
                Output: Ranked list of matching nodes with their full subtrees`,
                inputSchema: zodToJsonSchema(ExtractNodeArgsSchema),
            },
            {
                name: "extract_node_by_id",
                description: `Extract a specific node and its subtree using its unique ID:
                - Find and extract node using its XMind ID
                - Return complete subtree structure
                - Preserve all node properties and relationships
                - Fast direct access without path traversal
                Note: For a more detailed view with fuzzy matching, use "extract_node" with the node's path
                Input: {
                    path: Path to .xmind file,
                    nodeId: Unique identifier of the node
                }
                Output: JSON structure of the found node and its subtree`,
                inputSchema: zodToJsonSchema(ExtractNodeByIdArgsSchema),
            },
            {
                name: "search_nodes",
                description: `Advanced node search with multiple criteria:
                - Search through titles, notes, labels, callouts and tasks
                - Filter by task status (todo/done)
                - Find nodes by their relationships
                - Configure which fields to search in
                - Case-sensitive or insensitive search
                - Get full context including task status
                - Returns all matching nodes with their IDs
                - Includes relationship information and task status
                Input: {
                    path: Path to .xmind file,
                    query: Search text,
                    searchIn: Array of fields to search in ['title', 'notes', 'labels', 'callouts', 'tasks'],
                    taskStatus: 'todo' | 'done' (optional),
                    caseSensitive: Boolean (optional)
                }
                Output: Detailed search results with task status and context`,
                inputSchema: zodToJsonSchema(SearchNodesArgsSchema),
            },
        ],
    };
});

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

        switch (name) {
            case "read_xmind": {
                const parsed = ReadXMindArgsSchema.safeParse(args);
                if (!parsed.success) {
                    throw new Error(`Invalid arguments for read_xmind: ${parsed.error}`);
                }
                if (!isPathAllowed(parsed.data.path)) {
                    throw new Error(`Access denied: ${parsed.data.path} is not in an allowed directory`);
                }
                const parser = new XMindParser(parsed.data.path);
                const mindmap = await parser.parse();
                return {
                    content: [{ type: "text", text: JSON.stringify(mindmap, null, 2) }],
                };
            }

            case "list_xmind_directory": {
                const parsed = ListXMindDirectoryArgsSchema.safeParse(args);
                if (!parsed.success) {
                    throw new Error(`Invalid arguments for list_xmind_directory: ${parsed.error}`);
                }
                const files = await listXMindFiles(parsed.data.directory);
                return {
                    content: [{ type: "text", text: files.join('\n') }],
                };
            }

            case "read_multiple_xmind_files": {
                const parsed = ReadMultipleXMindArgsSchema.safeParse(args);
                if (!parsed.success) {
                    throw new Error(`Invalid arguments for read_multiple_xmind_files: ${parsed.error}`);
                }
                const results = await readMultipleXMindFiles(parsed.data.paths);
                return {
                    content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
                };
            }

            case "search_xmind_files": {
                const parsed = SearchXMindFilesSchema.safeParse(args);
                if (!parsed.success) {
                    throw new Error(`Invalid arguments for search_xmind_files: ${parsed.error}`);
                }
                // Corriger l'appel pour n'utiliser que le pattern
                const matches = await searchXMindFiles(parsed.data.pattern);
                return {
                    content: [{ type: "text", text: matches.join('\n') }],
                };
            }

            case "extract_node": {
                const parsed = ExtractNodeArgsSchema.safeParse(args);
                if (!parsed.success) {
                    throw new Error(`Invalid arguments for extract_node: ${parsed.error}`);
                }

                const parser = new XMindParser(parsed.data.path);
                const mindmap = await parser.parse();

                const allMatches = mindmap.flatMap(sheet =>
                    findNodesbyFuzzyPath(sheet, parsed.data.searchQuery)
                );

                // Trier par pertinence
                allMatches.sort((a, b) => b.matchConfidence - a.matchConfidence);

                if (allMatches.length === 0) {
                    throw new Error(`No nodes found matching: ${parsed.data.searchQuery}`);
                }

                // Retourner le résultat avec les meilleurs matchs
                return {
                    content: [{
                        type: "text",
                        text: JSON.stringify({
                            matches: allMatches.slice(0, 5), // Limiter aux 5 meilleurs résultats
                            totalMatches: allMatches.length,
                            query: parsed.data.searchQuery
                        }, null, 2)
                    }],
                };
            }

            case "extract_node_by_id": {
                const parsed = ExtractNodeByIdArgsSchema.safeParse(args);
                if (!parsed.success) {
                    throw new Error(`Invalid arguments for extract_node_by_id: ${parsed.error}`);
                }

                const parser = new XMindParser(parsed.data.path);
                const mindmap = await parser.parse();

                for (const sheet of mindmap) {
                    const result = findNodeById(sheet, parsed.data.nodeId);
                    if (result.found && result.node) {
                        return {
                            content: [{
                                type: "text",
                                text: JSON.stringify(result.node, null, 2)
                            }],
                        };
                    }
                }

                throw new Error(`Node not found with ID: ${parsed.data.nodeId}`);
            }

            case "search_nodes": {
                const parsed = SearchNodesArgsSchema.safeParse(args);
                if (!parsed.success) {
                    throw new Error(`Invalid arguments for search_nodes: ${parsed.error}`);
                }

                const parser = new XMindParser(parsed.data.path);
                const mindmap = await parser.parse();

                const matches: NodeMatch[] = mindmap.flatMap(sheet =>
                    searchNodes(sheet, parsed.data.query, {
                        searchIn: parsed.data.searchIn,
                        caseSensitive: parsed.data.caseSensitive,
                        taskStatus: parsed.data.taskStatus
                    })
                );

                const result: SearchResult = {
                    query: parsed.data.query,
                    matches,
                    totalMatches: matches.length,
                    searchedIn: parsed.data.searchIn || ['title', 'notes', 'labels', 'callouts', 'tasks']
                };

                return {
                    content: [{
                        type: "text",
                        text: JSON.stringify(result, null, 2)
                    }],
                };
            }

            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,
        };
    }
});

// Start server
async function runServer() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("XMind Analysis Server running on stdio");
    console.error("Allowed directories:", allowedDirectories);
}

runServer().catch((error) => {
    console.error("Fatal error running server:", error);
    process.exit(1);
});
```