@xevos117/mcp-zotero 1.0.7 → 1.0.8

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 CHANGED
@@ -132,7 +132,7 @@ claude mcp add-json "zotero" '{"command":"npx","args":["tsx","src/server.ts"],"e
132
132
  | `get_collections` | List all collections (folders) with keys, names, and parent relationships |
133
133
  | `get_collection_items` | Get items in a specific collection with keys, titles, authors, dates |
134
134
  | `search_library` | Search by query, or list items sorted by field (date, title, etc.) |
135
- | `get_items_details` | Batch metadata retrieval for multiple items in a single call |
135
+ | `get_items_details` | Batch metadata retrieval for multiple items — returns all type-specific fields (bookTitle, proceedingsTitle, university, etc.) |
136
136
  | `get_item_fulltext` | Get full-text content of a PDF attachment via Zotero's fulltext index |
137
137
 
138
138
  ### Adding content
@@ -165,7 +165,7 @@ claude mcp add-json "zotero" '{"command":"npx","args":["tsx","src/server.ts"],"e
165
165
  ```bash
166
166
  npm install
167
167
  npm run build # Compile TypeScript
168
- npm test # Run tests (vitest, 382 tests)
168
+ npm test # Run tests (vitest, 404 tests)
169
169
  npx tsx src/server.ts # Run directly without building
170
170
  ```
171
171
 
@@ -1,21 +1,40 @@
1
+ import { z } from "zod";
1
2
  import { isZoteroApiError } from "../types/zotero-types.js";
2
3
  import { formatErrorResponse } from "../utils/error-formatter.js";
3
4
  import { logger } from "../utils/logger.js";
4
5
  import { fetchAllPages } from "../utils/pagination.js";
5
6
  export const toolConfig = {
6
7
  name: "get_collections",
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
- inputSchema: {},
8
+ 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. Trashed collections are excluded by default.",
9
+ inputSchema: {
10
+ include_trashed: z
11
+ .boolean()
12
+ .optional()
13
+ .default(false)
14
+ .describe("Include trashed (deleted) collections. Default false."),
15
+ },
9
16
  };
