sage-governance 1.0.0
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/AGENTS.MD +481 -0
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/bin/sage.js +55 -0
- package/claude.json +16 -0
- package/codex.json +22 -0
- package/cursor.json +27 -0
- package/docs/architecture.md +38 -0
- package/opencode.json +24 -0
- package/package.json +58 -0
- package/requirements.txt +7 -0
- package/rules/general/EU_AI_Act_Annex_III.md +29 -0
- package/rules/general/OECD_Principles.md +20 -0
- package/rules/general/UNESCO_AI_Ethics.md +237 -0
- package/rules/general/UN_Human_Rights.md +183 -0
- package/rules/index.json +145 -0
- package/sage/mcp_server.py +459 -0
- package/sage/report_gen.py +408 -0
- package/sage/sage_agent.py +710 -0
- package/sage/security_agent.py +455 -0
- package/sage/startup.py +311 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mcp_server.py — SAGE MCP Server
|
|
3
|
+
═════════════════════════════════
|
|
4
|
+
Exposes 5 tools to any MCP-compatible coding agent:
|
|
5
|
+
|
|
6
|
+
1. sage_evaluate — classify intent + full ethics evaluation
|
|
7
|
+
2. security_scan — deterministic code security scan
|
|
8
|
+
3. intercept_file_write — PRE-WRITE interception (the key differentiator)
|
|
9
|
+
4. audit_write — append-only audit trail entry
|
|
10
|
+
5. report_generate — model card generation from audit trail
|
|
11
|
+
|
|
12
|
+
TRANSPORT: stdio (persistent process — NOT respawned per tool call)
|
|
13
|
+
DISTRIBUTION: npm install -g sage-governance (see bin/sage.js)
|
|
14
|
+
COMPATIBLE WITH: OpenCode, Cline, Claude Code, Continue, Cursor, Zed
|
|
15
|
+
|
|
16
|
+
ARGUMENT 2 CLARIFICATION (for team reference)
|
|
17
|
+
──────────────────────────────────────────────
|
|
18
|
+
The concern "FastMCP spawns a new process per call" is INCORRECT for
|
|
19
|
+
stdio transport. The host (e.g. OpenCode) launches this process ONCE
|
|
20
|
+
when the MCP connection is established. It stays alive for the session.
|
|
21
|
+
startup.py preloading is still correct practice — it eliminates first-
|
|
22
|
+
call latency from heavy imports inside tool functions.
|
|
23
|
+
|
|
24
|
+
Author: SAGE Team — Beunec Technologies, Inc. / Team SAGE (Hackathon)
|
|
25
|
+
License: MIT
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import hashlib
|
|
31
|
+
import json
|
|
32
|
+
import sys
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
# ── startup MUST be the first import — preloads all globals ──────────────────
|
|
37
|
+
from startup import AUDIT_FILE, LOGS_FILE, LOCAL_MEMORY, write_audit_entry
|
|
38
|
+
|
|
39
|
+
import sage_agent
|
|
40
|
+
import security_agent
|
|
41
|
+
import report_gen
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from mcp.server.fastmcp import FastMCP
|
|
45
|
+
except ImportError:
|
|
46
|
+
print(
|
|
47
|
+
"[SAGE] FATAL: 'mcp' package not found.\n"
|
|
48
|
+
" Run: pip install mcp\n"
|
|
49
|
+
" Or: pip install 'mcp[cli]'",
|
|
50
|
+
file=sys.stderr,
|
|
51
|
+
)
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
# SERVER INITIALISATION
|
|
56
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
|
|
58
|
+
mcp = FastMCP(
|
|
59
|
+
"sage-governance",
|
|
60
|
+
instructions=(
|
|
61
|
+
"SAGE (Supervisory Agentic Governance Engine) — governance layer for agentic coding. "
|
|
62
|
+
"Always call sage_evaluate BEFORE acting on any request involving data, ML models, "
|
|
63
|
+
"or automated decisions. Always call intercept_file_write BEFORE writing any file. "
|
|
64
|
+
"Call audit_write to record developer decisions. Call report_generate to produce "
|
|
65
|
+
"a human-readable governance report."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
# AUDIT TRAIL STATE (module-level, persists for the process lifetime)
|
|
71
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
_session_id: str = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _write_audit(entry: dict) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Append one JSON line to decisions.jsonl with SHA-256 chain link.
|
|
79
|
+
Delegates to write_audit_entry in startup.py to support multi-process chaining.
|
|
80
|
+
"""
|
|
81
|
+
if "session_id" not in entry:
|
|
82
|
+
entry["session_id"] = _session_id
|
|
83
|
+
return write_audit_entry(entry)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _write_log(summary: str) -> None:
|
|
87
|
+
"""Append human-readable summary to LOGS.md (append-only)."""
|
|
88
|
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
89
|
+
with open(LOGS_FILE, "a", encoding="utf-8") as fh:
|
|
90
|
+
fh.write(f"\n## {ts}\n\n{summary}\n\n---\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
94
|
+
# TOOL 1 — sage_evaluate
|
|
95
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@mcp.tool()
|
|
99
|
+
async def sage_evaluate(
|
|
100
|
+
prompt: str,
|
|
101
|
+
code: str = "",
|
|
102
|
+
context: str = "",
|
|
103
|
+
) -> dict:
|
|
104
|
+
"""
|
|
105
|
+
SAGE primary evaluation tool.
|
|
106
|
+
|
|
107
|
+
Call this BEFORE the coding agent acts on ANY request involving:
|
|
108
|
+
• machine learning models or classifiers
|
|
109
|
+
• predictions about people (recidivism, credit, hiring, health)
|
|
110
|
+
• datasets with demographic or behavioral signals
|
|
111
|
+
• automated decision-making systems
|
|
112
|
+
|
|
113
|
+
Returns a Pydantic-validated response (always parseable):
|
|
114
|
+
• risk_level : LOW | MEDIUM | HIGH | CRITICAL
|
|
115
|
+
• eu_ai_act_annex : applicable EU AI Act classification or null
|
|
116
|
+
• protected_attributes: attributes detected directly
|
|
117
|
+
• proxy_attributes : known proxy features detected
|
|
118
|
+
• fairness_options : 3 concrete options with pros/cons/API
|
|
119
|
+
• compliance_flags : actionable regulatory issues
|
|
120
|
+
• immediate_actions : ordered list of required next steps
|
|
121
|
+
• requires_human_review: true when risk is HIGH or CRITICAL
|
|
122
|
+
• sage_reasoning : 2-3 sentence explanation (LLM or fallback)
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
prompt: The developer's original request (required)
|
|
126
|
+
code: Optional code snippet already written
|
|
127
|
+
context: Optional dataset description or additional context
|
|
128
|
+
"""
|
|
129
|
+
result = sage_agent.evaluate(prompt=prompt, code=code, context=context)
|
|
130
|
+
result_dict = result.model_dump()
|
|
131
|
+
|
|
132
|
+
entry_hash = _write_audit({
|
|
133
|
+
"event_type": "sage_evaluate",
|
|
134
|
+
"intent_summary": result.intent_summary,
|
|
135
|
+
"domain": result.detected_domain,
|
|
136
|
+
"risk_level": result.risk_level,
|
|
137
|
+
"eu_ai_act_annex": result.eu_ai_act_annex,
|
|
138
|
+
"protected_attributes": result.protected_attributes,
|
|
139
|
+
"proxy_attributes": result.proxy_attributes,
|
|
140
|
+
"compliance_flags": result.compliance_flags,
|
|
141
|
+
"regulations": result.regulations,
|
|
142
|
+
"udhr_articles": result.udhr_articles,
|
|
143
|
+
"requires_human_review": result.requires_human_review,
|
|
144
|
+
"fairness_impossibility": result.fairness_impossibility,
|
|
145
|
+
})
|
|
146
|
+
result_dict["audit_entry_hash"] = entry_hash
|
|
147
|
+
|
|
148
|
+
_write_log(
|
|
149
|
+
f"**SAGE Evaluate** — Risk: `{result.risk_level}` | "
|
|
150
|
+
f"Domain: `{result.detected_domain}`\n\n"
|
|
151
|
+
f"**Prompt:** {prompt[:200]}\n\n"
|
|
152
|
+
f"**EU AI Act:** {result.eu_ai_act_annex or 'Not classified as high-risk'}\n\n"
|
|
153
|
+
f"**Protected attributes:** "
|
|
154
|
+
f"{', '.join(f'`{a}`' for a in result.protected_attributes) or 'none detected'}\n\n"
|
|
155
|
+
f"**SAGE reasoning:** {result.sage_reasoning}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return result_dict
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
162
|
+
# TOOL 2 — security_scan
|
|
163
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.tool()
|
|
167
|
+
async def security_scan(
|
|
168
|
+
code: str,
|
|
169
|
+
filepath: str = "",
|
|
170
|
+
context: str = "",
|
|
171
|
+
) -> dict:
|
|
172
|
+
"""
|
|
173
|
+
Full deterministic security scan of generated code.
|
|
174
|
+
|
|
175
|
+
Detects (in severity order P0–P4):
|
|
176
|
+
P0 — API keys / secrets hardcoded; biometric/medical PII; protected attrs
|
|
177
|
+
P1 — Government ID PII; geolocation; major compliance gaps
|
|
178
|
+
P2 — Proxy discrimination risk; black-box model; missing fairness metrics
|
|
179
|
+
P3 — Encoding choices; data quality; model serialization gaps
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
• passed : true only when zero P0/P1 findings
|
|
183
|
+
• highest_severity: P0|P1|P2|P3|P4|PASS
|
|
184
|
+
• findings : list of findings sorted by severity
|
|
185
|
+
• summary : human-readable verdict string
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
code: Code content to scan
|
|
189
|
+
filepath: Optional path for audit trail reference
|
|
190
|
+
context: Optional context (framework, dataset, purpose)
|
|
191
|
+
"""
|
|
192
|
+
report = security_agent.scan(code)
|
|
193
|
+
|
|
194
|
+
findings_dicts = [f.to_dict() for f in report.findings]
|
|
195
|
+
top = report.top_finding()
|
|
196
|
+
|
|
197
|
+
_write_audit({
|
|
198
|
+
"event_type": "security_scan",
|
|
199
|
+
"filepath": filepath,
|
|
200
|
+
"total_findings": report.total_findings,
|
|
201
|
+
"highest_severity": report.highest_severity,
|
|
202
|
+
"protected_attributes_found": report.protected_attributes_found,
|
|
203
|
+
"secrets_found_count": len(report.secrets_found),
|
|
204
|
+
"passed": report.passed,
|
|
205
|
+
"highest_risk_finding": {
|
|
206
|
+
"severity": top.severity,
|
|
207
|
+
"category": top.category,
|
|
208
|
+
"description": top.description,
|
|
209
|
+
} if top else None,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
verdict = "✅ PASSED" if report.passed else "❌ BLOCKED"
|
|
213
|
+
return {
|
|
214
|
+
"passed": report.passed,
|
|
215
|
+
"total_findings": report.total_findings,
|
|
216
|
+
"highest_severity": report.highest_severity,
|
|
217
|
+
"protected_attributes_found": report.protected_attributes_found,
|
|
218
|
+
"findings": findings_dicts,
|
|
219
|
+
"summary": (
|
|
220
|
+
f"{verdict} — {report.total_findings} finding(s), "
|
|
221
|
+
f"highest severity: {report.highest_severity}"
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
227
|
+
# TOOL 3 — intercept_file_write (THE KEY DIFFERENTIATOR)
|
|
228
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@mcp.tool()
|
|
232
|
+
async def intercept_file_write(
|
|
233
|
+
filepath: str,
|
|
234
|
+
code: str,
|
|
235
|
+
context: str = "",
|
|
236
|
+
) -> dict:
|
|
237
|
+
"""
|
|
238
|
+
CRITICAL — Call this BEFORE writing ANY file to disk.
|
|
239
|
+
|
|
240
|
+
SAGE scans the code before it touches the filesystem.
|
|
241
|
+
This is the only governance tool that blocks at the point of action,
|
|
242
|
+
not after the fact.
|
|
243
|
+
|
|
244
|
+
Behaviour:
|
|
245
|
+
• Zero P0/P1 findings → auto-approves, logs silently, returns approved=true
|
|
246
|
+
• P0/P1 found → BLOCKS write, surfaces the single highest-risk finding,
|
|
247
|
+
returns approved=false with 3 explicit developer choices
|
|
248
|
+
|
|
249
|
+
Developer choices (pass back via audit_write):
|
|
250
|
+
• "accept_as_is" — write file unchanged; risk accepted and logged
|
|
251
|
+
• "apply_suggestion" — apply SAGE's recommended fix before writing
|
|
252
|
+
• "reject" — discard this code; revise the approach
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
filepath: Target file path being written
|
|
256
|
+
code: Complete code content about to be written
|
|
257
|
+
context: Optional context (framework, dataset purpose)
|
|
258
|
+
"""
|
|
259
|
+
report = security_agent.scan(code)
|
|
260
|
+
|
|
261
|
+
# ── Auto-approve path ────────────────────────────────────────────────────
|
|
262
|
+
if report.passed:
|
|
263
|
+
_write_audit({
|
|
264
|
+
"event_type": "file_write_intercepted",
|
|
265
|
+
"filepath": filepath,
|
|
266
|
+
"decision": "auto_approved",
|
|
267
|
+
"total_findings": report.total_findings,
|
|
268
|
+
"highest_severity": report.highest_severity,
|
|
269
|
+
})
|
|
270
|
+
return {
|
|
271
|
+
"approved": True,
|
|
272
|
+
"decision": "auto_approved",
|
|
273
|
+
"findings": [f.to_dict() for f in report.findings],
|
|
274
|
+
"message": f"✅ SAGE approved write to `{filepath}` — no P0/P1 issues detected.",
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# ── Block path ───────────────────────────────────────────────────────────
|
|
278
|
+
top = report.top_finding()
|
|
279
|
+
|
|
280
|
+
audit_data = {
|
|
281
|
+
"event_type": "file_write_intercepted",
|
|
282
|
+
"filepath": filepath,
|
|
283
|
+
"decision": "blocked_pending_developer_action",
|
|
284
|
+
"total_findings": report.total_findings,
|
|
285
|
+
"highest_risk_finding": {
|
|
286
|
+
"severity": top.severity,
|
|
287
|
+
"category": top.category,
|
|
288
|
+
"line_number": top.line_number,
|
|
289
|
+
"snippet": top.snippet,
|
|
290
|
+
"description": top.description,
|
|
291
|
+
"fix": top.fix,
|
|
292
|
+
"regulation": top.regulation,
|
|
293
|
+
} if top else None,
|
|
294
|
+
"audit_pending": True,
|
|
295
|
+
}
|
|
296
|
+
_write_audit(audit_data)
|
|
297
|
+
|
|
298
|
+
_write_log(
|
|
299
|
+
f"**⛔ File Write Intercepted:** `{filepath}`\n\n"
|
|
300
|
+
f"- Severity: `{top.severity}`\n"
|
|
301
|
+
f"- Category: `{top.category}`\n"
|
|
302
|
+
f"- Line {top.line_number}: `{top.snippet[:80]}`\n"
|
|
303
|
+
f"- Issue: {top.description}\n"
|
|
304
|
+
f"- Regulation: {top.regulation}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"approved": False,
|
|
309
|
+
"requires_action": True,
|
|
310
|
+
"filepath": filepath,
|
|
311
|
+
"total_findings": report.total_findings,
|
|
312
|
+
"highest_risk": {
|
|
313
|
+
"severity": top.severity,
|
|
314
|
+
"category": top.category,
|
|
315
|
+
"line_number": top.line_number,
|
|
316
|
+
"code_snippet": top.snippet,
|
|
317
|
+
"risk_description": top.description,
|
|
318
|
+
"suggested_fix": top.fix,
|
|
319
|
+
"regulation": top.regulation,
|
|
320
|
+
},
|
|
321
|
+
"developer_choices": [
|
|
322
|
+
"accept_as_is — write file unchanged (risk accepted and logged in audit trail)",
|
|
323
|
+
"apply_suggestion — apply SAGE's suggested fix before writing",
|
|
324
|
+
"reject — discard this code and revise the approach",
|
|
325
|
+
],
|
|
326
|
+
"instruction": (
|
|
327
|
+
f"⛔ SAGE has BLOCKED write to `{filepath}`. "
|
|
328
|
+
f"Severity: {top.severity}. "
|
|
329
|
+
f"You MUST call audit_write with your developer_choice before proceeding. "
|
|
330
|
+
f"Total findings: {report.total_findings}."
|
|
331
|
+
),
|
|
332
|
+
"audit_pending": True,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
337
|
+
# TOOL 4 — audit_write
|
|
338
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@mcp.tool()
|
|
342
|
+
async def audit_write(
|
|
343
|
+
event_type: str,
|
|
344
|
+
developer_choice: str = "",
|
|
345
|
+
choice_reasoning: str = "",
|
|
346
|
+
filepath: str = "",
|
|
347
|
+
extra_data: dict = {},
|
|
348
|
+
) -> dict:
|
|
349
|
+
"""
|
|
350
|
+
Records a developer decision in the append-only audit trail.
|
|
351
|
+
|
|
352
|
+
Required after intercept_file_write when approved=false.
|
|
353
|
+
Also use to record any governance decision (fairness option selection,
|
|
354
|
+
DPIA acknowledgement, human review escalation).
|
|
355
|
+
|
|
356
|
+
The audit trail is append-only — entries can be added but never deleted
|
|
357
|
+
or modified via this tool.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
event_type: Type of event (e.g. "fairness_option_selected",
|
|
361
|
+
"file_write_decision", "human_review_escalated")
|
|
362
|
+
developer_choice: What the developer chose
|
|
363
|
+
choice_reasoning: Optional explanation of the choice
|
|
364
|
+
filepath: Optional file path reference
|
|
365
|
+
extra_data: Optional additional structured data
|
|
366
|
+
"""
|
|
367
|
+
entry = {
|
|
368
|
+
"event_type": event_type,
|
|
369
|
+
"developer_choice": developer_choice,
|
|
370
|
+
"choice_reasoning": choice_reasoning,
|
|
371
|
+
"filepath": filepath,
|
|
372
|
+
**extra_data,
|
|
373
|
+
}
|
|
374
|
+
entry_hash = _write_audit(entry)
|
|
375
|
+
|
|
376
|
+
_write_log(
|
|
377
|
+
f"**Developer Decision** — `{event_type}`\n\n"
|
|
378
|
+
f"- Choice: `{developer_choice or 'N/A'}`\n"
|
|
379
|
+
f"- File: {filepath or 'N/A'}\n"
|
|
380
|
+
f"- Reasoning: {choice_reasoning or '_Not provided_'}"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
"recorded": True,
|
|
385
|
+
"event_type": event_type,
|
|
386
|
+
"developer_choice": developer_choice,
|
|
387
|
+
"entry_hash": entry_hash,
|
|
388
|
+
"session_id": _session_id,
|
|
389
|
+
"message": (
|
|
390
|
+
f"✅ Decision recorded in audit trail "
|
|
391
|
+
f"(hash: {entry_hash[:12]}...)"
|
|
392
|
+
),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
397
|
+
# TOOL 5 — report_generate
|
|
398
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@mcp.tool()
|
|
402
|
+
async def report_generate(
|
|
403
|
+
session_id: str = "",
|
|
404
|
+
output_format: str = "markdown",
|
|
405
|
+
) -> dict:
|
|
406
|
+
"""
|
|
407
|
+
Generates a human-readable governance report from the audit trail.
|
|
408
|
+
|
|
409
|
+
Output formats:
|
|
410
|
+
• "markdown" (default) — full model card written to reports/governance_report_*.md
|
|
411
|
+
• "summary" — short terminal-friendly summary (no file written)
|
|
412
|
+
|
|
413
|
+
The model card follows Google Model Cards spec (Mitchell et al., 2019)
|
|
414
|
+
and covers: intended use, regulatory classification, fairness analysis,
|
|
415
|
+
security findings, compliance flags, audit integrity, and limitations.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
session_id: Filter to a specific session (empty = current session)
|
|
419
|
+
output_format: "markdown" or "summary"
|
|
420
|
+
"""
|
|
421
|
+
sid = session_id or _session_id
|
|
422
|
+
|
|
423
|
+
if output_format == "summary":
|
|
424
|
+
content = report_gen.generate_terminal_summary(sid)
|
|
425
|
+
report_path = None
|
|
426
|
+
else:
|
|
427
|
+
content = report_gen.generate_model_card(sid)
|
|
428
|
+
saved_path = report_gen.save_report(content, sid)
|
|
429
|
+
report_path = str(saved_path)
|
|
430
|
+
|
|
431
|
+
_write_audit({
|
|
432
|
+
"event_type": "report_generated",
|
|
433
|
+
"output_format": output_format,
|
|
434
|
+
"report_path": report_path,
|
|
435
|
+
"session_id": sid,
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
"generated": True,
|
|
440
|
+
"session_id": sid,
|
|
441
|
+
"report_path": report_path,
|
|
442
|
+
"content": content,
|
|
443
|
+
"message": (
|
|
444
|
+
f"✅ Governance report generated ({output_format})"
|
|
445
|
+
+ (f" → {report_path}" if report_path else "")
|
|
446
|
+
),
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
451
|
+
# ENTRY POINT
|
|
452
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
453
|
+
|
|
454
|
+
if __name__ == "__main__":
|
|
455
|
+
print(
|
|
456
|
+
f"[SAGE] MCP server starting — session {_session_id}",
|
|
457
|
+
file=sys.stderr,
|
|
458
|
+
)
|
|
459
|
+
mcp.run(transport="stdio")
|