#
tokens: 5485/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── overleaf-git-client.js
├── overleaf-mcp-server.js
├── package-lock.json
├── package.json
├── projects.example.json
└── README.md
```

# Files

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

```
1 | node_modules/
2 | temp/
3 | projects.json
4 | .env
5 | *.log
```

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

```markdown
  1 | # Overleaf MCP Server
  2 | 
  3 | An MCP (Model Context Protocol) server that provides access to Overleaf projects via Git integration. This allows Claude and other MCP clients to read LaTeX files, analyze document structure, and extract content from Overleaf projects.
  4 | 
  5 | ## Features
  6 | 
  7 | - 📄 **File Management**: List and read files from Overleaf projects
  8 | - 📋 **Document Structure**: Parse LaTeX sections and subsections
  9 | - 🔍 **Content Extraction**: Extract specific sections by title
 10 | - 📊 **Project Summary**: Get overview of project status and structure
 11 | - 🏗️ **Multi-Project Support**: Manage multiple Overleaf projects
 12 | 
 13 | ## Installation
 14 | 
 15 | 1. Clone this repository
 16 | 2. Install dependencies:
 17 |    ```bash
 18 |    npm install
 19 |    ```
 20 | 
 21 | 3. Set up your projects configuration:
 22 |    ```bash
 23 |    cp projects.example.json projects.json
 24 |    ```
 25 | 
 26 | 4. Edit `projects.json` with your Overleaf credentials:
 27 |    ```json
 28 |    {
 29 |      "projects": {
 30 |        "default": {
 31 |          "name": "My Paper",
 32 |          "projectId": "YOUR_OVERLEAF_PROJECT_ID",
 33 |          "gitToken": "YOUR_OVERLEAF_GIT_TOKEN"
 34 |        }
 35 |      }
 36 |    }
 37 |    ```
 38 | 
 39 | ## Getting Overleaf Credentials
 40 | 
 41 | 1. **Git Token**: 
 42 |    - Go to Overleaf Account Settings → Git Integration
 43 |    - Click "Create Token"
 44 | 
 45 | 2. **Project ID**: 
 46 |    - Open your Overleaf project
 47 |    - Find it in the URL: `https://www.overleaf.com/project/[PROJECT_ID]`
 48 | 
 49 | ## Claude Desktop Setup
 50 | 
 51 | Add to your Claude Desktop configuration file:
 52 | 
 53 | **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
 54 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
 55 | **Linux**: `~/.config/claude/claude_desktop_config.json`
 56 | 
 57 | ```json
 58 | {
 59 |   "mcpServers": {
 60 |     "overleaf": {
 61 |       "command": "node",
 62 |       "args": [
 63 |         "/path/to/OverleafMCP/overleaf-mcp-server.js"
 64 |       ]
 65 |     }
 66 |   }
 67 | }
 68 | ```
 69 | 
 70 | Restart Claude Desktop after configuration.
 71 | 
 72 | ## Available Tools
 73 | 
 74 | ### `list_projects`
 75 | List all configured projects.
 76 | 
 77 | ### `list_files`
 78 | List files in a project (default: .tex files).
 79 | - `extension`: File extension filter (optional)
 80 | - `projectName`: Project identifier (optional, defaults to "default")
 81 | 
 82 | ### `read_file`
 83 | Read a specific file from the project.
 84 | - `filePath`: Path to the file (required)
 85 | - `projectName`: Project identifier (optional)
 86 | 
 87 | ### `get_sections`
 88 | Get all sections from a LaTeX file.
 89 | - `filePath`: Path to the LaTeX file (required)
 90 | - `projectName`: Project identifier (optional)
 91 | 
 92 | ### `get_section_content`
 93 | Get content of a specific section.
 94 | - `filePath`: Path to the LaTeX file (required)
 95 | - `sectionTitle`: Title of the section (required)
 96 | - `projectName`: Project identifier (optional)
 97 | 
 98 | ### `status_summary`
 99 | Get a comprehensive project status summary.
