Alibrown commited on
Commit
bce20a3
Β·
verified Β·
1 Parent(s): d1c8b54

Update app/mcp.py

Browse files
Files changed (1) hide show
  1. 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 start_mcp() -> None:
30
  """
31
- Main entry point for the MCP Hub.
32
- Called by app/app.py in its own thread/event loop.
33
- Reads all config from app/.pyfun via app/config.py.
34
- NO fundaments passed in β€” sandboxed.
35
  """
36
- logger.info("MCP Hub starting...")
37
 
38
- # --- Load transport config from app/.pyfun [HUB] ---
39
- hub_cfg = app_config.get_hub()
40
- transport = os.getenv("MCP_TRANSPORT", hub_cfg.get("HUB_TRANSPORT", "stdio")).lower()
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
- mcp = FastMCP(
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
- # Tool Registration β€” MINIMAL BUILD
60
- # Tools register only if their ENV key exists (value never read here!).
61
- # Key NAMES come from app/.pyfun [LLM_PROVIDERS] / [SEARCH_PROVIDERS].
62
- # =========================================================================
63
-
64
- # --- LLM Tools ---
65
- _register_llm_tools(mcp)
66
 
67
- # --- Search Tools ---
68
- _register_search_tools(mcp)
69
 
70
- # --- DB Tools --- (disabled until db_sync is ready)
71
- # _register_db_tools(mcp)
72
 
73
- # --- System Tools (always registered) ---
74
- _register_system_tools(mcp)
 
 
 
 
 
 
 
75
 
76
- # =========================================================================
77
- # Start transport
78
- # =========================================================================
79
- if transport == "sse":
80
- logger.info(f"MCP Hub starting via SSE on {host}:{port}")
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
- logger.info("MCP Hub shut down.")
 
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 = os.getenv(env_key)
107
- _api_ver = cfg.get("api_version_header", "2023-06-01")
108
- _base_url = cfg.get("base_url", "https://api.anthropic.com/v1")
109
- _def_model = cfg.get("default_model", "claude-haiku-4-5-20251001")
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 = 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,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 = os.getenv(env_key)
171
- _base_url = cfg.get("base_url", "https://openrouter.ai/api/v1")
172
- _def_model = cfg.get("default_model", "mistralai/mistral-7b-instruct")
173
- _referer = os.getenv("APP_URL", "https://huggingface.co")
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 = os.getenv(env_key)
206
- _base_url = cfg.get("base_url", "https://api-inference.huggingface.co/models")
207
- _def_model = cfg.get("default_model", "mistralai/Mistral-7B-Instruct-v0.3")
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 = os.getenv(env_key)
285
- _base_url = cfg.get("base_url", "https://api.tavily.com/search")
286
- _def_results = int(cfg.get("default_results", "5"))
287
- _incl_answer = cfg.get("include_answer", "true").lower() == "true"
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 = app_config.get_active_llm_providers()
327
- search = app_config.get_active_search_providers()
328
- hub = app_config.get_hub()
329
  return {
330
- "hub": hub.get("HUB_NAME", "Universal MCP Hub"),
331
- "version": hub.get("HUB_VERSION", ""),
332
- "active_llm_providers": [
333
- name for name, cfg in llm.items()
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