10
- export async function handleGetCollections(zoteroApi, userId, _args) {
17
+ const GetCollectionsSchema = z.object(toolConfig.inputSchema);
18
+ export async function handleGetCollections(zoteroApi, userId, args) {
19
+ const { include_trashed } = GetCollectionsSchema.parse(args);
11
20
  try {
12
- const { items: collections } = await fetchAllPages((params) => zoteroApi.library("user", userId).collections().get(params));
13
- if (!Array.isArray(collections) || collections.length === 0) {
21
+ const { items: allCollections } = await fetchAllPages((params) => zoteroApi.library("user", userId).collections().get(params));
22
+ if (!Array.isArray(allCollections) || allCollections.length === 0) {
14
23
  return formatErrorResponse("No collections found", {
15
24
  suggestion: "Create a collection in your Zotero library first",
16
25
  helpUrl: "https://www.zotero.org/support/collections",
17
26
  });
18
27
  }
28
+ // Zotero API returns deleted collections despite not requesting them —
29
+ // filter client-side as a workaround
30
+ const collections = include_trashed
31
+ ? allCollections
32
+ : allCollections.filter((c) => !c.deleted);
33
+ if (collections.length === 0) {
34
+ return formatErrorResponse("No collections found", {
35
+ suggestion: "All collections are in the trash. Use include_trashed=true to see them.",
36
+ });
37
+ }
19
38
  return {
20
39
  content: [
21
40
  { type: "text", text: JSON.stringify(collections, null, 2) },
@@ -5,7 +5,7 @@ import { formatCreators } from "../utils/item-formatter.js";
5
5
  import { logger } from "../utils/logger.js";
6
6
  export const toolConfig = {
7
7
  name: "get_items_details",
8
- description: "Get metadata for multiple Zotero items in a single call. Accepts an array of item keys and returns a map of key → metadata. Use this instead of calling get_item_details multiple times. Returns title, authors, date, DOI, item type, publication title, and URL for each item. Set include_abstract to include abstracts (excluded by default to keep responses lightweight).",
8
+ description: "Get metadata for multiple Zotero items in a single call. Accepts an array of item keys and returns a map of key → metadata. Use this instead of calling get_item_details multiple times. Returns all type-specific fields (e.g. bookTitle for bookSection, proceedingsTitle for conferencePaper, university for thesis). Set include_abstract to include abstracts (excluded by default to keep responses lightweight).",
9
9
  inputSchema: {
10
10
  item_keys: z
11
11
  .array(z.string())
@@ -18,6 +18,12 @@ export const toolConfig = {
18
18
  },
19
19
  };
20
20
  const GetItemsDetailsSchema = z.object(toolConfig.inputSchema);
21
+ /** Fields excluded from the response (structural/internal, not bibliographic metadata). */
22
+ const SKIP_FIELDS = new Set([
23
+ "key", "version", "dateAdded", "dateModified",
24
+ "collections", "tags", "relations", "creators",
25
+ "abstractNote",
26
+ ]);
21
27
  export async function handleGetItemsDetails(zoteroApi, userId, args) {
22
28
  const { item_keys, include_abstract } = GetItemsDetailsSchema.parse(args);
23
29
  if (item_keys.length === 0) {
@@ -40,18 +46,26 @@ export async function handleGetItemsDetails(zoteroApi, userId, args) {
40
46
  const key = item.key;
41
47
  if (!key)
42
48
  continue;
43
- result[key] = {
49
+ const entry = {
50
+ itemType: item.itemType || "document",
44
51
  title: item.title || "Untitled",
45
52
  authors: formatCreators(item.creators),
46
- date: item.date || "No date",
47
- doi: item.DOI || null,
48
- itemType: item.itemType || "document",
49
- publicationTitle: item.publicationTitle || null,
50
- url: item.url || null,
51
53
  };
54
+ // Include all non-empty bibliographic fields from the Zotero response
55
+ const raw = item;
56
+ for (const [field, value] of Object.entries(raw)) {
57
+ if (SKIP_FIELDS.has(field))
58
+ continue;
59
+ if (field in entry)
60
+ continue;
61
+ if (value === undefined || value === null || value === "" || value === false)
62
+ continue;
63
+ entry[field] = value;
64
+ }
52
65
  if (include_abstract && item.abstractNote) {
53
- result[key].abstractNote = item.abstractNote;
66
+ entry.abstractNote = item.abstractNote;
54
67
  }
68
+ result[key] = entry;
55
69
  }
56
70
  return {
57
71
  content: [
@@ -1,4 +1,6 @@
1
+ import { ITEM_TYPE_FIELDS } from "./zotero-item-types.js";
1
2
  const CSL_TO_ZOTERO_TYPE = {
3
+ // Standard CSL types
2
4
  "article": "journalArticle",
3
5
  "article-journal": "journalArticle",
4
6
  "article-magazine": "magazineArticle",
@@ -34,6 +36,23 @@ const CSL_TO_ZOTERO_TYPE = {
34
36
  "standard": "standard",
35
37
  "thesis": "thesis",
36
38
  "webpage": "webpage",
39
+ // Non-standard types returned by CrossRef/DataCite DOI content negotiation
40
+ "journal-article": "journalArticle",
41
+ "book-chapter": "bookSection",
42
+ "proceedings-article": "conferencePaper",
43
+ "posted-content": "preprint",
44
+ "dissertation": "thesis",
45
+ "monograph": "book",
46
+ "reference-entry": "encyclopediaArticle",
47
+ "component": "document",
48
+ "peer-review": "journalArticle",
49
+ "edited-book": "book",
50
+ "reference-book": "book",
51
+ "book-series": "book",
52
+ "book-part": "bookSection",
53
+ "proceedings": "book",
54
+ "journal": "journalArticle",
55
+ "report-series": "report",
37
56
  };
38
57
  const ZOTERO_TO_CSL_TYPE = {
39
58
  journalArticle: "article-journal",
@@ -74,7 +93,27 @@ const ZOTERO_TO_CSL_TYPE = {
74
93
  email: "personal_communication",
75
94
  instantMessage: "personal_communication",
76
95
  };
96
+ /**
97
+ * Maps CSL `container-title` to the correct Zotero field name per item type.
98
+ * Types not listed here have no container-title equivalent.
99
+ */
100
+ const CONTAINER_TITLE_FIELD = {
101
+ journalArticle: "publicationTitle",
102
+ magazineArticle: "publicationTitle",
103
+ newspaperArticle: "publicationTitle",
104
+ bookSection: "bookTitle",
105
+ dictionaryEntry: "dictionaryTitle",
106
+ conferencePaper: "proceedingsTitle",
107
+ blogPost: "blogTitle",
108
+ encyclopediaArticle: "encyclopediaTitle",
109
+ forumPost: "forumTitle",
110
+ webpage: "websiteTitle",
111
+ radioBroadcast: "programTitle",
112
+ tvBroadcast: "programTitle",
113
+ podcast: "programTitle",
114
+ };
77
115
  export function cslToZoteroItem(csl, options) {
116
+ const itemType = (CSL_TO_ZOTERO_TYPE[csl.type] ?? "journalArticle");
78
117
  const creators = (csl.author ?? []).map((a) => ({
79
118
  firstName: a.given ?? "",
80
119
  lastName: a.family ?? a.literal ?? "",
@@ -87,13 +126,11 @@ export function cslToZoteroItem(csl, options) {
87
126
  collections.push(options.collectionKey);
88
127
  }
89
128
  const tags = (options?.tags ?? []).map((t) => ({ tag: t }));
90
- return {
91
- itemType: CSL_TO_ZOTERO_TYPE[csl.type] ?? "journalArticle",
129
+ // Build candidate fields (CSL → Zotero field names)
130
+ const candidates = {
92
131
  title: csl.title ?? "",
93
- creators,
94
132
  date,
95
133
  DOI: csl.DOI ?? "",
96
- publicationTitle: csl["container-title"] ?? "",
97
134
  volume: csl.volume ?? "",
98
135
  issue: csl.issue ?? "",
99
136
  pages: csl.page ?? "",
@@ -107,9 +144,29 @@ export function cslToZoteroItem(csl, options) {
107
144
  numPages: csl["number-of-pages"] ?? "",
108
145
  series: csl["collection-title"] ?? "",
109
146
  language: csl.language ?? "",
110
- collections,
111
- tags,
112
147
  };
148
+ // Map container-title to the correct field for this item type
149
+ const containerTitleField = CONTAINER_TITLE_FIELD[itemType];
150
+ if (containerTitleField) {
151
+ candidates[containerTitleField] = csl["container-title"] ?? "";
152
+ }
153
+ // Filter: only include fields valid for this item type
154
+ const validFields = ITEM_TYPE_FIELDS[itemType];
155
+ const result = { itemType, creators, collections, tags };
156
+ if (validFields) {
157
+ for (const [field, value] of Object.entries(candidates)) {
158
+ if (validFields.has(field)) {
159
+ result[field] = value;
160
+ }
161
+ }
162
+ }
163
+ else {
164
+ // Unknown item type — include all fields as fallback
165
+ for (const [field, value] of Object.entries(candidates)) {
166
+ result[field] = value;
167
+ }
168
+ }
169
+ return result;
113
170
  }
114
171
  export function zoteroItemToCsl(item) {
115
172
  const authors = (item.creators ?? [])
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xevos117/mcp-zotero",
3
3
  "type": "module",
4
- "version": "1.0.7",
4
+ "version": "1.0.8",
5
5
  "description": "Unofficial MCP server for Zotero integration with citation management",
6
6
  "repository": {
7
7
  "type": "git",