sunholo 0.142.0__py3-none-any.whl → 0.143.1__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.
@@ -6,6 +6,8 @@ import random
6
6
  from functools import partial
7
7
  import inspect
8
8
  import asyncio
9
+ from typing import Dict, List, Optional, Callable, Any
10
+
9
11
 
10
12
  from ..chat_history import extract_chat_history_with_cache, extract_chat_history_async_cached
11
13
  from ...qna.parsers import parse_output
@@ -29,6 +31,21 @@ try:
29
31
  except ImportError:
30
32
  PubSubManager = None
31
33
 
34
+ try:
35
+ from ...mcp.mcp_manager import MCPClientManager
36
+ except ImportError:
37
+ MCPClientManager = None
38
+
39
+ try:
40
+ from ...mcp.vac_mcp_server import VACMCPServer
41
+ from mcp.server.models import InitializationOptions
42
+ from mcp import JSONRPCMessage, ErrorData, INTERNAL_ERROR
43
+ except ImportError:
44
+ VACMCPServer = None
45
+ InitializationOptions = None
46
+ JSONRPCMessage = None
47
+
48
+
32
49
  # Cache dictionary to store validated API keys
33
50
  api_key_cache = {}
34
51
  cache_duration = timedelta(minutes=5) # Cache duration
@@ -61,11 +78,30 @@ if __name__ == "__main__":
61
78
  stream_interpreter: callable,
62
79
  vac_interpreter:callable=None,
63
80
  additional_routes:dict=None,
81
+ mcp_servers: List[Dict[str, Any]] = None,
64
82
  async_stream:bool=False,
65
- add_langfuse_eval:bool=True):
83
+ add_langfuse_eval:bool=True,
84
+ enable_mcp_server:bool=False):
66
85
  self.app = app
67
86
  self.stream_interpreter = stream_interpreter
68
87
  self.vac_interpreter = vac_interpreter or partial(self.vac_interpreter_default)
88
+
89
+ # MCP client initialization
90
+ self.mcp_servers = mcp_servers or []
91
+ self.mcp_client_manager = MCPClientManager()
92
+ # Initialize MCP connections
93
+ if self.mcp_servers and self.mcp_client_manager:
94
+ asyncio.create_task(self._initialize_mcp_servers())
95
+
96
+ # MCP server initialization
97
+ self.enable_mcp_server = enable_mcp_server
98
+ self.vac_mcp_server = None
99
+ if self.enable_mcp_server and VACMCPServer:
100
+ self.vac_mcp_server = VACMCPServer(
101
+ stream_interpreter=self.stream_interpreter,
102
+ vac_interpreter=self.vac_interpreter
103
+ )
104
+
69
105
  self.additional_routes = additional_routes if additional_routes is not None else []
70
106
  self.async_stream = async_stream
71
107
  self.add_langfuse_eval = add_langfuse_eval
@@ -129,6 +165,18 @@ if __name__ == "__main__":
129
165
  # OpenAI compatible endpoint
130
166
  self.app.route('/openai/v1/chat/completions', methods=['POST'])(self.handle_openai_compatible_endpoint)
131
167
  self.app.route('/openai/v1/chat/completions/<vector_name>', methods=['POST'])(self.handle_openai_compatible_endpoint)
168
+
169
+ # MCP client routes
170
+ if self.mcp_servers:
171
+ self.app.route('/mcp/tools', methods=['GET'])(self.handle_mcp_list_tools)
172
+ self.app.route('/mcp/tools/<server_name>', methods=['GET'])(self.handle_mcp_list_tools)
173
+ self.app.route('/mcp/call', methods=['POST'])(self.handle_mcp_call_tool)
174
+ self.app.route('/mcp/resources', methods=['GET'])(self.handle_mcp_list_resources)
175
+ self.app.route('/mcp/resources/read', methods=['POST'])(self.handle_mcp_read_resource)
176
+
177
+ # MCP server endpoint
178
+ if self.enable_mcp_server and self.vac_mcp_server:
179
+ self.app.route('/mcp', methods=['POST', 'GET'])(self.handle_mcp_server)
132
180
 
