sunholo 0.144.1__py3-none-any.whl → 0.144.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,7 +58,7 @@ except ImportError:
58
58
  MCPClientManager = None
59
59
 
60
60
  try:
61
- from ...mcp.vac_mcp_server import VACMCPServer
61
+ from ...mcp.vac_mcp_server_fastmcp import VACMCPServer
62
62
  except ImportError:
63
63
  VACMCPServer = None
64
64
 
@@ -81,33 +81,268 @@ class VACRequest(BaseModel):
81
81
 
82
82
  class VACRoutesFastAPI:
83
83
  """
84
- FastAPI implementation of VAC routes with streaming support.
84
+ FastAPI implementation of VAC routes with streaming support and extensible MCP integration.
85
85
 
86
- This class provides a FastAPI-compatible version of the Flask VACRoutes,
87
- with proper async streaming support using callbacks.
86
+ This class provides a comprehensive FastAPI application with:
87
+ - VAC (Virtual Agent Computer) endpoints for AI chat and streaming
88
+ - OpenAI-compatible API endpoints
89
+ - Extensible MCP (Model Context Protocol) server integration for Claude Desktop/Code
90
+ - MCP client support for connecting to external MCP servers
91
+ - A2A (Agent-to-Agent) protocol support
92
+ - Server-Sent Events (SSE) streaming capabilities
93
+
94
+ ## Key Features
95
+
96
+ ### 1. VAC Endpoints
97
+ - `/vac/{vector_name}` - Non-streaming VAC responses
98
+ - `/vac/streaming/{vector_name}` - Plain text streaming responses
99
+ - `/vac/streaming/{vector_name}/sse` - Server-Sent Events streaming
100
+
101
+ ### 2. OpenAI Compatible API
102
+ - `/openai/v1/chat/completions` - OpenAI-compatible chat completions
103
+ - Supports both streaming and non-streaming modes
104
+
105
+ ### 3. MCP Integration
106
+ - **MCP Server**: Expose your VAC as MCP tools for Claude Desktop/Code
107
+ - **MCP Client**: Connect to external MCP servers and use their tools
108
+ - **Custom Tools**: Easily add your own MCP tools using decorators
109
+
110
+ ### 4. A2A Agent Protocol
111
+ - Agent discovery and task execution
112
+ - Compatible with multi-agent workflows
113
+
114
+ ## Basic Usage
115
+
116
+ ### Simplified Setup (Recommended)
117
+
118
+ Use the helper method for automatic lifespan management:
88
119
 
89
- Usage Example:
90
120
  ```python
121
+ from sunholo.agents.fastapi import VACRoutesFastAPI
122
+
123
+ async def my_stream_interpreter(question, vector_name, chat_history, callback, **kwargs):
124
+ # Your streaming VAC logic here
125
+ # Use callback.async_on_llm_new_token(token) for streaming
126
+ return {"answer": "Response", "sources": []}
127
+
128
+ # Single call sets up everything with MCP server and proper lifespan management
129
+ app, vac_routes = VACRoutesFastAPI.create_app_with_mcp(
130
+ title="My VAC Application",
131
+ stream_interpreter=my_stream_interpreter
132
+ # MCP server is automatically enabled when using this method
133
+ )
134
+
135
+ # Add custom endpoints if needed
136
+ @app.get("/custom")
137
+ async def custom_endpoint():
138
+ return {"message": "Hello"}
139
+
140
+ # Run the app
141
+ if __name__ == "__main__":
142
+ import uvicorn
143
+ uvicorn.run(app, host="0.0.0.0", port=8000)
144
+ ```
145
+
146
+ ### Manual Setup (Advanced)
147
+
148
+ For more control over lifespan management:
149
+
150
+ ```python
151
+ from contextlib import asynccontextmanager
91
152
  from fastapi import FastAPI
92
153
  from sunholo.agents.fastapi import VACRoutesFastAPI
93
154
 
94
- app = FastAPI()
155
+ async def my_stream_interpreter(question, vector_name, chat_history, callback, **kwargs):
156
+ return {"answer": "Response", "sources": []}
95
157
 
96
- async def stream_interpreter(question, vector_name, chat_history, callback, **kwargs):
97
- # Implement your streaming logic with callbacks
98
- ...
158
+ # Define your app's lifespan
159
+ @asynccontextmanager
160
+ async def app_lifespan(app: FastAPI):
161
+ print("Starting up...")
162
+ yield
163
+ print("Shutting down...")
164
+
165
+ # Create temp app to get MCP lifespan
166
+ temp_app = FastAPI()
167
+ vac_routes_temp = VACRoutesFastAPI(
168
+ temp_app,
169
+ stream_interpreter=my_stream_interpreter,
170
+ enable_mcp_server=True
171
+ )
172
+
173
+ # Get MCP lifespan
174
+ mcp_lifespan = vac_routes_temp.get_mcp_lifespan()
175
+
176
+ # Combine lifespans
177
+ @asynccontextmanager
178
+ async def combined_lifespan(app: FastAPI):
179
+ async with app_lifespan(app):
180
+ if mcp_lifespan:
181
+ async with mcp_lifespan(app):
182
+ yield
183
+ else:
184
+ yield
99
185
 
100
- async def vac_interpreter(question, vector_name, chat_history, **kwargs):
101
- # Implement your static VAC logic
102
- ...
186
+ # Create app with combined lifespan
187
+ app = FastAPI(title="My VAC Application", lifespan=combined_lifespan)
103
188
 
189
+ # Initialize VAC routes
104
190
  vac_routes = VACRoutesFastAPI(
105
- app,
106
- stream_interpreter,
107
- vac_interpreter,
191
+ app=app,
192
+ stream_interpreter=my_stream_interpreter,
108
193
  enable_mcp_server=True
109
194
  )
110
195
  ```
196
+
197
+ Your FastAPI app now includes:
198
+ - All VAC endpoints
199
+ - MCP server at /mcp (for Claude Desktop/Code to connect)
200
+ - Built-in VAC tools: vac_stream, vac_query, list_available_vacs, get_vac_info
201
+
202
+ ## Adding Custom MCP Tools
203
+
204
+ ### Method 1: Using Decorators
205
+ ```python
206
+ vac_routes = VACRoutesFastAPI(app, stream_interpreter, enable_mcp_server=True)
207
+
208
+ @vac_routes.add_mcp_tool
209
+ async def get_weather(city: str) -> str:
210
+ '''Get weather information for a city.'''
211
+ # Your weather API logic
212
+ return f"Weather in {city}: Sunny, 22°C"
213
+
214
+ @vac_routes.add_mcp_tool("custom_search", "Search our database")
215
+ async def search_database(query: str, limit: int = 10) -> list:
216
+ '''Search internal database with custom name and description.'''
217
+ # Your database search logic
218
+ return [{"result": f"Found: {query}"}]
219
+ ```
220
+
221
+ ### Method 2: Programmatic Registration
222
+ ```python
223
+ async def my_business_tool(param: str) -> dict:
224
+ return {"processed": param}
225
+
226
+ # Add tool with custom name and description
227
+ vac_routes.add_mcp_tool(
228
+ my_business_tool,
229
+ "process_business_data",
230
+ "Process business data with our custom logic"
231
+ )
232
+ ```
233
+
234
+ ### Method 3: Advanced MCP Server Access
235
+ ```python
236
+ # Get direct access to MCP server for advanced customization
237
+ mcp_server = vac_routes.get_mcp_server()
238
+
239
+ @mcp_server.add_tool
240
+ async def advanced_tool(complex_param: dict) -> str:
241
+ return f"Advanced processing: {complex_param}"
242
+
243
+ # List all registered tools
244
+ print("Available MCP tools:", vac_routes.list_mcp_tools())
245
+ ```
246
+
247
+ ## MCP Client Integration
248
+
249
+ Connect to external MCP servers and use their tools:
250
+
251
+ ```python
252
+ mcp_servers = [
253
+ {
254
+ "name": "filesystem-server",
255
+ "command": "npx",
256
+ "args": ["@modelcontextprotocol/server-filesystem", "/path/to/files"]
257
+ }
258
+ ]
259
+
260
+ vac_routes = VACRoutesFastAPI(
261
+ app, stream_interpreter,
262
+ mcp_servers=mcp_servers, # Connect to external MCP servers
263
+ enable_mcp_server=True # Also expose our own MCP server
264
+ )
265
+
266
+ # External MCP tools available at:
267
+ # GET /mcp/tools - List all external tools
268
+ # POST /mcp/call - Call external MCP tools
269
+ ```
270
+
271
+ ## Claude Desktop Integration
272
+
273
+ ### Option 1: Remote Integration (Recommended for Development)
274
+ ```python
275
+ # Run your FastAPI app
276
+ uvicorn.run(vac_routes.app, host="0.0.0.0", port=8000)
277
+
278
+ # Configure Claude Desktop (Settings > Connectors > Add custom connector):
279
+ # URL: http://localhost:8000/mcp
280
+ ```
281
+
282
+ ### Option 2: Local Integration
283
+ Create a standalone script for Claude Desktop:
284
+ ```python
285
+ # claude_mcp_server.py
286
+ from sunholo.mcp.extensible_mcp_server import create_mcp_server
287
+
288
+ server = create_mcp_server("my-app", include_vac_tools=True)
289
+
290
+ @server.add_tool
291
+ async def my_app_tool(param: str) -> str:
292
+ return f"My app processed: {param}"
293
+
294
+ if __name__ == "__main__":
295
+ server.run()
296
+
297
+ # Install: fastmcp install claude-desktop claude_mcp_server.py --with sunholo[anthropic]
298
+ ```
299
+
300
+ ## Available Built-in MCP Tools
301
+
302
+ When `enable_mcp_server=True`, these tools are automatically available:
303
+
304
+ - **`vac_stream`**: Stream responses from any configured VAC
305
+ - **`vac_query`**: Query VACs with non-streaming responses
306
+ - **`list_available_vacs`**: List all available VAC configurations
307
+ - **`get_vac_info`**: Get detailed information about a specific VAC
308
+
309
+ ## Error Handling and Best Practices
310
+
311
+ ```python
312
+ @vac_routes.add_mcp_tool
313
+ async def robust_tool(user_input: str) -> str:
314
+ '''Example of robust tool implementation.'''
315
+ try:
316
+ # Validate input
317
+ if not user_input or len(user_input) > 1000:
318
+ return "Error: Invalid input length"
319
+
320
+ # Your business logic
321
+ result = await process_user_input(user_input)
322
+
323
+ return f"Processed: {result}"
324
+
325
+ except Exception as e:
326
+ # Log error and return user-friendly message
327
+ log.error(f"Tool error: {e}")
328
+ return f"Error processing request: {str(e)}"
329
+ ```
330
+
331
+ ## Configuration Options
332
+
333
+ ```python
334
+ vac_routes = VACRoutesFastAPI(
335
+ app=app,
336
+ stream_interpreter=my_stream_func,
337
+ vac_interpreter=my_vac_func, # Optional non-streaming function
338
+ additional_routes=[], # Custom FastAPI routes
339
+ mcp_servers=[], # External MCP servers to connect to
340
+ add_langfuse_eval=True, # Enable Langfuse evaluation
341
+ enable_mcp_server=True, # Enable MCP server for Claude
342
+ enable_a2a_agent=False, # Enable A2A agent protocol
343
+ a2a_vac_names=None # VACs available for A2A
344
+ )
345
+ ```
111
346
  """
