@xevos117/mcp-zotero 1.0.3 → 1.0.5
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/LICENSE +24 -24
- package/README.md +127 -127
- package/build/server.js +1 -1
- package/build/tools/add-items-by-doi.js +9 -4
- package/build/tools/add-items.js +174 -0
- package/build/tools/index.js +3 -3
- package/build/tools/inject-citations.js +25 -25
- package/build/tools/search-library.js +10 -10
- package/build/utils/csl-to-zotero.js +69 -8
- package/build/utils/zotero-item-types.js +280 -0
- package/package.json +58 -58
- package/build/tools/add-web-item.js +0 -97
package/LICENSE
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Xevos117
|
|
4
|
-
|
|
5
|
-
Based on mcp-zotero by Abhishek Kalia (https://github.com/kaliaboi/mcp-zotero),
|
|
6
|
-
Copyright (c) 2024 Abhishek Kalia, licensed under the MIT License.
|
|
7
|
-
|
|
8
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
-
in the Software without restriction, including without limitation the rights
|
|
11
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
-
furnished to do so, subject to the following conditions:
|
|
14
|
-
|
|
15
|
-
The above copyright notice and this permission notice shall be included in all
|
|
16
|
-
copies or substantial portions of the Software.
|
|
17
|
-
|
|
18
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Xevos117
|
|
4
|
+
|
|
5
|
+
Based on mcp-zotero by Abhishek Kalia (https://github.com/kaliaboi/mcp-zotero),
|
|
6
|
+
Copyright (c) 2024 Abhishek Kalia, licensed under the MIT License.
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
# MCP Zotero
|
|
2
|
-
|
|
3
|
-
> **Note:** This is an unofficial community project and is not affiliated with, endorsed by, or supported by the Zotero team or the Corporation for Digital Scholarship. "Zotero" is a registered trademark of the Corporation for Digital Scholarship.
|
|
4
|
-
|
|
5
|
-
A Model Context Protocol server for Zotero integration. It gives any LLM full access to your Zotero library: search, organize, add papers by DOI, import PDFs, read full-text content, and inject live citations into Word documents.
|
|
6
|
-
|
|
7
|
-
> Originally based on [mcp-zotero](https://github.com/kaliaboi/mcp-zotero) by Abhishek Kalia.
|
|
8
|
-
> This project has since been extensively rewritten with a new architecture, 13 tools (up from 5), citation injection, PDF management, and Claude skill support.
|
|
9
|
-
|
|
10
|
-
## How it works
|
|
11
|
-
|
|
12
|
-
The server is designed to be **usable by any LLM without external documentation**. On connection, it sends workflow instructions via the MCP `instructions` field, and each tool description includes cross-references and usage guidance. An LLM that has never seen this server before can discover the full workflow — from adding papers to producing a cited Word document — directly from the tool listing.
|
|
13
|
-
|
|
14
|
-
For advanced use cases (PDF upload policy, citation style guidance, source transparency), a **Claude skill** is included for Claude.ai Projects. But the skill is optional: the MCP server is fully self-documenting.
|
|
15
|
-
|
|
16
|
-
## Local vs Remote LLMs
|
|
17
|
-
|
|
18
|
-
| Scenario | MCP server | Skill needed? |
|
|
19
|
-
|---|---|---|
|
|
20
|
-
| Local LLM (Claude Code, LM Studio, etc.) | All 13 tools | No |
|
|
21
|
-
| Remote/sandboxed LLM (Claude.ai Projects) | API tools (search, add, metadata) | Yes, for citation injection |
|
|
22
|
-
|
|
23
|
-
Local LLMs with filesystem access can use all tools directly, including `inject_citations` which reads and writes `.docx` files on disk.
|
|
24
|
-
|
|
25
|
-
Remote LLMs without filesystem access can use the included **Claude skill** (`skills/zotero-skill-mcp-integrations/`), which runs citation injection entirely inside the sandbox. MCP tools handle all Zotero API operations; the skill handles document assembly.
|
|
26
|
-
|
|
27
|
-
## Setup
|
|
28
|
-
|
|
29
|
-
1. Get your Zotero credentials:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
# Create an API key at https://www.zotero.org/settings/keys
|
|
33
|
-
# (enable library read/write + file access)
|
|
34
|
-
# Then retrieve your user ID:
|
|
35
|
-
curl -H "Zotero-API-Key: YOUR_API_KEY" https://api.zotero.org/keys/current
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
2. Set environment variables:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
export ZOTERO_API_KEY="your-api-key"
|
|
42
|
-
export ZOTERO_USER_ID="user-id-from-curl"
|
|
43
|
-
export UNPAYWALL_EMAIL="your@email.edu" # Optional: enables OA PDF lookup via Unpaywall
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Environment Variables
|
|
47
|
-
|
|
48
|
-
| Variable | Required | Description |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| `ZOTERO_API_KEY` | Yes | API key for Zotero Web API v3. Create one at [zotero.org/settings/keys](https://www.zotero.org/settings/keys) with library read/write and file access permissions. |
|
|
51
|
-
| `ZOTERO_USER_ID` | Yes | Your Zotero numeric user ID. Retrieve it with `curl -H "Zotero-API-Key: KEY" https://api.zotero.org/keys/current`. |
|
|
52
|
-
| `UNPAYWALL_EMAIL` | No | Email for Unpaywall API requests ([rate-limit policy](https://unpaywall.org/products/api)). Enables OA PDF lookup in `add_items_by_doi` and `find_and_attach_pdfs`. If not set, OA PDF features are silently skipped. |
|
|
53
|
-
|
|
54
|
-
## Integration with Claude Desktop
|
|
55
|
-
|
|
56
|
-
Add to your Claude Desktop configuration:
|
|
57
|
-
|
|
58
|
-
```json
|
|
59
|
-
{
|
|
60
|
-
"mcpServers": {
|
|
61
|
-
"zotero": {
|
|
62
|
-
"command": "npx",
|
|
63
|
-
"args": ["-y", "@xevos117/mcp-zotero"],
|
|
64
|
-
"env": {
|
|
65
|
-
"ZOTERO_API_KEY": "YOUR_API_KEY",
|
|
66
|
-
"ZOTERO_USER_ID": "YOUR_USER_ID",
|
|
67
|
-
"UNPAYWALL_EMAIL": "YOUR_EMAIL"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Integration with Claude Code
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
claude mcp add-json "zotero" '{"command":"npx","args":["tsx","src/server.ts"],"env":{"ZOTERO_API_KEY":"...","ZOTERO_USER_ID":"..."}}'
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Available Tools
|
|
81
|
-
|
|
82
|
-
### Library browsing
|
|
83
|
-
|
|
84
|
-
| Tool | Description |
|
|
85
|
-
|---|---|
|
|
86
|
-
| `get_collections` | List all collections (folders) with keys, names, and parent relationships |
|
|
87
|
-
| `get_collection_items` | Get items in a specific collection with keys, titles, authors, dates |
|
|
88
|
-
| `search_library` | Search by query, or list items sorted by field (date, title, etc.) |
|
|
89
|
-
| `get_items_details` | Batch metadata retrieval for multiple items in a single call |
|
|
90
|
-
| `get_item_fulltext` | Get full-text content of a PDF attachment via Zotero's fulltext index |
|
|
91
|
-
|
|
92
|
-
### Adding content
|
|
93
|
-
|
|
94
|
-
| Tool | Description |
|
|
95
|
-
|---|---|
|
|
96
|
-
| `add_items_by_doi` | Add papers by DOI with automatic metadata resolution. Auto-attaches OA PDFs via Unpaywall |
|
|
97
|
-
| `
|
|
98
|
-
| `create_collection` | Create a new collection, optionally nested under a parent |
|
|
99
|
-
| `import_pdf_to_zotero` | Download a PDF from URL, upload to Zotero storage, auto-index full text |
|
|
100
|
-
| `find_and_attach_pdfs` | Batch OA PDF lookup and auto-attach via Unpaywall (by item keys or collection) |
|
|
101
|
-
| `add_linked_url_attachment` | Attach a URL to an existing item or create a standalone link |
|
|
102
|
-
|
|
103
|
-
### Citation & documents
|
|
104
|
-
|
|
105
|
-
| Tool | Description |
|
|
106
|
-
|---|---|
|
|
107
|
-
| `inject_citations` | Inject live Zotero citations into a Word document. Supports APA, IEEE, Vancouver, Harvard, Chicago |
|
|
108
|
-
| `get_user_id` | Returns the configured Zotero user ID |
|
|
109
|
-
|
|
110
|
-
## Development
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
npm install
|
|
114
|
-
npm run build # Compile TypeScript
|
|
115
|
-
npm test # Run tests (vitest,
|
|
116
|
-
npx tsx src/server.ts # Run directly without building
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### Debug with MCP Inspector
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
npx @modelcontextprotocol/inspector npx tsx src/server.ts
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
## License
|
|
126
|
-
|
|
127
|
-
MIT - see [LICENSE](LICENSE) for details.
|
|
1
|
+
# MCP Zotero
|
|
2
|
+
|
|
3
|
+
> **Note:** This is an unofficial community project and is not affiliated with, endorsed by, or supported by the Zotero team or the Corporation for Digital Scholarship. "Zotero" is a registered trademark of the Corporation for Digital Scholarship.
|
|
4
|
+
|
|
5
|
+
A Model Context Protocol server for Zotero integration. It gives any LLM full access to your Zotero library: search, organize, add papers by DOI, import PDFs, read full-text content, and inject live citations into Word documents.
|
|
6
|
+
|
|
7
|
+
> Originally based on [mcp-zotero](https://github.com/kaliaboi/mcp-zotero) by Abhishek Kalia.
|
|
8
|
+
> This project has since been extensively rewritten with a new architecture, 13 tools (up from 5), citation injection, PDF management, and Claude skill support.
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
The server is designed to be **usable by any LLM without external documentation**. On connection, it sends workflow instructions via the MCP `instructions` field, and each tool description includes cross-references and usage guidance. An LLM that has never seen this server before can discover the full workflow — from adding papers to producing a cited Word document — directly from the tool listing.
|
|
13
|
+
|
|
14
|
+
For advanced use cases (PDF upload policy, citation style guidance, source transparency), a **Claude skill** is included for Claude.ai Projects. But the skill is optional: the MCP server is fully self-documenting.
|
|
15
|
+
|
|
16
|
+
## Local vs Remote LLMs
|
|
17
|
+
|
|
18
|
+
| Scenario | MCP server | Skill needed? |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Local LLM (Claude Code, LM Studio, etc.) | All 13 tools | No |
|
|
21
|
+
| Remote/sandboxed LLM (Claude.ai Projects) | API tools (search, add, metadata) | Yes, for citation injection |
|
|
22
|
+
|
|
23
|
+
Local LLMs with filesystem access can use all tools directly, including `inject_citations` which reads and writes `.docx` files on disk.
|
|
24
|
+
|
|
25
|
+
Remote LLMs without filesystem access can use the included **Claude skill** (`skills/zotero-skill-mcp-integrations/`), which runs citation injection entirely inside the sandbox. MCP tools handle all Zotero API operations; the skill handles document assembly.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
1. Get your Zotero credentials:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Create an API key at https://www.zotero.org/settings/keys
|
|
33
|
+
# (enable library read/write + file access)
|
|
34
|
+
# Then retrieve your user ID:
|
|
35
|
+
curl -H "Zotero-API-Key: YOUR_API_KEY" https://api.zotero.org/keys/current
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. Set environment variables:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export ZOTERO_API_KEY="your-api-key"
|
|
42
|
+
export ZOTERO_USER_ID="user-id-from-curl"
|
|
43
|
+
export UNPAYWALL_EMAIL="your@email.edu" # Optional: enables OA PDF lookup via Unpaywall
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Environment Variables
|
|
47
|
+
|
|
48
|
+
| Variable | Required | Description |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `ZOTERO_API_KEY` | Yes | API key for Zotero Web API v3. Create one at [zotero.org/settings/keys](https://www.zotero.org/settings/keys) with library read/write and file access permissions. |
|
|
51
|
+
| `ZOTERO_USER_ID` | Yes | Your Zotero numeric user ID. Retrieve it with `curl -H "Zotero-API-Key: KEY" https://api.zotero.org/keys/current`. |
|
|
52
|
+
| `UNPAYWALL_EMAIL` | No | Email for Unpaywall API requests ([rate-limit policy](https://unpaywall.org/products/api)). Enables OA PDF lookup in `add_items_by_doi` and `find_and_attach_pdfs`. If not set, OA PDF features are silently skipped. |
|
|
53
|
+
|
|
54
|
+
## Integration with Claude Desktop
|
|
55
|
+
|
|
56
|
+
Add to your Claude Desktop configuration:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"zotero": {
|
|
62
|
+
"command": "npx",
|
|
63
|
+
"args": ["-y", "@xevos117/mcp-zotero"],
|
|
64
|
+
"env": {
|
|
65
|
+
"ZOTERO_API_KEY": "YOUR_API_KEY",
|
|
66
|
+
"ZOTERO_USER_ID": "YOUR_USER_ID",
|
|
67
|
+
"UNPAYWALL_EMAIL": "YOUR_EMAIL"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Integration with Claude Code
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
claude mcp add-json "zotero" '{"command":"npx","args":["tsx","src/server.ts"],"env":{"ZOTERO_API_KEY":"...","ZOTERO_USER_ID":"..."}}'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Available Tools
|
|
81
|
+
|
|
82
|
+
### Library browsing
|
|
83
|
+
|
|
84
|
+
| Tool | Description |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `get_collections` | List all collections (folders) with keys, names, and parent relationships |
|
|
87
|
+
| `get_collection_items` | Get items in a specific collection with keys, titles, authors, dates |
|
|
88
|
+
| `search_library` | Search by query, or list items sorted by field (date, title, etc.) |
|
|
89
|
+
| `get_items_details` | Batch metadata retrieval for multiple items in a single call |
|
|
90
|
+
| `get_item_fulltext` | Get full-text content of a PDF attachment via Zotero's fulltext index |
|
|
91
|
+
|
|
92
|
+
### Adding content
|
|
93
|
+
|
|
94
|
+
| Tool | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `add_items_by_doi` | Add papers by DOI with automatic metadata resolution. Auto-attaches OA PDFs via Unpaywall |
|
|
97
|
+
| `add_items` | Add items with direct metadata — supports all 37 Zotero item types (books, theses, reports, etc.), batch-capable |
|
|
98
|
+
| `create_collection` | Create a new collection, optionally nested under a parent |
|
|
99
|
+
| `import_pdf_to_zotero` | Download a PDF from URL, upload to Zotero storage, auto-index full text |
|
|
100
|
+
| `find_and_attach_pdfs` | Batch OA PDF lookup and auto-attach via Unpaywall (by item keys or collection) |
|
|
101
|
+
| `add_linked_url_attachment` | Attach a URL to an existing item or create a standalone link |
|
|
102
|
+
|
|
103
|
+
### Citation & documents
|
|
104
|
+
|
|
105
|
+
| Tool | Description |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `inject_citations` | Inject live Zotero citations into a Word document. Supports APA, IEEE, Vancouver, Harvard, Chicago. Output is saved in the same folder as the input file with a `_cited` suffix (e.g. `paper.docx` → `paper_cited.docx`) |
|
|
108
|
+
| `get_user_id` | Returns the configured Zotero user ID |
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm install
|
|
114
|
+
npm run build # Compile TypeScript
|
|
115
|
+
npm test # Run tests (vitest, 328 tests)
|
|
116
|
+
npx tsx src/server.ts # Run directly without building
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Debug with MCP Inspector
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx @modelcontextprotocol/inspector npx tsx src/server.ts
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT - see [LICENSE](LICENSE) for details.
|
package/build/server.js
CHANGED
|
@@ -19,7 +19,7 @@ class ZoteroServer {
|
|
|
19
19
|
"Zotero library management and citation injection for Word documents.",
|
|
20
20
|
"",
|
|
21
21
|
"CITATION WORKFLOW:",
|
|
22
|
-
"1. Collect item keys: use add_items_by_doi (or search_library for existing items)",
|
|
22
|
+
"1. Collect item keys: use add_items_by_doi for items with DOIs, add_items for items without DOIs (or search_library for existing items)",
|
|
23
23
|
"2. Generate .docx: create a Word document with <zcite keys=\"ITEMKEY\"/> placeholders where citations should appear. Each <zcite> must be in its own TextRun.",
|
|
24
24
|
"3. Call inject_citations with the .docx file path — it fetches metadata from Zotero automatically.",
|
|
25
25
|
"4. Tell the user to open in Word with Zotero plugin → click Zotero → Refresh.",
|
|
@@ -9,10 +9,15 @@ import { downloadAndUploadPdf } from "../utils/pdf-uploader.js";
|
|
|
9
9
|
import { mapWithConcurrency } from "../utils/concurrency.js";
|
|
10
10
|
export const toolConfig = {
|
|
11
11
|
name: "add_items_by_doi",
|
|
12
|
-
description: `Add
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
12
|
+
description: `Add items to your Zotero library by resolving DOIs. Works with ANY item type that has a DOI — journal articles, books, datasets, preprints, conference papers, reports, etc. For each DOI, resolves metadata via content negotiation and creates the item in Zotero with the correct type automatically. Returns a list of successfully added items (with item_key and title) and any failures.
|
|
13
|
+
|
|
14
|
+
WHEN TO USE vs add_items:
|
|
15
|
+
- Use add_items_by_doi when the item HAS a DOI — it auto-resolves all metadata and attaches OA PDFs.
|
|
16
|
+
- Use add_items when the item does NOT have a DOI, or when you need to override specific metadata fields (add_items_by_doi does not allow metadata overrides).
|
|
17
|
+
- Mixed batch: if some items have DOIs and others don't, make two separate calls — add_items_by_doi for the DOIs and add_items for the rest.
|
|
18
|
+
|
|
19
|
+
WORKFLOW TIPS:
|
|
20
|
+
- To collect metadata for all added items, call get_items_details with the returned item_keys (single batch call).
|
|
16
21
|
- To create a cited Word document, use the returned item_keys as <zcite keys="ITEMKEY"/> placeholders in a .docx, then call inject_citations. See inject_citations description for the full workflow.`,
|
|
17
22
|
inputSchema: {
|
|
18
23
|
dois: z
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isZoteroApiError } from "../types/zotero-types.js";
|
|
3
|
+
import { formatErrorResponse } from "../utils/error-formatter.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { ZOTERO_ITEM_TYPES, ITEM_TYPE_FIELDS, ITEM_TYPE_CREATOR_TYPES, TITLE_FIELD_NAME, } from "../utils/zotero-item-types.js";
|
|
6
|
+
const CreatorSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
firstName: z.string().optional(),
|
|
9
|
+
lastName: z.string().optional(),
|
|
10
|
+
name: z.string().optional(),
|
|
11
|
+
creatorType: z.string().default("author"),
|
|
12
|
+
})
|
|
13
|
+
.refine((data) => data.name != null || (data.firstName != null && data.lastName != null), {
|
|
14
|
+
message: "Provide either 'name' (institutional) or both firstName+lastName",
|
|
15
|
+
});
|
|
16
|
+
const ItemSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
itemType: z.enum(ZOTERO_ITEM_TYPES),
|
|
19
|
+
title: z.string(),
|
|
20
|
+
creators: z.array(CreatorSchema).optional(),
|
|
21
|
+
})
|
|
22
|
+
.catchall(z.string())
|
|
23
|
+
.superRefine((data, ctx) => {
|
|
24
|
+
const itemType = data.itemType;
|
|
25
|
+
const validFields = ITEM_TYPE_FIELDS[itemType];
|
|
26
|
+
for (const key of Object.keys(data)) {
|
|
27
|
+
if (key === "itemType" || key === "title" || key === "creators")
|
|
28
|
+
continue;
|
|
29
|
+
if (!validFields.has(key)) {
|
|
30
|
+
ctx.addIssue({
|
|
31
|
+
code: z.ZodIssueCode.custom,
|
|
32
|
+
message: `Field "${key}" is not valid for itemType "${data.itemType}". Valid fields: ${[...validFields].join(", ")}`,
|
|
33
|
+
path: [key],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const validCreatorTypes = ITEM_TYPE_CREATOR_TYPES[itemType];
|
|
38
|
+
for (const [i, creator] of (data.creators ?? []).entries()) {
|
|
39
|
+
if (!validCreatorTypes.has(creator.creatorType)) {
|
|
40
|
+
ctx.addIssue({
|
|
41
|
+
code: z.ZodIssueCode.custom,
|
|
42
|
+
message: `Invalid creatorType "${creator.creatorType}" for "${data.itemType}". Valid: ${[...validCreatorTypes].join(", ")}`,
|
|
43
|
+
path: ["creators", i, "creatorType"],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
export const toolConfig = {
|
|
49
|
+
name: "add_items",
|
|
50
|
+
description: `Add items to Zotero by providing metadata directly. Supports ALL 37 Zotero item types.
|
|
51
|
+
|
|
52
|
+
WHEN TO USE:
|
|
53
|
+
- For items that do not have a DOI (books, theses, reports, etc.)
|
|
54
|
+
- When you need full control over metadata (e.g., override a title, set a specific itemType, add custom fields) — even if a DOI exists, use add_items when the auto-resolved metadata would be incorrect or incomplete
|
|
55
|
+
- Mixed batch: if some items have DOIs and others don't, call add_items_by_doi for the DOIs and add_items for the rest (two separate calls)
|
|
56
|
+
- Prefer add_items_by_doi when DOIs are available AND you don't need to override metadata (it auto-resolves everything and attaches OA PDFs)
|
|
57
|
+
|
|
58
|
+
BATCH: Pass multiple items in the 'items' array (single API call).
|
|
59
|
+
|
|
60
|
+
COMMON FIELDS (available for most types):
|
|
61
|
+
title, date, abstractNote, url, DOI, publisher, place, pages, volume, language, extra
|
|
62
|
+
|
|
63
|
+
ITEM TYPE QUICK REFERENCE:
|
|
64
|
+
- journalArticle: publicationTitle, volume, issue, pages, DOI, ISSN
|
|
65
|
+
- book: publisher, ISBN, edition, numPages, series, seriesNumber
|
|
66
|
+
- bookSection: bookTitle, publisher, pages, ISBN, edition
|
|
67
|
+
- conferencePaper: proceedingsTitle, conferenceName, publisher, DOI
|
|
68
|
+
- thesis: thesisType ("PhD thesis"|"Master's thesis"), university
|
|
69
|
+
- report: reportType, reportNumber, institution
|
|
70
|
+
- webpage: websiteTitle, websiteType, accessDate
|
|
71
|
+
- preprint: repository, archiveID, genre ("Preprint")
|
|
72
|
+
- patent: patentNumber, assignee, issuingAuthority, filingDate
|
|
73
|
+
- computerProgram: versionNumber, company, system, programmingLanguage
|
|
74
|
+
|
|
75
|
+
CREATORS: Array of {firstName, lastName, creatorType} or {name, creatorType} for institutional.
|
|
76
|
+
Default creatorType is "author". Some types use different primary types (e.g., "director" for film, "inventor" for patent, "artist" for artwork).
|
|
77
|
+
|
|
78
|
+
Invalid fields or creatorTypes for a given type are rejected with helpful error messages listing the valid options.`,
|
|
79
|
+
inputSchema: {
|
|
80
|
+
items: z
|
|
81
|
+
.array(ItemSchema)
|
|
82
|
+
.min(1)
|
|
83
|
+
.describe("Array of items to add. Each item must have itemType and title. Additional fields depend on the item type."),
|
|
84
|
+
collection_key: z
|
|
85
|
+
.string()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Zotero collection key to add all items to. Get this from create_collection or get_collections."),
|
|
88
|
+
tags: z
|
|
89
|
+
.array(z.string())
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Tags to apply to all items"),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const AddItemsSchema = z.object(toolConfig.inputSchema);
|
|
95
|
+
export async function handleAddItems(zoteroApi, userId, args) {
|
|
96
|
+
const { items, collection_key, tags } = AddItemsSchema.parse(args);
|
|
97
|
+
const payloads = items.map((item) => {
|
|
98
|
+
const { itemType, title, creators, ...fields } = item;
|
|
99
|
+
const titleFieldName = TITLE_FIELD_NAME[itemType] ?? "title";
|
|
100
|
+
const payload = {
|
|
101
|
+
itemType,
|
|
102
|
+
...fields,
|
|
103
|
+
[titleFieldName]: title,
|
|
104
|
+
creators: (creators ?? []).map((c) => {
|
|
105
|
+
if (c.name) {
|
|
106
|
+
return { name: c.name, creatorType: c.creatorType };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
firstName: c.firstName,
|
|
110
|
+
lastName: c.lastName,
|
|
111
|
+
creatorType: c.creatorType,
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
collections: collection_key ? [collection_key] : [],
|
|
115
|
+
tags: (tags ?? []).map((t) => ({ tag: t })),
|
|
116
|
+
};
|
|
117
|
+
if (fields.url && !fields.accessDate) {
|
|
118
|
+
payload.accessDate = new Date().toISOString().split("T")[0];
|
|
119
|
+
}
|
|
120
|
+
return payload;
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const response = await zoteroApi
|
|
124
|
+
.library("user", userId)
|
|
125
|
+
.items()
|
|
126
|
+
.post(payloads);
|
|
127
|
+
const errors = response.getErrors();
|
|
128
|
+
const errorIndices = new Set(Object.keys(errors));
|
|
129
|
+
const success = [];
|
|
130
|
+
const failed = [];
|
|
131
|
+
for (const [idx, msg] of Object.entries(errors)) {
|
|
132
|
+
const i = Number(idx);
|
|
133
|
+
failed.push({ index: i, title: items[i].title, error: msg });
|
|
134
|
+
}
|
|
135
|
+
const created = response.getData();
|
|
136
|
+
let successIdx = 0;
|
|
137
|
+
for (let i = 0; i < items.length; i++) {
|
|
138
|
+
if (!errorIndices.has(String(i))) {
|
|
139
|
+
const entity = created[successIdx++];
|
|
140
|
+
success.push({
|
|
141
|
+
index: i,
|
|
142
|
+
item_key: entity?.key ?? "unknown",
|
|
143
|
+
title: entity?.title ?? items[i].title,
|
|
144
|
+
item_type: items[i].itemType,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (success.length === 0 && failed.length > 0) {
|
|
149
|
+
return formatErrorResponse("All items failed to create", { failed });
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: JSON.stringify({
|
|
156
|
+
success,
|
|
157
|
+
...(failed.length > 0 ? { failed } : {}),
|
|
158
|
+
}, null, 2),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
if (isZoteroApiError(err)) {
|
|
165
|
+
logger.error("Tool execution failed", {
|
|
166
|
+
tool: "add_items",
|
|
167
|
+
status: err.response?.status,
|
|
168
|
+
errorMessage: err.message,
|
|
169
|
+
url: err.response?.url,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/build/tools/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { handleInjectCitations, toolConfig as injectCitationsConfig } from "./in
|
|
|
8
8
|
import { handleGetItemFulltext, toolConfig as getItemFulltextConfig } from "./get-item-fulltext.js";
|
|
9
9
|
import { handleGetUserId, toolConfig as getUserIdConfig } from "./get-user-id.js";
|
|
10
10
|
import { handleAddLinkedUrlAttachment, toolConfig as addLinkedUrlAttachmentConfig } from "./add-linked-url-attachment.js";
|
|
11
|
-
import {
|
|
11
|
+
import { handleAddItems, toolConfig as addItemsConfig } from "./add-items.js";
|
|
12
12
|
import { handleImportPdfToZotero, toolConfig as importPdfToZoteroConfig } from "./import-pdf-to-zotero.js";
|
|
13
13
|
import { handleFindAndAttachPdfs, toolConfig as findAndAttachPdfsConfig } from "./find-and-attach-pdfs.js";
|
|
14
14
|
const handlers = {
|
|
@@ -22,7 +22,7 @@ const handlers = {
|
|
|
22
22
|
get_item_fulltext: handleGetItemFulltext,
|
|
23
23
|
get_user_id: handleGetUserId,
|
|
24
24
|
add_linked_url_attachment: handleAddLinkedUrlAttachment,
|
|
25
|
-
|
|
25
|
+
add_items: handleAddItems,
|
|
26
26
|
import_pdf_to_zotero: handleImportPdfToZotero,
|
|
27
27
|
find_and_attach_pdfs: handleFindAndAttachPdfs,
|
|
28
28
|
};
|
|
@@ -44,7 +44,7 @@ const toolConfigs = [
|
|
|
44
44
|
{ config: getItemFulltextConfig, handler: handleGetItemFulltext },
|
|
45
45
|
{ config: getUserIdConfig, handler: handleGetUserId },
|
|
46
46
|
{ config: addLinkedUrlAttachmentConfig, handler: handleAddLinkedUrlAttachment },
|
|
47
|
-
{ config:
|
|
47
|
+
{ config: addItemsConfig, handler: handleAddItems },
|
|
48
48
|
{ config: importPdfToZoteroConfig, handler: handleImportPdfToZotero },
|
|
49
49
|
{ config: findAndAttachPdfsConfig, handler: handleFindAndAttachPdfs },
|
|
50
50
|
];
|
|
@@ -5,31 +5,31 @@ import { injectCitations } from "../citation-injector/injector.js";
|
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
6
|
export const toolConfig = {
|
|
7
7
|
name: "inject_citations",
|
|
8
|
-
description: `Replace <zcite> placeholder tags in a .docx file with native Zotero field codes that Zotero for Word can recognize and manage. The tool fetches item metadata from Zotero automatically — you only need to provide the .docx file.
|
|
9
|
-
|
|
10
|
-
WORKFLOW — how to create a Word document with live Zotero citations:
|
|
11
|
-
1. Collect item keys: use add_items_by_doi (or search_library for existing items)
|
|
12
|
-
2. Generate .docx: create a Word document (e.g. with the "docx" npm package) with <zcite keys="ITEMKEY"/> placeholders where citations should appear. Each <zcite> MUST be in its own dedicated TextRun — do NOT mix it with surrounding text.
|
|
13
|
-
3. Call this tool with the .docx file path. It replaces every zcite tag with a Zotero field code and appends a bibliography.
|
|
14
|
-
4. Tell the user to open the file in Word with the Zotero plugin and click Zotero → Refresh.
|
|
15
|
-
|
|
16
|
-
CITATION STYLES — ask the user which style they want before generating:
|
|
17
|
-
- apa (default): author-year — (Smith, 2023)
|
|
18
|
-
- ieee / vancouver: numbered — [1], [2]
|
|
19
|
-
WARNING: for numbered styles every <zcite> MUST include a num="N" attribute with the sequential citation number. Without num, citations render as [?].
|
|
20
|
-
|
|
21
|
-
ZCITE TAG FORMAT:
|
|
22
|
-
<zcite keys="ITEMKEY"/>
|
|
23
|
-
Supported attributes (any order):
|
|
24
|
-
- keys (required): item key or comma-separated keys — "ABC12345" or "ABC12345,DEF67890"
|
|
25
|
-
- num: citation number for IEEE/Vancouver — "1" or "1,2" (required for numbered styles)
|
|
26
|
-
- locator: page reference — "pp. 12-15"
|
|
27
|
-
- prefix: text before citation — "see "
|
|
28
|
-
- suffix: text after citation — ", emphasis added"
|
|
29
|
-
|
|
30
|
-
OUTPUT:
|
|
31
|
-
A new .docx file (original filename with _cited suffix) with Zotero field codes and a ZOTERO_BIBL bibliography at the end.
|
|
32
|
-
|
|
8
|
+
description: `Replace <zcite> placeholder tags in a .docx file with native Zotero field codes that Zotero for Word can recognize and manage. The tool fetches item metadata from Zotero automatically — you only need to provide the .docx file.
|
|
9
|
+
|
|
10
|
+
WORKFLOW — how to create a Word document with live Zotero citations:
|
|
11
|
+
1. Collect item keys: use add_items_by_doi (or search_library for existing items)
|
|
12
|
+
2. Generate .docx: create a Word document (e.g. with the "docx" npm package) with <zcite keys="ITEMKEY"/> placeholders where citations should appear. Each <zcite> MUST be in its own dedicated TextRun — do NOT mix it with surrounding text.
|
|
13
|
+
3. Call this tool with the .docx file path. It replaces every zcite tag with a Zotero field code and appends a bibliography.
|
|
14
|
+
4. Tell the user to open the file in Word with the Zotero plugin and click Zotero → Refresh.
|
|
15
|
+
|
|
16
|
+
CITATION STYLES — ask the user which style they want before generating:
|
|
17
|
+
- apa (default): author-year — (Smith, 2023)
|
|
18
|
+
- ieee / vancouver: numbered — [1], [2]
|
|
19
|
+
WARNING: for numbered styles every <zcite> MUST include a num="N" attribute with the sequential citation number. Without num, citations render as [?].
|
|
20
|
+
|
|
21
|
+
ZCITE TAG FORMAT:
|
|
22
|
+
<zcite keys="ITEMKEY"/>
|
|
23
|
+
Supported attributes (any order):
|
|
24
|
+
- keys (required): item key or comma-separated keys — "ABC12345" or "ABC12345,DEF67890"
|
|
25
|
+
- num: citation number for IEEE/Vancouver — "1" or "1,2" (required for numbered styles)
|
|
26
|
+
- locator: page reference — "pp. 12-15"
|
|
27
|
+
- prefix: text before citation — "see "
|
|
28
|
+
- suffix: text after citation — ", emphasis added"
|
|
29
|
+
|
|
30
|
+
OUTPUT:
|
|
31
|
+
A new .docx file (original filename with _cited suffix) with Zotero field codes and a ZOTERO_BIBL bibliography at the end.
|
|
32
|
+
|
|
33
33
|
NOTE: If the inject-citations skill is available, prefer the skill workflow (runs in sandbox, no filesystem dependency). This tool serves as the primary path when the skill is not available.`,
|
|
34
34
|
inputSchema: {
|
|
35
35
|
file_path: z
|
|
@@ -5,16 +5,16 @@ import { formatCreators } from "../utils/item-formatter.js";
|
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
6
|
export const toolConfig = {
|
|
7
7
|
name: "search_library",
|
|
8
|
-
description: `Search your Zotero library or list items sorted by a field.
|
|
9
|
-
|
|
10
|
-
When 'query' is provided, searches by title, author, or any field.
|
|
11
|
-
When 'query' is omitted, lists items sorted by the chosen field (default: dateAdded, descending) — this replaces the old get_recent tool.
|
|
12
|
-
|
|
13
|
-
Examples:
|
|
14
|
-
- Search: { "query": "deep learning" }
|
|
15
|
-
- Recent items: { "sort": "dateAdded", "limit": 10 }
|
|
16
|
-
- Recent with search: { "query": "transformers", "sort": "dateAdded", "limit": 5 }
|
|
17
|
-
|
|
8
|
+
description: `Search your Zotero library or list items sorted by a field.
|
|
9
|
+
|
|
10
|
+
When 'query' is provided, searches by title, author, or any field.
|
|
11
|
+
When 'query' is omitted, lists items sorted by the chosen field (default: dateAdded, descending) — this replaces the old get_recent tool.
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
- Search: { "query": "deep learning" }
|
|
15
|
+
- Recent items: { "sort": "dateAdded", "limit": 10 }
|
|
16
|
+
- Recent with search: { "query": "transformers", "sort": "dateAdded", "limit": 5 }
|
|
17
|
+
|
|
18
18
|
Use the returned item keys with get_items_details, get_item_fulltext, or inject_citations.`,
|
|
19
19
|
inputSchema: {
|
|
20
20
|
query: z
|
|
@@ -1,23 +1,78 @@
|
|
|
1
1
|
const CSL_TO_ZOTERO_TYPE = {
|
|
2
|
+
"article": "journalArticle",
|
|
2
3
|
"article-journal": "journalArticle",
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
"article-magazine": "magazineArticle",
|
|
5
|
+
"article-newspaper": "newspaperArticle",
|
|
6
|
+
"bill": "bill",
|
|
7
|
+
"book": "book",
|
|
8
|
+
"broadcast": "tvBroadcast",
|
|
9
|
+
"chapter": "bookSection",
|
|
10
|
+
"dataset": "dataset",
|
|
11
|
+
"document": "document",
|
|
12
|
+
"entry-dictionary": "dictionaryEntry",
|
|
13
|
+
"entry-encyclopedia": "encyclopediaArticle",
|
|
14
|
+
"figure": "artwork",
|
|
15
|
+
"graphic": "artwork",
|
|
16
|
+
"hearing": "hearing",
|
|
17
|
+
"interview": "interview",
|
|
18
|
+
"legal_case": "case",
|
|
19
|
+
"legislation": "statute",
|
|
20
|
+
"manuscript": "manuscript",
|
|
21
|
+
"map": "map",
|
|
22
|
+
"motion_picture": "film",
|
|
5
23
|
"paper-conference": "conferencePaper",
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
24
|
+
"patent": "patent",
|
|
25
|
+
"personal_communication": "letter",
|
|
26
|
+
"post": "forumPost",
|
|
27
|
+
"post-weblog": "blogPost",
|
|
28
|
+
"report": "report",
|
|
29
|
+
"review": "journalArticle",
|
|
30
|
+
"review-book": "journalArticle",
|
|
31
|
+
"software": "computerProgram",
|
|
32
|
+
"song": "audioRecording",
|
|
33
|
+
"speech": "presentation",
|
|
34
|
+
"standard": "standard",
|
|
35
|
+
"thesis": "thesis",
|
|
36
|
+
"webpage": "webpage",
|
|
11
37
|
};
|
|
12
38
|
const ZOTERO_TO_CSL_TYPE = {
|
|
13
39
|
journalArticle: "article-journal",
|
|
40
|
+
magazineArticle: "article-magazine",
|
|
41
|
+
newspaperArticle: "article-newspaper",
|
|
42
|
+
bill: "bill",
|
|
14
43
|
book: "book",
|
|
44
|
+
tvBroadcast: "broadcast",
|
|
15
45
|
bookSection: "chapter",
|
|
46
|
+
dataset: "dataset",
|
|
47
|
+
document: "document",
|
|
48
|
+
dictionaryEntry: "entry-dictionary",
|
|
49
|
+
encyclopediaArticle: "entry-encyclopedia",
|
|
50
|
+
artwork: "graphic",
|
|
51
|
+
hearing: "hearing",
|
|
52
|
+
interview: "interview",
|
|
53
|
+
case: "legal_case",
|
|
54
|
+
statute: "legislation",
|
|
55
|
+
manuscript: "manuscript",
|
|
56
|
+
map: "map",
|
|
57
|
+
film: "motion_picture",
|
|
16
58
|
conferencePaper: "paper-conference",
|
|
59
|
+
patent: "patent",
|
|
60
|
+
letter: "personal_communication",
|
|
61
|
+
forumPost: "post",
|
|
62
|
+
blogPost: "post-weblog",
|
|
17
63
|
report: "report",
|
|
64
|
+
computerProgram: "software",
|
|
65
|
+
audioRecording: "song",
|
|
66
|
+
presentation: "speech",
|
|
67
|
+
standard: "standard",
|
|
18
68
|
thesis: "thesis",
|
|
19
69
|
webpage: "webpage",
|
|
20
|
-
|
|
70
|
+
radioBroadcast: "broadcast",
|
|
71
|
+
videoRecording: "motion_picture",
|
|
72
|
+
preprint: "article",
|
|
73
|
+
podcast: "song",
|
|
74
|
+
email: "personal_communication",
|
|
75
|
+
instantMessage: "personal_communication",
|
|
21
76
|
};
|
|
22
77
|
export function cslToZoteroItem(csl, options) {
|
|
23
78
|
const creators = (csl.author ?? []).map((a) => ({
|
|
@@ -46,6 +101,12 @@ export function cslToZoteroItem(csl, options) {
|
|
|
46
101
|
place: csl["publisher-place"] ?? "",
|
|
47
102
|
url: csl.URL ?? "",
|
|
48
103
|
abstractNote: csl.abstract ?? "",
|
|
104
|
+
ISBN: csl.ISBN ?? "",
|
|
105
|
+
ISSN: csl.ISSN ?? "",
|
|
106
|
+
edition: csl.edition ?? "",
|
|
107
|
+
numPages: csl["number-of-pages"] ?? "",
|
|
108
|
+
series: csl["collection-title"] ?? "",
|
|
109
|
+
language: csl.language ?? "",
|
|
49
110
|
collections,
|
|
50
111
|
tags,
|
|
51
112
|
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static registry of all 37 regular Zotero item types with their valid fields
|
|
3
|
+
* and creator types. Generated from the Zotero API (/itemTypes, /itemTypeFields,
|
|
4
|
+
* /itemTypeCreatorTypes).
|
|
5
|
+
*
|
|
6
|
+
* Used for data-driven validation in the add_items tool — a single superRefine
|
|
7
|
+
* handles all types via O(1) Set lookups instead of per-type switch-cases.
|
|
8
|
+
*/
|
|
9
|
+
export const ZOTERO_ITEM_TYPES = [
|
|
10
|
+
"artwork", "audioRecording", "bill", "blogPost", "book", "bookSection",
|
|
11
|
+
"case", "computerProgram", "conferencePaper", "dataset", "dictionaryEntry",
|
|
12
|
+
"document", "email", "encyclopediaArticle", "film", "forumPost", "hearing",
|
|
13
|
+
"instantMessage", "interview", "journalArticle", "letter", "magazineArticle",
|
|
14
|
+
"manuscript", "map", "newspaperArticle", "patent", "podcast", "preprint",
|
|
15
|
+
"presentation", "radioBroadcast", "report", "standard", "statute",
|
|
16
|
+
"thesis", "tvBroadcast", "videoRecording", "webpage",
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Item types where the "title" field has a different API name.
|
|
20
|
+
* The add_items tool always accepts `title` as a universal required field,
|
|
21
|
+
* then maps it to the correct API field name for these types.
|
|
22
|
+
*/
|
|
23
|
+
export const TITLE_FIELD_NAME = {
|
|
24
|
+
case: "caseName",
|
|
25
|
+
email: "subject",
|
|
26
|
+
statute: "nameOfAct",
|
|
27
|
+
};
|
|
28
|
+
/** Per-type valid fields — ReadonlySet for O(1) lookup. */
|
|
29
|
+
export const ITEM_TYPE_FIELDS = {
|
|
30
|
+
artwork: new Set([
|
|
31
|
+
"title", "abstractNote", "artworkMedium", "artworkSize", "date", "eventPlace",
|
|
32
|
+
"DOI", "citationKey", "url", "accessDate", "archive", "archiveLocation",
|
|
33
|
+
"shortTitle", "language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
34
|
+
]),
|
|
35
|
+
audioRecording: new Set([
|
|
36
|
+
"title", "abstractNote", "audioRecordingFormat", "seriesTitle", "volume",
|
|
37
|
+
"numberOfVolumes", "label", "place", "date", "runningTime", "ISBN", "DOI",
|
|
38
|
+
"citationKey", "url", "accessDate", "archive", "archiveLocation", "shortTitle",
|
|
39
|
+
"language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
40
|
+
]),
|
|
41
|
+
bill: new Set([
|
|
42
|
+
"title", "abstractNote", "billNumber", "code", "codeVolume", "section",
|
|
43
|
+
"codePages", "legislativeBody", "session", "history", "date", "DOI",
|
|
44
|
+
"citationKey", "url", "accessDate", "shortTitle", "language", "rights", "extra",
|
|
45
|
+
]),
|
|
46
|
+
blogPost: new Set([
|
|
47
|
+
"title", "abstractNote", "blogTitle", "websiteType", "date", "DOI",
|
|
48
|
+
"citationKey", "url", "accessDate", "ISSN", "shortTitle", "language",
|
|
49
|
+
"rights", "extra",
|
|
50
|
+
]),
|
|
51
|
+
book: new Set([
|
|
52
|
+
"title", "abstractNote", "series", "seriesNumber", "volume", "numberOfVolumes",
|
|
53
|
+
"edition", "date", "publisher", "place", "originalDate", "originalPublisher",
|
|
54
|
+
"originalPlace", "format", "numPages", "ISBN", "DOI", "citationKey", "url",
|
|
55
|
+
"accessDate", "ISSN", "archive", "archiveLocation", "shortTitle", "language",
|
|
56
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
57
|
+
]),
|
|
58
|
+
bookSection: new Set([
|
|
59
|
+
"title", "abstractNote", "bookTitle", "series", "seriesNumber", "volume",
|
|
60
|
+
"numberOfVolumes", "edition", "date", "publisher", "place", "originalDate",
|
|
61
|
+
"originalPublisher", "originalPlace", "format", "pages", "ISBN", "DOI",
|
|
62
|
+
"citationKey", "url", "accessDate", "ISSN", "archive", "archiveLocation",
|
|
63
|
+
"shortTitle", "language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
64
|
+
]),
|
|
65
|
+
case: new Set([
|
|
66
|
+
"caseName", "abstractNote", "court", "dateDecided", "docketNumber", "reporter",
|
|
67
|
+
"reporterVolume", "firstPage", "history", "DOI", "citationKey", "url",
|
|
68
|
+
"accessDate", "shortTitle", "language", "rights", "extra",
|
|
69
|
+
]),
|
|
70
|
+
computerProgram: new Set([
|
|
71
|
+
"title", "abstractNote", "seriesTitle", "versionNumber", "date", "system",
|
|
72
|
+
"company", "place", "programmingLanguage", "rights", "citationKey", "url",
|
|
73
|
+
"accessDate", "DOI", "ISBN", "archive", "archiveLocation", "libraryCatalog",
|
|
74
|
+
"callNumber", "shortTitle", "extra",
|
|
75
|
+
]),
|
|
76
|
+
conferencePaper: new Set([
|
|
77
|
+
"title", "abstractNote", "proceedingsTitle", "conferenceName", "publisher",
|
|
78
|
+
"place", "date", "eventPlace", "volume", "issue", "numberOfVolumes", "pages",
|
|
79
|
+
"series", "seriesNumber", "DOI", "ISBN", "citationKey", "url", "accessDate",
|
|
80
|
+
"ISSN", "archive", "archiveLocation", "shortTitle", "language",
|
|
81
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
82
|
+
]),
|
|
83
|
+
dataset: new Set([
|
|
84
|
+
"title", "abstractNote", "identifier", "type", "versionNumber", "date",
|
|
85
|
+
"repository", "repositoryLocation", "format", "DOI", "citationKey", "url",
|
|
86
|
+
"accessDate", "archive", "archiveLocation", "shortTitle", "language",
|
|
87
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
88
|
+
]),
|
|
89
|
+
dictionaryEntry: new Set([
|
|
90
|
+
"title", "abstractNote", "dictionaryTitle", "series", "seriesNumber", "volume",
|
|
91
|
+
"numberOfVolumes", "edition", "date", "publisher", "place", "pages", "ISBN",
|
|
92
|
+
"DOI", "citationKey", "url", "accessDate", "archive", "archiveLocation",
|
|
93
|
+
"shortTitle", "language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
94
|
+
]),
|
|
95
|
+
document: new Set([
|
|
96
|
+
"title", "abstractNote", "type", "date", "publisher", "place", "DOI",
|
|
97
|
+
"citationKey", "url", "accessDate", "archive", "archiveLocation", "shortTitle",
|
|
98
|
+
"language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
99
|
+
]),
|
|
100
|
+
email: new Set([
|
|
101
|
+
"subject", "abstractNote", "date", "DOI", "citationKey", "url", "accessDate",
|
|
102
|
+
"shortTitle", "language", "rights", "extra",
|
|
103
|
+
]),
|
|
104
|
+
encyclopediaArticle: new Set([
|
|
105
|
+
"title", "abstractNote", "encyclopediaTitle", "series", "seriesNumber", "volume",
|
|
106
|
+
"numberOfVolumes", "edition", "date", "publisher", "place", "pages", "ISBN",
|
|
107
|
+
"DOI", "citationKey", "url", "accessDate", "archive", "archiveLocation",
|
|
108
|
+
"shortTitle", "language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
109
|
+
]),
|
|
110
|
+
film: new Set([
|
|
111
|
+
"title", "abstractNote", "distributor", "place", "date", "genre",
|
|
112
|
+
"videoRecordingFormat", "runningTime", "DOI", "citationKey", "url",
|
|
113
|
+
"accessDate", "archive", "archiveLocation", "shortTitle", "language",
|
|
114
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
115
|
+
]),
|
|
116
|
+
forumPost: new Set([
|
|
117
|
+
"title", "abstractNote", "forumTitle", "postType", "date", "DOI", "citationKey",
|
|
118
|
+
"url", "accessDate", "shortTitle", "language", "rights", "extra",
|
|
119
|
+
]),
|
|
120
|
+
hearing: new Set([
|
|
121
|
+
"title", "abstractNote", "committee", "publisher", "numberOfVolumes",
|
|
122
|
+
"documentNumber", "pages", "legislativeBody", "session", "history", "date",
|
|
123
|
+
"place", "DOI", "citationKey", "url", "accessDate", "shortTitle", "language",
|
|
124
|
+
"rights", "extra",
|
|
125
|
+
]),
|
|
126
|
+
instantMessage: new Set([
|
|
127
|
+
"title", "abstractNote", "date", "DOI", "citationKey", "url", "accessDate",
|
|
128
|
+
"shortTitle", "language", "rights", "extra",
|
|
129
|
+
]),
|
|
130
|
+
interview: new Set([
|
|
131
|
+
"title", "abstractNote", "interviewMedium", "date", "publisher", "place", "DOI",
|
|
132
|
+
"citationKey", "url", "accessDate", "archive", "archiveLocation", "shortTitle",
|
|
133
|
+
"language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
134
|
+
]),
|
|
135
|
+
journalArticle: new Set([
|
|
136
|
+
"title", "abstractNote", "publicationTitle", "publisher", "place", "date",
|
|
137
|
+
"volume", "issue", "section", "partNumber", "partTitle", "pages", "series",
|
|
138
|
+
"seriesTitle", "seriesText", "journalAbbreviation", "DOI", "citationKey", "url",
|
|
139
|
+
"accessDate", "PMID", "PMCID", "ISSN", "archive", "archiveLocation",
|
|
140
|
+
"shortTitle", "language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
141
|
+
]),
|
|
142
|
+
letter: new Set([
|
|
143
|
+
"title", "abstractNote", "letterType", "date", "eventPlace", "DOI",
|
|
144
|
+
"citationKey", "url", "accessDate", "archive", "archiveLocation", "shortTitle",
|
|
145
|
+
"language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
146
|
+
]),
|
|
147
|
+
magazineArticle: new Set([
|
|
148
|
+
"title", "abstractNote", "publicationTitle", "publisher", "place", "date",
|
|
149
|
+
"volume", "issue", "pages", "ISSN", "DOI", "citationKey", "url", "accessDate",
|
|
150
|
+
"archive", "archiveLocation", "shortTitle", "language", "libraryCatalog",
|
|
151
|
+
"callNumber", "rights", "extra",
|
|
152
|
+
]),
|
|
153
|
+
manuscript: new Set([
|
|
154
|
+
"title", "abstractNote", "manuscriptType", "institution", "place", "date",
|
|
155
|
+
"numPages", "number", "DOI", "citationKey", "url", "accessDate", "archive",
|
|
156
|
+
"archiveLocation", "shortTitle", "language", "libraryCatalog", "callNumber",
|
|
157
|
+
"rights", "extra",
|
|
158
|
+
]),
|
|
159
|
+
map: new Set([
|
|
160
|
+
"title", "abstractNote", "mapType", "scale", "seriesTitle", "edition",
|
|
161
|
+
"publisher", "place", "date", "DOI", "ISBN", "citationKey", "url", "accessDate",
|
|
162
|
+
"archive", "archiveLocation", "shortTitle", "language", "libraryCatalog",
|
|
163
|
+
"callNumber", "rights", "extra",
|
|
164
|
+
]),
|
|
165
|
+
newspaperArticle: new Set([
|
|
166
|
+
"title", "abstractNote", "publicationTitle", "publisher", "place", "date",
|
|
167
|
+
"volume", "issue", "edition", "section", "pages", "ISSN", "DOI", "citationKey",
|
|
168
|
+
"url", "accessDate", "archive", "archiveLocation", "shortTitle", "language",
|
|
169
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
170
|
+
]),
|
|
171
|
+
patent: new Set([
|
|
172
|
+
"title", "abstractNote", "place", "country", "assignee", "issuingAuthority",
|
|
173
|
+
"patentNumber", "filingDate", "pages", "applicationNumber", "priorityNumbers",
|
|
174
|
+
"issueDate", "priorityDate", "references", "legalStatus", "DOI", "citationKey",
|
|
175
|
+
"url", "accessDate", "shortTitle", "language", "rights", "extra",
|
|
176
|
+
]),
|
|
177
|
+
podcast: new Set([
|
|
178
|
+
"title", "abstractNote", "seriesTitle", "episodeNumber", "audioFileType", "date",
|
|
179
|
+
"publisher", "place", "runningTime", "DOI", "citationKey", "url", "accessDate",
|
|
180
|
+
"shortTitle", "language", "rights", "extra",
|
|
181
|
+
]),
|
|
182
|
+
preprint: new Set([
|
|
183
|
+
"title", "abstractNote", "genre", "repository", "archiveID", "place", "date",
|
|
184
|
+
"series", "seriesNumber", "DOI", "citationKey", "url", "accessDate", "archive",
|
|
185
|
+
"archiveLocation", "shortTitle", "language", "libraryCatalog", "callNumber",
|
|
186
|
+
"rights", "extra",
|
|
187
|
+
]),
|
|
188
|
+
presentation: new Set([
|
|
189
|
+
"title", "abstractNote", "presentationType", "date", "meetingName", "place",
|
|
190
|
+
"series", "sessionTitle", "DOI", "citationKey", "url", "accessDate",
|
|
191
|
+
"shortTitle", "language", "rights", "extra",
|
|
192
|
+
]),
|
|
193
|
+
radioBroadcast: new Set([
|
|
194
|
+
"title", "abstractNote", "programTitle", "episodeNumber", "audioRecordingFormat",
|
|
195
|
+
"network", "place", "date", "runningTime", "DOI", "citationKey", "url",
|
|
196
|
+
"accessDate", "archive", "archiveLocation", "shortTitle", "language",
|
|
197
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
198
|
+
]),
|
|
199
|
+
report: new Set([
|
|
200
|
+
"title", "abstractNote", "reportNumber", "reportType", "institution", "place",
|
|
201
|
+
"date", "seriesTitle", "seriesNumber", "pages", "DOI", "ISBN", "citationKey",
|
|
202
|
+
"url", "accessDate", "ISSN", "archive", "archiveLocation", "shortTitle",
|
|
203
|
+
"language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
204
|
+
]),
|
|
205
|
+
standard: new Set([
|
|
206
|
+
"title", "abstractNote", "organization", "committee", "type", "number",
|
|
207
|
+
"versionNumber", "edition", "status", "date", "publisher", "place", "partNumber",
|
|
208
|
+
"partTitle", "ISBN", "DOI", "citationKey", "url", "accessDate", "archive",
|
|
209
|
+
"archiveLocation", "shortTitle", "numPages", "language", "libraryCatalog",
|
|
210
|
+
"callNumber", "rights", "extra",
|
|
211
|
+
]),
|
|
212
|
+
statute: new Set([
|
|
213
|
+
"nameOfAct", "abstractNote", "code", "codeNumber", "publicLawNumber",
|
|
214
|
+
"dateEnacted", "pages", "section", "session", "history", "DOI", "citationKey",
|
|
215
|
+
"url", "accessDate", "shortTitle", "language", "rights", "extra",
|
|
216
|
+
]),
|
|
217
|
+
thesis: new Set([
|
|
218
|
+
"title", "abstractNote", "thesisType", "university", "place", "date", "series",
|
|
219
|
+
"seriesNumber", "numPages", "DOI", "ISBN", "citationKey", "url", "accessDate",
|
|
220
|
+
"ISSN", "archive", "archiveLocation", "shortTitle", "language",
|
|
221
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
222
|
+
]),
|
|
223
|
+
tvBroadcast: new Set([
|
|
224
|
+
"title", "abstractNote", "programTitle", "episodeNumber", "videoRecordingFormat",
|
|
225
|
+
"network", "place", "date", "runningTime", "DOI", "citationKey", "url",
|
|
226
|
+
"accessDate", "archive", "archiveLocation", "shortTitle", "language",
|
|
227
|
+
"libraryCatalog", "callNumber", "rights", "extra",
|
|
228
|
+
]),
|
|
229
|
+
videoRecording: new Set([
|
|
230
|
+
"title", "abstractNote", "videoRecordingFormat", "seriesTitle", "volume",
|
|
231
|
+
"numberOfVolumes", "studio", "place", "date", "runningTime", "ISBN", "DOI",
|
|
232
|
+
"citationKey", "url", "accessDate", "archive", "archiveLocation", "shortTitle",
|
|
233
|
+
"language", "libraryCatalog", "callNumber", "rights", "extra",
|
|
234
|
+
]),
|
|
235
|
+
webpage: new Set([
|
|
236
|
+
"title", "abstractNote", "websiteTitle", "websiteType", "date", "publisher",
|
|
237
|
+
"place", "DOI", "citationKey", "url", "accessDate", "shortTitle", "language",
|
|
238
|
+
"rights", "extra",
|
|
239
|
+
]),
|
|
240
|
+
};
|
|
241
|
+
/** Per-type valid creator types — ReadonlySet for O(1) lookup. */
|
|
242
|
+
export const ITEM_TYPE_CREATOR_TYPES = {
|
|
243
|
+
artwork: new Set(["artist", "contributor"]),
|
|
244
|
+
audioRecording: new Set(["performer", "composer", "contributor", "originalCreator", "translator", "wordsBy"]),
|
|
245
|
+
bill: new Set(["sponsor", "contributor", "cosponsor"]),
|
|
246
|
+
blogPost: new Set(["author", "commenter", "contributor", "translator"]),
|
|
247
|
+
book: new Set(["author", "contributor", "editor", "seriesEditor", "translator"]),
|
|
248
|
+
bookSection: new Set(["author", "bookAuthor", "contributor", "editor", "seriesEditor", "translator"]),
|
|
249
|
+
case: new Set(["author", "contributor", "counsel"]),
|
|
250
|
+
computerProgram: new Set(["programmer", "contributor"]),
|
|
251
|
+
conferencePaper: new Set(["author", "contributor", "editor", "seriesEditor", "translator"]),
|
|
252
|
+
dataset: new Set(["author", "contributor"]),
|
|
253
|
+
dictionaryEntry: new Set(["author", "contributor", "editor", "seriesEditor", "translator"]),
|
|
254
|
+
document: new Set(["author", "contributor", "editor", "reviewedAuthor", "translator"]),
|
|
255
|
+
email: new Set(["author", "contributor", "recipient", "translator"]),
|
|
256
|
+
encyclopediaArticle: new Set(["author", "contributor", "editor", "seriesEditor", "translator"]),
|
|
257
|
+
film: new Set(["director", "castMember", "contributor", "guest", "host", "narrator", "producer", "scriptwriter", "translator"]),
|
|
258
|
+
forumPost: new Set(["author", "contributor"]),
|
|
259
|
+
hearing: new Set(["contributor"]),
|
|
260
|
+
instantMessage: new Set(["author", "contributor", "recipient"]),
|
|
261
|
+
interview: new Set(["interviewee", "contributor", "interviewer", "translator"]),
|
|
262
|
+
journalArticle: new Set(["author", "contributor", "editor", "reviewedAuthor", "translator"]),
|
|
263
|
+
letter: new Set(["author", "contributor", "recipient", "translator"]),
|
|
264
|
+
magazineArticle: new Set(["author", "contributor", "reviewedAuthor", "translator"]),
|
|
265
|
+
manuscript: new Set(["author", "contributor", "translator"]),
|
|
266
|
+
map: new Set(["cartographer", "contributor", "seriesEditor"]),
|
|
267
|
+
newspaperArticle: new Set(["author", "contributor", "reviewedAuthor", "translator"]),
|
|
268
|
+
patent: new Set(["inventor", "attorneyAgent", "contributor"]),
|
|
269
|
+
podcast: new Set(["podcaster", "castMember", "contributor", "director", "executiveProducer", "guest", "producer", "scriptwriter", "seriesCreator", "translator"]),
|
|
270
|
+
preprint: new Set(["author", "contributor", "editor", "reviewedAuthor", "translator"]),
|
|
271
|
+
presentation: new Set(["presenter", "chair", "contributor", "organizer", "translator"]),
|
|
272
|
+
radioBroadcast: new Set(["creator", "castMember", "contributor", "director", "executiveProducer", "guest", "host", "producer", "scriptwriter", "seriesCreator", "translator"]),
|
|
273
|
+
report: new Set(["author", "contributor", "editor", "seriesEditor", "translator"]),
|
|
274
|
+
standard: new Set(["author", "contributor", "editor"]),
|
|
275
|
+
statute: new Set(["author", "contributor"]),
|
|
276
|
+
thesis: new Set(["author", "contributor"]),
|
|
277
|
+
tvBroadcast: new Set(["director", "castMember", "contributor", "executiveProducer", "guest", "host", "narrator", "producer", "scriptwriter", "seriesCreator", "translator"]),
|
|
278
|
+
videoRecording: new Set(["creator", "castMember", "contributor", "director", "executiveProducer", "guest", "host", "narrator", "producer", "scriptwriter", "translator"]),
|
|
279
|
+
webpage: new Set(["author", "contributor", "translator"]),
|
|
280
|
+
};
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@xevos117/mcp-zotero",
|
|
3
|
-
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"description": "Unofficial MCP server for Zotero integration with citation management",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/Xevos117/mcp-zotero.git"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/Xevos117/mcp-zotero#readme",
|
|
11
|
-
"bugs": {
|
|
12
|
-
"url": "https://github.com/Xevos117/mcp-zotero/issues"
|
|
13
|
-
},
|
|
14
|
-
"main": "build/server.js",
|
|
15
|
-
"bin": {
|
|
16
|
-
"mcp-zotero": "build/server.js"
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"build"
|
|
20
|
-
],
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=18"
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"test": "vitest run",
|
|
26
|
-
"test:watch": "vitest",
|
|
27
|
-
"build": "tsc",
|
|
28
|
-
"start": "node build/server.js",
|
|
29
|
-
"prepublishOnly": "npm test && npm run build"
|
|
30
|
-
},
|
|
31
|
-
"keywords": [
|
|
32
|
-
"mcp",
|
|
33
|
-
"model-context-protocol",
|
|
34
|
-
"zotero",
|
|
35
|
-
"zotero-api",
|
|
36
|
-
"citation",
|
|
37
|
-
"bibliography",
|
|
38
|
-
"reference-manager",
|
|
39
|
-
"claude",
|
|
40
|
-
"ai"
|
|
41
|
-
],
|
|
42
|
-
"author": "Xevos17",
|
|
43
|
-
"license": "MIT",
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
46
|
-
"dotenv": "^16.4.7",
|
|
47
|
-
"fast-xml-parser": "^5.3.5",
|
|
48
|
-
"jszip": "^3.10.1",
|
|
49
|
-
"unpdf": "^1.4.0",
|
|
50
|
-
"zod": "^3.24.1",
|
|
51
|
-
"zotero-api-client": "^0.46.0"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@types/node": "latest",
|
|
55
|
-
"typescript": "latest",
|
|
56
|
-
"vitest": "^4.0.18"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@xevos117/mcp-zotero",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.5",
|
|
5
|
+
"description": "Unofficial MCP server for Zotero integration with citation management",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Xevos117/mcp-zotero.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Xevos117/mcp-zotero#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Xevos117/mcp-zotero/issues"
|
|
13
|
+
},
|
|
14
|
+
"main": "build/server.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"mcp-zotero": "build/server.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"build"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"start": "node build/server.js",
|
|
29
|
+
"prepublishOnly": "npm test && npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"zotero",
|
|
35
|
+
"zotero-api",
|
|
36
|
+
"citation",
|
|
37
|
+
"bibliography",
|
|
38
|
+
"reference-manager",
|
|
39
|
+
"claude",
|
|
40
|
+
"ai"
|
|
41
|
+
],
|
|
42
|
+
"author": "Xevos17",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
46
|
+
"dotenv": "^16.4.7",
|
|
47
|
+
"fast-xml-parser": "^5.3.5",
|
|
48
|
+
"jszip": "^3.10.1",
|
|
49
|
+
"unpdf": "^1.4.0",
|
|
50
|
+
"zod": "^3.24.1",
|
|
51
|
+
"zotero-api-client": "^0.46.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "latest",
|
|
55
|
+
"typescript": "latest",
|
|
56
|
+
"vitest": "^4.0.18"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { isZoteroApiError } from "../types/zotero-types.js";
|
|
3
|
-
import { formatErrorResponse } from "../utils/error-formatter.js";
|
|
4
|
-
import { logger } from "../utils/logger.js";
|
|
5
|
-
export const toolConfig = {
|
|
6
|
-
name: "add_web_item",
|
|
7
|
-
description: "Save a web page as a Zotero item of type 'webpage'. Use this for online articles, blog posts, or any web resource that does not have a DOI. Returns the new item key and metadata.",
|
|
8
|
-
inputSchema: {
|
|
9
|
-
url: z.string().url().describe("URL of the web page"),
|
|
10
|
-
title: z.string().describe("Title of the web page or article"),
|
|
11
|
-
website_title: z
|
|
12
|
-
.string()
|
|
13
|
-
.optional()
|
|
14
|
-
.describe('Name of the website or online publication (e.g. "Nature News", "Ars Technica")'),
|
|
15
|
-
date: z
|
|
16
|
-
.string()
|
|
17
|
-
.optional()
|
|
18
|
-
.describe("Publication date (e.g. \"2024-03-15\")"),
|
|
19
|
-
creators: z
|
|
20
|
-
.array(z.object({
|
|
21
|
-
firstName: z.string(),
|
|
22
|
-
lastName: z.string(),
|
|
23
|
-
}))
|
|
24
|
-
.optional()
|
|
25
|
-
.describe("Authors of the web page"),
|
|
26
|
-
collection_key: z
|
|
27
|
-
.string()
|
|
28
|
-
.optional()
|
|
29
|
-
.describe("Zotero collection key to add the item to. Get this from create_collection or get_collections."),
|
|
30
|
-
tags: z
|
|
31
|
-
.array(z.string())
|
|
32
|
-
.optional()
|
|
33
|
-
.describe("Tags to apply to the item"),
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
const AddWebItemSchema = z.object(toolConfig.inputSchema);
|
|
37
|
-
export async function handleAddWebItem(zoteroApi, userId, args) {
|
|
38
|
-
const { url, title, website_title, date, creators, collection_key, tags } = AddWebItemSchema.parse(args);
|
|
39
|
-
const itemData = {
|
|
40
|
-
itemType: "webpage",
|
|
41
|
-
title,
|
|
42
|
-
url,
|
|
43
|
-
accessDate: new Date().toISOString().split("T")[0],
|
|
44
|
-
creators: creators?.map((c) => ({
|
|
45
|
-
...c,
|
|
46
|
-
creatorType: "author",
|
|
47
|
-
})) ?? [],
|
|
48
|
-
collections: collection_key ? [collection_key] : [],
|
|
49
|
-
tags: tags?.map((t) => ({ tag: t })) ?? [],
|
|
50
|
-
};
|
|
51
|
-
if (website_title) {
|
|
52
|
-
itemData.websiteTitle = website_title;
|
|
53
|
-
}
|
|
54
|
-
if (date) {
|
|
55
|
-
itemData.date = date;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
const response = await zoteroApi
|
|
59
|
-
.library("user", userId)
|
|
60
|
-
.items()
|
|
61
|
-
.post([itemData]);
|
|
62
|
-
if (!response.isSuccess()) {
|
|
63
|
-
const errors = response.getErrors();
|
|
64
|
-
const errorMsg = Object.values(errors).join("; ") || "Unknown error";
|
|
65
|
-
return formatErrorResponse("Failed to create web page item", {
|
|
66
|
-
details: errorMsg,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
const created = response.getData();
|
|
70
|
-
const item = created[0];
|
|
71
|
-
return {
|
|
72
|
-
content: [
|
|
73
|
-
{
|
|
74
|
-
type: "text",
|
|
75
|
-
text: JSON.stringify({
|
|
76
|
-
item_key: item.key,
|
|
77
|
-
title: item.title ?? title,
|
|
78
|
-
url,
|
|
79
|
-
website_title: website_title ?? null,
|
|
80
|
-
item_type: "webpage",
|
|
81
|
-
}, null, 2),
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
if (isZoteroApiError(err)) {
|
|
88
|
-
logger.error("Tool execution failed", {
|
|
89
|
-
tool: "add_web_item",
|
|
90
|
-
status: err.response?.status,
|
|
91
|
-
errorMessage: err.message,
|
|
92
|
-
url: err.response?.url,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
}
|