133
181
  self.register_additional_routes()
134
182
 
@@ -803,3 +851,254 @@ if __name__ == "__main__":
803
851
  except Exception as e:
804
852
  raise Exception(f'File upload failed: {str(e)}')
805
853
 
854
+ async def _initialize_mcp_servers(self):
855
+ """Initialize connections to configured MCP servers."""
856
+ for server_config in self.mcp_servers:
857
+ try:
858
+ await self.mcp_client_manager.connect_to_server(
859
+ server_name=server_config["name"],
860
+ command=server_config["command"],
861
+ args=server_config.get("args", [])
862
+ )
863
+ log.info(f"Connected to MCP server: {server_config['name']}")
864
+ except Exception as e:
865
+ log.error(f"Failed to connect to MCP server {server_config['name']}: {e}")
866
+
867
+
868
+ def handle_mcp_list_tools(self, server_name: Optional[str] = None):
869
+ """List available MCP tools."""
870
+ async def get_tools():
871
+ tools = await self.mcp_client_manager.list_tools(server_name)
872
+ return [
873
+ {
874
+ "name": tool.name,
875
+ "description": tool.description,
876
+ "inputSchema": tool.inputSchema,
877
+ "server": tool.metadata.get("server") if tool.metadata else server_name
878
+ }
879
+ for tool in tools
880
+ ]
881
+
882
+ # Run async in sync context
883
+ loop = asyncio.new_event_loop()
884
+ asyncio.set_event_loop(loop)
885
+ try:
886
+ tools = loop.run_until_complete(get_tools())
887
+ return jsonify({"tools": tools})
888
+ finally:
889
+ loop.close()
890
+
891
+ def handle_mcp_call_tool(self):
892
+ """Call an MCP tool."""
893
+ data = request.get_json()
894
+ server_name = data.get("server")
895
+ tool_name = data.get("tool")
896
+ arguments = data.get("arguments", {})
897
+
898
+ if not server_name or not tool_name:
899
+ return jsonify({"error": "Missing 'server' or 'tool' parameter"}), 400
900
+
901
+ async def call_tool():
902
+ try:
903
+ result = await self.mcp_client_manager.call_tool(server_name, tool_name, arguments)
904
+
905
+ # Convert result to JSON-serializable format
906
+ if hasattr(result, 'content'):
907
+ # Handle different content types
908
+ if hasattr(result.content, 'text'):
909
+ return {"result": result.content.text}
910
+ elif hasattr(result.content, 'data'):
911
+ return {"result": result.content.data}
912
+ else:
913
+ return {"result": str(result.content)}
914
+ else:
915
+ return {"result": str(result)}
916
+
917
+ except Exception as e:
918
+ return {"error": str(e)}
919
+
920
+ loop = asyncio.new_event_loop()
921
+ asyncio.set_event_loop(loop)
922
+ try:
923
+ result = loop.run_until_complete(call_tool())
924
+ if "error" in result:
925
+ return jsonify(result), 500
926
+ return jsonify(result)
927
+ finally:
928
+ loop.close()
929
+
930
+ def handle_mcp_list_resources(self):
931
+ """List available MCP resources."""
932
+ server_name = request.args.get("server")
933
+
934
+ async def get_resources():
935
+ resources = await self.mcp_client_manager.list_resources(server_name)
936
+ return [
937
+ {
938
+ "uri": resource.uri,
939
+ "name": resource.name,
940
+ "description": resource.description,
941
+ "mimeType": resource.mimeType,
942
+ "server": resource.metadata.get("server") if resource.metadata else server_name
943
+ }
944
+ for resource in resources
945
+ ]
946
+
947
+ loop = asyncio.new_event_loop()
948
+ asyncio.set_event_loop(loop)
949
+ try:
950
+ resources = loop.run_until_complete(get_resources())
951
+ return jsonify({"resources": resources})
952
+ finally:
953
+ loop.close()
954
+
955
+ def handle_mcp_read_resource(self):
956
+ """Read an MCP resource."""
957
+ data = request.get_json()
958
+ server_name = data.get("server")
959
+ uri = data.get("uri")
960
+
961
+ if not server_name or not uri:
962
+ return jsonify({"error": "Missing 'server' or 'uri' parameter"}), 400
963
+
964
+ async def read_resource():
965
+ try:
966
+ contents = await self.mcp_client_manager.read_resource(server_name, uri)
967
+ return {
968
+ "contents": [
969
+ {"text": content.text} if hasattr(content, 'text') else {"data": str(content)}
970
+ for content in contents
971
+ ]
972
+ }
973
+ except Exception as e:
974
+ return {"error": str(e)}
975
+
976
+ loop = asyncio.new_event_loop()
977
+ asyncio.set_event_loop(loop)
978
+ try:
979
+ result = loop.run_until_complete(read_resource())
980
+ if "error" in result:
981
+ return jsonify(result), 500
982
+ return jsonify(result)
983
+ finally:
984
+ loop.close()
985
+
986
+ def handle_mcp_server(self):
987
+ """Handle MCP server requests using HTTP transport."""
988
+ if not self.vac_mcp_server:
989
+ return jsonify({"error": "MCP server not enabled"}), 501
990
+
991
+ import json as json_module
992
+
993
+ # Handle streaming for HTTP transport
994
+ if request.method == 'POST':
995
+ try:
996
+ # Get the JSON-RPC request
997
+ data = request.get_json()
998
+ log.info(f"MCP server received: {data}")
999
+
1000
+ # Create an async handler for the request
1001
+ async def process_request():
1002
+ # Create mock read/write streams for the server
1003
+ from io import StringIO
1004
+ import asyncio
1005
+
1006
+ # Convert request to proper format
1007
+ request_str = json_module.dumps(data) + '\n'
1008
+
1009
+ # Create read queue with the request
1010
+ read_queue = asyncio.Queue()
1011
+ await read_queue.put(request_str.encode())
1012
+ await read_queue.put(None) # EOF signal
1013
+
1014
+ # Create write queue for response
1015
+ write_queue = asyncio.Queue()
1016
+
1017
+ # Create async iterators
1018
+ async def read_messages():
1019
+ while True:
1020
+ msg = await read_queue.get()
1021
+ if msg is None:
1022
+ break
1023
+ yield msg
1024
+
1025
+ responses = []
1026
+ async def write_messages():
1027
+ async for msg in write_queue:
1028
+ if msg is None:
1029
+ break
1030
+ responses.append(msg.decode())
1031
+
1032
+ # Run the server with these streams
1033
+ server = self.vac_mcp_server.get_server()
1034
+
1035
+ # Start write handler
1036
+ write_task = asyncio.create_task(write_messages())
1037
+
1038
+ try:
1039
+ # Process the request through the server
1040
+ await server.run(
1041
+ read_messages(),
1042
+ write_queue,
1043
+ InitializationOptions() if InitializationOptions else None
1044
+ )
1045
+ except Exception as e:
1046
+ log.error(f"Error processing MCP request: {e}")
1047
+ await write_queue.put(None)
1048
+ await write_task
1049
+ raise
1050
+
1051
+ # Signal end and wait for write task
1052
+ await write_queue.put(None)
1053
+ await write_task
1054
+
1055
+ # Return collected responses
1056
+ return responses
1057
+
1058
+ # Run the async handler
1059
+ loop = asyncio.new_event_loop()
1060
+ asyncio.set_event_loop(loop)
1061
+ try:
1062
+ responses = loop.run_until_complete(process_request())
1063
+
1064
+ # Parse and return the response
1065
+ if responses:
1066
+ # The response should be a single JSON-RPC response
1067
+ response_data = json_module.loads(responses[0])
1068
+ return jsonify(response_data)
1069
+ else:
1070
+ return jsonify({"error": "No response from MCP server"}), 500
1071
+
1072
+ except Exception as e:
1073
+ log.error(f"MCP server error: {str(e)}")
1074
+ return jsonify({
1075
+ "jsonrpc": "2.0",
1076
+ "error": {
1077
+ "code": -32603,
1078
+ "message": f"Internal error: {str(e)}"
1079
+ },
1080
+ "id": data.get("id") if isinstance(data, dict) else None
1081
+ }), 500
1082
+ finally:
1083
+ loop.close()
1084
+
1085
+ except Exception as e:
1086
+ log.error(f"MCP server error: {str(e)}")
1087
+ return jsonify({
1088
+ "jsonrpc": "2.0",
1089
+ "error": {
1090
+ "code": -32603,
1091
+ "message": f"Internal error: {str(e)}"
1092
+ },
1093
+ "id": data.get("id") if isinstance(data, dict) else None
1094
+ }), 500
1095
+
1096
+ else:
1097
+ # GET request - return server information
1098
+ return jsonify({
1099
+ "name": "sunholo-vac-server",
1100
+ "version": "1.0.0",
1101
+ "transport": "http",
1102
+ "endpoint": "/mcp",
1103
+ "tools": ["vac_stream", "vac_query"] if self.vac_interpreter else ["vac_stream"]
1104
+ })
sunholo/mcp/__init__.py CHANGED
@@ -0,0 +1,20 @@
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
+ """MCP (Model Context Protocol) integration for Sunholo."""
16
+
17
+ from .mcp_manager import MCPClientManager
18
+ from .vac_mcp_server import VACMCPServer
19
+
20
+ __all__ = ['MCPClientManager', 'VACMCPServer']
@@ -0,0 +1,98 @@
1
+ """
2
+ Proper MCP integration for VACRoutes using the official MCP Python SDK.
3
+ This shows how to integrate MCP servers with your Flask/VACRoutes application.
4
+ """
5
+
6
+ from typing import Dict, Any, List, Optional
7
+
8
+ # Official MCP imports
9
+ from mcp import StdioServerParameters, ClientSession
10
+ from mcp.client.stdio import stdio_client
11
+ from mcp.types import Tool, Resource, TextContent, CallToolResult
12
+
13
+
14
+ class MCPClientManager:
15
+ """Manages MCP client connections to various MCP servers."""
16
+
17
+ def __init__(self):
18
+ self.sessions: Dict[str, ClientSession] = {}
19
+ self.server_configs: Dict[str, Dict[str, Any]] = {}
20
+
21
+ async def connect_to_server(self, server_name: str, command: str, args: List[str] = None) -> ClientSession:
22
+ """Connect to an MCP server via stdio."""
23
+ if server_name in self.sessions:
24
+ return self.sessions[server_name]
25
+
26
+ # Create server parameters
27
+ server_params = StdioServerParameters(
28
+ command=command,
29
+ args=args or []
30
+ )
31
+
32
+ # Connect to the server
33
+ async with stdio_client(server_params) as (read, write):
34
+ # Create and initialize client session directly
35
+ session = ClientSession(read, write)
36
+ await session.initialize()
37
+ self.sessions[server_name] = session
38
+ self.server_configs[server_name] = {
39
+ "command": command,
40
+ "args": args
41
+ }
42
+ return session
43
+
44
+ async def list_tools(self, server_name: Optional[str] = None) -> List[Tool]:
45
+ """List available tools from one or all connected servers."""
46
+ if server_name:
47
+ session = self.sessions.get(server_name)
48
+ if session:
49
+ return await session.list_tools()
50
+ return []
51
+
52
+ # List from all servers
53
+ all_tools = []
54
+ for name, session in self.sessions.items():
55
+ tools = await session.list_tools()
56
+ # Add server name to tool metadata
57
+ for tool in tools:
58
+ tool.metadata = tool.metadata or {}
59
+ tool.metadata["server"] = name
60
+ all_tools.extend(tools)
61
+ return all_tools
62
+
63
+ async def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> CallToolResult:
64
+ """Call a tool on a specific MCP server."""
65
+ session = self.sessions.get(server_name)
66
+ if not session:
67
+ raise ValueError(f"Not connected to server: {server_name}")
68
+
69
+ # Call the tool
70
+ result = await session.call_tool(tool_name, arguments)
71
+ return result
72
+
73
+ async def list_resources(self, server_name: Optional[str] = None) -> List[Resource]:
74
+ """List available resources from servers."""
75
+ if server_name:
76
+ session = self.sessions.get(server_name)
77
+ if session:
78
+ return await session.list_resources()
79
+ return []
80
+
81
+ # List from all servers
82
+ all_resources = []
83
+ for name, session in self.sessions.items():
84
+ resources = await session.list_resources()
85
+ for resource in resources:
86
+ resource.metadata = resource.metadata or {}
87
+ resource.metadata["server"] = name
88
+ all_resources.extend(resources)
89
+ return all_resources
90
+
91
+ async def read_resource(self, server_name: str, uri: str) -> List[TextContent]:
92
+ """Read a resource from an MCP server."""
93
+ session = self.sessions.get(server_name)
94
+ if not session:
95
+ raise ValueError(f"Not connected to server: {server_name}")
96
+
97
+ result = await session.read_resource(uri)
98
+ return result.contents
@@ -0,0 +1,259 @@
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
+ MCP Server wrapper for VAC functionality.
17
+ This module exposes VAC streaming capabilities as MCP tools.
18
+ """
19
+
20
+ from typing import Any, Sequence, Dict, List, Optional, Callable
21
+ import json
22
+ import asyncio
23
+ from functools import partial
24
+
25
+ try:
26
+ from mcp.server import Server
27
+ from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
28
+ except ImportError:
29
+ Server = None
30
+ Tool = None
31
+ TextContent = None
32
+
33
+ from ..custom_logging import log
34
+ from ..streaming import start_streaming_chat_async
35
+
36
+
37
+ class VACMCPServer:
38
+ """MCP Server that exposes VAC functionality as tools."""
39
+
40
+ def __init__(self, stream_interpreter: Callable, vac_interpreter: Callable = None):
41
+ """
42
+ Initialize the VAC MCP Server.
43
+
44
+ Args:
45
+ stream_interpreter: The streaming interpreter function
46
+ vac_interpreter: The static VAC interpreter function (optional)
47
+ """
48
+ if Server is None:
49
+ raise ImportError("MCP server requires `pip install sunholo[anthropic]`")
50
+
51
+ self.stream_interpreter = stream_interpreter
52
+ self.vac_interpreter = vac_interpreter
53
+ self.server = Server("sunholo-vac-server")
54
+
55
+ # Set up handlers
56
+ self._setup_handlers()
57
+
58
+ def _setup_handlers(self):
59
+ """Set up MCP protocol handlers."""
60
+
61
+ @self.server.list_tools()
62
+ async def list_tools() -> List[Tool]:
63
+ """List available VAC tools."""
64
+ tools = [
65
+ Tool(
66
+ name="vac_stream",
67
+ description="Stream responses from a Sunholo VAC (Virtual Agent Computer)",
68
+ inputSchema={
69
+ "type": "object",
70
+ "properties": {
71
+ "vector_name": {
72
+ "type": "string",
73
+ "description": "Name of the VAC to interact with"
74
+ },
75
+ "user_input": {
76
+ "type": "string",
77
+ "description": "The user's question or input"
78
+ },
79
+ "chat_history": {
80
+ "type": "array",
81
+ "description": "Previous conversation history",
82
+ "items": {
83
+ "type": "object",
84
+ "properties": {
85
+ "human": {"type": "string"},
86
+ "ai": {"type": "string"}
87
+ }
88
+ },
89
+ "default": []
90
+ },
91
+ "stream_wait_time": {
92
+ "type": "number",
93
+ "description": "Time to wait between stream chunks",
94
+ "default": 7
95
+ },
96
+ "stream_timeout": {
97
+ "type": "number",
98
+ "description": "Maximum time to wait for response",
99
+ "default": 120
100
+ }
101
+ },
102
+ "required": ["vector_name", "user_input"]
103
+ }
104
+ )
105
+ ]
106
+
107
+ # Add static VAC tool if interpreter is provided
108
+ if self.vac_interpreter:
109
+ tools.append(
110
+ Tool(
111
+ name="vac_query",
112
+ description="Query a Sunholo VAC (non-streaming)",
113
+ inputSchema={
114
+ "type": "object",
115
+ "properties": {
116
+ "vector_name": {
117
+ "type": "string",
118
+ "description": "Name of the VAC to interact with"
119
+ },
120
+ "user_input": {
121
+ "type": "string",
122
+ "description": "The user's question or input"
123
+ },
124
+ "chat_history": {
125
+ "type": "array",
126
+ "description": "Previous conversation history",
127
+ "items": {
128
+ "type": "object",
129
+ "properties": {
130
+ "human": {"type": "string"},
131
+ "ai": {"type": "string"}
132
+ }
133
+ },
134
+ "default": []
135
+ }
136
+ },
137
+ "required": ["vector_name", "user_input"]
138
+ }
139
+ )
140
+ )
141
+
142
+ return tools
143
+
144
+ @self.server.call_tool()
145
+ async def call_tool(
146
+ name: str,
147
+ arguments: Any
148
+ ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
149
+ """Handle tool calls for VAC interactions."""
150
+
151
+ if name == "vac_stream":
152
+ return await self._handle_vac_stream(arguments)
153
+ elif name == "vac_query" and self.vac_interpreter:
154
+ return await self._handle_vac_query(arguments)
155
+ else:
156
+ raise ValueError(f"Unknown tool: {name}")
157
+
158
+ async def _handle_vac_stream(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
159
+ """Handle streaming VAC requests."""
160
+ vector_name = arguments.get("vector_name")
161
+ user_input = arguments.get("user_input")
162
+ chat_history = arguments.get("chat_history", [])
163
+ stream_wait_time = arguments.get("stream_wait_time", 7)
164
+ stream_timeout = arguments.get("stream_timeout", 120)
165
+
166
+ if not vector_name or not user_input:
167
+ raise ValueError("Missing required arguments: vector_name and user_input")
168
+
169
+ log.info(f"MCP streaming request for VAC '{vector_name}': {user_input}")
170
+
171
+ try:
172
+ # Collect streaming responses
173
+ full_response = ""
174
+
175
+ async for chunk in start_streaming_chat_async(
176
+ question=user_input,
177
+ vector_name=vector_name,
178
+ qna_func_async=self.stream_interpreter,
179
+ chat_history=chat_history,
180
+ wait_time=stream_wait_time,
181
+ timeout=stream_timeout
182
+ ):
183
+ if isinstance(chunk, dict) and 'answer' in chunk:
184
+ full_response = chunk['answer']
185
+ elif isinstance(chunk, str):
186
+ full_response += chunk
187
+
188
+ return [
189
+ TextContent(
190
+ type="text",
191
+ text=full_response or "No response generated"
192
+ )
193
+ ]
194
+
195
+ except Exception as e:
196
+ log.error(f"Error in MCP VAC stream: {str(e)}")
197
+ return [
198
+ TextContent(
199
+ type="text",
200
+ text=f"Error: {str(e)}"
201
+ )
202
+ ]
203
+
204
+ async def _handle_vac_query(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
205
+ """Handle non-streaming VAC requests."""
206
+ vector_name = arguments.get("vector_name")
207
+ user_input = arguments.get("user_input")
208
+ chat_history = arguments.get("chat_history", [])
209
+
210
+ if not vector_name or not user_input:
211
+ raise ValueError("Missing required arguments: vector_name and user_input")
212
+
213
+ log.info(f"MCP query request for VAC '{vector_name}': {user_input}")
214
+
215
+ try:
216
+ # Run in executor if not async
217
+ if asyncio.iscoroutinefunction(self.vac_interpreter):
218
+ result = await self.vac_interpreter(
219
+ question=user_input,
220
+ vector_name=vector_name,
221
+ chat_history=chat_history
222
+ )
223
+ else:
224
+ loop = asyncio.get_event_loop()
225
+ result = await loop.run_in_executor(
226
+ None,
227
+ partial(
228
+ self.vac_interpreter,
229
+ question=user_input,
230
+ vector_name=vector_name,
231
+ chat_history=chat_history
232
+ )
233
+ )
234
+
235
+ # Extract answer from result
236
+ if isinstance(result, dict):
237
+ answer = result.get("answer", str(result))
238
+ else:
239
+ answer = str(result)
240
+
241
+ return [
242
+ TextContent(
243
+ type="text",
244
+ text=answer
245
+ )
246
+ ]
247
+
248
+ except Exception as e:
249
+ log.error(f"Error in MCP VAC query: {str(e)}")
250
+ return [
251
+ TextContent(
252
+ type="text",
253
+ text=f"Error: {str(e)}"
254
+ )
255
+ ]
256
+
257
+ def get_server(self) -> Server:
258
+ """Get the underlying MCP server instance."""
259
+ return self.server
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.142.0
3
+ Version: 0.143.1
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0
@@ -19,10 +19,11 @@ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE.txt
21
21
  Requires-Dist: aiohttp
22
+ Requires-Dist: flask>=3.1.0
22
23
  Requires-Dist: google-auth
23
- Requires-Dist: ollama>=0.4.7
24
- Requires-Dist: pillow>=11.0.0
24
+ Requires-Dist: mcp>=1.1.1
25
25
  Requires-Dist: pydantic
26
+ Requires-Dist: pytest-asyncio>=1.0.0
26
27
  Requires-Dist: requests
27
28
  Requires-Dist: ruamel.yaml
28
29
  Requires-Dist: tenacity
@@ -69,7 +70,7 @@ Requires-Dist: langchain-anthropic>=0.1.23; extra == "all"
69
70
  Requires-Dist: langchain-google-vertexai; extra == "all"
70
71
  Requires-Dist: langchain-unstructured; extra == "all"
71
72
  Requires-Dist: langfuse; extra == "all"
72
- Requires-Dist: mcp; extra == "all"
73
+ Requires-Dist: mcp>=1.1.1; extra == "all"
73
74
  Requires-Dist: numpy; extra == "all"
74
75
  Requires-Dist: opencv-python; extra == "all"
75
76
  Requires-Dist: pg8000; extra == "all"
@@ -155,7 +156,7 @@ Requires-Dist: langchain-openai>=0.3.2; extra == "openai"
155
156
  Requires-Dist: tiktoken; extra == "openai"
156
157
  Provides-Extra: anthropic
157
158
  Requires-Dist: langchain-anthropic>=0.1.23; extra == "anthropic"
158
- Requires-Dist: mcp; extra == "anthropic"
159
+ Requires-Dist: mcp>=1.1.1; extra == "anthropic"
159
160
  Provides-Extra: tools
160
161
  Requires-Dist: openapi-spec-validator; extra == "tools"
161
162
  Requires-Dist: playwright; extra == "tools"
@@ -14,7 +14,7 @@ sunholo/agents/fastapi/base.py,sha256=W-cyF8ZDUH40rc-c-Apw3-_8IIi2e4Y9qRtnoVnsc1
14
14
  sunholo/agents/fastapi/qna_routes.py,sha256=lKHkXPmwltu9EH3RMwmD153-J6pE7kWQ4BhBlV3to-s,3864
15
15
  sunholo/agents/flask/__init__.py,sha256=dEoByI3gDNUOjpX1uVKP7uPjhfFHJubbiaAv3xLopnk,63
16
16
  sunholo/agents/flask/base.py,sha256=vnpxFEOnCmt9humqj-jYPLfJcdwzsop9NorgkJ-tSaU,1756
17
- sunholo/agents/flask/vac_routes.py,sha256=TEM0u2vkZC0BSKJABxQVPm4QiUsEFoPOwJZIOxzi1Sk,32621
17
+ sunholo/agents/flask/vac_routes.py,sha256=P_swGAAEee2Z9BdOTQEFza9aI78ezCVtF1MNNi67pug,44599
18
18
  sunholo/archive/__init__.py,sha256=qNHWm5rGPVOlxZBZCpA1wTYPbalizRT7f8X4rs2t290,31
19
19
  sunholo/archive/archive.py,sha256=PxVfDtO2_2ZEEbnhXSCbXLdeoHoQVImo4y3Jr2XkCFY,1204
20
20
  sunholo/auth/__init__.py,sha256=TeP-OY0XGxYV_8AQcVGoh35bvyWhNUcMRfhuD5l44Sk,91
@@ -110,8 +110,10 @@ sunholo/llamaindex/llamaindex_class.py,sha256=PnpPoc7LpP7xvKIXYu-UvI4ehj67pGhE1E
110
110
  sunholo/llamaindex/user_history.py,sha256=ZtkecWuF9ORduyGB8kF8gP66bm9DdvCI-ZiK6Kt-cSE,2265
111
111
  sunholo/lookup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
112
  sunholo/lookup/model_lookup.yaml,sha256=O7o-jP53MLA06C8pI-ILwERShO-xf6z_258wtpZBv6A,739
113
- sunholo/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
+ sunholo/mcp/__init__.py,sha256=GmKB8Z0ecXUzpuzYEOXSI0aaZP7LZJc7yTCMEFdAmi4,791
114
114
  sunholo/mcp/cli.py,sha256=d24nnVzhZYz4AWgTqmN-qjKG4rPbf8RhdmEOHZkBHy8,10570
115
+ sunholo/mcp/mcp_manager.py,sha256=ooa_2JSNaKIuK9azEV0OaPVf5Mp_tL_DHaYEyFCmgKY,3752
116
+ sunholo/mcp/vac_mcp_server.py,sha256=fRcerTqp_pTK8AVITpGPhrez_IaDuUDr4WnVUHPv8GM,10114
115
117
  sunholo/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
118
  sunholo/ollama/ollama_images.py,sha256=H2cpcNu88R4TwyfL_nnqkQhdvBQ2FPCAy4Ok__0yQmo,2351
117
119
  sunholo/pubsub/__init__.py,sha256=DfTEk4zmCfqn6gFxRrqDO0pOrvXTDqH-medpgYO4PGw,117
@@ -169,9 +171,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
169
171
  sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
170
172
  sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
171
173
  sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
172
- sunholo-0.142.0.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
173
- sunholo-0.142.0.dist-info/METADATA,sha256=9v1AM0UCFJv_qx8vYOFO8gprTDulHCAxnMck7Jcmszs,18292
174
- sunholo-0.142.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
175
- sunholo-0.142.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
176
- sunholo-0.142.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
177
- sunholo-0.142.0.dist-info/RECORD,,
174
+ sunholo-0.143.1.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
175
+ sunholo-0.143.1.dist-info/METADATA,sha256=bvNatmW9CaH2Fyn2AN8np1IeiLF0MNciSg7gjihkYvI,18338
176
+ sunholo-0.143.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
177
+ sunholo-0.143.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
178
+ sunholo-0.143.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
179
+ sunholo-0.143.1.dist-info/RECORD,,