Skip to main content
Glama
server.py6.21 kB
"""Main MCP server implementation for ServiceNow.""" import asyncio import logging from typing import Any, Optional import click from mcp.server import Server from mcp.server.models import InitializationOptions from mcp.server.stdio import stdio_server from mcp.types import ( TextContent, Tool, ) from .client import ServiceNowClient from .config import Config, ConfigManager from .exceptions import ServiceNowError from .tools import ToolRegistry class ServiceNowMCPServer: """MCP Server for ServiceNow API integration.""" def __init__(self, config: Config): """Initialize the ServiceNow MCP Server.""" self.config = config self.client = ServiceNowClient(config.servicenow) self.server = Server(config.mcp.name) # type: ignore[var-annotated] self.tools = ToolRegistry(config.features) # Setup logging self._setup_logging() # Register handlers self._register_handlers() def _setup_logging(self) -> None: """Configure logging based on configuration.""" log_level = getattr(logging, self.config.logging.level.upper(), logging.INFO) formatter: logging.Formatter if self.config.logging.format == "json": import json class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: log_data = { "timestamp": self.formatTime(record), "level": record.levelname, "message": record.getMessage(), "module": record.module, } if record.exc_info: log_data["exception"] = self.formatException(record.exc_info) return json.dumps(log_data) formatter = JsonFormatter() else: formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger("servicenow_mcp") logger.setLevel(log_level) # Console handler console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) logger.addHandler(console_handler) # File handler if configured if self.config.logging.file: file_handler = logging.FileHandler(self.config.logging.file) file_handler.setFormatter(formatter) logger.addHandler(file_handler) self.logger = logger def _register_handlers(self) -> None: """Register MCP protocol handlers.""" @self.server.list_tools() # type: ignore[no-untyped-call,misc] async def list_tools() -> list[Tool]: """List all available tools.""" return self.tools.get_enabled_tools() @self.server.call_tool() # type: ignore[no-untyped-call,misc] async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: """Execute a tool and return results.""" try: self.logger.info(f"Executing tool: {name} with args: {arguments}") # Get tool handler handler = self.tools.get_handler(name) if not handler: return [ TextContent(type="text", text=f"Error: Unknown tool '{name}'") ] # Execute tool async with self.client: result = await handler(self.client, arguments) # Format result if isinstance(result, str): return [TextContent(type="text", text=result)] elif isinstance(result, (dict, list)): import json return [TextContent(type="text", text=json.dumps(result, indent=2))] else: return [TextContent(type="text", text=str(result))] except ServiceNowError as e: self.logger.error(f"ServiceNow error in tool {name}: {e}") return [TextContent(type="text", text=f"ServiceNow Error: {e!s}")] except Exception as e: self.logger.exception(f"Unexpected error in tool {name}") return [TextContent(type="text", text=f"Unexpected Error: {e!s}")] async def run(self) -> None: """Run the MCP server.""" self.logger.info(f"Starting ServiceNow MCP Server v{self.config.mcp.version}") self.logger.info(f"Connected to instance: {self.config.servicenow.instance}") # Run the stdio server async with stdio_server() as (read_stream, write_stream): init_options = InitializationOptions( server_name=self.config.mcp.name, server_version=self.config.mcp.version, capabilities=self.server.get_capabilities( notification_options=None, # type: ignore[arg-type] experimental_capabilities={}, ), ) await self.server.run( read_stream, write_stream, init_options, ) @click.command() @click.option( "--config-dir", type=click.Path(exists=True, file_okay=False, dir_okay=True), default="config", help="Configuration directory path", ) @click.option( "--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), help="Override log level", ) def main(config_dir: str, log_level: Optional[str]) -> None: """Run the ServiceNow MCP Server.""" # Load configuration from pathlib import Path config_manager = ConfigManager(Path(config_dir)) config = config_manager.load() # Override log level if provided if log_level: config.logging.level = log_level # Create and run server server = ServiceNowMCPServer(config) try: asyncio.run(server.run()) except KeyboardInterrupt: server.logger.info("Server stopped by user") except Exception as e: server.logger.exception(f"Server crashed: {e}") raise if __name__ == "__main__": main()

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/LokiMCPUniverse/servicenow-mcp-server'

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