Breaking change: openrouter_models.json -> custom_models.json
* Support for Custom URLs and custom models, including locally hosted models such as ollama * Support for native + openrouter + local models (i.e. dozens of models) means you can start delegating sub-tasks to particular models or work to local models such as localizations or other boring work etc. * Several tests added * precommit to also include untracked (new) files * Logfile auto rollover * Improved logging
This commit is contained in:
65
server.py
65
server.py
@@ -24,6 +24,7 @@ import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any
|
||||
|
||||
from mcp.server import Server
|
||||
@@ -79,16 +80,17 @@ logging.basicConfig(
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.setFormatter(LocalTimeFormatter(log_format))
|
||||
|
||||
# Add file handler for Docker log monitoring
|
||||
# Add rotating file handler for Docker log monitoring
|
||||
try:
|
||||
file_handler = logging.FileHandler("/tmp/mcp_server.log")
|
||||
# Main server log with rotation (10MB max, keep 2 files)
|
||||
file_handler = RotatingFileHandler("/tmp/mcp_server.log", maxBytes=10 * 1024 * 1024, backupCount=2)
|
||||
file_handler.setLevel(getattr(logging, log_level, logging.INFO))
|
||||
file_handler.setFormatter(LocalTimeFormatter(log_format))
|
||||
logging.getLogger().addHandler(file_handler)
|
||||
|
||||
# Create a special logger for MCP activity tracking
|
||||
# Create a special logger for MCP activity tracking with rotation
|
||||
mcp_logger = logging.getLogger("mcp_activity")
|
||||
mcp_file_handler = logging.FileHandler("/tmp/mcp_activity.log")
|
||||
mcp_file_handler = RotatingFileHandler("/tmp/mcp_activity.log", maxBytes=10 * 1024 * 1024, backupCount=2)
|
||||
mcp_file_handler.setLevel(logging.INFO)
|
||||
mcp_file_handler.setFormatter(LocalTimeFormatter("%(asctime)s - %(message)s"))
|
||||
mcp_logger.addHandler(mcp_file_handler)
|
||||
@@ -128,6 +130,7 @@ def configure_providers():
|
||||
"""
|
||||
from providers import ModelProviderRegistry
|
||||
from providers.base import ProviderType
|
||||
from providers.custom import CustomProvider
|
||||
from providers.gemini import GeminiModelProvider
|
||||
from providers.openai import OpenAIModelProvider
|
||||
from providers.openrouter import OpenRouterProvider
|
||||
@@ -135,6 +138,7 @@ def configure_providers():
|
||||
valid_providers = []
|
||||
has_native_apis = False
|
||||
has_openrouter = False
|
||||
has_custom = False
|
||||
|
||||
# Check for Gemini API key
|
||||
gemini_key = os.getenv("GEMINI_API_KEY")
|
||||
@@ -157,31 +161,68 @@ def configure_providers():
|
||||
has_openrouter = True
|
||||
logger.info("OpenRouter API key found - Multiple models available via OpenRouter")
|
||||
|
||||
# Register providers - native APIs first to ensure they take priority
|
||||
# Check for custom API endpoint (Ollama, vLLM, etc.)
|
||||
custom_url = os.getenv("CUSTOM_API_URL")
|
||||
if custom_url:
|
||||
# IMPORTANT: Always read CUSTOM_API_KEY even if empty
|
||||
# - Some providers (vLLM, LM Studio, enterprise APIs) require authentication
|
||||
# - Others (Ollama) work without authentication (empty key)
|
||||
# - DO NOT remove this variable - it's needed for provider factory function
|
||||
custom_key = os.getenv("CUSTOM_API_KEY", "") # Default to empty (Ollama doesn't need auth)
|
||||
custom_model = os.getenv("CUSTOM_MODEL_NAME", "llama3.2")
|
||||
valid_providers.append(f"Custom API ({custom_url})")
|
||||
has_custom = True
|
||||
logger.info(f"Custom API endpoint found: {custom_url} with model {custom_model}")
|
||||
if custom_key:
|
||||
logger.debug("Custom API key provided for authentication")
|
||||
else:
|
||||
logger.debug("No custom API key provided (using unauthenticated access)")
|
||||
|
||||
# Register providers in priority order:
|
||||
# 1. Native APIs first (most direct and efficient)
|
||||
if has_native_apis:
|
||||
if gemini_key and gemini_key != "your_gemini_api_key_here":
|
||||
ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
|
||||
if openai_key and openai_key != "your_openai_api_key_here":
|
||||
ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
|
||||
|
||||
# Register OpenRouter last so native APIs take precedence
|
||||
# 2. Custom provider second (for local/private models)
|
||||
if has_custom:
|
||||
# Factory function that creates CustomProvider with proper parameters
|
||||
def custom_provider_factory(api_key=None):
|
||||
# api_key is CUSTOM_API_KEY (can be empty for Ollama), base_url from CUSTOM_API_URL
|
||||
base_url = os.getenv("CUSTOM_API_URL", "")
|
||||
return CustomProvider(api_key=api_key or "", base_url=base_url) # Use provided API key or empty string
|
||||
|
||||
ModelProviderRegistry.register_provider(ProviderType.CUSTOM, custom_provider_factory)
|
||||
|
||||
# 3. OpenRouter last (catch-all for everything else)
|
||||
if has_openrouter:
|
||||
ModelProviderRegistry.register_provider(ProviderType.OPENROUTER, OpenRouterProvider)
|
||||
|
||||
# Require at least one valid provider
|
||||
if not valid_providers:
|
||||
raise ValueError(
|
||||
"At least one API key is required. Please set either:\n"
|
||||
"At least one API configuration is required. Please set either:\n"
|
||||
"- GEMINI_API_KEY for Gemini models\n"
|
||||
"- OPENAI_API_KEY for OpenAI o3 model\n"
|
||||
"- OPENROUTER_API_KEY for OpenRouter (multiple models)"
|
||||
"- OPENROUTER_API_KEY for OpenRouter (multiple models)\n"
|
||||
"- CUSTOM_API_URL for local models (Ollama, vLLM, etc.)"
|
||||
)
|
||||
|
||||
logger.info(f"Available providers: {', '.join(valid_providers)}")
|
||||
|
||||
# Log provider priority if both are configured
|
||||
if has_native_apis and has_openrouter:
|
||||
logger.info("Provider priority: Native APIs (Gemini, OpenAI) will be checked before OpenRouter")
|
||||
# Log provider priority
|
||||
priority_info = []
|
||||
if has_native_apis:
|
||||
priority_info.append("Native APIs (Gemini, OpenAI)")
|
||||
if has_custom:
|
||||
priority_info.append("Custom endpoints")
|
||||
if has_openrouter:
|
||||
priority_info.append("OpenRouter (catch-all)")
|
||||
|
||||
if len(priority_info) > 1:
|
||||
logger.info(f"Provider priority: {' → '.join(priority_info)}")
|
||||
|
||||
|
||||
@server.list_tools()
|
||||
@@ -536,7 +577,7 @@ async def handle_get_version() -> list[TextContent]:
|
||||
if ModelProviderRegistry.get_provider(ProviderType.OPENAI):
|
||||
configured_providers.append("OpenAI (o3, o3-mini)")
|
||||
if ModelProviderRegistry.get_provider(ProviderType.OPENROUTER):
|
||||
configured_providers.append("OpenRouter (configured via conf/openrouter_models.json)")
|
||||
configured_providers.append("OpenRouter (configured via conf/custom_models.json)")
|
||||
|
||||
# Format the information in a human-readable way
|
||||
text = f"""Zen MCP Server v{__version__}
|
||||
|
||||
Reference in New Issue
Block a user