Skip to main content
Glama
kltng

LCSH MCP Server

by kltng
server.py8.91 kB
from mcp.server.fastmcp import FastMCP import requests import traceback mcp = FastMCP("cataloger mcp server") @ mcp.tool() def search_lcsh(query: str) -> dict: """ Search Library of Congress Subject Headings (LCSH) using the public suggest2 API. Returns a dictionary with the top results. """ # Construct the API endpoint for LCSH subject headings url = "https://id.loc.gov/authorities/subjects/suggest2" params = {"q": query, "count": 25} headers = {"User-Agent": "cataloger mcp server/1.0 (contact: your-email@example.com)"} try: response = requests.get(url, params=params, headers=headers, timeout=10) response.raise_for_status() # Try to parse JSON, but handle unexpected formats robustly try: data = response.json() except Exception as json_err: return { "error": f"Failed to parse JSON: {json_err}", "raw_response": response.text, "type": type(json_err).__name__, "traceback": traceback.format_exc() } # Handle new API response format (dict with 'hits') if isinstance(data, dict) and 'hits' in data: results = [] for hit in data['hits']: label = hit.get('aLabel') or hit.get('label') or '' uri = hit.get('uri') or '' results.append({"label": label, "uri": uri}) return {"results": results} # Old format (list with ids/labels) if isinstance(data, list) and len(data) >= 3: results = [] for uri, label in zip(data[1], data[2]): results.append({"label": label, "uri": uri}) return {"results": results} else: return { "error": "Unexpected API response format", "data": data } except Exception as e: return { "error": str(e), "type": type(e).__name__, "traceback": traceback.format_exc() } @ mcp.tool() def search_lcsh_keyword(query: str) -> dict: """ Search Library of Congress Subject Headings (LCSH) using the public suggest2 API with keyword search. Returns a dictionary with the top results. """ # Construct the API endpoint for LCSH subject headings url = "https://id.loc.gov/authorities/subjects/suggest2" params = {"q": query, "searchtype": "keyword", "count": 50} headers = {"User-Agent": "cataloger mcp server/1.0 (contact: your-email@example.com)"} try: response = requests.get(url, params=params, headers=headers, timeout=10) response.raise_for_status() # Try to parse JSON, but handle unexpected formats robustly try: data = response.json() except Exception as json_err: return { "error": f"Failed to parse JSON: {json_err}", "raw_response": response.text, "type": type(json_err).__name__, "traceback": traceback.format_exc() } # Handle new API response format (dict with 'hits') if isinstance(data, dict) and 'hits' in data: results = [] for hit in data['hits']: label = hit.get('aLabel') or hit.get('label') or '' uri = hit.get('uri') or '' results.append({"label": label, "uri": uri}) return {"results": results} # Old format (list with ids/labels) if isinstance(data, list) and len(data) >= 3: results = [] for uri, label in zip(data[1], data[2]): results.append({"label": label, "uri": uri}) return {"results": results} else: return { "error": "Unexpected API response format", "data": data } except Exception as e: return { "error": str(e), "type": type(e).__name__, "traceback": traceback.format_exc() } @mcp.tool() def search_name_authority(query: str) -> dict: """ Search Library of Congress Name Authorities (LCNAF) using the public suggest2 API. Specifically targets Personal Names. Returns a dictionary with the top results. """ # Construct the API endpoint for LCNAF (Personal Names) url = "https://id.loc.gov/authorities/names/suggest2" params = {"q": query, "rdftype": "PersonalName", "count": 25} headers = {"User-Agent": "cataloger mcp server/1.0 (contact: your-email@example.com)"} try: response = requests.get(url, params=params, headers=headers, timeout=10) response.raise_for_status() # Try to parse JSON, but handle unexpected formats robustly try: data = response.json() except Exception as json_err: return { "error": f"Failed to parse JSON: {json_err}", "raw_response": response.text, "type": type(json_err).__name__, "traceback": traceback.format_exc() } # Handle API response format (dict with 'hits') - primary expected format for Suggest2 if isinstance(data, dict) and 'hits' in data: results = [] for hit in data['hits']: label = hit.get('aLabel') or hit.get('label') or '' uri = hit.get('uri') or '' results.append({"label": label, "uri": uri}) return {"results": results} # Fallback for other potential list-based formats elif isinstance(data, list) and len(data) > 0: # If it's a list of dicts (like 'hits' but without the top-level 'hits' key) if isinstance(data[0], dict) and ('aLabel' in data[0] or 'label' in data[0]) and 'uri' in data[0]: results = [] for hit in data: # Assuming each item in the list is a hit label = hit.get('aLabel') or hit.get('label') or '' uri = hit.get('uri') or '' results.append({"label": label, "uri": uri}) return {"results": results} # If it's the [query, [labels], [uris]] structure (more like 'Suggest' API) elif len(data) >= 3 and isinstance(data[1], list) and isinstance(data[2], list): results = [] # Ensure data[1] (labels) and data[2] (uris) are lists of same length if len(data[1]) == len(data[2]): for label_item, uri_item in zip(data[1], data[2]): label = str(label_item) if label_item is not None else '' uri = str(uri_item) if uri_item is not None else '' results.append({"label": label, "uri": uri}) return {"results": results} else: # Mismatched lengths in labels/URIs lists return { "error": "Mismatch in lengths of label and URI lists in API response", "data": data } else: # Unrecognized list format return { "error": "Unexpected list-based API response format for name authority search", "data": data } else: # Neither 'hits' dict nor a recognized list format return { "error": "Unexpected API response format for name authority search", "data": data } except Exception as e: return { "error": str(e), "type": type(e).__name__, "traceback": traceback.format_exc() } # Optionally, add a resource for the new tool @mcp.resource("lcnaf://search/{query}") def lcnaf_resource(query: str) -> dict: return search_name_authority(query) # Optionally, add a resource or prompt for demonstration @mcp.resource("lcsh://search/{query}") def lcsh_resource(query: str) -> dict: return search_lcsh(query) def start_mcp_server(port: int = None): """Starts the MCP server, either in HTTP/SSE mode or stdio mode.""" import uvicorn if port is not None: # Run as HTTP/SSE server print(f"Starting cataloger mcp server on HTTP port {port}") uvicorn.run(mcp.sse_app(), host="0.0.0.0", port=port) else: # Run in stdio mode (default) print("Starting cataloger mcp server in stdio mode") mcp.run() if __name__ == "__main__": # This allows running server.py directly for testing if needed, # though the primary entry point is via cli.py. # For direct execution, it will default to stdio mode unless a port is passed as a CLI arg. import sys cli_port = None if len(sys.argv) > 1 and sys.argv[1].isdigit(): cli_port = int(sys.argv[1]) start_mcp_server(port=cli_port)

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kltng/lcsh-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server