Spaces:
Running
Running
Update app/mcp.py
Browse files- app/mcp.py +63 -73
app/mcp.py
CHANGED
|
@@ -10,6 +10,10 @@
|
|
| 10 |
# NO direct access to fundaments/*, .env, or Guardian (main.py).
|
| 11 |
# All config comes from app/.pyfun via app/config.py.
|
| 12 |
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# TOOL REGISTRATION PRINCIPLE:
|
| 14 |
# Tools are only registered if their required ENV key exists.
|
| 15 |
# No key = no tool = no crash. Server always starts, just with fewer tools.
|
|
@@ -25,21 +29,21 @@ from . import config as app_config # reads app/.pyfun β only config source fo
|
|
| 25 |
|
| 26 |
logger = logging.getLogger('mcp')
|
| 27 |
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
async def
|
| 30 |
"""
|
| 31 |
-
|
| 32 |
-
Called by app/app.py
|
| 33 |
-
|
| 34 |
-
NO fundaments passed in β sandboxed.
|
| 35 |
"""
|
| 36 |
-
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
host = os.getenv("HOST", hub_cfg.get("HUB_HOST", "0.0.0.0"))
|
| 42 |
-
port = int(os.getenv("PORT", hub_cfg.get("HUB_PORT", "7860")))
|
| 43 |
|
| 44 |
try:
|
| 45 |
from mcp.server.fastmcp import FastMCP
|
|
@@ -47,7 +51,7 @@ async def start_mcp() -> None:
|
|
| 47 |
logger.critical("FastMCP not installed. Run: pip install mcp")
|
| 48 |
raise
|
| 49 |
|
| 50 |
-
|
| 51 |
name=hub_cfg.get("HUB_NAME", "Universal MCP Hub"),
|
| 52 |
instructions=(
|
| 53 |
f"{hub_cfg.get('HUB_DESCRIPTION', 'Universal MCP Hub on PyFundaments')} "
|
|
@@ -55,35 +59,33 @@ async def start_mcp() -> None:
|
|
| 55 |
)
|
| 56 |
)
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
#
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
# --- LLM Tools ---
|
| 65 |
-
_register_llm_tools(mcp)
|
| 66 |
|
| 67 |
-
|
| 68 |
-
_register_search_tools(mcp)
|
| 69 |
|
| 70 |
-
# --- DB Tools --- (disabled until db_sync is ready)
|
| 71 |
-
# _register_db_tools(mcp)
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
#
|
| 78 |
-
#
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
await mcp.run_sse_async(host=host, port=port)
|
| 82 |
-
else:
|
| 83 |
-
logger.info("MCP Hub starting via stdio (local mode)")
|
| 84 |
-
await mcp.run_stdio_async()
|
| 85 |
|
| 86 |
-
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
# =============================================================================
|
|
@@ -100,13 +102,12 @@ def _register_llm_tools(mcp) -> None:
|
|
| 100 |
logger.info(f"LLM provider '{name}' skipped β ENV key '{env_key}' not set.")
|
| 101 |
continue
|
| 102 |
|
| 103 |
-
# Anthropic
|
| 104 |
if name == "anthropic":
|
| 105 |
import httpx
|
| 106 |
-
_key
|
| 107 |
-
_api_ver
|
| 108 |
-
_base_url
|
| 109 |
-
_def_model
|
| 110 |
|
| 111 |
@mcp.tool()
|
| 112 |
async def anthropic_complete(
|
|
@@ -135,12 +136,11 @@ def _register_llm_tools(mcp) -> None:
|
|
| 135 |
|
| 136 |
logger.info(f"Tool registered: anthropic_complete (model: {_def_model})")
|
| 137 |
|
| 138 |
-
# Gemini
|
| 139 |
elif name == "gemini":
|
| 140 |
import httpx
|
| 141 |
-
_key
|
| 142 |
-
_base_url
|
| 143 |
-
_def_model
|
| 144 |
|
| 145 |
@mcp.tool()
|
| 146 |
async def gemini_complete(
|
|
@@ -164,13 +164,12 @@ def _register_llm_tools(mcp) -> None:
|
|
| 164 |
|
| 165 |
logger.info(f"Tool registered: gemini_complete (model: {_def_model})")
|
| 166 |
|
| 167 |
-
# OpenRouter
|
| 168 |
elif name == "openrouter":
|
| 169 |
import httpx
|
| 170 |
-
_key
|
| 171 |
-
_base_url
|
| 172 |
-
_def_model
|
| 173 |
-
_referer
|
| 174 |
|
| 175 |
@mcp.tool()
|
| 176 |
async def openrouter_complete(
|
|
@@ -199,12 +198,11 @@ def _register_llm_tools(mcp) -> None:
|
|
| 199 |
|
| 200 |
logger.info(f"Tool registered: openrouter_complete (model: {_def_model})")
|
| 201 |
|
| 202 |
-
# HuggingFace
|
| 203 |
elif name == "huggingface":
|
| 204 |
import httpx
|
| 205 |
-
_key
|
| 206 |
-
_base_url
|
| 207 |
-
_def_model
|
| 208 |
|
| 209 |
@mcp.tool()
|
| 210 |
async def hf_inference(
|
|
@@ -246,7 +244,6 @@ def _register_search_tools(mcp) -> None:
|
|
| 246 |
logger.info(f"Search provider '{name}' skipped β ENV key '{env_key}' not set.")
|
| 247 |
continue
|
| 248 |
|
| 249 |
-
# Brave
|
| 250 |
if name == "brave":
|
| 251 |
import httpx
|
| 252 |
_key = os.getenv(env_key)
|
|
@@ -278,13 +275,12 @@ def _register_search_tools(mcp) -> None:
|
|
| 278 |
|
| 279 |
logger.info("Tool registered: brave_search")
|
| 280 |
|
| 281 |
-
# Tavily
|
| 282 |
elif name == "tavily":
|
| 283 |
import httpx
|
| 284 |
-
_key
|
| 285 |
-
_base_url
|
| 286 |
-
_def_results
|
| 287 |
-
_incl_answer
|
| 288 |
|
| 289 |
@mcp.tool()
|
| 290 |
async def tavily_search(query: str, max_results: int = _def_results) -> str:
|
|
@@ -323,20 +319,14 @@ def _register_system_tools(mcp) -> None:
|
|
| 323 |
@mcp.tool()
|
| 324 |
def list_active_tools() -> Dict[str, Any]:
|
| 325 |
"""Show active providers and configured integrations (key names only, never values)."""
|
| 326 |
-
llm
|
| 327 |
-
search
|
| 328 |
-
hub
|
| 329 |
return {
|
| 330 |
-
"hub":
|
| 331 |
-
"version":
|
| 332 |
-
"active_llm_providers":
|
| 333 |
-
|
| 334 |
-
if os.getenv(cfg.get("env_key", ""))
|
| 335 |
-
],
|
| 336 |
-
"active_search_providers": [
|
| 337 |
-
name for name, cfg in search.items()
|
| 338 |
-
if os.getenv(cfg.get("env_key", ""))
|
| 339 |
-
],
|
| 340 |
}
|
| 341 |
logger.info("Tool registered: list_active_tools")
|
| 342 |
|
|
|
|
| 10 |
# NO direct access to fundaments/*, .env, or Guardian (main.py).
|
| 11 |
# All config comes from app/.pyfun via app/config.py.
|
| 12 |
#
|
| 13 |
+
# MCP SSE transport runs through Quart/hypercorn via /mcp route.
|
| 14 |
+
# All MCP traffic can be intercepted, logged, and transformed in app.py
|
| 15 |
+
# before reaching the MCP handler β this is by design.
|
| 16 |
+
#
|
| 17 |
# TOOL REGISTRATION PRINCIPLE:
|
| 18 |
# Tools are only registered if their required ENV key exists.
|
| 19 |
# No key = no tool = no crash. Server always starts, just with fewer tools.
|
|
|
|
| 29 |
|
| 30 |
logger = logging.getLogger('mcp')
|
| 31 |
|
| 32 |
+
# Global MCP instance β initialized once via initialize()
|
| 33 |
+
_mcp = None
|
| 34 |
+
|
| 35 |
|
| 36 |
+
async def initialize() -> None:
|
| 37 |
"""
|
| 38 |
+
Initializes the MCP instance and registers all tools.
|
| 39 |
+
Called once by app/app.py during startup.
|
| 40 |
+
No fundaments passed in β sandboxed.
|
|
|
|
| 41 |
"""
|
| 42 |
+
global _mcp
|
| 43 |
|
| 44 |
+
logger.info("MCP Hub initializing...")
|
| 45 |
+
|
| 46 |
+
hub_cfg = app_config.get_hub()
|
|
|
|
|
|
|
| 47 |
|
| 48 |
try:
|
| 49 |
from mcp.server.fastmcp import FastMCP
|
|
|
|
| 51 |
logger.critical("FastMCP not installed. Run: pip install mcp")
|
| 52 |
raise
|
| 53 |
|
| 54 |
+
_mcp = FastMCP(
|
| 55 |
name=hub_cfg.get("HUB_NAME", "Universal MCP Hub"),
|
| 56 |
instructions=(
|
| 57 |
f"{hub_cfg.get('HUB_DESCRIPTION', 'Universal MCP Hub on PyFundaments')} "
|
|
|
|
| 59 |
)
|
| 60 |
)
|
| 61 |
|
| 62 |
+
# --- Register tools ---
|
| 63 |
+
_register_llm_tools(_mcp)
|
| 64 |
+
_register_search_tools(_mcp)
|
| 65 |
+
# _register_db_tools(_mcp) # uncomment when db_sync is ready
|
| 66 |
+
_register_system_tools(_mcp)
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
logger.info("MCP Hub initialized.")
|
|
|
|
| 69 |
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
async def handle_request(request) -> None:
|
| 72 |
+
"""
|
| 73 |
+
Handles incoming MCP SSE requests routed through Quart /mcp endpoint.
|
| 74 |
+
This is the interceptor point β add auth, logging, rate limiting here.
|
| 75 |
+
"""
|
| 76 |
+
if _mcp is None:
|
| 77 |
+
logger.error("MCP not initialized β call initialize() first.")
|
| 78 |
+
from quart import jsonify
|
| 79 |
+
return jsonify({"error": "MCP not initialized"}), 503
|
| 80 |
|
| 81 |
+
# --- Interceptor hooks (add as needed) ---
|
| 82 |
+
# logger.debug(f"MCP request: {request.method} {request.path}")
|
| 83 |
+
# await _check_auth(request)
|
| 84 |
+
# await _rate_limit(request)
|
| 85 |
+
# await _log_payload(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
# --- Forward to FastMCP SSE handler ---
|
| 88 |
+
return await _mcp.handle_sse(request)
|
| 89 |
|
| 90 |
|
| 91 |
# =============================================================================
|
|
|
|
| 102 |
logger.info(f"LLM provider '{name}' skipped β ENV key '{env_key}' not set.")
|
| 103 |
continue
|
| 104 |
|
|
|
|
| 105 |
if name == "anthropic":
|
| 106 |
import httpx
|
| 107 |
+
_key = os.getenv(env_key)
|
| 108 |
+
_api_ver = cfg.get("api_version_header", "2023-06-01")
|
| 109 |
+
_base_url = cfg.get("base_url", "https://api.anthropic.com/v1")
|
| 110 |
+
_def_model = cfg.get("default_model", "claude-haiku-4-5-20251001")
|
| 111 |
|
| 112 |
@mcp.tool()
|
| 113 |
async def anthropic_complete(
|
|
|
|
| 136 |
|
| 137 |
logger.info(f"Tool registered: anthropic_complete (model: {_def_model})")
|
| 138 |
|
|
|
|
| 139 |
elif name == "gemini":
|
| 140 |
import httpx
|
| 141 |
+
_key = os.getenv(env_key)
|
| 142 |
+
_base_url = cfg.get("base_url", "https://generativelanguage.googleapis.com/v1beta")
|
| 143 |
+
_def_model = cfg.get("default_model", "gemini-2.0-flash")
|
| 144 |
|
| 145 |
@mcp.tool()
|
| 146 |
async def gemini_complete(
|
|
|
|
| 164 |
|
| 165 |
logger.info(f"Tool registered: gemini_complete (model: {_def_model})")
|
| 166 |
|
|
|
|
| 167 |
elif name == "openrouter":
|
| 168 |
import httpx
|
| 169 |
+
_key = os.getenv(env_key)
|
| 170 |
+
_base_url = cfg.get("base_url", "https://openrouter.ai/api/v1")
|
| 171 |
+
_def_model = cfg.get("default_model", "mistralai/mistral-7b-instruct")
|
| 172 |
+
_referer = os.getenv("APP_URL", "https://huggingface.co")
|
| 173 |
|
| 174 |
@mcp.tool()
|
| 175 |
async def openrouter_complete(
|
|
|
|
| 198 |
|
| 199 |
logger.info(f"Tool registered: openrouter_complete (model: {_def_model})")
|
| 200 |
|
|
|
|
| 201 |
elif name == "huggingface":
|
| 202 |
import httpx
|
| 203 |
+
_key = os.getenv(env_key)
|
| 204 |
+
_base_url = cfg.get("base_url", "https://api-inference.huggingface.co/models")
|
| 205 |
+
_def_model = cfg.get("default_model", "mistralai/Mistral-7B-Instruct-v0.3")
|
| 206 |
|
| 207 |
@mcp.tool()
|
| 208 |
async def hf_inference(
|
|
|
|
| 244 |
logger.info(f"Search provider '{name}' skipped β ENV key '{env_key}' not set.")
|
| 245 |
continue
|
| 246 |
|
|
|
|
| 247 |
if name == "brave":
|
| 248 |
import httpx
|
| 249 |
_key = os.getenv(env_key)
|
|
|
|
| 275 |
|
| 276 |
logger.info("Tool registered: brave_search")
|
| 277 |
|
|
|
|
| 278 |
elif name == "tavily":
|
| 279 |
import httpx
|
| 280 |
+
_key = os.getenv(env_key)
|
| 281 |
+
_base_url = cfg.get("base_url", "https://api.tavily.com/search")
|
| 282 |
+
_def_results = int(cfg.get("default_results", "5"))
|
| 283 |
+
_incl_answer = cfg.get("include_answer", "true").lower() == "true"
|
| 284 |
|
| 285 |
@mcp.tool()
|
| 286 |
async def tavily_search(query: str, max_results: int = _def_results) -> str:
|
|
|
|
| 319 |
@mcp.tool()
|
| 320 |
def list_active_tools() -> Dict[str, Any]:
|
| 321 |
"""Show active providers and configured integrations (key names only, never values)."""
|
| 322 |
+
llm = app_config.get_active_llm_providers()
|
| 323 |
+
search = app_config.get_active_search_providers()
|
| 324 |
+
hub = app_config.get_hub()
|
| 325 |
return {
|
| 326 |
+
"hub": hub.get("HUB_NAME", "Universal MCP Hub"),
|
| 327 |
+
"version": hub.get("HUB_VERSION", ""),
|
| 328 |
+
"active_llm_providers": [n for n, c in llm.items() if os.getenv(c.get("env_key", ""))],
|
| 329 |
+
"active_search_providers":[n for n, c in search.items() if os.getenv(c.get("env_key", ""))],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
}
|
| 331 |
logger.info("Tool registered: list_active_tools")
|
| 332 |
|