Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
56f8c32
Forgot minor version bump in MCP server
SemPlaatsman Mar 14, 2026
75a9267
normalise CRLF on read
SemPlaatsman Mar 14, 2026
6183a4c
move WRITE_TOOLS before getProject; disallowedTools startup; remove s…
SemPlaatsman Mar 14, 2026
972d440
Merge pull request #10 from SemPlaatsman/fix/polish
SemPlaatsman Mar 14, 2026
aa34156
Export tools for external usage (for now integration tests)
SemPlaatsman Mar 15, 2026
8b13860
Gitignore spec directory
SemPlaatsman Mar 15, 2026
26d090e
Export `PREVIEW_MAX_LENGTH` for usage in unit tests
SemPlaatsman Mar 15, 2026
8d4d287
First working version of unit tests
SemPlaatsman Mar 15, 2026
a396011
Overlapping positions by contract unit test addition
SemPlaatsman Mar 15, 2026
a02f7f7
Module level project import and server hang fix
SemPlaatsman Mar 15, 2026
5929192
Groups 1-4 for integration tests (tested)
SemPlaatsman Mar 15, 2026
5b2b46f
let `getProject` accept config object as param for integration testing
SemPlaatsman Mar 15, 2026
109e384
add integration test groups 5–8 and restore script
SemPlaatsman Mar 15, 2026
7163a2c
add unit and integration test workflows with npm test scripts
SemPlaatsman Mar 15, 2026
2d3181a
Keeping the workflow_dispatch regardless for easy debug
SemPlaatsman Mar 15, 2026
89e04fc
Retry for restore.js and higher intergroup delay
SemPlaatsman Mar 15, 2026
3704b79
Comments for future rate limiting issues
SemPlaatsman Mar 15, 2026
3cdfe50
Adding testing to README
SemPlaatsman Mar 15, 2026
ef800b0
Merge pull request #13 from SemPlaatsman/ci/testing-pipeline
SemPlaatsman Mar 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Integration Tests

on:
pull_request:
branches: [main]
workflow_dispatch:

jobs:
integration:
runs-on: ubuntu-latest
env:
OVERLEAF_GIT_AUTHOR_NAME: OverleafMCP CI
OVERLEAF_GIT_AUTHOR_EMAIL: ci@overleafmcp.test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Write projects.json from secrets
run: |
cat > projects.json << 'EOF'
{
"projects": {
"example_project": {
"name": "Example Project",
"projectId": "${{ secrets.EXAMPLE_PROJECT_ID }}",
"gitToken": "${{ secrets.EXAMPLE_PROJECT_TOKEN }}",
"readOnly": false
},
"readonly_example_project": {
"name": "ReadOnly Example Project",
"projectId": "${{ secrets.READONLY_PROJECT_ID }}",
"gitToken": "${{ secrets.READONLY_PROJECT_TOKEN }}",
"readOnly": true
}
}
}
EOF
- name: Run integration tests
run: node --test test/integration.test.js
- name: Restore test files on failure
if: failure()
run: node test/restore.js
18 changes: 18 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Unit Tests

on:
pull_request:
branches: [dev, main]
workflow_dispatch:

jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: node --test test/unit.test.js
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
temp/
spec/
projects.json
.env
*.log
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)
![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
![MCP](https://img.shields.io/badge/MCP-compatible-purple)
![Unit Tests](https://github.com/SemPlaatsman/OverleafMCP/actions/workflows/unit-tests.yml/badge.svg)
![Integration Tests](https://github.com/SemPlaatsman/OverleafMCP/actions/workflows/integration-tests.yml/badge.svg)

> Give any MCP-compatible AI assistant direct access to your Overleaf projects.

Expand Down Expand Up @@ -284,6 +286,26 @@ Resolution order: `readOnly: true` takes precedence over everything and blocks a

---

## Testing

OverleafMCP has a two-layer test suite built on Node.js's built-in `node:test` module — no extra test dependencies required.

**Unit tests** cover pure logic: section and BibTeX parsing, path traversal protection, and anchor uniqueness. They run in under a second with no credentials or network access:

```bash
npm test
```

**Integration tests** run the full tool stack against a live Overleaf project via git, verifying every tool end-to-end including push verification via `listHistory`. They require a `projects.json` with writable and read-only test projects configured:

```bash
npm run test:integration
```

CI runs unit tests on every PR to `dev` and `main`. Integration tests run on PRs to `main` only.

---

## Attribution

See [ATTRIBUTION.md](./ATTRIBUTION.md) for credits to the open-source projects that informed this work.
Expand Down
10 changes: 8 additions & 2 deletions overleaf-git-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const SECTION_LEVELS = {
subparagraph: 6,
};

export const PREVIEW_MAX_LENGTH = 100;

export class OverleafGitClient {
constructor(projectId, gitToken, tempDir) {
this.projectId = projectId;
Expand Down Expand Up @@ -238,7 +240,11 @@ export class OverleafGitClient {

async readFile(filePath) {
await this.cloneOrPull();
return fs.readFile(this._safePath(filePath), 'utf-8');
const content = await fs.readFile(this._safePath(filePath), 'utf-8');
// Normalise Windows-style CRLF to LF. Overleaf's git server uses CRLF on
// some projects. Without this, str_replace and insert_before/after anchors
// constructed with LF would fail to match even when visually identical.
return content.replace(/\r\n/g, '\n');
}

/**
Expand Down Expand Up @@ -271,7 +277,7 @@ export class OverleafGitClient {
const contentStart = entry._cmdEndIndex;
const contentEnd = i + 1 < flat.length ? flat[i + 1].startIndex : content.length;
entry.content = content.substring(contentStart, contentEnd).trim();
entry.preview = entry.content.substring(0, 100).replace(/\s+/g, ' ');
entry.preview = entry.content.substring(0, PREVIEW_MAX_LENGTH).replace(/\s+/g, ' ');
delete entry._cmdEndIndex;
});

Expand Down
Loading
Loading