#!/usr/bin/env python3
# src/chuk_mcp_math_server/math_server.py
"""
Mathematical MCP Server - using chuk-mcp-server framework.
"""
import json
import logging
from typing import Dict, Any
from chuk_mcp_server import ChukMCPServer
from .config import ServerConfig
from .function_filter import FunctionFilter
logger = logging.getLogger(__name__)
class ConfigurableMCPMathServer:
"""MCP Math Server with granular control over exposed mathematical functionality."""
def __init__(self, config: ServerConfig):
self.config = config
# Configure logging early
log_level = getattr(logging, config.log_level.upper())
logging.getLogger().setLevel(log_level)
# For stdio mode with WARNING or higher, silence noisy dependencies
if config.transport == "stdio" and log_level >= logging.WARNING:
logging.getLogger("chuk_mcp_math").setLevel(logging.WARNING)
logging.getLogger("chuk_mcp_math_server.function_filter").setLevel(
logging.WARNING
)
logging.getLogger("chuk_mcp_math_server.math_server").setLevel(
logging.WARNING
)
logging.getLogger("chuk_mcp_math_server.cli").setLevel(logging.WARNING)
self.function_filter = FunctionFilter(config)
# Create the underlying chuk-mcp-server instance
self.mcp_server = ChukMCPServer(
name=config.server_name or "chuk-mcp-math-server"
)
# Register all tools and resources
self._register_math_tools()
self._register_math_resources()
# Log math-specific initialization
stats = self.function_filter.get_function_stats()
logger.info(
f"Math server initialized with {stats['total_filtered']}/{stats['total_available']} functions"
)
def _register_math_tools(self):
"""Register filtered mathematical functions as MCP tools."""
# Get filtered functions
filtered_functions = self.function_filter.get_filtered_functions()
registered_count = 0
for qualified_name, func_spec in filtered_functions.items():
try:
# Create a wrapper function that will be registered as a tool
# We need to dynamically create the function with proper signature
self._register_dynamic_tool(func_spec)
registered_count += 1
except Exception as e:
logger.error(f"Failed to register tool {qualified_name}: {e}")
logger.info(f"Registered {registered_count} mathematical tools")
def _register_dynamic_tool(self, func_spec):
"""Dynamically register a mathematical function as a tool."""
# Get the original function
original_func = func_spec.function_ref
if not original_func:
logger.warning(f"No function reference for {func_spec.function_name}")
return
# Create description with domain and category info
description = f"{func_spec.description} (Domain: {func_spec.namespace}, Category: {func_spec.category})"
# Register the tool using the server's tool decorator
# The function already has proper type hints and async handling from chuk_mcp_math
self.mcp_server.tool(name=func_spec.function_name, description=description)(
original_func
)
def _register_math_resources(self):
"""Register mathematical resources with configuration info."""
# Create a reference to self for use in closures
server = self
# Available functions list resource
@self.mcp_server.resource( # type: ignore[untyped-decorator]
"math://available-functions",
name="Available Functions",
description="List of currently available mathematical functions after filtering",
mime_type="application/json",
)
async def available_functions() -> str:
filtered_functions = server.function_filter.get_filtered_functions()
functions_by_domain: dict[str, list[dict[str, Any]]] = {}
for func_spec in filtered_functions.values():
domain = func_spec.namespace
if domain not in functions_by_domain:
functions_by_domain[domain] = []
functions_by_domain[domain].append(
{
"name": func_spec.function_name,
"description": func_spec.description,
"category": func_spec.category,
"async_native": func_spec.is_async_native,
"cached": func_spec.cache_strategy != "none",
}
)
return json.dumps(
{
"total_functions": len(filtered_functions),
"functions_by_domain": functions_by_domain,
"filtering_applied": server.function_filter.get_function_stats()[
"filtering_active"
],
},
indent=2,
)
# Function statistics resource
@self.mcp_server.resource( # type: ignore[untyped-decorator]
"math://function-stats",
name="Function Statistics",
description="Statistics about function filtering and availability",
mime_type="application/json",
)
async def function_stats() -> str:
stats = server.function_filter.get_function_stats()
return json.dumps(stats, indent=2)
# Server configuration resource
@self.mcp_server.resource( # type: ignore[untyped-decorator]
"math://server-config",
name="Server Configuration",
description="Current server configuration",
mime_type="application/json",
)
async def server_config() -> str:
return json.dumps(server.config.model_dump(), indent=2)
logger.info("Registered mathematical resources")
def get_function_stats(self) -> Dict[str, Any]:
"""Get function filtering statistics."""
return self.function_filter.get_function_stats()
def run(self):
"""Run the MCP server."""
# Determine transport mode
if self.config.transport == "http":
logger.info(
f"Starting HTTP server on {self.config.host}:{self.config.port}"
)
self.mcp_server.run(
host=self.config.host,
port=self.config.port,
log_level=self.config.log_level.lower(),
)
else:
logger.info("Starting stdio server")
self.mcp_server.run(stdio=True, log_level=self.config.log_level.lower())
# Factory function for easy server creation
def create_math_server(config_file=None, **kwargs) -> ConfigurableMCPMathServer:
"""Create a configured math server instance.
Args:
config_file: Optional path to configuration file
**kwargs: Additional configuration options
Returns:
ConfigurableMCPMathServer instance
"""
if config_file:
config = ServerConfig.from_file(config_file)
# Apply any overrides
for key, value in kwargs.items():
if hasattr(config, key):
setattr(config, key, value)
else:
config = ServerConfig(**kwargs)
return ConfigurableMCPMathServer(config)