Alibrown commited on
Commit
78519ba
Β·
verified Β·
1 Parent(s): 9b296b2

Update app/mcp.py

Browse files
Files changed (1) hide show
  1. app/mcp.py +110 -28
app/mcp.py CHANGED
@@ -1,5 +1,6 @@
1
  # =============================================================================
2
  # root/app/mcp.py
 
3
  # Universal MCP Hub (Sandboxed) - based on PyFundaments Architecture
4
  # Copyright 2026 - Volkan KΓΌcΓΌkbudak
5
  # Apache License V. 2 + ESOL 1.1
@@ -10,9 +11,17 @@
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 this handler β€” this is by design.
 
 
 
 
 
 
 
 
16
  #
17
  # TOOL REGISTRATION PRINCIPLE:
18
  # Tools are registered via tools.py β€” NOT hardcoded here.
@@ -36,10 +45,14 @@ from . import models
36
  from . import tools
37
 
38
  logger = logging.getLogger('mcp')
 
39
  # =============================================================================
40
- # Global MCP instance β€” initialized once via initialize()
41
  # =============================================================================
42
- _mcp = None
 
 
 
43
  # =============================================================================
44
  # Initialization β€” called exclusively by app/app.py
45
  # =============================================================================
@@ -49,17 +62,25 @@ async def initialize() -> None:
49
  Called once by app/app.py during startup sequence.
50
  No fundaments passed in β€” fully sandboxed.
51
 
 
 
 
 
 
 
52
  Registration order:
53
  1. LLM tools β†’ via tools.py + providers.py (key-gated)
54
  2. Search tools β†’ via tools.py + providers.py (key-gated)
55
  3. System tools β†’ always registered, no key required
56
  4. DB tools β†’ uncomment when db_sync.py is ready
57
  """
58
- global _mcp
59
 
60
- logger.info("MCP Hub initializing...")
 
 
61
 
62
- hub_cfg = app_config.get_hub()
63
 
64
  try:
65
  from mcp.server.fastmcp import FastMCP
@@ -72,7 +93,8 @@ async def initialize() -> None:
72
  instructions=(
73
  f"{hub_cfg.get('HUB_DESCRIPTION', 'Universal MCP Hub on PyFundaments')} "
74
  "Use list_active_tools to see what is currently available."
75
- )
 
76
  )
77
 
78
  # --- Initialize registries ---
@@ -84,19 +106,51 @@ async def initialize() -> None:
84
  _register_llm_tools(_mcp)
85
  _register_search_tools(_mcp)
86
  _register_system_tools(_mcp)
87
- #_register_db_tools(_mcp) not needed any more
 
 
 
88
 
89
- logger.info("MCP Hub initialized.")
90
-
91
  # =============================================================================
92
- # Request Handler β€” Quart /mcp route entry point
93
  # =============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
 
 
 
95
  async def handle_request(request) -> None:
96
  """
97
- Handles incoming MCP SSE requests routed through Quart /mcp endpoint.
98
- Central interceptor point for all MCP traffic.
99
- Add auth, logging, rate limiting, payload transformation here as needed.
 
 
 
 
 
100
  """
101
  if _mcp is None:
102
  logger.error("MCP not initialized β€” call initialize() first.")
@@ -104,7 +158,7 @@ async def handle_request(request) -> None:
104
  return jsonify({"error": "MCP not initialized"}), 503
105
 
106
  # --- Interceptor hooks (uncomment as needed) ---
107
- # logger.debug(f"MCP request: {request.method} {request.path}")
108
  # await _check_auth(request)
109
  # await _rate_limit(request)
110
  # await _log_payload(request)
@@ -217,6 +271,7 @@ def _register_system_tools(mcp) -> None:
217
  return {
218
  "hub": hub.get("HUB_NAME", "Universal MCP Hub"),
219
  "version": hub.get("HUB_VERSION", ""),
 
220
  "active_llm_providers": providers.list_active_llm(),
221
  "active_search_providers": providers.list_active_search(),
222
  "active_tools": tools.list_all(),
@@ -231,9 +286,13 @@ def _register_system_tools(mcp) -> None:
231
  Health check endpoint for HuggingFace Spaces and monitoring systems.
232
 
233
  Returns:
234
- Dict with service status.
235
  """
236
- return {"status": "ok", "service": "Universal MCP Hub"}
 
 
 
 
237
 
238
  logger.info("Tool registered: health_check")
239
 
@@ -257,33 +316,56 @@ def _register_system_tools(mcp) -> None:
257
  # =============================================================================
258
  # DB Tools β€” uncomment when db_sync.py is ready
259
  # =============================================================================
260
-
261
  # def _register_db_tools(mcp) -> None:
262
  # """
263
  # Register internal SQLite query tool.
264
  # Uses db_sync.py (app/* internal SQLite) β€” NOT postgresql.py (Guardian-only)!
265
- # Only SELECT queries are permitted β€” read-only by design.
 
 
 
 
 
 
 
 
 
266
  # """
267
  # from . import db_sync
268
  #
269
  # @mcp.tool()
270
- # async def db_query(query: str) -> list:
271
  # """
