sunholo 0.144.1__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.
@@ -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
- from fastmcp import FastMCP
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 ..streaming import start_streaming_chat_async
31
+ from .extensible_mcp_server import ExtensibleMCPServer, MCPToolRegistry
28
32
 
29
33
 
30
34
  class VACMCPServer:
31
- """FastMCP Server that exposes VAC functionality as tools."""
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__(self, stream_interpreter: Callable, vac_interpreter: Callable = None):
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
- stream_interpreter: The streaming interpreter function
39
- vac_interpreter: The static VAC interpreter function (optional)
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
- self.stream_interpreter = stream_interpreter
42
- self.vac_interpreter = vac_interpreter
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
- # Initialize FastMCP server
45
- self.server = FastMCP("sunholo-vac-server")
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
- # Register tools
48
- self._register_tools()
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.server.run(transport=transport, **kwargs)
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.server.run_async(transport=transport, **kwargs)
130
+ await self.extensible_server.run_async(transport=transport, **kwargs)