signalpilot-ai-internal 0.10.0__py3-none-any.whl → 0.10.22__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.
Files changed (49) hide show
  1. signalpilot_ai_internal/__init__.py +1 -0
  2. signalpilot_ai_internal/_version.py +1 -1
  3. signalpilot_ai_internal/databricks_schema_service.py +902 -0
  4. signalpilot_ai_internal/handlers.py +72 -2
  5. signalpilot_ai_internal/mcp_handlers.py +508 -0
  6. signalpilot_ai_internal/mcp_server_manager.py +298 -0
  7. signalpilot_ai_internal/mcp_service.py +1303 -0
  8. signalpilot_ai_internal/schema_search_service.py +62 -1
  9. signalpilot_ai_internal/test_dbt_mcp_server.py +180 -0
  10. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json +2 -2
  11. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig +1 -1
  12. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/plugin.json +7 -1
  13. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/786.770dc7bcab77e14cc135.js → signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/110.224e83db03814fd03955.js +2 -2
  14. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.e9acd2e1f9739037f1ab.js → signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.c4232851631fb2e7e59a.js +1 -1
  15. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/726.318e4e791edb63cc788f.js +1 -0
  16. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.d9914229e4f120e7e9e4.js +1 -0
  17. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.d80de1e4da5b520d2f3b.js +1 -0
  18. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.b63c429ca81e743b403c.js +1 -0
  19. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/third-party-licenses.json +18 -0
  20. {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.10.22.dist-info}/METADATA +3 -2
  21. signalpilot_ai_internal-0.10.22.dist-info/RECORD +56 -0
  22. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/330.af2e9cb5def5ae2b84d5.js +0 -1
  23. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.25ddd15aca09421d3765.js +0 -1
  24. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.b05b2f0c9617ba28370d.js +0 -1
  25. signalpilot_ai_internal-0.10.0.dist-info/RECORD +0 -50
  26. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/etc/jupyter/jupyter_server_config.d/signalpilot_ai.json +0 -0
  27. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/install.json +0 -0
  28. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -0
  29. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -0
  30. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -0
  31. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.972abe1d2d66f083f9cc.js +0 -0
  32. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -0
  33. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -0
  34. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -0
  35. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.ad22ccddd74ee306fb56.js +0 -0
  36. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -0
  37. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -0
  38. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -0
  39. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js.LICENSE.txt +0 -0
  40. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -0
  41. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -0
  42. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -0
  43. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -0
  44. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.2d75de1a8d2c3131a8db.js +0 -0
  45. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.ca9e114a30896b669a3c.js +0 -0
  46. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -0
  47. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.10.22.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/style.js +0 -0
  48. {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.10.22.dist-info}/WHEEL +0 -0
  49. {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.10.22.dist-info}/licenses/LICENSE +0 -0
@@ -12,8 +12,22 @@ from .cache_service import get_cache_service
12
12
  from .cache_handlers import ChatHistoriesHandler, AppValuesHandler, CacheInfoHandler
13
13
  from .unified_database_schema_service import UnifiedDatabaseSchemaHandler, UnifiedDatabaseQueryHandler
14
14
  from .snowflake_schema_service import SnowflakeSchemaHandler, SnowflakeQueryHandler
15
+ from .databricks_schema_service import DatabricksSchemaHandler, DatabricksQueryHandler, DatabricksTestHandler
15
16
  from .file_scanner_service import get_file_scanner_service
16
17
  from .schema_search_service import SchemaSearchHandler
18
+ from .mcp_handlers import (
19
+ MCPServersHandler,
20
+ MCPServerHandler,
21
+ MCPConnectHandler,
22
+ MCPDisconnectHandler,
23
+ MCPToolsHandler,
24
+ MCPAllToolsHandler,
25
+ MCPToolCallHandler,
26
+ MCPServerEnableHandler,
27
+ MCPServerDisableHandler,
28
+ MCPToolEnableHandler,
29
+ MCPConfigFileHandler
30
+ )
17
31
 