272
  # Execute a read-only SELECT query on the internal hub state database.
273
- # Only SELECT statements are allowed β€” write operations are blocked.
 
 
 
 
 
 
 
 
 
 
274
  #
275
  # Args:
276
- # query: SQL SELECT statement to execute.
277
  #
278
  # Returns:
279
- # List of result rows as dicts.
 
 
 
 
280
  # """
281
- # return await db_sync.query(query)
282
  #
283
- # logger.info("Tool registered: db_query")
 
 
284
  # =============================================================================
285
  # Direct execution guard
286
  # =============================================================================
287
-
288
  if __name__ == '__main__':
289
  print("WARNING: Run via main.py β†’ app.py, not directly.")
 
1
  # =============================================================================
2
  # root/app/mcp.py
3
+ # 14.03.2026
4
  # Universal MCP Hub (Sandboxed) - based on PyFundaments Architecture
5
  # Copyright 2026 - Volkan KΓΌcΓΌkbudak
6
  # Apache License V. 2 + ESOL 1.1
 
11
  # NO direct access to fundaments/*, .env, or Guardian (main.py).
12
  # All config comes from app/.pyfun via app/config.py.
13
  #
14
+ # TRANSPORT:
15
+ # Primary: Streamable HTTP (MCP spec 2025-11-25) β†’ single /mcp endpoint
16
+ # Configured via HUB_TRANSPORT = "streamable-http" in .pyfun [HUB]
17
+ # ASGI-App via get_asgi_app() β†’ mounted by app/app.py
18
+ #
19
+ # Fallback: SSE (legacy, deprecated per spec) β†’ /mcp route via Quart
20
+ # Configured via HUB_TRANSPORT = "sse" in .pyfun [HUB]
21
+ # handle_request() called directly by app/app.py Quart route
22
+ #
23
+ # All MCP traffic (both transports) passes through app/app.py first β€”
24
+ # auth checks, rate limiting, logging can be added there before reaching MCP.
25
  #
26
  # TOOL REGISTRATION PRINCIPLE:
27
  # Tools are registered via tools.py β€” NOT hardcoded here.
 
45
  from . import tools
46
 
47
  logger = logging.getLogger('mcp')
48
+
49
  # =============================================================================
50
+ # Globals β€” set once during initialize(), never touched elsewhere
51
  # =============================================================================
52
+ _mcp = None # FastMCP instance
53
+ _transport = None # "streamable-http" | "sse" β€” from .pyfun [HUB] HUB_TRANSPORT
54
+ _stateless = None # True = HF Spaces / horizontal scaling safe
55
+
56
  # =============================================================================
57
  # Initialization β€” called exclusively by app/app.py
58
  # =============================================================================
 
62
  Called once by app/app.py during startup sequence.
63
  No fundaments passed in β€” fully sandboxed.
64
 
65
+ Reads HUB_TRANSPORT and HUB_STATELESS from .pyfun [HUB].
66
+
67
+ Transport modes:
68
+ streamable-http β†’ get_asgi_app() returns ASGI app β†’ app.py mounts it
69
+ sse β†’ handle_request() used by Quart route in app.py
70
+
71
  Registration order:
72
  1. LLM tools β†’ via tools.py + providers.py (key-gated)
73
  2. Search tools β†’ via tools.py + providers.py (key-gated)
74
  3. System tools β†’ always registered, no key required
75
  4. DB tools β†’ uncomment when db_sync.py is ready
76
  """
77
+ global _mcp, _transport, _stateless
78
 
79
+ hub_cfg = app_config.get_hub()
80
+ _transport = hub_cfg.get("HUB_TRANSPORT", "streamable-http").lower()
81
+ _stateless = hub_cfg.get("HUB_STATELESS", "true").lower() == "true"
82
 
83
+ logger.info(f"MCP Hub initializing (transport: {_transport}, stateless: {_stateless})...")
84
 
85
  try:
86
  from mcp.server.fastmcp import FastMCP
 
93
  instructions=(
94
  f"{hub_cfg.get('HUB_DESCRIPTION', 'Universal MCP Hub on PyFundaments')} "
95
  "Use list_active_tools to see what is currently available."
96
+ ),
97
+ stateless_http=_stateless, # True = no session state, HF Spaces safe
98
  )
99
 
100
  # --- Initialize registries ---
 
106
  _register_llm_tools(_mcp)
107
  _register_search_tools(_mcp)
108
  _register_system_tools(_mcp)
109
+ # _register_db_tools(_mcp) # uncomment when db_sync.py is ready
110
+
111
+ logger.info(f"MCP Hub initialized. Transport: {_transport}")
112
+
113
 
 
 
114
  # =============================================================================
115
+ # ASGI App β€” used by app/app.py for Streamable HTTP transport
116
  # =============================================================================
117
+ def get_asgi_app():
118
+ """
119
+ Returns the ASGI app for the configured transport.
120
+ Called by app/app.py AFTER initialize() β€” mounted as ASGI sub-app.
121
+
122
+ Streamable HTTP: mounts on /mcp β€” single endpoint for all MCP traffic.
123
+ SSE (fallback): returns sse_app() for legacy client compatibility.
124
+
125
+ NOTE: For SSE transport, app/app.py uses the Quart route + handle_request()
126
+ instead β€” get_asgi_app() is only called for streamable-http.
127
+ """
128
+ if _mcp is None:
129
+ raise RuntimeError("MCP not initialized β€” call initialize() first.")
130
+
131
+ if _transport == "streamable-http":
132
+ logger.info("MCP ASGI app: Streamable HTTP β†’ /mcp")
133
+ return _mcp.streamable_http_app()
134
+ else:
135
+ # SSE as ASGI app β€” only used if app.py mounts it directly
136
+ # (normally app.py uses the Quart route + handle_request() for SSE)
137
+ logger.info("MCP ASGI app: SSE (legacy) β†’ /sse")
138
+ return _mcp.sse_app()
139
+
140
 
141
+ # =============================================================================
142
+ # Request Handler β€” Quart /mcp route entry point (SSE legacy transport only)
143
+ # =============================================================================
144
  async def handle_request(request) -> None:
145
  """
