sunholo 0.144.0__py3-none-any.whl → 0.144.2__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 +403 -138
- sunholo/mcp/extensible_mcp_server.py +271 -0
- sunholo/mcp/vac_mcp_server_fastmcp.py +74 -137
- sunholo/mcp/vac_tools.py +249 -0
- {sunholo-0.144.0.dist-info → sunholo-0.144.2.dist-info}/METADATA +1 -1
- {sunholo-0.144.0.dist-info → sunholo-0.144.2.dist-info}/RECORD +10 -8
- {sunholo-0.144.0.dist-info → sunholo-0.144.2.dist-info}/WHEEL +0 -0
- {sunholo-0.144.0.dist-info → sunholo-0.144.2.dist-info}/entry_points.txt +0 -0
- {sunholo-0.144.0.dist-info → sunholo-0.144.2.dist-info}/licenses/LICENSE.txt +0 -0
- {sunholo-0.144.0.dist-info → sunholo-0.144.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,271 @@
|
|
1
|
+
# Copyright [2024] [Holosun ApS]
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""
|
16
|
+
Extensible MCP Server for Sunholo applications.
|
17
|
+
Allows easy integration with Claude Desktop/Code and custom tool registration.
|
18
|
+
"""
|
19
|
+
|
20
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
21
|
+
import asyncio
|
22
|
+
import inspect
|
23
|
+
from functools import wraps
|
24
|
+
|
25
|
+
try:
|
26
|
+
from fastmcp import FastMCP
|
27
|
+
FASTMCP_AVAILABLE = True
|
28
|
+
except ImportError:
|
29
|
+
FastMCP = None
|
30
|
+
FASTMCP_AVAILABLE = False
|
31
|
+
|
32
|
+
from ..custom_logging import log
|
33
|
+
|
34
|
+
|
35
|
+
class MCPToolRegistry:
|
36
|
+
"""Registry for MCP tools that can be shared across server instances."""
|
37
|
+
|
38
|
+
def __init__(self):
|
39
|
+
self.tools = {}
|
40
|
+
self.resources = {}
|
41
|
+
|
42
|
+
def register_tool(self, name: str, func: Callable, description: str = None):
|
43
|
+
"""Register a tool function."""
|
44
|
+
self.tools[name] = {
|
45
|
+
'func': func,
|
46
|
+
'description': description or func.__doc__,
|
47
|
+
'signature': inspect.signature(func)
|
48
|
+
}
|
49
|
+
|
50
|
+
def register_resource(self, name: str, func: Callable, description: str = None):
|
51
|
+
"""Register a resource function."""
|
52
|
+
self.resources[name] = {
|
53
|
+
'func': func,
|
54
|
+
'description': description or func.__doc__,
|
55
|
+
'signature': inspect.signature(func)
|
56
|
+
}
|
57
|
+
|
58
|
+
def get_tool(self, name: str):
|
59
|
+
"""Get a registered tool."""
|
60
|
+
return self.tools.get(name)
|
61
|
+
|
62
|
+
def get_resource(self, name: str):
|
63
|
+
"""Get a registered resource."""
|
64
|
+
return self.resources.get(name)
|
65
|
+
|
66
|
+
def list_tools(self):
|
67
|
+
"""List all registered tools."""
|
68
|
+
return list(self.tools.keys())
|
69
|
+
|
70
|
+
def list_resources(self):
|
71
|
+
"""List all registered resources."""
|
72
|
+
return list(self.resources.keys())
|
73
|
+
|
74
|
+
|
75
|
+
# Global registry instance
|
76
|
+
_global_registry = MCPToolRegistry()
|
77
|
+
|
78
|
+
|
79
|
+
def mcp_tool(name: str = None, description: str = None):
|
80
|
+
"""
|
81
|
+
Decorator to register a function as an MCP tool.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
name: Optional custom name for the tool
|
85
|
+
description: Optional description (uses docstring if not provided)
|
86
|
+
|
87
|
+
Example:
|
88
|
+
@mcp_tool("my_custom_tool", "Does something useful")
|
89
|
+
async def my_tool(param1: str, param2: int = 5) -> str:
|
90
|
+
return f"Result: {param1} * {param2}"
|
91
|
+
"""
|
92
|
+
def decorator(func):
|
93
|
+
tool_name = name or func.__name__
|
94
|
+
_global_registry.register_tool(tool_name, func, description)
|
95
|
+
return func
|
96
|
+
return decorator
|
97
|
+
|
98
|
+
|
99
|
+
def mcp_resource(name: str = None, description: str = None):
|
100
|
+
"""
|
101
|
+
Decorator to register a function as an MCP resource.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
name: Optional custom name for the resource
|
105
|
+
description: Optional description (uses docstring if not provided)
|
106
|
+
"""
|
107
|
+
def decorator(func):
|
108
|
+
resource_name = name or func.__name__
|
109
|
+
_global_registry.register_resource(resource_name, func, description)
|
110
|
+
return func
|
111
|
+
return decorator
|
112
|
+
|
113
|
+
|
114
|
+
class ExtensibleMCPServer:
|
115
|
+
"""
|
116
|
+
Extensible MCP Server that supports custom tool registration.
|
117
|
+
Can be used both as a standalone server and integrated into FastAPI apps.
|
118
|
+
"""
|
119
|
+
|
120
|
+
def __init__(
|
121
|
+
self,
|
122
|
+
server_name: str = "extensible-mcp-server",
|
123
|
+
registry: MCPToolRegistry = None,
|
124
|
+
include_vac_tools: bool = True
|
125
|
+
):
|
126
|
+
"""
|
127
|
+
Initialize the extensible MCP server.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
server_name: Name for the MCP server
|
131
|
+
registry: Custom tool registry (uses global if None)
|
132
|
+
include_vac_tools: Whether to include built-in VAC tools
|
133
|
+
"""
|
134
|
+
if not FASTMCP_AVAILABLE:
|
135
|
+
raise ImportError(
|
136
|
+
"fastmcp is required for MCP server functionality. "
|
137
|
+
"Install it with: pip install fastmcp>=2.12.0"
|
138
|
+
)
|
139
|
+
|
140
|
+
self.server_name = server_name
|
141
|
+
self.registry = registry or _global_registry
|
142
|
+
self.include_vac_tools = include_vac_tools
|
143
|
+
|
144
|
+
# Initialize FastMCP server
|
145
|
+
self.server = FastMCP(server_name)
|
146
|
+
|
147
|
+
# Register tools and resources
|
148
|
+
self._register_tools()
|
149
|
+
self._register_resources()
|
150
|
+
|
151
|
+
# Register built-in VAC tools if requested
|
152
|
+
if include_vac_tools:
|
153
|
+
self._register_vac_tools()
|
154
|
+
|
155
|
+
def _register_tools(self):
|
156
|
+
"""Register all tools from the registry with FastMCP."""
|
157
|
+
for tool_name, tool_info in self.registry.tools.items():
|
158
|
+
func = tool_info['func']
|
159
|
+
|
160
|
+
# Register with FastMCP using the @self.server.tool decorator
|
161
|
+
self.server.tool(func)
|
162
|
+
|
163
|
+
log.debug(f"Registered MCP tool: {tool_name}")
|
164
|
+
|
165
|
+
def _register_resources(self):
|
166
|
+
"""Register all resources from the registry with FastMCP."""
|
167
|
+
for resource_name, resource_info in self.registry.resources.items():
|
168
|
+
func = resource_info['func']
|
169
|
+
|
170
|
+
# Register with FastMCP (resources are handled differently)
|
171
|
+
# For now, we'll treat resources as tools since FastMCP doesn't have separate resource registration
|
172
|
+
self.server.tool(func)
|
173
|
+
|
174
|
+
log.debug(f"Registered MCP resource: {resource_name}")
|
175
|
+
|
176
|
+
def _register_vac_tools(self):
|
177
|
+
"""Register built-in VAC tools."""
|
178
|
+
from .vac_tools import register_vac_tools
|
179
|
+
register_vac_tools(self.server, self.registry)
|
180
|
+
|
181
|
+
def add_tool(self, func: Callable, name: str = None, description: str = None):
|
182
|
+
"""
|
183
|
+
Add a tool function directly to the server.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
func: The tool function
|
187
|
+
name: Optional custom name
|
188
|
+
description: Optional description
|
189
|
+
"""
|
190
|
+
tool_name = name or func.__name__
|
191
|
+
self.registry.register_tool(tool_name, func, description)
|
192
|
+
self.server.tool(func)
|
193
|
+
log.info(f"Added MCP tool: {tool_name}")
|
194
|
+
|
195
|
+
def add_resource(self, func: Callable, name: str = None, description: str = None):
|
196
|
+
"""
|
197
|
+
Add a resource function directly to the server.
|
198
|
+
|
199
|
+
Args:
|
200
|
+
func: The resource function
|
201
|
+
name: Optional custom name
|
202
|
+
description: Optional description
|
203
|
+
"""
|
204
|
+
resource_name = name or func.__name__
|
205
|
+
self.registry.register_resource(resource_name, func, description)
|
206
|
+
self.server.tool(func) # FastMCP treats resources as tools
|
207
|
+
log.info(f"Added MCP resource: {resource_name}")
|
208
|
+
|
209
|
+
def get_server(self) -> FastMCP:
|
210
|
+
"""Get the underlying FastMCP server instance."""
|
211
|
+
return self.server
|
212
|
+
|
213
|
+
def get_http_app(self):
|
214
|
+
"""Get the HTTP app for mounting in FastAPI."""
|
215
|
+
return self.server.get_app()
|
216
|
+
|
217
|
+
def run(self, transport: str = "stdio", **kwargs):
|
218
|
+
"""
|
219
|
+
Run the MCP server.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
transport: Transport type ("stdio" or "http")
|
223
|
+
**kwargs: Additional arguments for the transport
|
224
|
+
"""
|
225
|
+
self.server.run(transport=transport, **kwargs)
|
226
|
+
|
227
|
+
async def run_async(self, transport: str = "stdio", **kwargs):
|
228
|
+
"""
|
229
|
+
Run the MCP server asynchronously.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
transport: Transport type ("stdio" or "http")
|
233
|
+
**kwargs: Additional arguments for the transport
|
234
|
+
"""
|
235
|
+
await self.server.run_async(transport=transport, **kwargs)
|
236
|
+
|
237
|
+
def list_registered_tools(self) -> List[str]:
|
238
|
+
"""List all registered tools."""
|
239
|
+
return self.registry.list_tools()
|
240
|
+
|
241
|
+
def list_registered_resources(self) -> List[str]:
|
242
|
+
"""List all registered resources."""
|
243
|
+
return self.registry.list_resources()
|
244
|
+
|
245
|
+
|
246
|
+
def get_global_registry() -> MCPToolRegistry:
|
247
|
+
"""Get the global MCP tool registry."""
|
248
|
+
return _global_registry
|
249
|
+
|
250
|
+
|
251
|
+
def create_mcp_server(
|
252
|
+
server_name: str = "sunholo-mcp-server",
|
253
|
+
include_vac_tools: bool = True,
|
254
|
+
custom_registry: MCPToolRegistry = None
|
255
|
+
) -> ExtensibleMCPServer:
|
256
|
+
"""
|
257
|
+
Create a new extensible MCP server instance.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
server_name: Name for the MCP server
|
261
|
+
include_vac_tools: Whether to include built-in VAC tools
|
262
|
+
custom_registry: Optional custom tool registry
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
Configured ExtensibleMCPServer instance
|
266
|
+
"""
|
267
|
+
return ExtensibleMCPServer(
|
268
|
+
server_name=server_name,
|
269
|
+
registry=custom_registry,
|
270
|
+
include_vac_tools=include_vac_tools
|
271
|
+
)
|
@@ -15,163 +15,100 @@
|
|
15
15
|
"""
|
16
16
|
FastMCP-based MCP Server wrapper for VAC functionality.
|
17
17
|
This module exposes VAC streaming capabilities as MCP tools using FastMCP.
|
18
|
+
Now uses the extensible MCP server system for better customization.
|
18
19
|
"""
|
19
20
|
|
20
21
|
from typing import Any, Callable, Dict, List, Optional
|
21
|
-
import asyncio
|
22
|
-
from functools import partial
|
23
22
|
|
24
|
-
|
23
|
+
try:
|
24
|
+
from fastmcp import FastMCP
|
25
|
+
FASTMCP_AVAILABLE = True
|
26
|
+
except ImportError:
|
27
|
+
FastMCP = None
|
28
|
+
FASTMCP_AVAILABLE = False
|
25
29
|
|
26
30
|
from ..custom_logging import log
|
27
|
-
from
|
31
|
+
from .extensible_mcp_server import ExtensibleMCPServer, MCPToolRegistry
|
28
32
|
|
29
33
|
|
30
34
|
class VACMCPServer:
|
31
|
-
"""
|
35
|
+
"""
|
36
|
+
FastMCP Server that exposes VAC functionality as tools.
|
37
|
+
Now built on top of ExtensibleMCPServer for better customization.
|
38
|
+
"""
|
32
39
|
|
33
|
-
def __init__(
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
server_name: str = "sunholo-vac-server",
|
43
|
+
include_vac_tools: bool = True,
|
44
|
+
custom_registry: MCPToolRegistry = None
|
45
|
+
):
|
34
46
|
"""
|
35
47
|
Initialize the VAC MCP Server using FastMCP.
|
36
48
|
|
37
49
|
Args:
|
38
|
-
|
39
|
-
|
50
|
+
server_name: Name for the MCP server
|
51
|
+
include_vac_tools: Whether to include built-in VAC tools
|
52
|
+
custom_registry: Optional custom tool registry
|
40
53
|
"""
|
41
|
-
|
42
|
-
|
54
|
+
if not FASTMCP_AVAILABLE:
|
55
|
+
raise ImportError(
|
56
|
+
"fastmcp is required for MCP server functionality. "
|
57
|
+
"Install it with: pip install fastmcp>=2.12.0"
|
58
|
+
)
|
43
59
|
|
44
|
-
#
|
45
|
-
self.
|
60
|
+
# Use the extensible MCP server
|
61
|
+
self.extensible_server = ExtensibleMCPServer(
|
62
|
+
server_name=server_name,
|
63
|
+
registry=custom_registry,
|
64
|
+
include_vac_tools=include_vac_tools
|
65
|
+
)
|
46
66
|
|
47
|
-
#
|
48
|
-
self.
|
49
|
-
|
50
|
-
def _register_tools(self):
|
51
|
-
"""Register VAC tools with FastMCP."""
|
52
|
-
|
53
|
-
@self.server.tool
|
54
|
-
async def vac_stream(
|
55
|
-
vector_name: str,
|
56
|
-
user_input: str,
|
57
|
-
chat_history: List[Dict[str, str]] = None,
|
58
|
-
stream_wait_time: float = 7,
|
59
|
-
stream_timeout: float = 120
|
60
|
-
) -> str:
|
61
|
-
"""
|
62
|
-
Stream responses from a Sunholo VAC (Virtual Agent Computer).
|
63
|
-
|
64
|
-
Args:
|
65
|
-
vector_name: Name of the VAC to interact with
|
66
|
-
user_input: The user's question or input
|
67
|
-
chat_history: Previous conversation history
|
68
|
-
stream_wait_time: Time to wait between stream chunks
|
69
|
-
stream_timeout: Maximum time to wait for response
|
70
|
-
|
71
|
-
Returns:
|
72
|
-
The streamed response from the VAC
|
73
|
-
"""
|
74
|
-
if chat_history is None:
|
75
|
-
chat_history = []
|
76
|
-
|
77
|
-
log.info(f"MCP streaming request for VAC '{vector_name}': {user_input}")
|
78
|
-
|
79
|
-
try:
|
80
|
-
# Collect streaming responses
|
81
|
-
full_response = ""
|
82
|
-
|
83
|
-
# Check if stream_interpreter is async
|
84
|
-
if asyncio.iscoroutinefunction(self.stream_interpreter):
|
85
|
-
async for chunk in start_streaming_chat_async(
|
86
|
-
question=user_input,
|
87
|
-
vector_name=vector_name,
|
88
|
-
qna_func_async=self.stream_interpreter,
|
89
|
-
chat_history=chat_history,
|
90
|
-
wait_time=stream_wait_time,
|
91
|
-
timeout=stream_timeout
|
92
|
-
):
|
93
|
-
if isinstance(chunk, dict) and 'answer' in chunk:
|
94
|
-
full_response = chunk['answer']
|
95
|
-
elif isinstance(chunk, str):
|
96
|
-
full_response += chunk
|
97
|
-
else:
|
98
|
-
# Fall back to sync version for non-async interpreters
|
99
|
-
result = self.stream_interpreter(
|
100
|
-
question=user_input,
|
101
|
-
vector_name=vector_name,
|
102
|
-
chat_history=chat_history
|
103
|
-
)
|
104
|
-
if isinstance(result, dict):
|
105
|
-
full_response = result.get("answer", str(result))
|
106
|
-
else:
|
107
|
-
full_response = str(result)
|
108
|
-
|
109
|
-
return full_response or "No response generated"
|
110
|
-
|
111
|
-
except Exception as e:
|
112
|
-
log.error(f"Error in MCP VAC stream: {str(e)}")
|
113
|
-
return f"Error: {str(e)}"
|
114
|
-
|
115
|
-
# Register non-streaming tool if interpreter is provided
|
116
|
-
if self.vac_interpreter:
|
117
|
-
@self.server.tool
|
118
|
-
async def vac_query(
|
119
|
-
vector_name: str,
|
120
|
-
user_input: str,
|
121
|
-
chat_history: List[Dict[str, str]] = None
|
122
|
-
) -> str:
|
123
|
-
"""
|
124
|
-
Query a Sunholo VAC (non-streaming).
|
125
|
-
|
126
|
-
Args:
|
127
|
-
vector_name: Name of the VAC to interact with
|
128
|
-
user_input: The user's question or input
|
129
|
-
chat_history: Previous conversation history
|
130
|
-
|
131
|
-
Returns:
|
132
|
-
The response from the VAC
|
133
|
-
"""
|
134
|
-
if chat_history is None:
|
135
|
-
chat_history = []
|
136
|
-
|
137
|
-
log.info(f"MCP query request for VAC '{vector_name}': {user_input}")
|
138
|
-
|
139
|
-
try:
|
140
|
-
# Run in executor if not async
|
141
|
-
if asyncio.iscoroutinefunction(self.vac_interpreter):
|
142
|
-
result = await self.vac_interpreter(
|
143
|
-
question=user_input,
|
144
|
-
vector_name=vector_name,
|
145
|
-
chat_history=chat_history
|
146
|
-
)
|
147
|
-
else:
|
148
|
-
loop = asyncio.get_event_loop()
|
149
|
-
result = await loop.run_in_executor(
|
150
|
-
None,
|
151
|
-
partial(
|
152
|
-
self.vac_interpreter,
|
153
|
-
question=user_input,
|
154
|
-
vector_name=vector_name,
|
155
|
-
chat_history=chat_history
|
156
|
-
)
|
157
|
-
)
|
158
|
-
|
159
|
-
# Extract answer from result
|
160
|
-
if isinstance(result, dict):
|
161
|
-
answer = result.get("answer", str(result))
|
162
|
-
else:
|
163
|
-
answer = str(result)
|
164
|
-
|
165
|
-
return answer
|
166
|
-
|
167
|
-
except Exception as e:
|
168
|
-
log.error(f"Error in MCP VAC query: {str(e)}")
|
169
|
-
return f"Error: {str(e)}"
|
67
|
+
# Expose server for compatibility
|
68
|
+
self.server = self.extensible_server.server
|
170
69
|
|
171
70
|
def get_server(self) -> FastMCP:
|
172
71
|
"""Get the underlying FastMCP server instance."""
|
173
72
|
return self.server
|
174
73
|
|
74
|
+
def get_http_app(self):
|
75
|
+
"""Get the HTTP app for mounting in FastAPI."""
|
76
|
+
return self.server.get_app()
|
77
|
+
|
78
|
+
def add_tool(self, func: Callable, name: str = None, description: str = None):
|
79
|
+
"""
|
80
|
+
Add a custom tool function to the MCP server.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
func: The tool function
|
84
|
+
name: Optional custom name
|
85
|
+
description: Optional description
|
86
|
+
"""
|
87
|
+
self.extensible_server.add_tool(func, name, description)
|
88
|
+
|
89
|
+
def add_resource(self, func: Callable, name: str = None, description: str = None):
|
90
|
+
"""
|
91
|
+
Add a custom resource function to the MCP server.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
func: The resource function
|
95
|
+
name: Optional custom name
|
96
|
+
description: Optional description
|
97
|
+
"""
|
98
|
+
self.extensible_server.add_resource(func, name, description)
|
99
|
+
|
100
|
+
def get_registry(self) -> MCPToolRegistry:
|
101
|
+
"""Get the tool registry for advanced customization."""
|
102
|
+
return self.extensible_server.registry
|
103
|
+
|
104
|
+
def list_tools(self) -> List[str]:
|
105
|
+
"""List all registered tools."""
|
106
|
+
return self.extensible_server.list_registered_tools()
|
107
|
+
|
108
|
+
def list_resources(self) -> List[str]:
|
109
|
+
"""List all registered resources."""
|
110
|
+
return self.extensible_server.list_registered_resources()
|
111
|
+
|
175
112
|
def run(self, transport: str = "stdio", **kwargs):
|
176
113
|
"""
|
177
114
|
Run the MCP server.
|
@@ -180,7 +117,7 @@ class VACMCPServer:
|
|
180
117
|
transport: Transport type ("stdio" or "http")
|
181
118
|
**kwargs: Additional arguments for the transport
|
182
119
|
"""
|
183
|
-
self.
|
120
|
+
self.extensible_server.run(transport=transport, **kwargs)
|
184
121
|
|
185
122
|
async def run_async(self, transport: str = "stdio", **kwargs):
|
186
123
|
"""
|
@@ -190,4 +127,4 @@ class VACMCPServer:
|
|
190
127
|
transport: Transport type ("stdio" or "http")
|
191
128
|
**kwargs: Additional arguments for the transport
|
192
129
|
"""
|
193
|
-
await self.
|
130
|
+
await self.extensible_server.run_async(transport=transport, **kwargs)
|