18
32
 
19
33
  class HelloWorldHandler(APIHandler):
@@ -741,12 +755,30 @@ def setup_handlers(web_app):
741
755
  # Snowflake service endpoints
742
756
  snowflake_schema_route = url_path_join(base_url, "signalpilot-ai-internal", "snowflake", "schema")
743
757
  snowflake_query_route = url_path_join(base_url, "signalpilot-ai-internal", "snowflake", "query")
744
-
758
+
759
+ # Databricks service endpoints
760
+ databricks_schema_route = url_path_join(base_url, "signalpilot-ai-internal", "databricks", "schema")
761
+ databricks_query_route = url_path_join(base_url, "signalpilot-ai-internal", "databricks", "query")
762
+ databricks_test_route = url_path_join(base_url, "signalpilot-ai-internal", "databricks", "test")
763
+
745
764
  # Notebook HTML export endpoint
746
765
  notebook_html_route = url_path_join(base_url, "signalpilot-ai-internal", "notebook", "to-html")
747
766
 
748
767
  # Terminal endpoint
749
768
  terminal_execute_route = url_path_join(base_url, "signalpilot-ai-internal", "terminal", "execute")
769
+
770
+ # MCP service endpoints
771
+ mcp_servers_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "servers")
772
+ mcp_server_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "servers", "([^/]+)")
773
+ mcp_connect_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "connect")
774
+ mcp_disconnect_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "servers", "([^/]+)", "disconnect")
775
+ mcp_tools_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "servers", "([^/]+)", "tools")
776
+ mcp_all_tools_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "tools")
777
+ mcp_tool_call_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "call-tool")
778
+ mcp_tool_enable_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "servers", "([^/]+)", "tools", "([^/]+)")
779
+ mcp_server_enable_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "servers", "([^/]+)", "enable")
780
+ mcp_server_disable_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "servers", "([^/]+)", "disable")
781
+ mcp_config_file_route = url_path_join(base_url, "signalpilot-ai-internal", "mcp", "config-file")
750
782
 
751
783
  handlers = [
752
784
  # Original endpoint
@@ -791,9 +823,28 @@ def setup_handlers(web_app):
791
823
  # Snowflake service endpoints
792
824
  (snowflake_schema_route, SnowflakeSchemaHandler),
793
825
  (snowflake_query_route, SnowflakeQueryHandler),
794
-
826
+
827
+ # Databricks service endpoints
828
+ (databricks_schema_route, DatabricksSchemaHandler),
829
+ (databricks_query_route, DatabricksQueryHandler),
830
+ (databricks_test_route, DatabricksTestHandler),
831
+
795
832
  # Notebook HTML export endpoint
796
833
  (notebook_html_route, NotebookToHTMLHandler),
834
+
835
+ # MCP service endpoints
836
+ # Note: More specific routes should come before parameterized routes
837
+ (mcp_config_file_route, MCPConfigFileHandler),
838
+ (mcp_servers_route, MCPServersHandler),
839
+ (mcp_server_route, MCPServerHandler),
840
+ (mcp_connect_route, MCPConnectHandler),
841
+ (mcp_disconnect_route, MCPDisconnectHandler),
842
+ (mcp_tools_route, MCPToolsHandler),
843
+ (mcp_all_tools_route, MCPAllToolsHandler),
844
+ (mcp_tool_call_route, MCPToolCallHandler),
845
+ (mcp_tool_enable_route, MCPToolEnableHandler),
846
+ (mcp_server_enable_route, MCPServerEnableHandler),
847
+ (mcp_server_disable_route, MCPServerDisableHandler),
797
848
  ]
798
849
 
799
850
  web_app.add_handlers(host_pattern, handlers)
@@ -806,6 +857,18 @@ def setup_handlers(web_app):
806
857
  else:
807
858
  print("WARNING: SignalPilot AI cache service failed to initialize!")
808
859
 