112
347
 
113
348
  def __init__(
@@ -123,18 +358,86 @@ class VACRoutesFastAPI:
123
358
  a2a_vac_names: Optional[List[str]] = None
124
359
  ):
125
360
  """
126
- Initialize FastAPI VAC routes.
361
+ Initialize FastAPI VAC routes with comprehensive AI and MCP integration.
127
362
 
128
363
  Args:
129
- app: FastAPI application instance
130
- stream_interpreter: Async or sync function for streaming responses
131
- vac_interpreter: Optional function for non-streaming responses
132
- additional_routes: List of additional routes to register
133
- mcp_servers: List of MCP server configurations
134
- add_langfuse_eval: Whether to add Langfuse evaluation
135
- enable_mcp_server: Whether to enable MCP server endpoint
136
- enable_a2a_agent: Whether to enable A2A agent endpoints
137
- a2a_vac_names: List of VAC names for A2A agent
364
+ app: FastAPI application instance to register routes on
365
+ stream_interpreter: Function for streaming VAC responses. Can be async or sync.
366
+ Called with (question, vector_name, chat_history, callback, **kwargs)
367
+ vac_interpreter: Optional function for non-streaming VAC responses. If not provided,
368
+ will use stream_interpreter without streaming callbacks.
369
+ additional_routes: List of custom route dictionaries to register:
370
+ [{"path": "/custom", "handler": func, "methods": ["GET"]}]
371
+ mcp_servers: List of external MCP server configurations to connect to:
372
+ [{"name": "server-name", "command": "python", "args": ["server.py"]}]
373
+ add_langfuse_eval: Whether to enable Langfuse evaluation and tracing
374
+ enable_mcp_server: Whether to enable the MCP server at /mcp endpoint for
375
+ Claude Desktop/Code integration. When True, automatically
376
+ includes built-in VAC tools and supports custom tool registration.
377
+ enable_a2a_agent: Whether to enable A2A (Agent-to-Agent) protocol endpoints
378
+ a2a_vac_names: List of VAC names available for A2A agent interactions
379
+
380
+ ## Stream Interpreter Function
381
+
382
+ Your stream_interpreter should handle streaming responses:
383
+
384
+ ```python
385
+ async def my_stream_interpreter(question: str, vector_name: str,
386
+ chat_history: list, callback, **kwargs):
387
+ # Process the question using your AI/RAG pipeline
388
+
389
+ # For streaming tokens:
390
+ await callback.async_on_llm_new_token("partial response...")
391
+
392
+ # Return final result with sources:
393
+ return {
394
+ "answer": "Final complete answer",
395
+ "sources": [{"title": "Source 1", "url": "..."}]
396
+ }
397
+ ```
398
+
399
+ ## MCP Server Integration
400
+
401
+ When enable_mcp_server=True, the following happens:
402
+ 1. MCP server is mounted at /mcp endpoint
403
+ 2. Built-in VAC tools are automatically registered:
404
+ - vac_stream, vac_query, list_available_vacs, get_vac_info
405
+ 3. You can add custom MCP tools using add_mcp_tool()
406
+ 4. Claude Desktop/Code can connect to http://your-server/mcp
407
+
408
+ ## Complete Example
409
+
410
+ ```python
411
+ app = FastAPI(title="My VAC Application")
412
+
413
+ async def my_vac_logic(question, vector_name, chat_history, callback, **kwargs):
414
+ # Your AI/RAG implementation
415
+ result = await process_with_ai(question)
416
+ return {"answer": result, "sources": []}
417
+
418
+ # External MCP servers to connect to
419
+ external_mcp = [
420
+ {"name": "filesystem", "command": "mcp-server-fs", "args": ["/data"]}
421
+ ]
422
+
423
+ vac_routes = VACRoutesFastAPI(
424
+ app=app,
425
+ stream_interpreter=my_vac_logic,
426
+ mcp_servers=external_mcp,
427
+ enable_mcp_server=True # Enable for Claude integration
428
+ )
429
+
430
+ # Add custom MCP tools for your business logic
431
+ @vac_routes.add_mcp_tool
432
+ async def get_customer_info(customer_id: str) -> dict:
433
+ return await fetch_customer(customer_id)
434
+
435
+ # Your app now has:
436
+ # - VAC endpoints: /vac/{vector_name}, /vac/streaming/{vector_name}
437
+ # - OpenAI API: /openai/v1/chat/completions
438
+ # - MCP server: /mcp (with built-in + custom tools)
439
+ # - MCP client: /mcp/tools, /mcp/call (for external servers)
440
+ ```
138
441
  """
