signalpilot-ai-internal 0.10.22__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 (83) hide show
  1. signalpilot_ai_internal/_version.py +1 -1
  2. signalpilot_ai_internal/cache_service.py +22 -21
  3. signalpilot_ai_internal/composio_handlers.py +224 -0
  4. signalpilot_ai_internal/composio_service.py +511 -0
  5. signalpilot_ai_internal/database_config_handlers.py +182 -0
  6. signalpilot_ai_internal/database_config_service.py +166 -0
  7. signalpilot_ai_internal/databricks_schema_service.py +19 -14
  8. signalpilot_ai_internal/file_scanner_service.py +5 -146
  9. signalpilot_ai_internal/handlers.py +317 -8
  10. signalpilot_ai_internal/integrations_config.py +256 -0
  11. signalpilot_ai_internal/log_utils.py +31 -0
  12. signalpilot_ai_internal/mcp_handlers.py +33 -9
  13. signalpilot_ai_internal/mcp_service.py +94 -142
  14. signalpilot_ai_internal/oauth_token_store.py +141 -0
  15. signalpilot_ai_internal/schema_search_config.yml +17 -11
  16. signalpilot_ai_internal/schema_search_service.py +30 -10
  17. signalpilot_ai_internal/signalpilot_home.py +961 -0
  18. signalpilot_ai_internal/snowflake_schema_service.py +2 -0
  19. signalpilot_ai_internal/unified_database_schema_service.py +2 -0
  20. signalpilot_ai_internal-0.10.22.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
  21. signalpilot_ai_internal-0.10.22.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
  22. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.bab318d6caadb055e29c.js +1 -0
  23. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/129.868ca665e6fc225c20a0.js +1 -0
  24. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/179.fd45a2e75d471d0aa3b9.js +7 -0
  25. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.81105a94aa873fc51a94.js +1 -0
  26. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.a002dd4630d3b6404a90.js +1 -0
  27. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.cc6f6ecacd703bcdb468.js +1 -0
  28. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.817a883549d55a0e0576.js +1 -0
  29. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.a4daecd44f1e9364e44a.js +1 -0
  30. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.667225aab294fb5ed161.js +1 -0
  31. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/447.8138af2522716e5a926f.js +1 -0
  32. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.925c73e32f3c07448da0.js +1 -0
  33. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/477.aaa4cc9e87801fb45f5b.js +1 -0
  34. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.370056149a59022b700c.js +1 -0
  35. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/510.868ca665e6fc225c20a0.js +1 -0
  36. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.835f97f7ccfc70ff5c93.js +1 -0
  37. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.6c13335f73de089d6b1e.js +1 -0
  38. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/574.ad2709e91ebcac5bbe68.js +1 -0
  39. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.bddbab8e464fe31f0393.js +1 -0
  40. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.fda1bcdb10497b0a6ade.js +1 -0
  41. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.d046701f475fcbf6697d.js +1 -0
  42. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.c306dffd4cfe8a613d13.js +1 -0
  43. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.e39898b6f336539f228c.js +1 -0
  44. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.77cc0ca10a1860df1b52.js +1 -0
  45. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.4e2850b2af985ed0d378.js +1 -0
  46. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/956.eeffe67d7781fd63ef4b.js +2 -0
  47. signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.055f50d20a31f3068c72.js +1 -0
  48. {signalpilot_ai_internal-0.10.22.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/third-party-licenses.json +29 -29
  49. {signalpilot_ai_internal-0.10.22.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/METADATA +13 -31
  50. signalpilot_ai_internal-0.11.24.dist-info/RECORD +66 -0
  51. signalpilot_ai_internal-0.11.24.dist-info/licenses/LICENSE +7 -0
  52. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/110.224e83db03814fd03955.js +0 -7
  53. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -1
  54. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -1
  55. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -1
  56. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.972abe1d2d66f083f9cc.js +0 -1
  57. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -1
  58. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -1
  59. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -1
  60. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.ad22ccddd74ee306fb56.js +0 -1
  61. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -1
  62. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -1
  63. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -2
  64. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.c4232851631fb2e7e59a.js +0 -1
  65. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -1
  66. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -1
  67. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/726.318e4e791edb63cc788f.js +0 -1
  68. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -1
  69. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -1
  70. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.2d75de1a8d2c3131a8db.js +0 -1
  71. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.ca9e114a30896b669a3c.js +0 -1
  72. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.d9914229e4f120e7e9e4.js +0 -1
  73. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -1
  74. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.d80de1e4da5b520d2f3b.js +0 -1
  75. signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.b63c429ca81e743b403c.js +0 -1
  76. signalpilot_ai_internal-0.10.22.dist-info/RECORD +0 -56
  77. signalpilot_ai_internal-0.10.22.dist-info/licenses/LICENSE +0 -29
  78. {signalpilot_ai_internal-0.10.22.data → signalpilot_ai_internal-0.11.24.data}/data/etc/jupyter/jupyter_server_config.d/signalpilot_ai.json +0 -0
  79. {signalpilot_ai_internal-0.10.22.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/install.json +0 -0
  80. {signalpilot_ai_internal-0.10.22.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/schemas/signalpilot-ai-internal/plugin.json +0 -0
  81. /signalpilot_ai_internal-0.10.22.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
  82. {signalpilot_ai_internal-0.10.22.data → signalpilot_ai_internal-0.11.24.data}/data/share/jupyter/labextensions/signalpilot-ai-internal/static/style.js +0 -0
  83. {signalpilot_ai_internal-0.10.22.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/WHEEL +0 -0
@@ -8,6 +8,7 @@ import traceback
8
8
  import tornado
9
9
  from jupyter_server.base.handlers import APIHandler
10
10
  from .mcp_service import get_mcp_service
11
+ from .oauth_token_store import get_oauth_token_store
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
@@ -23,8 +24,9 @@ class MCPServersHandler(APIHandler):
23
24
  """Get all configured MCP servers"""
24
25
  try:
25
26
  mcp_service = get_mcp_service()
27
+ token_store = get_oauth_token_store()
26
28
  configs = mcp_service.load_all_configs()
27
-
29
+
28
30
  # Add connection status to each server
29
31
  servers = []
30
32
  for server_id, config in configs.items():
@@ -33,13 +35,24 @@ class MCPServersHandler(APIHandler):
33
35
  'status': mcp_service.get_connection_status(server_id),
34
36
  'enabled': config.get('enabled', True)
35
37
  }
36
-
38
+
37
39
  # Add tool count if connected
38
40
  if server_id in mcp_service.tools_cache:
39
41
  server_info['toolCount'] = len(mcp_service.tools_cache[server_id])
40
-
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
+
41
54
  servers.append(server_info)
42
-
55
+
43
56
  self.finish(json.dumps({
44
57
  'servers': servers
45
58
  }))
@@ -326,27 +339,38 @@ class MCPAllToolsHandler(APIHandler):
326
339
 
327
340
  class MCPToolCallHandler(APIHandler):
328
341
  """Handler for calling MCP tools"""
329
-
342
+
330
343
  @tornado.web.authenticated
331
344
  async def post(self):
332
345
  """Call a tool on an MCP server"""
333
346
  try:
334
347
  data = json.loads(self.request.body.decode('utf-8'))
335
-
348
+
336
349
  server_id = data.get('server_id')
337
350
  tool_name = data.get('tool_name')
338
351
  arguments = data.get('arguments', {})
339
-
352
+
340
353
  if not server_id or not tool_name:
341
354
  self.set_status(400)
342
355
  self.finish(json.dumps({
343
356
  'error': 'server_id and tool_name are required'
344
357
  }))
345
358
  return
346
-
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
+
347
371
  mcp_service = get_mcp_service()
348
372
  result = await mcp_service.call_tool(server_id, tool_name, arguments)
349
-
373
+
350
374
  self.finish(json.dumps({
351
375
  'success': True,
352
376
  'result': result
@@ -19,7 +19,8 @@ import os
19
19
  from typing import Dict, List, Optional, Any
20
20
  from pathlib import Path
21
21
  import aiohttp
22
- from .cache_service import get_cache_service, RobustFileOperations
22
+ from .signalpilot_home import get_signalpilot_home
23
+ from .oauth_token_store import get_oauth_token_store
23
24
 
24
25
  logger = logging.getLogger(__name__)
25
26
 
@@ -96,10 +97,7 @@ class MCPConnectionService:
96
97
  def __init__(self):
97
98
  self.connections: Dict[str, 'MCPConnection'] = {}
98
99
  self.tools_cache: Dict[str, List[Dict]] = {}
99
- self.cache = get_cache_service()
100
- self.mcp_config_key = 'mcp_servers'
101
- self._migrated_from_cache = False
102
- self._ensure_migration()
100
+ self.home_manager = get_signalpilot_home()
103
101
 
104
102
  @classmethod
105
103
  def get_instance(cls):
@@ -108,22 +106,6 @@ class MCPConnectionService:
108
106
  cls._instance = MCPConnectionService()
109
107
  return cls._instance
110
108
 
111
- def get_mcp_config_path(self) -> Path:
112
- """Get the path to the MCP configuration JSON file (Cursor format)"""
113
- # Use the same cache directory as other SignalPilot files for consistency
114
- from .cache_service import CacheDirectoryManager
115
- cache_dir = CacheDirectoryManager.find_usable_cache_directory()
116
- if cache_dir:
117
- return cache_dir / 'mcp.json'
118
- else:
119
- # Fallback to old location if cache directory not available
120
- if platform.system() == 'Windows':
121
- config_dir = Path(os.environ.get('USERPROFILE', Path.home())) / '.signalpilot-ai-internal'
122
- else:
123
- config_dir = Path.home() / '.signalpilot-ai-internal'
124
- config_dir.mkdir(parents=True, exist_ok=True)
125
- return config_dir / 'mcp.json'
126
-
127
109
  def _infer_server_type(self, config: Dict) -> str:
128
110
  """Infer server type from config structure (Cursor format)"""
129
111
  if 'command' in config:
@@ -164,16 +146,17 @@ class MCPConnectionService:
164
146
  """Convert internal format to Cursor schema format"""
165
147
  # Remove internal-only fields
166
148
  storage_config = {}
167
-
149
+
168
150
  # Copy relevant fields based on type
169
151
  server_type = config.get('type', 'command')
170
-
152
+
171
153
  if server_type == 'command':
172
154
  if 'command' in config:
173
155
  storage_config['command'] = config['command']
174
156
  if 'args' in config:
175
157
  storage_config['args'] = config['args']
176
- if 'env' in config:
158
+ # Only store env if NOT an OAuth integration (OAuth tokens are stored securely elsewhere)
159
+ if 'env' in config and not config.get('isOAuthIntegration', False):
177
160
  storage_config['env'] = config['env']
178
161
  else: # http/sse
179
162
  if 'name' in config:
@@ -182,86 +165,37 @@ class MCPConnectionService:
182
165
  storage_config['url'] = config['url']
183
166
  if 'token' in config:
184
167
  storage_config['token'] = config['token']
185
-
168
+
186
169
  # Add enabled if not default (True)
187
170
  enabled = config.get('enabled', True)
188
171
  if not enabled:
189
172
  storage_config['enabled'] = False
190
-
173
+
191
174
  # Add enabledTools if present
192
175
  if 'enabledTools' in config:
193
176
  storage_config['enabledTools'] = config['enabledTools']
194
-
177
+
178
+ # Mark as OAuth integration if set (tokens stored securely elsewhere)
179
+ if config.get('isOAuthIntegration', False):
180
+ storage_config['isOAuthIntegration'] = True
181
+
195
182
  return storage_config
196
183
 
197
- def _ensure_migration(self):
198
- """Migrate from cache-based storage to JSON file if needed"""
199
- if self._migrated_from_cache:
200
- return
201
-
202
- config_path = self.get_mcp_config_path()
203
-
204
- # If JSON file already exists, migration already done
205
- if config_path.exists():
206
- self._migrated_from_cache = True
207
- return
208
-
209
- # Try to migrate from cache
210
- try:
211
- cache_data = self.cache.get_app_value(self.mcp_config_key)
212
- if cache_data:
213
- old_configs = json.loads(cache_data)
214
- if old_configs:
215
- logger.info(f"[MCP] Migrating {len(old_configs)} servers from cache to JSON file")
216
-
217
- # Convert to Cursor format
218
- cursor_format = {'mcpServers': {}}
219
- for server_id, config in old_configs.items():
220
- # Normalize old config
221
- if 'id' not in config:
222
- config['id'] = server_id
223
- if 'enabled' not in config:
224
- config['enabled'] = True
225
-
226
- # Convert to storage format
227
- storage_config = self._normalize_config_for_storage(config)
228
- cursor_format['mcpServers'][server_id] = storage_config
229
-
230
- # Write to JSON file
231
- if RobustFileOperations.safe_write_json(config_path, cursor_format):
232
- logger.info(f"[MCP] Successfully migrated to {config_path}")
233
- # Optionally delete old cache entry (commented for safety)
234
- # self.cache.delete_app_value(self.mcp_config_key)
235
- else:
236
- logger.error(f"[MCP] Failed to write migrated config to {config_path}")
237
- except Exception as e:
238
- logger.warning(f"[MCP] Migration from cache failed: {e}, starting fresh")
239
-
240
- self._migrated_from_cache = True
241
-
242
184
  def save_server_config(self, server_config: Dict) -> Dict:
243
185
  """Save MCP server configuration to JSON file (Cursor format)"""
244
186
  try:
245
187
  # Ensure server has an ID
246
188
  if 'id' not in server_config:
247
189
  server_config['id'] = str(uuid.uuid4())
248
-
190
+
249
191
  server_id = server_config['id']
250
-
251
- # Load existing configs
252
- config_path = self.get_mcp_config_path()
253
- cursor_data = RobustFileOperations.safe_read_json(config_path, {})
254
- mcp_servers = cursor_data.get('mcpServers', {})
255
-
256
- # Convert to storage format and add/update
192
+
193
+ # Convert to storage format and save
257
194
  storage_config = self._normalize_config_for_storage(server_config)
258
- mcp_servers[server_id] = storage_config
259
-
260
- # Write back to file
261
- cursor_data['mcpServers'] = mcp_servers
262
- if not RobustFileOperations.safe_write_json(config_path, cursor_data):
263
- raise RuntimeError(f"Failed to write MCP config to {config_path}")
264
-
195
+
196
+ if not self.home_manager.set_mcp_server(server_id, storage_config):
197
+ raise RuntimeError(f"Failed to write MCP config")
198
+
265
199
  logger.info(f"Saved MCP server config: {server_config.get('name', server_id)}")
266
200
  return server_config
267
201
  except Exception as e:
@@ -272,22 +206,14 @@ class MCPConnectionService:
272
206
  def load_all_configs(self) -> Dict[str, Dict]:
273
207
  """Load all MCP server configurations from JSON file (Cursor format)"""
274
208
  try:
275
- config_path = self.get_mcp_config_path()
276
-
277
- if not config_path.exists():
278
- return {}
279
-
280
- # Read JSON file
281
- cursor_data = RobustFileOperations.safe_read_json(config_path, {})
282
-
283
- # Extract mcpServers object
284
- mcp_servers = cursor_data.get('mcpServers', {})
285
-
209
+ # Get all MCP servers from home manager
210
+ mcp_servers = self.home_manager.get_mcp_servers()
211
+
286
212
  # Convert from Cursor format to internal format
287
213
  configs = {}
288
214
  for server_id, server_config in mcp_servers.items():
289
215
  configs[server_id] = self._normalize_config_from_storage(server_id, server_config)
290
-
216
+
291
217
  return configs
292
218
  except Exception as e:
293
219
  logger.error(f"Error loading MCP configs: {e}")
@@ -302,27 +228,12 @@ class MCPConnectionService:
302
228
  def delete_server_config(self, server_id: str) -> bool:
303
229
  """Delete a server configuration from JSON file"""
304
230
  try:
305
- config_path = self.get_mcp_config_path()
306
-
307
- if not config_path.exists():
308
- return False
309
-
310
- # Load current config
311
- cursor_data = RobustFileOperations.safe_read_json(config_path, {})
312
- mcp_servers = cursor_data.get('mcpServers', {})
313
-
314
- if server_id in mcp_servers:
315
- del mcp_servers[server_id]
316
-
317
- # Write back to file
318
- cursor_data['mcpServers'] = mcp_servers
319
- if not RobustFileOperations.safe_write_json(config_path, cursor_data):
320
- raise RuntimeError(f"Failed to write MCP config to {config_path}")
321
-
231
+ # Remove from home manager
232
+ if self.home_manager.remove_mcp_server(server_id):
322
233
  # Also disconnect if connected
323
234
  if server_id in self.connections:
324
235
  asyncio.create_task(self.disconnect(server_id))
325
-
236
+
326
237
  logger.info(f"Deleted MCP server config: {server_id}")
327
238
  return True
328
239
  return False
@@ -502,7 +413,15 @@ class MCPConnectionService:
502
413
  """Get server information for response"""
503
414
  tools = self.tools_cache.get(server_id, [])
504
415
  enabled_tools = config.get('enabledTools', [])
505
- return {
416
+
417
+ # Check if this is an OAuth integration
418
+ is_oauth = config.get('isOAuthIntegration', False)
419
+ if not is_oauth:
420
+ # Also check token store in case the flag wasn't set
421
+ token_store = get_oauth_token_store()
422
+ is_oauth = token_store.is_oauth_server(server_id)
423
+
424
+ result = {
506
425
  'serverId': server_id,
507
426
  'name': config.get('name', server_id),
508
427
  'status': self.get_connection_status(server_id),
@@ -512,6 +431,14 @@ class MCPConnectionService:
512
431
  'enabled': config.get('enabled', True),
513
432
  'enabledTools': enabled_tools
514
433
  }
434
+
435
+ # Add OAuth info if it's an OAuth integration
436
+ if is_oauth:
437
+ result['isOAuthIntegration'] = True
438
+ token_store = get_oauth_token_store()
439
+ result['integrationId'] = token_store.get_integration_id(server_id)
440
+
441
+ return result
515
442
 
516
443
  def enable_server(self, server_id: str) -> bool:
517
444
  """Enable an MCP server"""
@@ -678,67 +605,65 @@ class MCPConnectionService:
678
605
  def update_config_file(self, new_json_content: str) -> Dict[str, Any]:
679
606
  """Update the entire config file and apply diff-based changes"""
680
607
  try:
681
- config_path = self.get_mcp_config_path()
682
-
683
608
  # Parse new JSON
684
609
  try:
685
610
  new_data = json.loads(new_json_content)
686
611
  except json.JSONDecodeError as e:
687
612
  raise ValueError(f"Invalid JSON: {e}")
688
-
613
+
689
614
  if 'mcpServers' not in new_data:
690
615
  raise ValueError("JSON must contain 'mcpServers' object")
691
-
616
+
692
617
  new_servers = new_data.get('mcpServers', {})
693
-
618
+
694
619
  # Load current configs
695
620
  old_configs = self.load_all_configs()
696
621
  old_server_ids = set(old_configs.keys())
697
622
  new_server_ids = set(new_servers.keys())
698
-
623
+
699
624
  changes = {
700
625
  'added': [],
701
626
  'removed': [],
702
627
  'modified': [],
703
628
  'enabled_changes': []
704
629
  }
705
-
630
+
706
631
  # Detect added servers
707
632
  for server_id in new_server_ids - old_server_ids:
708
633
  changes['added'].append(server_id)
709
-
634
+
710
635
  # Detect removed servers
711
636
  for server_id in old_server_ids - new_server_ids:
712
637
  changes['removed'].append(server_id)
713
638
  # Disconnect removed servers
714
639
  if server_id in self.connections:
715
640
  asyncio.create_task(self.disconnect(server_id))
716
-
641
+
717
642
  # Detect modified servers
718
643
  for server_id in new_server_ids & old_server_ids:
719
644
  old_config = old_configs[server_id]
720
645
  new_storage_config = new_servers[server_id]
721
646
  new_config = self._normalize_config_from_storage(server_id, new_storage_config)
722
-
647
+
723
648
  # Check if enabled status changed
724
649
  old_enabled = old_config.get('enabled', True)
725
650
  new_enabled = new_config.get('enabled', True)
726
-
651
+
727
652
  if old_enabled != new_enabled:
728
653
  changes['enabled_changes'].append({
729
654
  'server_id': server_id,
730
655
  'old_enabled': old_enabled,
731
656
  'new_enabled': new_enabled
732
657
  })
733
-
658
+
734
659
  # Check if config changed (simple comparison)
735
660
  old_storage = self._normalize_config_for_storage(old_config)
736
661
  if old_storage != new_storage_config:
737
662
  changes['modified'].append(server_id)
738
-
739
- # Write new config to file
740
- if not RobustFileOperations.safe_write_json(config_path, new_data):
741
- raise RuntimeError(f"Failed to write updated config to {config_path}")
663
+
664
+ # Write new config to file using home manager
665
+ if not self.home_manager.write_mcp_config(new_data):
666
+ raise RuntimeError(f"Failed to write updated config")
742
667
 
743
668
  # Apply changes asynchronously
744
669
  async def apply_changes():
@@ -793,11 +718,7 @@ class MCPConnectionService:
793
718
  def get_config_file_content(self) -> str:
794
719
  """Get the raw JSON file content"""
795
720
  try:
796
- config_path = self.get_mcp_config_path()
797
- if not config_path.exists():
798
- return json.dumps({'mcpServers': {}}, indent=2)
799
-
800
- cursor_data = RobustFileOperations.safe_read_json(config_path, {'mcpServers': {}})
721
+ cursor_data = self.home_manager.read_mcp_config()
801
722
  return json.dumps(cursor_data, indent=2)
802
723
  except Exception as e:
803
724
  logger.error(f"Error reading config file: {e}")
@@ -850,10 +771,26 @@ class MCPCommandConnection(MCPConnection):
850
771
  command = self.config.get('command')
851
772
  args = self.config.get('args', [])
852
773
  env = self.config.get('env', {})
853
-
774
+
854
775
  if not command:
855
776
  raise ValueError("Command is required for command-based MCP")
856
-
777
+
778
+ # Check if this is an OAuth integration and inject tokens from secure store
779
+ is_oauth = self.config.get('isOAuthIntegration', False)
780
+ token_store = get_oauth_token_store()
781
+
782
+ # Also check token store directly in case flag is not set
783
+ if not is_oauth:
784
+ is_oauth = token_store.is_oauth_server(self.server_id)
785
+
786
+ if is_oauth:
787
+ oauth_env = token_store.get_tokens(self.server_id)
788
+ if oauth_env:
789
+ logger.debug(f"[MCP] Injecting OAuth tokens for server {self.server_id}")
790
+ env = {**env, **oauth_env} # OAuth tokens override any existing env vars
791
+ else:
792
+ logger.warning(f"[MCP] Server {self.server_id} is marked as OAuth but no tokens found in store")
793
+
857
794
  # Merge environment variables
858
795
  import os
859
796
  import shlex
@@ -1071,7 +1008,22 @@ class MCPCommandConnection(MCPConnection):
1071
1008
  logger.error(f"[MCP] Error during disconnect: {e}")
1072
1009
  finally:
1073
1010
  self.connected = False
1074
-
1011
+
1012
+ def is_connected(self) -> bool:
1013
+ """Check if connected - also verify subprocess is still alive"""
1014
+ if not self.connected:
1015
+ return False
1016
+ # Check if subprocess is still running
1017
+ if self.process is None:
1018
+ return False
1019
+ poll_result = self.process.poll()
1020
+ if poll_result is not None:
1021
+ # Process has exited, update connected flag
1022
+ logger.warning(f"[MCP] Process for {self.server_id} has exited with code {poll_result}")
1023
+ self.connected = False
1024
+ return False
1025
+ return True
1026
+
1075
1027
  async def list_tools(self) -> List[Dict]:
1076
1028
  """List tools via JSON-RPC"""
1077
1029
  try:
@@ -0,0 +1,141 @@
1
+ """
2
+ OAuth Token Store - Secure storage for OAuth tokens in .env format
3
+ Stores tokens at <cache_dir>/connect/.env
4
+ (e.g., ~/Library/Caches/SignalPilotAI/connect/.env on macOS)
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, Optional
9
+
10
+ from .signalpilot_home import get_signalpilot_home
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class OAuthTokenStore:
16
+ """
17
+ Secure storage for OAuth tokens using .env format.
18
+ Tokens are stored with server-id prefixes for namespacing.
19
+ """
20
+
21
+ _instance = None
22
+
23
+ def __init__(self):
24
+ self._home_manager = get_signalpilot_home()
25
+
26
+ @classmethod
27
+ def get_instance(cls) -> 'OAuthTokenStore':
28
+ """Get singleton instance."""
29
+ if cls._instance is None:
30
+ cls._instance = OAuthTokenStore()
31
+ return cls._instance
32
+
33
+ def store_tokens(self, integration_id: str, mcp_server_id: str, env_vars: Dict[str, str]):
34
+ """
35
+ Store OAuth tokens for an integration.
36
+
37
+ Args:
38
+ integration_id: The integration ID (e.g., 'notion', 'slack')
39
+ mcp_server_id: The MCP server ID associated with this integration
40
+ env_vars: Environment variables containing tokens
41
+ """
42
+ # Store the registry entry (mapping server_id -> integration_id)
43
+ self._home_manager.set_oauth_registry_entry(mcp_server_id, integration_id)
44
+
45
+ # Store the actual tokens
46
+ self._home_manager.set_oauth_tokens(mcp_server_id, env_vars)
47
+
48
+ logger.info(f"[OAuthTokenStore] Stored tokens for {mcp_server_id}")
49
+
50
+ def get_tokens(self, mcp_server_id: str) -> Optional[Dict[str, str]]:
51
+ """
52
+ Get OAuth tokens for an MCP server.
53
+
54
+ Args:
55
+ mcp_server_id: The MCP server ID
56
+
57
+ Returns:
58
+ Environment variables dict or None if not found
59
+ """
60
+ return self._home_manager.get_oauth_tokens(mcp_server_id)
61
+
62
+ def get_integration_id(self, mcp_server_id: str) -> Optional[str]:
63
+ """
64
+ Get the integration ID for an MCP server.
65
+
66
+ Args:
67
+ mcp_server_id: The MCP server ID
68
+
69
+ Returns:
70
+ Integration ID or None if not found
71
+ """
72
+ registry = self._home_manager.get_oauth_registry()
73
+ return registry.get(mcp_server_id)
74
+
75
+ def is_oauth_server(self, mcp_server_id: str) -> bool:
76
+ """
77
+ Check if an MCP server is an OAuth integration.
78
+
79
+ Args:
80
+ mcp_server_id: The MCP server ID
81
+
82
+ Returns:
83
+ True if this server has stored OAuth tokens
84
+ """
85
+ registry = self._home_manager.get_oauth_registry()
86
+ return mcp_server_id in registry
87
+
88
+ def remove_tokens(self, mcp_server_id: str) -> bool:
89
+ """
90
+ Remove OAuth tokens for an MCP server.
91
+
92
+ Args:
93
+ mcp_server_id: The MCP server ID
94
+
95
+ Returns:
96
+ True if tokens were removed, False if not found
97
+ """
98
+ # Remove from registry
99
+ self._home_manager.remove_oauth_registry_entry(mcp_server_id)
100
+
101
+ # Remove the tokens
102
+ result = self._home_manager.remove_oauth_tokens(mcp_server_id)
103
+
104
+ if result:
105
+ logger.info(f"[OAuthTokenStore] Removed tokens for {mcp_server_id}")
106
+
107
+ return result
108
+
109
+ def update_tokens(self, mcp_server_id: str, env_vars: Dict[str, str]) -> bool:
110
+ """
111
+ Update OAuth tokens for an existing MCP server.
112
+
113
+ Args:
114
+ mcp_server_id: The MCP server ID
115
+ env_vars: New environment variables containing tokens
116
+
117
+ Returns:
118
+ True if tokens were updated, False if server not found
119
+ """
120
+ if not self.is_oauth_server(mcp_server_id):
121
+ logger.warning(f"[OAuthTokenStore] Server {mcp_server_id} not found for update")
122
+ return False
123
+
124
+ result = self._home_manager.set_oauth_tokens(mcp_server_id, env_vars)
125
+ if result:
126
+ logger.info(f"[OAuthTokenStore] Updated tokens for {mcp_server_id}")
127
+ return result
128
+
129
+ def get_all_oauth_servers(self) -> Dict[str, str]:
130
+ """
131
+ Get mapping of all OAuth MCP server IDs to their integration IDs.
132
+
133
+ Returns:
134
+ Dict mapping mcp_server_id -> integration_id
135
+ """
136
+ return self._home_manager.get_oauth_registry()
137
+
138
+
139
+ def get_oauth_token_store() -> OAuthTokenStore:
140
+ """Get the singleton instance of the OAuth token store."""
141
+ return OAuthTokenStore.get_instance()
@@ -1,32 +1,38 @@
1
1
  logging:
2
- level: 'WARNING'
2
+ level: "WARNING"
3
3
 
4
4
  embedding:
5
- location: 'memory'
6
- model: 'multi-qa-MiniLM-L6-cos-v1'
7
- metric: 'cosine'
5
+ location: "memory" # Options: "memory", "vectordb" (coming soon)
6
+ model: "multi-qa-MiniLM-L6-cos-v1"
7
+ metric: "cosine" # Options: "cosine", "euclidean", "manhattan", "dot"
8
8
  batch_size: 32
9
9
  show_progress: false
10
- cache_dir: '/tmp/.schema_search_cache'
10
+ cache_dir: "/tmp/.schema_search_cache"
11
11
 
12
12
  chunking:
13
- strategy: 'raw'
13
+ strategy: "raw" # Options: "raw", "llm"
14
14
  max_tokens: 256
15
15
  overlap_tokens: 50
16
- model: 'gpt-4o-mini'
16
+ model: "gpt-4o-mini"
17
17
 
18
18
  search:
19
- strategy: 'bm25'
19
+ # Search strategy: "semantic" (embeddings), "bm25" (BM25 lexical), "fuzzy" (fuzzy string matching), "hybrid" (semantic + bm25)
20
+ strategy: "bm25"
20
21
  initial_top_k: 20
21
22
  rerank_top_k: 5
22
- semantic_weight: 0.67
23
- hops: 1
23
+ semantic_weight: 0.67 # For hybrid search (bm25_weight = 1 - semantic_weight)
24
+ hops: 1 # Number of foreign key hops for graph expansion (0-2 recommended)
24
25
 
25
26
  reranker:
26
- model: null
27
+ # CrossEncoder model for reranking. Set to null to disable reranking
28
+ model: null # "Alibaba-NLP/gte-reranker-modernbert-base"
27
29
 
28
30
  schema:
29
31
  include_columns: true
30
32
  include_indices: true
31
33
  include_foreign_keys: true
32
34
  include_constraints: true
35
+
36
+ output:
37
+ format: "markdown" # Options: "json", "markdown"
38
+ limit: 5 # Default number of results to return