Skip to main content
Glama
server.py8.84 kB
""" FI-MCP Server Main MCP server implementation that auto-generates tools from FI functions. """ import asyncio import logging from inspect import cleandoc, signature from typing import Any, Dict from mcp.server import Server from mcp.server.lowlevel.helper_types import ReadResourceContents from mcp.server.models import InitializationOptions from mcp.server.stdio import stdio_server from mcp.types import Resource, ServerCapabilities, TextContent, Tool from .introspection import get_fi_functions, get_mcp_func_args, validate_mcp_arguments from .schema_generator import generate_all_tool_schemas, get_tool_schema_summary # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) NO_DOCUMENTATION_AVAILABLE = "No documentation available." # Create MCP server instance app = Server("fi-mcp") # Module-level storage for FI functions and their schemas. # Note: Use of mutable global state is generally discouraged in Python, but it's # acceptable here because: # 1. Server initialization happens once at startup # 2. These act as read-only caches after initialization # 3. MCP handlers need fast, direct access without re-discovering functions # 4. The server runs as a single-process daemon with no concurrent modifications fi_functions: Dict[str, callable] = {} tool_schemas: Dict[str, Dict[str, Any]] = {} @app.list_tools() async def list_tools() -> list[Tool]: """ List all available FI tools. Returns: List of Tool objects for all FI functions. """ tools = [] for tool_name, schema in tool_schemas.items(): tool = Tool( name=schema["name"], description=schema["description"], inputSchema=schema["parameters"], ) tools.append(tool) logger.info(f"Listed {len(tools)} FI tools") return tools def _format_function_name(func_name: str) -> str: """ Convert function_name to Function Name (Title Case). Special handling for abbreviations: - FI (Financial Independence) is always uppercase - POT (Pay-Over-Tuition) is always uppercase Args: func_name: Function name with underscores. Returns: Formatted name with spaces and title case. """ formatted = func_name.replace('_', ' ').title() # Handle special abbreviations formatted = formatted.replace('Fi', 'FI') formatted = formatted.replace('Pot', 'POT') return formatted def _get_function_docstring(func: callable) -> str: """ Get cleaned docstring from a function. Args: func: Function to extract docstring from. Returns: Cleaned docstring or fallback message. """ return cleandoc(func.__doc__) if func.__doc__ else NO_DOCUMENTATION_AVAILABLE def _format_function_help( func_name: str, func: callable, heading_level: int = 1 ) -> str: """ Format a single function's help documentation. Args: func_name: Name of the function. func: The function object. heading_level: Markdown heading level (1 for #, 2 for ##, etc). Returns: Formatted markdown help text. """ docstring = _get_function_docstring(func) display_name = _format_function_name(func_name) heading = "#" * heading_level return f"{heading} {display_name}\n\n{docstring}" def _create_resource_contents(content: str) -> list[ReadResourceContents]: """ Create ReadResourceContents with markdown mime type. Args: content: Markdown content. Returns: List containing single ReadResourceContents object. """ return [ReadResourceContents(content=content, mime_type="text/markdown")] @app.list_resources() async def list_resources() -> list[Resource]: """ List all available help resources. Returns: List of Resource objects for FI function documentation. """ resources = [] # Add individual function help resources for func_name in fi_functions.keys(): resource = Resource( uri=f"fi://help/{func_name}", name=f"Help: {func_name}", description=f"Documentation for the {func_name} function", mimeType="text/markdown", ) resources.append(resource) # Add a resource for all help resources.append( Resource( uri="fi://help/all", name="Help: All Functions", description="Documentation for all FI functions", mimeType="text/markdown", ) ) logger.info(f"Listed {len(resources)} help resources") return resources @app.read_resource() async def read_resource(uri: str) -> list[ReadResourceContents]: """ Read a help resource. Args: uri: URI of the resource to read. Returns: List of resource contents with markdown. """ # Convert AnyUrl to string if needed uri_str = str(uri) logger.info(f"Reading resource: {uri_str}") if not uri_str.startswith("fi://help/"): raise ValueError(f"Unknown resource URI: {uri_str}") # Extract function name from URI func_name = uri_str.replace("fi://help/", "") if func_name == "all": all_help = [ _format_function_help(name, func, heading_level=2) for name, func in sorted(fi_functions.items()) ] content = "\n\n---\n\n".join(all_help) return _create_resource_contents(content) elif func_name in fi_functions: func = fi_functions[func_name] content = _format_function_help(func_name, func, heading_level=1) return _create_resource_contents(content) else: raise ValueError(f"Unknown function: {func_name}") @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: """ Execute a FI function tool. Args: name: Name of the tool to call. arguments: Arguments passed to the tool. Returns: Tool execution result. """ logger.info(f"Calling tool: {name} with arguments: {arguments}") # Remove 'fi_' prefix to get function name if not name.startswith('fi_'): raise ValueError(f"Invalid tool name: {name}") func_name = name[3:] # Remove 'fi_' prefix if func_name not in fi_functions: raise ValueError(f"Unknown function: {func_name}") function_to_call = fi_functions[func_name] try: # Get function signature sig = signature(function_to_call) fun_params = sig.parameters # Validate required arguments missing_args = validate_mcp_arguments(fun_params, arguments) if missing_args: error_msg = f"Missing required arguments: {', '.join(missing_args)}" return [TextContent(type="text", text=f"Error: {error_msg}")] # Convert arguments to proper types fail, fun_args = get_mcp_func_args(fun_params, arguments) if fail: return [TextContent(type="text", text="Error: Failed to convert arguments")] # Call the FI function result = function_to_call(*fun_args) # Format result as string result_text = str(result) logger.info(f"Tool {name} executed successfully: {result_text}") return [TextContent(type="text", text=result_text)] except Exception as e: error_msg = f"Error executing {name}: {str(e)}" logger.error(error_msg) return [TextContent(type="text", text=f"Error: {error_msg}")] def initialize_server(): """ Initialize the server by discovering FI functions and generating schemas. """ global fi_functions, tool_schemas logger.info("Initializing FI-MCP server...") # Discover all FI functions fi_functions = get_fi_functions() logger.info(f"Discovered {len(fi_functions)} FI functions") # Generate MCP tool schemas tool_schemas = generate_all_tool_schemas(fi_functions) logger.info(f"Generated {len(tool_schemas)} tool schemas") # Log summary summary = get_tool_schema_summary(tool_schemas) for tool_name, info in summary.items(): logger.debug( f"Tool: {tool_name} - {info['parameter_count']} params " f"({info['required_parameters']} required)" ) logger.info("FI-MCP server initialization complete") async def main(): """ Main entry point for the MCP server. """ # Initialize the server initialize_server() # Run the server async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, InitializationOptions( server_name="fi-mcp", server_version="0.1.0", capabilities=ServerCapabilities(tools={}, resources={}), ), ) if __name__ == "__main__": asyncio.run(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/bbusenius/FI-MCP'

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