MCP Weather Server

  • src
import "dotenv/config"; // Add this at the top of the file import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { ZoteroCreator, ZoteroTag, ZoteroItem } from "./types/zotero-types.js"; import { z } from "zod"; import { createRequire } from "module"; // At the very top of the file after imports console.error("Starting MCP Zotero Server..."); // Validation schemas const GetCollectionItemsSchema = z.object({ collectionKey: z.string(), }); const GetItemDetailsSchema = z.object({ itemKey: z.string(), }); const SearchLibrarySchema = z.object({ query: z.string(), }); const GetRecentSchema = z.object({ limit: z.number().optional().default(10), }); class ZoteroServer { private server: Server; private zoteroApi: any; private userId: string; private apiKey: string; constructor() { console.error("Initializing ZoteroServer..."); this.server = new Server( { name: "zotero", version: "1.0.0" }, { capabilities: { tools: {} } } ); console.error("MCP Server initialized"); this.userId = process.env.ZOTERO_USER_ID || ""; this.apiKey = process.env.ZOTERO_API_KEY || ""; if (!this.apiKey || !this.userId) { throw new Error( "Missing ZOTERO_API_KEY or ZOTERO_USER_ID environment variables" ); } const require = createRequire(import.meta.url); const zoteroApi = require("zotero-api-client/lib/main-node.cjs").default; this.zoteroApi = zoteroApi(this.apiKey); console.error("Zotero API client initialized"); } private async setupHandlers() { console.error("Setting up handlers..."); // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("🛠️ LIST TOOLS: Starting request handler"); const response = { tools: [ { name: "get_collections", description: "List all collections in your Zotero library", inputSchema: { type: "object", properties: {}, }, }, { name: "get_collection_items", description: "Get all items in a specific collection", inputSchema: { type: "object", properties: { collectionKey: { type: "string", description: "The collection key/ID", }, }, required: ["collectionKey"], }, }, { name: "get_item_details", description: "Get detailed information about a specific paper", inputSchema: { type: "object", properties: { itemKey: { type: "string", description: "The paper's item key/ID", }, }, required: ["itemKey"], }, }, { name: "search_library", description: "Search your entire Zotero library", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query", }, }, required: ["query"], }, }, { name: "get_recent", description: "Get recently added papers to your library", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Number of papers to return (default 10)", }, }, }, }, ], }; console.error("📝 LIST TOOLS: Sending response"); return response; }); // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async (request) => { console.error( `[DEBUG] Received tool call request: ${request.params.name}` ); console.error(`[DEBUG] Tool arguments:`, request.params.arguments); const { name, arguments: args } = request.params; try { switch (name) { case "get_collections": { console.error( `[DEBUG] GET_COLLECTIONS: Starting with userId ${this.userId}` ); try { // Test API connection first console.error( `[DEBUG] GET_COLLECTIONS: Testing API connection...` ); const response = await this.zoteroApi .library("user", this.userId) .collections() .get(); const collections = response.getData(); console.error( `[DEBUG] GET_COLLECTIONS: Found ${collections.length} collections` ); if (!Array.isArray(collections) || collections.length === 0) { return this.formatErrorResponse("No collections found", { suggestion: "Create a collection in your Zotero library first", helpUrl: "https://www.zotero.org/support/collections", }); } return { content: [ { type: "text", text: JSON.stringify(collections, null, 2) }, ], }; } catch (err) { const error = err as { response?: { status: number; url?: string; }; message: string; }; console.error(`[ERROR] GET_COLLECTIONS: Failed:`, { status: error.response?.status, message: error.message, userId: this.userId, url: error.response?.url, }); throw error; } } case "get_collection_items": { const { collectionKey } = GetCollectionItemsSchema.parse(args); console.error( `[DEBUG] GET_COLLECTION_ITEMS: Fetching items for collection ${collectionKey}` ); try { const response = await this.zoteroApi .library("user", this.userId) .collections(collectionKey) .items() .get(); const items = response.getData(); console.error( `[DEBUG] GET_COLLECTION_ITEMS: Raw response:`, JSON.stringify(items, null, 2) ); if (!items || !Array.isArray(items) || items.length === 0) { return this.formatErrorResponse("Collection is empty", { collectionKey, suggestion: "Add some items to this collection in Zotero", status: "empty", }); } const formatted = items .filter((item) => item) .map((item: any) => ({ title: item.title || "Untitled", authors: item.creators ?.map((c: ZoteroCreator) => `${c.firstName || ""} ${c.lastName || ""}`.trim() ) .filter(Boolean) .join(", ") || "No authors listed", date: item.date || "No date", key: item.key || "No key", itemType: item.itemType || "Unknown type", abstractNote: item.abstractNote || "No abstract available", tags: item.tags?.map((t: ZoteroTag) => t.tag).filter(Boolean) || [], doi: item.DOI || null, url: item.url || null, publicationTitle: item.publicationTitle || null, })); console.error( `[DEBUG] GET_COLLECTION_ITEMS: Formatted ${formatted.length} items` ); if (formatted.length === 0) { return this.formatErrorResponse( "No valid items found in collection", { collectionKey, suggestion: "Check that items in this collection have the expected metadata", status: "invalid_items", } ); } return { content: [ { type: "text", text: JSON.stringify(formatted, null, 2), }, ], }; } catch (err) { const error = err as { response?: { status: number; url?: string; }; message: string; }; if (error.response?.status === 404) { return this.formatErrorResponse( "Collection is empty or not accessible", { collectionKey, suggestion: "Verify the collection exists and try adding some items to it", status: "not_found", } ); } console.error(`[ERROR] GET_COLLECTION_ITEMS: Failed:`, { status: error.response?.status, message: error.message, collectionKey, url: error.response?.url, }); throw error; } } case "get_item_details": { const { itemKey } = GetItemDetailsSchema.parse(args); if (!itemKey?.trim()) { return this.formatErrorResponse("Item key is required"); } try { const response = await this.zoteroApi .library("user", this.userId) .items(itemKey) .get(); const item = response.getData(); console.error( `[DEBUG] GET_ITEM_DETAILS: Raw response:`, JSON.stringify(item, null, 2) ); if (!item) { return this.formatErrorResponse( "Item not found or inaccessible", { itemKey, suggestion: "Verify the item exists and you have permission to access it", } ); } const formatted = { title: item.title || "Untitled", authors: item.creators ?.map((c: ZoteroCreator) => `${c.firstName || ""} ${c.lastName || ""}`.trim() ) .filter(Boolean) .join(", ") || "No authors listed", date: item.date || "No date", abstract: item.abstractNote || "No abstract available", publicationTitle: item.publicationTitle || "No publication title", doi: item.DOI || "No DOI", url: item.url || "No URL", tags: item.tags?.map((t: ZoteroTag) => t.tag) || [], collections: item.collections || [], }; return { content: [ { type: "text", text: JSON.stringify(formatted, null, 2) }, ], }; } catch (err) { const error = err as { response?: { status: number; url?: string; }; message: string; }; console.error(`[ERROR] GET_ITEM_DETAILS: Failed:`, { status: error.response?.status, message: error.message, userId: this.userId, url: error.response?.url, }); throw error; } } case "search_library": { const { query } = SearchLibrarySchema.parse(args); if (!query?.trim()) { return this.formatErrorResponse("Search query is required"); } try { const response = await this.zoteroApi .library("user", this.userId) .items() .get({ q: query }); const items = response.getData(); console.error( `[DEBUG] SEARCH_LIBRARY: Found ${ items?.length || 0 } items for query "${query}"` ); if (!Array.isArray(items) || items.length === 0) { return this.formatErrorResponse("No results found", { query, suggestion: "Try a different search term or verify your library contains matching items", }); } const formatted = items.map((item) => ({ title: item.title || "Untitled", authors: item.creators ?.map((c: ZoteroCreator) => `${c.firstName || ""} ${c.lastName || ""}`.trim() ) .filter(Boolean) .join(", ") || "No authors listed", date: item.date || "No date", key: item.key, itemType: item.itemType, abstractNote: item.abstractNote || "No abstract available", })); return { content: [ { type: "text", text: JSON.stringify(formatted, null, 2) }, ], }; } catch (err) { const error = err as { response?: { status: number; url?: string; }; message: string; }; console.error(`[ERROR] SEARCH_LIBRARY: Failed:`, { status: error.response?.status, message: error.message, userId: this.userId, url: error.response?.url, }); throw error; } } case "get_recent": { const { limit } = GetRecentSchema.parse(args); try { const response = await this.zoteroApi .library("user", this.userId) .items() .get({ sort: "dateAdded", direction: "desc", limit: Math.min(limit || 10, 100), // Cap at 100 items }); const items = response.getData(); console.error( `[DEBUG] GET_RECENT: Found ${items?.length || 0} recent items` ); if (!Array.isArray(items) || items.length === 0) { return this.formatErrorResponse("No recent items found", { suggestion: "Add some items to your Zotero library first", }); } const formatted = items.map((item) => ({ title: item.title || "Untitled", authors: item.creators ?.map((c: ZoteroCreator) => `${c.firstName || ""} ${c.lastName || ""}`.trim() ) .filter(Boolean) .join(", ") || "No authors listed", dateAdded: item.dateAdded || "No date", key: item.key, itemType: item.itemType, })); return { content: [ { type: "text", text: JSON.stringify(formatted, null, 2) }, ], }; } catch (err) { const error = err as { response?: { status: number; url?: string; }; message: string; }; console.error(`[ERROR] GET_RECENT: Failed:`, { status: error.response?.status, message: error.message, userId: this.userId, url: error.response?.url, }); throw error; } } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error(`[ERROR] Tool execution failed:`, error); throw error; } }); console.error("Handlers setup complete"); } async start() { console.error("Starting server..."); await this.setupHandlers(); const transport = new StdioServerTransport(); console.error("Connecting to transport..."); await this.server .connect(transport) .then(() => { console.error("🔌 TRANSPORT: Connection established"); }) .catch((error) => { console.error("❌ TRANSPORT: Connection failed:", error); }); console.error("Zotero MCP Server running on stdio"); } // Add a helper function for consistent error responses private formatErrorResponse(message: string, details: any = {}) { return { content: [ { type: "text", text: JSON.stringify( { error: message, ...details, }, null, 2 ), }, ], }; } } // Start the server async function main() { try { const server = new ZoteroServer(); await server.start(); } catch (error) { console.error("Fatal error:", error); process.exit(1); } } main(); console.error("Server module loaded");