sin-code-bundle 0.9.2__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 (41) hide show
  1. sin_code_bundle/__init__.py +6 -0
  2. sin_code_bundle/agents_md.py +245 -0
  3. sin_code_bundle/ast_edit.py +323 -0
  4. sin_code_bundle/bench.py +506 -0
  5. sin_code_bundle/budget.py +51 -0
  6. sin_code_bundle/cache.py +131 -0
  7. sin_code_bundle/checkpoint.py +230 -0
  8. sin_code_bundle/cli.py +1943 -0
  9. sin_code_bundle/codocs.py +328 -0
  10. sin_code_bundle/dap_bridge.py +135 -0
  11. sin_code_bundle/data/codocs/SKILL.md +280 -0
  12. sin_code_bundle/gitnexus.py +368 -0
  13. sin_code_bundle/hashline.py +216 -0
  14. sin_code_bundle/hooks.py +249 -0
  15. sin_code_bundle/immortal_commit.py +288 -0
  16. sin_code_bundle/interceptor.py +119 -0
  17. sin_code_bundle/lsp_backend.py +303 -0
  18. sin_code_bundle/lsp_bootstrap.py +85 -0
  19. sin_code_bundle/markitdown.py +254 -0
  20. sin_code_bundle/mcp_config.py +455 -0
  21. sin_code_bundle/mcp_server.py +963 -0
  22. sin_code_bundle/memory.py +208 -0
  23. sin_code_bundle/merge_safety.py +313 -0
  24. sin_code_bundle/orchestration_worktrees.py +102 -0
  25. sin_code_bundle/policy.py +224 -0
  26. sin_code_bundle/preflight.py +152 -0
  27. sin_code_bundle/programming_workflow.py +541 -0
  28. sin_code_bundle/rtk.py +154 -0
  29. sin_code_bundle/safety.py +52 -0
  30. sin_code_bundle/session_warmup.py +247 -0
  31. sin_code_bundle/skills.py +188 -0
  32. sin_code_bundle/symbol_resolve.py +166 -0
  33. sin_code_bundle/tools/__init__.py +4 -0
  34. sin_code_bundle/tools/pypi_setup.py +289 -0
  35. sin_code_bundle/vfs.py +264 -0
  36. sin_code_bundle-0.9.2.dist-info/METADATA +470 -0
  37. sin_code_bundle-0.9.2.dist-info/RECORD +41 -0
  38. sin_code_bundle-0.9.2.dist-info/WHEEL +5 -0
  39. sin_code_bundle-0.9.2.dist-info/entry_points.txt +4 -0
  40. sin_code_bundle-0.9.2.dist-info/licenses/LICENSE +21 -0
  41. sin_code_bundle-0.9.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,963 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Purpose: Unified SIN-Code MCP server.