146
+ Handles incoming MCP SSE requests via Quart /mcp route.
147
+ Only active when HUB_TRANSPORT = "sse" in .pyfun [HUB].
148
+
149
+ For Streamable HTTP transport this function is NOT called β€”
150
+ app/app.py mounts the ASGI app from get_asgi_app() directly.
151
+
152
+ Interceptor point for SSE traffic:
153
+ Add auth, rate limiting, logging here before reaching MCP.
154
  """
155
  if _mcp is None:
156
  logger.error("MCP not initialized β€” call initialize() first.")
 
158
  return jsonify({"error": "MCP not initialized"}), 503
159
 
160
  # --- Interceptor hooks (uncomment as needed) ---
161
+ # logger.debug(f"MCP SSE request: {request.method} {request.path}")
162
  # await _check_auth(request)
163
  # await _rate_limit(request)
164
  # await _log_payload(request)
 
271
  return {
272
  "hub": hub.get("HUB_NAME", "Universal MCP Hub"),
273
  "version": hub.get("HUB_VERSION", ""),
274
+ "transport": _transport,
275
  "active_llm_providers": providers.list_active_llm(),
276
  "active_search_providers": providers.list_active_search(),
277
  "active_tools": tools.list_all(),
 
286
  Health check endpoint for HuggingFace Spaces and monitoring systems.
287
 
288
  Returns:
289
+ Dict with service status and active transport.
290
  """
291
+ return {
292
+ "status": "ok",
293
+ "service": "Universal MCP Hub",
294
+ "transport": _transport,
295
+ }
296
 
297
  logger.info("Tool registered: health_check")
298
 
 
316
  # =============================================================================
317
  # DB Tools β€” uncomment when db_sync.py is ready
318
  # =============================================================================
 
319
  # def _register_db_tools(mcp) -> None:
320
  # """
321
  # Register internal SQLite query tool.
322
  # Uses db_sync.py (app/* internal SQLite) β€” NOT postgresql.py (Guardian-only)!
323
+ #
324
+ # SECURITY: Only SELECT queries are permitted.
325
+ # Enforced at application level in db_sync.query() β€” not just in docs.
326
+ # Tables accessible: hub_state, tool_cache (app/* only)
327
+ # Tables blocked: users, sessions (Guardian-only, different owner)
328
+ #
329
+ # To enable:
330
+ # 1. Uncomment this function
331
+ # 2. Uncomment _register_db_tools(_mcp) in initialize()
332
+ # 3. Make sure db_sync.initialize() is called in app/app.py before mcp.initialize()
333
  # """
334
  # from . import db_sync
335
  #
336
  # @mcp.tool()
337
+ # async def db_query(sql: str) -> list:
338
  # """
339
  # Execute a read-only SELECT query on the internal hub state database.
340
+ #
341
+ # Only SELECT statements are permitted β€” all write operations are blocked
342
+ # at the db_sync layer (not just by convention).
343
+ #
344
+ # Accessible tables:
345
+ # hub_state β€” current hub runtime state (tool status, uptime, etc.)
346
+ # tool_cache β€” cached tool results for repeated queries
347
+ #
348
+ # NOT accessible (Guardian-only):
349
+ # users β€” managed by fundaments/user_handler.py
350
+ # sessions β€” managed by fundaments/user_handler.py
351
  #
352
  # Args:
353
+ # sql: SQL SELECT statement. Example: "SELECT * FROM hub_state LIMIT 10"
354
  #
355
  # Returns:
356
+ # List of result rows as dicts. Empty list if no results.
357
+ #
358
+ # Raises:
359
+ # ValueError: If statement is not a SELECT query.
360
+ # RuntimeError: If db_sync is not initialized.
361
  # """
362
+ # return await db_sync.query(sql)
363
  #
364
+ # logger.info("Tool registered: db_query (SQLite SELECT-only, app/* tables)")
365
+
366
+
367
  # =============================================================================
368
  # Direct execution guard
369
  # =============================================================================
 
370
  if __name__ == '__main__':
371
  print("WARNING: Run via main.py β†’ app.py, not directly.")