github-manager MCP Server

from __future__ import annotations from typing import Optional, List, Dict, Any import os import json from pyzotero import zotero from mcp.server.fastmcp import FastMCP, Context mcp = FastMCP("Zotero", dependencies=["pyzotero", "mcp[cli]"]) class ZoteroWrapper(zotero.Zotero): """Wrapper for pyzotero client with error handling""" def __init__(self): try: user_id = os.getenv('ZOTERO_USER_ID') if user_id is None: user_id = 0 super().__init__(1, 'user', '', local=True) #FIXME: Work around a bug #202 in pyzotero. self.library_id = user_id except Exception as e: return json.dumps({ "error": "Failed to initialize Zotero connection.", "message": str(e) }, indent=2) def format_creators(self, creators: List[Dict[str, str]]) -> str: """Format creator names into a string""" names = [] for creator in creators: name_parts = [] if creator.get('firstName'): name_parts.append(creator['firstName']) if creator.get('lastName'): name_parts.append(creator['lastName']) if name_parts: names.append(' '.join(name_parts)) return ', '.join(names) or "No authors listed" def format_item(self, item: Dict[str, Any], include_abstract: bool = True) -> Dict[str, Any]: """Format a Zotero item into a standardized dictionary""" data = item.get('data', {}) formatted = { 'title': data.get('title', 'Untitled'), 'authors': self.format_creators(data.get('creators', [])), 'date': data.get('date', 'No date'), 'key': data.get('key'), 'itemType': data.get('itemType', 'Unknown type'), } if include_abstract: formatted['abstractNote'] = data.get('abstractNote', 'No abstract available') if 'DOI' in data: formatted['doi'] = data['DOI'] if 'url' in data: formatted['url'] = data['url'] if 'publicationTitle' in data: formatted['publicationTitle'] = data['publicationTitle'] if 'tags' in data: formatted['tags'] = [t.get('tag') for t in data.get('tags', []) if t.get('tag')] return formatted @mcp.tool() def get_collections(*, ctx: Context) -> str: """List all collections in your Zotero library""" try: client = ZoteroWrapper() collections = client.collections() return json.dumps(collections, indent=2) except Exception as e: ctx.error(f"Failed to fetch collections. Message: {str(e)}") return @mcp.tool() def get_collection_items(collection_key: str, *, ctx: Context) -> str: """ Get all items in a specific collection Args: collection_key: The collection key/ID """ try: client = ZoteroWrapper() items = client.collection_items(collection_key) if not items: return json.dumps({ "error": "Collection is empty", "collection_key": collection_key, "suggestion": "Add some items to this collection in Zotero" }, indent=2) formatted_items = [client.format_item(item) for item in items] return json.dumps(formatted_items, indent=2) except Exception as e: ctx.error(f"Failed to fetch collection items {collection_key}. Message: {str(e)}") return @mcp.tool() def get_item_details(item_key: str, *, ctx: Context) -> str: """ Get detailed information about a specific paper Args: item_key: The paper's item key/ID """ try: client = ZoteroWrapper() item = client.item(item_key) if not item: return json.dumps({ "error": "Item not found", "item_key": item_key, "suggestion": "Verify the item exists and you have permission to access it" }, indent=2) formatted_item = client.format_item(item, include_abstract=True) return json.dumps(formatted_item, indent=2) except Exception as e: ctx.error(f"Failed to fetch item details {item_key}. Message: {str(e)}") return @mcp.tool() def search_library(query: str, *, ctx: Context) -> str: """ Search your entire Zotero library Args: query: Search query """ if not query.strip(): return json.dumps({ "error": "Search query is required" }, indent=2) try: client = ZoteroWrapper() items = client.items(q=query) if not items: return json.dumps({ "error": "No results found", "query": query, "suggestion": "Try a different search term or verify your library contains matching items" }, indent=2) formatted_items = [client.format_item(item, include_abstract=False) for item in items] return json.dumps(formatted_items, indent=2) except Exception as e: ctx.error(f"Search failed ({query}). Message: {str(e)}") return @mcp.tool() def get_recent(limit: Optional[int] = 10, *, ctx: Context) -> str: """ Get recently added papers to your library Args: limit: Number of papers to return (default 10) """ try: client = ZoteroWrapper() items = client.items(limit=min(limit or 10, 100), sort='dateAdded', direction='desc') if not items: return json.dumps({ "error": "No recent items found", "suggestion": "Add some items to your Zotero library first" }, indent=2) formatted_items = [client.format_item(item, include_abstract=False) for item in items] return json.dumps(formatted_items, indent=2) except Exception as e: ctx.error(f"Failed to fetch recent items. Message: {str(e)}") return if __name__ == "__main__": # Initialize and run the server mcp.run(transport='stdio')