@xevos117/mcp-zotero 1.0.4 → 1.0.6
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/README.md +50 -3
- package/build/server.js +4 -1
- package/build/tools/delete-collection.js +79 -0
- package/build/tools/delete-items.js +89 -0
- package/build/tools/find-and-attach-pdfs.js +3 -8
- package/build/tools/get-collection-items.js +9 -8
- package/build/tools/get-collections.js +2 -5
- package/build/tools/index.js +10 -4
- package/build/utils/pagination.js +22 -0
- package/build/utils/unsafe-operations.js +14 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
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
6
|
|
|
7
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,
|
|
8
|
+
> This project has since been extensively rewritten with a new architecture, 15 tools (up from 5), citation injection, PDF management, and Claude skill support.
|
|
9
9
|
|
|
10
10
|
## How it works
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ For advanced use cases (PDF upload policy, citation style guidance, source trans
|
|
|
17
17
|
|
|
18
18
|
| Scenario | MCP server | Skill needed? |
|
|
19
19
|
|---|---|---|
|
|
20
|
-
| Local LLM (Claude Code, LM Studio, etc.) | All
|
|
20
|
+
| Local LLM (Claude Code, LM Studio, etc.) | All 15 tools | No |
|
|
21
21
|
| Remote/sandboxed LLM (Claude.ai Projects) | API tools (search, add, metadata) | Yes, for citation injection |
|
|
22
22
|
|
|
23
23
|
Local LLMs with filesystem access can use all tools directly, including `inject_citations` which reads and writes `.docx` files on disk.
|
|
@@ -41,6 +41,7 @@ Remote LLMs without filesystem access can use the included **Claude skill** (`sk
|
|
|
41
41
|
export ZOTERO_API_KEY="your-api-key"
|
|
42
42
|
export ZOTERO_USER_ID="user-id-from-curl"
|
|
43
43
|
export UNPAYWALL_EMAIL="your@email.edu" # Optional: enables OA PDF lookup via Unpaywall
|
|
44
|
+
export UNSAFE_OPERATIONS="none" # Optional: "none" | "items" | "all" (see below)
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
## Environment Variables
|
|
@@ -50,6 +51,45 @@ Remote LLMs without filesystem access can use the included **Claude skill** (`sk
|
|
|
50
51
|
| `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
52
|
| `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
53
|
| `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. |
|
|
54
|
+
| `UNSAFE_OPERATIONS` | No | Controls destructive operations (deletion). See [Unsafe Operations](#unsafe-operations) below. Default: `none` (all deletions blocked). |
|
|
55
|
+
|
|
56
|
+
### Unsafe Operations
|
|
57
|
+
|
|
58
|
+
By default, the MCP server **does not allow any deletion**. This is a safety measure to prevent an LLM from accidentally deleting items or collections from your library.
|
|
59
|
+
|
|
60
|
+
To enable deletion, set the `UNSAFE_OPERATIONS` environment variable to one of the following values:
|
|
61
|
+
|
|
62
|
+
| Value | `delete_items` | `delete_collection` | Use case |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `none` (default) | Blocked | Blocked | Safe mode — no deletions possible |
|
|
65
|
+
| `items` | **Allowed** | Blocked | Allow deleting items but protect collection structure |
|
|
66
|
+
| `all` | **Allowed** | **Allowed** | Full access — items and collections can be deleted |
|
|
67
|
+
|
|
68
|
+
**Important notes:**
|
|
69
|
+
|
|
70
|
+
- If `UNSAFE_OPERATIONS` is not set, empty, or set to an unrecognized value, it defaults to `none`.
|
|
71
|
+
- The value is **case-insensitive** (e.g. `ALL`, `Items`, `NONE` all work).
|
|
72
|
+
- `delete_items` moves items to the Zotero trash (recoverable from the Zotero desktop client).
|
|
73
|
+
- `delete_collection` removes the collection (folder) only — items inside it are **not** deleted and remain in your library.
|
|
74
|
+
- The `all` value includes both item and collection deletion because managing collections inherently requires item-level access.
|
|
75
|
+
|
|
76
|
+
**Configuration example:**
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"zotero": {
|
|
82
|
+
"command": "npx",
|
|
83
|
+
"args": ["-y", "@xevos117/mcp-zotero"],
|
|
84
|
+
"env": {
|
|
85
|
+
"ZOTERO_API_KEY": "YOUR_API_KEY",
|
|
86
|
+
"ZOTERO_USER_ID": "YOUR_USER_ID",
|
|
87
|
+
"UNSAFE_OPERATIONS": "items"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
53
93
|
|
|
54
94
|
## Integration with Claude Desktop
|
|
55
95
|
|
|
@@ -100,6 +140,13 @@ claude mcp add-json "zotero" '{"command":"npx","args":["tsx","src/server.ts"],"e
|
|
|
100
140
|
| `find_and_attach_pdfs` | Batch OA PDF lookup and auto-attach via Unpaywall (by item keys or collection) |
|
|
101
141
|
| `add_linked_url_attachment` | Attach a URL to an existing item or create a standalone link |
|
|
102
142
|
|
|
143
|
+
### Deleting content
|
|
144
|
+
|
|
145
|
+
| Tool | Description |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `delete_items` | Delete up to 50 items per call (moves to Zotero trash). Requires `UNSAFE_OPERATIONS=items` or `all` |
|
|
148
|
+
| `delete_collection` | Delete a collection (folder). Items inside are kept. Requires `UNSAFE_OPERATIONS=all` |
|
|
149
|
+
|
|
103
150
|
### Citation & documents
|
|
104
151
|
|
|
105
152
|
| Tool | Description |
|
|
@@ -112,7 +159,7 @@ claude mcp add-json "zotero" '{"command":"npx","args":["tsx","src/server.ts"],"e
|
|
|
112
159
|
```bash
|
|
113
160
|
npm install
|
|
114
161
|
npm run build # Compile TypeScript
|
|
115
|
-
npm test # Run tests (vitest,
|
|
162
|
+
npm test # Run tests (vitest, 377 tests)
|
|
116
163
|
npx tsx src/server.ts # Run directly without building
|
|
117
164
|
```
|
|
118
165
|
|
package/build/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
import { registerAllTools } from "./tools/index.js";
|
|
7
7
|
import { logger } from "./utils/logger.js";
|
|
8
|
+
import { parseUnsafeOperations } from "./utils/unsafe-operations.js";
|
|
8
9
|
// Re-export for backward compatibility with existing tests
|
|
9
10
|
export { formatErrorResponse } from "./utils/error-formatter.js";
|
|
10
11
|
export { handleToolCall } from "./tools/index.js";
|
|
@@ -12,6 +13,7 @@ class ZoteroServer {
|
|
|
12
13
|
server;
|
|
13
14
|
zoteroApi;
|
|
14
15
|
userId;
|
|
16
|
+
unsafeOps;
|
|
15
17
|
constructor(options = {}) {
|
|
16
18
|
this.server = new McpServer({ name: "zotero", version: "1.0.0" }, {
|
|
17
19
|
capabilities: { tools: {} },
|
|
@@ -30,6 +32,7 @@ class ZoteroServer {
|
|
|
30
32
|
"- get_user_id is only needed by the standalone skill script (inject.js), not by the inject_citations tool.",
|
|
31
33
|
].join("\n"),
|
|
32
34
|
});
|
|
35
|
+
this.unsafeOps = parseUnsafeOperations(process.env.UNSAFE_OPERATIONS);
|
|
33
36
|
if (options.zoteroApi) {
|
|
34
37
|
this.userId = options.userId || "";
|
|
35
38
|
this.zoteroApi = options.zoteroApi;
|
|
@@ -46,7 +49,7 @@ class ZoteroServer {
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
setupHandlers() {
|
|
49
|
-
registerAllTools(this.server, this.zoteroApi, this.userId);
|
|
52
|
+
registerAllTools(this.server, this.zoteroApi, this.userId, this.unsafeOps);
|
|
50
53
|
}
|
|
51
54
|
async start() {
|
|
52
55
|
this.setupHandlers();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isZoteroApiError } from "../types/zotero-types.js";
|
|
3
|
+
import { formatErrorResponse } from "../utils/error-formatter.js";
|
|
4
|
+
import { canDeleteCollections } from "../utils/unsafe-operations.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
export const toolConfig = {
|
|
7
|
+
name: "delete_collection",
|
|
8
|
+
description: "Delete a collection (folder) from your Zotero library. Items inside the collection are NOT deleted — they remain in your library. Requires UNSAFE_OPERATIONS environment variable set to 'all'.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
collection_key: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe("Zotero collection key to delete. Get this from get_collections."),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const DeleteCollectionSchema = z.object(toolConfig.inputSchema);
|
|
17
|
+
export async function handleDeleteCollection(zoteroApi, userId, args, unsafeOps = "none") {
|
|
18
|
+
const { collection_key } = DeleteCollectionSchema.parse(args);
|
|
19
|
+
if (!canDeleteCollections(unsafeOps)) {
|
|
20
|
+
return formatErrorResponse("Deletion of collections is not allowed. Set the UNSAFE_OPERATIONS environment variable to 'all' to enable this operation.", {
|
|
21
|
+
env_var: "UNSAFE_OPERATIONS",
|
|
22
|
+
current_value: unsafeOps,
|
|
23
|
+
required_values: ["all"],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = await zoteroApi
|
|
28
|
+
.library("user", userId)
|
|
29
|
+
.collections(collection_key)
|
|
30
|
+
.get();
|
|
31
|
+
const collection = response.getData();
|
|
32
|
+
const version = response.getVersion();
|
|
33
|
+
if (version === null) {
|
|
34
|
+
return formatErrorResponse("Could not determine collection version", {
|
|
35
|
+
collection_key,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
await zoteroApi
|
|
39
|
+
.library("user", userId)
|
|
40
|
+
.collections(collection_key)
|
|
41
|
+
.version(version)
|
|
42
|
+
.delete();
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
deleted: true,
|
|
49
|
+
collection_key,
|
|
50
|
+
name: collection.name || collection_key,
|
|
51
|
+
}, null, 2),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (isZoteroApiError(err)) {
|
|
58
|
+
if (err.response.status === 404) {
|
|
59
|
+
return formatErrorResponse("Collection not found", {
|
|
60
|
+
collection_key,
|
|
61
|
+
status: "not_found",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (err.response.status === 412) {
|
|
65
|
+
return formatErrorResponse("Collection was modified by another client. Retry the operation.", {
|
|
66
|
+
collection_key,
|
|
67
|
+
status: "version_conflict",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
logger.error("Tool execution failed", {
|
|
71
|
+
tool: "delete_collection",
|
|
72
|
+
status: err.response.status,
|
|
73
|
+
errorMessage: err.message,
|
|
74
|
+
url: err.response.url,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isZoteroApiError } from "../types/zotero-types.js";
|
|
3
|
+
import { formatErrorResponse } from "../utils/error-formatter.js";
|
|
4
|
+
import { canDeleteItems } from "../utils/unsafe-operations.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
export const toolConfig = {
|
|
7
|
+
name: "delete_items",
|
|
8
|
+
description: "Delete one or more items from your Zotero library permanently (moves to trash). Accepts up to 50 item keys per call. Requires UNSAFE_OPERATIONS environment variable set to 'items' or 'all'.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
item_keys: z
|
|
11
|
+
.array(z.string())
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(50)
|
|
14
|
+
.describe('Array of Zotero item keys to delete (e.g. ["EUHUT5K3", "F9UQM7N2"]). Max 50 per call.'),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
const DeleteItemsSchema = z.object(toolConfig.inputSchema);
|
|
18
|
+
export async function handleDeleteItems(zoteroApi, userId, args, unsafeOps = "none") {
|
|
19
|
+
const { item_keys } = DeleteItemsSchema.parse(args);
|
|
20
|
+
if (!canDeleteItems(unsafeOps)) {
|
|
21
|
+
return formatErrorResponse("Deletion of items is not allowed. Set the UNSAFE_OPERATIONS environment variable to 'items' or 'all' to enable this operation.", {
|
|
22
|
+
env_var: "UNSAFE_OPERATIONS",
|
|
23
|
+
current_value: unsafeOps,
|
|
24
|
+
required_values: ["items", "all"],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const response = await zoteroApi
|
|
29
|
+
.library("user", userId)
|
|
30
|
+
.items()
|
|
31
|
+
.get({ itemKey: item_keys.join(",") });
|
|
32
|
+
const items = response.getData();
|
|
33
|
+
const itemList = Array.isArray(items) ? items : [items];
|
|
34
|
+
const foundKeys = new Set(itemList.map((item) => item.key).filter(Boolean));
|
|
35
|
+
const notFoundKeys = item_keys.filter((k) => !foundKeys.has(k));
|
|
36
|
+
if (foundKeys.size === 0) {
|
|
37
|
+
return formatErrorResponse("No items found for the given keys", {
|
|
38
|
+
item_keys,
|
|
39
|
+
status: "not_found",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Use library version from response header (Last-Modified-Version),
|
|
43
|
+
// not individual item versions — required for multi-object DELETE
|
|
44
|
+
const libraryVersion = response.getVersion();
|
|
45
|
+
if (libraryVersion === null) {
|
|
46
|
+
return formatErrorResponse("Could not determine library version", {
|
|
47
|
+
item_keys,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const keysToDelete = [...foundKeys];
|
|
51
|
+
await zoteroApi
|
|
52
|
+
.library("user", userId)
|
|
53
|
+
.items()
|
|
54
|
+
.version(libraryVersion)
|
|
55
|
+
.delete(keysToDelete);
|
|
56
|
+
const result = {
|
|
57
|
+
deleted_keys: keysToDelete,
|
|
58
|
+
deleted_count: keysToDelete.length,
|
|
59
|
+
};
|
|
60
|
+
if (notFoundKeys.length > 0) {
|
|
61
|
+
result.not_found = notFoundKeys;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify(result, null, 2),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (isZoteroApiError(err)) {
|
|
74
|
+
if (err.response.status === 412) {
|
|
75
|
+
return formatErrorResponse("Items were modified by another client. Retry the operation.", {
|
|
76
|
+
item_keys,
|
|
77
|
+
status: "version_conflict",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
logger.error("Tool execution failed", {
|
|
81
|
+
tool: "delete_items",
|
|
82
|
+
status: err.response.status,
|
|
83
|
+
errorMessage: err.message,
|
|
84
|
+
url: err.response.url,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -5,6 +5,7 @@ import { logger } from "../utils/logger.js";
|
|
|
5
5
|
import { lookupOaPdfWithFallbacks } from "../utils/unpaywall.js";
|
|
6
6
|
import { downloadAndUploadPdf } from "../utils/pdf-uploader.js";
|
|
7
7
|
import { mapWithConcurrency } from "../utils/concurrency.js";
|
|
8
|
+
import { fetchAllPages } from "../utils/pagination.js";
|
|
8
9
|
export const toolConfig = {
|
|
9
10
|
name: "find_and_attach_pdfs",
|
|
10
11
|
description: "For each Zotero item, check Unpaywall for open access PDFs and attach them. Items must have a DOI. Uses the same source as Zotero Desktop's 'Find Available PDFs'.",
|
|
@@ -45,14 +46,8 @@ export async function handleFindAndAttachPdfs(zoteroApi, userId, args) {
|
|
|
45
46
|
// 1. Resolve item keys
|
|
46
47
|
let keys;
|
|
47
48
|
if (collection_key) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
.collections(collection_key)
|
|
51
|
-
.items()
|
|
52
|
-
.get();
|
|
53
|
-
const collItems = collResponse.getData();
|
|
54
|
-
const itemsArray = Array.isArray(collItems) ? collItems : [collItems];
|
|
55
|
-
keys = itemsArray
|
|
49
|
+
const { items: collItems } = await fetchAllPages((params) => zoteroApi.library("user", userId).collections(collection_key).items().get(params));
|
|
50
|
+
keys = collItems
|
|
56
51
|
.filter((item) => item.itemType !== "attachment" && item.itemType !== "note")
|
|
57
52
|
.map((item) => item.key)
|
|
58
53
|
.filter(Boolean);
|
|
@@ -3,6 +3,7 @@ import { isZoteroApiError } from "../types/zotero-types.js";
|
|
|
3
3
|
import { formatErrorResponse } from "../utils/error-formatter.js";
|
|
4
4
|
import { formatCreators, formatTags } from "../utils/item-formatter.js";
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { fetchAllPages } from "../utils/pagination.js";
|
|
6
7
|
export const toolConfig = {
|
|
7
8
|
name: "get_collection_items",
|
|
8
9
|
description: "Get all items in a specific Zotero collection. Returns item keys, titles, authors, and dates. Use the collectionKey from get_collections. Use the returned item keys with get_items_details, get_item_fulltext, or inject_citations.",
|
|
@@ -18,19 +19,15 @@ const CollectionItemsSchema = z.object(toolConfig.inputSchema);
|
|
|
18
19
|
export async function handleGetCollectionItems(zoteroApi, userId, args) {
|
|
19
20
|
const { collectionKey, excludeAttachments } = CollectionItemsSchema.parse(args);
|
|
20
21
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
.collections(collectionKey)
|
|
24
|
-
.items()
|
|
25
|
-
.get();
|
|
26
|
-
let items = response.getData();
|
|
27
|
-
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
22
|
+
const { items: allItems, totalResults } = await fetchAllPages((params) => zoteroApi.library("user", userId).collections(collectionKey).items().get(params));
|
|
23
|
+
if (!allItems || allItems.length === 0) {
|
|
28
24
|
return formatErrorResponse("Collection is empty", {
|
|
29
25
|
collectionKey,
|
|
30
26
|
suggestion: "Add some items to this collection in Zotero",
|
|
31
27
|
status: "empty",
|
|
32
28
|
});
|
|
33
29
|
}
|
|
30
|
+
let items = allItems;
|
|
34
31
|
if (excludeAttachments) {
|
|
35
32
|
items = items.filter((item) => item.itemType !== "attachment" && item.itemType !== "note");
|
|
36
33
|
}
|
|
@@ -58,7 +55,11 @@ export async function handleGetCollectionItems(zoteroApi, userId, args) {
|
|
|
58
55
|
content: [
|
|
59
56
|
{
|
|
60
57
|
type: "text",
|
|
61
|
-
text: JSON.stringify(
|
|
58
|
+
text: JSON.stringify({
|
|
59
|
+
total_items: totalResults,
|
|
60
|
+
returned_items: formatted.length,
|
|
61
|
+
items: formatted,
|
|
62
|
+
}, null, 2),
|
|
62
63
|
},
|
|
63
64
|
],
|
|
64
65
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isZoteroApiError } from "../types/zotero-types.js";
|
|
2
2
|
import { formatErrorResponse } from "../utils/error-formatter.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
|
+
import { fetchAllPages } from "../utils/pagination.js";
|
|
4
5
|
export const toolConfig = {
|
|
5
6
|
name: "get_collections",
|
|
6
7
|
description: "List all collections (folders) in your Zotero library. Returns collection keys, names, and parent relationships. Use collection keys with get_collection_items or as parent_collection in create_collection.",
|
|
@@ -8,11 +9,7 @@ export const toolConfig = {
|
|
|
8
9
|
};
|
|
9
10
|
export async function handleGetCollections(zoteroApi, userId, _args) {
|
|
10
11
|
try {
|
|
11
|
-
const
|
|
12
|
-
.library("user", userId)
|
|
13
|
-
.collections()
|
|
14
|
-
.get();
|
|
15
|
-
const collections = response.getData();
|
|
12
|
+
const { items: collections } = await fetchAllPages((params) => zoteroApi.library("user", userId).collections().get(params));
|
|
16
13
|
if (!Array.isArray(collections) || collections.length === 0) {
|
|
17
14
|
return formatErrorResponse("No collections found", {
|
|
18
15
|
suggestion: "Create a collection in your Zotero library first",
|
package/build/tools/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import { handleAddLinkedUrlAttachment, toolConfig as addLinkedUrlAttachmentConfi
|
|
|
11
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
|
+
import { handleDeleteCollection, toolConfig as deleteCollectionConfig } from "./delete-collection.js";
|
|
15
|
+
import { handleDeleteItems, toolConfig as deleteItemsConfig } from "./delete-items.js";
|
|
14
16
|
const handlers = {
|
|
15
17
|
get_collections: handleGetCollections,
|
|
16
18
|
get_collection_items: handleGetCollectionItems,
|
|
@@ -25,13 +27,15 @@ const handlers = {
|
|
|
25
27
|
add_items: handleAddItems,
|
|
26
28
|
import_pdf_to_zotero: handleImportPdfToZotero,
|
|
27
29
|
find_and_attach_pdfs: handleFindAndAttachPdfs,
|
|
30
|
+
delete_collection: handleDeleteCollection,
|
|
31
|
+
delete_items: handleDeleteItems,
|
|
28
32
|
};
|
|
29
|
-
export async function handleToolCall(name, args, zoteroApi, userId) {
|
|
33
|
+
export async function handleToolCall(name, args, zoteroApi, userId, unsafeOps = "none") {
|
|
30
34
|
const handler = handlers[name];
|
|
31
35
|
if (!handler) {
|
|
32
36
|
throw new Error(`Unknown tool: ${name}`);
|
|
33
37
|
}
|
|
34
|
-
return handler(zoteroApi, userId, args);
|
|
38
|
+
return handler(zoteroApi, userId, args, unsafeOps);
|
|
35
39
|
}
|
|
36
40
|
const toolConfigs = [
|
|
37
41
|
{ config: collectionsConfig, handler: handleGetCollections },
|
|
@@ -47,14 +51,16 @@ const toolConfigs = [
|
|
|
47
51
|
{ config: addItemsConfig, handler: handleAddItems },
|
|
48
52
|
{ config: importPdfToZoteroConfig, handler: handleImportPdfToZotero },
|
|
49
53
|
{ config: findAndAttachPdfsConfig, handler: handleFindAndAttachPdfs },
|
|
54
|
+
{ config: deleteCollectionConfig, handler: handleDeleteCollection },
|
|
55
|
+
{ config: deleteItemsConfig, handler: handleDeleteItems },
|
|
50
56
|
];
|
|
51
|
-
export function registerAllTools(server, zoteroApi, userId) {
|
|
57
|
+
export function registerAllTools(server, zoteroApi, userId, unsafeOps = "none") {
|
|
52
58
|
for (const { config, handler } of toolConfigs) {
|
|
53
59
|
server.registerTool(config.name, {
|
|
54
60
|
description: config.description,
|
|
55
61
|
inputSchema: config.inputSchema,
|
|
56
62
|
}, async (args) => {
|
|
57
|
-
return handler(zoteroApi, userId, args);
|
|
63
|
+
return handler(zoteroApi, userId, args, unsafeOps);
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
66
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const PAGE_LIMIT = 100;
|
|
2
|
+
export async function fetchAllPages(buildRequest) {
|
|
3
|
+
const firstResponse = await buildRequest({ limit: PAGE_LIMIT, start: 0 });
|
|
4
|
+
const firstData = firstResponse.getData();
|
|
5
|
+
const firstItems = Array.isArray(firstData) ? firstData : firstData ? [firstData] : [];
|
|
6
|
+
const totalResults = firstResponse.getTotalResults();
|
|
7
|
+
if (totalResults === null || firstItems.length >= totalResults) {
|
|
8
|
+
return { items: firstItems, totalResults };
|
|
9
|
+
}
|
|
10
|
+
const allItems = [...firstItems];
|
|
11
|
+
let start = firstItems.length;
|
|
12
|
+
while (start < totalResults) {
|
|
13
|
+
const response = await buildRequest({ limit: PAGE_LIMIT, start });
|
|
14
|
+
const data = response.getData();
|
|
15
|
+
const pageItems = Array.isArray(data) ? data : data ? [data] : [];
|
|
16
|
+
if (pageItems.length === 0)
|
|
17
|
+
break;
|
|
18
|
+
allItems.push(...pageItems);
|
|
19
|
+
start += pageItems.length;
|
|
20
|
+
}
|
|
21
|
+
return { items: allItems, totalResults };
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const VALID_MODES = new Set(["none", "items", "all"]);
|
|
2
|
+
export function parseUnsafeOperations(value) {
|
|
3
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
4
|
+
if (VALID_MODES.has(normalized)) {
|
|
5
|
+
return normalized;
|
|
6
|
+
}
|
|
7
|
+
return "none";
|
|
8
|
+
}
|
|
9
|
+
export function canDeleteCollections(mode) {
|
|
10
|
+
return mode === "all";
|
|
11
|
+
}
|
|
12
|
+
export function canDeleteItems(mode) {
|
|
13
|
+
return mode === "items" || mode === "all";
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xevos117/mcp-zotero",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.6",
|
|
5
5
|
"description": "Unofficial MCP server for Zotero integration with citation management",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"url": "https://github.com/Xevos117/mcp-zotero/issues"
|
|
13
13
|
},
|
|
14
14
|
"main": "build/server.js",
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
"bin": {
|
|
16
|
+
"mcp-zotero": "build/server.js"
|
|
17
|
+
},
|
|
17
18
|
"files": [
|
|
18
19
|
"build"
|
|
19
20
|
],
|