This is page 34 of 45. Use http://codebase.md/dicklesworthstone/llm_gateway_mcp_server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .cursorignore
├── .env.example
├── .envrc
├── .gitignore
├── additional_features.md
├── check_api_keys.py
├── completion_support.py
├── comprehensive_test.py
├── docker-compose.yml
├── Dockerfile
├── empirically_measured_model_speeds.json
├── error_handling.py
├── example_structured_tool.py
├── examples
│ ├── __init__.py
│ ├── advanced_agent_flows_using_unified_memory_system_demo.py
│ ├── advanced_extraction_demo.py
│ ├── advanced_unified_memory_system_demo.py
│ ├── advanced_vector_search_demo.py
│ ├── analytics_reporting_demo.py
│ ├── audio_transcription_demo.py
│ ├── basic_completion_demo.py
│ ├── cache_demo.py
│ ├── claude_integration_demo.py
│ ├── compare_synthesize_demo.py
│ ├── cost_optimization.py
│ ├── data
│ │ ├── sample_event.txt
│ │ ├── Steve_Jobs_Introducing_The_iPhone_compressed.md
│ │ └── Steve_Jobs_Introducing_The_iPhone_compressed.mp3
│ ├── docstring_refiner_demo.py
│ ├── document_conversion_and_processing_demo.py
│ ├── entity_relation_graph_demo.py
│ ├── filesystem_operations_demo.py
│ ├── grok_integration_demo.py
│ ├── local_text_tools_demo.py
│ ├── marqo_fused_search_demo.py
│ ├── measure_model_speeds.py
│ ├── meta_api_demo.py
│ ├── multi_provider_demo.py
│ ├── ollama_integration_demo.py
│ ├── prompt_templates_demo.py
│ ├── python_sandbox_demo.py
│ ├── rag_example.py
│ ├── research_workflow_demo.py
│ ├── sample
│ │ ├── article.txt
│ │ ├── backprop_paper.pdf
│ │ ├── buffett.pdf
│ │ ├── contract_link.txt
│ │ ├── legal_contract.txt
│ │ ├── medical_case.txt
│ │ ├── northwind.db
│ │ ├── research_paper.txt
│ │ ├── sample_data.json
│ │ └── text_classification_samples
│ │ ├── email_classification.txt
│ │ ├── news_samples.txt
│ │ ├── product_reviews.txt
│ │ └── support_tickets.txt
│ ├── sample_docs
│ │ └── downloaded
│ │ └── attention_is_all_you_need.pdf
│ ├── sentiment_analysis_demo.py
│ ├── simple_completion_demo.py
│ ├── single_shot_synthesis_demo.py
│ ├── smart_browser_demo.py
│ ├── sql_database_demo.py
│ ├── sse_client_demo.py
│ ├── test_code_extraction.py
│ ├── test_content_detection.py
│ ├── test_ollama.py
│ ├── text_classification_demo.py
│ ├── text_redline_demo.py
│ ├── tool_composition_examples.py
│ ├── tournament_code_demo.py
│ ├── tournament_text_demo.py
│ ├── unified_memory_system_demo.py
│ ├── vector_search_demo.py
│ ├── web_automation_instruction_packs.py
│ └── workflow_delegation_demo.py
├── LICENSE
├── list_models.py
├── marqo_index_config.json.example
├── mcp_protocol_schema_2025-03-25_version.json
├── mcp_python_lib_docs.md
├── mcp_tool_context_estimator.py
├── model_preferences.py
├── pyproject.toml
├── quick_test.py
├── README.md
├── resource_annotations.py
├── run_all_demo_scripts_and_check_for_errors.py
├── storage
│ └── smart_browser_internal
│ ├── locator_cache.db
│ ├── readability.js
│ └── storage_state.enc
├── test_client.py
├── test_connection.py
├── TEST_README.md
├── test_sse_client.py
├── test_stdio_client.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── integration
│ │ ├── __init__.py
│ │ └── test_server.py
│ ├── manual
│ │ ├── test_extraction_advanced.py
│ │ └── test_extraction.py
│ └── unit
│ ├── __init__.py
│ ├── test_cache.py
│ ├── test_providers.py
│ └── test_tools.py
├── TODO.md
├── tool_annotations.py
├── tools_list.json
├── ultimate_mcp_banner.webp
├── ultimate_mcp_logo.webp
├── ultimate_mcp_server
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── commands.py
│ │ ├── helpers.py
│ │ └── typer_cli.py
│ ├── clients
│ │ ├── __init__.py
│ │ ├── completion_client.py
│ │ └── rag_client.py
│ ├── config
│ │ └── examples
│ │ └── filesystem_config.yaml
│ ├── config.py
│ ├── constants.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── evaluation
│ │ │ ├── base.py
│ │ │ └── evaluators.py
│ │ ├── providers
│ │ │ ├── __init__.py
│ │ │ ├── anthropic.py
│ │ │ ├── base.py
│ │ │ ├── deepseek.py
│ │ │ ├── gemini.py
│ │ │ ├── grok.py
│ │ │ ├── ollama.py
│ │ │ ├── openai.py
│ │ │ └── openrouter.py
│ │ ├── server.py
│ │ ├── state_store.py
│ │ ├── tournaments
│ │ │ ├── manager.py
│ │ │ ├── tasks.py
│ │ │ └── utils.py
│ │ └── ums_api
│ │ ├── __init__.py
│ │ ├── ums_database.py
│ │ ├── ums_endpoints.py
│ │ ├── ums_models.py
│ │ └── ums_services.py
│ ├── exceptions.py
│ ├── graceful_shutdown.py
│ ├── services
│ │ ├── __init__.py
│ │ ├── analytics
│ │ │ ├── __init__.py
│ │ │ ├── metrics.py
│ │ │ └── reporting.py
│ │ ├── cache
│ │ │ ├── __init__.py
│ │ │ ├── cache_service.py
│ │ │ ├── persistence.py
│ │ │ ├── strategies.py
│ │ │ └── utils.py
│ │ ├── cache.py
│ │ ├── document.py
│ │ ├── knowledge_base
│ │ │ ├── __init__.py
│ │ │ ├── feedback.py
│ │ │ ├── manager.py
│ │ │ ├── rag_engine.py
│ │ │ ├── retriever.py
│ │ │ └── utils.py
│ │ ├── prompts
│ │ │ ├── __init__.py
│ │ │ ├── repository.py
│ │ │ └── templates.py
│ │ ├── prompts.py
│ │ └── vector
│ │ ├── __init__.py
│ │ ├── embeddings.py
│ │ └── vector_service.py
│ ├── tool_token_counter.py
│ ├── tools
│ │ ├── __init__.py
│ │ ├── audio_transcription.py
│ │ ├── base.py
│ │ ├── completion.py
│ │ ├── docstring_refiner.py
│ │ ├── document_conversion_and_processing.py
│ │ ├── enhanced-ums-lookbook.html
│ │ ├── entity_relation_graph.py
│ │ ├── excel_spreadsheet_automation.py
│ │ ├── extraction.py
│ │ ├── filesystem.py
│ │ ├── html_to_markdown.py
│ │ ├── local_text_tools.py
│ │ ├── marqo_fused_search.py
│ │ ├── meta_api_tool.py
│ │ ├── ocr_tools.py
│ │ ├── optimization.py
│ │ ├── provider.py
│ │ ├── pyodide_boot_template.html
│ │ ├── python_sandbox.py
│ │ ├── rag.py
│ │ ├── redline-compiled.css
│ │ ├── sentiment_analysis.py
│ │ ├── single_shot_synthesis.py
│ │ ├── smart_browser.py
│ │ ├── sql_databases.py
│ │ ├── text_classification.py
│ │ ├── text_redline_tools.py
│ │ ├── tournament.py
│ │ ├── ums_explorer.html
│ │ └── unified_memory_system.py
│ ├── utils
│ │ ├── __init__.py
│ │ ├── async_utils.py
│ │ ├── display.py
│ │ ├── logging
│ │ │ ├── __init__.py
│ │ │ ├── console.py
│ │ │ ├── emojis.py
│ │ │ ├── formatter.py
│ │ │ ├── logger.py
│ │ │ ├── panels.py
│ │ │ ├── progress.py
│ │ │ └── themes.py
│ │ ├── parse_yaml.py
│ │ ├── parsing.py
│ │ ├── security.py
│ │ └── text.py
│ └── working_memory_api.py
├── unified_memory_system_technical_analysis.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/ultimate_mcp_server/core/server.py:
--------------------------------------------------------------------------------
```python
1 | """Main server implementation for Ultimate MCP Server."""
2 |
3 | import asyncio
4 | import logging
5 | import logging.config
6 | import os
7 | import sys
8 | import time
9 | from contextlib import asynccontextmanager
10 | from dataclasses import dataclass
11 | from functools import wraps
12 | from typing import Any, Dict, List, Optional
13 |
14 | from fastapi import FastAPI
15 | from fastapi.middleware.cors import CORSMiddleware
16 | from fastmcp import Context, FastMCP
17 |
18 | import ultimate_mcp_server
19 |
20 | # Import core specifically to set the global instance
21 | import ultimate_mcp_server.core
22 | from ultimate_mcp_server.config import get_config, load_config
23 | from ultimate_mcp_server.constants import Provider
24 | from ultimate_mcp_server.core.state_store import StateStore
25 |
26 | # Import UMS API utilities and database functions
27 | from ultimate_mcp_server.core.ums_api import (
28 | setup_ums_api,
29 | )
30 | from ultimate_mcp_server.graceful_shutdown import (
31 | create_quiet_server,
32 | enable_quiet_shutdown,
33 | register_shutdown_handler,
34 | )
35 | from ultimate_mcp_server.tools.smart_browser import (
36 | _ensure_initialized as smart_browser_ensure_initialized,
37 | )
38 | from ultimate_mcp_server.tools.smart_browser import (
39 | shutdown as smart_browser_shutdown,
40 | )
41 | from ultimate_mcp_server.tools.sql_databases import initialize_sql_tools, shutdown_sql_tools
42 |
43 | # --- Import the trigger function directly instead of the whole module---
44 | from ultimate_mcp_server.utils import get_logger
45 | from ultimate_mcp_server.utils.logging import logger
46 |
47 | # --- Define Logging Configuration Dictionary ---
48 |
49 | LOG_FILE_PATH = "logs/ultimate_mcp_server.log"
50 |
51 | # Ensure log directory exists before config is used
52 | log_dir = os.path.dirname(LOG_FILE_PATH)
53 | if log_dir:
54 | os.makedirs(log_dir, exist_ok=True)
55 |
56 | LOGGING_CONFIG = {
57 | "version": 1,
58 | "disable_existing_loggers": False, # Let Uvicorn's loggers pass through if needed
59 | "formatters": {
60 | "default": {
61 | "()": "uvicorn.logging.DefaultFormatter",
62 | "fmt": "%(levelprefix)s %(message)s",
63 | "use_colors": None,
64 | },
65 | "access": {
66 | "()": "uvicorn.logging.AccessFormatter",
67 | "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
68 | },
69 | "file": { # Formatter for file output
70 | "format": "%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s",
71 | "datefmt": "%Y-%m-%d %H:%M:%S",
72 | },
73 | },
74 | "handlers": {
75 | "default": { # Console handler - redirect to stderr
76 | "formatter": "default",
77 | "class": "logging.StreamHandler",
78 | "stream": "ext://sys.stderr", # Changed from stdout to stderr
79 | },
80 | "access": { # Access log handler - redirect to stderr
81 | "formatter": "access",
82 | "class": "logging.StreamHandler",
83 | "stream": "ext://sys.stderr", # Changed from stdout to stderr
84 | },
85 | "rich_console": { # Rich console handler
86 | "()": "ultimate_mcp_server.utils.logging.formatter.create_rich_console_handler",
87 | "stderr": True, # Add this parameter to use stderr
88 | },
89 | "file": { # File handler
90 | "formatter": "file",
91 | "class": "logging.handlers.RotatingFileHandler",
92 | "filename": LOG_FILE_PATH,
93 | "maxBytes": 2 * 1024 * 1024, # 2 MB
94 | "backupCount": 5,
95 | "encoding": "utf-8",
96 | },
97 | "tools_file": { # Tools log file handler
98 | "formatter": "file",
99 | "class": "logging.FileHandler",
100 | "filename": "logs/direct_tools.log",
101 | "encoding": "utf-8",
102 | },
103 | "completions_file": { # Completions log file handler
104 | "formatter": "file",
105 | "class": "logging.FileHandler",
106 | "filename": "logs/direct_completions.log",
107 | "encoding": "utf-8",
108 | },
109 | },
110 | "loggers": {
111 | "uvicorn": {"handlers": ["rich_console"], "level": "INFO", "propagate": False},
112 | "uvicorn.error": {"level": "INFO", "propagate": True}, # Propagate errors to root
113 | "uvicorn.access": {"handlers": ["access", "file"], "level": "INFO", "propagate": False},
114 | "ultimate_mcp_server": { # Our application's logger namespace
115 | "handlers": ["rich_console", "file"],
116 | "level": "DEBUG",
117 | "propagate": False,
118 | },
119 | "ultimate_mcp_server.tools": { # Tools-specific logger
120 | "handlers": ["tools_file"],
121 | "level": "DEBUG",
122 | "propagate": True, # Propagate to parent for console display
123 | },
124 | "ultimate_mcp_server.completions": { # Completions-specific logger
125 | "handlers": ["completions_file"],
126 | "level": "DEBUG",
127 | "propagate": True, # Propagate to parent for console display
128 | },
129 | },
130 | "root": { # Root logger configuration
131 | "level": "INFO",
132 | "handlers": ["rich_console", "file"], # Root catches logs not handled by specific loggers
133 | },
134 | }
135 |
136 | # DO NOT apply the config here - it will be applied by Uvicorn through log_config parameter
137 |
138 | # Global server instance
139 | _server_app = None
140 | _gateway_instance = None
141 |
142 | # Get loggers
143 | tools_logger = get_logger("ultimate_mcp_server.tools")
144 | completions_logger = get_logger("ultimate_mcp_server.completions")
145 |
146 |
147 | @dataclass
148 | class ProviderStatus:
149 | """
150 | Structured representation of an LLM provider's configuration and availability status.
151 |
152 | This dataclass encapsulates all essential status information about a language model
153 | provider in the Ultimate MCP Server. It's used to track the state of each provider,
154 | including whether it's properly configured, successfully initialized, and what models
155 | it offers. This information is vital for:
156 |
157 | 1. Displaying provider status to clients via API endpoints
158 | 2. Making runtime decisions about provider availability
159 | 3. Debugging provider configuration and connectivity issues
160 | 4. Resource listings and capability discovery
161 |
162 | The status is typically maintained in the Gateway's provider_status dictionary,
163 | with provider names as keys and ProviderStatus instances as values.
164 |
165 | Attributes:
166 | enabled: Whether the provider is enabled in the configuration.
167 | This reflects the user's intent, not actual availability.
168 | available: Whether the provider is successfully initialized and ready for use.
169 | This is determined by runtime checks during server initialization.
170 | api_key_configured: Whether a valid API key was found for this provider.
171 | A provider might be enabled but have no API key configured.
172 | models: List of available models from this provider, with each model represented
173 | as a dictionary containing model ID, name, and capabilities.
174 | error: Error message explaining why a provider is unavailable, or None if
175 | the provider initialized successfully or hasn't been initialized yet.
176 | """
177 |
178 | enabled: bool
179 | available: bool
180 | api_key_configured: bool
181 | models: List[Dict[str, Any]]
182 | error: Optional[str] = None
183 |
184 |
185 | class Gateway:
186 | """
187 | Main Ultimate MCP Server implementation and central orchestrator.
188 |
189 | The Gateway class serves as the core of the Ultimate MCP Server, providing a unified
190 | interface to multiple LLM providers (OpenAI, Anthropic, etc.) and implementing the
191 | Model Control Protocol (MCP). It manages provider connections, tool registration,
192 | state persistence, and request handling.
193 |
194 | Key responsibilities:
195 | - Initializing and managing connections to LLM providers
196 | - Registering and exposing tools for model interaction
197 | - Providing consistent error handling and logging
198 | - Managing state persistence across requests
199 | - Exposing resources (guides, examples, reference info) for models
200 | - Implementing the MCP protocol for standardized model interaction
201 |
202 | The Gateway is designed to be instantiated once per server instance and serves
203 | as the central hub for all model interactions. It can be accessed globally through
204 | the ultimate_mcp_server.core._gateway_instance reference.
205 | """
206 |
207 | def __init__(
208 | self,
209 | name: str = "main",
210 | register_tools: bool = True,
211 | provider_exclusions: List[str] = None,
212 | load_all_tools: bool = False, # Remove result_serialization_mode
213 | ):
214 | """
215 | Initialize the MCP Gateway with configured providers and tools.
216 |
217 | This constructor sets up the complete MCP Gateway environment, including:
218 | - Loading configuration from environment variables and config files
219 | - Setting up logging infrastructure
220 | - Initializing the MCP server framework
221 | - Creating a state store for persistence
222 | - Registering tools and resources based on configuration
223 |
224 | The initialization process is designed to be flexible, allowing for customization
225 | through the provided parameters and the configuration system. Provider initialization
226 | is deferred until server startup to ensure proper async handling.
227 |
228 | Args:
229 | name: Server instance name, used for logging and identification purposes.
230 | Default is "main".
231 | register_tools: Whether to register standard MCP tools with the server.
232 | If False, only the minimal core functionality will be available.
233 | Default is True.
234 | provider_exclusions: List of provider names to exclude from initialization.
235 | This allows selectively disabling specific providers
236 | regardless of their configuration status.
237 | Default is None (no exclusions).
238 | load_all_tools: If True, load all available tools. If False (default),
239 | load only the defined 'Base Toolset'.
240 | """
241 | self.name = name
242 | self.providers = {}
243 | self.provider_status = {}
244 | self.logger = get_logger(f"ultimate_mcp_server.{name}")
245 | self.event_handlers = {}
246 | self.provider_exclusions = provider_exclusions or []
247 | self.api_meta_tool = None # Initialize api_meta_tool attribute
248 | self.load_all_tools = load_all_tools # Store the flag
249 |
250 | # Load configuration if not already loaded
251 | if get_config() is None:
252 | self.logger.info("Initializing Gateway: Loading configuration...")
253 | load_config()
254 |
255 | # Initialize logger
256 | self.logger.info(f"Initializing {self.name}...")
257 |
258 | # Set MCP protocol version to 2025-03-25
259 | import os
260 |
261 | os.environ["MCP_PROTOCOL_VERSION"] = "2025-03-25"
262 |
263 | # Create MCP server with modern FastMCP constructor
264 | self.mcp = FastMCP(
265 | name=self.name,
266 | lifespan=self._server_lifespan,
267 | instructions=self.system_instructions,
268 | )
269 |
270 | # Initialize the state store
271 | persistence_dir = None
272 | if (
273 | get_config()
274 | and hasattr(get_config(), "state_persistence")
275 | and hasattr(get_config().state_persistence, "dir")
276 | ):
277 | persistence_dir = get_config().state_persistence.dir
278 | self.state_store = StateStore(persistence_dir)
279 |
280 | # Register tools if requested
281 | if register_tools:
282 | self._register_tools(load_all=self.load_all_tools)
283 | self._register_resources()
284 |
285 | self.logger.info(f"Ultimate MCP Server '{self.name}' initialized")
286 |
287 | def log_tool_calls(self, func):
288 | """
289 | Decorator to log MCP tool calls with detailed timing and result information.
290 |
291 | This decorator wraps MCP tool functions to provide consistent logging of:
292 | - Tool name and parameters at invocation time
293 | - Execution time for performance tracking
294 | - Success or failure status
295 | - Summarized results or error information
296 |
297 | The decorator ensures that all tool calls are logged to a dedicated tools logger,
298 | which helps with diagnostics, debugging, and monitoring of tool usage patterns.
299 | Successful calls include timing information and a brief summary of the result,
300 | while failed calls include exception details.
301 |
302 | Args:
303 | func: The async function to wrap with logging. This should be a tool function
304 | registered with the MCP server that will be called by models.
305 |
306 | Returns:
307 | A wrapped async function that performs the same operations as the original
308 | but with added logging before and after execution.
309 |
310 | Note:
311 | This decorator is automatically applied to all functions registered as tools
312 | via the @mcp.tool() decorator in the _register_tools method, so it doesn't
313 | need to be applied manually in most cases.
314 | """
315 |
316 | @wraps(func)
317 | async def wrapper(*args, **kwargs):
318 | start_time = time.time()
319 | tool_name = func.__name__
320 |
321 | # Format parameters for logging
322 | args_str = ", ".join([repr(arg) for arg in args[1:] if arg is not None])
323 | kwargs_str = ", ".join([f"{k}={repr(v)}" for k, v in kwargs.items() if k != "ctx"])
324 | params_str = ", ".join(filter(None, [args_str, kwargs_str]))
325 |
326 | # Log the request - only through tools_logger
327 | tools_logger.info(f"TOOL CALL: {tool_name}({params_str})")
328 |
329 | try:
330 | result = await func(*args, **kwargs)
331 | processing_time = time.time() - start_time
332 |
333 | # Format result for logging
334 | if isinstance(result, dict):
335 | result_keys = list(result.keys())
336 | result_summary = f"dict with keys: {result_keys}"
337 | else:
338 | result_str = str(result)
339 | result_summary = (
340 | (result_str[:100] + "...") if len(result_str) > 100 else result_str
341 | )
342 |
343 | # Log successful completion - only through tools_logger
344 | tools_logger.info(
345 | f"TOOL SUCCESS: {tool_name} completed in {processing_time:.2f}s - Result: {result_summary}"
346 | )
347 |
348 | return result
349 | except Exception as e:
350 | processing_time = time.time() - start_time
351 | tools_logger.error(
352 | f"TOOL ERROR: {tool_name} failed after {processing_time:.2f}s: {str(e)}",
353 | exc_info=True,
354 | )
355 | raise
356 |
357 | return wrapper
358 |
359 | @asynccontextmanager
360 | async def _server_lifespan(self, server: FastMCP):
361 | """
362 | Async context manager managing the server lifecycle during startup and shutdown.
363 |
364 | This method implements the lifespan protocol used by FastMCP (based on ASGI) to:
365 | 1. Perform startup initialization before the server begins accepting requests
366 | 2. Clean up resources when the server is shutting down
367 | 3. Make shared context available to request handlers during the server's lifetime
368 |
369 | During startup, this method:
370 | - Initializes all configured LLM providers
371 | - Triggers dynamic docstring generation for tools that need it
372 | - Sets the global Gateway instance for access from other components
373 | - Prepares a shared context dictionary for use by request handlers
374 |
375 | During shutdown, it:
376 | - Clears the global Gateway instance reference
377 | - Handles any necessary cleanup of resources
378 |
379 | The lifespan context is active throughout the entire server runtime, from
380 | startup until shutdown is initiated.
381 |
382 | Args:
383 | server: The FastMCP server instance that's starting up, which provides
384 | the framework context for the lifespan.
385 |
386 | Yields:
387 | Dict containing initialized resources that will be available to all
388 | request handlers during the server's lifetime.
389 |
390 | Note:
391 | This method is called automatically by the FastMCP framework during
392 | server startup and is not intended to be called directly.
393 | """
394 | self.logger.info(f"Starting Ultimate MCP Server '{self.name}'")
395 |
396 | # Add a flag to track if this is an SSE instance
397 | is_sse_mode = getattr(self, '_sse_mode', False)
398 | if is_sse_mode:
399 | self.logger.info("SSE mode detected - using persistent lifespan management")
400 |
401 | # Initialize providers
402 | await self._initialize_providers()
403 |
404 | try:
405 | await initialize_sql_tools()
406 | self.logger.info("SQL tools state initialized.")
407 | except Exception as e:
408 | self.logger.error(f"Failed to initialize SQL tools state: {e}", exc_info=True)
409 |
410 | # --- OPTIONAL: Pre-initialize SmartBrowser ---
411 | try:
412 | self.logger.info("Pre-initializing Smart Browser components...")
413 | # Call the imported initialization function
414 | await smart_browser_ensure_initialized()
415 | self.logger.info("Smart Browser successfully pre-initialized.")
416 | except Exception as e:
417 | # Log warning but don't stop server startup if pre-init fails
418 | self.logger.warning(f"Could not pre-initialize Smart Browser: {e}", exc_info=True)
419 | # ---------------------------------------------------------------------
420 |
421 | # --- Trigger Dynamic Docstring Generation ---
422 | # This should run after config is loaded but before the server is fully ready
423 | # It checks cache and potentially calls an LLM.
424 | self.logger.info("Initiating dynamic docstring generation for Marqo tool...")
425 | try:
426 | # Import the function here to avoid circular imports
427 | from ultimate_mcp_server.tools.marqo_fused_search import (
428 | trigger_dynamic_docstring_generation,
429 | )
430 |
431 | await trigger_dynamic_docstring_generation()
432 | self.logger.info("Dynamic docstring generation/loading complete.")
433 | except Exception as e:
434 | self.logger.error(
435 | f"Error during dynamic docstring generation startup task: {e}", exc_info=True
436 | )
437 | # ---------------------------------------------
438 |
439 | # --- Set the global instance variable ---
440 | # Make the fully initialized instance accessible globally AFTER init
441 | ultimate_mcp_server.core._gateway_instance = self
442 | self.logger.info("Global gateway instance set.")
443 | # ----------------------------------------
444 |
445 | # --- Attach StateStore to application state ---
446 | # This makes the StateStore available to all tools via ctx.fastmcp._state_store
447 | # Note: In FastMCP 2.0+, we store the state_store directly on the server instance
448 | # Tools can access it via the with_state_management decorator
449 | server._state_store = self.state_store
450 | self.logger.info("StateStore attached to server instance.")
451 | # -----------------------------------------------
452 |
453 | # Create lifespan context (still useful for framework calls)
454 | context = {
455 | "providers": self.providers,
456 | "provider_status": self.provider_status,
457 | }
458 |
459 | self.logger.info("Lifespan context initialized, MCP server ready to handle requests")
460 |
461 | try:
462 | # Import and call trigger_dynamic_docstring_generation again
463 | from ultimate_mcp_server.tools.marqo_fused_search import (
464 | trigger_dynamic_docstring_generation,
465 | )
466 |
467 | await trigger_dynamic_docstring_generation()
468 | logger.info("Dynamic docstring generation/loading complete.")
469 |
470 | if is_sse_mode:
471 | # For SSE mode, create a persistent context that doesn't shutdown easily
472 | self.logger.info("Creating persistent SSE lifespan context")
473 |
474 | # Add a keepalive task for SSE mode
475 | async def sse_lifespan_keepalive():
476 | """Keepalive task to maintain SSE server lifespan."""
477 | while True:
478 | await asyncio.sleep(60) # Keep alive every minute
479 | # This task existing keeps the lifespan active
480 |
481 | # Start the keepalive task
482 | keepalive_task = asyncio.create_task(sse_lifespan_keepalive())
483 |
484 | try:
485 | yield context
486 | finally:
487 | # Cancel the keepalive task during shutdown
488 | keepalive_task.cancel()
489 | try:
490 | await keepalive_task
491 | except asyncio.CancelledError:
492 | pass
493 | else:
494 | yield context
495 |
496 | finally:
497 | if is_sse_mode:
498 | self.logger.info("SSE mode shutdown initiated")
499 |
500 | try:
501 | # --- Shutdown SQL Tools State ---
502 | await shutdown_sql_tools()
503 | self.logger.info("SQL tools state shut down.")
504 | except Exception as e:
505 | self.logger.error(f"Failed to shut down SQL tools state: {e}", exc_info=True)
506 |
507 | # 2. Shutdown Smart Browser explicitly
508 | try:
509 | self.logger.info("Initiating explicit Smart Browser shutdown...")
510 | await smart_browser_shutdown() # Call the imported function
511 | self.logger.info("Smart Browser shutdown completed successfully.")
512 | except Exception as e:
513 | logger.error(f"Error during explicit Smart Browser shutdown: {e}", exc_info=True)
514 |
515 | # --- Clear the global instance on shutdown ---
516 | ultimate_mcp_server.core._gateway_instance = None
517 | self.logger.info("Global gateway instance cleared.")
518 | # -------------------------------------------
519 | self.logger.info(f"Shutting down Ultimate MCP Server '{self.name}'")
520 |
521 | async def _initialize_providers(self):
522 | """
523 | Initialize all enabled LLM providers based on the loaded configuration.
524 |
525 | This asynchronous method performs the following steps:
526 | 1. Identifies which providers are enabled and properly configured with API keys
527 | 2. Skips providers that are in the exclusion list (specified at Gateway creation)
528 | 3. Initializes each valid provider in parallel using asyncio tasks
529 | 4. Updates the provider_status dictionary with the initialization results
530 |
531 | The method uses a defensive approach, handling cases where:
532 | - A provider is enabled but missing API keys
533 | - Configuration is incomplete or inconsistent
534 | - Initialization errors occur with specific providers
535 |
536 | After initialization, the Gateway will have a populated providers dictionary
537 | with available provider instances, and a comprehensive provider_status dictionary
538 | with status information for all providers (including those that failed to initialize).
539 |
540 | This method is automatically called during server startup and is not intended
541 | to be called directly by users of the Gateway class.
542 |
543 | Raises:
544 | No exceptions are propagated from this method. All provider initialization
545 | errors are caught, logged, and reflected in the provider_status dictionary.
546 | """
547 | self.logger.info("Initializing LLM providers")
548 |
549 | cfg = get_config()
550 | providers_to_init = []
551 |
552 | # Determine which providers to initialize based SOLELY on the loaded config
553 | for provider_name in [p.value for p in Provider]:
554 | # Skip providers that are in the exclusion list
555 | if provider_name in self.provider_exclusions:
556 | self.logger.debug(f"Skipping provider {provider_name} (excluded)")
557 | continue
558 |
559 | provider_config = getattr(cfg.providers, provider_name, None)
560 | # Special exception for Ollama: it doesn't require an API key since it runs locally
561 | if (
562 | provider_name == Provider.OLLAMA.value
563 | and provider_config
564 | and provider_config.enabled
565 | ):
566 | self.logger.debug(
567 | f"Found configured and enabled provider: {provider_name} (API key not required)"
568 | )
569 | providers_to_init.append(provider_name)
570 | # Check if the provider is enabled AND has an API key configured in the loaded settings
571 | elif provider_config and provider_config.enabled and provider_config.api_key:
572 | self.logger.debug(f"Found configured and enabled provider: {provider_name}")
573 | providers_to_init.append(provider_name)
574 | elif provider_config and provider_config.enabled:
575 | self.logger.warning(
576 | f"Provider {provider_name} is enabled but missing API key in config. Skipping."
577 | )
578 | # else: # Provider not found in config or not enabled
579 | # self.logger.debug(f"Provider {provider_name} not configured or not enabled.")
580 |
581 | # Initialize providers in parallel
582 | init_tasks = [
583 | asyncio.create_task(
584 | self._initialize_provider(provider_name), name=f"init-{provider_name}"
585 | )
586 | for provider_name in providers_to_init
587 | ]
588 |
589 | if init_tasks:
590 | await asyncio.gather(*init_tasks)
591 |
592 | # Log initialization summary
593 | available_providers = [
594 | name for name, status in self.provider_status.items() if status.available
595 | ]
596 | self.logger.info(
597 | f"Providers initialized: {len(available_providers)}/{len(providers_to_init)} available"
598 | )
599 |
600 | async def _initialize_provider(self, provider_name: str):
601 | """
602 | Initialize a single LLM provider with its API key and configuration.
603 |
604 | This method is responsible for initializing an individual provider by:
605 | 1. Retrieving the provider's configuration and API key
606 | 2. Importing the appropriate provider class
607 | 3. Instantiating the provider with the configured API key
608 | 4. Calling the provider's initialize method to establish connectivity
609 | 5. Recording the provider's status (including available models)
610 |
611 | The method handles errors gracefully, ensuring that exceptions during any
612 | stage of initialization are caught, logged, and reflected in the provider's
613 | status rather than propagated up the call stack.
614 |
615 | Args:
616 | provider_name: Name of the provider to initialize, matching a value
617 | in the Provider enum (e.g., "openai", "anthropic").
618 |
619 | Returns:
620 | None. Results are stored in the Gateway's providers and provider_status
621 | dictionaries rather than returned directly.
622 |
623 | Note:
624 | This method is called by _initialize_providers during server startup
625 | and is not intended to be called directly by users of the Gateway class.
626 | """
627 | api_key = None
628 | api_key_configured = False
629 | provider_config = None
630 |
631 | try:
632 | cfg = get_config()
633 | provider_config = getattr(cfg.providers, provider_name, None)
634 |
635 | # Get API key ONLY from the loaded config object
636 | if provider_config and provider_config.api_key:
637 | api_key = provider_config.api_key
638 | api_key_configured = True
639 | # Special case for Ollama: doesn't require an API key
640 | elif provider_name == Provider.OLLAMA.value and provider_config:
641 | api_key = None
642 | api_key_configured = True
643 | self.logger.debug("Initializing Ollama provider without API key (not required)")
644 | else:
645 | # This case should ideally not be reached if checks in _initialize_providers are correct,
646 | # but handle defensively.
647 | self.logger.warning(
648 | f"Attempted to initialize {provider_name}, but API key not found in loaded config."
649 | )
650 | api_key_configured = False
651 |
652 | if not api_key_configured:
653 | # Record status for providers found in config but without a key
654 | if provider_config:
655 | self.provider_status[provider_name] = ProviderStatus(
656 | enabled=provider_config.enabled, # Reflects config setting
657 | available=False,
658 | api_key_configured=False,
659 | models=[],
660 | error="API key not found in loaded configuration",
661 | )
662 | # Do not log the warning here again, just return
663 | return
664 |
665 | # --- API Key is configured, proceed with initialization ---
666 | self.logger.debug(f"Initializing provider {provider_name} with key from config.")
667 |
668 | # Import PROVIDER_REGISTRY to use centralized provider registry
669 | from ultimate_mcp_server.core.providers import PROVIDER_REGISTRY
670 |
671 | # Use the registry instead of hardcoded providers dictionary
672 | provider_class = PROVIDER_REGISTRY.get(provider_name)
673 | if not provider_class:
674 | raise ValueError(f"Invalid provider name mapping: {provider_name}")
675 |
676 | # Instantiate provider with the API key retrieved from the config (via decouple)
677 | # Ensure provider classes' __init__ expect 'api_key' as a keyword argument
678 | provider = provider_class(api_key=api_key)
679 |
680 | # Initialize provider (which should use the config passed)
681 | available = await provider.initialize()
682 |
683 | # Update status based on initialization result
684 | if available:
685 | models = await provider.list_models()
686 | self.providers[provider_name] = provider
687 | self.provider_status[provider_name] = ProviderStatus(
688 | enabled=provider_config.enabled,
689 | available=True,
690 | api_key_configured=True,
691 | models=models,
692 | )
693 | self.logger.success(
694 | f"Provider {provider_name} initialized successfully with {len(models)} models",
695 | emoji_key="provider",
696 | )
697 | else:
698 | self.provider_status[provider_name] = ProviderStatus(
699 | enabled=provider_config.enabled,
700 | available=False,
701 | api_key_configured=True, # Key was found, but init failed
702 | models=[],
703 | error="Initialization failed (check provider API status or logs)",
704 | )
705 | self.logger.error(
706 | f"Provider {provider_name} initialization failed", emoji_key="error"
707 | )
708 |
709 | except Exception as e:
710 | # Handle unexpected errors during initialization
711 | error_msg = f"Error initializing provider {provider_name}: {str(e)}"
712 | self.logger.error(error_msg, exc_info=True)
713 | # Ensure status is updated even on exceptions
714 | enabled_status = provider_config.enabled if provider_config else False # Best guess
715 | self.provider_status[provider_name] = ProviderStatus(
716 | enabled=enabled_status,
717 | available=False,
718 | api_key_configured=api_key_configured, # Reflects if key was found before error
719 | models=[],
720 | error=error_msg,
721 | )
722 |
723 | @property
724 | def system_instructions(self) -> str:
725 | """
726 | Return comprehensive system-level instructions for LLMs on how to use the gateway.
727 |
728 | This property generates detailed instructions that are injected into the system prompt
729 | for LLMs using the Gateway. These instructions serve as a guide for LLMs to effectively
730 | utilize the available tools and capabilities, helping them understand:
731 |
732 | - The categories of available tools and their purposes
733 | - Best practices for provider and model selection
734 | - Error handling strategies and patterns
735 | - Recommendations for efficient and appropriate tool usage
736 | - Guidelines for choosing the right tool for specific tasks
737 |
738 | The instructions are designed to be clear and actionable, helping LLMs make
739 | informed decisions about when and how to use different components of the
740 | Ultimate MCP Server. They're structured in a hierarchical format with sections
741 | covering core categories, best practices, and additional resources.
742 |
743 | Returns:
744 | A formatted string containing detailed instructions for LLMs on how to
745 | effectively use the Gateway's tools and capabilities. These instructions
746 | are automatically included in the system prompt for all LLM interactions.
747 | """
748 | # Tool loading message can be adjusted based on self.load_all_tools if needed
749 | tool_loading_info = "all available tools" if self.load_all_tools else "the Base Toolset"
750 |
751 | return f"""
752 | # Ultimate MCP Server Tool Usage Instructions
753 |
754 | You have access to the Ultimate MCP Server, which provides unified access to multiple language model
755 | providers (OpenAI, Anthropic, etc.) through a standardized interface. This server instance has loaded {tool_loading_info}.
756 | Follow these instructions to effectively use the available tools.
757 |
758 | ## Core Tool Categories
759 |
760 | 1. **Provider Tools**: Use these to discover available providers and models
761 | - `get_provider_status`: Check which providers are available
762 | - `list_models`: List models available from a specific provider
763 |
764 | 2. **Completion Tools**: Use these for text generation
765 | - `generate_completion`: Single-prompt text generation (non-streaming)
766 | - `chat_completion`: Multi-turn conversation with message history
767 | - `multi_completion`: Compare outputs from multiple providers/models
768 |
769 | 3. **Tournament Tools**: Use these to run competitions between models
770 | - `create_tournament`: Create and start a new tournament
771 | - `get_tournament_status`: Check tournament progress
772 | - `get_tournament_results`: Get detailed tournament results
773 | - `list_tournaments`: List all tournaments
774 | - `cancel_tournament`: Cancel a running tournament
775 |
776 | ## Best Practices
777 |
778 | 1. **Provider Selection**:
779 | - Always check provider availability with `get_provider_status` before use
780 | - Verify model availability with `list_models` before using specific models
781 |
782 | 2. **Error Handling**:
783 | - All tools include error handling in their responses
784 | - Check for the presence of an "error" field in responses
785 | - If an error occurs, adapt your approach based on the error message
786 |
787 | 3. **Efficient Usage**:
788 | - Use cached tools when repeatedly calling the same function with identical parameters
789 | - For long-running operations like tournaments, poll status periodically
790 |
791 | 4. **Tool Selection Guidelines**:
792 | - For single-turn text generation → `generate_completion`
793 | - For conversation-based interactions → `chat_completion`
794 | - For comparing outputs across models → `multi_completion`
795 | - For evaluating model performance → Tournament tools
796 |
797 | ## Additional Resources
798 |
799 | For more detailed information and examples, access these MCP resources:
800 | - `info://server`: Basic server information
801 | - `info://tools`: Overview of available tools
802 | - `provider://{{provider_name}}`: Details about a specific provider
803 | - `guide://llm`: Comprehensive usage guide for LLMs
804 | - `guide://error-handling`: Detailed error handling guidance
805 | - `examples://workflows`: Detailed examples of common workflows
806 | - `examples://completions`: Examples of different completion types
807 | - `examples://tournaments`: Guidance on tournament configuration and analysis
808 |
809 | Remember to use appropriate error handling and follow the documented parameter formats
810 | for each tool. All providers may not be available at all times, so always check status
811 | first and be prepared to adapt to available providers.
812 | """
813 |
814 | def _register_tools(self, load_all: bool = False):
815 | """
816 | Register all MCP tools with the server instance.
817 |
818 | This internal method sets up all available tools in the Ultimate MCP Server,
819 | making them accessible to LLMs through the MCP protocol. It handles:
820 |
821 | 1. Setting up the basic echo tool for connectivity testing
822 | 2. Conditionally calling the register_all_tools function to set up either
823 | the 'Base Toolset' or all specialized tools based on the `load_all` flag.
824 |
825 | The registration process wraps each tool function with logging functionality
826 | via the log_tool_calls decorator, ensuring consistent logging behavior across
827 | all tools. This provides valuable diagnostic information during tool execution.
828 |
829 | All registered tools become available through the MCP interface and can be
830 | discovered and used by LLMs interacting with the server.
831 |
832 | Args:
833 | load_all: If True, register all tools. If False, register only the base set.
834 |
835 | Note:
836 | This method is called automatically during Gateway initialization when
837 | register_tools=True (the default) and is not intended to be called directly.
838 | """
839 | # Import here to avoid circular dependency
840 | from ultimate_mcp_server.tools import register_all_tools
841 |
842 | self.logger.info("Registering core tools...")
843 |
844 | # Echo tool - define the function first, then register it
845 | @self.log_tool_calls
846 | async def echo(message: str, ctx: Context = None) -> Dict[str, Any]:
847 | """
848 | Echo back the message for testing MCP connectivity.
849 |
850 | Args:
851 | message: The message to echo back
852 |
853 | Returns:
854 | Dictionary containing the echoed message
855 | """
856 | self.logger.info(f"Echo tool called with message: {message}")
857 | return {"message": message}
858 |
859 | # Now register the decorated function with mcp.tool
860 | self.mcp.tool(echo)
861 |
862 | # Define our base toolset - use function names not module names
863 | base_toolset = [
864 | # Completion tools
865 | "generate_completion",
866 | "chat_completion",
867 | "multi_completion",
868 | # "stream_completion", # Not that useful for MCP
869 | # Provider tools
870 | "get_provider_status",
871 | "list_models",
872 | # Filesystem tools
873 | "read_file",
874 | "read_multiple_files",
875 | "write_file",
876 | "edit_file",
877 | "create_directory",
878 | "list_directory",
879 | "directory_tree",
880 | "move_file",
881 | "search_files",
882 | "get_file_info",
883 | "list_allowed_directories",
884 | "get_unique_filepath",
885 | # Optimization tools
886 | "estimate_cost",
887 | "compare_models",
888 | "recommend_model",
889 | # Local text tools
890 | "run_ripgrep",
891 | "run_awk",
892 | "run_sed",
893 | "run_jq",
894 | # Search tools
895 | "marqo_fused_search",
896 | # SmartBrowser class methods
897 | "search",
898 | "download",
899 | "download_site_pdfs",
900 | "collect_documentation",
901 | "run_macro",
902 | "autopilot",
903 | # SQL class methods
904 | "manage_database",
905 | "execute_sql",
906 | "explore_database",
907 | "access_audit_log",
908 | # Document processing class methods
909 | "convert_document",
910 | "chunk_document",
911 | "clean_and_format_text_as_markdown",
912 | "batch_format_texts",
913 | "optimize_markdown_formatting",
914 | "generate_qa_pairs",
915 | "summarize_document",
916 | "ocr_image",
917 | "enhance_ocr_text",
918 | "analyze_pdf_structure",
919 | "extract_tables",
920 | "process_document_batch",
921 | # Python sandbox class methods
922 | "execute_python",
923 | "repl_python",
924 | ]
925 |
926 | # Conditionally register tools based on load_all flag
927 | if load_all:
928 | self.logger.info("Calling register_all_tools to register ALL available tools...")
929 | register_all_tools(self.mcp)
930 | else:
931 | self.logger.info("Calling register_all_tools to register only the BASE toolset...")
932 | # Check if tool_registration filter is enabled in config
933 | cfg = get_config()
934 | if cfg.tool_registration.filter_enabled:
935 | # If filtering is already enabled, respect that configuration
936 | self.logger.info("Tool filtering is enabled - using config filter settings")
937 | register_all_tools(self.mcp)
938 | else:
939 | # Otherwise, set up filtering for base toolset
940 | cfg.tool_registration.filter_enabled = True
941 | cfg.tool_registration.included_tools = base_toolset
942 | self.logger.info(f"Registering base toolset: {', '.join(base_toolset)}")
943 | register_all_tools(self.mcp)
944 |
945 | # After tools are registered, save the tool names to a file for the tools estimator script
946 | try:
947 | import json
948 |
949 | from ultimate_mcp_server.tools import STANDALONE_TOOL_FUNCTIONS
950 |
951 | # Get tools from STANDALONE_TOOL_FUNCTIONS plus class-based tools
952 | all_tool_names = []
953 |
954 | # Add standalone tool function names
955 | for tool_func in STANDALONE_TOOL_FUNCTIONS:
956 | if hasattr(tool_func, "__name__"):
957 | all_tool_names.append(tool_func.__name__)
958 |
959 | # Add echo tool
960 | all_tool_names.append("echo")
961 |
962 | # Write to file
963 | with open("tools_list.json", "w") as f:
964 | json.dump(all_tool_names, f, indent=2)
965 |
966 | self.logger.info(
967 | f"Wrote {len(all_tool_names)} tool names to tools_list.json for context estimator"
968 | )
969 | except Exception as e:
970 | self.logger.warning(f"Failed to write tool names to file: {str(e)}")
971 |
972 | def _register_resources(self):
973 | """
974 | Register all MCP resources with the server instance.
975 |
976 | This internal method registers standard MCP resources that provide static
977 | information and guidance to LLMs using the Ultimate MCP Server. Resources differ
978 | from tools in that they:
979 |
980 | 1. Provide static reference information rather than interactive functionality
981 | 2. Are accessed via URI-like identifiers (e.g., "info://server", "guide://llm")
982 | 3. Don't require API calls or external services to generate their responses
983 |
984 | Registered resources include:
985 | - Server and tool information (info:// resources)
986 | - Provider details (provider:// resources)
987 | - Usage guides and tutorials (guide:// resources)
988 | - Example workflows and usage patterns (examples:// resources)
989 |
990 | These resources serve as a knowledge base for LLMs to better understand how to
991 | effectively use the available tools and follow best practices. They help reduce
992 | the need for extensive contextual information in prompts by making reference
993 | material available on-demand through the MCP protocol.
994 |
995 | Note:
996 | This method is called automatically during Gateway initialization when
997 | register_tools=True (the default) and is not intended to be called directly.
998 | """
999 |
1000 | @self.mcp.resource("info://server")
1001 | def get_server_info() -> Dict[str, Any]:
1002 | """
1003 | Get information about the Ultimate MCP Server server.
1004 |
1005 | This resource provides basic metadata about the Ultimate MCP Server server instance,
1006 | including its name, version, and supported providers. Use this resource to
1007 | discover server capabilities and version information.
1008 |
1009 | Resource URI: info://server
1010 |
1011 | Returns:
1012 | Dictionary containing server information:
1013 | - name: Name of the Ultimate MCP Server server
1014 | - version: Version of the Ultimate MCP Server server
1015 | - description: Brief description of server functionality
1016 | - providers: List of supported LLM provider names
1017 |
1018 | Example:
1019 | {
1020 | "name": "Ultimate MCP Server",
1021 | "version": "0.1.0",
1022 | "description": "MCP server for accessing multiple LLM providers",
1023 | "providers": ["openai", "anthropic", "deepseek", "gemini"]
1024 | }
1025 |
1026 | Usage:
1027 | This resource is useful for clients to verify server identity, check compatibility,
1028 | and discover basic capabilities. For detailed provider status, use the
1029 | get_provider_status tool instead.
1030 | """
1031 | return {
1032 | "name": self.name,
1033 | "version": "0.1.0",
1034 | "description": "MCP server for accessing multiple LLM providers",
1035 | "providers": [p.value for p in Provider],
1036 | }
1037 |
1038 | @self.mcp.resource("info://tools")
1039 | def get_tools_info() -> Dict[str, Any]:
1040 | """
1041 | Get information about available Ultimate MCP Server tools.
1042 |
1043 | This resource provides a descriptive overview of the tools available in the
1044 | Ultimate MCP Server, organized by category. Use this resource to understand which
1045 | tools are available and how they're organized.
1046 |
1047 | Resource URI: info://tools
1048 |
1049 | Returns:
1050 | Dictionary containing tools information organized by category:
1051 | - provider_tools: Tools for interacting with LLM providers
1052 | - completion_tools: Tools for text generation and completion
1053 | - tournament_tools: Tools for running model tournaments
1054 | - document_tools: Tools for document processing
1055 |
1056 | Example:
1057 | {
1058 | "provider_tools": {
1059 | "description": "Tools for accessing and managing LLM providers",
1060 | "tools": ["get_provider_status", "list_models"]
1061 | },
1062 | "completion_tools": {
1063 | "description": "Tools for text generation and completion",
1064 | "tools": ["generate_completion", "chat_completion", "multi_completion"]
1065 | },
1066 | "tournament_tools": {
1067 | "description": "Tools for running and managing model tournaments",
1068 | "tools": ["create_tournament", "list_tournaments", "get_tournament_status",
1069 | "get_tournament_results", "cancel_tournament"]
1070 | }
1071 | }
1072 |
1073 | Usage:
1074 | Use this resource to understand the capabilities of the Ultimate MCP Server and
1075 | discover available tools. For detailed information about specific tools,
1076 | use the MCP list_tools method.
1077 | """
1078 | return {
1079 | "provider_tools": {
1080 | "description": "Tools for accessing and managing LLM providers",
1081 | "tools": ["get_provider_status", "list_models"],
1082 | },
1083 | "completion_tools": {
1084 | "description": "Tools for text generation and completion",
1085 | "tools": ["generate_completion", "chat_completion", "multi_completion"],
1086 | },
1087 | "tournament_tools": {
1088 | "description": "Tools for running and managing model tournaments",
1089 | "tools": [
1090 | "create_tournament",
1091 | "list_tournaments",
1092 | "get_tournament_status",
1093 | "get_tournament_results",
1094 | "cancel_tournament",
1095 | ],
1096 | },
1097 | "document_tools": {
1098 | "description": "Tools for document processing (placeholder for future implementation)",
1099 | "tools": [],
1100 | },
1101 | }
1102 |
1103 | @self.mcp.resource("guide://llm")
1104 | def get_llm_guide() -> str:
1105 | """
1106 | Usage guide for LLMs using the Ultimate MCP Server.
1107 |
1108 | This resource provides structured guidance specifically designed for LLMs to
1109 | effectively use the tools and resources provided by the Ultimate MCP Server. It includes
1110 | recommended tool selection strategies, common usage patterns, and examples.
1111 |
1112 | Resource URI: guide://llm
1113 |
1114 | Returns:
1115 | A detailed text guide with sections on tool selection, usage patterns,
1116 | and example workflows.
1117 |
1118 | Usage:
1119 | This resource is primarily intended to be included in context for LLMs
1120 | that will be using the gateway tools, to help them understand how to
1121 | effectively use the available capabilities.
1122 | """
1123 | return """
1124 | # Ultimate MCP Server Usage Guide for Language Models
1125 |
1126 | ## Overview
1127 |
1128 | The Ultimate MCP Server provides a set of tools for accessing multiple language model providers
1129 | (OpenAI, Anthropic, etc.) through a unified interface. This guide will help you understand
1130 | how to effectively use these tools.
1131 |
1132 | ## Tool Selection Guidelines
1133 |
1134 | ### For Text Generation:
1135 |
1136 | 1. For single-prompt text generation:
1137 | - Use `generate_completion` with a specific provider and model
1138 |
1139 | 2. For multi-turn conversations:
1140 | - Use `chat_completion` with a list of message dictionaries
1141 |
1142 | 3. For streaming responses (real-time text output):
1143 | - Use streaming tools in the CompletionTools class
1144 |
1145 | 4. For comparing outputs across providers:
1146 | - Use `multi_completion` with a list of provider configurations
1147 |
1148 | ### For Provider Management:
1149 |
1150 | 1. To check available providers:
1151 | - Use `get_provider_status` to see which providers are available
1152 |
1153 | 2. To list available models:
1154 | - Use `list_models` to view models from all providers or a specific provider
1155 |
1156 | ### For Running Tournaments:
1157 |
1158 | 1. To create a new tournament:
1159 | - Use `create_tournament` with a prompt and list of model IDs
1160 |
1161 | 2. To check tournament status:
1162 | - Use `get_tournament_status` with a tournament ID
1163 |
1164 | 3. To get detailed tournament results:
1165 | - Use `get_tournament_results` with a tournament ID
1166 |
1167 | ## Common Workflows
1168 |
1169 | ### Provider Selection Workflow:
1170 | ```
1171 | 1. Call get_provider_status() to see available providers
1172 | 2. Call list_models(provider="openai") to see available models
1173 | 3. Call generate_completion(prompt="...", provider="openai", model="gpt-4o")
1174 | ```
1175 |
1176 | ### Multi-Provider Comparison Workflow:
1177 | ```
1178 | 1. Call multi_completion(
1179 | prompt="...",
1180 | providers=[
1181 | {"provider": "openai", "model": "gpt-4o"},
1182 | {"provider": "anthropic", "model": "claude-3-opus-20240229"}
1183 | ]
1184 | )
1185 | 2. Compare results from each provider
1186 | ```
1187 |
1188 | ### Tournament Workflow:
1189 | ```
1190 | 1. Call create_tournament(name="...", prompt="...", model_ids=["openai/gpt-4o", "anthropic/claude-3-opus"])
1191 | 2. Store the tournament_id from the response
1192 | 3. Call get_tournament_status(tournament_id="...") to monitor progress
1193 | 4. Once status is "COMPLETED", call get_tournament_results(tournament_id="...")
1194 | ```
1195 |
1196 | ## Error Handling Best Practices
1197 |
1198 | 1. Always check for "error" fields in tool responses
1199 | 2. Verify provider availability before attempting to use specific models
1200 | 3. For tournament tools, handle potential 404 errors for invalid tournament IDs
1201 |
1202 | ## Performance Considerations
1203 |
1204 | 1. Most completion tools include token usage and cost metrics in their responses
1205 | 2. Use caching decorators for repetitive requests to save costs
1206 | 3. Consider using stream=True for long completions to improve user experience
1207 | """
1208 |
1209 | @self.mcp.resource("provider://{{provider_name}}")
1210 | def get_provider_info(provider_name: str) -> Dict[str, Any]:
1211 | """
1212 | Get detailed information about a specific LLM provider.
1213 |
1214 | This resource provides comprehensive information about a specific provider,
1215 | including its capabilities, available models, and configuration status.
1216 |
1217 | Resource URI template: provider://{provider_name}
1218 |
1219 | Args:
1220 | provider_name: Name of the provider to retrieve information for
1221 | (e.g., "openai", "anthropic", "gemini")
1222 |
1223 | Returns:
1224 | Dictionary containing detailed provider information:
1225 | - name: Provider name
1226 | - status: Current status (enabled, available, etc.)
1227 | - capabilities: List of supported capabilities
1228 | - models: List of available models and their details
1229 | - config: Current configuration settings (with sensitive info redacted)
1230 |
1231 | Example:
1232 | {
1233 | "name": "openai",
1234 | "status": {
1235 | "enabled": true,
1236 | "available": true,
1237 | "api_key_configured": true,
1238 | "error": null
1239 | },
1240 | "capabilities": ["chat", "completion", "embeddings", "vision"],
1241 | "models": [
1242 | {
1243 | "id": "gpt-4o",
1244 | "name": "GPT-4o",
1245 | "context_window": 128000,
1246 | "features": ["chat", "completion", "vision"]
1247 | },
1248 | # More models...
1249 | ],
1250 | "config": {
1251 | "base_url": "https://api.openai.com/v1",
1252 | "timeout_seconds": 30,
1253 | "default_model": "gpt-4.1-mini"
1254 | }
1255 | }
1256 |
1257 | Error Handling:
1258 | If the provider doesn't exist or isn't configured, returns an appropriate
1259 | error message in the response.
1260 |
1261 | Usage:
1262 | Use this resource to get detailed information about a specific provider
1263 | before using its models for completions or other operations.
1264 | """
1265 | # Check if provider exists in status dictionary
1266 | provider_status = self.provider_status.get(provider_name)
1267 | if not provider_status:
1268 | return {
1269 | "name": provider_name,
1270 | "error": f"Provider '{provider_name}' not found or not configured",
1271 | "status": {"enabled": False, "available": False, "api_key_configured": False},
1272 | "models": [],
1273 | }
1274 |
1275 | # Get provider instance if available
1276 | provider_instance = self.providers.get(provider_name)
1277 |
1278 | # Build capability list based on provider name
1279 | capabilities = []
1280 | if provider_name in [
1281 | Provider.OPENAI.value,
1282 | Provider.ANTHROPIC.value,
1283 | Provider.GEMINI.value,
1284 | ]:
1285 | capabilities = ["chat", "completion"]
1286 |
1287 | if provider_name == Provider.OPENAI.value:
1288 | capabilities.extend(["embeddings", "vision", "image_generation"])
1289 | elif provider_name == Provider.ANTHROPIC.value:
1290 | capabilities.extend(["vision"])
1291 |
1292 | # Return provider details
1293 | return {
1294 | "name": provider_name,
1295 | "status": {
1296 | "enabled": provider_status.enabled,
1297 | "available": provider_status.available,
1298 | "api_key_configured": provider_status.api_key_configured,
1299 | "error": provider_status.error,
1300 | },
1301 | "capabilities": capabilities,
1302 | "models": provider_status.models,
1303 | "config": {
1304 | # Include non-sensitive config info
1305 | "default_model": provider_instance.default_model if provider_instance else None,
1306 | "timeout_seconds": 30, # Example default
1307 | },
1308 | }
1309 |
1310 | @self.mcp.resource("guide://error-handling")
1311 | def get_error_handling_guide() -> Dict[str, Any]:
1312 | """
1313 | Get comprehensive guidance on handling errors from Ultimate MCP Server tools.
1314 |
1315 | This resource provides detailed information about common error patterns,
1316 | error handling strategies, and recovery approaches for each tool in the
1317 | Ultimate MCP Server. It helps LLMs understand how to gracefully handle and recover
1318 | from various error conditions.
1319 |
1320 | Resource URI: guide://error-handling
1321 |
1322 | Returns:
1323 | Dictionary containing error handling guidance organized by tool type:
1324 | - provider_tools: Error handling for provider-related tools
1325 | - completion_tools: Error handling for completion tools
1326 | - tournament_tools: Error handling for tournament tools
1327 |
1328 | Usage:
1329 | This resource helps LLMs implement robust error handling when using
1330 | the Ultimate MCP Server tools, improving the resilience of their interactions.
1331 | """
1332 | return {
1333 | "general_principles": {
1334 | "error_detection": {
1335 | "description": "How to detect errors in tool responses",
1336 | "patterns": [
1337 | "Check for an 'error' field in the response dictionary",
1338 | "Look for status codes in error messages (e.g., 404, 500)",
1339 | "Check for empty or null results where data is expected",
1340 | "Look for 'warning' fields that may indicate partial success",
1341 | ],
1342 | },
1343 | "error_recovery": {
1344 | "description": "General strategies for recovering from errors",
1345 | "strategies": [
1346 | "Retry with different parameters when appropriate",
1347 | "Fallback to alternative tools or providers",
1348 | "Gracefully degrade functionality when optimal path is unavailable",
1349 | "Clearly communicate errors to users with context and suggestions",
1350 | ],
1351 | },
1352 | },
1353 | "provider_tools": {
1354 | "get_provider_status": {
1355 | "common_errors": [
1356 | {
1357 | "error": "Server context not available",
1358 | "cause": "The server may not be fully initialized",
1359 | "handling": "Wait and retry or report server initialization issue",
1360 | },
1361 | {
1362 | "error": "No providers are currently configured",
1363 | "cause": "No LLM providers are enabled or initialization is incomplete",
1364 | "handling": "Proceed with caution and check if specific providers are required",
1365 | },
1366 | ],
1367 | "recovery_strategies": [
1368 | "If no providers are available, clearly inform the user of limited capabilities",
1369 | "If specific providers are unavailable, suggest alternatives based on task requirements",
1370 | ],
1371 | },
1372 | "list_models": {
1373 | "common_errors": [
1374 | {
1375 | "error": "Invalid provider",
1376 | "cause": "Specified provider name doesn't exist or isn't configured",
1377 | "handling": "Use valid providers from the error message's 'valid_providers' field",
1378 | },
1379 | {
1380 | "warning": "Provider is configured but not available",
1381 | "cause": "Provider API key issues or service connectivity problems",
1382 | "handling": "Use an alternative provider or inform user of limited options",
1383 | },
1384 | ],
1385 | "recovery_strategies": [
1386 | "When provider is invalid, fall back to listing all available providers",
1387 | "When models list is empty, suggest using the default model or another provider",
1388 | ],
1389 | },
1390 | },
1391 | "completion_tools": {
1392 | "generate_completion": {
1393 | "common_errors": [
1394 | {
1395 | "error": "Provider not available",
1396 | "cause": "Specified provider doesn't exist or isn't configured",
1397 | "handling": "Switch to an available provider (check with get_provider_status)",
1398 | },
1399 | {
1400 | "error": "Failed to initialize provider",
1401 | "cause": "API key configuration or network issues",
1402 | "handling": "Try another provider or check provider status",
1403 | },
1404 | {
1405 | "error": "Completion generation failed",
1406 | "cause": "Provider API errors, rate limits, or invalid parameters",
1407 | "handling": "Retry with different parameters or use another provider",
1408 | },
1409 | ],
1410 | "recovery_strategies": [
1411 | "Use multi_completion to try multiple providers simultaneously",
1412 | "Progressively reduce complexity (max_tokens, simplify prompt) if facing limits",
1413 | "Fall back to more reliable models if specialized ones are unavailable",
1414 | ],
1415 | },
1416 | "multi_completion": {
1417 | "common_errors": [
1418 | {
1419 | "error": "Invalid providers format",
1420 | "cause": "Providers parameter is not a list of provider configurations",
1421 | "handling": "Correct the format to a list of dictionaries with provider info",
1422 | },
1423 | {
1424 | "partial_failure": "Some providers failed",
1425 | "cause": "Indicated by successful_count < total_providers",
1426 | "handling": "Use the successful results and analyze error fields for failed ones",
1427 | },
1428 | ],
1429 | "recovery_strategies": [
1430 | "Focus on successful completions even if some providers failed",
1431 | "Check each provider's 'success' field to identify which ones worked",
1432 | "If timeout occurs, consider increasing the timeout parameter or reducing providers",
1433 | ],
1434 | },
1435 | },
1436 | "tournament_tools": {
1437 | "create_tournament": {
1438 | "common_errors": [
1439 | {
1440 | "error": "Invalid input",
1441 | "cause": "Missing required fields or validation errors",
1442 | "handling": "Check all required parameters are provided with valid values",
1443 | },
1444 | {
1445 | "error": "Failed to start tournament execution",
1446 | "cause": "Server resource constraints or initialization errors",
1447 | "handling": "Retry with fewer rounds or models, or try again later",
1448 | },
1449 | ],
1450 | "recovery_strategies": [
1451 | "Verify model IDs are valid before creating tournament",
1452 | "Start with simple tournaments to validate functionality before complex ones",
1453 | "Use error message details to correct specific input problems",
1454 | ],
1455 | },
1456 | "get_tournament_status": {
1457 | "common_errors": [
1458 | {
1459 | "error": "Tournament not found",
1460 | "cause": "Invalid tournament ID or tournament was deleted",
1461 | "handling": "Verify tournament ID or use list_tournaments to see available tournaments",
1462 | },
1463 | {
1464 | "error": "Invalid tournament ID format",
1465 | "cause": "Tournament ID is not a string or is empty",
1466 | "handling": "Ensure tournament ID is a valid string matching the expected format",
1467 | },
1468 | ],
1469 | "recovery_strategies": [
1470 | "When tournament not found, list all tournaments to find valid ones",
1471 | "If tournament status is FAILED, check error_message for details",
1472 | "Implement polling with backoff for monitoring long-running tournaments",
1473 | ],
1474 | },
1475 | },
1476 | "error_pattern_examples": {
1477 | "retry_with_fallback": {
1478 | "description": "Retry with fallback to another provider",
1479 | "example": """
1480 | # Try primary provider
1481 | result = generate_completion(prompt="...", provider="openai", model="gpt-4o")
1482 |
1483 | # Check for errors and fall back if needed
1484 | if "error" in result:
1485 | logger.warning(f"Primary provider failed: {result['error']}")
1486 | # Fall back to alternative provider
1487 | result = generate_completion(prompt="...", provider="anthropic", model="claude-3-opus-20240229")
1488 | """,
1489 | },
1490 | "validation_before_call": {
1491 | "description": "Validate parameters before making tool calls",
1492 | "example": """
1493 | # Get available providers first
1494 | provider_status = get_provider_status()
1495 |
1496 | # Check if requested provider is available
1497 | requested_provider = "openai"
1498 | if requested_provider not in provider_status["providers"] or not provider_status["providers"][requested_provider]["available"]:
1499 | # Fall back to any available provider
1500 | available_providers = [p for p, status in provider_status["providers"].items() if status["available"]]
1501 | if available_providers:
1502 | requested_provider = available_providers[0]
1503 | else:
1504 | return {"error": "No LLM providers are available"}
1505 | """,
1506 | },
1507 | },
1508 | }
1509 |
1510 | @self.mcp.resource("examples://workflows")
1511 | def get_workflow_examples() -> Dict[str, Any]:
1512 | """
1513 | Get comprehensive examples of multi-tool workflows.
1514 |
1515 | This resource provides detailed, executable examples showing how to combine
1516 | multiple tools into common workflows. These examples demonstrate best practices
1517 | for tool sequencing, error handling, and result processing.
1518 |
1519 | Resource URI: examples://workflows
1520 |
1521 | Returns:
1522 | Dictionary containing workflow examples organized by scenario:
1523 | - basic_provider_selection: Example of selecting a provider and model
1524 | - model_comparison: Example of comparing outputs across providers
1525 | - tournaments: Example of creating and monitoring a tournament
1526 | - advanced_chat: Example of a multi-turn conversation with system prompts
1527 |
1528 | Usage:
1529 | These examples are designed to be used as reference by LLMs to understand
1530 | how to combine multiple tools in the Ultimate MCP Server to accomplish common tasks.
1531 | Each example includes expected outputs to help understand the flow.
1532 | """
1533 | return {
1534 | "basic_provider_selection": {
1535 | "description": "Selecting a provider and model for text generation",
1536 | "steps": [
1537 | {
1538 | "step": 1,
1539 | "tool": "get_provider_status",
1540 | "parameters": {},
1541 | "purpose": "Check which providers are available",
1542 | "example_output": {
1543 | "providers": {
1544 | "openai": {"available": True, "models_count": 12},
1545 | "anthropic": {"available": True, "models_count": 6},
1546 | }
1547 | },
1548 | },
1549 | {
1550 | "step": 2,
1551 | "tool": "list_models",
1552 | "parameters": {"provider": "openai"},
1553 | "purpose": "Get available models for the selected provider",
1554 | "example_output": {
1555 | "models": {
1556 | "openai": [
1557 | {
1558 | "id": "gpt-4o",
1559 | "name": "GPT-4o",
1560 | "features": ["chat", "completion"],
1561 | }
1562 | ]
1563 | }
1564 | },
1565 | },
1566 | {
1567 | "step": 3,
1568 | "tool": "generate_completion",
1569 | "parameters": {
1570 | "prompt": "Explain quantum computing in simple terms",
1571 | "provider": "openai",
1572 | "model": "gpt-4o",
1573 | "temperature": 0.7,
1574 | },
1575 | "purpose": "Generate text with the selected provider and model",
1576 | "example_output": {
1577 | "text": "Quantum computing is like...",
1578 | "model": "gpt-4o",
1579 | "provider": "openai",
1580 | "tokens": {"input": 8, "output": 150, "total": 158},
1581 | "cost": 0.000123,
1582 | },
1583 | },
1584 | ],
1585 | "error_handling": [
1586 | "If get_provider_status shows provider unavailable, try a different provider",
1587 | "If list_models returns empty list, select a different provider",
1588 | "If generate_completion returns an error, check the error message for guidance",
1589 | ],
1590 | },
1591 | "model_comparison": {
1592 | "description": "Comparing multiple models on the same task",
1593 | "steps": [
1594 | {
1595 | "step": 1,
1596 | "tool": "multi_completion",
1597 | "parameters": {
1598 | "prompt": "Write a haiku about programming",
1599 | "providers": [
1600 | {"provider": "openai", "model": "gpt-4o"},
1601 | {"provider": "anthropic", "model": "claude-3-opus-20240229"},
1602 | ],
1603 | "temperature": 0.7,
1604 | },
1605 | "purpose": "Generate completions from multiple providers simultaneously",
1606 | "example_output": {
1607 | "results": {
1608 | "openai/gpt-4o": {
1609 | "success": True,
1610 | "text": "Code flows like water\nBugs emerge from the depths\nPatience brings order",
1611 | "model": "gpt-4o",
1612 | },
1613 | "anthropic/claude-3-opus-20240229": {
1614 | "success": True,
1615 | "text": "Fingers dance on keys\nLogic blooms in silent thought\nPrograms come alive",
1616 | "model": "claude-3-opus-20240229",
1617 | },
1618 | },
1619 | "successful_count": 2,
1620 | "total_providers": 2,
1621 | },
1622 | },
1623 | {
1624 | "step": 2,
1625 | "suggestion": "Compare the results for quality, style, and adherence to the haiku format",
1626 | },
1627 | ],
1628 | "error_handling": [
1629 | "Check successful_count vs total_providers to see if all providers succeeded",
1630 | "For each provider, check the success field to determine if it completed successfully",
1631 | "If a provider failed, look at its error field for details",
1632 | ],
1633 | },
1634 | "tournaments": {
1635 | "description": "Creating and monitoring a multi-model tournament",
1636 | "steps": [
1637 | {
1638 | "step": 1,
1639 | "tool": "create_tournament",
1640 | "parameters": {
1641 | "name": "Sorting Algorithm Tournament",
1642 | "prompt": "Implement a quicksort algorithm in Python that handles duplicates efficiently",
1643 | "model_ids": ["openai/gpt-4o", "anthropic/claude-3-opus-20240229"],
1644 | "rounds": 3,
1645 | "tournament_type": "code",
1646 | },
1647 | "purpose": "Create a new tournament comparing multiple models",
1648 | "example_output": {
1649 | "tournament_id": "tour_abc123xyz789",
1650 | "status": "PENDING",
1651 | },
1652 | },
1653 | {
1654 | "step": 2,
1655 | "tool": "get_tournament_status",
1656 | "parameters": {"tournament_id": "tour_abc123xyz789"},
1657 | "purpose": "Check if the tournament has started running",
1658 | "example_output": {
1659 | "tournament_id": "tour_abc123xyz789",
1660 | "status": "RUNNING",
1661 | "current_round": 1,
1662 | "total_rounds": 3,
1663 | },
1664 | },
1665 | {
1666 | "step": 3,
1667 | "suggestion": "Wait for the tournament to complete",
1668 | "purpose": "Tournaments run asynchronously and may take time to complete",
1669 | },
1670 | {
1671 | "step": 4,
1672 | "tool": "get_tournament_results",
1673 | "parameters": {"tournament_id": "tour_abc123xyz789"},
1674 | "purpose": "Retrieve full results once the tournament is complete",
1675 | "example_output": {
1676 | "tournament_id": "tour_abc123xyz789",
1677 | "status": "COMPLETED",
1678 | "rounds_data": [
1679 | {
1680 | "round_number": 1,
1681 | "model_outputs": {
1682 | "openai/gpt-4o": "def quicksort(arr): ...",
1683 | "anthropic/claude-3-opus-20240229": "def quicksort(arr): ...",
1684 | },
1685 | "scores": {
1686 | "openai/gpt-4o": 0.85,
1687 | "anthropic/claude-3-opus-20240229": 0.92,
1688 | },
1689 | }
1690 | # Additional rounds would be here in a real response
1691 | ],
1692 | },
1693 | },
1694 | ],
1695 | "error_handling": [
1696 | "If create_tournament fails, check the error message for missing or invalid parameters",
1697 | "If get_tournament_status returns an error, verify the tournament_id is correct",
1698 | "If tournament status is FAILED, check the error_message field for details",
1699 | ],
1700 | },
1701 | "advanced_chat": {
1702 | "description": "Multi-turn conversation with system prompt and context",
1703 | "steps": [
1704 | {
1705 | "step": 1,
1706 | "tool": "chat_completion",
1707 | "parameters": {
1708 | "messages": [
1709 | {
1710 | "role": "user",
1711 | "content": "Hello, can you help me with Python?",
1712 | }
1713 | ],
1714 | "provider": "anthropic",
1715 | "model": "claude-3-opus-20240229",
1716 | "system_prompt": "You are an expert Python tutor. Provide concise, helpful answers with code examples when appropriate.",
1717 | "temperature": 0.5,
1718 | },
1719 | "purpose": "Start a conversation with a system prompt for context",
1720 | "example_output": {
1721 | "text": "Hello! I'd be happy to help you with Python. What specific aspect are you interested in learning about?",
1722 | "model": "claude-3-opus-20240229",
1723 | "provider": "anthropic",
1724 | },
1725 | },
1726 | {
1727 | "step": 2,
1728 | "tool": "chat_completion",
1729 | "parameters": {
1730 | "messages": [
1731 | {
1732 | "role": "user",
1733 | "content": "Hello, can you help me with Python?",
1734 | },
1735 | {
1736 | "role": "assistant",
1737 | "content": "Hello! I'd be happy to help you with Python. What specific aspect are you interested in learning about?",
1738 | },
1739 | {
1740 | "role": "user",
1741 | "content": "How do I write a function that checks if a string is a palindrome?",
1742 | },
1743 | ],
1744 | "provider": "anthropic",
1745 | "model": "claude-3-opus-20240229",
1746 | "system_prompt": "You are an expert Python tutor. Provide concise, helpful answers with code examples when appropriate.",
1747 | "temperature": 0.5,
1748 | },
1749 | "purpose": "Continue the conversation by including the full message history",
1750 | "example_output": {
1751 | "text": "Here's a simple function to check if a string is a palindrome in Python:\n\n```python\ndef is_palindrome(s):\n # Remove spaces and convert to lowercase for more flexible matching\n s = s.lower().replace(' ', '')\n # Compare the string with its reverse\n return s == s[::-1]\n\n# Examples\nprint(is_palindrome('racecar')) # True\nprint(is_palindrome('hello')) # False\nprint(is_palindrome('A man a plan a canal Panama')) # True\n```\n\nThis function works by:\n1. Converting the string to lowercase and removing spaces\n2. Checking if the processed string equals its reverse (using slice notation `[::-1]`)\n\nIs there anything specific about this solution you'd like me to explain further?",
1752 | "model": "claude-3-opus-20240229",
1753 | "provider": "anthropic",
1754 | },
1755 | },
1756 | ],
1757 | "error_handling": [
1758 | "Always include the full conversation history in the messages array",
1759 | "Ensure each message has both 'role' and 'content' fields",
1760 | "If using system_prompt, ensure it's appropriate for the provider",
1761 | ],
1762 | },
1763 | }
1764 |
1765 | @self.mcp.resource("examples://completions")
1766 | def get_completion_examples() -> Dict[str, Any]:
1767 | """
1768 | Get examples of different completion types and when to use them.
1769 |
1770 | This resource provides detailed examples of different completion tools available
1771 | in the Ultimate MCP Server, along with guidance on when to use each type. It helps with
1772 | selecting the most appropriate completion tool for different scenarios.
1773 |
1774 | Resource URI: examples://completions
1775 |
1776 | Returns:
1777 | Dictionary containing completion examples organized by type:
1778 | - standard_completion: When to use generate_completion
1779 | - chat_completion: When to use chat_completion
1780 | - streaming_completion: When to use stream_completion
1781 | - multi_provider: When to use multi_completion
1782 |
1783 | Usage:
1784 | This resource helps LLMs understand the appropriate completion tool
1785 | to use for different scenarios, with concrete examples and use cases.
1786 | """
1787 | return {
1788 | "standard_completion": {
1789 | "tool": "generate_completion",
1790 | "description": "Single-turn text generation without streaming",
1791 | "best_for": [
1792 | "Simple, one-off text generation tasks",
1793 | "When you need a complete response at once",
1794 | "When you don't need conversation history",
1795 | ],
1796 | "example": {
1797 | "request": {
1798 | "prompt": "Explain the concept of quantum entanglement in simple terms",
1799 | "provider": "openai",
1800 | "model": "gpt-4o",
1801 | "temperature": 0.7,
1802 | },
1803 | "response": {
1804 | "text": "Quantum entanglement is like having two magic coins...",
1805 | "model": "gpt-4o",
1806 | "provider": "openai",
1807 | "tokens": {"input": 10, "output": 150, "total": 160},
1808 | "cost": 0.00032,
1809 | "processing_time": 2.1,
1810 | },
1811 | },
1812 | },
1813 | "chat_completion": {
1814 | "tool": "chat_completion",
1815 | "description": "Multi-turn conversation with message history",
1816 | "best_for": [
1817 | "Maintaining conversation context across multiple turns",
1818 | "When dialogue history matters for the response",
1819 | "When using system prompts to guide assistant behavior",
1820 | ],
1821 | "example": {
1822 | "request": {
1823 | "messages": [
1824 | {"role": "user", "content": "What's the capital of France?"},
1825 | {"role": "assistant", "content": "The capital of France is Paris."},
1826 | {"role": "user", "content": "And what's its population?"},
1827 | ],
1828 | "provider": "anthropic",
1829 | "model": "claude-3-opus-20240229",
1830 | "system_prompt": "You are a helpful geography assistant.",
1831 | },
1832 | "response": {
1833 | "text": "The population of Paris is approximately 2.1 million people in the city proper...",
1834 | "model": "claude-3-opus-20240229",
1835 | "provider": "anthropic",
1836 | "tokens": {"input": 62, "output": 48, "total": 110},
1837 | "cost": 0.00055,
1838 | "processing_time": 1.8,
1839 | },
1840 | },
1841 | },
1842 | "streaming_completion": {
1843 | "tool": "stream_completion",
1844 | "description": "Generates text in smaller chunks as a stream",
1845 | "best_for": [
1846 | "When you need to show incremental progress to users",
1847 | "For real-time display of model outputs",
1848 | "Long-form content generation where waiting for the full response would be too long",
1849 | ],
1850 | "example": {
1851 | "request": {
1852 | "prompt": "Write a short story about a robot learning to paint",
1853 | "provider": "openai",
1854 | "model": "gpt-4o",
1855 | },
1856 | "response_chunks": [
1857 | {
1858 | "text": "In the year 2150, ",
1859 | "chunk_index": 1,
1860 | "provider": "openai",
1861 | "model": "gpt-4o",
1862 | "finished": False,
1863 | },
1864 | {
1865 | "text": "a maintenance robot named ARIA-7 was assigned to",
1866 | "chunk_index": 2,
1867 | "provider": "openai",
1868 | "model": "gpt-4o",
1869 | "finished": False,
1870 | },
1871 | {
1872 | "text": "",
1873 | "chunk_index": 25,
1874 | "provider": "openai",
1875 | "full_text": "In the year 2150, a maintenance robot named ARIA-7 was assigned to...",
1876 | "processing_time": 8.2,
1877 | "finished": True,
1878 | },
1879 | ],
1880 | },
1881 | },
1882 | "multi_provider": {
1883 | "tool": "multi_completion",
1884 | "description": "Get completions from multiple providers simultaneously",
1885 | "best_for": [
1886 | "Comparing outputs from different models",
1887 | "Finding consensus among multiple models",
1888 | "Fallback scenarios where one provider might fail",
1889 | "Benchmarking different providers on the same task",
1890 | ],
1891 | "example": {
1892 | "request": {
1893 | "prompt": "Provide three tips for sustainable gardening",
1894 | "providers": [
1895 | {"provider": "openai", "model": "gpt-4o"},
1896 | {"provider": "anthropic", "model": "claude-3-opus-20240229"},
1897 | ],
1898 | },
1899 | "response": {
1900 | "results": {
1901 | "openai/gpt-4o": {
1902 | "provider_key": "openai/gpt-4o",
1903 | "success": True,
1904 | "text": "1. Use compost instead of chemical fertilizers...",
1905 | "model": "gpt-4o",
1906 | },
1907 | "anthropic/claude-3-opus-20240229": {
1908 | "provider_key": "anthropic/claude-3-opus-20240229",
1909 | "success": True,
1910 | "text": "1. Implement water conservation techniques...",
1911 | "model": "claude-3-opus-20240229",
1912 | },
1913 | },
1914 | "successful_count": 2,
1915 | "total_providers": 2,
1916 | "processing_time": 3.5,
1917 | },
1918 | },
1919 | },
1920 | }
1921 |
1922 | @self.mcp.resource("examples://tournaments")
1923 | def get_tournament_examples() -> Dict[str, Any]:
1924 | """
1925 | Get detailed examples and guidance for running LLM tournaments.
1926 |
1927 | This resource provides comprehensive examples and guidance for creating,
1928 | monitoring, and analyzing LLM tournaments. It includes detailed information
1929 | about tournament configuration, interpreting results, and best practices.
1930 |
1931 | Resource URI: examples://tournaments
1932 |
1933 | Returns:
1934 | Dictionary containing tournament examples and guidance:
1935 | - tournament_types: Different types of tournaments and their uses
1936 | - configuration_guide: Guidance on how to configure tournaments
1937 | - analysis_guide: How to interpret tournament results
1938 | - example_tournaments: Complete examples of different tournament configurations
1939 |
1940 | Usage:
1941 | This resource helps LLMs understand how to effectively use the tournament
1942 | tools, with guidance on configuration, execution, and analysis.
1943 | """
1944 | return {
1945 | "tournament_types": {
1946 | "code": {
1947 | "description": "Tournaments where models compete on coding tasks",
1948 | "ideal_for": [
1949 | "Algorithm implementation challenges",
1950 | "Debugging exercises",
1951 | "Code optimization problems",
1952 | "Comparing models' coding abilities",
1953 | ],
1954 | "evaluation_criteria": [
1955 | "Code correctness",
1956 | "Efficiency",
1957 | "Readability",
1958 | "Error handling",
1959 | ],
1960 | },
1961 | # Other tournament types could be added in the future
1962 | },
1963 | "configuration_guide": {
1964 | "model_selection": {
1965 | "description": "Guidelines for selecting models to include in tournaments",
1966 | "recommendations": [
1967 | "Include models from different providers for diverse approaches",
1968 | "Compare models within the same family (e.g., different Claude versions)",
1969 | "Consider including both specialized and general models",
1970 | "Ensure all models can handle the task complexity",
1971 | ],
1972 | },
1973 | "rounds": {
1974 | "description": "How to determine the appropriate number of rounds",
1975 | "recommendations": [
1976 | "Start with 3 rounds for most tournaments",
1977 | "Use more rounds (5+) for more complex or nuanced tasks",
1978 | "Consider that each round increases total runtime and cost",
1979 | "Each round gives models a chance to refine their solutions",
1980 | ],
1981 | },
1982 | "prompt_design": {
1983 | "description": "Best practices for tournament prompt design",
1984 | "recommendations": [
1985 | "Be specific about the problem requirements",
1986 | "Clearly define evaluation criteria",
1987 | "Specify output format expectations",
1988 | "Consider including test cases",
1989 | "Avoid ambiguous or underspecified requirements",
1990 | ],
1991 | },
1992 | },
1993 | "analysis_guide": {
1994 | "score_interpretation": {
1995 | "description": "How to interpret model scores in tournament results",
1996 | "guidance": [
1997 | "Scores are normalized to a 0-1 scale (1 being perfect)",
1998 | "Consider relative scores between models rather than absolute values",
1999 | "Look for consistency across rounds",
2000 | "Consider output quality even when scores are similar",
2001 | ],
2002 | },
2003 | "output_analysis": {
2004 | "description": "How to analyze model outputs from tournaments",
2005 | "guidance": [
2006 | "Compare approaches used by different models",
2007 | "Look for patterns in errors or limitations",
2008 | "Identify unique strengths of different providers",
2009 | "Consider both the score and actual output quality",
2010 | ],
2011 | },
2012 | },
2013 | "example_tournaments": {
2014 | "algorithm_implementation": {
2015 | "name": "Binary Search Algorithm",
2016 | "prompt": "Implement a binary search algorithm in Python that can search for an element in a sorted array. Include proper error handling, documentation, and test cases.",
2017 | "model_ids": ["openai/gpt-4o", "anthropic/claude-3-opus-20240229"],
2018 | "rounds": 3,
2019 | "tournament_type": "code",
2020 | "explanation": "This tournament tests the models' ability to implement a standard algorithm with proper error handling and testing.",
2021 | },
2022 | "code_optimization": {
2023 | "name": "String Processing Optimization",
2024 | "prompt": "Optimize the following Python function to process large strings more efficiently: def find_substring_occurrences(text, pattern): return [i for i in range(len(text)) if text[i:i+len(pattern)] == pattern]",
2025 | "model_ids": [
2026 | "openai/gpt-4o",
2027 | "anthropic/claude-3-opus-20240229",
2028 | "anthropic/claude-3-sonnet-20240229",
2029 | ],
2030 | "rounds": 4,
2031 | "tournament_type": "code",
2032 | "explanation": "This tournament compares models' ability to recognize and implement optimization opportunities in existing code.",
2033 | },
2034 | },
2035 | "workflow_examples": {
2036 | "basic_tournament": {
2037 | "description": "A simple tournament workflow from creation to result analysis",
2038 | "steps": [
2039 | {
2040 | "step": 1,
2041 | "description": "Create the tournament",
2042 | "code": "tournament_id = create_tournament(name='Sorting Algorithm Challenge', prompt='Implement an efficient sorting algorithm...', model_ids=['openai/gpt-4o', 'anthropic/claude-3-opus-20240229'], rounds=3, tournament_type='code')",
2043 | },
2044 | {
2045 | "step": 2,
2046 | "description": "Poll for tournament status",
2047 | "code": "status = get_tournament_status(tournament_id)['status']\nwhile status in ['PENDING', 'RUNNING']:\n time.sleep(30) # Check every 30 seconds\n status = get_tournament_status(tournament_id)['status']",
2048 | },
2049 | {
2050 | "step": 3,
2051 | "description": "Retrieve and analyze results",
2052 | "code": "results = get_tournament_results(tournament_id)\nwinner = max(results['final_scores'].items(), key=lambda x: x[1])[0]\noutputs = {model_id: results['rounds_data'][-1]['model_outputs'][model_id] for model_id in results['config']['model_ids']}",
2053 | },
2054 | ],
2055 | }
2056 | },
2057 | }
2058 |
2059 |
2060 | def start_server(
2061 | host: Optional[str] = None,
2062 | port: Optional[int] = None,
2063 | workers: Optional[int] = None,
2064 | log_level: Optional[str] = None,
2065 | reload: bool = False,
2066 | transport_mode: str = "streamable-http",
2067 | include_tools: Optional[List[str]] = None,
2068 | exclude_tools: Optional[List[str]] = None,
2069 | load_all_tools: bool = False, # Added: Flag to control tool loading
2070 | ) -> None:
2071 | """
2072 | Start the Ultimate MCP Server with configurable settings.
2073 |
2074 | This function serves as the main entry point for starting the Ultimate MCP Server
2075 | in either SSE (HTTP server) or stdio (direct process communication) mode. It handles
2076 | complete server initialization including:
2077 |
2078 | 1. Configuration loading and parameter validation
2079 | 2. Logging setup with proper levels and formatting
2080 | 3. Gateway instantiation with tool registration
2081 | 4. Transport mode selection and server startup
2082 |
2083 | The function provides flexibility in server configuration through parameters that
2084 | override settings from the configuration file, allowing for quick adjustments without
2085 | modifying configuration files. It also supports tool filtering, enabling selective
2086 | registration of specific tools.
2087 |
2088 | Args:
2089 | host: Hostname or IP address to bind the server to (e.g., "localhost", "0.0.0.0").
2090 | If None, uses the value from the configuration file.
2091 | port: TCP port for the server to listen on when in SSE mode.
2092 | If None, uses the value from the configuration file.
2093 | workers: Number of worker processes to spawn for handling requests.
2094 | Higher values improve concurrency but increase resource usage.
2095 | If None, uses the value from the configuration file.
2096 | log_level: Logging verbosity level. One of "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL".
2097 | If None, uses the value from the configuration file.
2098 | reload: Whether to automatically reload the server when code changes are detected.
2099 | Useful during development but not recommended for production.
2100 | transport_mode: Communication mode for the server. Options:
2101 | - "stdio": Run using standard input/output for direct process communication (default)
2102 | - "sse": Run as an HTTP server with Server-Sent Events for streaming
2103 | - "streamable-http": Run as an HTTP server with streaming request/response bodies (recommended for HTTP clients)
2104 | include_tools: Optional list of specific tool names to include in registration.
2105 | If provided, only these tools will be registered unless they are
2106 | also in exclude_tools. If None, all tools are included by default.
2107 | exclude_tools: Optional list of tool names to exclude from registration.
2108 | These tools will not be registered even if they are also in include_tools.
2109 | load_all_tools: If True, load all available tools. If False (default), load only the base set.
2110 |
2111 | Raises:
2112 | ValueError: If transport_mode is not one of the valid options.
2113 | ConfigurationError: If there are critical errors in the server configuration.
2114 |
2115 | Note:
2116 | This function does not return as it initiates the server event loop, which
2117 | runs until interrupted (e.g., by a SIGINT signal). In SSE mode, it starts
2118 | a Uvicorn server; in stdio mode, it runs the FastMCP stdio handler.
2119 | """
2120 | server_host = host or get_config().server.host
2121 | server_port = port or get_config().server.port
2122 | server_workers = workers or get_config().server.workers
2123 |
2124 | # Get the current config and update tool registration settings
2125 | cfg = get_config()
2126 | if include_tools or exclude_tools:
2127 | cfg.tool_registration.filter_enabled = True
2128 |
2129 | if include_tools:
2130 | cfg.tool_registration.included_tools = include_tools
2131 |
2132 | if exclude_tools:
2133 | cfg.tool_registration.excluded_tools = exclude_tools
2134 |
2135 | # Validate transport_mode
2136 | if transport_mode not in ["sse", "stdio", "streamable-http"]:
2137 | raise ValueError(
2138 | f"Invalid transport_mode: {transport_mode}. Must be 'sse', 'stdio', or 'streamable-http'"
2139 | )
2140 |
2141 | # Determine final log level from the provided parameter or fallback to INFO
2142 | final_log_level = (log_level or "INFO").upper()
2143 |
2144 | # Update LOGGING_CONFIG with the final level
2145 | LOGGING_CONFIG["root"]["level"] = final_log_level
2146 | LOGGING_CONFIG["loggers"]["ultimate_mcp_server"]["level"] = final_log_level
2147 | LOGGING_CONFIG["loggers"]["ultimate_mcp_server.tools"]["level"] = final_log_level
2148 | LOGGING_CONFIG["loggers"]["ultimate_mcp_server.completions"]["level"] = final_log_level
2149 |
2150 | # Set Uvicorn access level based on final level
2151 | LOGGING_CONFIG["loggers"]["uvicorn.access"]["level"] = (
2152 | final_log_level if final_log_level != "CRITICAL" else "CRITICAL"
2153 | )
2154 |
2155 | # Ensure Uvicorn base/error logs are at least INFO unless final level is DEBUG
2156 | uvicorn_base_level = "INFO" if final_log_level not in ["DEBUG"] else "DEBUG"
2157 | LOGGING_CONFIG["loggers"]["uvicorn"]["level"] = uvicorn_base_level
2158 | LOGGING_CONFIG["loggers"]["uvicorn.error"]["level"] = uvicorn_base_level
2159 |
2160 | # Configure logging
2161 | logging.config.dictConfig(LOGGING_CONFIG)
2162 |
2163 | # Initialize the gateway if not already created
2164 | global _gateway_instance
2165 | if not _gateway_instance:
2166 | # Create gateway with tool filtering based on config
2167 | cfg = get_config()
2168 | _gateway_instance = Gateway(
2169 | name=cfg.server.name,
2170 | register_tools=True,
2171 | load_all_tools=load_all_tools, # Pass the flag to Gateway
2172 | )
2173 |
2174 | # Log startup info to stderr instead of using logging directly
2175 | print("Starting Ultimate MCP Server server", file=sys.stderr)
2176 | print(f"Host: {server_host}", file=sys.stderr)
2177 | print(f"Port: {server_port}", file=sys.stderr)
2178 | print(f"Workers: {server_workers}", file=sys.stderr)
2179 | print(f"Log level: {final_log_level}", file=sys.stderr)
2180 | print(f"Transport mode: {transport_mode}", file=sys.stderr)
2181 | if transport_mode == "streamable-http":
2182 | print(
2183 | "Note: streamable-http is the recommended transport for HTTP-based MCP clients",
2184 | file=sys.stderr,
2185 | )
2186 |
2187 | # Log tool loading strategy
2188 | if load_all_tools:
2189 | print("Tool Loading: ALL available tools", file=sys.stderr)
2190 | else:
2191 | print("Tool Loading: Base Toolset Only", file=sys.stderr)
2192 | base_toolset = [
2193 | "completion",
2194 | "filesystem",
2195 | "optimization",
2196 | "provider",
2197 | "local_text",
2198 | "search",
2199 | ]
2200 | print(f" (Includes: {', '.join(base_toolset)})", file=sys.stderr)
2201 |
2202 | # Log tool filtering info if enabled
2203 | if cfg.tool_registration.filter_enabled:
2204 | if cfg.tool_registration.included_tools:
2205 | print(
2206 | f"Including tools: {', '.join(cfg.tool_registration.included_tools)}",
2207 | file=sys.stderr,
2208 | )
2209 | if cfg.tool_registration.excluded_tools:
2210 | print(
2211 | f"Excluding tools: {', '.join(cfg.tool_registration.excluded_tools)}",
2212 | file=sys.stderr,
2213 | )
2214 |
2215 | if transport_mode in ["sse", "streamable-http"]:
2216 | # Run in HTTP mode (unified handling for both SSE and streamable-http)
2217 | import os
2218 | import subprocess
2219 | import threading
2220 | import time
2221 |
2222 | import uvicorn
2223 |
2224 | print(f"Running in {transport_mode} mode...", file=sys.stderr)
2225 |
2226 | # Set up a function to run the tool context estimator after the server starts
2227 | def run_tool_context_estimator():
2228 | # Wait a bit for the server to start up
2229 | time.sleep(5)
2230 | try:
2231 | # Ensure tools_list.json exists
2232 | if not os.path.exists("tools_list.json"):
2233 | print("\n--- Tool Context Window Analysis ---", file=sys.stderr)
2234 | print(
2235 | "Error: tools_list.json not found. Tool registration may have failed.",
2236 | file=sys.stderr,
2237 | )
2238 | print(
2239 | "The tool context estimator will run with limited functionality.",
2240 | file=sys.stderr,
2241 | )
2242 | print("-" * 40, file=sys.stderr)
2243 |
2244 | # Run the tool context estimator script with appropriate transport
2245 | cmd = ["python", "-m", "mcp_tool_context_estimator", "--quiet"]
2246 | # Pass transport mode for both HTTP transports (sse and streamable-http)
2247 | if transport_mode in ["sse", "streamable-http"]:
2248 | cmd.extend(["--transport", transport_mode])
2249 |
2250 | result = subprocess.run(cmd, capture_output=True, text=True)
2251 |
2252 | # Output the results to stderr
2253 | if result.stdout:
2254 | print("\n--- Tool Context Window Analysis ---", file=sys.stderr)
2255 | print(result.stdout, file=sys.stderr)
2256 | print("-" * 40, file=sys.stderr)
2257 | # Check if there was an error
2258 | if result.returncode != 0:
2259 | print("\n--- Tool Context Estimator Error ---", file=sys.stderr)
2260 | print(
2261 | "Failed to run mcp_tool_context_estimator.py - likely due to an error.",
2262 | file=sys.stderr,
2263 | )
2264 | print("Error output:", file=sys.stderr)
2265 | print(result.stderr, file=sys.stderr)
2266 | print("-" * 40, file=sys.stderr)
2267 | except Exception as e:
2268 | print(f"\nError running tool context estimator: {str(e)}", file=sys.stderr)
2269 | print(
2270 | "Check if mcp_tool_context_estimator.py exists and is executable.",
2271 | file=sys.stderr,
2272 | )
2273 |
2274 | # Skip the tool-context estimator for SSE transport because it causes the server
2275 | # to shut down when the estimator disconnects after completing its analysis.
2276 | # SSE servers shut down when all clients disconnect, and the estimator is treated
2277 | # as a client. Run it for streamable-http mode where this isn't an issue.
2278 | if transport_mode == "streamable-http" and os.path.exists("mcp_tool_context_estimator.py"):
2279 | threading.Thread(target=run_tool_context_estimator, daemon=True).start()
2280 |
2281 | # Setup graceful shutdown
2282 | logger = logging.getLogger("ultimate_mcp_server.server")
2283 |
2284 | # Configure graceful shutdown with error suppression
2285 | enable_quiet_shutdown()
2286 |
2287 | # Create a shutdown handler for gateway cleanup
2288 | async def cleanup_resources():
2289 | """Performs cleanup for various components during shutdown."""
2290 |
2291 | # First attempt quick tasks then long tasks with timeouts
2292 | print("Cleaning up Gateway instance and associated resources...", file=sys.stderr)
2293 |
2294 | # Shutdown SQL Tools with timeout
2295 | try:
2296 | await asyncio.wait_for(shutdown_sql_tools(), timeout=3.0)
2297 | except (asyncio.TimeoutError, Exception):
2298 | pass # Suppress errors during shutdown
2299 |
2300 | # Shutdown Connection Manager with timeout
2301 | try:
2302 | from ultimate_mcp_server.tools.sql_databases import _connection_manager
2303 |
2304 | await asyncio.wait_for(_connection_manager.shutdown(), timeout=2.0)
2305 | except (asyncio.TimeoutError, Exception):
2306 | pass # Suppress errors during shutdown
2307 |
2308 | # Shutdown Smart Browser with timeout
2309 | try:
2310 | await asyncio.wait_for(smart_browser_shutdown(), timeout=5.0)
2311 | except (asyncio.TimeoutError, Exception):
2312 | pass # Suppress errors during shutdown
2313 |
2314 | # Register the cleanup function with the graceful shutdown system
2315 | register_shutdown_handler(cleanup_resources)
2316 |
2317 | # Create FastMCP app with proper path configuration
2318 | if transport_mode == "sse":
2319 | # Mark the gateway instance as SSE mode for lifespan management
2320 | _gateway_instance._sse_mode = True
2321 |
2322 | mcp_app = _gateway_instance.mcp.http_app(transport="sse", path="/sse")
2323 | print("Note: Running in legacy SSE mode.", file=sys.stderr)
2324 |
2325 | # Add SSE keepalive mechanism to prevent automatic shutdown
2326 | def sse_keepalive():
2327 | """Keepalive thread to prevent SSE server from shutting down when no clients are connected."""
2328 | while True:
2329 | time.sleep(30) # Send keepalive every 30 seconds
2330 | try:
2331 | # This simple presence keeps the server alive
2332 | # The actual SSE connections will handle their own keepalive
2333 | pass
2334 | except Exception:
2335 | # If there's any error, just continue
2336 | pass
2337 |
2338 | # Start the keepalive thread as a daemon so it doesn't prevent shutdown
2339 | keepalive_thread = threading.Thread(target=sse_keepalive, daemon=True, name="SSE-Keepalive")
2340 | keepalive_thread.start()
2341 | print("SSE keepalive thread started to prevent automatic shutdown.", file=sys.stderr)
2342 |
2343 | else: # This path is for streamable-http
2344 | mcp_app = _gateway_instance.mcp.http_app(path="/mcp")
2345 |
2346 | print(f"Running in {transport_mode} mode...", file=sys.stderr)
2347 | print(f"[DEBUG] {transport_mode} app type: {type(mcp_app)}", file=sys.stderr)
2348 |
2349 | # === BEGIN NEW SPLIT-APP ARCHITECTURE ===
2350 | from starlette.applications import Starlette
2351 | from starlette.routing import Mount
2352 |
2353 | # 1) PRISTINE FastMCP wrapper – **NO** extra routes
2354 | mcp_starlette = Starlette(
2355 | routes=[Mount("/", mcp_app)],
2356 | lifespan=mcp_app.lifespan,
2357 | )
2358 |
2359 | # 2) FastAPI application for rich REST APIs & automatic docs
2360 | api_app = FastAPI(
2361 | title="Ultimate MCP Server API",
2362 | description="REST API endpoints for the Ultimate MCP Server",
2363 | version="1.0.0",
2364 | docs_url="/docs",
2365 | redoc_url="/redoc",
2366 | openapi_url="/openapi.json",
2367 | )
2368 |
2369 | # Add CORS middleware (FastAPI uses Starlette under the hood)
2370 | api_app.add_middleware(
2371 | CORSMiddleware,
2372 | allow_origins=["*"],
2373 | allow_methods=["*"],
2374 | allow_headers=["*"],
2375 | allow_credentials=True,
2376 | )
2377 |
2378 | endpoint_path = "/sse" if transport_mode == "sse" else "/mcp"
2379 |
2380 | # Setup all UMS API endpoints
2381 | setup_ums_api(api_app)
2382 |
2383 | # --- UMS Explorer Placeholder ---
2384 | # 3) Combined application – avoid overlapping mounts
2385 | final_app = Starlette(
2386 | routes=[
2387 | Mount(endpoint_path, mcp_starlette), # /mcp or /sse
2388 | Mount("/api", api_app), # REST API under /api
2389 | ],
2390 | lifespan=mcp_app.lifespan,
2391 | )
2392 |
2393 | # Logging of endpoints for clarity
2394 | print(
2395 | f"{transport_mode.upper()} endpoint available at: http://{server_host}:{server_port}{endpoint_path}",
2396 | file=sys.stderr,
2397 | )
2398 | print(
2399 | f"API endpoints available at: http://{server_host}:{server_port}/api/*",
2400 | file=sys.stderr,
2401 | )
2402 | print(
2403 | f"UMS Explorer available at: http://{server_host}:{server_port}/api/ums-explorer",
2404 | file=sys.stderr,
2405 | )
2406 | print(
2407 | f"Swagger UI available at: http://{server_host}:{server_port}/api/docs",
2408 | file=sys.stderr,
2409 | )
2410 | print(
2411 | f"ReDoc available at: http://{server_host}:{server_port}/api/redoc",
2412 | file=sys.stderr,
2413 | )
2414 | print(
2415 | f"OpenAPI spec available at: http://{server_host}:{server_port}/api/openapi.json",
2416 | file=sys.stderr,
2417 | )
2418 | print(
2419 | f"Discovery endpoint available at: http://{server_host}:{server_port}/",
2420 | file=sys.stderr,
2421 | )
2422 | # === END NEW SPLIT-APP ARCHITECTURE ===
2423 |
2424 | # Use our custom quiet Uvicorn server for silent shutdown
2425 | config = uvicorn.Config(
2426 | final_app,
2427 | host=server_host,
2428 | port=server_port,
2429 | log_config=LOGGING_CONFIG,
2430 | log_level=final_log_level.lower(),
2431 | lifespan="on", # This tells uvicorn to look for and use the app's lifespan
2432 | )
2433 | server = create_quiet_server(config)
2434 | server.run()
2435 | else: # stdio mode
2436 | # --- Stdio Mode Execution ---
2437 | logger.info("Running in stdio mode...")
2438 |
2439 | # Create a shutdown handler for stdio mode cleanup
2440 | async def cleanup_resources():
2441 | """Performs cleanup for various components during shutdown."""
2442 |
2443 | print("Cleaning up Gateway instance and associated resources...", file=sys.stderr)
2444 |
2445 | # Shutdown SQL Tools with timeout
2446 | try:
2447 | await asyncio.wait_for(shutdown_sql_tools(), timeout=3.0)
2448 | except (asyncio.TimeoutError, Exception):
2449 | pass # Suppress errors during shutdown
2450 |
2451 | # Shutdown Connection Manager with timeout
2452 | try:
2453 | from ultimate_mcp_server.tools.sql_databases import _connection_manager
2454 |
2455 | await asyncio.wait_for(_connection_manager.shutdown(), timeout=2.0)
2456 | except (asyncio.TimeoutError, Exception):
2457 | pass # Suppress errors during shutdown
2458 |
2459 | # Shutdown Smart Browser with timeout
2460 | try:
2461 | await asyncio.wait_for(smart_browser_shutdown(), timeout=5.0)
2462 | except (asyncio.TimeoutError, Exception):
2463 | pass # Suppress errors during shutdown
2464 |
2465 | # Configure graceful shutdown with error suppression
2466 | enable_quiet_shutdown()
2467 |
2468 | # Register the same cleanup function for stdio mode
2469 | register_shutdown_handler(cleanup_resources)
2470 |
2471 | try:
2472 | # Run the FastMCP stdio loop - this will block until interrupted
2473 | _gateway_instance.mcp.run()
2474 | except (KeyboardInterrupt, SystemExit):
2475 | # Normal shutdown - handled by graceful shutdown system
2476 | pass
2477 | except Exception:
2478 | # Any other error - also handled by graceful shutdown
2479 | pass
2480 | # --- End Stdio Mode ---
2481 |
2482 | # --- Post-Server Exit ---
2483 | logger.info("Server loop exited.")
2484 |
```