860
+ # Register cleanup handler for MCP servers on shutdown
861
+ from .mcp_server_manager import get_mcp_server_manager
862
+
863
+ def cleanup_mcp_servers():
864
+ """Stop all MCP servers on shutdown"""
865
+ manager = get_mcp_server_manager()
866
+ manager.stop_all_servers()
867
+
868
+ # Register cleanup with web app
869
+ import atexit
870
+ atexit.register(cleanup_mcp_servers)
871
+
809
872
  print("SignalPilot AI backend handlers registered:")
810
873
  print(f" - Hello World: {hello_route}")
811
874
  print(f" - Read All Files: {read_all_files_route}")
@@ -821,5 +884,12 @@ def setup_handlers(web_app):
821
884
  print(f" - MySQL Query: {mysql_query_route}")
822
885
  print(f" - Snowflake Schema: {snowflake_schema_route}")
823
886
  print(f" - Snowflake Query: {snowflake_query_route}")
887
+ print(f" - Databricks Schema: {databricks_schema_route}")
888
+ print(f" - Databricks Query: {databricks_query_route}")
889
+ print(f" - Databricks Test: {databricks_test_route}")
824
890
  print(f" - Notebook Cells: {notebook_cells_route}")
825
891
  print(f" - Notebook to HTML: {notebook_html_route}")