100 | - `projectName`: Project identifier (optional)
101 | 
102 | ## Usage Examples
103 | 
104 | ```
105 | # List all projects
106 | Use the list_projects tool
107 | 
108 | # Get project overview
109 | Use status_summary tool
110 | 
111 | # Read main.tex file
112 | Use read_file with filePath: "main.tex"
113 | 
114 | # Get Introduction section
115 | Use get_section_content with filePath: "main.tex" and sectionTitle: "Introduction"
116 | 
117 | # List all sections in a file
118 | Use get_sections with filePath: "main.tex"
119 | ```
120 | 
121 | ## Multi-Project Usage
122 | 
123 | To work with multiple projects, add them to `projects.json`:
124 | 
125 | ```json
126 | {
127 |   "projects": {
128 |     "default": {
129 |       "name": "Main Paper",
130 |       "projectId": "project-id-1",
131 |       "gitToken": "token-1"
132 |     },
133 |     "paper2": {
134 |       "name": "Second Paper", 
135 |       "projectId": "project-id-2",
136 |       "gitToken": "token-2"
137 |     }
138 |   }
139 | }
140 | ```
141 | 
142 | Then specify the project in tool calls:
143 | ```
144 | Use get_section_content with projectName: "paper2", filePath: "main.tex", sectionTitle: "Methods"
145 | ```
146 | 
147 | ## File Structure
148 | 
149 | ```
150 | OverleafMCP/
151 | ├── overleaf-mcp-server.js    # Main MCP server
152 | ├── overleaf-git-client.js    # Git client library
153 | ├── projects.json             # Your project configuration (gitignored)
154 | ├── projects.example.json     # Example configuration
155 | ├── package.json              # Dependencies
156 | └── README.md                 # This file
157 | ```
158 | 
159 | ## Security Notes
160 | 
161 | - `projects.json` is gitignored to protect your credentials
162 | - Never commit real project IDs or Git tokens
163 | - Use the provided `projects.example.json` as a template
164 | 
165 | ## License
166 | 
167 | MIT License
```

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

```json
 1 | {
 2 |   "name": "overleaf-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "MCP server for Overleaf Git integration",
 5 |   "main": "overleaf-mcp-server.js",
 6 |   "scripts": {
 7 |     "start": "node overleaf-mcp-server.js"
 8 |   },
 9 |   "dependencies": {
10 |     "@modelcontextprotocol/sdk": "^0.5.0"
11 |   }
12 | }
```

--------------------------------------------------------------------------------
/projects.example.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "projects": {
 3 |     "default": {
 4 |       "name": "My Paper",
 5 |       "projectId": "YOUR_PROJECT_ID",
 6 |       "gitToken": "YOUR_GIT_TOKEN"
 7 |     },
 8 |     "project2": {
 9 |       "name": "Another Paper",
10 |       "projectId": "ANOTHER_PROJECT_ID", 
11 |       "gitToken": "ANOTHER_GIT_TOKEN"
12 |     }
13 |   }
14 | }
```

--------------------------------------------------------------------------------
/overleaf-git-client.js:
--------------------------------------------------------------------------------

