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
@@ -8,6 +8,7 @@ except ImportError:
8
8
  warnings.warn("Importing 'signalpilot_ai_internal' outside a proper installation.")
9
9
  __version__ = "dev"
10
10
  from .handlers import setup_handlers
11
+ from .mcp_server_manager import autostart_mcp_servers
11
12
 
12
13
 
13
14
  def _jupyter_labextension_paths():
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.10.0'
4
+ __version__ = VERSION = '0.11.24'
@@ -15,6 +15,9 @@ import uuid
15
15
  from pathlib import Path
16
16
  from typing import Any, Dict, List, Optional
17
17
 
18
+ # Import controlled print function
19
+ from .log_utils import print
20
+
18
21
 
19
22
  class CacheDirectoryManager:
20
23
  """OS-specific cache directory management with fallbacks"""
@@ -75,7 +78,7 @@ class CacheDirectoryManager:
75
78
  directories.append(Path(tempfile.gettempdir()) / f"signalpilot-ai-internal-{os.getuid() if hasattr(os, 'getuid') else 'user'}")
76
79
 
77
80
  except Exception as e:
78
- print(f"Error determining cache directories: {e}")
81
+ print(f"ERROR: determining cache directories: {e}")
79
82
  # Emergency fallback
80
83
  directories.append(Path(tempfile.gettempdir()) / "signalpilot-ai-internal-emergency")
81
84
 
@@ -93,7 +96,7 @@ class CacheDirectoryManager:
93
96
  test_file = cache_dir / f"test_write_{uuid.uuid4().hex[:8]}.tmp"
94
97
  test_file.write_text("test")
95
98
  test_file.unlink()
96
-
99
+
97
100
  print(f"Using cache directory: {cache_dir}")
98
101
  return cache_dir
99
102
 
@@ -111,14 +114,13 @@ class RobustFileOperations:
111
114
  @staticmethod
112
115
  def safe_write_json(file_path: Path, data: Any, max_retries: int = 3) -> bool:
113
116
  """Safely write JSON data with atomic operations and backups"""
114
- # print(f"Attempting to write JSON to: {file_path}")
115
117
 
116
118
  if not file_path.parent.exists():
117
119
  try:
118
120
  print(f"Creating parent directory: {file_path.parent}")
119
121
  file_path.parent.mkdir(parents=True, exist_ok=True)
120
122
  except Exception as e:
121
- print(f"Failed to create directory {file_path.parent}: {e}")
123
+ print(f"ERROR: Failed to create directory {file_path.parent}: {e}")
122
124
  return False
123
125
 
124
126
  # Create backup if file exists and is valid, but only if last backup is older than 1 hour
@@ -135,7 +137,6 @@ class RobustFileOperations:
135
137
  if should_create_backup:
136
138
  backup_path = file_path.with_suffix(f".backup.{int(time.time())}")
137
139
  shutil.copy2(file_path, backup_path)
138
- print(f"Created backup: {backup_path}")
139
140
 
140
141
  # Keep only the most recent backup that's at least 1 hour old
141
142
  RobustFileOperations._cleanup_backups(file_path)
@@ -183,7 +184,7 @@ class RobustFileOperations:
183
184
  shutil.copy2(backup_path, file_path)
184
185
  print(f"Restored {file_path} from backup")
185
186
  except Exception as restore_error:
186
- print(f"Failed to restore backup: {restore_error}")
187
+ print(f"ERROR: Failed to restore backup: {restore_error}")
187
188
 
188
189
  return False
189
190
 
@@ -203,7 +204,7 @@ class RobustFileOperations:
203
204
  with open(file_path, 'r', encoding='utf-8') as f:
204
205
  return json.load(f)
205
206
  except Exception as e:
206
- print(f"Failed to read {file_path}: {e}")
207
+ print(f"ERROR: Failed to read {file_path}: {e}")
207
208
 
208
209
  # Try to recover from backup
