-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy path__main__.py
More file actions
384 lines (316 loc) · 16.6 KB
/
__main__.py
File metadata and controls
384 lines (316 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
"""
AIDEFEND MCP Service - Unified Entry Point
This module provides a unified entry point for running the AIDEFEND service
in either REST API mode or MCP (Model Context Protocol) mode.
Usage:
python __main__.py # REST API mode (default)
python __main__.py --mcp # MCP mode for Claude Desktop
python __main__.py --help # Show help message
"""
import sys
import asyncio
def print_help():
"""Print usage information."""
help_text = """
AIDEFEND MCP Service - AI Security Defense Knowledge Base
USAGE:
python __main__.py [OPTIONS]
OPTIONS:
(no options) Start REST API server (default mode)
- Access at: http://127.0.0.1:8000
- API docs: http://127.0.0.1:8000/docs
- Health check: http://127.0.0.1:8000/api/v1/health
--api Start REST API server (explicit, same as no options)
- Use this for clarity in documentation or scripts
--mcp Start MCP server for Claude Desktop
- Uses stdio transport (standard input/output)
- Configure in Claude Desktop's config.json
- See INSTALL.md for setup instructions
--resync Delete existing database and resync from configured source
- Deletes data/aidefend_kb.lancedb and data/local_version.json
- Then exits (you can then start any mode)
- Use this when upgrading embedding models or fixing database issues
- Requires all running services to be stopped first
--force Use with --resync to force cleanup even if server is running
- WARNING: May cause running services to crash
- Only use if you're sure no important operations are in progress
--help, -h Show this help message
EXAMPLES:
# Start REST API server (for system integration)
python __main__.py
# Start MCP server (for Claude Desktop)
python __main__.py --mcp
# Resync database (when upgrading embedding models)
python __main__.py --resync
ENVIRONMENT:
Configuration is loaded from .env file (see .env.example)
DOCUMENTATION:
- README.md: Project overview and features
- INSTALL.md: Installation and configuration guide
- GitHub: https://github.com/edward-playground/aidefend-mcp
For more information, visit the documentation or run the service with --help.
"""
print(help_text)
def check_for_running_server() -> bool:
"""
Check if MCP server or other instance is currently running.
Uses cross-process lock detection to identify if another process
is holding the sync lock.
Returns:
True if server is running, False otherwise
"""
from app.config import settings
# Method 1: Check if lock file exists and is held by another process
lock_file = settings.DATA_PATH / "sync.lock"
if lock_file.exists():
try:
from app.sync import is_lock_held_by_other_process
if is_lock_held_by_other_process():
return True
except Exception:
# If we can't determine, assume it's safe to proceed
pass
return False
def main():
"""
Main entry point for AIDEFEND MCP Service.
Supports multiple modes:
1. REST API mode (default or --api): FastAPI server for HTTP queries
2. MCP mode (--mcp): stdio-based server for Claude Desktop integration
3. Resync mode (--resync): Delete database and resync from configured source
The mode is selected via command-line argument.
"""
# Handle resync first (cleanup, then exit)
if len(sys.argv) > 1 and sys.argv[1].lower() == "--resync":
print("🔄 Database Resync Mode", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("This will delete the existing database and force a fresh sync.", file=sys.stderr)
print("Use this when upgrading embedding models or fixing database issues.", file=sys.stderr)
print("=" * 60, file=sys.stderr)
# Check for --force flag
force_mode = len(sys.argv) > 2 and sys.argv[2].lower() == "--force"
try:
from app.config import settings
import shutil
# Step 1: Check if server is running (unless --force is used)
if not force_mode and check_for_running_server():
print("\n" + "=" * 60, file=sys.stderr)
print("⚠️ ERROR: AIDEFEND MCP Server is currently running!", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("\nResync requires exclusive access to the database.", file=sys.stderr)
print("Please stop all running instances first:\n", file=sys.stderr)
print(" 1. Close Claude Desktop (or other MCP clients)", file=sys.stderr)
print(" 2. Wait 5-10 seconds for graceful shutdown", file=sys.stderr)
print(" 3. Run resync again: python __main__.py --resync\n", file=sys.stderr)
print("Alternative: Use --force flag to override (⚠️ may cause data loss):", file=sys.stderr)
print(" python __main__.py --resync --force\n", file=sys.stderr)
sys.exit(1)
# Step 2: If force mode, remove lock file
if force_mode:
lock_file = settings.DATA_PATH / "sync.lock"
if lock_file.exists():
# First check lock file age for diagnostics
try:
from datetime import datetime
mtime = datetime.fromtimestamp(lock_file.stat().st_mtime)
age = datetime.now() - mtime
age_seconds = age.total_seconds()
print(f"⚠️ Force mode: Lock file age = {age_seconds:.1f} seconds", file=sys.stderr)
except Exception:
pass
# Check if lock is actively held by another process
from app.sync import is_lock_held_by_other_process
if is_lock_held_by_other_process():
print("\n" + "=" * 60, file=sys.stderr)
print("❌ ERROR: Lock is actively held by another process!", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("\n--force cannot override locks held by running processes.", file=sys.stderr)
print("\nTo proceed safely:", file=sys.stderr)
print(" 1. Stop the running AIDEFEND service first", file=sys.stderr)
print(" 2. Close Claude Desktop (if using MCP mode)", file=sys.stderr)
print(" 3. Wait 5-10 seconds for graceful shutdown", file=sys.stderr)
print(" 4. Run resync again: python __main__.py --resync --force\n", file=sys.stderr)
sys.exit(1)
# Try to remove lock file
print("⚠️ Force mode: Removing lock file", file=sys.stderr)
try:
lock_file.unlink()
print("✓ Lock file removed", file=sys.stderr)
# CRITICAL: Wait for file system to fully release the lock
# This prevents "lock age = 0.0 seconds" bug on Windows
print(" Waiting for lock to fully release...", file=sys.stderr)
import time
time.sleep(2)
print("✓ Lock released", file=sys.stderr)
except PermissionError as e:
# Windows-specific: File is locked by OS
print("\n" + "=" * 60, file=sys.stderr)
print("❌ ERROR: Cannot remove lock file (Windows file lock)", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print(f"\nError: {e}", file=sys.stderr)
print("\nOn Windows, locked files cannot be deleted even with --force.", file=sys.stderr)
print("The lock is held by another process (MCP server or sync operation).", file=sys.stderr)
print("\nTo proceed:", file=sys.stderr)
print(" 1. Close all AIDEFEND instances (Claude Desktop, REST API, etc.)", file=sys.stderr)
print(" 2. Wait 10-15 seconds", file=sys.stderr)
print(" 3. Run resync again: python __main__.py --resync --force\n", file=sys.stderr)
sys.exit(1)
except Exception as e:
# Other unexpected errors
print("\n" + "=" * 60, file=sys.stderr)
print(f"❌ ERROR: Failed to remove lock file: {e}", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("\nCannot proceed with resync.\n", file=sys.stderr)
sys.exit(1)
# Step 3: Acquire lock before deleting database
from app.sync import _acquire_sync_lock, _release_sync_lock
lock_acquired = asyncio.run(_acquire_sync_lock())
if not lock_acquired:
print("=" * 60, file=sys.stderr)
print("❌ Failed to acquire lock. Another sync may be in progress.", file=sys.stderr)
print("=" * 60, file=sys.stderr)
if force_mode:
# Already using --force, so provide different guidance
print("\nEven with --force, cannot acquire lock.", file=sys.stderr)
print("This indicates an active sync operation is in progress.", file=sys.stderr)
print("\nPlease wait for the sync to complete, or:", file=sys.stderr)
print(" 1. Stop all AIDEFEND instances", file=sys.stderr)
print(" 2. Wait 10-15 seconds", file=sys.stderr)
print(" 3. Try again\n", file=sys.stderr)
else:
# Not using --force yet
print("\nIf you're sure no sync is running, use --force flag:", file=sys.stderr)
print(" python __main__.py --resync --force\n", file=sys.stderr)
sys.exit(1)
try:
# Step 4: Delete database (now protected by lock)
if settings.DB_PATH.exists():
print(f"✓ Deleting database: {settings.DB_PATH.name}", file=sys.stderr)
shutil.rmtree(settings.DB_PATH)
else:
print(f" Database not found (already clean): {settings.DB_PATH.name}", file=sys.stderr)
# Delete version file
if settings.VERSION_FILE.exists():
print(f"✓ Deleting version file: {settings.VERSION_FILE.name}", file=sys.stderr)
settings.VERSION_FILE.unlink()
else:
print(f" Version file not found (already clean): {settings.VERSION_FILE.name}", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("✅ Cleanup complete! Starting fresh sync...", file=sys.stderr)
print("=" * 60, file=sys.stderr)
# Run sync WHILE holding the lock (prevents race conditions)
# This ensures users see progress in the same terminal
print("", file=sys.stderr)
print("📊 Running initial sync (this will take 5-15 minutes)...", file=sys.stderr)
print("=" * 60, file=sys.stderr)
# Import sync function
from app.sync import core_sync
from app.logger import setup_logger
import logging
# Setup logging with console output
setup_logger()
# Add console handler to show progress in terminal
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s',
datefmt='%H:%M:%S'
)
console_handler.setFormatter(console_formatter)
# Configure root logger to show INFO messages
# All child loggers (including 'app.sync') will inherit this configuration
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO) # Set root logger level to INFO
root_logger.addHandler(console_handler)
print("", file=sys.stderr)
# Run core_sync with force_rebuild=True
# We already hold the lock, so use core_sync() not run_sync()
sync_success = asyncio.run(core_sync(force_rebuild=True))
print("", file=sys.stderr)
if not sync_success:
from app.sync import get_last_sync_error
last_error = get_last_sync_error()
print("=" * 60, file=sys.stderr)
print("❌ Initial sync failed!", file=sys.stderr)
if last_error:
print(f" Error: {last_error}", file=sys.stderr)
print(" Check data/logs/aidefend_mcp.log for details", file=sys.stderr)
print("=" * 60, file=sys.stderr)
sys.exit(1)
print("=" * 60, file=sys.stderr)
print("✅ Sync complete!", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("", file=sys.stderr)
print("You can now start the service with:", file=sys.stderr)
print(" • MCP mode: python __main__.py --mcp", file=sys.stderr)
print(" • REST API mode: python __main__.py --api", file=sys.stderr)
print("", file=sys.stderr)
sys.exit(0)
finally:
# Step 5: Release lock after sync completes
_release_sync_lock()
except Exception as e:
print(f"❌ Error during resync: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
sys.exit(1)
# Parse command-line arguments
if len(sys.argv) > 1:
arg = sys.argv[1].lower()
# Help command
if arg in ["--help", "-h", "help"]:
print_help()
sys.exit(0)
# MCP mode
elif arg == "--mcp":
print("Starting AIDEFEND MCP Server (stdio mode)...", file=sys.stderr)
print("This server uses stdin/stdout for MCP protocol.", file=sys.stderr)
print("Configure Claude Desktop to connect to this server.", file=sys.stderr)
print("-" * 60, file=sys.stderr)
try:
from mcp_server import serve
asyncio.run(serve())
sys.exit(0)
except KeyboardInterrupt:
print("\nMCP Server stopped by user", file=sys.stderr)
sys.exit(0)
except Exception as e:
print(f"MCP Server error: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
sys.exit(1)
# --resync already handled above (exits after sync)
# --api falls through to REST API startup below
elif arg in ["--resync", "--api"]:
pass
# Unknown argument
else:
print(f"Error: Unknown argument '{sys.argv[1]}'", file=sys.stderr)
print("Use --help to see available options", file=sys.stderr)
sys.exit(1)
# Default: REST API mode (also reached via --api)
print("Starting AIDEFEND REST API Server...", file=sys.stderr)
print("API will be available at: http://127.0.0.1:8000", file=sys.stderr)
print("API documentation: http://127.0.0.1:8000/docs", file=sys.stderr)
print("-" * 60, file=sys.stderr)
try:
import uvicorn
from app.main import app
from app.config import settings
uvicorn.run(
app,
host=settings.API_HOST,
port=settings.API_PORT,
workers=settings.API_WORKERS,
log_level=settings.LOG_LEVEL.lower()
)
except KeyboardInterrupt:
print("\nREST API Server stopped by user", file=sys.stderr)
sys.exit(0)
except Exception as e:
print(f"REST API Server error: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()