Skip to main content
Glama
server.py43.5 kB
import sys import os import traceback import argparse import yaml import json import logging as py_logging from typing import Annotated from pydantic import Field # Initialize logging py_logging.basicConfig(level=py_logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') # Support relative imports try: from .recursive_thinking_ai import EnhancedRecursiveThinkingChat py_logging.debug("Imported EnhancedRecursiveThinkingChat via relative import") except ImportError as e: py_logging.debug(f"Relative import failed: {e}, trying absolute import") try: # When executed directly from cort_mcp.recursive_thinking_ai import EnhancedRecursiveThinkingChat py_logging.debug("Imported EnhancedRecursiveThinkingChat via absolute import") except ImportError as e2: py_logging.debug(f"Absolute import failed: {e2}, trying sys.path modification") # When executed in development mode src_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) py_logging.debug(f"Adding path to sys.path: {src_path}") sys.path.append(src_path) try: from recursive_thinking_ai import EnhancedRecursiveThinkingChat py_logging.debug("Imported EnhancedRecursiveThinkingChat via sys.path modification") except ImportError as e3: py_logging.error(f"All import attempts failed: {e3}") raise # Import MCP server library try: from fastmcp import FastMCP py_logging.debug("Imported FastMCP from fastmcp package") except ImportError as e: py_logging.debug(f"Import from fastmcp failed: {e}, trying mcp.server.fastmcp") try: from mcp.server.fastmcp import FastMCP py_logging.debug("Imported FastMCP from mcp.server.fastmcp") except ImportError as e2: py_logging.error(f"Failed to import FastMCP: {e2}") raise # Define default values as constants DEFAULT_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free" DEFAULT_PROVIDER = "openrouter" # --- Logging Setup --- def setup_logging(log: str, logfile: str): print(f"[DEBUG_SETUP] setup_logging called with log='{log}', logfile='{logfile}'", file=sys.stderr) if log == "on": if not logfile or not logfile.startswith("/"): print("[FATAL_SETUP] --logfile must be an absolute path when --log=on", file=sys.stderr) sys.exit(1) log_dir = os.path.dirname(logfile) print(f"[DEBUG_SETUP] Attempting to use log directory: {log_dir}", file=sys.stderr) if not os.path.exists(log_dir): print(f"[DEBUG_SETUP] Log directory {log_dir} does not exist. Attempting to create.", file=sys.stderr) try: os.makedirs(log_dir, exist_ok=True) print(f"[INFO_SETUP] Successfully created log directory: {log_dir}", file=sys.stderr) except Exception as e: print(f"[FATAL_SETUP] Failed to create log directory: {log_dir} error={e}", file=sys.stderr) traceback.print_exc(file=sys.stderr) sys.exit(1) else: print(f"[DEBUG_SETUP] Log directory {log_dir} already exists.", file=sys.stderr) if os.path.exists(log_dir) and not os.access(log_dir, os.W_OK | os.X_OK): print(f"[WARNING_SETUP] Log directory {log_dir} exists but may not be writable/executable by current user (UID: {os.getuid()})!", file=sys.stderr) try: print(f"[DEBUG_SETUP] Attempting to initialize FileHandler with logfile: {logfile}", file=sys.stderr) root_logger = py_logging.getLogger() print(f"[DEBUG_SETUP] Root logger obtained: {root_logger}. Current handlers: {root_logger.handlers}", file=sys.stderr) for handler in root_logger.handlers[:]: print(f"[DEBUG_SETUP] Removing handler {handler} from root logger.", file=sys.stderr) root_logger.removeHandler(handler) root_logger.setLevel(py_logging.DEBUG) print(f"[DEBUG_SETUP] Root logger handlers cleared and level set to DEBUG.", file=sys.stderr) # Add only FileHandler to root logger file_handler = py_logging.FileHandler(logfile, mode="a", encoding="utf-8") file_handler.setLevel(py_logging.DEBUG) formatter = py_logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) print(f"[DEBUG_SETUP] FileHandler added to root logger. Root logger handlers: {root_logger.handlers}", file=sys.stderr) # Also add StreamHandler (stdout) stream_handler = py_logging.StreamHandler(sys.stderr) # Output debug to stderr print(f"[DEBUG_SETUP] StreamHandler (stderr) initialized. Attempting to add to root_logger.", file=sys.stderr) stream_handler.setLevel(py_logging.DEBUG) stream_handler.setFormatter(formatter) root_logger.addHandler(stream_handler) print(f"[DEBUG_SETUP] StreamHandler (stderr) added to root logger. Root logger handlers: {root_logger.handlers}", file=sys.stderr) # Explicitly call flush # file_handler.flush() # Typically not needed immediately, OS handles buffering # print(f"[DEBUG_SETUP] FileHandler flushed for {logfile}.", file=sys.stderr) # Set global logger as well specific_logger = py_logging.getLogger("cort-mcp-server") # specific_logger.handlers.clear() # Not needed if propagate is True and root is configured specific_logger.setLevel(py_logging.DEBUG) # specific_logger.addHandler(file_handler) # Already on root # specific_logger.addHandler(stream_handler) # Already on root specific_logger.propagate = True # Let root logger handle it print(f"[DEBUG_SETUP] Specific logger 'cort-mcp-server' configured. Propagate: {specific_logger.propagate}. Handlers: {specific_logger.handlers}. Effective level: {specific_logger.getEffectiveLevel()}", file=sys.stderr) py_logging.debug("[DEBUG_SETUP_TEST_ROOT] Root logger test: Logging configured.") specific_logger.debug(f"[DEBUG_SETUP_TEST_SPECIFIC] Specific logger 'cort-mcp-server' test: Log initialized for {logfile}") print(f"[INFO_SETUP] MCP Server log initialization attempted for (print): {logfile}", file=sys.stderr) if os.path.exists(logfile): print(f"[INFO_SETUP] Log file {logfile} exists after FileHandler setup (print).", file=sys.stderr) specific_logger.info(f"Log file {logfile} exists after FileHandler setup.") try: with open(logfile, "a", encoding="utf-8") as f_test: f_test.write(f"TEST_WRITE_SUCCESS at {py_logging.Formatter('%(asctime)s').format(py_logging.LogRecord(name='test-write', level=py_logging.INFO, pathname='', lineno=0, msg='', args=(), exc_info=None, func=''))}\n") print(f"[INFO_SETUP] Successfully wrote a test line to {logfile}.", file=sys.stderr) except Exception as e_write: print(f"[ERROR_SETUP] Failed to write a test line to {logfile}: {e_write}", file=sys.stderr) specific_logger.error(f"Failed to write a test line to {logfile}: {e_write}") else: print(f"[WARNING_SETUP] Log file {logfile} was NOT created or is not visible after FileHandler setup (print).", file=sys.stderr) specific_logger.warning(f"Log file {logfile} was NOT created or is not visible after FileHandler setup.") return specific_logger except Exception as e: print(f"[FATAL_SETUP] Failed to create log file or setup handler: {logfile} error={e}", file=sys.stderr) traceback.print_exc(file=sys.stderr) sys.exit(1) elif log == "off": # Completely disable logging functionality py_logging.disable(py_logging.CRITICAL + 1) # Disable all levels including CRITICAL print("[INFO_SETUP] Logging disabled (--log=off)", file=sys.stderr) return None else: print("[FATAL_SETUP] --log must be 'on' or 'off'", file=sys.stderr) sys.exit(1) def resolve_model_and_provider(params): print("=== resolve_model_and_provider called ===") py_logging.info("=== resolve_model_and_provider called ===") import os # Use existing py_logging (already imported as py_logging) # Debug: Output environment variable status def mask_key(key): if key: return 'SET' return 'NOT_SET' py_logging.info(f"[DEBUG] ENV OPENROUTER_API_KEY={mask_key(os.getenv('OPENROUTER_API_KEY'))}") py_logging.info(f"[DEBUG] ENV OPENAI_API_KEY={mask_key(os.getenv('OPENAI_API_KEY'))}") # params: dict model = params.get("model") provider = params.get("provider") py_logging.info(f"[DEBUG] params: model={model}, provider={provider}") if not model: model = DEFAULT_MODEL if not provider: provider = DEFAULT_PROVIDER py_logging.info(f"[DEBUG] after default: model={model}, provider={provider}") # Check API key existence here (including invalid/unset provider) api_key = get_api_key(provider) py_logging.info(f"[DEBUG] get_api_key(provider={provider}) -> {mask_key(api_key)}") if not api_key: # Invalid provider or no API key -> fallback to default provider = DEFAULT_PROVIDER model = DEFAULT_MODEL api_key = get_api_key(provider) py_logging.info(f"[DEBUG] fallback: model={model}, provider={provider}, api_key={mask_key(api_key)}") # Additional checks like "model not existing in provider" are detected by exceptions in AI-side API return model, provider, api_key def get_api_key(provider): if provider == "openai": key = os.getenv("OPENAI_API_KEY") elif provider == "openrouter": key = os.getenv("OPENROUTER_API_KEY") else: key = None return key # Create FastMCP instance server = FastMCP( name="Chain-of-Recursive-Thoughts MCP Server", instructions="Provide deeper recursive thinking and reasoning for the given prompt. Use the MCP Server when you encounter complex problems.", ) # Define tools using decorators @server.tool( name="cort.think.simple", description=""" Return a simple recursive thinking AI response. Parameters: prompt (str, required): Input prompt for the AI. model (str, optional): LLM model name. If not specified, uses default. provider (str, optional): API provider name. If not specified, uses default. Returns: dict: { "response": AI response (string), "model": model name used (string), "provider": provider name used (string) } Notes: - If model/provider is omitted, defaults are used. - Do not pass null or empty string for optional params. - See README for fallback logic on API errors. """ ) async def cort_think_simple( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")], model: Annotated[str | None, Field(description="LLM model name. If not specified, uses default.")]=None, provider: Annotated[str | None, Field(description="API provider name. If not specified, uses default.")]=None ): resolved_model, resolved_provider, api_key = resolve_model_and_provider({"model": model, "provider": provider}) py_logging.info(f"cort_think_simple called: prompt={prompt} model={resolved_model} provider={resolved_provider}") if not prompt: py_logging.warning("cort_think_simple: prompt is required") return { "error": "prompt is required" } try: chat = EnhancedRecursiveThinkingChat(api_key=api_key, model=resolved_model, provider=resolved_provider) result = chat.think(prompt, details=False) py_logging.info("cort_think_simple: result generated successfully") return { "response": result.get("response"), "model": result.get("model"), "provider": result.get("provider") } except Exception as e: py_logging.exception(f"[ERROR] cort_think_simple failed: {e}") fallback_api_key = get_api_key(DEFAULT_PROVIDER) if fallback_api_key: try: chat = EnhancedRecursiveThinkingChat(api_key=fallback_api_key, model=DEFAULT_MODEL, provider=DEFAULT_PROVIDER) result = chat.think(prompt, details=False) py_logging.info("cort_think_simple: fallback result generated successfully") return { "response": result.get("response"), "model": result.get("model"), "provider": result.get("provider") } except Exception as e2: py_logging.exception(f"[ERROR] cort_think_simple fallback also failed: {e2}") return { "error": f"Failed to process request: {str(e)}. Fallback also failed: {str(e2)}" } else: py_logging.error("cort_think_simple: API key for OpenAI is missing (cannot fallback)") return { "error": f"Failed to process request: {str(e)}. API key for OpenAI is missing (cannot fallback)" } @server.tool( name="cort.think.simple.neweval", description=""" Return a simple recursive thinking AI response (new evaluation prompt version). Parameters: prompt (str, required): Input prompt for the AI. model (str, optional): LLM model name. If not specified, uses default. provider (str, optional): API provider name. If not specified, uses default. Returns: dict: { "response": AI response (string), "model": model name used (string), "provider": provider name used (string) } Notes: - If model/provider is omitted, defaults are used. - Do not pass null or empty string for optional params. - See README for fallback logic on API errors. """ ) async def cort_think_simple_neweval( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")], model: Annotated[str | None, Field(description="LLM model name. If not specified, uses default.")]=None, provider: Annotated[str | None, Field(description="API provider name. If not specified, uses default.")]=None ): resolved_model, resolved_provider, api_key = resolve_model_and_provider({"model": model, "provider": provider}) py_logging.info(f"cort_think_simple_neweval called: prompt={prompt} model={resolved_model} provider={resolved_provider}") if not prompt: py_logging.warning("cort_think_simple_neweval: prompt is required") return { "error": "prompt is required" } try: chat = EnhancedRecursiveThinkingChat(api_key=api_key, model=resolved_model, provider=resolved_provider) result = chat.think(prompt, details=False, neweval=True) py_logging.info("cort_think_simple_neweval: result generated successfully") return { "response": result.get("response"), "model": result.get("model"), "provider": result.get("provider") } except Exception as e: py_logging.exception(f"[ERROR] cort_think_simple_neweval failed: {e}") fallback_api_key = get_api_key(DEFAULT_PROVIDER) if fallback_api_key: try: chat = EnhancedRecursiveThinkingChat(api_key=fallback_api_key, model=DEFAULT_MODEL, provider=DEFAULT_PROVIDER) result = chat.think(prompt, details=False, neweval=True) py_logging.info("cort_think_simple_neweval: fallback result generated successfully") return { "response": result["response"], "model": DEFAULT_MODEL, "provider": f"{DEFAULT_PROVIDER} (fallback)" } except Exception as e2: py_logging.exception(f"[ERROR] cort_think_simple_neweval fallback also failed: {e2}") return { "error": f"Failed to process request: {str(e)}. Fallback also failed: {str(e2)}" } else: py_logging.error("cort_think_simple_neweval: API key for OpenAI is missing (cannot fallback)") return { "error": f"Failed to process request: {str(e)}. API key for OpenAI is missing (cannot fallback)" } @server.tool( name="cort.think.details", description=""" Returns a recursive thinking AI response with full reasoning details. Parameters: prompt (str, required): Input prompt for the AI. model (str, optional): LLM model name. If not specified, the default model is used. provider (str, optional): API provider name. If not specified, the default provider is used. Returns: dict: { "response": Final AI response (string), "details": Reasoning process history (YAML string), "model": Model name used (string), "provider": Provider name used (string) } Notes: - If model/provider is omitted, defaults are applied automatically. - On exceptions, fallback logic is applied. - Reasoning history is included in the 'details' key as YAML. """ ) async def cort_think_details( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")], model: Annotated[str | None, Field(description="LLM model name to use.\n- Recommended (OpenAI): 'gpt-4.1-nano'\n- Recommended (OpenRouter): 'meta-llama/llama-4-maverick:free'\n- Default: mistralai/mistral-small-3.1-24b-instruct:free\nRefer to the official provider list for available models. If not specified, the default model will be used automatically.")]=None, provider: Annotated[str | None, Field(description="API provider name to use.\n- Allowed: 'openai' or 'openrouter'\n- Default: openrouter\nModel availability depends on the provider. Please ensure the correct combination. If not specified, the default provider will be used automatically.")]=None ): resolved_model, resolved_provider, api_key = resolve_model_and_provider({"model": model, "provider": provider}) py_logging.info(f"cort_think_details called: prompt={prompt} model={resolved_model} provider={resolved_provider}") if not prompt: py_logging.warning("cort_think_details: prompt is required") return { "error": "prompt is required" } try: chat = EnhancedRecursiveThinkingChat(api_key=api_key, model=resolved_model, provider=resolved_provider) result = chat.think(prompt, details=True) yaml_log = yaml.safe_dump({ "thinking_rounds": result.get("thinking_rounds"), "thinking_history": result.get("thinking_history") }, allow_unicode=True, sort_keys=False) py_logging.info("cort_think_details: result generated successfully") return { "response": result["response"], "details": yaml_log, "model": resolved_model, "provider": resolved_provider } except Exception as e: py_logging.exception(f"[ERROR] cort_think_details failed: {e}") fallback_api_key = get_api_key(DEFAULT_PROVIDER) if fallback_api_key: try: chat = EnhancedRecursiveThinkingChat(api_key=fallback_api_key, model=DEFAULT_MODEL, provider=DEFAULT_PROVIDER) result = chat.think(prompt, details=True) yaml_log = yaml.safe_dump({ "thinking_rounds": result.get("thinking_rounds"), "thinking_history": result.get("thinking_history") }, allow_unicode=True, sort_keys=False) py_logging.info("cort_think_details: fallback result generated successfully") return { "response": result["response"], "details": yaml_log, "model": DEFAULT_MODEL, "provider": f"{DEFAULT_PROVIDER} (fallback)" } except Exception as e2: py_logging.exception(f"[ERROR] cort_think_details fallback also failed: {e2}") return { "error": f"Failed to process request: {str(e)}. Fallback also failed: {str(e2)}" } else: py_logging.error("cort_think_details: API key for OpenAI is missing (cannot fallback)") return { "error": f"Failed to process request: {str(e)}. API key for OpenAI is missing (cannot fallback)" } @server.tool( name="cort.think.details.neweval", description=""" Returns a recursive thinking AI response with full reasoning details (new evaluation prompt version). Features: - Provides a recursive thinking AI response and the reasoning process/history (YAML format) for the given prompt. Parameters: prompt (str, required): Input prompt for the AI. model (str, optional): LLM model name. If not specified, the default model is used. - Recommended (OpenAI): "gpt-4.1-nano" - Recommended (OpenRouter): "meta-llama/llama-4-maverick:free" - Default: mistralai/mistral-small-3.1-24b-instruct:free - Please refer to the official provider list for available models. provider (str, optional): API provider name. If not specified, the default provider is used. - Allowed: "openai" or "openrouter" - Default: openrouter - Model availability depends on the provider. Please ensure the correct combination. Returns: dict: { "response": AI response (string), "details": Reasoning process/history (YAML string), "model": Model name used (string), "provider": Provider name used (string) } Notes: - If model/provider is omitted, omit the parameter entirely. - Passing null or empty string may cause API errors. - For fallback behavior on API errors, see the "Parameter Specification and Fallback Handling" section in README.md. """ ) async def cort_think_details_neweval( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")], model: Annotated[str | None, Field(description="LLM model name to use.\n- Recommended (OpenAI): 'gpt-4.1-nano'\n- Recommended (OpenRouter): 'meta-llama/llama-4-maverick:free'\n- Default: mistralai/mistral-small-3.1-24b-instruct:free\nRefer to the official provider list for available models. If not specified, the default model will be used automatically.")]=None, provider: Annotated[str | None, Field(description="API provider name to use.\n- Allowed: 'openai' or 'openrouter'\n- Default: openrouter\nModel availability depends on the provider. Please ensure the correct combination. If not specified, the default provider will be used automatically.")]=None ): resolved_model, resolved_provider, api_key = resolve_model_and_provider({"model": model, "provider": provider}) py_logging.info(f"cort_think_details_neweval called: prompt={prompt} model={resolved_model} provider={resolved_provider}") if not prompt: py_logging.warning("cort_think_details_neweval: prompt is required") return { "error": "prompt is required" } try: chat = EnhancedRecursiveThinkingChat(api_key=api_key, model=resolved_model, provider=resolved_provider) result = chat.think(prompt, details=True, neweval=True) yaml_log = yaml.safe_dump({ "thinking_rounds": result.get("thinking_rounds"), "thinking_history": result.get("thinking_history") }, allow_unicode=True, sort_keys=False) py_logging.info("cort_think_details_neweval: result generated successfully") return { "response": result["response"], "details": yaml_log, "model": resolved_model, "provider": resolved_provider } except Exception as e: py_logging.exception(f"[ERROR] cort_think_details_neweval failed: {e}") fallback_api_key = get_api_key(DEFAULT_PROVIDER) if fallback_api_key: try: chat = EnhancedRecursiveThinkingChat(api_key=fallback_api_key, model=DEFAULT_MODEL, provider=DEFAULT_PROVIDER) result = chat.think(prompt, details=True) yaml_log = yaml.safe_dump({ "thinking_rounds": result.get("thinking_rounds"), "thinking_history": result.get("thinking_history") }, allow_unicode=True, sort_keys=False) py_logging.info("cort_think_details_neweval: fallback result generated successfully") return { "response": result["response"], "details": yaml_log, "model": DEFAULT_MODEL, "provider": f"{DEFAULT_PROVIDER} (fallback)" } except Exception as e2: py_logging.exception(f"[ERROR] cort_think_details_neweval fallback also failed: {e2}") return { "error": f"Failed to process request: {str(e)}. Fallback also failed: {str(e2)}" } else: py_logging.error("cort_think_details_neweval: API key for OpenAI is missing (cannot fallback)") return { "error": f"Failed to process request: {str(e)}. API key for OpenAI is missing (cannot fallback)" } # --- Mixed LLM List Definition --- MIXED_LLM_LIST = [ {"provider": "openai", "model": "gpt-4.1-nano"}, {"provider": "openrouter", "model": "meta-llama/llama-4-scout:free"}, {"provider": "openrouter", "model": "google/gemini-2.0-flash-exp:free"}, {"provider": "openrouter", "model": "mistralai/mistral-small-3.1-24b-instruct:free"}, {"provider": "openrouter", "model": "meta-llama/llama-3.2-3b-instruct:free"}, {"provider": "openrouter", "model": "thudm/glm-4-9b:free"}, ] def get_available_mixed_llms(): """Return only LLMs with valid API keys""" available = [] for entry in MIXED_LLM_LIST: api_key = get_api_key(entry["provider"]) if api_key: available.append({**entry, "api_key": api_key}) return available import random from typing import Dict, Any def generate_with_mixed_llm(prompt: str, details: bool = False, neweval: bool = False) -> Dict[str, Any]: available_llms = get_available_mixed_llms() if not prompt: py_logging.warning("mixed_llm: prompt is required") return {"error": "prompt is required"} if not available_llms: py_logging.error("mixed_llm: No available LLMs (API key missing)") return {"error": "No available LLMs (API key missing)"} # --- Number of rounds and alternatives are determined by AI based on existing logic --- # First, randomly select a base LLM base_llm = random.choice(available_llms) chat = EnhancedRecursiveThinkingChat(api_key=base_llm["api_key"], model=base_llm["model"], provider=base_llm["provider"]) # Generate base response (initial) thinking_rounds = chat._determine_thinking_rounds(prompt) py_logging.info("\n=== GENERATING INITIAL RESPONSE ===") py_logging.info(f"Base LLM: provider={base_llm['provider']}, model={base_llm['model']}, rounds={thinking_rounds}") base_response = chat._call_api([{"role": "user", "content": prompt}], temperature=0.7, stream=False) # --- base_response contains only AI response (similar to simple mode) --- # If API response is a dict or structure, extract only content key; otherwise, use as is if isinstance(base_response, dict) and "content" in base_response: current_best = base_response["content"] else: current_best = base_response py_logging.info("=" * 50) thinking_history = [{ "round": 0, "llm_prompt": prompt, "llm_response": base_response, "response": base_response, "alternatives": [], "selected": -1, "explanation": "Initial base response", "provider": base_llm["provider"], "model": base_llm["model"] }] # Generate alternatives for each round # Use the same logic as EnhancedRecursiveThinkingChat.think (num_alternatives) num_alternatives = 3 if hasattr(chat, 'num_alternatives'): num_alternatives = chat.num_alternatives for r in range(thinking_rounds): py_logging.info(f"\n=== ROUND {r+1}/{thinking_rounds} ===") alternatives = [] alt_llm_info = [] alt_llm_responses = [] alt_llm_prompts = [] for i in range(num_alternatives): py_logging.info(f"\n✨ ALTERNATIVE {i+1} ✨") alt_llm = random.choice(available_llms) alt_prompt = f"""Original message: {prompt}\n\nCurrent response: {current_best}\n\nGenerate an alternative response that might be better. Be creative and consider different approaches.\nAlternative response:""" alt_messages = [{"role": "user", "content": alt_prompt}] alt_chat = EnhancedRecursiveThinkingChat(api_key=alt_llm["api_key"], model=alt_llm["model"], provider=alt_llm["provider"]) alt_response = alt_chat._call_api(alt_messages, temperature=0.7 + i * 0.1, stream=False) # --- alt_response also contains only AI response (similar to simple mode) --- if isinstance(alt_response, dict) and "content" in alt_response: alt_response_text = alt_response["content"] else: alt_response_text = alt_response py_logging.info(f"Alternative {i+1}: provider={alt_llm['provider']}, model={alt_llm['model']}") alternatives.append({ "response": alt_response_text, "provider": alt_llm["provider"], "model": alt_llm["model"] }) alt_llm_info.append({"provider": alt_llm["provider"], "model": alt_llm["model"]}) alt_llm_responses.append(alt_response) alt_llm_prompts.append(alt_prompt) # Evaluation is performed by base LLM (following current CoRT practice) py_logging.info("\n=== EVALUATING RESPONSES ===") alts_text = "\n".join([f"{i+1}. {alt['response']}" for i, alt in enumerate(alternatives)]) # Evaluation prompt is centrally managed on AI core side eval_prompt = chat._build_eval_prompt(prompt, current_best, [alt['response'] for alt in alternatives], neweval=neweval) eval_messages = [{"role": "user", "content": eval_prompt}] evaluation = chat._call_api(eval_messages, temperature=0.2, stream=False) py_logging.info("=" * 50) lines = [line.strip() for line in evaluation.split('\n') if line.strip()] choice = 'current' explanation_text = "No explanation provided" if lines: first_line = lines[0].lower() if 'current' in first_line: choice = 'current' else: for char in first_line: if char.isdigit(): choice = char break if len(lines) > 1: explanation_text = ' '.join(lines[1:]) if choice == 'current': selected_response = current_best selected_idx = -1 py_logging.info(f"\n ✓ Kept current response: {explanation_text}") else: try: idx = int(choice) - 1 if 0 <= idx < len(alternatives): selected_response = alternatives[idx]["response"] selected_idx = idx py_logging.info(f"\n ✓ Selected alternative {idx+1}: {explanation_text}") else: selected_response = current_best selected_idx = -1 py_logging.info(f"\n ✓ Invalid selection, keeping current response") except Exception: selected_response = current_best selected_idx = -1 py_logging.info(f"\n ✓ Could not parse selection, keeping current response") # Record the selected provider/model if selected_idx != -1 and 0 <= selected_idx < len(alternatives): sel_provider = alternatives[selected_idx]["provider"] sel_model = alternatives[selected_idx]["model"] else: # current_best is either base_llm or previous best # Pick from the last thinking_history (if not, use base_llm) if thinking_history: sel_provider = thinking_history[-1].get("provider", base_llm["provider"]) sel_model = thinking_history[-1].get("model", base_llm["model"]) else: sel_provider = base_llm["provider"] sel_model = base_llm["model"] thinking_history.append({ "round": r + 1, "llm_prompt": alt_llm_prompts, "llm_response": alt_llm_responses, "response": selected_response, "alternatives": alternatives, "selected": selected_idx, "explanation": explanation_text, "alternatives_llm": alt_llm_info, "provider": sel_provider, "model": sel_model }) current_best = selected_response py_logging.info("\n" + "=" * 50) py_logging.info("🎯 FINAL RESPONSE SELECTED") py_logging.info("=" * 50) result = {"response": current_best} # Regardless of details, always return minimal meta information result["thinking_rounds"] = thinking_rounds result["thinking_history"] = thinking_history # Always store the provider/model that generated the final response in best (for simple mode) last_provider = None last_model = None if thinking_history and isinstance(thinking_history[-1], dict): last_provider = thinking_history[-1].get("provider") last_model = thinking_history[-1].get("model") # Prevent null values just in case if not last_provider or not last_model: # Get from the last alternatives (if options exist) last_alts = thinking_history[-1].get("alternatives", []) if last_alts and isinstance(last_alts, list): last_alt = last_alts[-1] last_provider = last_provider or last_alt.get("provider") last_model = last_model or last_alt.get("model") # If still not found, use base_llm if not last_provider: last_provider = base_llm["provider"] if not last_model: last_model = base_llm["model"] result["best"] = { "response": current_best, "provider": last_provider, "model": last_model } if details: # Additional information only in details mode result["alternatives"] = thinking_history[-1]["alternatives"] if thinking_history else [] return result # --- MCP Tool Definitions --- from typing import Annotated from pydantic import Field @server.tool( name="cort.think.simple_mixed_llm", description="Generate recursive thinking AI response using a different LLM (provider/model) for each alternative. No history/details output. Parameters: prompt (str, required). model/provider cannot be specified (randomly selected internally). Provider/model info for each alternative is always logged and included in the output.", ) async def cort_think_simple_mixed_llm( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")] ): result = generate_with_mixed_llm(prompt, details=False) # 必要な情報のみ抽出 response = result.get("response") best = result.get("best") return { "response": response, "model": best.get("model"), "provider": best.get("provider") } @server.tool( name="cort.think.simple_mixed_llm.neweval", description=""" Generate recursive thinking AI response using a different LLM (provider/model) for each alternative. No history/details output. (new evaluation prompt version) Parameters: prompt (str, required): Input prompt for the AI (required). model/provider cannot be specified (randomly selected internally)。 Provider/model info for each alternative is always logged and included in the output. Returns: dict: { "response": AI response (string), "provider": provider name used (string), "model": model name used (string) } """ ) async def cort_think_simple_mixed_llm_neweval( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")] ): result = generate_with_mixed_llm(prompt, details=False, neweval=True) # neweval専用プロンプトで評価するために、details=False, neweval=Trueでthinkを呼び出す必要がある場合はここで明示 response = result.get("response") best = result.get("best") return { "response": response, "model": best.get("model"), "provider": best.get("provider") } @server.tool( name="cort.think.details_mixed_llm", description="Generate recursive thinking AI response with full history, using a different LLM (provider/model) for each alternative. Parameters: prompt (str, required). model/provider cannot be specified (randomly selected internally). Provider/model info for each alternative is always logged and included in the output and history.", ) async def cort_think_details_mixed_llm( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")] ): result = generate_with_mixed_llm(prompt, details=True) import yaml if "thinking_rounds" in result and "thinking_history" in result: result["details"] = yaml.safe_dump({ "thinking_rounds": result["thinking_rounds"], "thinking_history": result["thinking_history"] }, allow_unicode=True, sort_keys=False) return result @server.tool( name="cort.think.details_mixed_llm.neweval", description=""" Generate recursive thinking AI response with full history, using a different LLM (provider/model) for each alternative. (new evaluation prompt version) Parameters: prompt (str, required): Input prompt for the AI (required). model/provider cannot be specified (randomly selected internally). Provider/model info for each alternative is always logged and included in the output and history. Returns: dict: { "response": AI response (string), "details": YAML-formatted thinking history (string), "thinking_rounds": int, "thinking_history": list, "best": dict, # Information about the LLM that generated the best/final response "alternatives": list (only when details=True) # List of alternative responses considered in the final round } """ ) async def cort_think_details_mixed_llm_neweval( prompt: Annotated[str, Field(description="Input prompt for the AI (required)")] ): result = generate_with_mixed_llm(prompt, details=True, neweval=True) import yaml if "thinking_rounds" in result and "thinking_history" in result: result["details"] = yaml.safe_dump({ "thinking_rounds": result["thinking_rounds"], "thinking_history": result["thinking_history"] }, allow_unicode=True, sort_keys=False) return result # Tools are registered with decorators def initialize_and_run_server(): # Initialize and run the MCP server. # Logging should be configured by setup_logging by now. # We can get the logger instance. logger = py_logging.getLogger("cort-mcp-server") if not logger.handlers and py_logging.getLogger().hasHandlers(): # Check if specific has no handlers but root does logger = py_logging.getLogger() # Fallback to root if specific has no handlers (and propagate is true) logger.info("cort-mcp server starting... (using root logger for this message in initialize_and_run_server)") else: logger.info("cort-mcp server starting... (using 'cort-mcp-server' logger in initialize_and_run_server)") # Run the MCP server server.run() def main(): parser = argparse.ArgumentParser(description="Chain-of-Recursive-Thoughts MCP Server/CLI") parser.add_argument("--log", choices=["on", "off"], required=True, help="Enable or disable logging (on/off)") parser.add_argument("--logfile", type=str, default=None, help="Absolute path to log file (required if --log=on)") args = parser.parse_args() print(f"[DEBUG_MAIN] Parsed arguments: log='{args.log}', logfile='{args.logfile}'", file=sys.stderr) if args.log == "on" and not args.logfile: print("[FATAL_MAIN] --logfile is required when --log=on", file=sys.stderr) sys.exit(1) if args.log == "on" and args.logfile and not os.path.isabs(args.logfile): # Check if logfile is not None print(f"[FATAL_MAIN] --logfile must be an absolute path when --log=on. Received: '{args.logfile}'", file=sys.stderr) sys.exit(1) # Call setup_logging to configure logging based on arguments. print(f"[DEBUG_MAIN] Calling setup_logging with log='{args.log}', logfile='{args.logfile}'", file=sys.stderr) logger = setup_logging(args.log, args.logfile) print(f"[DEBUG_MAIN] setup_logging returned: {logger}", file=sys.stderr) if logger: # If setup_logging returned a logger instance (i.e., log was 'on') logger.info("cort-mcp main() started, using 'cort-mcp-server' logger.") # Test root logger as well, if specific logger is different if logger is not py_logging.getLogger(): py_logging.info("cort-mcp main() started, test message via root logger.") elif args.log == "on": # Should not happen if setup_logging exits on error print("[ERROR_MAIN] Logger was not configured by setup_logging despite --log=on. Check stderr for setup_logging messages.", file=sys.stderr) else: # log == "off" print("[INFO_MAIN] Logging is off. No logs will be generated by the application.", file=sys.stderr) try: if logger: logger.info("Server mode: waiting for MCP stdio requests...") elif args.log == "on": # Log was on, but logger is None (error in setup_logging) print("[INFO_MAIN] Server mode: waiting for MCP stdio requests... (logger not fully available)", file=sys.stderr) else: # log == "off" print("[INFO_MAIN] Server mode: waiting for MCP stdio requests... (logging is off)", file=sys.stderr) # Start the server using FastMCP initialize_and_run_server() except Exception as e: if logger: logger.exception(f"[ERROR_MAIN] main() unhandled exception: {e}") else: # Fallback if logger is not available print(f"[FATAL_ERROR_MAIN] main() unhandled exception: {e}", file=sys.stderr) traceback.print_exc(file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/KunihiroS/cort-mcp'

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