209
210
  backup_files = sorted(
@@ -224,7 +225,7 @@ class RobustFileOperations:
224
225
  shutil.copy2(backup_path, file_path)
225
226
  print(f"Restored {file_path} from {backup_path}")
226
227
  except Exception as restore_error:
227
- print(f"Could not restore main file: {restore_error}")
228
+ print(f"ERROR: Could not restore main file: {restore_error}")
228
229
 
229
230
  return data
230
231
 
@@ -254,7 +255,7 @@ class RobustFileOperations:
254
255
  return backup_age > 3600 # 3600 seconds = 1 hour
255
256
 
256
257
  except Exception as e:
257
- print(f"Error checking backup age: {e}")
258
+ print(f"ERROR: checking backup age: {e}")
258
259
  return True # If we can't check, err on the side of creating a backup
259
260
 
260
261
  @staticmethod
@@ -273,10 +274,10 @@ class RobustFileOperations:
273
274
  old_backup.unlink()
274
275
  print(f"Cleaned up old backup: {old_backup}")
275
276
  except Exception as cleanup_error:
276
- print(f"Failed to cleanup backup {old_backup}: {cleanup_error}")
277
+ print(f"ERROR: Failed to cleanup backup {old_backup}: {cleanup_error}")
277
278
 
278
279
  except Exception as e:
279
- print(f"Error cleaning up backups: {e}")
280
+ print(f"ERROR: cleaning up backups: {e}")
280
281
 
281
282
 
282
283
  class PersistentCacheService:
@@ -404,21 +405,21 @@ class PersistentCacheService:
404
405
  if not self.chat_histories_file:
405
406
  print(f"ERROR: Cannot save chat history for {chat_id} - no chat histories file configured")
406
407
  return False
407
-
408
+
408
409
  try:
409
410
  print(f"Attempting to save chat history for chat_id: {chat_id}")
410
411
  histories = self.get_chat_histories()
411
412
  print(f"Current histories count: {len(histories)}")
412
-
413
+
413
414
  histories[chat_id] = history
414
415
  print(f"Updated histories count: {len(histories)}")
415
-
416
+
416
417
  success = RobustFileOperations.safe_write_json(self.chat_histories_file, histories)
417
418
  if success:
418
419
  print(f"Successfully saved chat history for {chat_id}")
419
420
  else:
420
421
  print(f"ERROR: Failed to write chat history file for {chat_id}")
421
-
422
+
422
423
  return success
423
424
 
424
425
  except Exception as e:
@@ -584,7 +585,7 @@ class FileScanCacheManager:
584
585
  cache_path = self._get_file_cache_path(file_path)
585
586
  return RobustFileOperations.safe_read_json(cache_path, None)
586
587
  except Exception as e:
587
- print(f"Error reading file cache for {file_path}: {e}")
588
+ print(f"ERROR: reading file cache for {file_path}: {e}")
588
589
  return None
589
590
 
590
591
  def set_file_entry(self, file_path: str, entry: Dict[str, Any]) -> bool:
@@ -596,7 +597,7 @@ class FileScanCacheManager:
596
597
  cache_path = self._get_file_cache_path(file_path)
597
598
  return RobustFileOperations.safe_write_json(cache_path, entry)
598
599
  except Exception as e:
599
- print(f"Error writing file cache for {file_path}: {e}")
600
+ print(f"ERROR: writing file cache for {file_path}: {e}")
600
601
  return False
601
602
 
602
603
  def delete_file_entry(self, file_path: str) -> bool:
@@ -611,7 +612,7 @@ class FileScanCacheManager:
611
612
  return True
612
613
  return True # File doesn't exist, consider it deleted
613
614
  except Exception as e:
614
- print(f"Error deleting file cache for {file_path}: {e}")
615
+ print(f"ERROR: deleting file cache for {file_path}: {e}")
615
616
  return False
616
617
 
617
618
  def get_scanned_directories(self) -> List[Dict[str, Any]]:
@@ -622,7 +623,7 @@ class FileScanCacheManager:
622
623
  try:
623
624
  return RobustFileOperations.safe_read_json(self.scanned_directories_file, [])
624
625
  except Exception as e:
625
- print(f"Error reading scanned directories: {e}")
626
+ print(f"ERROR: reading scanned directories: {e}")
626
627
  return []
627
628
 
628
629
  def set_scanned_directories(self, directories: List[Dict[str, Any]]) -> bool:
@@ -633,7 +634,7 @@ class FileScanCacheManager:
633
634
  try:
634
635
  return RobustFileOperations.safe_write_json(self.scanned_directories_file, directories)
635
636
  except Exception as e:
636
- print(f"Error writing scanned directories: {e}")
637
+ print(f"ERROR: writing scanned directories: {e}")
637
638
  return False
638
639
 
639
640
  def clear_all_file_entries(self) -> bool:
@@ -648,7 +649,7 @@ class FileScanCacheManager:
648
649
  cache_file.unlink()
649
650
  return True
650
651
  except Exception as e:
651
- print(f"Error clearing file entries: {e}")
652
+ print(f"ERROR: clearing file entries: {e}")
652
653
  return False
653
654
 
654
655
  def get_cache_stats(self) -> Dict[str, Any]:
@@ -0,0 +1,224 @@
1
+ """
2
+ Composio Integration Handlers - Tornado HTTP handlers for OAuth integrations
3
+ Provides REST API for managing Composio OAuth integrations (Notion, Slack, Google)
4
+ """
5
+ import json
6
+ import logging
7
+ import traceback
8
+ import tornado
9
+ from jupyter_server.base.handlers import APIHandler
10
+ from .composio_service import get_composio_service
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Enable debug logging
15
+ logger.setLevel(logging.DEBUG)
16
+
17
+
18
+ class IntegrationsHandler(APIHandler):
19
+ """Handler for listing all integrations and their status"""
20
+
21
+ @tornado.web.authenticated
22
+ async def get(self):
23
+ """Get all available integrations with their connection status"""
24
+ try:
25
+ service = get_composio_service()
26
+ integrations = service.get_integrations()
27
+
28
+ self.finish(json.dumps({
29
+ 'integrations': integrations,
30
+ 'configured': service.is_configured(),
31
+ 'workerUrl': service.get_worker_url() if service.is_configured() else None,
32
+ }))
33
+ except Exception as e:
34
+ logger.error(f"Error getting integrations: {e}")
35
+ logger.error(traceback.format_exc())
36
+ self.set_status(500)
37
+ self.finish(json.dumps({
38
+ 'error': str(e)
39
+ }))
40
+
41
+
42
+ class IntegrationConnectHandler(APIHandler):
43
+ """Handler for initiating OAuth connection for an integration"""
44
+
45
+ @tornado.web.authenticated
46
+ async def post(self, integration_id):
47
+ """
48
+ Get worker URL and user ID for initiating OAuth connection.
49
+ Frontend will call the worker directly.
50
+ """
51
+ try:
52
+ service = get_composio_service()
53
+
54
+ if not service.is_configured():
55
+ self.set_status(400)
56
+ self.finish(json.dumps({
57
+ 'error': 'Composio worker URL not configured. Set COMPOSIO_WORKER_URL environment variable.',
58
+ 'configured': False,
59
+ }))
60
+ return
61
+
62
+ result = service.get_initiate_url(integration_id)
63
+
64
+ self.finish(json.dumps({
65
+ 'success': True,
66
+ 'workerUrl': result['workerUrl'],
67
+ 'userId': result['userId'],
68
+ }))
69
+ except ValueError as e:
70
+ self.set_status(400)
71
+ self.finish(json.dumps({
72
+ 'error': str(e)
73
+ }))
74
+ except Exception as e:
75
+ logger.error(f"Error initiating connection for {integration_id}: {e}")
76
+ logger.error(traceback.format_exc())
77
+ self.set_status(500)
78
+ self.finish(json.dumps({
79
+ 'error': str(e)
80
+ }))
81
+
82
+
83
+ class IntegrationCompleteHandler(APIHandler):
84
+ """Handler for completing OAuth connection after callback"""
85
+
86
+ @tornado.web.authenticated
87
+ async def post(self, integration_id):
88
+ """Complete OAuth connection and create MCP server"""
89
+ try:
90
+ service = get_composio_service()
91
+
92
+ if not service.is_configured():
93
+ self.set_status(400)
94
+ self.finish(json.dumps({
95
+ 'error': 'Composio worker URL not configured',
96
+ 'configured': False,
97
+ }))
98
+ return
99
+
100
+ result = await service.complete_connection(integration_id)
101
+
102
+ self.finish(json.dumps({
103
+ 'success': True,
104
+ **result,
105
+ }))
106
+ except ValueError as e:
107
+ self.set_status(400)
108
+ self.finish(json.dumps({
109
+ 'error': str(e)
110
+ }))
111
+ except Exception as e:
112
+ logger.error(f"Error completing connection for {integration_id}: {e}")
113
+ logger.error(traceback.format_exc())
114
+ self.set_status(500)
115
+ self.finish(json.dumps({
116
+ 'error': str(e)
117
+ }))
118
+
119
+
120
+ class IntegrationStatusHandler(APIHandler):
121
+ """Handler for checking connection status of an integration"""
122
+
123
+ @tornado.web.authenticated
124
+ async def get(self, integration_id):
125
+ """Check connection status for an integration"""
126
+ try:
127
+ service = get_composio_service()
128
+
129
+ if not service.is_configured():
130
+ self.set_status(400)
131
+ self.finish(json.dumps({
132
+ 'error': 'Composio worker URL not configured',
133
+ 'configured': False,
134
+ }))
135
+ return
136
+
137
+ result = await service.check_connection_status(integration_id)
138
+
139
+ self.finish(json.dumps({
140
+ 'success': True,
141
+ **result,
142
+ }))
143
+ except ValueError as e:
144
+ self.set_status(400)
145
+ self.finish(json.dumps({
146
+ 'error': str(e)
147
+ }))
148
+ except Exception as e:
149
+ logger.error(f"Error checking status for {integration_id}: {e}")
150
+ logger.error(traceback.format_exc())
151
+ self.set_status(500)
152
+ self.finish(json.dumps({
153
+ 'error': str(e)
154
+ }))
155
+
156
+
157
+ class IntegrationDisconnectHandler(APIHandler):
158
+ """Handler for disconnecting an integration"""
159
+
160
+ @tornado.web.authenticated
161
+ async def delete(self, integration_id):
162
+ """Disconnect an integration and remove MCP server"""
163
+ try:
164
+ service = get_composio_service()
165
+
166
+ result = await service.disconnect(integration_id)
167
+
168
+ self.finish(json.dumps({
169
+ 'success': True,
170
+ **result,
171
+ }))
172
+ except ValueError as e:
173
+ self.set_status(400)
174
+ self.finish(json.dumps({
175
+ 'error': str(e)
176
+ }))
177
+ except Exception as e:
178
+ logger.error(f"Error disconnecting {integration_id}: {e}")
179
+ logger.error(traceback.format_exc())
180
+ self.set_status(500)
181
+ self.finish(json.dumps({
182
+ 'error': str(e)
183
+ }))
184
+
185
+
186
+ class IntegrationRefreshHandler(APIHandler):
187
+ """Handler for manually refreshing OAuth tokens for an integration"""
188
+
189
+ @tornado.web.authenticated
190
+ async def post(self, integration_id):
191
+ """
192
+ Manually trigger token refresh for an integration.
193
+ Fetches fresh credentials from Composio and updates MCP server if tokens changed.
194
+ """
195
+ try:
196
+ service = get_composio_service()
197
+
198
+ if not service.is_configured():
199
+ self.set_status(400)
200
+ self.finish(json.dumps({
201
+ 'error': 'Composio worker URL not configured',
202
+ 'configured': False,
203
+ }))
204
+ return
205
+
206
+ result = await service.refresh_token(integration_id)
207
+
208
+ self.finish(json.dumps({
209
+ 'success': result.get('success', False),
210
+ 'tokens_updated': result.get('tokens_updated', False),
211
+ 'error': result.get('error'),
212
+ }))
213
+ except ValueError as e:
214
+ self.set_status(400)
215
+ self.finish(json.dumps({
216
+ 'error': str(e)
217
+ }))
218
+ except Exception as e:
219
+ logger.error(f"Error refreshing tokens for {integration_id}: {e}")
220
+ logger.error(traceback.format_exc())
221
+ self.set_status(500)
222
+ self.finish(json.dumps({
223
+ 'error': str(e)
224
+ }))