892
+ print(f" - MCP Servers: {mcp_servers_route}")
893
+ print(f" - MCP Connect: {mcp_connect_route}")
894
+ print(f" - MCP Tools: {mcp_all_tools_route}")
895
+ print(f" - MCP Tool Call: {mcp_tool_call_route}")
@@ -0,0 +1,508 @@
1
+ """
2
+ MCP Handlers - Tornado HTTP handlers for MCP API endpoints
3
+ Provides REST API for managing MCP server connections and tool calls
4
+ """
5
+ import json
6
+ import logging
7
+ import traceback
8
+ import tornado
9
+ from jupyter_server.base.handlers import APIHandler
10
+ from .mcp_service import get_mcp_service
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Enable debug logging
15
+ logger.setLevel(logging.DEBUG)
16
+
17
+
18
+ class MCPServersHandler(APIHandler):
19
+ """Handler for managing MCP server configurations"""
20
+
21
+ @tornado.web.authenticated
22
+ async def get(self):
23
+ """Get all configured MCP servers"""
24
+ try:
25
+ mcp_service = get_mcp_service()
26
+ configs = mcp_service.load_all_configs()
27
+
28
+ # Add connection status to each server
29
+ servers = []
30
+ for server_id, config in configs.items():
31
+ server_info = {
32
+ **config,
33
+ 'status': mcp_service.get_connection_status(server_id),
34
+ 'enabled': config.get('enabled', True)
35
+ }
36
+
37
+ # Add tool count if connected
38
+ if server_id in mcp_service.tools_cache:
39
+ server_info['toolCount'] = len(mcp_service.tools_cache[server_id])
40
+
41
+ servers.append(server_info)
42
+
43
+ self.finish(json.dumps({
44
+ 'servers': servers
45
+ }))
46
+ except Exception as e:
47
+ logger.error(f"Error getting MCP servers: {e}")
48
+ self.set_status(500)
49
+ self.finish(json.dumps({
50
+ 'error': str(e)
51
+ }))
52
+
53
+ @tornado.web.authenticated
54
+ async def post(self):
55
+ """Save a new MCP server configuration"""
56
+ try:
57
+ data = json.loads(self.request.body.decode('utf-8'))
58
+
59
+ # Validate required fields
60
+ if 'name' not in data:
61
+ self.set_status(400)
62
+ self.finish(json.dumps({
63
+ 'error': 'Server name is required'
64
+ }))
65
+ return
66
+
67
+ # Determine connection type (default to 'command' if not specified)
68
+ connection_type = data.get('type', 'command')
69
+ data['type'] = connection_type # Ensure type is set in the data
70
+
71
+ if connection_type == 'command':
72
+ if 'command' not in data:
73
+ self.set_status(400)
74
+ self.finish(json.dumps({
75
+ 'error': 'Command is required for command-based MCP'
76
+ }))
77
+ return
78
+ elif connection_type in ['http', 'sse']:
79
+ if 'url' not in data:
80
+ self.set_status(400)
81
+ self.finish(json.dumps({
82
+ 'error': 'URL is required for HTTP/SSE MCP'
83
+ }))
84
+ return
85
+ else:
86
+ self.set_status(400)
87
+ self.finish(json.dumps({
88
+ 'error': f'Invalid connection type: {connection_type}'
89
+ }))
90
+ return
91
+
92
+ # Save configuration
93
+ mcp_service = get_mcp_service()
94
+ saved_config = mcp_service.save_server_config(data)
95
+
96
+ self.finish(json.dumps({
97
+ 'success': True,
98
+ 'server': saved_config
99
+ }))
100
+ except json.JSONDecodeError:
101
+ self.set_status(400)
102
+ self.finish(json.dumps({
103
+ 'error': 'Invalid JSON in request body'
104
+ }))
105
+ except Exception as e:
106
+ logger.error(f"Error saving MCP server: {e}")
107
+ self.set_status(500)
108
+ self.finish(json.dumps({
109
+ 'error': str(e)
110
+ }))
111
+
112
+
113
+ class MCPServerHandler(APIHandler):
114
+ """Handler for individual MCP server operations"""
115
+
116
+ @tornado.web.authenticated
117
+ async def delete(self, server_id):
118
+ """Delete an MCP server configuration"""
119
+ try:
120
+ mcp_service = get_mcp_service()
121
+ success = mcp_service.delete_server_config(server_id)
122
+
123
+ if success:
124
+ self.finish(json.dumps({
125
+ 'success': True,
126
+ 'message': f'Server {server_id} deleted'
127
+ }))
128
+ else:
129
+ self.set_status(404)
130
+ self.finish(json.dumps({
131
+ 'error': 'Server not found'
132
+ }))
133
+ except Exception as e:
134
+ logger.error(f"Error deleting MCP server: {e}")
135
+ self.set_status(500)
136
+ self.finish(json.dumps({
137
+ 'error': str(e)
138
+ }))
139
+
140
+ @tornado.web.authenticated
141
+ async def put(self, server_id):
142
+ """Update an MCP server configuration"""
143
+ try:
144
+ data = json.loads(self.request.body.decode('utf-8'))
145
+ mcp_service = get_mcp_service()
146
+
147
+ # Get existing config
148
+ config = mcp_service.get_server_config(server_id)
149
+ if not config:
150
+ self.set_status(404)
151
+ self.finish(json.dumps({
152
+ 'error': 'Server not found'
153
+ }))
154
+ return
155
+
156
+ # Update config with new data
157
+ config.update(data)
158
+ config['id'] = server_id # Ensure ID is preserved
159
+
160
+ # Save updated config
161
+ saved_config = mcp_service.save_server_config(config)
162
+
163
+ self.finish(json.dumps({
164
+ 'success': True,
165
+ 'server': saved_config
166
+ }))
167
+ except json.JSONDecodeError:
168
+ self.set_status(400)
169
+ self.finish(json.dumps({
170
+ 'error': 'Invalid JSON in request body'
171
+ }))
172
+ except Exception as e:
173
+ logger.error(f"Error updating MCP server: {e}")
174
+ self.set_status(500)
175
+ self.finish(json.dumps({
176
+ 'error': str(e)
177
+ }))
178
+
179
+
180
+ class MCPConnectHandler(APIHandler):
181
+ """Handler for connecting to MCP servers"""
182
+
183
+ @tornado.web.authenticated
184
+ async def post(self):
185
+ """Connect to a specific MCP server"""
186
+ server_id = None
187
+ try:
188
+ logger.debug(f"[MCP Handler] Received connect request")
189
+ data = json.loads(self.request.body.decode('utf-8'))
190
+ server_id = data.get('server_id')
191
+
192
+ logger.debug(f"[MCP Handler] Request data: {data}")
193
+
194
+ if not server_id:
195
+ logger.warning(f"[MCP Handler] Missing server_id in request")
196
+ self.set_status(400)
197
+ self.finish(json.dumps({
198
+ 'error': 'server_id is required'
199
+ }))
200
+ return
201
+
202
+ logger.info(f"[MCP Handler] Attempting to connect to server: {server_id}")
203
+ mcp_service = get_mcp_service()
204
+ server_info = await mcp_service.connect(server_id)
205
+
206
+ logger.info(f"[MCP Handler] Successfully connected to {server_id}")
207
+ self.finish(json.dumps({
208
+ 'success': True,
209
+ 'server': server_info
210
+ }))
211
+ except json.JSONDecodeError as e:
212
+ logger.error(f"[MCP Handler] Invalid JSON in request body: {e}")
213
+ logger.error(f"[MCP Handler] Stack trace:\n{traceback.format_exc()}")
214
+ self.set_status(400)
215
+ self.finish(json.dumps({
216
+ 'error': f'Invalid JSON in request body: {str(e)}'
217
+ }))
218
+ except ValueError as e:
219
+ logger.error(f"[MCP Handler] ValueError for server {server_id}: {e}")
220
+ logger.error(f"[MCP Handler] Stack trace:\n{traceback.format_exc()}")
221
+ self.set_status(404)
222
+ self.finish(json.dumps({
223
+ 'error': str(e),
224
+ 'errorType': 'ValueError',
225
+ 'serverId': server_id
226
+ }))
227
+ except RuntimeError as e:
228
+ logger.error(f"[MCP Handler] RuntimeError connecting to {server_id}: {e}")
229
+ logger.error(f"[MCP Handler] Stack trace:\n{traceback.format_exc()}")
230
+ self.set_status(500)
231
+ self.finish(json.dumps({
232
+ 'error': str(e),
233
+ 'errorType': 'RuntimeError',
234
+ 'serverId': server_id,
235
+ 'details': 'Check server logs for detailed error information'
236
+ }))
237
+ except Exception as e:
238
+ error_type = type(e).__name__
239
+ error_msg = str(e)
240
+ logger.error(f"[MCP Handler] Unexpected error ({error_type}) connecting to {server_id}: {error_msg}")
241
+ logger.error(f"[MCP Handler] Full stack trace:\n{traceback.format_exc()}")
242
+ self.set_status(500)
243
+ self.finish(json.dumps({
244
+ 'error': error_msg,
245
+ 'errorType': error_type,
246
+ 'serverId': server_id,
247
+ 'stackTrace': traceback.format_exc(),
248
+ 'details': 'An unexpected error occurred. Check server logs for more information.'
249
+ }))
250
+
251
+
252
+ class MCPDisconnectHandler(APIHandler):
253
+ """Handler for disconnecting from MCP servers"""
254
+
255
+ @tornado.web.authenticated
256
+ async def post(self, server_id):
257
+ """Disconnect from a specific MCP server"""
258
+ try:
259
+ mcp_service = get_mcp_service()
260
+ success = await mcp_service.disconnect(server_id)
261
+
262
+ if success:
263
+ self.finish(json.dumps({
264
+ 'success': True,
265
+ 'message': f'Disconnected from server {server_id}'
266
+ }))
267
+ else:
268
+ self.set_status(404)
269
+ self.finish(json.dumps({
270
+ 'error': 'Server not found or not connected'
271
+ }))
272
+ except Exception as e:
273
+ logger.error(f"Error disconnecting from MCP server: {e}")
274
+ self.set_status(500)
275
+ self.finish(json.dumps({
276
+ 'error': str(e)
277
+ }))
278
+
279
+
280
+ class MCPToolsHandler(APIHandler):
281
+ """Handler for listing MCP tools"""
282
+
283
+ @tornado.web.authenticated
284
+ async def get(self, server_id):
285
+ """Get available tools from a connected MCP server"""
286
+ try:
287
+ mcp_service = get_mcp_service()
288
+ tools = await mcp_service.list_tools(server_id)
289
+
290
+ self.finish(json.dumps({
291
+ 'tools': tools
292
+ }))
293
+ except ValueError as e:
294
+ self.set_status(404)
295
+ self.finish(json.dumps({
296
+ 'error': str(e)
297
+ }))
298
+ except Exception as e:
299
+ logger.error(f"Error listing MCP tools: {e}")
300
+ self.set_status(500)
301
+ self.finish(json.dumps({
302
+ 'error': str(e)
303
+ }))
304
+
305
+
306
+ class MCPAllToolsHandler(APIHandler):
307
+ """Handler for getting all tools from all connected servers"""
308
+
309
+ @tornado.web.authenticated
310
+ async def get(self):
311
+ """Get all tools from all connected MCP servers"""
312
+ try:
313
+ mcp_service = get_mcp_service()
314
+ tools = await mcp_service.get_all_tools()
315
+
316
+ self.finish(json.dumps({
317
+ 'tools': tools
318
+ }))
319
+ except Exception as e:
320
+ logger.error(f"Error getting all MCP tools: {e}")
321
+ self.set_status(500)
322
+ self.finish(json.dumps({
323
+ 'error': str(e)
324
+ }))
325
+
326
+
327
+ class MCPToolCallHandler(APIHandler):
328
+ """Handler for calling MCP tools"""
329
+
330
+ @tornado.web.authenticated
331
+ async def post(self):
332
+ """Call a tool on an MCP server"""
333
+ try:
334
+ data = json.loads(self.request.body.decode('utf-8'))
335
+
336
+ server_id = data.get('server_id')
337
+ tool_name = data.get('tool_name')
338
+ arguments = data.get('arguments', {})
339
+
340
+ if not server_id or not tool_name:
341
+ self.set_status(400)
342
+ self.finish(json.dumps({
343
+ 'error': 'server_id and tool_name are required'
344
+ }))
345
+ return
346
+
347
+ mcp_service = get_mcp_service()
348
+ result = await mcp_service.call_tool(server_id, tool_name, arguments)
349
+
350
+ self.finish(json.dumps({
351
+ 'success': True,
352
+ 'result': result
353
+ }))
354
+ except ValueError as e:
355
+ self.set_status(404)
356
+ self.finish(json.dumps({
357
+ 'error': str(e)
358
+ }))
359
+ except Exception as e:
360
+ logger.error(f"Error calling MCP tool: {e}")
361
+ self.set_status(500)
362
+ self.finish(json.dumps({
363
+ 'error': str(e)
364
+ }))
365
+
366
+
367
+ class MCPServerEnableHandler(APIHandler):
368
+ """Handler for enabling MCP servers"""
369
+
370
+ @tornado.web.authenticated
371
+ async def post(self, server_id):
372
+ """Enable an MCP server"""
373
+ try:
374
+ mcp_service = get_mcp_service()
375
+ success = mcp_service.enable_server(server_id)
376
+
377
+ if success:
378
+ self.finish(json.dumps({
379
+ 'success': True,
380
+ 'message': f'Server {server_id} enabled'
381
+ }))
382
+ else:
383
+ self.set_status(404)
384
+ self.finish(json.dumps({
385
+ 'error': 'Server not found'
386
+ }))
387
+ except Exception as e:
388
+ logger.error(f"Error enabling MCP server: {e}")
389
+ self.set_status(500)
390
+ self.finish(json.dumps({
391
+ 'error': str(e)
392
+ }))
393
+
394
+
395
+ class MCPServerDisableHandler(APIHandler):
396
+ """Handler for disabling MCP servers"""
397
+
398
+ @tornado.web.authenticated
399
+ async def post(self, server_id):
400
+ """Disable an MCP server"""
401
+ try:
402
+ mcp_service = get_mcp_service()
403
+ success = mcp_service.disable_server(server_id)
404
+
405
+ if success:
406
+ self.finish(json.dumps({
407
+ 'success': True,
408
+ 'message': f'Server {server_id} disabled'
409
+ }))
410
+ else:
411
+ self.set_status(404)
412
+ self.finish(json.dumps({
413
+ 'error': 'Server not found'
414
+ }))
415
+ except Exception as e:
416
+ logger.error(f"Error disabling MCP server: {e}")
417
+ self.set_status(500)
418
+ self.finish(json.dumps({
419
+ 'error': str(e)
420
+ }))
421
+
422
+
423
+ class MCPToolEnableHandler(APIHandler):
424
+ """Handler for enabling/disabling individual MCP tools"""
425
+
426
+ @tornado.web.authenticated
427
+ async def put(self, server_id, tool_name):
428
+ """Update enabled/disabled state for a specific tool"""
429
+ try:
430
+ data = json.loads(self.request.body.decode('utf-8'))
431
+ enabled = data.get('enabled', True)
432
+
433
+ mcp_service = get_mcp_service()
434
+ success = mcp_service.update_tool_enabled(server_id, tool_name, enabled)
435
+
436
+ if success:
437
+ self.finish(json.dumps({
438
+ 'success': True,
439
+ 'message': f'Tool {tool_name} {"enabled" if enabled else "disabled"}'
440
+ }))
441
+ else:
442
+ self.set_status(404)
443
+ self.finish(json.dumps({
444
+ 'error': 'Server not found'
445
+ }))
446
+ except Exception as e:
447
+ logger.error(f"Error updating tool enabled state: {e}")
448
+ self.set_status(500)
449
+ self.finish(json.dumps({
450
+ 'error': str(e)
451
+ }))
452
+
453
+
454
+ class MCPConfigFileHandler(APIHandler):
455
+ """Handler for managing the entire MCP config file"""
456
+
457
+ @tornado.web.authenticated
458
+ async def get(self):
459
+ """Get the raw JSON config file content"""
460
+ try:
461
+ logger.debug(f"[MCP ConfigFile Handler] GET request received")
462
+ mcp_service = get_mcp_service()
463
+ content = mcp_service.get_config_file_content()
464
+
465
+ # Ensure content is valid JSON string
466
+ if not content:
467
+ content = json.dumps({'mcpServers': {}}, indent=2)
468
+
469
+ # Validate it's valid JSON
470
+ try:
471
+ json.loads(content)
472
+ except json.JSONDecodeError as e:
473
+ logger.error(f"Config file content is not valid JSON: {e}")
474
+ content = json.dumps({'mcpServers': {}}, indent=2)
475
+
476
+ logger.debug(f"[MCP ConfigFile Handler] Returning config file content ({len(content)} chars)")
477
+ self.set_header('Content-Type', 'application/json; charset=utf-8')
478
+ self.finish(content)
479
+ except Exception as e:
480
+ logger.error(f"Error reading config file: {e}")
481
+ logger.error(f"Stack trace:\n{traceback.format_exc()}")
482
+ self.set_status(500)
483
+ self.set_header('Content-Type', 'application/json')
484
+ self.finish(json.dumps({
485
+ 'error': str(e)
486
+ }))
487
+
488
+ @tornado.web.authenticated
489
+ async def put(self):
490
+ """Update the entire config file with diff detection"""
491
+ try:
492
+ content = self.request.body.decode('utf-8')
493
+ mcp_service = get_mcp_service()
494
+
495
+ result = mcp_service.update_config_file(content)
496
+
497
+ self.finish(json.dumps(result))
498
+ except ValueError as e:
499
+ self.set_status(400)
500
+ self.finish(json.dumps({
501
+ 'error': str(e)
502
+ }))
503
+ except Exception as e:
504
+ logger.error(f"Error updating config file: {e}")
505
+ self.set_status(500)
506
+ self.finish(json.dumps({
507
+ 'error': str(e)
508
+ }))