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.
- sunholo/agents/fastapi/vac_routes.py +590 -134
- sunholo/mcp/extensible_mcp_server.py +271 -0
- sunholo/mcp/vac_mcp_server_fastmcp.py +74 -137
- sunholo/mcp/vac_tools.py +250 -0
- {sunholo-0.144.1.dist-info → sunholo-0.144.3.dist-info}/METADATA +1 -1
- {sunholo-0.144.1.dist-info → sunholo-0.144.3.dist-info}/RECORD +10 -8
- {sunholo-0.144.1.dist-info → sunholo-0.144.3.dist-info}/WHEEL +0 -0
- {sunholo-0.144.1.dist-info → sunholo-0.144.3.dist-info}/entry_points.txt +0 -0
- {sunholo-0.144.1.dist-info → sunholo-0.144.3.dist-info}/licenses/LICENSE.txt +0 -0
- {sunholo-0.144.1.dist-info → sunholo-0.144.3.dist-info}/top_level.txt +0 -0
@@ -58,7 +58,7 @@ except ImportError:
|
|
58
58
|
MCPClientManager = None
|
59
59
|
|
60
60
|
try:
|
61
|
-
from ...mcp.
|
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
|
87
|
-
|
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
|
-
|
155
|
+
async def my_stream_interpreter(question, vector_name, chat_history, callback, **kwargs):
|
156
|
+
return {"answer": "Response", "sources": []}
|
95
157
|
|
96
|
-
|
97
|
-
|
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
|
-
|
101
|
-
|
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:
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
158
|
-
|
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
|
-
|
238
|
-
|
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 []
|