139
442
  self.app = app
140
443
  self.stream_interpreter = stream_interpreter
@@ -152,11 +455,17 @@ class VACRoutesFastAPI:
152
455
  # MCP server initialization
153
456
  self.enable_mcp_server = enable_mcp_server
154
457
  self.vac_mcp_server = None
458
+ self._custom_mcp_tools = []
459
+ self._custom_mcp_resources = []
460
+
155
461
  if self.enable_mcp_server and VACMCPServer:
156
462
  self.vac_mcp_server = VACMCPServer(
157
- stream_interpreter=self.stream_interpreter,
158
- vac_interpreter=self.vac_interpreter
463
+ server_name="sunholo-vac-fastapi-server",
464
+ include_vac_tools=True
159
465
  )
466
+
467
+ # Add any pre-registered custom tools
468
+ self._register_custom_tools()
160
469
 
161
470
  # A2A agent initialization
162
471
  self.enable_a2a_agent = enable_a2a_agent
@@ -168,6 +477,138 @@ class VACRoutesFastAPI:
168
477
 
169
478
  self.register_routes()
170
479
 
480
+ @staticmethod
481
+ def create_app_with_mcp(
482
+ title: str = "VAC Application",
483
+ stream_interpreter: Optional[callable] = None,
484
+ vac_interpreter: Optional[callable] = None,
485
+ app_lifespan: Optional[callable] = None,
486
+ **kwargs
487
+ ) -> tuple[FastAPI, 'VACRoutesFastAPI']:
488
+ """
489
+ Helper method to create a FastAPI app with proper MCP lifespan management.
490
+
491
+ This method simplifies the setup process by handling the lifespan combination
492
+ automatically, avoiding the need for the double initialization pattern.
493
+ MCP server is automatically enabled when using this method.
494
+
495
+ Args:
496
+ title: Title for the FastAPI app
497
+ stream_interpreter: Streaming interpreter function
498
+ vac_interpreter: Non-streaming interpreter function
499
+ app_lifespan: Optional app lifespan context manager
500
+ **kwargs: Additional arguments passed to VACRoutesFastAPI (except enable_mcp_server)
501
+
502
+ Returns:
503
+ Tuple of (FastAPI app, VACRoutesFastAPI instance)
504
+
505
+ Example:
506
+ ```python
507
+ from sunholo.agents.fastapi import VACRoutesFastAPI
508
+
509
+ async def my_interpreter(question, vector_name, chat_history, callback, **kwargs):
510
+ # Your logic here
511
+ return {"answer": "response", "sources": []}
512
+
513
+ # Single call to set up everything (MCP is automatically enabled)
514
+ app, vac_routes = VACRoutesFastAPI.create_app_with_mcp(
515
+ title="My VAC App",
516
+ stream_interpreter=my_interpreter
517
+ )
518
+
519
+ # Add custom endpoints
520
+ @app.get("/custom")
521
+ async def custom_endpoint():
522
+ return {"message": "Custom endpoint"}
523
+
524
+ if __name__ == "__main__":
525
+ import uvicorn
526
+ uvicorn.run(app, host="0.0.0.0", port=8000)
527
+ ```
528
+ """
529
+ from contextlib import asynccontextmanager
530
+
531
+ # Default app lifespan if not provided
532
+ if app_lifespan is None:
533
+ @asynccontextmanager
534
+ async def app_lifespan(app: FastAPI):
535
+ yield
536
+
537
+ # Create temporary app to get MCP app (always enabled for this method)
538
+ temp_app = FastAPI()
539
+ temp_routes = VACRoutesFastAPI(
540
+ temp_app,
541
+ stream_interpreter=stream_interpreter,
542
+ vac_interpreter=vac_interpreter,
543
+ enable_mcp_server=True, # Always enabled for create_app_with_mcp
544
+ **kwargs
545
+ )
546
+
547
+ mcp_app = None
548
+ if temp_routes.vac_mcp_server:
549
+ mcp_app = temp_routes.vac_mcp_server.get_http_app()
550
+
551
+ # Create combined lifespan
552
+ @asynccontextmanager
553
+ async def combined_lifespan(app: FastAPI):
554
+ async with app_lifespan(app):
555
+ if mcp_app:
556
+ async with mcp_app.lifespan(app):
557
+ yield
558
+ else:
559
+ yield
560
+
561
+ # Create the actual app with combined lifespan
562
+ app = FastAPI(
563
+ title=title,
564
+ lifespan=combined_lifespan if mcp_app else app_lifespan
565
+ )
566
+
567
+ # Initialize VAC routes (MCP always enabled for this method)
568
+ vac_routes = VACRoutesFastAPI(
569
+ app,
570
+ stream_interpreter=stream_interpreter,
571
+ vac_interpreter=vac_interpreter,
572
+ enable_mcp_server=True, # Always enabled for create_app_with_mcp
573
+ **kwargs
574
+ )
575
+
576
+ return app, vac_routes
577
+
578
+ def get_mcp_lifespan(self):
579
+ """
580
+ Get the MCP app's lifespan for manual lifespan management.
581
+
582
+ Returns:
583
+ The MCP app's lifespan if MCP server is enabled, None otherwise.
584
+
585
+ Example:
586
+ ```python
587
+ from contextlib import asynccontextmanager
588
+
589
+ # Create temp app to get MCP lifespan
590
+ temp_app = FastAPI()
591
+ vac_routes = VACRoutesFastAPI(temp_app, ..., enable_mcp_server=True)
592
+ mcp_lifespan = vac_routes.get_mcp_lifespan()
593
+
594
+ # Combine with your app's lifespan
595
+ @asynccontextmanager
596
+ async def combined_lifespan(app: FastAPI):
597
+ async with my_app_lifespan(app):
598
+ if mcp_lifespan:
599
+ async with mcp_lifespan(app):
600
+ yield
601
+ else:
602
+ yield
603
+
604
+ app = FastAPI(lifespan=combined_lifespan)
605
+ ```
606
+ """
607
+ if self.vac_mcp_server:
608
+ mcp_app = self.vac_mcp_server.get_http_app()
609
+ return mcp_app.lifespan
610
+ return None
611
+
171
612
  async def vac_interpreter_default(self, question: str, vector_name: str, chat_history=None, **kwargs):
