trace-sec 2.1.4 → 2.1.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trace-sec",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "Threat Reconnaissance & Attack-path Correlation Engine — Security Verification Layer for Coding Agents with Fine-Tuned SecureBERT 2.0 & Laya AI",
5
5
  "type": "module",
6
6
  "main": "bin/trace.js",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "trace-sec"
7
- version = "2.1.4"
7
+ version = "2.1.5"
8
8
  description = "Threat Reconnaissance & Attack-path Correlation Engine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -320,6 +320,10 @@ def scan(
320
320
  ):
321
321
  """Execute end-to-end scan: Ingestion -> APM -> Signals -> Runtime Tests -> Correlated Findings."""
322
322
  project_dir = path.resolve()
323
+ from trace_engine.intelligence.downloader import prompt_and_bootstrap_models
324
+ if not prompt_and_bootstrap_models(interactive=True):
325
+ raise typer.Exit(code=1)
326
+
323
327
  config = load_config(project_dir)
324
328
 
325
329
  console.print(f"\n[bold green]Starting TRACE Full Scan[/bold green] on [white]{project_dir.name}[/white]")
@@ -438,6 +442,11 @@ def test_all(
438
442
  from trace_engine.intelligence.orchestrator import IntelligenceOrchestrator
439
443
 
440
444
  project_dir = path.resolve()
445
+ if models:
446
+ from trace_engine.intelligence.downloader import prompt_and_bootstrap_models
447
+ if not prompt_and_bootstrap_models(interactive=True):
448
+ raise typer.Exit(code=1)
449
+
441
450
  config = load_config(project_dir)
442
451
 
443
452
  resolved_target = target if target else (config.target.url or "http://127.0.0.1:18080")
@@ -824,29 +833,53 @@ def verify(
824
833
 
825
834
 
826
835
  @app.command()
827
- def mcp():
828
- """Start the TRACE Model Context Protocol (MCP) server over stdio for Claude Code / Codex."""
836
+ def mcp(
837
+ server: bool = typer.Option(False, "--server", "-s", help="Run local HTTP/SSE server instead of stdio"),
838
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host interface to bind HTTP/SSE server"),
839
+ port: int = typer.Option(8765, "--port", "-p", help="Port to bind HTTP/SSE server"),
840
+ ):
841
+ """Start TRACE Model Context Protocol (MCP) server for Claude Code and coding agents."""
842
+ from trace_engine.mcp.server import TraceMCPServer
843
+
844
+ mcp_obj = TraceMCPServer()
845
+ if server:
846
+ mcp_obj.run_sse_server(host=host, port=port)
847
+ else:
848
+ mcp_obj.run_stdio()
849
+
850
+
851
+ @app.command(name="mcp-server")
852
+ @app.command(name="serve-mcp")
853
+ def mcp_server_cmd(
854
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host interface to bind HTTP/SSE server"),
855
+ port: int = typer.Option(8765, "--port", "-p", help="Port to bind HTTP/SSE server"),
856
+ ):
857
+ """Start local HTTP/SSE TRACE MCP server and display connection link for Claude Code."""
829
858
  from trace_engine.mcp.server import TraceMCPServer
830
859
 
831
- server = TraceMCPServer()
832
- server.run_stdio()
860
+ mcp_obj = TraceMCPServer()
861
+ mcp_obj.run_sse_server(host=host, port=port)
833
862
 
834
863
 
835
864
  @app.command("setup-mcp")
836
865
  def setup_mcp(
837
866
  path: Path = typer.Argument(Path("."), help="Path to project repository")
838
867
  ):
839
- """Generate .mcp.json and configure MCP coding agent integration (Section 64)."""
868
+ """Generate .mcp.json and configure MCP coding agent integration."""
840
869
  from trace_engine.mcp.config import generate_mcp_json
841
870
 
842
871
  project_dir = path.resolve()
843
872
  mcp_file = generate_mcp_json(project_dir)
844
873
 
845
874
  console.print(f"\n[bold green]Configured TRACE MCP[/bold green] in [white]{mcp_file}[/white]")
846
- console.print("\n[bold white]Connect to Claude Code:[/bold white]")
847
- console.print(" [green]claude mcp add trace -- cmd /c npx -y @trace-security/trace mcp[/green]")
848
- console.print("\n[bold white]Connect to Codex / Cursor / Antigravity:[/bold white]")
849
- console.print(" [dim]The generated .mcp.json is already recognized by compatible IDEs and CLI agents.[/dim]\n")
875
+ console.print("\n[bold white]Option A: Direct Stdio Integration in Claude Code:[/bold white]")
876
+ console.print(" [green]claude mcp add trace -- npx -y trace-sec mcp[/green]")
877
+ console.print("\n[bold white]Option B: Local HTTP/SSE Server Link (Real-Time Bridge):[/bold white]")
878
+ console.print(" 1. Launch server: [bold green]trace mcp-server[/bold green]")
879
+ console.print(" 2. Connect via CLI: [green]claude mcp add --transport sse trace http://127.0.0.1:8765/sse[/green]")
880
+ console.print(" 3. Or paste [cyan]http://127.0.0.1:8765/sse[/cyan] directly in your Claude Code chat!")
881
+ console.print("\n[bold white]Option C: Cursor IDE / Antigravity / Claude Code:[/bold white]")
882
+ console.print(" [dim]The created .mcp.json is already active in your workspace.[/dim]\n")
850
883
 
851
884
 
852
885
  @app.command()
@@ -885,6 +918,10 @@ def remediate(
885
918
  """Execute autonomous Agent Harness self-healing and remediation loop: patch vulnerabilities and verify fixes."""
886
919
  from trace_engine.harness.engine import AgentHarness
887
920
 
921
+ from trace_engine.intelligence.downloader import prompt_and_bootstrap_models
922
+ if not prompt_and_bootstrap_models(interactive=True):
923
+ raise typer.Exit(code=1)
924
+
888
925
  target_path = repo or path
889
926
  _, resolved_dir = resolve_finding_store(target_path.resolve())
890
927
  resolved_target = target or "http://127.0.0.1:18082"
@@ -1,4 +1,4 @@
1
- """Automated model downloader and provisioner for TRACE fine-tuned weights."""
1
+ """Automated model downloader, verification gate, and provisioner for TRACE fine-tuned weights."""
2
2
 
3
3
  import os
4
4
  import sys
@@ -17,19 +17,31 @@ MODEL_REGISTRY: Dict[str, Dict[str, Any]] = {
17
17
  "securebert-finetuned": {
18
18
  "files": {
19
19
  "config.json": {
20
- "url": f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/config.json",
20
+ "urls": [
21
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/securebert-finetuned/config.json",
22
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/config.json",
23
+ ],
21
24
  "min_size": 500,
22
25
  },
23
26
  "tokenizer.json": {
24
- "url": f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/tokenizer.json",
27
+ "urls": [
28
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/securebert-finetuned/tokenizer.json",
29
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/tokenizer.json",
30
+ ],
25
31
  "min_size": 1000000,
26
32
  },
27
33
  "tokenizer_config.json": {
28
- "url": f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/tokenizer_config.json",
34
+ "urls": [
35
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/securebert-finetuned/tokenizer_config.json",
36
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/tokenizer_config.json",
37
+ ],
29
38
  "min_size": 200,
30
39
  },
31
40
  "model.safetensors": {
32
- "url": f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/model.safetensors",
41
+ "urls": [
42
+ f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/securebert-finetuned/model.safetensors",
43
+ f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/securebert-finetuned/model.safetensors",
44
+ ],
33
45
  "min_size": 100000000, # ~498 MB
34
46
  },
35
47
  },
@@ -37,23 +49,38 @@ MODEL_REGISTRY: Dict[str, Dict[str, Any]] = {
37
49
  "laya-finetuned": {
38
50
  "files": {
39
51
  "laya_metadata.json": {
40
- "url": f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/laya_metadata.json",
52
+ "urls": [
53
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/laya-finetuned/laya_metadata.json",
54
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/laya_metadata.json",
55
+ ],
41
56
  "min_size": 300,
42
57
  },
43
58
  "tokenizer.json": {
44
- "url": f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/tokenizer.json",
59
+ "urls": [
60
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/laya-finetuned/tokenizer.json",
61
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/tokenizer.json",
62
+ ],
45
63
  "min_size": 500000,
46
64
  },
47
65
  "tokenizer_config.json": {
48
- "url": f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/tokenizer_config.json",
66
+ "urls": [
67
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/laya-finetuned/tokenizer_config.json",
68
+ f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/tokenizer_config.json",
69
+ ],
49
70
  "min_size": 200,
50
71
  },
51
72
  "laya_dual_head.onnx": {
52
- "url": f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/laya_dual_head.onnx",
73
+ "urls": [
74
+ f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/laya-finetuned/laya_dual_head.onnx",
75
+ f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/laya_dual_head.onnx",
76
+ ],
53
77
  "min_size": 50000000, # ~267 MB
54
78
  },
55
79
  "laya_dual_head.pt": {
56
- "url": f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/laya_dual_head.pt",
80
+ "urls": [
81
+ f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/models/laya-finetuned/laya_dual_head.pt",
82
+ f"https://media.githubusercontent.com/media/{REPO_OWNER}/{REPO_NAME}/{BRANCH}/.trace/models/laya-finetuned/laya_dual_head.pt",
83
+ ],
57
84
  "min_size": 50000000, # ~267 MB
58
85
  },
59
86
  },
@@ -88,6 +115,33 @@ def is_model_installed(model_name: str, candidate_dir: Optional[Path] = None) ->
88
115
  return True
89
116
 
90
117
 
118
+ def is_environment_ready() -> bool:
119
+ """Checks if both fine-tuned SecureBERT 2.0 and Laya System 1 models are installed and valid."""
120
+ repo_root = Path(__file__).resolve().parents[4]
121
+ candidate_roots = [
122
+ Path.home() / ".trace" / "models",
123
+ Path("models"),
124
+ Path(".trace/models"),
125
+ repo_root / "models",
126
+ repo_root / ".trace/models",
127
+ Path("D:/Startup/TRACE/models"),
128
+ Path("D:/Startup/TRACE/.trace/models"),
129
+ ]
130
+
131
+ sb_ok = False
132
+ laya_ok = False
133
+
134
+ for r in candidate_roots:
135
+ if not sb_ok and is_model_installed("securebert-finetuned", r / "securebert-finetuned"):
136
+ sb_ok = True
137
+ if not laya_ok and is_model_installed("laya-finetuned", r / "laya-finetuned"):
138
+ laya_ok = True
139
+ if sb_ok and laya_ok:
140
+ return True
141
+
142
+ return False
143
+
144
+
91
145
  def ensure_model(model_name: str) -> Path:
92
146
  """Ensures model is installed locally; downloads missing files from GitHub LFS/Repo if needed."""
93
147
  models_dir = get_default_models_dir()
@@ -109,42 +163,54 @@ def ensure_model(model_name: str) -> Path:
109
163
  continue
110
164
 
111
165
  temp_path = target_dir / f"{filename}.tmp"
112
- url = meta["url"]
166
+ urls = meta.get("urls", [])
113
167
  print(f" \033[38;2;255;158;59m›\033[0m Fetching {filename}...")
114
168
 
115
- try:
116
- req = urllib.request.Request(
117
- url,
118
- headers={"User-Agent": "TRACE-Security-Engine/2.1 (github:VK-Amogh/TRACE)"},
119
- )
120
- with urllib.request.urlopen(req, timeout=120) as resp:
121
- total_size = int(resp.headers.get("Content-Length", 0))
122
- downloaded = 0
123
- block_size = 1024 * 1024 # 1 MB chunk
124
-
125
- with open(temp_path, "wb") as f:
126
- while True:
127
- chunk = resp.read(block_size)
128
- if not chunk:
129
- break
130
- f.write(chunk)
131
- downloaded += len(chunk)
132
- if total_size > 0:
133
- percent = (downloaded / total_size) * 100
134
- mb_down = downloaded / (1024 * 1024)
135
- mb_tot = total_size / (1024 * 1024)
136
- sys.stdout.write(f"\r [{percent:.1f}%] {mb_down:.1f} MB / {mb_tot:.1f} MB")
137
- sys.stdout.flush()
138
-
139
- print()
140
- if temp_path.exists():
141
- temp_path.replace(file_path)
142
-
143
- except Exception as e:
144
- if temp_path.exists():
145
- temp_path.unlink()
146
- logger.warning(f"Failed to download {filename} from {url}: {e}")
147
- raise RuntimeError(f"Could not download {filename} for {model_name}: {e}")
169
+ success = False
170
+ last_error = None
171
+ for url in urls:
172
+ try:
173
+ req = urllib.request.Request(
174
+ url,
175
+ headers={"User-Agent": "TRACE-Security-Engine/2.1 (github:VK-Amogh/TRACE)"},
176
+ )
177
+ with urllib.request.urlopen(req, timeout=120) as resp:
178
+ total_size = int(resp.headers.get("Content-Length", 0))
179
+ downloaded = 0
180
+ block_size = 1024 * 1024 # 1 MB chunk
181
+
182
+ with open(temp_path, "wb") as f:
183
+ while True:
184
+ chunk = resp.read(block_size)
185
+ if not chunk:
186
+ break
187
+ f.write(chunk)
188
+ downloaded += len(chunk)
189
+ if total_size > 0:
190
+ percent = (downloaded / total_size) * 100
191
+ mb_down = downloaded / (1024 * 1024)
192
+ mb_tot = total_size / (1024 * 1024)
193
+ sys.stdout.write(f"\r [{percent:.1f}%] {mb_down:.1f} MB / {mb_tot:.1f} MB")
194
+ sys.stdout.flush()
195
+
196
+ print()
197
+ if temp_path.exists():
198
+ temp_path.replace(file_path)
199
+ success = True
200
+ break
201
+
202
+ except Exception as e:
203
+ last_error = e
204
+ if temp_path.exists():
205
+ try:
206
+ temp_path.unlink()
207
+ except Exception:
208
+ pass
209
+ continue
210
+
211
+ if not success:
212
+ logger.warning(f"Failed to download {filename}: {last_error}")
213
+ raise RuntimeError(f"Could not download {filename} for {model_name}: {last_error}")
148
214
 
149
215
  print(f"\033[38;2;16;185;129m✓ Successfully provisioned {model_name}\033[0m\n")
150
216
  return target_dir
@@ -154,3 +220,48 @@ def ensure_all_models() -> None:
154
220
  """Proactively ensures all fine-tuned models are downloaded and verified."""
155
221
  for model_name in MODEL_REGISTRY:
156
222
  ensure_model(model_name)
223
+
224
+
225
+ def prompt_and_bootstrap_models(interactive: bool = True) -> bool:
226
+ """Interactive gate: checks if models are installed, prompts user to agree and downloads them."""
227
+ if is_environment_ready():
228
+ return True
229
+
230
+ CYAN = "\033[38;2;56;189;248m"
231
+ GREEN = "\033[38;2;16;185;129m"
232
+ ORANGE = "\033[38;2;255;158;59m"
233
+ BOLD = "\033[1m"
234
+ DIM = "\033[2m"
235
+ RESET = "\033[0m"
236
+
237
+ print(f"\n {GREEN}┌{'─' * 74}┐{RESET}")
238
+
239
+ print(f" {GREEN}│{RESET} {BOLD}TRACE Neural Intelligence Engine — First-Time Environment Setup{RESET} {GREEN}│{RESET}")
240
+ print(f" {GREEN}├{'─' * 74}┤{RESET}")
241
+ print(f" {GREEN}│{RESET} TRACE requires fine-tuned neural models for autonomous AST attack-path {GREEN}│{RESET}")
242
+ print(f" {GREEN}│{RESET} correlation and vulnerability verification: {GREEN}│{RESET}")
243
+ print(f" {GREEN}│{RESET} • {CYAN}SecureBERT 2.0{RESET} (Fine-Tuned OWASP API Vulnerability Classifier) {GREEN}│{RESET}")
244
+ print(f" {GREEN}│{RESET} • {CYAN}Laya System 1{RESET} (Non-Autoregressive Fast Decision & Priority Router) {GREEN}│{RESET}")
245
+ print(f" {GREEN}│{RESET} {GREEN}│{RESET}")
246
+ print(f" {GREEN}│{RESET} {DIM}Dedicated Location : ~/.trace/models/{RESET} {GREEN}│{RESET}")
247
+ print(f" {GREEN}│{RESET} {DIM}Source : Official TRACE Git LFS Hub (github:VK-Amogh/TRACE){RESET} {GREEN}│{RESET}")
248
+ print(f" {GREEN}└{'─' * 74}┘{RESET}\n")
249
+
250
+ if not interactive or not sys.stdin.isatty():
251
+ print(f" {ORANGE}[!] Non-interactive environment detected. Run 'trace setup-models' to install.{RESET}\n")
252
+ return False
253
+
254
+ try:
255
+ ans = input(f" {ORANGE}›{RESET} {BOLD}Download and configure fine-tuned neural models now? (y/n) [y]:{RESET} ").strip().lower()
256
+ if ans in ("", "y", "yes"):
257
+ ensure_all_models()
258
+ print(f" {GREEN}✓ TRACE Neural Intelligence Engine initialized successfully.{RESET}\n")
259
+ return True
260
+ else:
261
+ print(f"\n {ORANGE}[!] TRACE setup cancelled.{RESET}")
262
+ print(f" {DIM}Fine-tuned neural intelligence models are required to perform security audits.{RESET}")
263
+ print(f" {DIM}Run 'trace setup-models' at any time to configure the engine.{RESET}\n")
264
+ return False
265
+ except (KeyboardInterrupt, EOFError):
266
+ print(f"\n {ORANGE}[!] Cancelled.{RESET}\n")
267
+ return False
@@ -58,9 +58,12 @@ class LayaDecisionEngine:
58
58
  """Resolves Laya model directory across project root, package dir, and home."""
59
59
  candidates = [
60
60
  Path.home() / ".trace/models/laya-finetuned",
61
+ Path("models/laya-finetuned"),
61
62
  Path(".trace/models/laya-finetuned"),
63
+ Path(__file__).resolve().parents[4] / "models/laya-finetuned",
62
64
  Path(__file__).resolve().parents[4] / ".trace/models/laya-finetuned",
63
65
  Path(__file__).resolve().parents[2] / "models/laya-finetuned",
66
+ Path("D:/Startup/TRACE/models/laya-finetuned"),
64
67
  Path("D:/Startup/TRACE/.trace/models/laya-finetuned"),
65
68
  ]
66
69
  repo_root = Path(__file__).resolve().parents[4]
@@ -35,9 +35,12 @@ class SecureBERTClassifier:
35
35
  ):
36
36
  candidates = [
37
37
  Path.home() / ".trace/models/securebert-finetuned",
38
+ Path("models/securebert-finetuned"),
38
39
  Path(".trace/models/securebert-finetuned"),
40
+ Path(__file__).resolve().parents[4] / "models/securebert-finetuned",
39
41
  Path(__file__).resolve().parents[4] / ".trace/models/securebert-finetuned",
40
42
  Path(__file__).resolve().parents[2] / "models/securebert-finetuned",
43
+ Path("D:/Startup/TRACE/models/securebert-finetuned"),
41
44
  Path("D:/Startup/TRACE/.trace/models/securebert-finetuned"),
42
45
  ]
43
46
  default_model = None
@@ -278,6 +278,10 @@ def run_interactive_story() -> None:
278
278
  """Executes the complete interactive, step-by-step security audit story."""
279
279
  render_big_banner(animated=True)
280
280
 
281
+ from trace_engine.intelligence.downloader import prompt_and_bootstrap_models
282
+ if not prompt_and_bootstrap_models(interactive=True):
283
+ return
284
+
281
285
  console.print(f" [{WHITE}]Welcome, Security Operator. TRACE stands ready to audit, verify, and heal codebases.[/{WHITE}]")
282
286
  console.print(f" [{DIM_WHITE}]Navigate using the options below (type 'exit' or 'q' at any prompt to quit).[/{DIM_WHITE}]\n")
283
287
 
@@ -288,26 +292,33 @@ def run_interactive_story() -> None:
288
292
  console.print(f" [{ORANGE}][1][/{ORANGE}] [{BOLD_WHITE}]Full Security Audit[/{BOLD_WHITE}] [{GREEN}]★ RECOMMENDED[/{GREEN}] [{DIM_WHITE}]─ Multi-language AST, APM graph, & Neural AI[/{DIM_WHITE}]")
289
293
  console.print(f" [{ORANGE}][2][/{ORANGE}] [{BOLD_WHITE}]Targeted Vulnerability Probes[/{BOLD_WHITE}] [{DIM_WHITE}]─ Select specific testpacks (BOLA, SQLi, SSRF, BFLA)[/{DIM_WHITE}]")
290
294
  console.print(f" [{ORANGE}][3][/{ORANGE}] [{BOLD_WHITE}]Autonomous AST Self-Healing[/{BOLD_WHITE}] [{DIM_WHITE}]─ Surgical refactoring with live rollback verification[/{DIM_WHITE}]")
291
- console.print(f" [{ORANGE}][4][/{ORANGE}] [{BOLD_WHITE}]How TRACE Works & Architecture Guide[/{BOLD_WHITE}] [{GREEN}]★ 5 Q&A[/{GREEN}] [{DIM_WHITE}]─ Interactive architectural diagram & neural models[/{DIM_WHITE}]")
292
- console.print(f" [{ORANGE}][5][/{ORANGE}] [{BOLD_WHITE}]Environment Diagnostics (Doctor)[/{BOLD_WHITE}] [{DIM_WHITE}]─ Inspect local security tools and dependencies[/{DIM_WHITE}]")
293
- console.print(f" [{ORANGE}][6][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]\n")
295
+ console.print(f" [{ORANGE}][4][/{ORANGE}] [{BOLD_WHITE}]Start Claude Code Local MCP Server[/{BOLD_WHITE}] [{GREEN}]★ LOCAL MCP[/{GREEN}] [{DIM_WHITE}]─ Run HTTP/SSE bridge for Claude Code chat[/{DIM_WHITE}]")
296
+ console.print(f" [{ORANGE}][5][/{ORANGE}] [{BOLD_WHITE}]How TRACE Works & Architecture Guide[/{BOLD_WHITE}] [{DIM_WHITE}]─ Interactive architectural diagram & neural models[/{DIM_WHITE}]")
297
+ console.print(f" [{ORANGE}][6][/{ORANGE}] [{BOLD_WHITE}]Environment Diagnostics (Doctor)[/{BOLD_WHITE}] [{DIM_WHITE}]─ Inspect local security tools and dependencies[/{DIM_WHITE}]")
298
+ console.print(f" [{ORANGE}][7][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]\n")
294
299
 
295
300
  choice = ask_input(
296
301
  f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Choose option[/{BOLD_WHITE}]",
297
- choices=["1", "2", "3", "4", "5", "6"],
302
+ choices=["1", "2", "3", "4", "5", "6", "7"],
298
303
  default="1",
299
304
  show_default=True,
300
305
  )
301
306
 
302
- if choice == "6":
307
+ if choice == "7":
303
308
  console.print(f"\n [{GREEN}]✓ TRACE session closed. Happy hacking![/{GREEN}]\n")
304
309
  return
305
310
 
306
311
  if choice == "4":
307
- run_architecture_guide()
312
+ from trace_engine.mcp.server import TraceMCPServer
313
+ mcp_obj = TraceMCPServer()
314
+ mcp_obj.run_sse_server(host="127.0.0.1", port=8765)
308
315
  return
309
316
 
310
317
  if choice == "5":
318
+ run_architecture_guide()
319
+ return
320
+
321
+ if choice == "6":
311
322
  display_story_header(2, 2, "Environment Diagnostics", "Auditing local dependencies and runtime requirements")
312
323
  from trace_engine.doctor import run_doctor
313
324
  report = run_doctor(Path("."))
@@ -396,3 +396,174 @@ class TraceMCPServer:
396
396
  }
397
397
  sys.stdout.write(json.dumps(err_resp) + "\n")
398
398
  sys.stdout.flush()
399
+
400
+ def run_sse_server(self, host: str = "127.0.0.1", port: int = 8765):
401
+ """Run an MCP HTTP/SSE transport server for Claude Code and local AI coding agents."""
402
+ import uuid
403
+ import queue
404
+ import urllib.parse
405
+ from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
406
+
407
+ sessions: Dict[str, queue.Queue] = {}
408
+ server_instance = self
409
+
410
+ class MCPHandler(BaseHTTPRequestHandler):
411
+ def log_message(self, format, *args):
412
+ pass # Suppress request clutter in terminal
413
+
414
+ def do_OPTIONS(self):
415
+ self.send_response(200)
416
+ self.send_header("Access-Control-Allow-Origin", "*")
417
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
418
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
419
+ self.end_headers()
420
+
421
+ def do_GET(self):
422
+ parsed = urllib.parse.urlparse(self.path)
423
+ if parsed.path == "/sse":
424
+ session_id = uuid.uuid4().hex
425
+ msg_queue: queue.Queue = queue.Queue()
426
+ sessions[session_id] = msg_queue
427
+
428
+ self.send_response(200)
429
+ self.send_header("Content-Type", "text/event-stream")
430
+ self.send_header("Cache-Control", "no-cache")
431
+ self.send_header("Connection", "keep-alive")
432
+ self.send_header("Access-Control-Allow-Origin", "*")
433
+ self.end_headers()
434
+
435
+ # Emit endpoint event as defined in MCP SSE transport specification
436
+ endpoint_msg = f"event: endpoint\r\ndata: /message?sessionId={session_id}\r\n\r\n"
437
+ self.wfile.write(endpoint_msg.encode("utf-8"))
438
+ self.wfile.flush()
439
+
440
+ try:
441
+ while server_instance.running:
442
+ try:
443
+ msg = msg_queue.get(timeout=1.0)
444
+ if msg is None:
445
+ break
446
+ event_str = f"event: message\r\ndata: {json.dumps(msg)}\r\n\r\n"
447
+ self.wfile.write(event_str.encode("utf-8"))
448
+ self.wfile.flush()
449
+ except queue.Empty:
450
+ self.wfile.write(b": keepalive\r\n\r\n")
451
+ self.wfile.flush()
452
+ except (BrokenPipeError, ConnectionResetError):
453
+ pass
454
+ finally:
455
+ sessions.pop(session_id, None)
456
+
457
+ elif parsed.path in ("/health", "/status"):
458
+ self.send_response(200)
459
+ self.send_header("Content-Type", "application/json")
460
+ self.send_header("Access-Control-Allow-Origin", "*")
461
+ self.end_headers()
462
+ status_info = {
463
+ "status": "online",
464
+ "server": "TRACE MCP Server v2.1",
465
+ "transport": "sse",
466
+ "sse_url": f"http://{host}:{port}/sse",
467
+ "active_sessions": len(sessions),
468
+ "tools": [t["name"] for t in TRACE_TOOLS],
469
+ }
470
+ self.wfile.write(json.dumps(status_info, indent=2).encode("utf-8"))
471
+
472
+ else:
473
+ self.send_response(200)
474
+ self.send_header("Content-Type", "text/html; charset=utf-8")
475
+ self.send_header("Access-Control-Allow-Origin", "*")
476
+ self.end_headers()
477
+ html = f"""<!DOCTYPE html>
478
+ <html>
479
+ <head>
480
+ <meta charset="utf-8">
481
+ <title>TRACE Local MCP Server</title>
482
+ <style>
483
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0b0f17; color: #f8fafc; padding: 40px; margin: 0; }}
484
+ .container {{ max-width: 680px; margin: 0 auto; background: #131b2e; border: 1px solid #10b981; border-radius: 12px; padding: 32px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }}
485
+ h1 {{ color: #10b981; margin-top: 0; display: flex; align-items: center; gap: 10px; font-size: 24px; }}
486
+ .badge {{ background: #10b981; color: #0b0f17; padding: 3px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }}
487
+ p {{ color: #94a3b8; font-size: 14px; line-height: 1.6; }}
488
+ .cmd-box {{ background: #070a12; border: 1px solid #1e293b; border-radius: 6px; padding: 12px 16px; margin: 12px 0; font-family: monospace; color: #38bdf8; font-size: 13px; word-break: break-all; }}
489
+ .section-title {{ color: #ffffff; font-size: 14px; font-weight: bold; margin-top: 20px; }}
490
+ </style>
491
+ </head>
492
+ <body>
493
+ <div class="container">
494
+ <h1>TRACE MCP Server <span class="badge">ONLINE</span></h1>
495
+ <p>Local Model Context Protocol (MCP) server listening for Claude Code and AI coding agents.</p>
496
+
497
+ <div class="section-title">Local MCP Endpoint Link:</div>
498
+ <div class="cmd-box">http://{host}:{port}/sse</div>
499
+
500
+ <div class="section-title">Connect via Claude Code CLI:</div>
501
+ <div class="cmd-box">claude mcp add --transport sse trace http://{host}:{port}/sse</div>
502
+
503
+ <div class="section-title">Connect in Claude Chat:</div>
504
+ <p>Paste the local MCP link into your Claude chat to grant autonomous AST scanning and verification powers:</p>
505
+ <div class="cmd-box">http://{host}:{port}/sse</div>
506
+ </div>
507
+ </body>
508
+ </html>"""
509
+ self.wfile.write(html.encode("utf-8"))
510
+
511
+ def do_POST(self):
512
+ parsed = urllib.parse.urlparse(self.path)
513
+ if parsed.path.startswith("/message"):
514
+ query = urllib.parse.parse_qs(parsed.query)
515
+ session_id = query.get("sessionId", [None])[0]
516
+
517
+ content_len = int(self.headers.get("Content-Length", 0))
518
+ body = self.rfile.read(content_len).decode("utf-8")
519
+ try:
520
+ req = json.loads(body)
521
+ resp = server_instance.handle_request(req)
522
+ except Exception as e:
523
+ resp = {
524
+ "jsonrpc": "2.0",
525
+ "id": None,
526
+ "error": {"code": -32603, "message": str(e)},
527
+ }
528
+
529
+ if session_id and session_id in sessions and resp:
530
+ sessions[session_id].put(resp)
531
+
532
+ self.send_response(200)
533
+ self.send_header("Content-Type", "application/json")
534
+ self.send_header("Access-Control-Allow-Origin", "*")
535
+ self.end_headers()
536
+ self.wfile.write(json.dumps(resp or {"jsonrpc": "2.0", "result": None}).encode("utf-8"))
537
+ else:
538
+ self.send_response(404)
539
+ self.end_headers()
540
+
541
+ GREEN = "\033[38;2;16;185;129m"
542
+ CYAN = "\033[38;2;56;189;248m"
543
+ ORANGE = "\033[38;2;255;158;59m"
544
+ BOLD = "\033[1m"
545
+ DIM = "\033[2m"
546
+ RESET = "\033[0m"
547
+
548
+ print(f"\n {GREEN}┌{'─' * 74}┐{RESET}")
549
+ print(f" {GREEN}│{RESET} {BOLD}TRACE Local MCP Server (Model Context Protocol){RESET} {GREEN}│{RESET}")
550
+ print(f" {GREEN}├{'─' * 74}┤{RESET}")
551
+ print(f" {GREEN}│{RESET} {BOLD}Local MCP Link :{RESET} {CYAN}http://{host}:{port}/sse{RESET}{' ' * max(0, 39 - len(f'http://{host}:{port}/sse'))}{GREEN}│{RESET}")
552
+ print(f" {GREEN}│{RESET} {BOLD}Web Dashboard :{RESET} {CYAN}http://{host}:{port}/{RESET}{' ' * max(0, 42 - len(f'http://{host}:{port}/'))}{GREEN}│{RESET}")
553
+ print(f" {GREEN}│{RESET} {BOLD}Status :{RESET} {GREEN}ONLINE{RESET} (Listening for Claude Code & AI Coding Agents) {GREEN}│{RESET}")
554
+ print(f" {GREEN}│{RESET} {GREEN}│{RESET}")
555
+ print(f" {GREEN}│{RESET} {BOLD}Connect in Claude Code CLI:{RESET} {GREEN}│{RESET}")
556
+ print(f" {GREEN}│{RESET} {ORANGE}›{RESET} {BOLD}claude mcp add --transport sse trace http://{host}:{port}/sse{RESET} {GREEN}│{RESET}")
557
+ print(f" {GREEN}│{RESET} {GREEN}│{RESET}")
558
+ print(f" {GREEN}│{RESET} {BOLD}Or paste this link directly in your Claude Code chat:{RESET} {GREEN}│{RESET}")
559
+ print(f" {GREEN}│{RESET} {CYAN}http://{host}:{port}/sse{RESET}{' ' * max(0, 49 - len(f'http://{host}:{port}/sse'))}{GREEN}│{RESET}")
560
+ print(f" {GREEN}└{'─' * 74}┘{RESET}\n")
561
+ print(f" {DIM}Press Ctrl+C to terminate the local MCP server.{RESET}\n")
562
+
563
+ httpd = ThreadingHTTPServer((host, port), MCPHandler)
564
+ try:
565
+ httpd.serve_forever()
566
+ except KeyboardInterrupt:
567
+ print(f"\n {ORANGE}[TRACE] MCP Server stopped.{RESET}\n")
568
+ finally:
569
+ httpd.server_close()