bibtex-reference-manager-mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +94 -7
- package/build/src/analyze.js +41 -0
- package/build/src/bib.js +116 -9
- package/build/src/server.js +1 -1
- package/build/src/tools.js +41 -8
- package/llms.txt +171 -8
- package/package.json +20 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- Added `fix_entries`, which applies the safe fixes from `lint_entries` (double hyphen page ranges, capitalized titles, bare DOIs) and returns the cleaned BibTeX with a list of each change.
|
|
6
|
+
|
|
7
|
+
- `convert_bibliography` takes an optional `style` (`apa`, `mla` or `ieee`) for the plain text output, so a formatted reference list takes one call. Without it the output is the same as before.
|
|
8
|
+
|
|
9
|
+
## 0.1.1
|
|
10
|
+
|
|
11
|
+
- The README now has a Common workflows section showing how the five tools work together in a typical thesis or cleaning workflow.
|
|
12
|
+
|
|
13
|
+
- The README now has a competitor comparison table showing how it differs from servers that connect to online accounts or web APIs.
|
|
14
|
+
|
|
15
|
+
- The README and llms.txt now have a Why this server section and steps for exporting a Zotero or Mendeley library to .bib or RIS and pasting it in.
|
|
16
|
+
|
|
17
|
+
- The README intro, the npm description and the npm keywords now use the words students search for, such as LaTeX, Zotero export, Mendeley export, EndNote RIS, thesis bibliography and fix .bib file.
|
|
18
|
+
|
|
19
|
+
- BibTeX and RIS parse errors now name the entry, the field and the line that failed, for example an unclosed brace, an unclosed quote, an entry that is never closed, or a RIS record without a TY line. Before, the parser guessed and returned a damaged entry.
|
|
20
|
+
|
|
3
21
|
## 0.1.0
|
|
4
22
|
|
|
5
23
|
- First release of BibTeX Reference Manager MCP.
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
BibTeX Reference Manager MCP is a free, open source [MCP](https://modelcontextprotocol.io) server, published on npm as `bibtex-reference-manager-mcp`. It cleans, converts, dedupes and formats BibTeX, RIS and CSL JSON reference lists, so you stop fixing broken `.bib` files by hand. Paste a list into your AI app and ask it to read the entries, convert them to another format, find the duplicates, flag the problems or give every entry a tidy cite key.
|
|
8
8
|
|
|
9
|
+
It is made with students in mind. Writing a thesis in LaTeX? Ask it to fix a broken `.bib` file, clean up a Zotero export or a Mendeley export, turn an EndNote RIS file into BibTeX, and tidy your thesis bibliography before you hand it in.
|
|
10
|
+
|
|
9
11
|
Use it with Claude Desktop, Claude Code, Cursor, VS Code Copilot and any other app that supports MCP. It runs on your computer, needs no internet and no login, and your references never leave your machine.
|
|
10
12
|
|
|
11
13
|
Free forever. No account and no subscription.
|
|
@@ -18,6 +20,14 @@ Run it straight from npm with `npx`. You need Node.js 20 or newer, see [Node ver
|
|
|
18
20
|
npx -y bibtex-reference-manager-mcp
|
|
19
21
|
```
|
|
20
22
|
|
|
23
|
+
## Try these prompts
|
|
24
|
+
|
|
25
|
+
Paste your reference list into the chat first, or attach the file, then ask:
|
|
26
|
+
|
|
27
|
+
1. "Here is my references.bib. Lint it and tell me what is broken, entry by entry."
|
|
28
|
+
2. "Find the duplicates in this list and give me back a clean BibTeX file without them."
|
|
29
|
+
3. "Convert these RIS entries from my library export to BibTeX, then rename the keys so they look like smith2024deep."
|
|
30
|
+
|
|
21
31
|
Add it to your AI app's MCP config so the app can start it for you:
|
|
22
32
|
|
|
23
33
|
- [Claude Desktop](#claude-desktop)
|
|
@@ -84,22 +94,55 @@ Add this to `.vscode/mcp.json` in your workspace:
|
|
|
84
94
|
}
|
|
85
95
|
```
|
|
86
96
|
|
|
87
|
-
##
|
|
97
|
+
## Why this server
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
Some MCP servers connect to a reference manager account and read your library online. This one does not. It works on text you paste in, so it is a different tool for a different job.
|
|
90
100
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
- It needs no account, no API key and no internet. Nothing is sent anywhere.
|
|
102
|
+
- It does not connect to Zotero or Mendeley. You export a file and paste it in, so it works with any reference manager that can write BibTeX, RIS or CSL JSON.
|
|
103
|
+
- The work is done by code, not by the AI guessing. Converting, deduping, linting and renaming keys give the same result every time.
|
|
104
|
+
- It is read only. It never edits your library or your files. You get the result back and decide what to keep.
|
|
105
|
+
|
|
106
|
+
If you want an assistant to search or change a live online library, you need a server that connects to that service. This one is for cleaning up an exported list.
|
|
107
|
+
|
|
108
|
+
## Competitor comparison
|
|
109
|
+
|
|
110
|
+
Other MCP servers work with reference libraries too. Most connect to an online account or a web API, so your data leaves your computer and you need an account or an API key. This server works on text you paste in, so it needs no account, no API key and no internet, and nothing is sent anywhere.
|
|
111
|
+
|
|
112
|
+
| Server | Works offline | Needs account or key | Reads a live library | Read only | Formats |
|
|
113
|
+
| --- | --- | --- | --- | --- | --- |
|
|
114
|
+
| BibTeX Reference Manager MCP | Yes | No | No | Yes | BibTeX, RIS, CSL JSON |
|
|
115
|
+
| zotero-mcp | No | Yes | Yes | No | Zotero, BibTeX |
|
|
116
|
+
| mendeley-mcp | No | Yes | Yes | No | Mendeley, BibTeX |
|
|
117
|
+
| bibtex-mcp | No | Optional | No | Yes | BibTeX |
|
|
118
|
+
| reference-mcp | No | No | No | Yes | BibTeX |
|
|
119
|
+
| CrossRef-MCP | No | No | No | Yes | BibTeX |
|
|
120
|
+
| mcp_doi2bib | No | No | No | Yes | BibTeX |
|
|
121
|
+
| Local-Zotero-MCP | No | No | Yes | Yes | Zotero |
|
|
122
|
+
| jabref-mcp | No | No | Yes | No | BibTeX |
|
|
123
|
+
|
|
124
|
+
zotero-mcp and mendeley-mcp connect to your online Zotero or Mendeley library and can change it. bibtex-mcp, reference-mcp, CrossRef-MCP and mcp_doi2bib fetch citations from web sources such as Semantic Scholar, OpenAlex, CiteAs, Google Scholar and CrossRef. Local-Zotero-MCP reads your local Zotero library through the Zotero desktop app. jabref-mcp reads a JabRef .bib file and can add entries through JabRef.
|
|
125
|
+
|
|
126
|
+
Pick this server when you want to clean, convert, dedupe, lint and rename an exported list without sending it anywhere. Pick one of the others when you want to search or change a live online library.
|
|
127
|
+
|
|
128
|
+
## Export from Zotero or Mendeley
|
|
129
|
+
|
|
130
|
+
1. In Zotero, right click a collection (or select some items), choose the export option and pick BibTeX or RIS. In Mendeley, select your references, choose Export and pick BibTeX or RIS. Menu names change between versions, so look for Export.
|
|
131
|
+
2. Open the saved `.bib` or `.ris` file in any text editor and copy everything, or attach the file in an app that accepts attachments.
|
|
132
|
+
3. Paste it into your AI app and ask, for example: "Lint this list, remove the duplicates and give me back clean BibTeX with tidy cite keys."
|
|
133
|
+
4. Copy the result into your `.bib` file. To get it back into your reference manager, save it as a `.bib` or `.ris` file and import it.
|
|
134
|
+
|
|
135
|
+
The format is detected for you. If a file is not recognized, say which format it is.
|
|
94
136
|
|
|
95
137
|
## Tools
|
|
96
138
|
|
|
97
139
|
| Tool | What it does |
|
|
98
140
|
| --- | --- |
|
|
99
141
|
| `parse_bibliography` | Reads BibTeX, RIS or CSL JSON text and returns normalized entries: lower case field names, tidy spacing, bare DOIs, the year filled in from a date, and authors split into "Family, Given". |
|
|
100
|
-
| `convert_bibliography` | Converts between BibTeX, RIS, CSL JSON and plain text. The input format is detected unless you pass `from`. Page ranges become `12--15` in BibTeX. |
|
|
142
|
+
| `convert_bibliography` | Converts between BibTeX, RIS, CSL JSON and plain text. The input format is detected unless you pass `from`. Page ranges become `12--15` in BibTeX. For plain text, pass `style` as `apa` (the default), `mla` or `ieee` to get a formatted reference list. |
|
|
101
143
|
| `dedupe_entries` | Finds duplicates by DOI, by the same cite key and by similar titles (the years must not clash). Says which entry to keep and returns a BibTeX list without the extra copies. |
|
|
102
144
|
| `lint_entries` | Flags missing required fields, bad page ranges, uncapitalized titles, invalid DOIs, odd years and repeated keys. |
|
|
145
|
+
| `fix_entries` | Applies the safe fixes from `lint_entries` and returns the cleaned BibTeX: double hyphen page ranges, capitalized titles and bare DOIs. It lists each change and does not guess missing fields. |
|
|
103
146
|
| `generate_cite_keys` | Makes keys from the first author, the year and the first useful title word, like `smith2024deep`. Repeats get a letter, like `smith2024deepa`. |
|
|
104
147
|
|
|
105
148
|
Every tool takes the reference list as `text`, and every tool returns both a readable answer and structured fields. All of them are read only. They never change a file, because you paste the text in and get the result back.
|
|
@@ -110,6 +153,30 @@ Every tool takes the reference list as `text`, and every tool returns both a rea
|
|
|
110
153
|
- RIS: the usual tags such as `TY`, `AU`, `TI`, `T2`, `PY`, `SP`, `EP`, `DO` and `KW`.
|
|
111
154
|
- CSL JSON: a list of items, one item, or an object with an `items` list.
|
|
112
155
|
|
|
156
|
+
## Common workflows
|
|
157
|
+
|
|
158
|
+
These show how the six tools fit together. All of them take the reference list as `text` and are read only.
|
|
159
|
+
|
|
160
|
+
### Clean a Zotero or Mendeley export
|
|
161
|
+
|
|
162
|
+
1. Run `parse_bibliography` to normalize the entries and fill in missing years.
|
|
163
|
+
2. Run `dedupe_entries` to remove duplicates by DOI, cite key or title.
|
|
164
|
+
3. Run `lint_entries` to see missing fields, bad page ranges and odd years.
|
|
165
|
+
4. Run `generate_cite_keys` to give every entry a tidy key like `smith2024deep`.
|
|
166
|
+
5. Run `convert_bibliography` to get back a clean BibTeX file.
|
|
167
|
+
|
|
168
|
+
### Turn an EndNote RIS export into BibTeX
|
|
169
|
+
|
|
170
|
+
1. Run `parse_bibliography` on the RIS text to check it parses.
|
|
171
|
+
2. Run `convert_bibliography` with `to` set to BibTeX.
|
|
172
|
+
3. Run `generate_cite_keys` if you want keys like `smith2024deep`.
|
|
173
|
+
|
|
174
|
+
### Check a finished .bib before you hand it in
|
|
175
|
+
|
|
176
|
+
1. Run `parse_bibliography` to normalize.
|
|
177
|
+
2. Run `lint_entries`, then `fix_entries` to apply the safe fixes it can make.
|
|
178
|
+
3. Run `dedupe_entries` to make sure nothing is listed twice.
|
|
179
|
+
|
|
113
180
|
## Privacy
|
|
114
181
|
|
|
115
182
|
Everything runs on your computer. The server reads only the text your AI app passes to it. It makes no network requests, does not look up DOIs online, needs no API key and keeps nothing between calls. DOI checks only look at the shape of the DOI, they do not ask a website whether it exists.
|
|
@@ -134,7 +201,27 @@ npm run build
|
|
|
134
201
|
npm test
|
|
135
202
|
```
|
|
136
203
|
|
|
137
|
-
The code is in `src`. `src/bib.ts` reads and writes the formats, `src/analyze.ts` finds duplicates and lint problems, and `src/tools.ts` defines the
|
|
204
|
+
The code is in `src`. `src/bib.ts` reads and writes the formats, `src/analyze.ts` finds duplicates and lint problems, and `src/tools.ts` defines the six tools.
|
|
205
|
+
|
|
206
|
+
## More free student MCP servers
|
|
207
|
+
|
|
208
|
+
All free, open source and read only where they touch your accounts. Browse the full list at [brightspacemcp.com/more-mcps](https://brightspacemcp.com/more-mcps/).
|
|
209
|
+
|
|
210
|
+
| Server | What it does |
|
|
211
|
+
| --- | --- |
|
|
212
|
+
| [Brightspace MCP](https://github.com/AnonymousOnyx22/brightspace-d2l-mcp) | D2L Brightspace for AI. Due dates with real submission status, grades, a target grade calculator and course files. |
|
|
213
|
+
| [Canvas MCP](https://github.com/AnonymousOnyx22/canvas-lms-student-mcp) | Canvas LMS for AI, read only. Due dates, missing work, grades and announcements. |
|
|
214
|
+
| [Moodle MCP](https://github.com/AnonymousOnyx22/moodle-student-mcp) | Moodle for AI, read only. Due dates, overdue work, grades and announcements. |
|
|
215
|
+
| [Google Classroom MCP](https://github.com/AnonymousOnyx22/google-classroom-student-mcp) | Google Classroom for AI, read only. Todos, due dates, missing work and grades. |
|
|
216
|
+
| [GPA Calculator MCP](https://github.com/AnonymousOnyx22/gpa-calculator-mcp) | GPA, grade conversion and the grade you need on the final, for AI. Works offline, no account. |
|
|
217
|
+
| [Citations MCP](https://github.com/AnonymousOnyx22/student-citations-mcp) | APA, MLA and Chicago citations from a DOI, an ISBN or typed details. No account. |
|
|
218
|
+
| [Study Planner MCP](https://github.com/AnonymousOnyx22/study-planner-mcp) | Exam study schedules, spaced repetition dates, pomodoro plans and assignment milestones. Works offline. |
|
|
219
|
+
| [Paper Finder MCP](https://github.com/AnonymousOnyx22/paper-finder-student-mcp) | Search scholarly papers, find open access copies and related work through OpenAlex. No key needed. |
|
|
220
|
+
| [Open Textbook Finder MCP](https://github.com/AnonymousOnyx22/open-textbook-finder-mcp) | Find free legal copies of textbooks on OpenStax, DOAB and Open Library, look up editions by ISBN and suggest cheaper older ones. |
|
|
221
|
+
| [STEM Calculator MCP](https://github.com/AnonymousOnyx22/stem-calc-student-mcp) | Chemistry and physics homework math: molar mass, balancing equations, grams to moles, unit conversion and significant figures. Works offline. |
|
|
222
|
+
| [Essay Stats MCP](https://github.com/AnonymousOnyx22/essay-stats-mcp) | Offline writing metrics for essays. Word and page counts, readability scores, passive voice, repeated openers, filler words, long sentences and word limit checks, so students can check their work against assignment requirements. No account, no network. |
|
|
223
|
+
| [Syllabus to Calendar MCP](https://github.com/AnonymousOnyx22/syllabus-calendar-mcp) | Syllabus to calendar MCP server. Extract dates from syllabus text, build an ics calendar, expand recurring classes and merge calendars. Works offline, no account. |
|
|
224
|
+
| [Job Application Tracker MCP](https://github.com/AnonymousOnyx22/job-tracker-student-mcp) | Job application tracker MCP server. Track internships, jobs, interviews and deadlines offline, see what is coming up, and export your list as CSV or Markdown. No account and no API keys. |
|
|
138
225
|
|
|
139
226
|
## License
|
|
140
227
|
|
package/build/src/analyze.js
CHANGED
|
@@ -167,3 +167,44 @@ export function lintEntries(entries) {
|
|
|
167
167
|
}
|
|
168
168
|
return issues;
|
|
169
169
|
}
|
|
170
|
+
function capitalizeTitle(title) {
|
|
171
|
+
const raw = title.trim();
|
|
172
|
+
if (/^[a-z]/.test(raw))
|
|
173
|
+
return raw[0].toUpperCase() + raw.slice(1);
|
|
174
|
+
let depth = 0;
|
|
175
|
+
return raw
|
|
176
|
+
.split(/(\s+)/)
|
|
177
|
+
.map((word) => {
|
|
178
|
+
const inBraces = depth > 0 || word.startsWith("{");
|
|
179
|
+
for (const c of word)
|
|
180
|
+
depth += c === "{" ? 1 : c === "}" ? -1 : 0;
|
|
181
|
+
if (inBraces || SMALL_WORDS.has(word.toLowerCase()))
|
|
182
|
+
return word;
|
|
183
|
+
return word.replace(/^[a-z]/, (c) => c.toUpperCase());
|
|
184
|
+
})
|
|
185
|
+
.join("");
|
|
186
|
+
}
|
|
187
|
+
export function fixEntries(entries) {
|
|
188
|
+
const fixes = [];
|
|
189
|
+
const fixed = entries.map((e) => {
|
|
190
|
+
const f = { ...e.fields };
|
|
191
|
+
const set = (field, to) => {
|
|
192
|
+
if (to !== f[field])
|
|
193
|
+
fixes.push({ key: e.key, field, from: f[field], to });
|
|
194
|
+
f[field] = to;
|
|
195
|
+
};
|
|
196
|
+
if (f.pages && pageProblem(f.pages)?.startsWith("use a double hyphen")) {
|
|
197
|
+
set("pages", f.pages.trim().replace(/\s*[-–—]\s*/, "--"));
|
|
198
|
+
}
|
|
199
|
+
if (f.title && lintEntries([{ ...e, fields: f }]).some((i) => i.rule === "uncapitalized-title")) {
|
|
200
|
+
set("title", capitalizeTitle(f.title));
|
|
201
|
+
}
|
|
202
|
+
if (f.doi) {
|
|
203
|
+
const doi = cleanDoi(f.doi);
|
|
204
|
+
if (/^10\.\d{4,9}\/\S+$/.test(doi))
|
|
205
|
+
set("doi", doi);
|
|
206
|
+
}
|
|
207
|
+
return { ...e, fields: f };
|
|
208
|
+
});
|
|
209
|
+
return { entries: fixed, fixes };
|
|
210
|
+
}
|
package/build/src/bib.js
CHANGED
|
@@ -124,6 +124,14 @@ export function parseBibtex(text) {
|
|
|
124
124
|
const macros = {};
|
|
125
125
|
let i = 0;
|
|
126
126
|
const n = text.length;
|
|
127
|
+
// The entry being read, so an error can name it and the line it went wrong on.
|
|
128
|
+
let where = { type: "", key: "", field: "" };
|
|
129
|
+
const lineAt = (pos) => text.slice(0, pos).split("\n").length;
|
|
130
|
+
const fail = (what, pos) => {
|
|
131
|
+
const field = where.field ? ` in field ${where.field}` : "";
|
|
132
|
+
const entry = where.key ? `entry ${where.key}` : `@${where.type} entry`;
|
|
133
|
+
throw new Error(`${what}${field} of ${entry}, line ${lineAt(pos)}.`);
|
|
134
|
+
};
|
|
127
135
|
const skipWs = () => {
|
|
128
136
|
while (i < n && /\s/.test(text[i]))
|
|
129
137
|
i++;
|
|
@@ -132,6 +140,9 @@ export function parseBibtex(text) {
|
|
|
132
140
|
let depth = 0;
|
|
133
141
|
const start = i + 1;
|
|
134
142
|
for (; i < n; i++) {
|
|
143
|
+
// A new entry starting at the front of a line means this value was never closed.
|
|
144
|
+
if (text[i] === "\n" && /^\n[ \t]*@[A-Za-z]+\s*[{(]/.test(text.slice(i, i + 80)))
|
|
145
|
+
break;
|
|
135
146
|
if (text[i] === "{")
|
|
136
147
|
depth++;
|
|
137
148
|
else if (text[i] === "}" && --depth === 0) {
|
|
@@ -140,7 +151,7 @@ export function parseBibtex(text) {
|
|
|
140
151
|
return out;
|
|
141
152
|
}
|
|
142
153
|
}
|
|
143
|
-
return
|
|
154
|
+
return fail("Unclosed brace", start - 1);
|
|
144
155
|
};
|
|
145
156
|
const readQuoted = () => {
|
|
146
157
|
let depth = 0;
|
|
@@ -156,7 +167,7 @@ export function parseBibtex(text) {
|
|
|
156
167
|
return out;
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
|
-
return
|
|
170
|
+
return fail("Unclosed quote", start - 1);
|
|
160
171
|
};
|
|
161
172
|
const readValue = () => {
|
|
162
173
|
let out = "";
|
|
@@ -191,6 +202,8 @@ export function parseBibtex(text) {
|
|
|
191
202
|
if (!head)
|
|
192
203
|
continue;
|
|
193
204
|
const type = head[1].toLowerCase();
|
|
205
|
+
const entryLine = lineAt(at);
|
|
206
|
+
where = { type, key: "", field: "" };
|
|
194
207
|
i += head[0].length;
|
|
195
208
|
const close = head[2] === "{" ? "}" : ")";
|
|
196
209
|
if (type === "comment" || type === "preamble") {
|
|
@@ -219,11 +232,13 @@ export function parseBibtex(text) {
|
|
|
219
232
|
const keyM = text.slice(i).match(/^[^,\s{}]*/);
|
|
220
233
|
const key = keyM ? keyM[0] : "";
|
|
221
234
|
i += key.length;
|
|
235
|
+
where.key = key;
|
|
222
236
|
const fields = {};
|
|
223
237
|
for (;;) {
|
|
224
238
|
skipWs();
|
|
225
|
-
if (i >= n)
|
|
226
|
-
|
|
239
|
+
if (i >= n) {
|
|
240
|
+
throw new Error(`Entry ${key || `@${type}`} starting on line ${entryLine} is never closed. Add a closing "${close}" at the end of it.`);
|
|
241
|
+
}
|
|
227
242
|
if (text[i] === ",") {
|
|
228
243
|
i++;
|
|
229
244
|
continue;
|
|
@@ -242,7 +257,9 @@ export function parseBibtex(text) {
|
|
|
242
257
|
if (text[i] !== "=")
|
|
243
258
|
continue;
|
|
244
259
|
i++;
|
|
245
|
-
|
|
260
|
+
where.field = nameM[0].toLowerCase();
|
|
261
|
+
fields[where.field] = readValue();
|
|
262
|
+
where.field = "";
|
|
246
263
|
}
|
|
247
264
|
entries.push(normalizeEntry({ type, key, fields }));
|
|
248
265
|
}
|
|
@@ -280,11 +297,16 @@ const RIS_OUT = {
|
|
|
280
297
|
export function parseRis(text) {
|
|
281
298
|
const entries = [];
|
|
282
299
|
let tags = [];
|
|
300
|
+
let startLine = 1;
|
|
301
|
+
let lineNo = 0;
|
|
283
302
|
const flush = () => {
|
|
284
303
|
if (!tags.length)
|
|
285
304
|
return;
|
|
286
305
|
const get = (...t) => tags.find(([k]) => t.includes(k))?.[1];
|
|
287
306
|
const all = (...t) => tags.filter(([k]) => t.includes(k)).map(([, v]) => v);
|
|
307
|
+
if (!get("TY")) {
|
|
308
|
+
throw new Error(`The RIS record starting on line ${startLine} has no TY line. Every record must begin with "TY - " and a type like JOUR.`);
|
|
309
|
+
}
|
|
288
310
|
const ty = get("TY") ?? "GEN";
|
|
289
311
|
const type = RIS_TYPES[ty] ?? "misc";
|
|
290
312
|
const fields = {};
|
|
@@ -318,7 +340,10 @@ export function parseRis(text) {
|
|
|
318
340
|
tags = [];
|
|
319
341
|
};
|
|
320
342
|
for (const line of text.split(/\r?\n/)) {
|
|
343
|
+
lineNo++;
|
|
321
344
|
const m = line.match(/^([A-Z][A-Z0-9])\s{2}-\s?(.*)$/);
|
|
345
|
+
if (m && !tags.length)
|
|
346
|
+
startLine = lineNo;
|
|
322
347
|
if (!m) {
|
|
323
348
|
// A line that does not start with a tag continues the last value.
|
|
324
349
|
if (tags.length && line.trim())
|
|
@@ -329,6 +354,7 @@ export function parseRis(text) {
|
|
|
329
354
|
flush();
|
|
330
355
|
else if (m[1] === "TY" && tags.length) {
|
|
331
356
|
flush();
|
|
357
|
+
startLine = lineNo;
|
|
332
358
|
tags.push([m[1], m[2].trim()]);
|
|
333
359
|
}
|
|
334
360
|
else
|
|
@@ -484,9 +510,90 @@ function initials(given) {
|
|
|
484
510
|
.map((w) => (w.includes("-") ? w.split("-").map((p) => `${p[0].toUpperCase()}.`).join("-") : `${w[0].toUpperCase()}.`))
|
|
485
511
|
.join(" ");
|
|
486
512
|
}
|
|
487
|
-
|
|
513
|
+
function endWith(s) {
|
|
514
|
+
return /[.?!]$/.test(s) ? s : `${s}.`;
|
|
515
|
+
}
|
|
516
|
+
function mlaText(e) {
|
|
517
|
+
const f = e.fields;
|
|
518
|
+
const people = parseNames(f.author).map((n) => ({
|
|
519
|
+
first: n.literal || !n.given ? plain(n.family) : `${plain(n.family)}, ${plain(n.given)}`,
|
|
520
|
+
last: n.literal || !n.given ? plain(n.family) : `${plain(n.given)} ${plain(n.family)}`,
|
|
521
|
+
}));
|
|
522
|
+
let who = "";
|
|
523
|
+
if (people.length === 1)
|
|
524
|
+
who = people[0].first;
|
|
525
|
+
else if (people.length === 2)
|
|
526
|
+
who = `${people[0].first}, and ${people[1].last}`;
|
|
527
|
+
else if (people.length > 2)
|
|
528
|
+
who = `${people[0].first}, et al`;
|
|
529
|
+
const parts = [];
|
|
530
|
+
if (who)
|
|
531
|
+
parts.push(endWith(who));
|
|
532
|
+
const container = f.journal ?? f.booktitle;
|
|
533
|
+
if (f.title)
|
|
534
|
+
parts.push(container ? `"${endWith(plain(f.title))}"` : endWith(plain(f.title)));
|
|
535
|
+
const bits = [];
|
|
536
|
+
if (container)
|
|
537
|
+
bits.push(plain(container));
|
|
538
|
+
else if (f.publisher)
|
|
539
|
+
bits.push(plain(f.publisher));
|
|
540
|
+
if (container && f.volume)
|
|
541
|
+
bits.push(`vol. ${f.volume}`);
|
|
542
|
+
if (container && f.number)
|
|
543
|
+
bits.push(`no. ${f.number}`);
|
|
544
|
+
if (f.year)
|
|
545
|
+
bits.push(f.year);
|
|
546
|
+
if (container && f.pages)
|
|
547
|
+
bits.push(`pp. ${normalizePages(f.pages).replace("--", "-")}`);
|
|
548
|
+
if (bits.length)
|
|
549
|
+
parts.push(`${bits.join(", ")}.`);
|
|
550
|
+
if (f.doi)
|
|
551
|
+
parts.push(`https://doi.org/${f.doi}.`);
|
|
552
|
+
else if (f.url)
|
|
553
|
+
parts.push(`${f.url}.`);
|
|
554
|
+
return parts.join(" ");
|
|
555
|
+
}
|
|
556
|
+
function ieeeText(e, index) {
|
|
557
|
+
const f = e.fields;
|
|
558
|
+
const names = parseNames(f.author).map((n) => (n.literal || !n.given ? plain(n.family) : `${initials(plain(n.given))} ${plain(n.family)}`));
|
|
559
|
+
let who = "";
|
|
560
|
+
if (names.length === 1)
|
|
561
|
+
who = names[0];
|
|
562
|
+
else if (names.length === 2)
|
|
563
|
+
who = `${names[0]} and ${names[1]}`;
|
|
564
|
+
else if (names.length > 2)
|
|
565
|
+
who = `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
|
|
566
|
+
const container = f.journal ?? f.booktitle;
|
|
567
|
+
const title = f.title ? plain(f.title).replace(/[.,]$/, "") : "";
|
|
568
|
+
const bits = [];
|
|
569
|
+
if (container)
|
|
570
|
+
bits.push(plain(container));
|
|
571
|
+
else if (f.publisher)
|
|
572
|
+
bits.push(plain(f.publisher));
|
|
573
|
+
if (container && f.volume)
|
|
574
|
+
bits.push(`vol. ${f.volume}`);
|
|
575
|
+
if (container && f.number)
|
|
576
|
+
bits.push(`no. ${f.number}`);
|
|
577
|
+
if (container && f.pages)
|
|
578
|
+
bits.push(`pp. ${normalizePages(f.pages).replace("--", "-")}`);
|
|
579
|
+
if (f.year)
|
|
580
|
+
bits.push(f.year);
|
|
581
|
+
const head = [who, container && title ? `"${title},"` : title].filter(Boolean).join(", ");
|
|
582
|
+
let body = container && title ? `${head} ${bits.join(", ")}` : [head, bits.join(", ")].filter(Boolean).join(", ");
|
|
583
|
+
body = endWith(body.trim());
|
|
584
|
+
if (f.doi)
|
|
585
|
+
body += ` doi: ${f.doi}.`;
|
|
586
|
+
else if (f.url)
|
|
587
|
+
body += ` ${f.url}`;
|
|
588
|
+
return `[${index + 1}] ${body}`;
|
|
589
|
+
}
|
|
590
|
+
export function toText(entries, style = "apa") {
|
|
488
591
|
return entries
|
|
489
|
-
.map((e) => {
|
|
592
|
+
.map((e, i) => {
|
|
593
|
+
if (style === "mla")
|
|
594
|
+
return mlaText(e);
|
|
595
|
+
if (style === "ieee")
|
|
596
|
+
return ieeeText(e, i);
|
|
490
597
|
const f = e.fields;
|
|
491
598
|
const names = parseNames(f.author).map((n) => (n.literal || !n.given ? plain(n.family) : `${plain(n.family)}, ${initials(plain(n.given))}`));
|
|
492
599
|
let who = "";
|
|
@@ -537,7 +644,7 @@ export function parseAny(text, format = "auto") {
|
|
|
537
644
|
}
|
|
538
645
|
return { format: fmt, entries };
|
|
539
646
|
}
|
|
540
|
-
export function serialize(entries, format) {
|
|
647
|
+
export function serialize(entries, format, style = "apa") {
|
|
541
648
|
switch (format) {
|
|
542
649
|
case "bibtex":
|
|
543
650
|
return toBibtex(entries);
|
|
@@ -546,7 +653,7 @@ export function serialize(entries, format) {
|
|
|
546
653
|
case "csl_json":
|
|
547
654
|
return toCslJson(entries);
|
|
548
655
|
default:
|
|
549
|
-
return toText(entries);
|
|
656
|
+
return toText(entries, style);
|
|
550
657
|
}
|
|
551
658
|
}
|
|
552
659
|
/* ---------- cite keys ---------- */
|
package/build/src/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { registerTools } from "./core/tools.js";
|
|
3
3
|
import { tools } from "./tools.js";
|
|
4
|
-
export const VERSION = "0.1.
|
|
4
|
+
export const VERSION = "0.1.2";
|
|
5
5
|
export function createServer() {
|
|
6
6
|
const server = new McpServer({ name: "bibtex-reference-manager-mcp", version: VERSION });
|
|
7
7
|
registerTools(server, tools, null);
|
package/build/src/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { bullets } from "./core/tools.js";
|
|
3
|
-
import { findDuplicates, lintEntries } from "./analyze.js";
|
|
3
|
+
import { findDuplicates, fixEntries, lintEntries } from "./analyze.js";
|
|
4
4
|
import { makeKey, parseAny, parseNames, plain, serialize, toBibtex, uniqueKey } from "./bib.js";
|
|
5
5
|
const text = z.string().min(1).describe("The reference list as text: BibTeX, RIS or CSL JSON.");
|
|
6
6
|
const format = z
|
|
@@ -29,7 +29,7 @@ function plural(n, word) {
|
|
|
29
29
|
const parseBibliography = {
|
|
30
30
|
name: "parse_bibliography",
|
|
31
31
|
title: "Parse a bibliography",
|
|
32
|
-
description: "Read BibTeX, RIS or CSL JSON text and return the entries in one normalized shape: lower case field names, tidy whitespace, bare DOIs, the year filled in from a date, and authors split into \"Family, Given\". Nothing is sent anywhere.",
|
|
32
|
+
description: "Read BibTeX, RIS or CSL JSON text and return the entries in one normalized shape: lower case field names, tidy whitespace, bare DOIs, the year filled in from a date, and authors split into \"Family, Given\". Nothing is sent anywhere. Example: text \"@article{k1, AUTHOR = {John Smith and Jane Doe}, title={Deep Learning for Graphs}, year={2024}}\" returns one entry with authors [\"Smith, John\", \"Doe, Jane\"] and lower case field names. Leave format out unless auto picks the wrong one.",
|
|
33
33
|
inputSchema: { text, format },
|
|
34
34
|
outputSchema: {
|
|
35
35
|
format: z.string().describe("The format that was read: bibtex, ris or csl_json."),
|
|
@@ -48,11 +48,15 @@ const parseBibliography = {
|
|
|
48
48
|
const convertBibliography = {
|
|
49
49
|
name: "convert_bibliography",
|
|
50
50
|
title: "Convert a bibliography",
|
|
51
|
-
description: "Convert a reference list between BibTeX, RIS, CSL JSON and plain text (a simple
|
|
51
|
+
description: "Convert a reference list between BibTeX, RIS, CSL JSON and plain text (a simple reference list style, APA by default, or MLA or IEEE when you pass style). The input format is detected unless you pass it. Page ranges, DOIs and names are cleaned on the way. Example: text is a BibTeX entry and to is \"ris\", and output is the same reference as RIS. Set from only to override detection.",
|
|
52
52
|
inputSchema: {
|
|
53
53
|
text,
|
|
54
54
|
to: z.enum(["bibtex", "ris", "csl_json", "text"]).describe("The format to write."),
|
|
55
55
|
from: format,
|
|
56
|
+
style: z
|
|
57
|
+
.enum(["apa", "mla", "ieee"])
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("The citation style for the text output: apa, mla or ieee. Defaults to apa. Ignored for the other formats."),
|
|
56
60
|
},
|
|
57
61
|
outputSchema: {
|
|
58
62
|
from: z.string().describe("The format that was read."),
|
|
@@ -64,7 +68,7 @@ const convertBibliography = {
|
|
|
64
68
|
const { format: fmt, entries } = parseAny(a.text, a.from ?? "auto");
|
|
65
69
|
if (!entries.length)
|
|
66
70
|
throw new Error("No entries were found in the text, so there is nothing to convert.");
|
|
67
|
-
const output = serialize(entries, a.to);
|
|
71
|
+
const output = serialize(entries, a.to, a.style ?? "apa");
|
|
68
72
|
return {
|
|
69
73
|
text: `Converted ${plural(entries.length, "entry")} from ${fmt} to ${a.to}.\n\n${output}`,
|
|
70
74
|
structured: { from: fmt, to: a.to, count: entries.length, output },
|
|
@@ -74,7 +78,7 @@ const convertBibliography = {
|
|
|
74
78
|
const dedupeEntries = {
|
|
75
79
|
name: "dedupe_entries",
|
|
76
80
|
title: "Find duplicate entries",
|
|
77
|
-
description: "Find duplicate references by matching DOI, by the same cite key, and by similar titles (the years must not clash). Each group says which entry to keep, the one with the most fields, and which to remove. Also returns the list as BibTeX with the extra copies dropped.",
|
|
81
|
+
description: "Find duplicate references by matching DOI, by the same cite key, and by similar titles (the years must not clash). Each group says which entry to keep, the one with the most fields, and which to remove. Also returns the list as BibTeX with the extra copies dropped. Example: two entries with the same DOI, one with 3 fields and one with 6, give one group that keeps the entry with 6 fields and lists the other under remove. Your files are not changed.",
|
|
78
82
|
inputSchema: {
|
|
79
83
|
text,
|
|
80
84
|
format,
|
|
@@ -121,7 +125,7 @@ const dedupeEntries = {
|
|
|
121
125
|
const lintEntriesTool = {
|
|
122
126
|
name: "lint_entries",
|
|
123
127
|
title: "Lint entries",
|
|
124
|
-
description: "Check a reference list for problems: missing required fields for the entry type, bad page ranges, titles that are not capitalized, invalid DOIs, odd years and repeated keys. Each issue names the entry key, the field and a plain message.",
|
|
128
|
+
description: "Check a reference list for problems: missing required fields for the entry type, bad page ranges, titles that are not capitalized, invalid DOIs, odd years and repeated keys. Each issue names the entry key, the field and a plain message. Example: an @article with no journal field gives an issue with the rule missing-field and the field journal. Pass the whole list as text so repeated keys can be found.",
|
|
125
129
|
inputSchema: { text, format },
|
|
126
130
|
outputSchema: {
|
|
127
131
|
count: z.number().describe("How many entries were checked."),
|
|
@@ -147,10 +151,39 @@ const lintEntriesTool = {
|
|
|
147
151
|
};
|
|
148
152
|
},
|
|
149
153
|
};
|
|
154
|
+
const fixEntriesTool = {
|
|
155
|
+
name: "fix_entries",
|
|
156
|
+
title: "Fix entries",
|
|
157
|
+
description: "Apply the safe fixes that lint_entries points to and return the cleaned list as BibTeX: page ranges with one hyphen get a double hyphen, titles that start with a lower case letter or are all lower case get capitals (words inside braces are left alone), and DOIs written as a link or with a doi: prefix become bare DOIs. Missing fields, backwards page ranges and DOIs that are not valid are not guessed, so run lint_entries on the result. Example: pages \"12-15\" becomes \"12--15\". Your files are not changed.",
|
|
158
|
+
inputSchema: { text, format },
|
|
159
|
+
outputSchema: {
|
|
160
|
+
count: z.number().describe("How many entries were read."),
|
|
161
|
+
fixed: z.number().describe("How many changes were made."),
|
|
162
|
+
fixes: z.array(z.object({
|
|
163
|
+
key: z.string(),
|
|
164
|
+
field: z.enum(["pages", "title", "doi"]),
|
|
165
|
+
from: z.string(),
|
|
166
|
+
to: z.string(),
|
|
167
|
+
})),
|
|
168
|
+
bibtex: z.string().describe("The cleaned list as BibTeX."),
|
|
169
|
+
},
|
|
170
|
+
run: async (a) => {
|
|
171
|
+
const { entries } = parseAny(a.text, a.format ?? "auto");
|
|
172
|
+
if (!entries.length)
|
|
173
|
+
throw new Error("No entries were found in the text, so there is nothing to fix.");
|
|
174
|
+
const { entries: cleaned, fixes } = fixEntries(entries);
|
|
175
|
+
const bibtex = toBibtex(cleaned);
|
|
176
|
+
const lines = fixes.map((f) => `${f.key}: ${f.field} "${f.from}" becomes "${f.to}"`);
|
|
177
|
+
return {
|
|
178
|
+
text: `Checked ${plural(entries.length, "entry")} and made ${plural(fixes.length, "change")}.\n${bullets(lines, "Nothing needed fixing.")}\n\n${bibtex}`,
|
|
179
|
+
structured: { count: entries.length, fixed: fixes.length, fixes, bibtex },
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
150
183
|
const generateCiteKeys = {
|
|
151
184
|
name: "generate_cite_keys",
|
|
152
185
|
title: "Generate cite keys",
|
|
153
|
-
description: "Create consistent cite keys from the first author's family name, the year and the first meaningful word of the title, like smith2024deep. Repeated keys get a letter added. Returns the old and new key for each entry and the list as BibTeX with the new keys.",
|
|
186
|
+
description: "Create consistent cite keys from the first author's family name, the year and the first meaningful word of the title, like smith2024deep. Repeated keys get a letter added. Returns the old and new key for each entry and the list as BibTeX with the new keys. Example: an entry with key old1, author \"Smith, John\", year 2024 and title \"Deep Learning for Graphs\" gives old_key old1 and new_key smith2024deep, or smith2024 when include_title_word is false.",
|
|
154
187
|
inputSchema: {
|
|
155
188
|
text,
|
|
156
189
|
format,
|
|
@@ -177,4 +210,4 @@ const generateCiteKeys = {
|
|
|
177
210
|
};
|
|
178
211
|
},
|
|
179
212
|
};
|
|
180
|
-
export const tools = [parseBibliography, convertBibliography, dedupeEntries, lintEntriesTool, generateCiteKeys];
|
|
213
|
+
export const tools = [parseBibliography, convertBibliography, dedupeEntries, lintEntriesTool, fixEntriesTool, generateCiteKeys];
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BibTeX Reference Manager MCP
|
|
2
2
|
|
|
3
|
-
> A free, open source MCP server that cleans, converts, dedupes and formats BibTeX, RIS and CSL JSON reference lists. It runs offline on the user's computer with no account and no API key. npm package: bibtex-reference-manager-mcp. Version 0.1.
|
|
3
|
+
> A free, open source MCP server that cleans, converts, dedupes and formats BibTeX, RIS and CSL JSON reference lists. It runs offline on the user's computer with no account and no API key. npm package: bibtex-reference-manager-mcp. Version 0.1.2.
|
|
4
4
|
|
|
5
5
|
Use this server when a user pastes or attaches a reference list and wants it read, converted, deduplicated, checked or renamed. Pass the list as the `text` argument. The tools do the work in code, so do not rewrite entries by hand when a tool can do it.
|
|
6
6
|
|
|
@@ -23,22 +23,181 @@ MCP config for Claude Desktop, Cursor and similar apps:
|
|
|
23
23
|
|
|
24
24
|
Needs Node.js 20 or newer.
|
|
25
25
|
|
|
26
|
+
## Why this server
|
|
27
|
+
|
|
28
|
+
It works on pasted text, not on a live library. It needs no account, API key or internet, and sends nothing anywhere. It does not connect to Zotero or Mendeley; the user exports a file and pastes it in, so any reference manager that writes BibTeX, RIS or CSL JSON works. The work is done by code, so results are the same each time. All tools are read only.
|
|
29
|
+
|
|
30
|
+
## Exporting from Zotero or Mendeley
|
|
31
|
+
|
|
32
|
+
1. In Zotero, right click a collection or select items, choose the export option and pick BibTeX or RIS. In Mendeley, select references, choose Export and pick BibTeX or RIS.
|
|
33
|
+
2. Copy the contents of the saved .bib or .ris file, or attach the file.
|
|
34
|
+
3. Pass it as the `text` argument. The format is detected, or pass `format`.
|
|
35
|
+
4. Return the result to the user. To get it back into their reference manager, they save it as .bib or .ris and import it.
|
|
36
|
+
|
|
26
37
|
## Tools
|
|
27
38
|
|
|
28
|
-
All tools are read only, work offline and take `text` (the reference list). Most take an optional `format` of auto, bibtex, ris or csl_json. The default, auto, looks at the text.
|
|
39
|
+
All tools are read only, work offline and take `text` (the reference list). Most take an optional `format` of auto, bibtex, ris or csl_json. The default, auto, looks at the text. Every tool returns both a readable answer and structured fields.
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- dedupe_entries: finds duplicate groups by DOI, same cite key (ignoring case) and title similarity. Optional `title_threshold` from 0.5 to 1, default 0.9. Titles are not matched when both entries have a year and the years differ. Each group gives `keep` (the entry with the most fields) and `remove`. Also returns `deduplicated_bibtex`.
|
|
33
|
-
- lint_entries: returns issues with `key`, `rule`, `severity`, `field` and `message`. Rules: missing-field, bad-pages, uncapitalized-title, invalid-doi, bad-year, duplicate-key.
|
|
34
|
-
- generate_cite_keys: returns old and new keys and `bibtex` with the new keys. Keys look like smith2024deep. Set `include_title_word` to false for smith2024. Repeated keys get a letter, like smith2024deepa.
|
|
41
|
+
### parse_bibliography
|
|
42
|
+
Read BibTeX, RIS or CSL JSON text and return the entries in one normalized shape: lower case field names, tidy whitespace, bare DOIs, the year filled in from a date, and authors split into "Family, Given".
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
Inputs:
|
|
45
|
+
- text (required): the reference list
|
|
46
|
+
- format (optional): auto, bibtex, ris, or csl_json
|
|
47
|
+
|
|
48
|
+
Example input (BibTeX):
|
|
49
|
+
```bibtex
|
|
50
|
+
@article{smith2024,
|
|
51
|
+
AUTHOR = {John Smith and Jane Doe},
|
|
52
|
+
title={Deep Learning for Graphs},
|
|
53
|
+
year={2024},
|
|
54
|
+
doi = {https://doi.org/10.1000/abc123},
|
|
55
|
+
journal = {J Graph Learn}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Example result:
|
|
60
|
+
- format: bibtex
|
|
61
|
+
- count: 1
|
|
62
|
+
- entries[0].authors: ["Smith, John", "Doe, Jane"]
|
|
63
|
+
- entries[0].fields.doi: "10.1000/abc123"
|
|
64
|
+
- entries[0].fields.year: "2024"
|
|
65
|
+
|
|
66
|
+
Notes: leave format out unless auto picks the wrong one. If the text cannot be read, the tool returns an error asking for the format to be passed.
|
|
37
67
|
|
|
68
|
+
### convert_bibliography
|
|
69
|
+
Convert between BibTeX, RIS, CSL JSON and plain text. For plain text, pass style as apa (the default), mla or ieee to get a formatted reference list. Input format is detected unless passed. Page ranges become 12--15 in BibTeX. DOIs and names are cleaned on the way.
|
|
70
|
+
|
|
71
|
+
Inputs:
|
|
72
|
+
- text (required): the reference list
|
|
73
|
+
- to (required): bibtex, ris, csl_json, or text
|
|
74
|
+
- from (optional): auto, bibtex, ris, or csl_json
|
|
75
|
+
|
|
76
|
+
Example 1: BibTeX to RIS
|
|
77
|
+
Input text is BibTeX. to: "ris". Output contains RIS tags like TY, AU, TI, PY, DO and ER - .
|
|
78
|
+
|
|
79
|
+
Example 2: RIS to BibTeX
|
|
80
|
+
RIS entries convert to BibTeX. Page ranges like 100-112 become pages = {100--112}.
|
|
81
|
+
|
|
82
|
+
Example 3: CSL JSON to plain text
|
|
83
|
+
Produces lines like "Garcia, L., & World Health Organization. (2022). Remote learning outcomes."
|
|
84
|
+
|
|
85
|
+
Example 4: BibTeX to CSL JSON
|
|
86
|
+
Returns a JSON array of items.
|
|
87
|
+
|
|
88
|
+
If there are no entries, the tool returns an error that says so.
|
|
89
|
+
|
|
90
|
+
### dedupe_entries
|
|
91
|
+
Find duplicate references by matching DOI, by the same cite key, and by similar titles (years must not clash). For each group it says which entry to keep (the one with the most fields) and which to remove. Also returns the list as BibTeX with the extra copies dropped.
|
|
92
|
+
|
|
93
|
+
Inputs:
|
|
94
|
+
- text (required): the reference list
|
|
95
|
+
- format (optional): auto, bibtex, ris, or csl_json
|
|
96
|
+
- title_threshold (optional): 0.5 to 1, default 0.9
|
|
97
|
+
|
|
98
|
+
How matching works:
|
|
99
|
+
- DOI match: normalizes DOI (strips https://doi.org/) and matches case insensitively
|
|
100
|
+
- key match: matches cite keys ignoring case
|
|
101
|
+
- title match: lowercases, drops punctuation and accents, compares pairs of letters. Two entries with different years are not matched by title
|
|
102
|
+
|
|
103
|
+
Example: two entries with the same DOI, one with 3 fields and one with 6. The group keeps the entry with 6 fields and lists the other under remove. The deduplicated_bibtex omits the removed copy.
|
|
104
|
+
|
|
105
|
+
If loose titles should match, lower title_threshold (e.g. 0.8). If there are no duplicates, the tool reports no duplicates found and returns the full list.
|
|
106
|
+
|
|
107
|
+
### lint_entries
|
|
108
|
+
Check a reference list for problems. Each issue gives key, rule, severity, field (if any), and message.
|
|
109
|
+
|
|
110
|
+
Inputs:
|
|
111
|
+
- text (required): the reference list
|
|
112
|
+
- format (optional): auto, bibtex, ris, or csl_json
|
|
113
|
+
|
|
114
|
+
Rules:
|
|
115
|
+
- missing-field: required fields missing for the entry type. Checked types: article, book, inproceedings, incollection, phdthesis, mastersthesis, techreport, unpublished, misc, online. Other types need a title. For example @article needs journal; @book needs author.
|
|
116
|
+
- bad-pages: page ranges that run backwards, use one hyphen, or are not numbers. Use double hyphen for ranges.
|
|
117
|
+
- uncapitalized-title: titles that start with a lower case letter (protected capitals in {DNA} are allowed)
|
|
118
|
+
- invalid-doi: DOI does not look like 10.xxxx/something (DOIs are not looked up online)
|
|
119
|
+
- bad-year: year is not a normal year value
|
|
120
|
+
- duplicate-key: the same cite key appears more than once
|
|
121
|
+
|
|
122
|
+
Example issues:
|
|
123
|
+
- bad1: missing-field on journal (error)
|
|
124
|
+
- bad1: uncapitalized-title (warning)
|
|
125
|
+
- bad1: invalid-doi (warning)
|
|
126
|
+
- bad1: bad-year (warning)
|
|
127
|
+
- bad2: bad-pages because single hyphen (warning)
|
|
128
|
+
- bad3: missing-field on author and bad-pages (errors/warnings)
|
|
129
|
+
- dup: duplicate-key (error)
|
|
130
|
+
|
|
131
|
+
Pass the whole list so repeated keys can be found. A clean list returns no problems found.
|
|
132
|
+
|
|
133
|
+
### fix_entries
|
|
134
|
+
Apply the safe fixes that lint_entries points to and return the cleaned list as BibTeX, plus a list of each change (key, field, from, to).
|
|
135
|
+
|
|
136
|
+
Inputs:
|
|
137
|
+
- text (required): the reference list
|
|
138
|
+
- format (optional): auto, bibtex, ris, or csl_json
|
|
139
|
+
|
|
140
|
+
Fixes:
|
|
141
|
+
- pages: a single hyphen range like 12-15 becomes 12--15
|
|
142
|
+
- title: a title that starts with a lower case letter gets a capital; a title with every word in lower case gets capitals on the main words. Text inside braces is left alone
|
|
143
|
+
- doi: a link or doi: prefix is removed, leaving the bare DOI
|
|
144
|
+
|
|
145
|
+
Not fixed: missing fields, backwards page ranges, bad years, repeated keys and DOIs that are not valid. Run lint_entries on the result to see what is left. The list is returned as text, no file is changed.
|
|
146
|
+
|
|
147
|
+
### generate_cite_keys
|
|
148
|
+
Create consistent cite keys from the first author's family name, the year, and the first meaningful word of the title. Repeated keys get a letter added.
|
|
149
|
+
|
|
150
|
+
Inputs:
|
|
151
|
+
- text (required): the reference list
|
|
152
|
+
- format (optional): auto, bibtex, ris, or csl_json
|
|
153
|
+
- include_title_word (optional): add the first meaningful title word (default true)
|
|
154
|
+
|
|
155
|
+
Behavior:
|
|
156
|
+
- Uses first author's family name, normalized (accents removed)
|
|
157
|
+
- Uses year if present
|
|
158
|
+
- If include_title_word is true, uses first meaningful title word
|
|
159
|
+
- If no author, uses anon plus year and title word
|
|
160
|
+
- Repeats get a letter: smith2024, smith2024a, smith2024b
|
|
161
|
+
- Returns old and new keys for each entry and the list as BibTeX with new keys
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
- Smith, John, 2024, "Deep learning for sequencing" -> smith2024deep
|
|
165
|
+
- Same author/year/title start -> smith2024deepa
|
|
166
|
+
- Muller with umlaut -> muller2019art
|
|
167
|
+
- No author -> keys based on anon and title
|
|
168
|
+
|
|
169
|
+
## Workflows
|
|
170
|
+
|
|
171
|
+
### Clean up a Zotero or Mendeley export
|
|
172
|
+
1. Export from Zotero or Mendeley as BibTeX or RIS
|
|
173
|
+
2. Pass to lint_entries to find problems
|
|
174
|
+
3. Pass to dedupe_entries to remove duplicates
|
|
175
|
+
4. Pass to generate_cite_keys to make consistent keys
|
|
176
|
+
5. Use convert_bibliography if the target format differs
|
|
177
|
+
|
|
178
|
+
Example sequence with text from an export:
|
|
179
|
+
- lint_entries on the raw list shows missing fields and bad page ranges
|
|
180
|
+
- dedupe_entries returns deduplicated_bibtex with duplicates removed
|
|
181
|
+
- generate_cite_keys on the cleaned list returns bibtex with new keys
|
|
182
|
+
|
|
183
|
+
### Convert between formats
|
|
184
|
+
1. parse_bibliography to inspect normalized entries
|
|
185
|
+
2. convert_bibliography with to set to target format (bibtex, ris, csl_json, or text)
|
|
186
|
+
3. Page ranges become 12--15 in BibTeX. Protected capitals in braces are kept in BibTeX output
|
|
187
|
+
|
|
188
|
+
### Check before submitting
|
|
189
|
+
1. Run lint_entries to catch missing required fields, bad pages, bad DOIs, bad years, repeated keys
|
|
190
|
+
2. Fix issues in the source or regenerate
|
|
191
|
+
3. Run dedupe_entries to ensure no duplicates remain
|
|
192
|
+
4. Optionally rename keys with generate_cite_keys
|
|
193
|
+
|
|
194
|
+
## Example prompts
|
|
38
195
|
- "Lint this .bib file and list what to fix."
|
|
39
196
|
- "Remove the duplicate references and give me the clean BibTeX."
|
|
40
197
|
- "Convert this RIS export to BibTeX and rename the keys."
|
|
41
198
|
- "Turn this CSL JSON into a plain text reference list."
|
|
199
|
+
- "Parse this list and show me normalized authors and DOIs."
|
|
200
|
+
- "Deduplicate this mixed list using a looser title match."
|
|
42
201
|
|
|
43
202
|
## FAQ
|
|
44
203
|
|
|
@@ -50,6 +209,10 @@ What happens with an empty list or text it cannot place? The tool returns an err
|
|
|
50
209
|
|
|
51
210
|
Which entry types are checked for required fields? article, book, inproceedings, incollection, phdthesis, mastersthesis, techreport, unpublished, misc and online. Other types need a title.
|
|
52
211
|
|
|
212
|
+
Will it change my protected capitals like {DNA}? No. Braces are kept in BibTeX output and only removed when you convert to RIS, CSL JSON or plain text.
|
|
213
|
+
|
|
214
|
+
How does it decide two titles are the same? It lowercases the titles, drops punctuation and accents, and compares pairs of letters. The default match is 0.9 out of 1. Pass title_threshold to loosen or tighten that. Two entries with different years are not matched by title.
|
|
215
|
+
|
|
53
216
|
## Links
|
|
54
217
|
|
|
55
218
|
- Source: https://github.com/AnonymousOnyx22/bibtex-reference-manager-mcp
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bibtex-reference-manager-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "BibTeX reference manager MCP server for
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "BibTeX reference manager MCP server for students writing LaTeX theses and papers. Ask your AI app to fix a broken .bib file, clean a Zotero export or Mendeley export, convert EndNote RIS to BibTeX, remove duplicates, check DOIs and make consistent cite keys for your thesis bibliography. Also reads CSL JSON. Works offline with no account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -35,11 +35,17 @@
|
|
|
35
35
|
"bibtex cleaner",
|
|
36
36
|
"bibtex converter",
|
|
37
37
|
"bib file",
|
|
38
|
+
"fix bib file",
|
|
39
|
+
"latex bibliography",
|
|
40
|
+
"thesis bibliography",
|
|
41
|
+
"zotero export",
|
|
42
|
+
"mendeley export",
|
|
43
|
+
"endnote ris",
|
|
44
|
+
"ris to bibtex",
|
|
38
45
|
"ris",
|
|
39
46
|
"ris converter",
|
|
40
47
|
"csl json",
|
|
41
48
|
"citation",
|
|
42
|
-
"citations",
|
|
43
49
|
"references",
|
|
44
50
|
"bibliography",
|
|
45
51
|
"reference manager",
|
|
@@ -61,11 +67,19 @@
|
|
|
61
67
|
"model-context-protocol",
|
|
62
68
|
"bibtex-reference-manager-mcp",
|
|
63
69
|
"student",
|
|
64
|
-
"students",
|
|
65
70
|
"university",
|
|
66
|
-
"education",
|
|
67
71
|
"claude",
|
|
68
|
-
"cursor"
|
|
72
|
+
"cursor",
|
|
73
|
+
"bibtex reference manager mcp",
|
|
74
|
+
"master thesis bibtex",
|
|
75
|
+
"phd thesis bibliography",
|
|
76
|
+
"thesis citations",
|
|
77
|
+
"references for thesis",
|
|
78
|
+
"literature review bibtex",
|
|
79
|
+
"research paper citations",
|
|
80
|
+
"cite references latex",
|
|
81
|
+
"bibliography for thesis",
|
|
82
|
+
"academic references"
|
|
69
83
|
],
|
|
70
84
|
"author": "AnonymousOnyx22",
|
|
71
85
|
"license": "MIT",
|