172
613
  """Default VAC interpreter that uses the stream interpreter without streaming."""
173
614
  class NoOpCallback:
@@ -232,10 +673,38 @@ class VACRoutesFastAPI:
232
673
  self.app.get("/mcp/resources")(self.handle_mcp_list_resources)
233
674
  self.app.post("/mcp/resources/read")(self.handle_mcp_read_resource)
234
675
 
235
- # MCP server endpoint
676
+ # MCP server endpoint - mount the FastMCP app
236
677
  if self.enable_mcp_server and self.vac_mcp_server:
237
- self.app.post("/mcp")(self.handle_mcp_server)
238
- self.app.get("/mcp")(self.handle_mcp_server_info)
678
+ try:
679
+ mcp_app = self.vac_mcp_server.get_http_app()
680
+
681
+ # Note: FastAPI doesn't expose lifespan as a public attribute,
682
+ # so we can't easily check if it's configured. The error will be
683
+ # caught below if lifespan is missing.
684
+
685
+ self.app.mount("/mcp", mcp_app)
686
+ log.info("✅ MCP server mounted at /mcp endpoint")
687
+
688
+ except RuntimeError as e:
689
+ if "Task group is not initialized" in str(e):
690
+ error_msg = (
691
+ "MCP server initialization failed: Lifespan not configured properly.\n"
692
+ "The FastAPI app must be created with the MCP lifespan.\n\n"
693
+ "Quick fix: Use the helper method:\n"
694
+ " app, vac_routes = VACRoutesFastAPI.create_app_with_mcp(\n"
695
+ " stream_interpreter=your_interpreter,\n"
696
+ " enable_mcp_server=True\n"
697
+ " )\n\n"
698
+ "Or manually configure the lifespan - see documentation for details."
699
+ )
700
+ log.error(error_msg)
701
+ raise RuntimeError(error_msg) from e
702
+ else:
703
+ log.error(f"Failed to mount MCP server: {e}")
704
+ raise RuntimeError(f"MCP server initialization failed: {e}") from e
705
+ except Exception as e:
706
+ log.error(f"Failed to mount MCP server: {e}")
707
+ raise RuntimeError(f"MCP server initialization failed: {e}") from e
239
708
 
