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.
- signalpilot_ai_internal/_version.py +1 -1
- signalpilot_ai_internal/cache_service.py +22 -21
- signalpilot_ai_internal/composio_handlers.py +224 -0
- signalpilot_ai_internal/composio_service.py +511 -0
- signalpilot_ai_internal/database_config_handlers.py +182 -0
- signalpilot_ai_internal/database_config_service.py +166 -0
- signalpilot_ai_internal/databricks_schema_service.py +19 -14
- signalpilot_ai_internal/file_scanner_service.py +5 -146
- signalpilot_ai_internal/handlers.py +317 -8
- signalpilot_ai_internal/integrations_config.py +256 -0
- signalpilot_ai_internal/log_utils.py +31 -0
- signalpilot_ai_internal/mcp_handlers.py +33 -9
- signalpilot_ai_internal/mcp_service.py +94 -142
- signalpilot_ai_internal/oauth_token_store.py +141 -0
- signalpilot_ai_internal/schema_search_config.yml +17 -11
- signalpilot_ai_internal/schema_search_service.py +30 -10
- signalpilot_ai_internal/signalpilot_home.py +961 -0
- signalpilot_ai_internal/snowflake_schema_service.py +2 -0
- signalpilot_ai_internal/unified_database_schema_service.py +2 -0
- 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
- 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
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.bab318d6caadb055e29c.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/129.868ca665e6fc225c20a0.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/179.fd45a2e75d471d0aa3b9.js +7 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.81105a94aa873fc51a94.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.a002dd4630d3b6404a90.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.cc6f6ecacd703bcdb468.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.817a883549d55a0e0576.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.a4daecd44f1e9364e44a.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.667225aab294fb5ed161.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/447.8138af2522716e5a926f.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.925c73e32f3c07448da0.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/477.aaa4cc9e87801fb45f5b.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.370056149a59022b700c.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/510.868ca665e6fc225c20a0.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.835f97f7ccfc70ff5c93.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.6c13335f73de089d6b1e.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/574.ad2709e91ebcac5bbe68.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.bddbab8e464fe31f0393.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.fda1bcdb10497b0a6ade.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.d046701f475fcbf6697d.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.c306dffd4cfe8a613d13.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.e39898b6f336539f228c.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.77cc0ca10a1860df1b52.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.4e2850b2af985ed0d378.js +1 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/956.eeffe67d7781fd63ef4b.js +2 -0
- signalpilot_ai_internal-0.11.24.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.055f50d20a31f3068c72.js +1 -0
- {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
- {signalpilot_ai_internal-0.10.22.dist-info → signalpilot_ai_internal-0.11.24.dist-info}/METADATA +13 -31
- signalpilot_ai_internal-0.11.24.dist-info/RECORD +66 -0
- signalpilot_ai_internal-0.11.24.dist-info/licenses/LICENSE +7 -0
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/110.224e83db03814fd03955.js +0 -7
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/122.e2dadf63dc64d7b5f1ee.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/220.328403b5545f268b95c6.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/262.726e1da31a50868cb297.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/353.972abe1d2d66f083f9cc.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/364.dbec4c2dc12e7b050dcc.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/384.fa432bdb7fb6b1c95ad6.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/439.37e271d7a80336daabe2.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/476.ad22ccddd74ee306fb56.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/481.73c7a9290b7d35a8b9c1.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/512.b58fc0093d080b8ee61c.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/553.b4042a795c91d9ff71ef.js +0 -2
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/57.c4232851631fb2e7e59a.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/635.9720593ee20b768da3ca.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/713.8e6edc9a965bdd578ca7.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/726.318e4e791edb63cc788f.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/741.dc49867fafb03ea2ba4d.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/742.91e7b516c8699eea3373.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/785.2d75de1a8d2c3131a8db.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/801.ca9e114a30896b669a3c.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/880.d9914229e4f120e7e9e4.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/888.34054db17bcf6e87ec95.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/936.d80de1e4da5b520d2f3b.js +0 -1
- signalpilot_ai_internal-0.10.22.data/data/share/jupyter/labextensions/signalpilot-ai-internal/static/remoteEntry.b63c429ca81e743b403c.js +0 -1
- signalpilot_ai_internal-0.10.22.dist-info/RECORD +0 -56
- signalpilot_ai_internal-0.10.22.dist-info/licenses/LICENSE +0 -29
- {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
- {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
- {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
- /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
- {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
- {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 .
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
741
|
-
raise RuntimeError(f"Failed to write updated config
|
|
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
|
-
|
|
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:
|
|
2
|
+
level: "WARNING"
|
|
3
3
|
|
|
4
4
|
embedding:
|
|
5
|
-
location:
|
|
6
|
-
model:
|
|
7
|
-
metric:
|
|
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:
|
|
10
|
+
cache_dir: "/tmp/.schema_search_cache"
|
|
11
11
|
|
|
12
12
|
chunking:
|
|
13
|
-
strategy:
|
|
13
|
+
strategy: "raw" # Options: "raw", "llm"
|
|
14
14
|
max_tokens: 256
|
|
15
15
|
overlap_tokens: 50
|
|
16
|
-
model:
|
|
16
|
+
model: "gpt-4o-mini"
|
|
17
17
|
|
|
18
18
|
search:
|
|
19
|
-
strategy:
|
|
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
|
|
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
|