signalpilot-ai-internal 0.10.0__py3-none-any.whl → 0.11.24__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 (85) hide show
  1. signalpilot_ai_internal/__init__.py +1 -0
  2. signalpilot_ai_internal/_version.py +1 -1
  3. signalpilot_ai_internal/cache_service.py +22 -21
  4. signalpilot_ai_internal/composio_handlers.py +224 -0
  5. signalpilot_ai_internal/composio_service.py +511 -0
  6. signalpilot_ai_internal/database_config_handlers.py +182 -0
  7. signalpilot_ai_internal/database_config_service.py +166 -0
  8. signalpilot_ai_internal/databricks_schema_service.py +907 -0
  9. signalpilot_ai_internal/file_scanner_service.py +5 -146
  10. signalpilot_ai_internal/handlers.py +388 -9
  11. signalpilot_ai_internal/integrations_config.py +256 -0
  12. signalpilot_ai_internal/log_utils.py +31 -0
  13. signalpilot_ai_internal/mcp_handlers.py +532 -0
  14. signalpilot_ai_internal/mcp_server_manager.py +298 -0
  15. signalpilot_ai_internal/mcp_service.py +1255 -0
  16. signalpilot_ai_internal/oauth_token_store.py +141 -0
  17. signalpilot_ai_internal/schema_search_config.yml +17 -11
  18. signalpilot_ai_internal/schema_search_service.py +85 -4
  19. signalpilot_ai_internal/signalpilot_home.py +961 -0
  20. signalpilot_ai_internal/snowflake_schema_service.py +2 -0
  21. signalpilot_ai_internal/test_dbt_mcp_server.py +180 -0
  22. signalpilot_ai_internal/unified_database_schema_service.py +2 -0
  23. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig → signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json +15 -48
  24. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/package.json → signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/package.json.orig +9 -52
  25. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/plugin.json +7 -1
  26. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.bab318d6caadb055e29c.js +1 -0
  27. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/129.868ca665e6fc225c20a0.js +1 -0
  28. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/179.fd45a2e75d471d0aa3b9.js +7 -0
  29. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.81105a94aa873fc51a94.js +1 -0
  30. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.a002dd4630d3b6404a90.js +1 -0
  31. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.cc6f6ecacd703bcdb468.js +1 -0
  32. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.817a883549d55a0e0576.js +1 -0
  33. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.a4daecd44f1e9364e44a.js +1 -0
  34. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.667225aab294fb5ed161.js +1 -0
  35. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/447.8138af2522716e5a926f.js +1 -0
  36. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.925c73e32f3c07448da0.js +1 -0
  37. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/477.aaa4cc9e87801fb45f5b.js +1 -0
  38. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.370056149a59022b700c.js +1 -0
  39. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/510.868ca665e6fc225c20a0.js +1 -0
  40. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.835f97f7ccfc70ff5c93.js +1 -0
  41. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.6c13335f73de089d6b1e.js +1 -0
  42. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/574.ad2709e91ebcac5bbe68.js +1 -0
  43. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.bddbab8e464fe31f0393.js +1 -0
  44. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.fda1bcdb10497b0a6ade.js +1 -0
  45. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.d046701f475fcbf6697d.js +1 -0
  46. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.c306dffd4cfe8a613d13.js +1 -0
  47. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.e39898b6f336539f228c.js +1 -0
  48. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.77cc0ca10a1860df1b52.js +1 -0
  49. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.4e2850b2af985ed0d378.js +1 -0
  50. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/956.eeffe67d7781fd63ef4b.js +2 -0
  51. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.055f50d20a31f3068c72.js +1 -0
  52. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/third-party-licenses.json +47 -29
  53. {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/METADATA +14 -31
  54. signalpilot_ai_internal-0.11.24.dist-info/RECORD +66 -0
  55. signalpilot_ai_internal-0.11.24.dist-info/licenses/LICENSE +7 -0
  56. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -1
  57. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -1
  58. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -1
  59. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/330.af2e9cb5def5ae2b84d5.js +0 -1
  60. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.972abe1d2d66f083f9cc.js +0 -1
  61. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -1
  62. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -1
  63. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -1
  64. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.ad22ccddd74ee306fb56.js +0 -1
  65. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -1
  66. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -1
  67. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -2
  68. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.e9acd2e1f9739037f1ab.js +0 -1
  69. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -1
  70. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -1
  71. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -1
  72. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -1
  73. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.2d75de1a8d2c3131a8db.js +0 -1
  74. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/786.770dc7bcab77e14cc135.js +0 -7
  75. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.ca9e114a30896b669a3c.js +0 -1
  76. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.25ddd15aca09421d3765.js +0 -1
  77. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -1
  78. signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.b05b2f0c9617ba28370d.js +0 -1
  79. signalpilot_ai_internal-0.10.0.dist-info/RECORD +0 -50
  80. signalpilot_ai_internal-0.10.0.dist-info/licenses/LICENSE +0 -29
  81. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/etc/jupyter/jupyter_server_config.d/signalpilot_ai.json +0 -0
  82. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/install.json +0 -0
  83. /signalpilot_ai_internal-0.10.0.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js.LICENSE.txt → /signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/956.eeffe67d7781fd63ef4b.js.LICENSE.txt +0 -0
  84. {signalpilot_ai_internal-0.10.0.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/style.js +0 -0
  85. {signalpilot_ai_internal-0.10.0.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/WHEEL +0 -0
@@ -0,0 +1,532 @@
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
+ from .oauth_token_store import get_oauth_token_store
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Enable debug logging
16
+ logger.setLevel(logging.DEBUG)
17
+
18
+
19
+ class MCPServersHandler(APIHandler):
20
+ """Handler for managing MCP server configurations"""
21
+
22
+ @tornado.web.authenticated
23
+ async def get(self):
24
+ """Get all configured MCP servers"""
25
+ try:
26
+ mcp_service = get_mcp_service()
27
+ token_store = get_oauth_token_store()
28
+ configs = mcp_service.load_all_configs()
29
+
30
+ # Add connection status to each server
31
+ servers = []
32
+ for server_id, config in configs.items():
33
+ server_info = {
34
+ **config,
35
+ 'status': mcp_service.get_connection_status(server_id),
36
+ 'enabled': config.get('enabled', True)
37
+ }
38
+
39
+ # Add tool count if connected
40
+ if server_id in mcp_service.tools_cache:
41
+ server_info['toolCount'] = len(mcp_service.tools_cache[server_id])
42
+
43
+ # Check if this is an OAuth integration and add the integration ID
44
+ is_oauth = config.get('isOAuthIntegration', False)
45
+ if not is_oauth:
46
+ is_oauth = token_store.is_oauth_server(server_id)
47
+
48
+ if is_oauth:
49
+ server_info['isOAuthIntegration'] = True
50
+ integration_id = token_store.get_integration_id(server_id)
51
+ if integration_id:
52
+ server_info['integrationId'] = integration_id
53
+
54
+ servers.append(server_info)
55
+
56
+ self.finish(json.dumps({
57
+ 'servers': servers
58
+ }))
59
+ except Exception as e:
60
+ logger.error(f"Error getting MCP servers: {e}")
61
+ self.set_status(500)
62
+ self.finish(json.dumps({
63
+ 'error': str(e)
64
+ }))
65
+
66
+ @tornado.web.authenticated
67
+ async def post(self):
68
+ """Save a new MCP server configuration"""
69
+ try:
70
+ data = json.loads(self.request.body.decode('utf-8'))
71
+
72
+ # Validate required fields
73
+ if 'name' not in data:
74
+ self.set_status(400)
75
+ self.finish(json.dumps({
76
+ 'error': 'Server name is required'
77
+ }))
78
+ return
79
+
80
+ # Determine connection type (default to 'command' if not specified)
81
+ connection_type = data.get('type', 'command')
82
+ data['type'] = connection_type # Ensure type is set in the data
83
+
84
+ if connection_type == 'command':
85
+ if 'command' not in data:
86
+ self.set_status(400)
87
+ self.finish(json.dumps({
88
+ 'error': 'Command is required for command-based MCP'
89
+ }))
90
+ return
91
+ elif connection_type in ['http', 'sse']:
92
+ if 'url' not in data:
93
+ self.set_status(400)
94
+ self.finish(json.dumps({
95
+ 'error': 'URL is required for HTTP/SSE MCP'
96
+ }))
97
+ return
98
+ else:
99
+ self.set_status(400)
100
+ self.finish(json.dumps({
101
+ 'error': f'Invalid connection type: {connection_type}'
102
+ }))
103
+ return
104
+
105
+ # Save configuration
106
+ mcp_service = get_mcp_service()
107
+ saved_config = mcp_service.save_server_config(data)
108
+
109
+ self.finish(json.dumps({
110
+ 'success': True,
111
+ 'server': saved_config
112
+ }))
113
+ except json.JSONDecodeError:
114
+ self.set_status(400)
115
+ self.finish(json.dumps({
116
+ 'error': 'Invalid JSON in request body'
117
+ }))
118
+ except Exception as e:
119
+ logger.error(f"Error saving MCP server: {e}")
120
+ self.set_status(500)
121
+ self.finish(json.dumps({
122
+ 'error': str(e)
123
+ }))
124
+
125
+
126
+ class MCPServerHandler(APIHandler):
127
+ """Handler for individual MCP server operations"""
128
+
129
+ @tornado.web.authenticated
130
+ async def delete(self, server_id):
131
+ """Delete an MCP server configuration"""
132
+ try:
133
+ mcp_service = get_mcp_service()
134
+ success = mcp_service.delete_server_config(server_id)
135
+
136
+ if success:
137
+ self.finish(json.dumps({
138
+ 'success': True,
139
+ 'message': f'Server {server_id} deleted'
140
+ }))
141
+ else:
142
+ self.set_status(404)
143
+ self.finish(json.dumps({
144
+ 'error': 'Server not found'
145
+ }))
146
+ except Exception as e:
147
+ logger.error(f"Error deleting MCP server: {e}")
148
+ self.set_status(500)
149
+ self.finish(json.dumps({
150
+ 'error': str(e)
151
+ }))
152
+
153
+ @tornado.web.authenticated
154
+ async def put(self, server_id):
155
+ """Update an MCP server configuration"""
156
+ try:
157
+ data = json.loads(self.request.body.decode('utf-8'))
158
+ mcp_service = get_mcp_service()
159
+
160
+ # Get existing config
161
+ config = mcp_service.get_server_config(server_id)
162
+ if not config:
163
+ self.set_status(404)
164
+ self.finish(json.dumps({
165
+ 'error': 'Server not found'
166
+ }))
167
+ return
168
+
169
+ # Update config with new data
170
+ config.update(data)
171
+ config['id'] = server_id # Ensure ID is preserved
172
+
173
+ # Save updated config
174
+ saved_config = mcp_service.save_server_config(config)
175
+
176
+ self.finish(json.dumps({
177
+ 'success': True,
178
+ 'server': saved_config
179
+ }))
180
+ except json.JSONDecodeError:
181
+ self.set_status(400)
182
+ self.finish(json.dumps({
183
+ 'error': 'Invalid JSON in request body'
184
+ }))
185
+ except Exception as e:
186
+ logger.error(f"Error updating MCP server: {e}")
187
+ self.set_status(500)
188
+ self.finish(json.dumps({
189
+ 'error': str(e)
190
+ }))
191
+
192
+
193
+ class MCPConnectHandler(APIHandler):
194
+ """Handler for connecting to MCP servers"""
195
+
196
+ @tornado.web.authenticated
197
+ async def post(self):
198
+ """Connect to a specific MCP server"""
199
+ server_id = None
200
+ try:
201
+ logger.debug(f"[MCP Handler] Received connect request")
202
+ data = json.loads(self.request.body.decode('utf-8'))
203
+ server_id = data.get('server_id')
204
+
205
+ logger.debug(f"[MCP Handler] Request data: {data}")
206
+
207
+ if not server_id:
208
+ logger.warning(f"[MCP Handler] Missing server_id in request")
209
+ self.set_status(400)
210
+ self.finish(json.dumps({
211
+ 'error': 'server_id is required'
212
+ }))
213
+ return
214
+
215
+ logger.info(f"[MCP Handler] Attempting to connect to server: {server_id}")
216
+ mcp_service = get_mcp_service()
217
+ server_info = await mcp_service.connect(server_id)
218
+
219
+ logger.info(f"[MCP Handler] Successfully connected to {server_id}")
220
+ self.finish(json.dumps({
221
+ 'success': True,
222
+ 'server': server_info
223
+ }))
224
+ except json.JSONDecodeError as e:
225
+ logger.error(f"[MCP Handler] Invalid JSON in request body: {e}")
226
+ logger.error(f"[MCP Handler] Stack trace:\n{traceback.format_exc()}")
227
+ self.set_status(400)
228
+ self.finish(json.dumps({
229
+ 'error': f'Invalid JSON in request body: {str(e)}'
230
+ }))
231
+ except ValueError as e:
232
+ logger.error(f"[MCP Handler] ValueError for server {server_id}: {e}")
233
+ logger.error(f"[MCP Handler] Stack trace:\n{traceback.format_exc()}")
234
+ self.set_status(404)
235
+ self.finish(json.dumps({
236
+ 'error': str(e),
237
+ 'errorType': 'ValueError',
238
+ 'serverId': server_id
239
+ }))
240
+ except RuntimeError as e:
241
+ logger.error(f"[MCP Handler] RuntimeError connecting to {server_id}: {e}")
242
+ logger.error(f"[MCP Handler] Stack trace:\n{traceback.format_exc()}")
243
+ self.set_status(500)
244
+ self.finish(json.dumps({
245
+ 'error': str(e),
246
+ 'errorType': 'RuntimeError',
247
+ 'serverId': server_id,
248
+ 'details': 'Check server logs for detailed error information'
249
+ }))
250
+ except Exception as e:
251
+ error_type = type(e).__name__
252
+ error_msg = str(e)
253
+ logger.error(f"[MCP Handler] Unexpected error ({error_type}) connecting to {server_id}: {error_msg}")
254
+ logger.error(f"[MCP Handler] Full stack trace:\n{traceback.format_exc()}")
255
+ self.set_status(500)
256
+ self.finish(json.dumps({
257
+ 'error': error_msg,
258
+ 'errorType': error_type,
259
+ 'serverId': server_id,
260
+ 'stackTrace': traceback.format_exc(),
261
+ 'details': 'An unexpected error occurred. Check server logs for more information.'
262
+ }))
263
+
264
+
265
+ class MCPDisconnectHandler(APIHandler):
266
+ """Handler for disconnecting from MCP servers"""
267
+
268
+ @tornado.web.authenticated
269
+ async def post(self, server_id):
270
+ """Disconnect from a specific MCP server"""
271
+ try:
272
+ mcp_service = get_mcp_service()
273
+ success = await mcp_service.disconnect(server_id)
274
+
275
+ if success:
276
+ self.finish(json.dumps({
277
+ 'success': True,
278
+ 'message': f'Disconnected from server {server_id}'
279
+ }))
280
+ else:
281
+ self.set_status(404)
282
+ self.finish(json.dumps({
283
+ 'error': 'Server not found or not connected'
284
+ }))
285
+ except Exception as e:
286
+ logger.error(f"Error disconnecting from MCP server: {e}")
287
+ self.set_status(500)
288
+ self.finish(json.dumps({
289
+ 'error': str(e)
290
+ }))
291
+
292
+
293
+ class MCPToolsHandler(APIHandler):
294
+ """Handler for listing MCP tools"""
295
+
296
+ @tornado.web.authenticated
297
+ async def get(self, server_id):
298
+ """Get available tools from a connected MCP server"""
299
+ try:
300
+ mcp_service = get_mcp_service()
301
+ tools = await mcp_service.list_tools(server_id)
302
+
303
+ self.finish(json.dumps({
304
+ 'tools': tools
305
+ }))
306
+ except ValueError as e:
307
+ self.set_status(404)
308
+ self.finish(json.dumps({
309
+ 'error': str(e)
310
+ }))
311
+ except Exception as e:
312
+ logger.error(f"Error listing MCP tools: {e}")
313
+ self.set_status(500)
314
+ self.finish(json.dumps({
315
+ 'error': str(e)
316
+ }))
317
+
318
+
319
+ class MCPAllToolsHandler(APIHandler):
320
+ """Handler for getting all tools from all connected servers"""
321
+
322
+ @tornado.web.authenticated
323
+ async def get(self):
324
+ """Get all tools from all connected MCP servers"""
325
+ try:
326
+ mcp_service = get_mcp_service()
327
+ tools = await mcp_service.get_all_tools()
328
+
329
+ self.finish(json.dumps({
330
+ 'tools': tools
331
+ }))
332
+ except Exception as e:
333
+ logger.error(f"Error getting all MCP tools: {e}")
334
+ self.set_status(500)
335
+ self.finish(json.dumps({
336
+ 'error': str(e)
337
+ }))
338
+
339
+
340
+ class MCPToolCallHandler(APIHandler):
341
+ """Handler for calling MCP tools"""
342
+
343
+ @tornado.web.authenticated
344
+ async def post(self):
345
+ """Call a tool on an MCP server"""
346
+ try:
347
+ data = json.loads(self.request.body.decode('utf-8'))
348
+
349
+ server_id = data.get('server_id')
350
+ tool_name = data.get('tool_name')
351
+ arguments = data.get('arguments', {})
352
+
353
+ if not server_id or not tool_name:
354
+ self.set_status(400)
355
+ self.finish(json.dumps({
356
+ 'error': 'server_id and tool_name are required'
357
+ }))
358
+ return
359
+
360
+ # Workaround: Inject user_google_email for Google tools
361
+ # The MCP server requires this parameter even with --single-user mode
362
+ # See: https://github.com/taylorwilsdon/google_workspace_mcp/issues/338
363
+ token_store = get_oauth_token_store()
364
+ if token_store.is_oauth_server(server_id):
365
+ integration_id = token_store.get_integration_id(server_id)
366
+ if integration_id == 'google' and 'user_google_email' not in arguments:
367
+ oauth_env = token_store.get_tokens(server_id)
368
+ if oauth_env and 'USER_GOOGLE_EMAIL' in oauth_env:
369
+ arguments['user_google_email'] = oauth_env['USER_GOOGLE_EMAIL']
370
+
371
+ mcp_service = get_mcp_service()
372
+ result = await mcp_service.call_tool(server_id, tool_name, arguments)
373
+
374
+ self.finish(json.dumps({
375
+ 'success': True,
376
+ 'result': result
377
+ }))
378
+ except ValueError as e:
379
+ self.set_status(404)
380
+ self.finish(json.dumps({
381
+ 'error': str(e)
382
+ }))
383
+ except Exception as e:
384
+ logger.error(f"Error calling MCP tool: {e}")
385
+ self.set_status(500)
386
+ self.finish(json.dumps({
387
+ 'error': str(e)
388
+ }))
389
+
390
+
391
+ class MCPServerEnableHandler(APIHandler):
392
+ """Handler for enabling MCP servers"""
393
+
394
+ @tornado.web.authenticated
395
+ async def post(self, server_id):
396
+ """Enable an MCP server"""
397
+ try:
398
+ mcp_service = get_mcp_service()
399
+ success = mcp_service.enable_server(server_id)
400
+
401
+ if success:
402
+ self.finish(json.dumps({
403
+ 'success': True,
404
+ 'message': f'Server {server_id} enabled'
405
+ }))
406
+ else:
407
+ self.set_status(404)
408
+ self.finish(json.dumps({
409
+ 'error': 'Server not found'
410
+ }))
411
+ except Exception as e:
412
+ logger.error(f"Error enabling MCP server: {e}")
413
+ self.set_status(500)
414
+ self.finish(json.dumps({
415
+ 'error': str(e)
416
+ }))
417
+
418
+
419
+ class MCPServerDisableHandler(APIHandler):
420
+ """Handler for disabling MCP servers"""
421
+
422
+ @tornado.web.authenticated
423
+ async def post(self, server_id):
424
+ """Disable an MCP server"""
425
+ try:
426
+ mcp_service = get_mcp_service()
427
+ success = mcp_service.disable_server(server_id)
428
+
429
+ if success:
430
+ self.finish(json.dumps({
431
+ 'success': True,
432
+ 'message': f'Server {server_id} disabled'
433
+ }))
434
+ else:
435
+ self.set_status(404)
436
+ self.finish(json.dumps({
437
+ 'error': 'Server not found'
438
+ }))
439
+ except Exception as e:
440
+ logger.error(f"Error disabling MCP server: {e}")
441
+ self.set_status(500)
442
+ self.finish(json.dumps({
443
+ 'error': str(e)
444
+ }))
445
+
446
+
447
+ class MCPToolEnableHandler(APIHandler):
448
+ """Handler for enabling/disabling individual MCP tools"""
449
+
450
+ @tornado.web.authenticated
451
+ async def put(self, server_id, tool_name):
452
+ """Update enabled/disabled state for a specific tool"""
453
+ try:
454
+ data = json.loads(self.request.body.decode('utf-8'))
455
+ enabled = data.get('enabled', True)
456
+
457
+ mcp_service = get_mcp_service()
458
+ success = mcp_service.update_tool_enabled(server_id, tool_name, enabled)
459
+
460
+ if success:
461
+ self.finish(json.dumps({
462
+ 'success': True,
463
+ 'message': f'Tool {tool_name} {"enabled" if enabled else "disabled"}'
464
+ }))
465
+ else:
466
+ self.set_status(404)
467
+ self.finish(json.dumps({
468
+ 'error': 'Server not found'
469
+ }))
470
+ except Exception as e:
471
+ logger.error(f"Error updating tool enabled state: {e}")
472
+ self.set_status(500)
473
+ self.finish(json.dumps({
474
+ 'error': str(e)
475
+ }))
476
+
477
+
478
+ class MCPConfigFileHandler(APIHandler):
479
+ """Handler for managing the entire MCP config file"""
480
+
481
+ @tornado.web.authenticated
482
+ async def get(self):
483
+ """Get the raw JSON config file content"""
484
+ try:
485
+ logger.debug(f"[MCP ConfigFile Handler] GET request received")
486
+ mcp_service = get_mcp_service()
487
+ content = mcp_service.get_config_file_content()
488
+
489
+ # Ensure content is valid JSON string
490
+ if not content:
491
+ content = json.dumps({'mcpServers': {}}, indent=2)
492
+
493
+ # Validate it's valid JSON
494
+ try:
495
+ json.loads(content)
496
+ except json.JSONDecodeError as e:
497
+ logger.error(f"Config file content is not valid JSON: {e}")
498
+ content = json.dumps({'mcpServers': {}}, indent=2)
499
+
500
+ logger.debug(f"[MCP ConfigFile Handler] Returning config file content ({len(content)} chars)")
501
+ self.set_header('Content-Type', 'application/json; charset=utf-8')
502
+ self.finish(content)
503
+ except Exception as e:
504
+ logger.error(f"Error reading config file: {e}")
505
+ logger.error(f"Stack trace:\n{traceback.format_exc()}")
506
+ self.set_status(500)
507
+ self.set_header('Content-Type', 'application/json')
508
+ self.finish(json.dumps({
509
+ 'error': str(e)
510
+ }))
511
+
512
+ @tornado.web.authenticated
513
+ async def put(self):
514
+ """Update the entire config file with diff detection"""
515
+ try:
516
+ content = self.request.body.decode('utf-8')
517
+ mcp_service = get_mcp_service()
518
+
519
+ result = mcp_service.update_config_file(content)
520
+
521
+ self.finish(json.dumps(result))
522
+ except ValueError as e:
523
+ self.set_status(400)
524
+ self.finish(json.dumps({
525
+ 'error': str(e)
526
+ }))
527
+ except Exception as e:
528
+ logger.error(f"Error updating config file: {e}")
529
+ self.set_status(500)
530
+ self.finish(json.dumps({
531
+ 'error': str(e)
532
+ }))