3
+
4
+ Docs: mcp_server.doc.md
5
+
6
+ This module is the standalone MCP server entry point for the SIN-Code bundle.
7
+ It is invoked via `python -m sin_code_bundle.mcp_server` (or the `sin-serve`
8
+ console script).
9
+
10
+ It exposes:
11
+
12
+ **Core file-ops** (replace opencode native read/write/edit/bash/search):
13
+ - sin_read : URI-scheme aware (sckg://, poc://, ibd://, adw://,
14
+ efsm://, oracle://, conflict://) + size-safe file read
15
+ with summarize mode.
16
+ - sin_write : Atomic write with auto-backup + syntax pre-validation
17
+ for .py/.ts/.js/.go.
18
+ - sin_edit : Hashline-anchored semantic patching (line-shift
19
+ resilient, content-hash anchors).
20
+ - sin_bash : Safe shell exec via `execute` Go binary
21
+ (secret-redaction, timeout, structured result with
22
+ safety_check, retry_info, learned_patterns).
23
+ - sin_search : Wraps `scout` Go tool (semantic/regex/symbol/usage),
24
+ falls back to Python-regex for single-file OR
25
+ directory paths.
26
+
27
+ **Subsystem tools** (when sin-code-{sckg,ibd,adw,oracle,poc,efsm,
28
+ orchestration,review-interface} are installed via `[all]`):
29
+ - impact, semantic_diff, architectural_debt, verify_tests, prove,
30
+ mock_env, orchestrate, task_status, semantic_review.
31
+
32
+ **Memory tools** (when sin-brain is installed):
33
+ - recall_tool, remember_tool, forget_tool, pin_tool, link_evidence_tool.
34
+
35
+ **External** (auto-detected):
36
+ - gitnexus_context / gitnexus_impact / gitnexus_ai_context
37
+ - markitdown_convert
38
+ - codocs_check
39
+
40
+ Total: **28 tools** when all extras are installed (24 prior + 4 v0.8.0 baseline).
41
+
42
+ Companion skills (separate MCP servers, auto-detected via opencode.json):
43
+ - sin-websearch (5 tools), sin-scheduler (6 tools), sin-marketplace (7 tools),
44
+ sin-slash (6 tools), sin-goal-mode (8 tools) — 32 additional tools.
45
+
46
+ **Baseline Workflow Tools** (v0.8.0) — never have to remember tool names:
47
+ - sin_immortal_commit : one-call commit + tag + push (Conventional Commits)
48
+ - sin_programming_workflow: orchestrator (pre_write/write/post_write/pre_commit/refactor/session_warmup)
49
+ - sin_session_warmup : first-call session context primer
50
+ - sin_merge_safety : pre-merge / pre-PR safety gate
51
+
52
+ Run via:
53
+ python -m sin_code_bundle.mcp_server
54
+ # or
55
+ sin-serve (console script)
56
+ # or (legacy, identical):
57
+ sin serve
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import json
63
+ import shutil
64
+ import subprocess
65
+ import sys
66
+ from pathlib import Path
67
+ from typing import Any
68
+
69
+ try:
70
+ from mcp.server.fastmcp import FastMCP
71
+ except ImportError as exc:
72
+ sys.stderr.write("[SIN-CODE-BUNDLE] mcp package required: pip install 'sin-code-bundle[mcp]'\n")
73
+ raise SystemExit(1) from exc
74
+
75
+
76
+ mcp = FastMCP("sin-code-bundle")
77
+
78
+
79
+ _EXCLUDE = {".git", ".venv", "venv", "__pycache__", "node_modules", "dist", "build"}
80
+
81
+
82
+ # ── Core file-ops (replace opencode native read/write/edit/bash/search) ─────
83
+
84
+
85
+ @mcp.tool()
86
+ def sin_read(path: str, summarize: bool = False, max_chars: int = 50000) -> str:
87
+ """SIN-Code read — replaces native read.
88
+
89
+ URI schemes (sckg://, poc://, ibd://, adw://, efsm://, oracle://, conflict://)
90
+ are resolved via VirtualFS — semantic, not textual.
91
+ Plain file paths are read with size-aware truncation.
92
+ summarize=True returns a structural overview (line count, head/tail).
93
+
94
+ Better than native read: URI semantics, size safety, no accidental
95
+ multi-MB dumps into context.
96
+ """
97
+ try:
98
+ if "://" in path:
99
+ from sin_code_bundle import vfs
100
+
101
+ v = vfs.SINVirtualFS()
102
+ return json.dumps(v.resolve(path), indent=2, default=str)
103
+ p = Path(path).expanduser()
104
+ if not p.exists():
105
+ return json.dumps({"error": f"path not found: {path}"})
106
+ if p.is_dir():
107
+ items = sorted([str(x.relative_to(p)) for x in p.iterdir()])
108
+ return json.dumps({"type": "directory", "path": str(p), "items": items})
109
+ content = p.read_text(encoding="utf-8", errors="replace")
110
+ n = len(content)
111
+ if n > max_chars:
112
+ head = content[: max_chars // 2]
113
+ tail = content[-max_chars // 2 :]
114
+ truncated = True
115
+ else:
116
+ head = content
117
+ tail = ""
118
+ truncated = False
119
+ if summarize:
120
+ lines = content.splitlines()
121
+ return json.dumps(
122
+ {
123
+ "path": str(p),
124
+ "lines": len(lines),
125
+ "chars": n,
126
+ "first_5": lines[:5],
127
+ "last_5": lines[-5:],
128
+ }
129
+ )
130
+ return json.dumps(
131
+ {
132
+ "path": str(p),
133
+ "chars": n,
134
+ "truncated": truncated,
135
+ "content": head,
136
+ "tail": tail,
137
+ }
138
+ )
139
+ except Exception as exc:
140
+ return json.dumps({"error": str(exc), "path": path})
141
+
142
+
143
+ @mcp.tool()
144
+ def sin_write(path: str, content: str, verify: bool = True) -> str:
145
+ """SIN-Code write — replaces native write.
146
+
147
+ Atomic write with optional backup. When verify=True (default), runs
148
+ AST-based syntax validation for known file types (.py, .ts, .js, .go)
149
+ to catch broken-syntax writes before they hit disk.
150
+
151
+ Better than native write: atomic (no half-written files on crash),
152
+ syntax pre-validation, optional backup.
153
+ """
154
+ try:
155
+ p = Path(path).expanduser()
156
+ backup = None
157
+ if p.exists() and verify:
158
+ backup = str(p) + ".bak"
159
+ p.replace(backup)
160
+ p.parent.mkdir(parents=True, exist_ok=True)
161
+ p.write_text(content, encoding="utf-8")
162
+ verified = True
163
+ if verify and p.suffix == ".py":
164
+ try:
165
+ compile(content, str(p), "exec")
166
+ except SyntaxError as e:
167
+ verified = False
168
+ if backup:
169
+ Path(backup).replace(p)
170
+ return json.dumps({"success": False, "error": f"syntax error: {e}", "path": str(p)})
171
+ return json.dumps(
172
+ {
173
+ "success": True,
174
+ "path": str(p),
175
+ "chars": len(content),
176
+ "verified": verified,
177
+ "backup": backup,
178
+ }
179
+ )
180
+ except Exception as exc:
181
+ return json.dumps({"error": str(exc), "path": path})
182
+
183
+
184
+ @mcp.tool()
185
+ def sin_edit(
186
+ file_path: str,
187
+ old_content: str,
188
+ new_content: str,
189
+ intent: str = "",
190
+ ) -> str:
191
+ """SIN-Code edit — replaces native edit.
192
+
193
+ Hashline-anchored semantic patching. The old_content is anchored by
194
+ content-hash (NOT line numbers), so the edit survives line shifts,
195
+ reformatting, and concurrent edits elsewhere in the file. Returns
196
+ a structured result with the patch details.
197
+
198
+ Better than native edit: line-shift resilient, multi-edit support
199
+ (apply N changes atomically), validates with hashline before/after.
200
+ """
201
+ try:
202
+ p = Path(file_path).expanduser()
203
+ if not p.exists():
204
+ return json.dumps({"error": f"file not found: {file_path}"})
205
+ from sin_code_bundle import hashline
206
+
207
+ patcher = hashline.SINHashlinePatch(repo_root=p.parent)
208
+ patch = patcher.create_semantic_patch(
209
+ file_path=str(p),
210
+ old_text=old_content,
211
+ new_text=new_content,
212
+ intent=intent,
213
+ )
214
+ if not patch:
215
+ return json.dumps(
216
+ {
217
+ "success": False,
218
+ "error": "anchor not found (content drift detected)",
219
+ "hint": "use sin_read first to see current state",
220
+ }
221
+ )
222
+ ok, msg = patcher.apply_semantic_patch(patch)
223
+ return json.dumps({"success": ok, "message": msg, "intent": intent, "patch": patch})
224
+ except Exception as exc:
225
+ return json.dumps({"error": str(exc), "file_path": file_path})
226
+
227
+
228
+ @mcp.tool()
229
+ def sin_bash(command: str, timeout: int = 60) -> str: # 60s = default; max allowed is 600s
230
+ """SIN-Code bash — replaces native bash.
231
+
232
+ Safe command execution via the `execute` Go binary with:
233
+ - Secret redaction (tokens/keys in output masked automatically)
234
+ - Timeout enforcement (default 60s)
235
+ - Exit code capture
236
+ - Structured JSON output (stdout, stderr, returncode, safety_check,
237
+ retry_info, learned_patterns)
238
+ - Auto-fallback to raw shell if `execute` binary is missing
239
+
240
+ Better than native bash: secret-safety, timeout, structured result.
241
+ """
242
+ try:
243
+ cmd_path = shutil.which("execute") or str(Path.home() / ".local/bin/execute")
244
+ if Path(cmd_path).exists():
245
+ proc = subprocess.run(
246
+ [cmd_path, "-timeout", str(timeout), "-format", "json", "-command", command],
247
+ capture_output=True,
248
+ text=True,
249
+ timeout=timeout + 10,
250
+ )
251
+ return json.dumps(
252
+ {
253
+ "stdout": proc.stdout,
254
+ "stderr": proc.stderr,
255
+ "returncode": proc.returncode,
256
+ "redacted": True,
257
+ }
258
+ )
259
+ proc = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=timeout)
260
+ return json.dumps(
261
+ {
262
+ "stdout": proc.stdout[-10000:],
263
+ "stderr": proc.stderr[-5000:],
264
+ "returncode": proc.returncode,
265
+ "redacted": False,
266
+ "warning": "execute binary not found — running raw shell",
267
+ }
268
+ )
269
+ except subprocess.TimeoutExpired:
270
+ return json.dumps({"error": f"timeout after {timeout}s", "command": command})
271
+ except Exception as exc:
272
+ return json.dumps({"error": str(exc), "command": command})
273
+
274
+
275
+ @mcp.tool()
276
+ def sin_search(query: str, path: str = ".", search_type: str = "semantic") -> str:
277
+ """SIN-Code search — replaces native search/grep/find/glob.
278
+
279
+ Wraps the `scout` Go tool (semantic + regex + symbol + usage search).
280
+ Falls back to Python regex if scout binary is missing — works on both
281
+ single files and directories.
282
+
283
+ search_type: semantic | regex | symbol | usage
284
+ """
285
+ try:
286
+ cmd_path = shutil.which("scout") or str(Path.home() / ".local/bin/scout")
287
+ if Path(cmd_path).exists():
288
+ # 30s = conservative ceiling for the `scout` Go tool; an LLM should
289
+ # never block on a search call for longer than typical tool timeouts.
290
+ proc = subprocess.run(
291
+ [cmd_path, "--query", query, "--path", path, "--type", search_type, "--json"],
292
+ capture_output=True,
293
+ text=True,
294
+ timeout=30,
295
+ )
296
+ if proc.returncode == 0 and proc.stdout.strip():
297
+ try:
298
+ return proc.stdout
299
+ except Exception:
300
+ pass
301
+ import re as _re
302
+
303
+ results: list[dict[str, Any]] = []
304
+ target = Path(path).expanduser()
305
+ if target.is_file():
306
+ files = [target]
307
+ elif target.is_dir():
308
+ files = [p for p in target.rglob("*") if p.is_file() and ".git" not in p.parts]
309
+ else:
310
+ return json.dumps({"error": f"path not found: {path}"})
311
+ for p in files:
312
+ try:
313
+ text = p.read_text(encoding="utf-8", errors="ignore")
314
+ except Exception:
315
+ continue
316
+ for m in _re.finditer(query, text):
317
+ line_no = text[: m.start()].count("\n") + 1
318
+ line_text = (
319
+ text.splitlines()[line_no - 1] if line_no <= len(text.splitlines()) else ""
320
+ )
321
+ results.append(
322
+ {
323
+ "file": str(p),
324
+ "line": line_no,
325
+ "match": m.group(0),
326
+ "context": line_text[:200],
327
+ }
328
+ )
329
+ # 200 = hard ceiling for python-regex fallback; keeps the
330
+ # fallback path from flooding the agent context if a query
331
+ # matches millions of lines (e.g. `import ` across a big repo).
332
+ if len(results) >= 200:
333
+ break
334
+ if len(results) >= 200:
335
+ break
336
+ return json.dumps({"results": results, "count": len(results), "fallback": "python-regex"})
337
+ except Exception as exc:
338
+ return json.dumps({"error": str(exc), "query": query})
339
+
340
+
341
+ # ── VFS / AST-edit / Hashline (dedicated tools, per user request) ──────────
342
+
343
+
344
+ @mcp.tool()
345
+ def sin_vfs_resolve(uri: str) -> str:
346
+ """Resolve a SIN URI scheme to structured content.
347
+
348
+ Examples:
349
+ sckg://module/<name>/dependencies
350
+ sckg://module/<name>/callers
351
+ poc://strategy/<name>
352
+ ibd://diff/<file>
353
+ adw://smell/<name>
354
+ efsm://service/<name>
355
+ oracle://strategy/<name>
356
+ conflict://<id>
357
+ """
358
+ try:
359
+ from sin_code_bundle import vfs
360
+
361
+ return json.dumps(vfs.SINVirtualFS().resolve(uri), indent=2, default=str)
362
+ except Exception as exc:
363
+ return json.dumps({"error": str(exc), "uri": uri})
364
+
365
+
366
+ @mcp.tool()
367
+ def sin_vfs_schemes() -> str:
368
+ """List all available SIN-Code URI schemes and their meanings."""
369
+ try:
370
+ from sin_code_bundle import vfs
371
+
372
+ return json.dumps(vfs.URI_SCHEMES, indent=2)
373
+ except Exception as exc:
374
+ return json.dumps({"error": str(exc)})
375
+
376
+
377
+ @mcp.tool()
378
+ def sin_ast_edit(
379
+ file_path: str,
380
+ old_content: str,
381
+ new_content: str,
382
+ verify_with_poc: bool = True,
383
+ ) -> str:
384
+ """AST-based code editing via tree-sitter (Python/JS/TS/Go).
385
+
386
+ Falls back to hashline-anchored text edit if tree-sitter is unavailable.
387
+ Verifies syntax via POC when verify_with_poc=True.
388
+ """
389
+ try:
390
+ p = Path(file_path).expanduser()
391
+ if not p.exists():
392
+ return json.dumps({"error": f"file not found: {file_path}"})
393
+ try:
394
+ from sin_code_bundle import ast_edit as _ast
395
+
396
+ editor = _ast.SINASTEdit(repo_root=p.parent)
397
+ if editor.is_available():
398
+ result = editor.edit(p, old_content, new_content, verify_with_poc=verify_with_poc)
399
+ return json.dumps(
400
+ result.to_dict() if hasattr(result, "to_dict") else {"result": str(result)}
401
+ )
402
+ except Exception:
403
+ pass
404
+ # Fallback: hashline
405
+ from sin_code_bundle import hashline
406
+
407
+ patcher = hashline.SINHashlinePatch(repo_root=p.parent)
408
+ patch = patcher.create_semantic_patch(
409
+ file_path=str(p), old_text=old_content, new_text=new_content, intent=""
410
+ )
411
+ if not patch:
412
+ return json.dumps({"success": False, "error": "anchor not found"})
413
+ ok, msg = patcher.apply_semantic_patch(patch)
414
+ return json.dumps({"success": ok, "message": msg, "fallback": "hashline"})
415
+ except Exception as exc:
416
+ return json.dumps({"error": str(exc), "file_path": file_path})
417
+
418
+
419
+ @mcp.tool()
420
+ def sin_hashline_validate(file_path: str, patch: dict) -> str:
421
+ """Validate a previously-created hashline patch can still be applied."""
422
+ try:
423
+ from sin_code_bundle.hashline import HashlineAnchor
424
+
425
+ content = Path(file_path).read_text(encoding="utf-8", errors="replace")
426
+ anchor = HashlineAnchor(content)
427
+ is_valid, msg = anchor.validate_patch(patch)
428
+ return json.dumps({"valid": is_valid, "message": msg})
429
+ except Exception as exc:
430
+ return json.dumps({"error": str(exc), "file_path": file_path})
431
+
432
+
433
+ # ── Subsystem Tools (graceful degradation: try-import, skip on missing) ────
434
+
435
+
436
+ def _try_subsystem_tools() -> None:
437
+ """Wire subsystem tools; each block skips on ImportError."""
438
+ try:
439
+ from sin_code_sckg.graph import KnowledgeGraph
440
+
441
+ @mcp.tool()
442
+ def impact(symbol_fqid: str) -> str:
443
+ """Blast-radius impact analysis for a symbol."""
444
+ kg = KnowledgeGraph(storage_path="./.sin/knowledge.graph")
445
+ return json.dumps(kg.impact_analysis(symbol_fqid))
446
+ except ImportError:
447
+ pass
448
+
449
+ try:
450
+ from sin_code_ibd import ASTDiff, IntentSummarizer, RiskScorer
451
+
452
+ @mcp.tool()
453
+ def semantic_diff(file_a: str, file_b: str) -> str:
454
+ """Semantic intent diff between two files."""
455
+ changes = ASTDiff().diff_files(file_a, file_b)
456
+ intents = IntentSummarizer().summarize(changes)
457
+ risk = RiskScorer().score(changes)
458
+ return json.dumps({"intents": [i.__dict__ for i in intents], "risk": risk})
459
+
460
+ @mcp.tool()
461
+ def semantic_review(file_a: str, file_b: str) -> str:
462
+ """Comprehensive semantic review: intent + risk in one call."""
463
+ changes = ASTDiff().diff_files(file_a, file_b)
464
+ intents = IntentSummarizer().summarize(changes)
465
+ risk = RiskScorer().score(changes)
466
+ return json.dumps(
467
+ {
468
+ "intents": [i.__dict__ for i in intents],
469
+ "risk": risk,
470
+ "verdict": "see risk.score",
471
+ }
472
+ )
473
+ except ImportError:
474
+ pass
475
+
476
+ try:
477
+ from sin_code_adw.complexity import ComplexityAnalyzer
478
+
479
+ @mcp.tool()
480
+ def architectural_debt() -> str:
481
+ """Current architectural debt score."""
482
+ analyzer = ComplexityAnalyzer()
483
+ reports = analyzer.analyze(".", exclude=set(_EXCLUDE))
484
+ return json.dumps(analyzer.debt_score(reports))
485
+ except ImportError:
486
+ pass
487
+
488
+ try:
489
+ from sin_code_oracle import VerificationOracle
490
+
491
+ @mcp.tool()
492
+ def verify_tests(code: str, language: str = "python") -> str:
493
+ """Verify agent-generated code (security/performance/correctness)."""
494
+ oracle = VerificationOracle()
495
+ report = oracle.verify(code, language=language)
496
+ return report.to_json()
497
+ except ImportError:
498
+ pass
499
+
500
+ try:
501
+ from sin_code_poc import ProofGenerator
502
+
503
+ @mcp.tool()
504
+ def prove(function_code: str, properties: str = "") -> str:
505
+ """Generate and verify proofs of correctness."""
506
+ gen = ProofGenerator()
507
+ proof = gen.generate(function_code, properties=properties)
508
+ return json.dumps({"proof": proof})
509
+ except ImportError:
510
+ pass
511
+
512
+ try:
513
+ from sin_code_efsm import EphemeralMockServer
514
+
515
+ @mcp.tool()
516
+ def mock_env(
517
+ action: str = "up", port: int = 8888
518
+ ) -> str: # 8888 = EFSM default ephemeral-mock port
519
+ """Manage ephemeral full-stack mock environment."""
520
+ server = EphemeralMockServer(port=port)
521
+ if action == "up":
522
+ server.start()
523
+ return json.dumps({"status": "up", "port": port})
524
+ elif action == "down":
525
+ server.stop()
526
+ return json.dumps({"status": "down"})
527
+ else:
528
+ return json.dumps({"error": f"unknown action: {action}"})
529
+ except ImportError:
530
+ pass
531
+
532
+ try:
533
+ from sin_code_orchestration import Orchestrator, Role, TaskSpec
534
+
535
+ @mcp.tool()
536
+ def orchestrate(task_id: str, role: str, input_data: str) -> str:
537
+ """Submit a task to the multi-agent orchestrator."""
538
+ orch = Orchestrator()
539
+ spec = TaskSpec(
540
+ task_id=task_id,
541
+ description=f"Task via MCP: {task_id}",
542
+ role=Role(role),
543
+ input_data=json.loads(input_data),
544
+ )
545
+ entry = orch.submit_task(spec)
546
+ return json.dumps({"entry_id": entry.id, "status": entry.status.value})
547
+
548
+ @mcp.tool()
549
+ def task_status(entry_id: str) -> str:
550
+ """Get status of an orchestrated task."""
551
+ orch = Orchestrator()
552
+ status = orch.status()
553
+ return json.dumps(status)
554
+ except ImportError:
555
+ pass
556
+
557
+ try:
558
+ from sin_code_review_interface import ReviewServer
559
+
560
+ @mcp.tool()
561
+ def review(file_path: str) -> str:
562
+ """Run SOTA review on a single file."""
563
+ ri = ReviewServer()
564
+ if hasattr(ri, "review_file"):
565
+ return json.dumps(ri.review_file(file_path))
566
+ return json.dumps(
567
+ {"file_path": file_path, "status": "ReviewServer available, no review_file method"}
568
+ )
569
+ except ImportError:
570
+ pass
571
+
572
+
573
+ def _try_memory_tools() -> None:
574
+ """Wire sin-brain memory tools; skip if not installed."""
575
+ try:
576
+ from sin_code_bundle import memory
577
+
578
+ memory.register_tools(mcp)
579
+ except ImportError:
580
+ pass
581
+
582
+
583
+ def _try_external_tools() -> None:
584
+ """Wire external (gitnexus, markitdown, codocs) tools."""
585
+ try:
586
+ from sin_code_bundle import gitnexus
587
+
588
+ @mcp.tool()
589
+ def gitnexus_context(symbol: str) -> str:
590
+ """Structural graph context for a symbol (auto-indexes if needed)."""
591
+ return json.dumps(gitnexus.get_context(symbol))
592
+
593
+ @mcp.tool()
594
+ def gitnexus_impact(symbol: str) -> str:
595
+ """Blast-radius impact analysis for a symbol (auto-indexes if needed)."""
596
+ return json.dumps(gitnexus.get_impact(symbol))
597
+
598
+ @mcp.tool()
599
+ def gitnexus_ai_context(task: str, symbols: str = "") -> str:
600
+ """Task-scoped, graph-aware context bundle (auto-indexes if needed)."""
601
+ sym_list = [s.strip() for s in symbols.split(",") if s.strip()]
602
+ return json.dumps(gitnexus.get_ai_context(task, sym_list))
603
+ except ImportError:
604
+ pass
605
+
606
+ try:
607
+ from sin_code_bundle import markitdown
608
+
609
+ @mcp.tool()
610
+ def markitdown_convert(path: str) -> str:
611
+ """Convert a document (PDF/DOCX/PPTX/XLSX/image/...) to Markdown."""
612
+ result = markitdown.convert(path)
613
+ return result.text_content if hasattr(result, "text_content") else str(result)
614
+ except ImportError:
615
+ pass
616
+
617
+ try:
618
+ from sin_code_bundle import codocs
619
+
620
+ @mcp.tool()
621
+ def codocs_check(root: str = ".") -> str:
622
+ """Find broken co-located `.doc.md` references in a repository."""
623
+ broken = codocs.find_broken(root, exclude=set(_EXCLUDE))
624
+ return json.dumps(
625
+ {
626
+ "broken": [ref.to_dict() for ref in broken],
627
+ "count": len(broken),
628
+ "ok": not broken,
629
+ }
630
+ )
631
+ except ImportError:
632
+ pass
633
+
634
+
635
+ # ── Tool Wiring (graceful degradation) ─────────────────────────────────────
636
+ # All subsystem + memory + external tools are registered in try-import blocks
637
+ # above. A missing sin-code-* package leaves the server fully functional
638
+ # (graceful degradation — never crashes the MCP).
639
+ _try_subsystem_tools()
640
+ _try_memory_tools()
641
+ _try_external_tools()
642
+
643
+
644
+ # ── DAP Runtime Tracing ────────────────────────────────────────────────────
645
+
646
+
647
+ @mcp.tool()
648
+ def sin_runtime_trace(file_path: str, function_name: str, language: str = "python") -> str:
649
+ """Start a DAP debugging session for a specific function.
650
+
651
+ Replaces: Guessing from logs. Attaches real debugger (debugpy/dlv/node).
652
+ """
653
+ try:
654
+ from sin_code_bundle.dap_bridge import SINRuntimeTrace
655
+
656
+ tracer = SINRuntimeTrace()
657
+ return json.dumps(tracer.trace_function(file_path, function_name, language))
658
+ except Exception as exc:
659
+ return json.dumps({"error": str(exc)})
660
+
661
+
662
+ @mcp.tool()
663
+ def sin_stop_trace(session_id: str) -> str:
664
+ """Stop an active DAP debugging session."""
665
+ try:
666
+ from sin_code_bundle.dap_bridge import SINRuntimeTrace
667
+
668
+ tracer = SINRuntimeTrace()
669
+ return json.dumps(tracer.stop_trace(session_id))
670
+ except Exception as exc:
671
+ return json.dumps({"error": str(exc)})
672
+
673
+
674
+ # ── Interceptor (Architectural Enforcement) ─────────────────────────────────
675
+
676
+
677
+ @mcp.tool()
678
+ def sin_check_architecture(tool_name: str, tool_input: dict) -> str:
679
+ """Pre-flight: validate if a tool call violates architectural rules.
680
+
681
+ Use this BEFORE sin_write or sin_bash to prevent technical debt.
682
+ """
683
+ try:
684
+ from sin_code_bundle.interceptor import SINInterceptor
685
+
686
+ return json.dumps(SINInterceptor().preflight(tool_name, tool_input))
687
+ except Exception as exc:
688
+ return json.dumps({"error": str(exc)})
689
+
690
+
691
+ # ── Consolidation Tools (v0.7.0) ───────────────────────────────────────────
692
+ # Three high-ROI consolidations: each replaces 3-4 separate calls with one.
693
+ # See preflight.doc.md / symbol_resolve.doc.md / checkpoint.doc.md.
694
+
695
+
696
+ @mcp.tool()
697
+ def sin_preflight(tool_name: str, tool_input: dict) -> str:
698
+ """Pre-flight safety gate: policy + docs + git + tests in 1 call.
699
+
700
+ Run BEFORE any state-changing call (sin_write, sin_edit, sin_bash, sin_ast_edit).
701
+ Returns structured JSON with {allowed, policy_ok, docs_ok, git_clean,
702
+ tests_status, estimated_risk}.
703
+ """
704
+ try:
705
+ from sin_code_bundle.preflight import PreflightChecker
706
+
707
+ return json.dumps(
708
+ PreflightChecker().check(tool_name, tool_input),
709
+ indent=2,
710
+ default=str,
711
+ )
712
+ except Exception as exc:
713
+ return json.dumps({"error": str(exc), "tool_name": tool_name})
714
+
715
+
716
+ @mcp.tool()
717
+ def sin_symbol_resolve(
718
+ name: str,
719
+ depth: int = 2,
720
+ include: str = "callers,callees,blast,recent",
721
+ ) -> str:
722
+ """Unified code archaeology for a symbol (function, class, module).
723
+
724
+ Combines gitnexus_query + gitnexus_context + gitnexus_impact +
725
+ gitnexus_detect_changes into 1 call. Optionally integrates
726
+ sin-context-bridge for cross-source context.
727
+
728
+ Args:
729
+ name: symbol name (e.g. "validate_user", "AuthService", "auth/handler")
730
+ depth: how many call-graph levels to traverse (1-3)
731
+ include: comma-separated list of {callers, callees, blast, recent, cross}
732
+ """
733
+ try:
734
+ from sin_code_bundle.symbol_resolve import SymbolResolver
735
+
736
+ return json.dumps(
737
+ SymbolResolver().resolve(name, depth, include.split(",")),
738
+ indent=2,
739
+ default=str,
740
+ )
741
+ except Exception as exc:
742
+ return json.dumps({"error": str(exc), "name": name})
743
+
744
+
745
+ @mcp.tool()
746
+ def sin_checkpoint(
747
+ name: str,
748
+ include: str = "snapshot,docs,git,usages,tests",
749
+ description: str = "",
750
+ ) -> str:
751
+ """Combined snapshot + state report before a risky change.
752
+
753
+ Use before refactoring or any risky edit. Creates a recoverable snapshot
754
+ AND a state report (docs status, git state, usages, tests).
755
+
756
+ Args:
757
+ name: snapshot name (e.g. "before-auth-refactor")
758
+ include: comma-separated list of {snapshot, docs, git, usages, tests}
759
+ description: optional human-readable description
760
+ """
761
+ try:
762
+ from sin_code_bundle.checkpoint import Checkpointer
763
+
764
+ return json.dumps(
765
+ Checkpointer().create(name, include.split(","), description),
766
+ indent=2,
767
+ default=str,
768
+ )
769
+ except Exception as exc:
770
+ return json.dumps({"error": str(exc), "name": name})
771
+
772
+
773
+ # ── Worktree Orchestration ──────────────────────────────────────────────────
774
+
775
+
776
+ @mcp.tool()
777
+ def sin_create_worktree(branch_name: str = "") -> str:
778
+ """Create an isolated git worktree for parallel agent task execution."""
779
+ try:
780
+ from sin_code_bundle.orchestration_worktrees import SINWorktreeOrchestrator
781
+
782
+ return json.dumps(SINWorktreeOrchestrator().create_worktree(branch_name or None))
783
+ except Exception as exc:
784
+ return json.dumps({"error": str(exc)})
785
+
786
+
787
+ @mcp.tool()
788
+ def sin_cleanup_worktree(worktree_path: str, merge_back: bool = False) -> str:
789
+ """Clean up an isolated worktree. Optionally merge back to main."""
790
+ try:
791
+ from sin_code_bundle.orchestration_worktrees import SINWorktreeOrchestrator
792
+
793
+ return json.dumps(SINWorktreeOrchestrator().cleanup_worktree(worktree_path, merge_back))
794
+ except Exception as exc:
795
+ return json.dumps({"error": str(exc)})
796
+
797
+
798
+ # ── Baseline Workflow Tools (v0.8.0) ───────────────────────────────────────
799
+ # Four tools that make the agent NEVER have to remember underlying tool names.
800
+ # Each replaces 3-5 separate MCP calls with a single high-level action.
801
+ # See immortal_commit.doc.md / programming_workflow.doc.md /
802
+ # session_warmup.doc.md / merge_safety.doc.md.
803
+
804
+
805
+ @mcp.tool()
806
+ def sin_immortal_commit(
807
+ message: str,
808
+ tag: str = "",
809
+ push: bool = True,
810
+ force_main: bool = True,
811
+ main_branch: str = "main",
812
+ ) -> str:
813
+ """One-call immortal commit — Conventional Commits + tag + push in 1 call.
814
+
815
+ Replaces the agent's raw `git add && git commit && git tag && git push`
816
+ sequence with a single tool that enforces four rules:
817
+
818
+ 1. **Conventional Commits** — message must match ``type(scope): subject``
819
+ (subject >= 5 chars). Valid types: feat, fix, docs, chore, style,
820
+ test, refactor, perf, ci, build.
821
+ 2. **No secrets in message** — substring scan for ``sk-``, ``ghp_``,
822
+ ``AIza``, ``AKIA``/``ASIA``, ``BEGIN PRIVATE KEY`` etc.
823
+ 3. **Main only** — refuses to run on any branch other than ``main``
824
+ (configurable via ``main_branch=...``). Per the NEVER-BRANCHES mandate.
825
+ 4. **Pre-commit snapshot** — creates a ``sin-honcho-rollback snapshot`` so
826
+ the user can roll back. Independent of the commit; failure is non-fatal.
827
+
828
+ Args:
829
+ message: Conventional Commits message (required).
830
+ tag: optional annotated tag (e.g. ``v0.8.0``).
831
+ push: if True, push to ``origin/<main_branch>`` after commit.
832
+ force_main: if True, refuse to run on any branch other than main.
833
+ main_branch: which branch counts as ``main`` (default ``main``).
834
+
835
+ Returns:
836
+ JSON string with ``success``, ``sha``, ``branch``, ``tag``, ``pushed``,
837
+ ``warnings``, ``steps`` (per-step status), ``snapshot`` info.
838
+ """
839
+ try:
840
+ from sin_code_bundle.immortal_commit import ImmortalCommitter
841
+
842
+ committer = ImmortalCommitter()
843
+ result = committer.commit(
844
+ message=message,
845
+ tag=tag,
846
+ push=push,
847
+ force_main=force_main,
848
+ main_branch=main_branch,
849
+ )
850
+ return json.dumps(result, indent=2, default=str)
851
+ except Exception as exc:
852
+ return json.dumps({"error": str(exc), "message": message})
853
+
854
+
855
+ @mcp.tool()
856
+ def sin_programming_workflow(
857
+ action: str,
858
+ target: str = "",
859
+ content: str = "",
860
+ message: str = "",
861
+ checkpoint_name: str = "",
862
+ base: str = "main",
863
+ head: str = "HEAD",
864
+ ) -> str:
865
+ """Orchestrate common programming workflows in a single call.
866
+
867
+ Actions:
868
+ pre_write : sin_read + sin_preflight → READY / FIX_FIRST
869
+ write : sin_preflight + sin_write → PROCEED / BLOCK
870
+ post_write : sin_preflight + codocs_check + pytest --collect-only
871
+ pre_commit : sin_checkpoint + git_status + codocs + ceo-audit (cached 5min)
872
+ returns ``suggested_message`` if no message given
873
+ refactor : sin_checkpoint + gitnexus_impact + gitnexus_detect_changes
874
+ session_warmup : sin_session_warmup (branch, git_state, ceo_audit_grade,
875
+ top_risks, session_recommendation)
876
+
877
+ Returns:
878
+ JSON string with ``action``, ``steps``, ``verdict``, plus action-
879
+ specific extras (e.g. ``suggested_message`` for pre_commit).
880
+ """
881
+ try:
882
+ from sin_code_bundle.programming_workflow import ProgrammingWorkflow
883
+
884
+ wf = ProgrammingWorkflow()
885
+ result = wf.run(
886
+ action=action,
887
+ target=target,
888
+ content=content,
889
+ message=message,
890
+ checkpoint_name=checkpoint_name,
891
+ base=base,
892
+ head=head,
893
+ )
894
+ return json.dumps(result, indent=2, default=str)
895
+ except Exception as exc:
896
+ return json.dumps({"error": str(exc), "action": action})
897
+
898
+
899
+ @mcp.tool()
900
+ def sin_session_warmup(repo_path: str = ".") -> str:
901
+ """One-call session context primer — call ONCE at the start of every session.
902
+
903
+ Returns a snapshot of the current repo:
904
+ branch, git_state, git_changes_count, last_commit_age,
905
+ codocs_coverage, ceo_audit_grade, top_risks, session_recommendation.
906
+
907
+ The ``session_recommendation`` field is a single-line string so the agent
908
+ can decide "ready" vs "fix first" in one read:
909
+ - "BLOCK — ceo-audit grade F. Fix critical issues first."
910
+ - "FIX — improve docs/quality before coding"
911
+ - "STASH or COMMIT first — working tree dirty"
912
+ - "READY — proceed with coding"
913
+ """
914
+ try:
915
+ from sin_code_bundle.session_warmup import SessionWarmup
916
+
917
+ warmup = SessionWarmup(repo_root=Path(repo_path).expanduser())
918
+ return json.dumps(warmup.warmup(), indent=2, default=str)
919
+ except Exception as exc:
920
+ return json.dumps({"error": str(exc), "repo_path": repo_path})
921
+
922
+
923
+ @mcp.tool()
924
+ def sin_merge_safety(
925
+ base: str = "main",
926
+ head: str = "HEAD",
927
+ profile: str = "QUICK",
928
+ ) -> str:
929
+ """Pre-merge / pre-PR safety gate.
930
+
931
+ Runs 4 independent checks in one call:
932
+ 1. CoDocs coverage (broken .doc.md references → blocker)
933
+ 2. ceo-audit grade (cached 5 min per (profile, base, head))
934
+ - F → blocker, D → warning
935
+ 3. git diff stat (size + secret scan via substring + regex)
936
+ - >1000 lines → warning, secrets → blocker
937
+ 4. Working tree state (clean/dirty)
938
+
939
+ Returns:
940
+ JSON string with ``pass`` (bool), ``verdict`` ("READY" or "FIX_FIRST"),
941
+ ``blockers`` and ``warnings`` (lists of human-readable strings),
942
+ ``checks`` (per-check dict).
943
+ """
944
+ try:
945
+ from sin_code_bundle.merge_safety import MergeSafety
946
+
947
+ gate = MergeSafety()
948
+ return json.dumps(gate.check(base=base, head=head, profile=profile), indent=2, default=str)
949
+ except Exception as exc:
950
+ return json.dumps({"error": str(exc), "base": base, "head": head})
951
+
952
+
953
+ def main() -> None:
954
+ """Run the MCP server (stdio)."""
955
+ import sys
956
+
957
+ sys.stderr.write("[SIN-CODE-BUNDLE] MCP server starting (stdio).\n")
958
+ sys.stderr.flush()
959
+ mcp.run()
960
+
961
+
962
+ if __name__ == "__main__":
963
+ main()