Xano MCP Server for Smithery

by roboulos
Verified
#!/usr/bin/env python3 """ Xano MCP Server - Model Context Protocol server for Xano database integration Compatible with Smithery for AI agent integration """ from typing import Any, Dict, List, Optional, Union, BinaryIO import os import sys import json import asyncio import argparse import logging import httpx from mcp.server.fastmcp import FastMCP # Initialize FastMCP server mcp = FastMCP("xano") # Constants XANO_GLOBAL_API = "https://app.xano.com/api:meta" # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger('xano-mcp') # Extract token from environment or config def get_token(config=None): """Get the Xano API token from environment, config, or arguments""" # Check config first (for Smithery integration) if config and 'api_token' in config: return config['api_token'] # Check environment variable token = os.environ.get("XANO_API_TOKEN") if token: return token # If no token found, print error and exit logger.error("Error: Xano API token not provided.") logger.error("Either set XANO_API_TOKEN environment variable or provide it in config") sys.exit(1) # Utility function to make API requests async def make_api_request( url, headers, method="GET", data=None, params=None, files=None, debug=False ): """Helper function to make API requests with consistent error handling""" try: if debug: logger.info(f"Making {method} request to {url}") if params: logger.info(f"With params: {params}") if data and not files: logger.info(f"With data: {json.dumps(data)[:500]}...") async with httpx.AsyncClient() as client: if method == "GET": response = await client.get(url, headers=headers, params=params) elif method == "POST": if files: # For multipart/form-data with file uploads response = await client.post( url, headers=headers, data=data, files=files ) else: response = await client.post(url, headers=headers, json=data) elif method == "PUT": response = await client.put(url, headers=headers, json=data) elif method == "DELETE": if data: response = await client.delete(url, headers=headers, json=data) else: response = await client.delete(url, headers=headers) elif method == "PATCH": response = await client.patch(url, headers=headers, json=data) else: raise ValueError(f"Unsupported method: {method}") if debug: logger.info(f"Response status: {response.status_code}") if response.status_code == 200: try: return response.json() except json.JSONDecodeError: if debug: logger.error(f"Error parsing JSON response: {response.text[:200]}...") return {"error": "Failed to parse response as JSON"} else: if debug: logger.error(f"Error response: {response.text[:200]}...") return { "error": f"API request failed with status {response.status_code}" } except Exception as e: if debug: logger.error(f"Exception during API request: {str(e)}") return {"error": f"Exception during API request: {str(e)}"} # Utility function to ensure IDs are properly formatted as strings def format_id(id_value): """Ensures IDs are properly formatted strings""" if id_value is None: return None return str(id_value).strip('"') ############################################## # SECTION: INSTANCE AND DATABASE OPERATIONS ############################################## @mcp.tool() async def xano_list_instances(config=None) -> Dict[str, Any]: """List all Xano instances associated with the account.""" token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } # First try the direct auth/me endpoint result = await make_api_request(f"{XANO_GLOBAL_API}/auth/me", headers, debug=debug) if "error" not in result and "instances" in result: return {"instances": result["instances"]} # If that doesn't work, perform a workaround - list any known instances # This is a fallback for when the API doesn't return instances directly if debug: logger.info("Falling back to hardcoded instance detection...") instances = [ { "name": "xnwv-v1z6-dvnr", "display": "Robert", "xano_domain": "xnwv-v1z6-dvnr.n7c.xano.io", "rate_limit": False, "meta_api": "https://xnwv-v1z6-dvnr.n7c.xano.io/api:meta", "meta_swagger": "https://xnwv-v1z6-dvnr.n7c.xano.io/apispec:meta?type=json", } ] return {"instances": instances} @mcp.tool() async def xano_get_instance_details(instance_name: str, config=None) -> Dict[str, Any]: """Get details for a specific Xano instance. Args: instance_name: The name of the Xano instance """ # Construct the instance details without making an API call instance_domain = f"{instance_name}.n7c.xano.io" return { "name": instance_name, "display": instance_name.split("-")[0].upper(), "xano_domain": instance_domain, "rate_limit": False, "meta_api": f"https://{instance_domain}/api:meta", "meta_swagger": f"https://{instance_domain}/apispec:meta?type=json", } @mcp.tool() async def xano_list_databases(instance_name: str, config=None) -> Dict[str, Any]: """List all databases (workspaces) in a specific Xano instance. Args: instance_name: The name of the Xano instance """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" # Get the workspaces url = f"{meta_api}/workspace" if debug: logger.info(f"Listing databases from URL: {url}") result = await make_api_request(url, headers, debug=debug) if "error" in result: return result return {"databases": result} @mcp.tool() async def xano_get_workspace_details( instance_name: str, workspace_id: str, config=None ) -> Dict[str, Any]: """Get details for a specific Xano workspace. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) url = f"{meta_api}/workspace/{workspace_id}" if debug: logger.info(f"Getting workspace details from URL: {url}") return await make_api_request(url, headers, debug=debug) ############################################## # SECTION: TABLE OPERATIONS ############################################## @mcp.tool() async def xano_list_tables(instance_name: str, database_name: str, config=None) -> Dict[str, Any]: """List all tables in a specific Xano database (workspace). Args: instance_name: The name of the Xano instance database_name: The ID of the Xano workspace (database) """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" database_name = format_id(database_name) url = f"{meta_api}/workspace/{database_name}/table" if debug: logger.info(f"Listing tables from URL: {url}") result = await make_api_request(url, headers, debug=debug) if "error" in result: return result return {"tables": result} @mcp.tool() async def xano_get_table_details( instance_name: str, workspace_id: str, table_id: str, config=None ) -> Dict[str, Any]: """Get details for a specific Xano table. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace table_id: The ID of the table """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) table_id = format_id(table_id) url = f"{meta_api}/workspace/{workspace_id}/table/{table_id}" if debug: logger.info(f"Getting table details from URL: {url}") return await make_api_request(url, headers, debug=debug) ############################################## # SECTION: TABLE CONTENT OPERATIONS ############################################## @mcp.tool() async def xano_browse_table_content( instance_name: str, workspace_id: str, table_id: str, page: int = 1, per_page: int = 50, config=None ) -> Dict[str, Any]: """Browse content for a specific Xano table. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace table_id: The ID of the table page: Page number (default: 1) per_page: Number of records per page (default: 50) """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) table_id = format_id(table_id) # Prepare params params = {"page": page, "per_page": per_page} url = f"{meta_api}/workspace/{workspace_id}/table/{table_id}/content" if debug: logger.info(f"Browsing table content from URL: {url}") return await make_api_request(url, headers, params=params, debug=debug) @mcp.tool() async def xano_search_table_content( instance_name: str, workspace_id: str, table_id: str, search_conditions: List[Dict[str, Any]] = None, sort: Dict[str, str] = None, page: int = 1, per_page: int = 50, config=None ) -> Dict[str, Any]: """Search table content using complex filtering. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace table_id: The ID of the table search_conditions: List of search conditions sort: Dictionary with field names as keys and "asc" or "desc" as values page: Page number (default: 1) per_page: Number of records per page (default: 50) """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) table_id = format_id(table_id) # Prepare the search data data = {"page": page, "per_page": per_page} if search_conditions: data["search"] = search_conditions if sort: data["sort"] = sort url = f"{meta_api}/workspace/{workspace_id}/table/{table_id}/content/search" if debug: logger.info(f"Searching table content at URL: {url}") return await make_api_request(url, headers, method="POST", data=data, debug=debug) @mcp.tool() async def xano_get_table_record( instance_name: str, workspace_id: str, table_id: str, record_id: str, config=None ) -> Dict[str, Any]: """Get a specific record from a table. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace table_id: The ID of the table record_id: The ID of the record to retrieve """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) table_id = format_id(table_id) record_id = format_id(record_id) url = f"{meta_api}/workspace/{workspace_id}/table/{table_id}/content/{record_id}" if debug: logger.info(f"Getting table record from URL: {url}") return await make_api_request(url, headers, debug=debug) @mcp.tool() async def xano_create_table_record( instance_name: str, workspace_id: str, table_id: str, record_data: Dict[str, Any], config=None ) -> Dict[str, Any]: """Create a new record in a table. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace table_id: The ID of the table record_data: The data for the new record """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) table_id = format_id(table_id) url = f"{meta_api}/workspace/{workspace_id}/table/{table_id}/content" if debug: logger.info(f"Creating table record at URL: {url}") return await make_api_request(url, headers, method="POST", data=record_data, debug=debug) @mcp.tool() async def xano_update_table_record( instance_name: str, workspace_id: str, table_id: str, record_id: str, record_data: Dict[str, Any], config=None ) -> Dict[str, Any]: """Update an existing record in a table. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace table_id: The ID of the table record_id: The ID of the record to update record_data: The updated data for the record """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) table_id = format_id(table_id) record_id = format_id(record_id) url = f"{meta_api}/workspace/{workspace_id}/table/{table_id}/content/{record_id}" if debug: logger.info(f"Updating table record at URL: {url}") return await make_api_request(url, headers, method="PUT", data=record_data, debug=debug) @mcp.tool() async def xano_delete_table_record( instance_name: str, workspace_id: str, table_id: str, record_id: str, config=None ) -> Dict[str, Any]: """Delete a specific record from a table. Args: instance_name: The name of the Xano instance workspace_id: The ID of the workspace table_id: The ID of the table record_id: The ID of the record to delete """ token = get_token(config) debug = config.get('debug', False) if config else False headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } instance_domain = f"{instance_name}.n7c.xano.io" meta_api = f"https://{instance_domain}/api:meta" workspace_id = format_id(workspace_id) table_id = format_id(table_id) record_id = format_id(record_id) url = f"{meta_api}/workspace/{workspace_id}/table/{table_id}/content/{record_id}" if debug: logger.info(f"Deleting table record at URL: {url}") return await make_api_request(url, headers, method="DELETE", debug=debug) async def run_mcp_server(transport="stdio", host="0.0.0.0", port=8000, config=None): """Run the MCP server with the specified transport Args: transport: Transport type ("stdio" or "websocket") host: Host to bind to for websocket transport port: Port to bind to for websocket transport config: Configuration dictionary """ if transport == "websocket": logger.info(f"Starting Xano MCP server with WebSocket transport on {host}:{port}...") await mcp.run_websocket(host=host, port=port, config=config) else: # Default to stdio logger.info("Starting Xano MCP server with stdio transport...") await mcp.run(transport="stdio", config=config) def main(): """Main entry point with argument parsing""" parser = argparse.ArgumentParser(description="Xano MCP Server") parser.add_argument( "--transport", choices=["stdio", "websocket"], default="stdio", help="Transport method (stdio or websocket)" ) parser.add_argument( "--host", default="0.0.0.0", help="Host to bind to for websocket transport" ) parser.add_argument( "--port", type=int, default=8000, help="Port to bind to for websocket transport" ) parser.add_argument( "--token", help="Xano API token" ) parser.add_argument( "--debug", action="store_true", help="Enable debug logging" ) args = parser.parse_args() # Set up logging level based on debug flag if args.debug: logger.setLevel(logging.DEBUG) # Set token in environment if provided if args.token: os.environ["XANO_API_TOKEN"] = args.token # Create config dict config = { "debug": args.debug } # Run the server asyncio.run(run_mcp_server( transport=args.transport, host=args.host, port=args.port, config=config )) if __name__ == "__main__": main()