240
709
  # A2A agent endpoints
241
710
  if self.enable_a2a_agent:
@@ -787,109 +1256,6 @@ class VACRoutesFastAPI:
787
1256
  except Exception as e:
788
1257
  raise HTTPException(status_code=500, detail=str(e))
789
1258
 
790
- async def handle_mcp_server(self, request: Request):
791
- """Handle MCP server requests."""
792
- if not self.vac_mcp_server:
793
- raise HTTPException(status_code=501, detail="MCP server not enabled")
794
-
795
- data = await request.json()
796
- log.info(f"MCP server received: {data}")
797
-
798
- # Process MCP request - simplified version
799
- # Full implementation would handle all MCP protocol methods
800
- method = data.get("method")
801
- params = data.get("params", {})
802
- request_id = data.get("id")
803
-
804
- try:
805
- if method == "initialize":
806
- response = {
807
- "jsonrpc": "2.0",
808
- "result": {
809
- "protocolVersion": "2025-06-18",
810
- "capabilities": {"tools": {}},
811
- "serverInfo": {
812
- "name": "sunholo-vac-server",
813
- "version": sunholo_version()
814
- }
815
- },
816
- "id": request_id
817
- }
818
- elif method == "tools/list":
819
- tools = [
820
- {
821
- "name": "vac_stream",
822
- "description": "Stream responses from a Sunholo VAC",
823
- "inputSchema": {
824
- "type": "object",
825
- "properties": {
826
- "vector_name": {"type": "string"},
827
- "user_input": {"type": "string"},
828
- "chat_history": {"type": "array", "default": []}
829
- },
830
- "required": ["vector_name", "user_input"]
831
- }
832
- }
833
- ]
834
- if self.vac_interpreter:
835
- tools.append({
836
- "name": "vac_query",
837
- "description": "Query a Sunholo VAC (non-streaming)",
838
- "inputSchema": {
839
- "type": "object",
840
- "properties": {
841
- "vector_name": {"type": "string"},
842
- "user_input": {"type": "string"},
843
- "chat_history": {"type": "array", "default": []}
844
- },
845
- "required": ["vector_name", "user_input"]
846
- }
847
- })
848
- response = {
849
- "jsonrpc": "2.0",
850
- "result": {"tools": tools},
851
- "id": request_id
852
- }
853
- elif method == "tools/call":
854
- tool_name = params.get("name")
855
- arguments = params.get("arguments", {})
856
-
857
- if tool_name == "vac_stream":
858
- result = await self.vac_mcp_server._handle_vac_stream(arguments)
859
- elif tool_name == "vac_query":
860
- result = await self.vac_mcp_server._handle_vac_query(arguments)
861
- else:
862
- raise ValueError(f"Unknown tool: {tool_name}")
863
-
864
- response = {
865
- "jsonrpc": "2.0",
866
- "result": {"content": [item.model_dump() for item in result]},
867
- "id": request_id
868
- }
869
- else:
870
- raise ValueError(f"Unknown method: {method}")
871
-
872
- except Exception as e:
873
- response = {
874
- "jsonrpc": "2.0",
875
- "error": {
876
- "code": -32603,
877
- "message": str(e)
878
- },
879
- "id": request_id
880
- }
881
-
882
- return JSONResponse(content=response)
883
-
884
- async def handle_mcp_server_info(self):
885
- """Return MCP server information."""
886
- return JSONResponse(content={
887
- "name": "sunholo-vac-server",
888
- "version": "1.0.0",
889
- "transport": "http",
890
- "endpoint": "/mcp",
891
- "tools": ["vac_stream", "vac_query"] if self.vac_interpreter else ["vac_stream"]
892
- })
893
1259
 