```javascript
  1 | const { exec } = require('child_process');
  2 | const fs = require('fs').promises;
  3 | const path = require('path');
  4 | const { promisify } = require('util');
  5 | const execAsync = promisify(exec);
  6 | 
  7 | class OverleafGitClient {
  8 |     constructor(gitToken, projectId, tempDir = './temp') {
  9 |         this.gitToken = gitToken;
 10 |         this.projectId = projectId;
 11 |         this.tempDir = tempDir;
 12 |         this.repoUrl = `https://git:${gitToken}@git.overleaf.com/${projectId}`;
 13 |         this.localPath = path.join(tempDir, projectId);
 14 |     }
 15 | 
 16 |     async cloneOrPull() {
 17 |         try {
 18 |             await fs.access(this.localPath);
 19 |             await execAsync(`cd "${this.localPath}" && git pull`, { 
 20 |                 env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
 21 |             });
 22 |         } catch {
 23 |             await fs.mkdir(this.tempDir, { recursive: true });
 24 |             await execAsync(`git clone "${this.repoUrl}" "${this.localPath}"`, {
 25 |                 env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
 26 |             });
 27 |         }
 28 |     }
 29 | 
 30 |     async listFiles(extension = '.tex') {
 31 |         await this.cloneOrPull();
 32 |         
 33 |         const files = [];
 34 |         async function walk(dir) {
 35 |             const entries = await fs.readdir(dir, { withFileTypes: true });
 36 |             
 37 |             for (const entry of entries) {
 38 |                 const fullPath = path.join(dir, entry.name);
 39 |                 if (entry.isDirectory() && entry.name !== '.git') {
 40 |                     await walk(fullPath);
 41 |                 } else if (entry.isFile() && (!extension || entry.name.endsWith(extension))) {
 42 |                     files.push(fullPath);
 43 |                 }
 44 |             }
 45 |         }
 46 |         
 47 |         await walk(this.localPath);
 48 |         return files.map(f => path.relative(this.localPath, f));
 49 |     }
 50 | 
 51 |     async readFile(filePath) {
 52 |         await this.cloneOrPull();
 53 |         const fullPath = path.join(this.localPath, filePath);
 54 |         return await fs.readFile(fullPath, 'utf8');
 55 |     }
 56 | 
 57 |     async getSections(filePath) {
 58 |         const content = await this.readFile(filePath);
 59 |         
 60 |         const sections = [];
 61 |         const sectionRegex = /\\(part|chapter|section|subsection|subsubsection|paragraph|subparagraph)\*?\{([^}]+)\}/g;
 62 |         
 63 |         let match;
 64 |         let lastIndex = 0;
 65 |         
 66 |         while ((match = sectionRegex.exec(content)) !== null) {
 67 |             const type = match[1];
 68 |             const title = match[2];
 69 |             const startIndex = match.index;
 70 |             
 71 |             if (sections.length > 0) {
 72 |                 sections[sections.length - 1].content = content.substring(lastIndex + match[0].length, startIndex).trim();
 73 |             }
 74 |             
 75 |             sections.push({
 76 |                 type,
 77 |                 title,
 78 |                 startIndex,
 79 |                 content: ''
 80 |             });
 81 |             
 82 |             lastIndex = startIndex;
 83 |         }
 84 |         
 85 |         if (sections.length > 0) {
 86 |             sections[sections.length - 1].content = content.substring(lastIndex + sections[sections.length - 1].title.length + 3).trim();
 87 |         }
 88 |         
 89 |         return sections;
 90 |     }
 91 | 
 92 |     async getSection(filePath, sectionTitle) {
 93 |         const sections = await this.getSections(filePath);
 94 |         return sections.find(s => s.title === sectionTitle);
 95 |     }
 96 | 
 97 |     async getSectionsByType(filePath, type) {
 98 |         const sections = await this.getSections(filePath);
 99 |         return sections.filter(s => s.type === type);
100 |     }
101 | }
102 | 
103 | module.exports = OverleafGitClient;
```

--------------------------------------------------------------------------------
/overleaf-mcp-server.js:
--------------------------------------------------------------------------------

```javascript
  1 | // Load project configuration
  2 | const fs = require('fs');
  3 | const path = require('path');
  4 | let projectsConfig = {};
  5 | try {
  6 |   const projectsFile = fs.readFileSync(path.join(__dirname, 'projects.json'), 'utf8');
  7 |   projectsConfig = JSON.parse(projectsFile);
  8 | } catch (err) {
  9 |   // Ignore if projects.json file doesn't exist
 10 | }
 11 | const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
 12 | const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
 13 | const {
 14 |   ListToolsRequestSchema,
 15 |   CallToolRequestSchema,
 16 | } = require('@modelcontextprotocol/sdk/types.js');
 17 | const OverleafGitClient = require('./overleaf-git-client.js');
 18 | 
 19 | const server = new Server(
 20 |   {
 21 |     name: 'overleaf-mcp',
 22 |     version: '1.0.0'
 23 |   },
 24 |   {
 25 |     capabilities: {
 26 |       tools: {}
 27 |     }
 28 |   }
 29 | );
 30 | 
 31 | // Tools list
 32 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 33 |   return {
 34 |     tools: [
 35 |       {
 36 |         name: 'list_files',
 37 |         description: 'List all files in an Overleaf project',
 38 |         inputSchema: {
 39 |           type: 'object',
 40 |           properties: {
 41 |             extension: { type: 'string', description: 'File extension filter (e.g., .tex)', default: '.tex' },
 42 |             projectName: { type: 'string', description: 'Project name (default, project2, etc.)' },
 43 |             gitToken: { type: 'string', description: 'Git token (optional, uses env var)' },
 44 |             projectId: { type: 'string', description: 'Project ID (optional, uses env var)' }
 45 |           },
 46 |           additionalProperties: false
 47 |         }
 48 |       },
 49 |       {
 50 |         name: 'read_file',
 51 |         description: 'Read a file from an Overleaf project',
 52 |         inputSchema: {
 53 |           type: 'object',
 54 |           properties: {
 55 |             filePath: { type: 'string', description: 'Path to the file' },
 56 |             projectName: { type: 'string', description: 'Project name (default, project2, etc.)' },
 57 |             gitToken: { type: 'string', description: 'Git token (optional, uses env var)' },
 58 |             projectId: { type: 'string', description: 'Project ID (optional, uses env var)' }
 59 |           },
 60 |           required: ['filePath'],
 61 |           additionalProperties: false
 62 |         }
 63 |       },
 64 |       {
 65 |         name: 'get_sections',
 66 |         description: 'Get all sections from a LaTeX file',
 67 |         inputSchema: {
 68 |           type: 'object',
 69 |           properties: {
 70 |             filePath: { type: 'string', description: 'Path to the LaTeX file' },
 71 |             projectName: { type: 'string', description: 'Project name (default, project2, etc.)' },
 72 |             gitToken: { type: 'string', description: 'Git token (optional, uses env var)' },
 73 |             projectId: { type: 'string', description: 'Project ID (optional, uses env var)' }
 74 |           },
 75 |           required: ['filePath'],
 76 |           additionalProperties: false
 77 |         }
 78 |       },
 79 |       {
 80 |         name: 'get_section_content',
 81 |         description: 'Get content of a specific section',
 82 |         inputSchema: {
 83 |           type: 'object',
 84 |           properties: {
 85 |             filePath: { type: 'string', description: 'Path to the LaTeX file' },
 86 |             sectionTitle: { type: 'string', description: 'Title of the section' },
 87 |             projectName: { type: 'string', description: 'Project name (default, project2, etc.)' },
 88 |             gitToken: { type: 'string', description: 'Git token (optional, uses env var)' },
 89 |             projectId: { type: 'string', description: 'Project ID (optional, uses env var)' }
 90 |           },
 91 |           required: ['filePath', 'sectionTitle'],
 92 |           additionalProperties: false
 93 |         }
 94 |       },
 95 |       {
 96 |         name: 'status_summary',
 97 |         description: 'Get a summary of the project status using default credentials',
 98 |         inputSchema: {
 99 |           type: 'object',
100 |           properties: {
101 |             projectName: { type: 'string', description: 'Project name (default, project2, etc.)' }
102 |           },
103 |           additionalProperties: false
104 |         }
105 |       },
106 |       {
107 |         name: 'list_projects',
108 |         description: 'List all available projects',
109 |         inputSchema: {
110 |           type: 'object',
111 |           properties: {},
112 |           additionalProperties: false
113 |         }
114 |       }
115 |     ]
116 |   };
117 | });
118 | 
119 | // Tool execution
120 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
121 |   const { name, arguments: args } = request.params;
122 |   
123 |   try {
124 |     // Get project information
125 |     function getProjectConfig(projectName) {
126 |       if (projectName && projectsConfig.projects && projectsConfig.projects[projectName]) {
127 |         return projectsConfig.projects[projectName];
128 |       }
129 |       // Use 'default' project as fallback
130 |       if (projectsConfig.projects && projectsConfig.projects.default) {
131 |         return projectsConfig.projects.default;
132 |       }
133 |       throw new Error('No project configuration found. Please set up projects.json with at least a "default" project.');
134 |     }
135 |     
136 |     const projectConfig = getProjectConfig(args.projectName);
137 |     const gitToken = args.gitToken || projectConfig.gitToken;
138 |     const projectId = args.projectId || projectConfig.projectId;
139 |     
140 |     if (!gitToken || !projectId) {
141 |       throw new Error('Git token and project ID are required. Set in projects.json or environment variables.');
142 |     }
143 |     
144 |     const client = new OverleafGitClient(gitToken, projectId);
145 |     
146 |     switch (name) {
147 |       case 'list_files':
148 |         const files = await client.listFiles(args.extension || '.tex');
149 |         return {
150 |           content: [{
151 |             type: 'text',
152 |             text: `Files found: ${files.length}\n\n${files.map(f => `• ${f}`).join('\n')}`
153 |           }]
154 |         };
155 |       
156 |       case 'read_file':
157 |         const content = await client.readFile(args.filePath);
158 |         return {
159 |           content: [{
160 |             type: 'text',
161 |             text: `File: ${args.filePath}\nSize: ${content.length} characters\n\n${content}`
162 |           }]
163 |         };
164 |       
165 |       case 'get_sections':
166 |         const sections = await client.getSections(args.filePath);
167 |         const sectionSummary = sections.map((s, i) => 
168 |           `${i + 1}. [${s.type}] ${s.title}\n   Content preview: ${s.content.substring(0, 100).replace(/\s+/g, ' ')}...`
169 |         ).join('\n\n');
170 |         return {
171 |           content: [{
172 |             type: 'text',
173 |             text: `Sections in ${args.filePath} (${sections.length} total):\n\n${sectionSummary}`
174 |           }]
175 |         };
176 |       
177 |       case 'get_section_content':
178 |         const section = await client.getSection(args.filePath, args.sectionTitle);
179 |         if (!section) {
180 |           throw new Error(`Section "${args.sectionTitle}" not found`);
181 |         }
182 |         return {
183 |           content: [{
184 |             type: 'text',
185 |             text: `Section: ${section.title}\nType: ${section.type}\nContent length: ${section.content.length} characters\n\n${section.content}`
186 |           }]
187 |         };
188 |       
189 |       case 'list_projects':
190 |         const projectList = Object.entries(projectsConfig.projects || {}).map(([key, project]) => 
191 |           `• ${key}: ${project.name} (${project.projectId})`
192 |         );
193 |         return {
194 |           content: [{
195 |             type: 'text',
196 |             text: `Available Projects:\n\n${projectList.join('\n') || 'No projects configured in projects.json'}`
197 |           }]
198 |         };
199 |       
200 |       case 'status_summary':
201 |         // Project status summary
202 |         const allFiles = await client.listFiles('.tex');
203 |         const projectName = projectConfig.name || 'Unknown Project';
204 |         let summary = `📄 ${projectName} Status Summary\n\n`;
205 |         summary += `Project ID: ${projectId}\n`;
206 |         summary += `Total .tex files: ${allFiles.length}\n`;
207 |         summary += `Files: ${allFiles.join(', ')}\n\n`;
208 |         
209 |         if (allFiles.length > 0) {
210 |           const mainFile = allFiles.find(f => f.includes('main')) || allFiles[0];
211 |           const sections = await client.getSections(mainFile);
212 |           summary += `📋 Structure of ${mainFile}:\n`;
213 |           summary += `Total sections: ${sections.length}\n\n`;
214 |           
215 |           sections.slice(0, 10).forEach((s, i) => {
216 |             summary += `${i + 1}. [${s.type}] ${s.title}\n`;
217 |           });
218 |           
219 |           if (sections.length > 10) {
220 |             summary += `... and ${sections.length - 10} more sections\n`;
221 |           }
222 |         }
223 |         
224 |         return {
225 |           content: [{
226 |             type: 'text',
227 |             text: summary
228 |           }]
229 |         };
230 |       
231 |       default:
232 |         throw new Error(`Unknown tool: ${name}`);
233 |     }
234 |   } catch (error) {
235 |     return {
236 |       content: [{
237 |         type: 'text',
238 |         text: `Error: ${error.message}`
239 |       }],
240 |       isError: true
241 |     };
242 |   }
243 | });
244 | 
245 | // Start server
246 | async function main() {
247 |   const transport = new StdioServerTransport();
248 |   await server.connect(transport);
249 | }
250 | 
251 | main().catch(() => {});
```