894
1260
  def _get_or_create_a2a_agent(self, request: Request):
895
1261
  """Get or create the A2A agent instance with current request context."""
@@ -1039,4 +1405,94 @@ class VACRoutesFastAPI:
1039
1405
  "id": data.get("id") if 'data' in locals() else None
1040
1406
  },
1041
1407
  status_code=500
1042
- )
1408
+ )
1409
+
1410
+ # MCP Tool Registration Methods
1411
+
1412
+ def _register_custom_tools(self):
1413
+ """Register any custom tools that were added before MCP server initialization."""
1414
+ if self.vac_mcp_server:
1415
+ for tool_func, name, description in self._custom_mcp_tools:
1416
+ self.vac_mcp_server.add_tool(tool_func, name, description)
1417
+ for resource_func, name, description in self._custom_mcp_resources:
1418
+ self.vac_mcp_server.add_resource(resource_func, name, description)
1419
+
1420
+ def add_mcp_tool(self, func: Callable, name: str = None, description: str = None):
1421
+ """
1422
+ Add a custom MCP tool to the server.
1423
+
1424
+ Args:
1425
+ func: The tool function
1426
+ name: Optional custom name for the tool
1427
+ description: Optional description (uses docstring if not provided)
1428
+
1429
+ Example:
1430
+ @app.add_mcp_tool
1431
+ async def my_custom_tool(param: str) -> str:
1432
+ '''Custom tool that does something useful.'''
1433
+ return f"Result: {param}"
1434
+
1435
+ # Or with custom name and description
1436
+ app.add_mcp_tool(my_function, "custom_name", "Custom description")
1437
+ """
1438
+ if self.vac_mcp_server:
1439
+ self.vac_mcp_server.add_tool(func, name, description)
1440
+ else:
1441
+ # Store for later registration
1442
+ self._custom_mcp_tools.append((func, name, description))
1443
+
1444
+ return func # Allow use as decorator
1445
+
1446
+ def add_mcp_resource(self, func: Callable, name: str = None, description: str = None):
1447
+ """
1448
+ Add a custom MCP resource to the server.
1449
+
1450
+ Args:
1451
+ func: The resource function
1452
+ name: Optional custom name for the resource
1453
+ description: Optional description (uses docstring if not provided)
1454
+
1455
+ Example:
1456
+ @app.add_mcp_resource
1457
+ async def my_custom_resource(uri: str) -> str:
1458
+ '''Custom resource that provides data.'''
1459
+ return f"Resource data for: {uri}"
1460
+ """
1461
+ if self.vac_mcp_server:
1462
+ self.vac_mcp_server.add_resource(func, name, description)
1463
+ else:
1464
+ # Store for later registration
1465
+ self._custom_mcp_resources.append((func, name, description))
1466
+
1467
+ return func # Allow use as decorator
1468
+
1469
+ def get_mcp_server(self):
1470
+ """
1471
+ Get the MCP server instance for advanced customization.
1472
+
1473
+ Returns:
1474
+ VACMCPServer instance or None if MCP server is not enabled
1475
+ """
1476
+ return self.vac_mcp_server
1477
+
1478
+ def list_mcp_tools(self) -> List[str]:
1479
+ """
1480
+ List all registered MCP tools.
1481
+
1482
+ Returns:
1483
+ List of tool names
1484
+ """
1485
+ if self.vac_mcp_server:
1486
+ return self.vac_mcp_server.list_tools()
1487
+ return []
1488
+
1489
+ def list_mcp_resources(self) -> List[str]:
1490
+ """
1491
+ List all registered MCP resources.
1492
+
1493
+ Returns:
1494
+ List of resource names
1495
+ """
1496
+ if self.vac_mcp_server:
1497
+ return self.vac_mcp_server.list_resources()
1498
+ return []