@rigour-labs/mcp 5.2.3 → 5.2.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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Dashboard HTML — Self-contained MCP App UI
3
+ *
4
+ * Served as a ui:// resource. Renders in a sandboxed iframe
5
+ * inside Claude Desktop, VS Code Copilot, ChatGPT, Goose.
6
+ *
7
+ * Features:
8
+ * - SVG circular score gauge (color-coded)
9
+ * - Scrolling activity timeline
10
+ * - Severity breakdown bar
11
+ * - Brain learning indicator
12
+ *
13
+ * No external dependencies. Initial state injected via __INITIAL_STATE__.
14
+ */
15
+ export declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Rigour Governance</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0d1117;color:#c9d1d9;padding:16px;font-size:13px;line-height:1.5}\n.header{display:flex;align-items:center;gap:8px;margin-bottom:16px}\n.header h1{font-size:15px;font-weight:600;color:#f0f6fc}\n.header .badge{font-size:11px;padding:2px 8px;border-radius:12px;font-weight:500}\n.badge-pass{background:#1a7f37;color:#fff}\n.badge-fail{background:#da3633;color:#fff}\n.badge-scanning{background:#9e6a03;color:#fff}\n.badge-idle{background:#30363d;color:#8b949e}\n.top{display:flex;gap:16px;margin-bottom:16px}\n.gauge-wrap{flex:0 0 100px;text-align:center}\n.gauge-label{font-size:11px;color:#8b949e;margin-top:4px}\n.severity-wrap{flex:1;display:flex;flex-direction:column;justify-content:center;gap:6px}\n.sev-row{display:flex;align-items:center;gap:6px;font-size:12px}\n.sev-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n.sev-critical{background:#da3633}\n.sev-high{background:#db6d28}\n.sev-medium{background:#9e6a03}\n.sev-low{background:#388bfd}\n.sev-info{background:#8b949e}\n.sev-count{color:#f0f6fc;font-weight:600;min-width:18px}\n.section-title{font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;font-weight:600}\n.timeline{max-height:200px;overflow-y:auto;margin-bottom:16px}\n.timeline::-webkit-scrollbar{width:4px}\n.timeline::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}\n.tl-entry{display:flex;gap:8px;padding:4px 0;border-bottom:1px solid #21262d;font-size:12px}\n.tl-time{color:#8b949e;flex:0 0 55px;font-family:'SF Mono',Monaco,monospace;font-size:11px}\n.tl-tool{color:#58a6ff;flex:0 0 auto}\n.tl-arrow{color:#484f58}\n.tl-detail{color:#c9d1d9;flex:1}\n.tl-status-success .tl-detail{color:#3fb950}\n.tl-status-error .tl-detail{color:#da3633}\n.tl-status-warning .tl-detail{color:#d29922}\n.brain{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#161b22;border:1px solid #30363d;border-radius:6px;font-size:12px}\n.brain-icon{font-size:16px}\n.brain-text{color:#c9d1d9}\n.brain-trend{color:#3fb950;font-weight:600}\n.brain-trend.down{color:#da3633}\n.empty{text-align:center;padding:32px 16px;color:#484f58;font-size:12px}\n</style>\n</head>\n<body>\n<div class=\"header\">\n <h1>Rigour Governance</h1>\n <span class=\"badge badge-idle\" id=\"statusBadge\">idle</span>\n</div>\n\n<div class=\"top\">\n <div class=\"gauge-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"90\" height=\"90\">\n <circle cx=\"50\" cy=\"50\" r=\"42\" fill=\"none\" stroke=\"#21262d\" stroke-width=\"8\"/>\n <circle cx=\"50\" cy=\"50\" r=\"42\" fill=\"none\" stroke=\"#30363d\" stroke-width=\"8\"\n stroke-dasharray=\"264\" stroke-dashoffset=\"264\" stroke-linecap=\"round\"\n transform=\"rotate(-90 50 50)\" id=\"gaugeArc\"/>\n <text x=\"50\" y=\"46\" text-anchor=\"middle\" fill=\"#f0f6fc\" font-size=\"22\" font-weight=\"700\" id=\"gaugeText\">--</text>\n <text x=\"50\" y=\"62\" text-anchor=\"middle\" fill=\"#8b949e\" font-size=\"9\">SCORE</text>\n </svg>\n </div>\n <div class=\"severity-wrap\" id=\"severityWrap\">\n <div class=\"sev-row\"><span class=\"sev-dot sev-critical\"></span><span class=\"sev-count\" id=\"sevCritical\">0</span>critical</div>\n <div class=\"sev-row\"><span class=\"sev-dot sev-high\"></span><span class=\"sev-count\" id=\"sevHigh\">0</span>high</div>\n <div class=\"sev-row\"><span class=\"sev-dot sev-medium\"></span><span class=\"sev-count\" id=\"sevMedium\">0</span>medium</div>\n <div class=\"sev-row\"><span class=\"sev-dot sev-low\"></span><span class=\"sev-count\" id=\"sevLow\">0</span>low</div>\n </div>\n</div>\n\n<div class=\"section-title\">Activity</div>\n<div class=\"timeline\" id=\"timeline\">\n <div class=\"empty\">Waiting for agent activity...</div>\n</div>\n\n<div class=\"brain\" id=\"brainSection\">\n <span class=\"brain-icon\">&#x1F9E0;</span>\n <span class=\"brain-text\" id=\"brainText\">Brain: waiting for data</span>\n <span class=\"brain-trend\" id=\"brainTrend\"></span>\n</div>\n\n<script>\nconst state = JSON.parse('__INITIAL_STATE__');\n\nfunction scoreColor(s) {\n if (s === null) return '#30363d';\n if (s < 50) return '#da3633';\n if (s < 80) return '#d29922';\n return '#3fb950';\n}\n\nfunction updateGauge(score) {\n const arc = document.getElementById('gaugeArc');\n const text = document.getElementById('gaugeText');\n if (score === null) {\n arc.setAttribute('stroke-dashoffset', '264');\n arc.setAttribute('stroke', '#30363d');\n text.textContent = '--';\n return;\n }\n const offset = 264 - (264 * Math.min(score, 100) / 100);\n arc.setAttribute('stroke-dashoffset', String(offset));\n arc.setAttribute('stroke', scoreColor(score));\n text.textContent = String(Math.round(score));\n}\n\nfunction updateBadge(status) {\n const badge = document.getElementById('statusBadge');\n badge.className = 'badge badge-' + (status || 'idle');\n const labels = {pass:'PASS',fail:'FAIL',scanning:'SCANNING'};\n badge.textContent = labels[status] || 'IDLE';\n}\n\nfunction updateSeverity(sev) {\n document.getElementById('sevCritical').textContent = sev.critical || 0;\n document.getElementById('sevHigh').textContent = sev.high || 0;\n document.getElementById('sevMedium').textContent = sev.medium || 0;\n document.getElementById('sevLow').textContent = sev.low || 0;\n}\n\nfunction formatTime(iso) {\n const d = new Date(iso);\n return d.toLocaleTimeString('en-US', {hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});\n}\n\nfunction renderTimeline(entries) {\n const el = document.getElementById('timeline');\n if (!entries.length) {\n el.innerHTML = '<div class=\"empty\">Waiting for agent activity...</div>';\n return;\n }\n el.innerHTML = entries.map(e => {\n const cls = e.status === 'success' ? 'tl-status-success' : e.status === 'error' ? 'tl-status-error' : 'tl-status-warning';\n return '<div class=\"tl-entry ' + cls + '\">' +\n '<span class=\"tl-time\">' + formatTime(e.timestamp) + '</span>' +\n '<span class=\"tl-tool\">' + e.tool + '</span>' +\n '<span class=\"tl-arrow\">\u2192</span>' +\n '<span class=\"tl-detail\">' + e.details + '</span></div>';\n }).join('');\n el.scrollTop = el.scrollHeight;\n}\n\nfunction updateBrain(patterns, trend) {\n const text = document.getElementById('brainText');\n const trendEl = document.getElementById('brainTrend');\n text.textContent = 'Brain: ' + patterns + ' patterns learned';\n if (trend) {\n const arrows = {improving:'\u2191',stable:'\u2194',declining:'\u2193'};\n trendEl.textContent = (arrows[trend] || '') + ' ' + trend;\n trendEl.className = 'brain-trend' + (trend === 'declining' ? ' down' : '');\n }\n}\n\nfunction render() {\n updateGauge(state.currentScore);\n updateBadge(state.status);\n updateSeverity(state.severityBreakdown || {});\n renderTimeline(state.timeline || []);\n updateBrain(state.brainPatterns || 0, state.brainTrend);\n}\n\nrender();\n</script>\n</body>\n</html>";
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Dashboard HTML — Self-contained MCP App UI
3
+ *
4
+ * Served as a ui:// resource. Renders in a sandboxed iframe
5
+ * inside Claude Desktop, VS Code Copilot, ChatGPT, Goose.
6
+ *
7
+ * Features:
8
+ * - SVG circular score gauge (color-coded)
9
+ * - Scrolling activity timeline
10
+ * - Severity breakdown bar
11
+ * - Brain learning indicator
12
+ *
13
+ * No external dependencies. Initial state injected via __INITIAL_STATE__.
14
+ */
15
+ export const DASHBOARD_HTML = `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width,initial-scale=1">
20
+ <title>Rigour Governance</title>
21
+ <style>
22
+ *{margin:0;padding:0;box-sizing:border-box}
23
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0d1117;color:#c9d1d9;padding:16px;font-size:13px;line-height:1.5}
24
+ .header{display:flex;align-items:center;gap:8px;margin-bottom:16px}
25
+ .header h1{font-size:15px;font-weight:600;color:#f0f6fc}
26
+ .header .badge{font-size:11px;padding:2px 8px;border-radius:12px;font-weight:500}
27
+ .badge-pass{background:#1a7f37;color:#fff}
28
+ .badge-fail{background:#da3633;color:#fff}
29
+ .badge-scanning{background:#9e6a03;color:#fff}
30
+ .badge-idle{background:#30363d;color:#8b949e}
31
+ .top{display:flex;gap:16px;margin-bottom:16px}
32
+ .gauge-wrap{flex:0 0 100px;text-align:center}
33
+ .gauge-label{font-size:11px;color:#8b949e;margin-top:4px}
34
+ .severity-wrap{flex:1;display:flex;flex-direction:column;justify-content:center;gap:6px}
35
+ .sev-row{display:flex;align-items:center;gap:6px;font-size:12px}
36
+ .sev-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
37
+ .sev-critical{background:#da3633}
38
+ .sev-high{background:#db6d28}
39
+ .sev-medium{background:#9e6a03}
40
+ .sev-low{background:#388bfd}
41
+ .sev-info{background:#8b949e}
42
+ .sev-count{color:#f0f6fc;font-weight:600;min-width:18px}
43
+ .section-title{font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;font-weight:600}
44
+ .timeline{max-height:200px;overflow-y:auto;margin-bottom:16px}
45
+ .timeline::-webkit-scrollbar{width:4px}
46
+ .timeline::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}
47
+ .tl-entry{display:flex;gap:8px;padding:4px 0;border-bottom:1px solid #21262d;font-size:12px}
48
+ .tl-time{color:#8b949e;flex:0 0 55px;font-family:'SF Mono',Monaco,monospace;font-size:11px}
49
+ .tl-tool{color:#58a6ff;flex:0 0 auto}
50
+ .tl-arrow{color:#484f58}
51
+ .tl-detail{color:#c9d1d9;flex:1}
52
+ .tl-status-success .tl-detail{color:#3fb950}
53
+ .tl-status-error .tl-detail{color:#da3633}
54
+ .tl-status-warning .tl-detail{color:#d29922}
55
+ .brain{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#161b22;border:1px solid #30363d;border-radius:6px;font-size:12px}
56
+ .brain-icon{font-size:16px}
57
+ .brain-text{color:#c9d1d9}
58
+ .brain-trend{color:#3fb950;font-weight:600}
59
+ .brain-trend.down{color:#da3633}
60
+ .empty{text-align:center;padding:32px 16px;color:#484f58;font-size:12px}
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="header">
65
+ <h1>Rigour Governance</h1>
66
+ <span class="badge badge-idle" id="statusBadge">idle</span>
67
+ </div>
68
+
69
+ <div class="top">
70
+ <div class="gauge-wrap">
71
+ <svg viewBox="0 0 100 100" width="90" height="90">
72
+ <circle cx="50" cy="50" r="42" fill="none" stroke="#21262d" stroke-width="8"/>
73
+ <circle cx="50" cy="50" r="42" fill="none" stroke="#30363d" stroke-width="8"
74
+ stroke-dasharray="264" stroke-dashoffset="264" stroke-linecap="round"
75
+ transform="rotate(-90 50 50)" id="gaugeArc"/>
76
+ <text x="50" y="46" text-anchor="middle" fill="#f0f6fc" font-size="22" font-weight="700" id="gaugeText">--</text>
77
+ <text x="50" y="62" text-anchor="middle" fill="#8b949e" font-size="9">SCORE</text>
78
+ </svg>
79
+ </div>
80
+ <div class="severity-wrap" id="severityWrap">
81
+ <div class="sev-row"><span class="sev-dot sev-critical"></span><span class="sev-count" id="sevCritical">0</span>critical</div>
82
+ <div class="sev-row"><span class="sev-dot sev-high"></span><span class="sev-count" id="sevHigh">0</span>high</div>
83
+ <div class="sev-row"><span class="sev-dot sev-medium"></span><span class="sev-count" id="sevMedium">0</span>medium</div>
84
+ <div class="sev-row"><span class="sev-dot sev-low"></span><span class="sev-count" id="sevLow">0</span>low</div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="section-title">Activity</div>
89
+ <div class="timeline" id="timeline">
90
+ <div class="empty">Waiting for agent activity...</div>
91
+ </div>
92
+
93
+ <div class="brain" id="brainSection">
94
+ <span class="brain-icon">&#x1F9E0;</span>
95
+ <span class="brain-text" id="brainText">Brain: waiting for data</span>
96
+ <span class="brain-trend" id="brainTrend"></span>
97
+ </div>
98
+
99
+ <script>
100
+ const state = JSON.parse('__INITIAL_STATE__');
101
+
102
+ function scoreColor(s) {
103
+ if (s === null) return '#30363d';
104
+ if (s < 50) return '#da3633';
105
+ if (s < 80) return '#d29922';
106
+ return '#3fb950';
107
+ }
108
+
109
+ function updateGauge(score) {
110
+ const arc = document.getElementById('gaugeArc');
111
+ const text = document.getElementById('gaugeText');
112
+ if (score === null) {
113
+ arc.setAttribute('stroke-dashoffset', '264');
114
+ arc.setAttribute('stroke', '#30363d');
115
+ text.textContent = '--';
116
+ return;
117
+ }
118
+ const offset = 264 - (264 * Math.min(score, 100) / 100);
119
+ arc.setAttribute('stroke-dashoffset', String(offset));
120
+ arc.setAttribute('stroke', scoreColor(score));
121
+ text.textContent = String(Math.round(score));
122
+ }
123
+
124
+ function updateBadge(status) {
125
+ const badge = document.getElementById('statusBadge');
126
+ badge.className = 'badge badge-' + (status || 'idle');
127
+ const labels = {pass:'PASS',fail:'FAIL',scanning:'SCANNING'};
128
+ badge.textContent = labels[status] || 'IDLE';
129
+ }
130
+
131
+ function updateSeverity(sev) {
132
+ document.getElementById('sevCritical').textContent = sev.critical || 0;
133
+ document.getElementById('sevHigh').textContent = sev.high || 0;
134
+ document.getElementById('sevMedium').textContent = sev.medium || 0;
135
+ document.getElementById('sevLow').textContent = sev.low || 0;
136
+ }
137
+
138
+ function formatTime(iso) {
139
+ const d = new Date(iso);
140
+ return d.toLocaleTimeString('en-US', {hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
141
+ }
142
+
143
+ function renderTimeline(entries) {
144
+ const el = document.getElementById('timeline');
145
+ if (!entries.length) {
146
+ el.innerHTML = '<div class="empty">Waiting for agent activity...</div>';
147
+ return;
148
+ }
149
+ el.innerHTML = entries.map(e => {
150
+ const cls = e.status === 'success' ? 'tl-status-success' : e.status === 'error' ? 'tl-status-error' : 'tl-status-warning';
151
+ return '<div class="tl-entry ' + cls + '">' +
152
+ '<span class="tl-time">' + formatTime(e.timestamp) + '</span>' +
153
+ '<span class="tl-tool">' + e.tool + '</span>' +
154
+ '<span class="tl-arrow">\u2192</span>' +
155
+ '<span class="tl-detail">' + e.details + '</span></div>';
156
+ }).join('');
157
+ el.scrollTop = el.scrollHeight;
158
+ }
159
+
160
+ function updateBrain(patterns, trend) {
161
+ const text = document.getElementById('brainText');
162
+ const trendEl = document.getElementById('brainTrend');
163
+ text.textContent = 'Brain: ' + patterns + ' patterns learned';
164
+ if (trend) {
165
+ const arrows = {improving:'\u2191',stable:'\u2194',declining:'\u2193'};
166
+ trendEl.textContent = (arrows[trend] || '') + ' ' + trend;
167
+ trendEl.className = 'brain-trend' + (trend === 'declining' ? ' down' : '');
168
+ }
169
+ }
170
+
171
+ function render() {
172
+ updateGauge(state.currentScore);
173
+ updateBadge(state.status);
174
+ updateSeverity(state.severityBreakdown || {});
175
+ renderTimeline(state.timeline || []);
176
+ updateBrain(state.brainPatterns || 0, state.brainTrend);
177
+ }
178
+
179
+ render();
180
+ </script>
181
+ </body>
182
+ </html>`;
@@ -0,0 +1,4 @@
1
+ export declare const DASHBOARD_URI = "ui://rigour/dashboard";
2
+ /** Get dashboard HTML with current state injected. */
3
+ export declare function getDashboardHtml(): string;
4
+ export { getState, pushTimelineEntry, updateScore, updateBrainStatus, setScanning } from './state.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Dashboard Resource Serving
3
+ *
4
+ * Serves the MCP App dashboard as a ui:// resource.
5
+ * Injects current DashboardState into the HTML on each fetch
6
+ * so the client always gets the latest governance snapshot.
7
+ */
8
+ import { DASHBOARD_HTML } from './html.js';
9
+ import { getState } from './state.js';
10
+ export const DASHBOARD_URI = "ui://rigour/dashboard";
11
+ /** Get dashboard HTML with current state injected. */
12
+ export function getDashboardHtml() {
13
+ const stateJson = JSON.stringify(getState()).replace(/'/g, "\\'");
14
+ return DASHBOARD_HTML.replace('__INITIAL_STATE__', stateJson);
15
+ }
16
+ export { getState, pushTimelineEntry, updateScore, updateBrainStatus, setScanning } from './state.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Dashboard State — Server-side state accumulator
3
+ *
4
+ * Maintains the current governance state that gets injected into
5
+ * the MCP App dashboard HTML on each resource fetch.
6
+ * Single-threaded Node.js — no locking needed.
7
+ */
8
+ export interface TimelineEntry {
9
+ timestamp: string;
10
+ tool: string;
11
+ status: string;
12
+ details: string;
13
+ }
14
+ export interface DashboardState {
15
+ currentScore: number | null;
16
+ status: "pass" | "fail" | "scanning" | null;
17
+ timeline: TimelineEntry[];
18
+ severityBreakdown: Record<string, number>;
19
+ brainPatterns: number;
20
+ brainTrend: string | null;
21
+ }
22
+ export declare function getState(): DashboardState;
23
+ export declare function pushTimelineEntry(tool: string, status: string, details: string): void;
24
+ export declare function updateScore(score: number, status: "pass" | "fail", severity?: Record<string, number>): void;
25
+ export declare function updateBrainStatus(patterns: number, trend?: string): void;
26
+ export declare function setScanning(): void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Dashboard State — Server-side state accumulator
3
+ *
4
+ * Maintains the current governance state that gets injected into
5
+ * the MCP App dashboard HTML on each resource fetch.
6
+ * Single-threaded Node.js — no locking needed.
7
+ */
8
+ const MAX_TIMELINE = 50;
9
+ const state = {
10
+ currentScore: null,
11
+ status: null,
12
+ timeline: [],
13
+ severityBreakdown: {},
14
+ brainPatterns: 0,
15
+ brainTrend: null,
16
+ };
17
+ export function getState() {
18
+ return state;
19
+ }
20
+ export function pushTimelineEntry(tool, status, details) {
21
+ state.timeline.push({
22
+ timestamp: new Date().toISOString(),
23
+ tool,
24
+ status,
25
+ details,
26
+ });
27
+ if (state.timeline.length > MAX_TIMELINE) {
28
+ state.timeline = state.timeline.slice(-MAX_TIMELINE);
29
+ }
30
+ }
31
+ export function updateScore(score, status, severity) {
32
+ state.currentScore = score;
33
+ state.status = status;
34
+ if (severity)
35
+ state.severityBreakdown = severity;
36
+ }
37
+ export function updateBrainStatus(patterns, trend) {
38
+ state.brainPatterns = patterns;
39
+ if (trend)
40
+ state.brainTrend = trend;
41
+ }
42
+ export function setScanning() {
43
+ state.status = "scanning";
44
+ }
package/dist/index.js CHANGED
@@ -9,11 +9,14 @@
9
9
  */
10
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
13
13
  import { randomUUID } from "crypto";
14
14
  import { GateRunner } from "@rigour-labs/core";
15
15
  // Utils
16
16
  import { loadConfig, loadMcpSettings, logStudioEvent } from './utils/config.js';
17
+ import { bindServer } from './utils/notifications.js';
18
+ // Dashboard (MCP App)
19
+ import { DASHBOARD_URI, getDashboardHtml, pushTimelineEntry, updateScore } from './dashboard/index.js';
17
20
  // Tool definitions
18
21
  import { TOOL_DEFINITIONS } from './tools/definitions.js';
19
22
  // Tool handlers
@@ -27,7 +30,9 @@ import { handleHooksCheck, handleHooksInit } from './tools/hooks-handler.js';
27
30
  import { handleCheckDeep, handleDeepStats } from './tools/deep-handlers.js';
28
31
  import { handleMcpGetSettings, handleMcpSetSettings } from './tools/mcp-settings-handler.js';
29
32
  // ─── Server Setup ─────────────────────────────────────────────────
30
- const server = new Server({ name: "rigour-mcp", version: "3.0.1" }, { capabilities: { tools: {}, prompts: {} } });
33
+ const server = new Server({ name: "rigour-mcp", version: "3.0.1" }, { capabilities: { tools: {}, prompts: {}, logging: {}, resources: {} } });
34
+ // Bind server for logging notifications from handlers
35
+ bindServer(server);
31
36
  // ─── Tool Listing ─────────────────────────────────────────────────
32
37
  // Only expose essential tools by default to improve agent tool selection.
33
38
  // Research shows agents degrade at 30+ tools (wrong picks, hallucinated args).
@@ -38,6 +43,7 @@ const ESSENTIAL_TOOLS = new Set([
38
43
  'rigour_recall', // Load project memory (START of every task)
39
44
  'rigour_remember', // Store conventions/decisions
40
45
  'rigour_explain', // Explain gate failures
46
+ 'rigour_get_fix_packet', // Get structured fix instructions on FAIL
41
47
  'rigour_review', // Review diffs
42
48
  'rigour_security_audit', // CVE check
43
49
  'rigour_forget', // Remove stored memory
@@ -45,6 +51,27 @@ const ESSENTIAL_TOOLS = new Set([
45
51
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
46
52
  tools: TOOL_DEFINITIONS.filter(t => ESSENTIAL_TOOLS.has(t.name)),
47
53
  }));
54
+ // ─── Resource Listing (MCP App Dashboard) ────────────────────────
55
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
56
+ resources: [{
57
+ uri: DASHBOARD_URI,
58
+ name: "Rigour Governance Dashboard",
59
+ description: "Real-time governance dashboard — quality scores, agent activity timeline, severity breakdown",
60
+ mimeType: "text/html",
61
+ }],
62
+ }));
63
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
64
+ if (request.params.uri === DASHBOARD_URI) {
65
+ return {
66
+ contents: [{
67
+ uri: DASHBOARD_URI,
68
+ mimeType: "text/html",
69
+ text: getDashboardHtml(),
70
+ }],
71
+ };
72
+ }
73
+ throw new Error(`Unknown resource: ${request.params.uri}`);
74
+ });
48
75
  // ─── Tool Dispatch ────────────────────────────────────────────────
49
76
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
50
77
  const { name, arguments: args } = request.params;
@@ -165,6 +192,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
165
192
  type: "tool_response", requestId, tool: name, status: "success",
166
193
  content: result.content, _rigour_report: result._rigour_report,
167
194
  });
195
+ // ─── Dashboard State + MCP App UI Hint ──────────────
196
+ const report = result._rigour_report;
197
+ const details = report
198
+ ? `${report.status.toUpperCase()} — Score: ${report.stats?.score ?? '?'}/100`
199
+ : "completed";
200
+ pushTimelineEntry(name, result.isError ? "error" : "success", details);
201
+ if (report?.stats) {
202
+ updateScore(report.stats.score ?? 0, report.status === "PASS" ? "pass" : "fail", report.stats.severity_breakdown);
203
+ }
204
+ // Attach UI hint for MCP-App-capable clients (Claude Desktop, VS Code, ChatGPT, Goose)
205
+ if (!result.isError) {
206
+ result._meta = { ui: { resourceUri: DASHBOARD_URI } };
207
+ }
208
+ // Notify clients that the dashboard resource has updated
209
+ try {
210
+ await server.notification({
211
+ method: "notifications/resources/updated",
212
+ params: { uri: DASHBOARD_URI },
213
+ });
214
+ }
215
+ catch {
216
+ // Client doesn't support resource notifications — fine
217
+ }
168
218
  return result;
169
219
  }
170
220
  catch (error) {
@@ -6,6 +6,7 @@
6
6
  * @since v4.0.0
7
7
  */
8
8
  import path from "path";
9
+ import { notifyProgress } from '../utils/notifications.js';
9
10
  function resolveDeepExecution(args) {
10
11
  const requestedProvider = (args.provider || '').toLowerCase();
11
12
  const isForcedLocal = requestedProvider === 'local';
@@ -31,7 +32,10 @@ export async function handleCheckDeep(runner, cwd, config, args) {
31
32
  apiBaseUrl: args.apiBaseUrl,
32
33
  modelName: args.modelName,
33
34
  };
35
+ notifyProgress("info", `Deep analysis starting (${execution.isLocal ? 'local sidecar' : execution.provider})...`);
34
36
  const report = await runner.run(cwd, undefined, deepOpts);
37
+ const notifyScore = report.stats.score ?? 100;
38
+ notifyProgress(report.status === "PASS" ? "info" : "warning", `Deep analysis complete: ${notifyScore}/100 \u2014 ${report.failures.length} findings`);
35
39
  // Persist to SQLite (best-effort). Skip during tests to avoid CI timing flakes.
36
40
  if (!isTestRuntime()) {
37
41
  try {
@@ -27,7 +27,7 @@ export const TOOL_DEFINITIONS = [
27
27
  // ─── Core Quality Gates ───────────────────────────────
28
28
  {
29
29
  name: "rigour_check",
30
- description: "Run quality gate checks on the project. MUST be called before declaring any coding task complete. Checks code complexity, file size, required docs, security patterns, and more. Returns PASS or FAIL with details.",
30
+ description: "Run quality gate checks on the project. MUST be called before declaring any coding task complete. Checks code complexity, file size, required docs, security patterns, and more. Returns PASS or FAIL with details. **Always show the user the headline summary from this tool's output** — it tells them what Rigour caught. If FAIL, call rigour_get_fix_packet for structured fix instructions with exact file locations and step-by-step remediation.",
31
31
  inputSchema: {
32
32
  type: "object",
33
33
  properties: {
@@ -52,7 +52,7 @@ export const TOOL_DEFINITIONS = [
52
52
  },
53
53
  {
54
54
  name: "rigour_explain",
55
- description: "Explain the last quality gate failures with actionable bullets. Matches the CLI 'explain' command.",
55
+ description: "Explain WHY quality gates failed in human-readable language. Use this to understand the reasoning behind each violation before fixing. For machine-readable fix instructions, use rigour_get_fix_packet instead.",
56
56
  inputSchema: {
57
57
  type: "object",
58
58
  properties: cwdParam(),
@@ -84,7 +84,7 @@ export const TOOL_DEFINITIONS = [
84
84
  },
85
85
  {
86
86
  name: "rigour_get_fix_packet",
87
- description: "Retrieves a prioritized 'Fix Packet' (v2 schema) containing detailed machine-readable diagnostic data.",
87
+ description: "Call this after rigour_check returns FAIL. Returns a prioritized Fix Packet with exact violations, file locations, and step-by-step fix instructions. Apply fixes in severity order (critical first), then re-run rigour_check to verify. Repeat until PASS. **After applying all fixes, tell the user what Rigour caught and that fixes were applied.**",
88
88
  inputSchema: {
89
89
  type: "object",
90
90
  properties: cwdParam(),
@@ -9,6 +9,7 @@ import fs from "fs-extra";
9
9
  import path from "path";
10
10
  import { FixPacketService } from "@rigour-labs/core";
11
11
  import { logStudioEvent } from '../utils/config.js';
12
+ import { notifyProgress } from '../utils/notifications.js';
12
13
  export async function handleRun(cwd, command, requestId) {
13
14
  // 1. Log Interceptable Event
14
15
  await logStudioEvent(cwd, {
@@ -54,8 +55,10 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
54
55
  maxRetries,
55
56
  dryRun,
56
57
  });
58
+ notifyProgress("info", `Supervisor started: ${command} (max ${maxRetries} retries)`);
57
59
  while (iteration < maxRetries) {
58
60
  iteration++;
61
+ notifyProgress("info", `Supervisor iteration ${iteration}/${maxRetries}...`);
59
62
  if (!dryRun) {
60
63
  try {
61
64
  await execa(command, { shell: true, cwd });
@@ -84,6 +87,7 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
84
87
  failedGates: failedGateIds,
85
88
  });
86
89
  if (lastReport.status === "PASS") {
90
+ notifyProgress("info", `Supervisor PASSED on iteration ${iteration} \u2014 Score: ${lastReport.stats.score ?? '?'}/100`);
87
91
  const score = lastReport.stats.score !== undefined ? ` | Score: ${lastReport.stats.score}/100` : '';
88
92
  result = {
89
93
  content: [{
@@ -94,6 +98,7 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
94
98
  break;
95
99
  }
96
100
  if (iteration >= maxRetries) {
101
+ notifyProgress("error", `Supervisor FAILED after ${iteration} iterations \u2014 ${lastReport.failures.length} violations remain`);
97
102
  // Use FixPacketService for structured output instead of ad-hoc formatting
98
103
  let fixPacketText;
99
104
  if (config) {
@@ -6,6 +6,7 @@
6
6
  * @since v2.17.0 — extracted from monolithic index.ts
7
7
  */
8
8
  import { PatternMatcher, loadPatternIndex, getDefaultIndexPath, StalenessDetector, SecurityDetector, } from "@rigour-labs/core/pattern-index";
9
+ import { notifyProgress } from '../utils/notifications.js';
9
10
  export async function handleCheckPattern(cwd, patternName, type, intent) {
10
11
  const indexPath = getDefaultIndexPath(cwd);
11
12
  const index = await loadPatternIndex(indexPath);
@@ -66,7 +67,9 @@ export async function handleCheckPattern(cwd, patternName, type, intent) {
66
67
  return { content: [{ type: "text", text: resultText }] };
67
68
  }
68
69
  export async function handleSecurityAudit(cwd) {
70
+ notifyProgress("info", "Running CVE security audit...");
69
71
  const security = new SecurityDetector(cwd);
70
72
  const summary = await security.getSecuritySummary();
73
+ notifyProgress("info", "Security audit complete");
71
74
  return { content: [{ type: "text", text: summary }] };
72
75
  }
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Quality Gate Tool Handlers
3
+ *
4
+ * Handlers for: rigour_check, rigour_explain, rigour_status,
5
+ * rigour_get_fix_packet, rigour_list_gates, rigour_get_config
6
+ *
7
+ * @since v2.17.0 — extracted from monolithic index.ts
8
+ */
9
+ import { renderMcpHeadline, renderFixAttribution } from "@rigour-labs/core";
10
+ import { notifyProgress } from '../utils/notifications.js';
1
11
  function resolveDeepExecution(args) {
2
12
  const requestedProvider = (args.provider || '').toLowerCase();
3
13
  const isForcedLocal = requestedProvider === 'local';
@@ -42,7 +52,22 @@ export async function handleCheck(runner, cwd, args = {}) {
42
52
  modelName: args.modelName,
43
53
  };
44
54
  }
55
+ notifyProgress("info", fileTargets ? `Scanning ${fileTargets.length} files...` : "Scanning project...");
56
+ if (deepMode !== 'off') {
57
+ notifyProgress("info", `Deep analysis: ${execution.isLocal ? 'local sidecar' : execution.provider} (${deepMode} mode)`);
58
+ }
45
59
  const report = await runner.run(cwd, fileTargets, deepOpts);
60
+ if (report.status === "PASS") {
61
+ notifyProgress("info", `PASS \u2014 Score: ${report.stats.score ?? '?'}/100`);
62
+ }
63
+ else {
64
+ const sev = report.stats.severity_breakdown || {};
65
+ const sevParts = Object.entries(sev).filter(([, c]) => c > 0).map(([s, c]) => `${c} ${s}`).join(', ');
66
+ notifyProgress("warning", `FAIL \u2014 ${sevParts || report.failures.length + ' violations'} (Score: ${report.stats.score ?? '?'}/100)`);
67
+ const worst = report.failures.find(f => f.severity === 'critical') || report.failures[0];
68
+ if (worst)
69
+ notifyProgress("warning", `${worst.title}${worst.files?.[0] ? ' in ' + worst.files[0] : ''}`);
70
+ }
46
71
  const scoreText = formatScoreText(report.stats);
47
72
  const sevText = formatSeverityText(report.stats);
48
73
  const deepText = deepMode === 'off'
@@ -51,10 +76,12 @@ export async function handleCheck(runner, cwd, args = {}) {
51
76
  `${execution.isLocal
52
77
  ? '\nPrivacy: Local sidecar/model execution. Code remains on this machine.'
53
78
  : `\nPrivacy: Cloud provider execution. Code context may be sent to ${execution.provider} API.`}`;
79
+ // Human-facing headline — agents naturally pass this through to users
80
+ const headline = renderMcpHeadline(report);
54
81
  const result = {
55
82
  content: [{
56
83
  type: "text",
57
- text: `RIGOUR AUDIT RESULT: ${report.status}${scoreText}${sevText}${deepText}\n\nSummary:\n${Object.entries(report.summary).map(([k, v]) => `- ${k}: ${v}`).join("\n")}`,
84
+ text: `${headline}\n\n${scoreText.trim()}${sevText}${deepText}\n\nSummary:\n${Object.entries(report.summary).map(([k, v]) => `- ${k}: ${v}`).join("\n")}`,
58
85
  }],
59
86
  };
60
87
  result._rigour_report = report;
@@ -105,11 +132,18 @@ export async function handleGetFixPacket(runner, cwd, config) {
105
132
  content: [{ type: "text", text: `ALL QUALITY GATES PASSED.${passScore} The current state meets the required engineering standards.` }],
106
133
  };
107
134
  }
135
+ notifyProgress("info", `Generating fix packet for ${report.failures.length} violations...`);
108
136
  const { FixPacketService } = await import("@rigour-labs/core");
109
137
  const fixPacketService = new FixPacketService();
110
138
  const fixPacket = fixPacketService.generate(report, config);
139
+ // Find worst violation for attribution
140
+ const worst = report.failures.find(f => f.severity === 'critical')
141
+ || report.failures.find(f => f.severity === 'high')
142
+ || report.failures[0];
143
+ const worstLabel = worst ? worst.title : 'quality violations';
144
+ const attribution = renderFixAttribution(report.failures.length, worstLabel);
111
145
  return {
112
- content: [{ type: "text", text: formatFixPacketText(fixPacket, report) }],
146
+ content: [{ type: "text", text: formatFixPacketText(fixPacket, report) + attribution }],
113
147
  };
114
148
  }
115
149
  /**
@@ -1,9 +1,11 @@
1
1
  import { parseDiff } from '../utils/config.js';
2
+ import { notifyProgress } from '../utils/notifications.js';
2
3
  export async function handleReview(runner, cwd, diff, changedFiles) {
3
4
  // 1. Map diff to line numbers for filtering
4
5
  const diffMapping = parseDiff(diff);
5
6
  const targetFiles = changedFiles || Object.keys(diffMapping);
6
7
  // 2. Run high-fidelity analysis on changed files
8
+ notifyProgress("info", `Reviewing ${targetFiles.length} changed files...`);
7
9
  const report = await runner.run(cwd, targetFiles);
8
10
  // 3. Filter failures to only those on changed lines (or global gate failures)
9
11
  const filteredFailures = report.failures.filter(failure => {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MCP Logging Notifications — Singleton
3
+ *
4
+ * Gives all handler modules access to server.sendLoggingMessage()
5
+ * without threading the server instance through every function signature.
6
+ *
7
+ * Usage:
8
+ * bindServer(server) — call once at startup
9
+ * notifyProgress("info", msg) — fire-and-forget from any handler
10
+ */
11
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ /** Bind the MCP server instance (call once at startup). */
13
+ export declare function bindServer(server: Server): void;
14
+ /**
15
+ * Send a logging notification to the MCP client.
16
+ * Fire-and-forget — silently drops if client doesn't support logging.
17
+ */
18
+ export declare function notifyProgress(level: "debug" | "info" | "notice" | "warning" | "error" | "critical", message: string, logger?: string): void;
@@ -0,0 +1,19 @@
1
+ let _server = null;
2
+ /** Bind the MCP server instance (call once at startup). */
3
+ export function bindServer(server) {
4
+ _server = server;
5
+ }
6
+ /**
7
+ * Send a logging notification to the MCP client.
8
+ * Fire-and-forget — silently drops if client doesn't support logging.
9
+ */
10
+ export function notifyProgress(level, message, logger = "rigour") {
11
+ if (!_server)
12
+ return;
13
+ try {
14
+ void _server.sendLoggingMessage({ level, logger, data: message });
15
+ }
16
+ catch {
17
+ // Client may not support logging — graceful fallback
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,30 +1,44 @@
1
1
  {
2
2
  "name": "@rigour-labs/mcp",
3
- "version": "5.2.3",
4
- "description": "MCP server for AI code governance — OWASP LLM Top 10 (10/10), real-time hooks, 25+ security patterns, hallucinated import detection, multi-agent governance. Works with Claude, Cursor, Cline, Windsurf, Gemini. Industry presets for HIPAA, SOC2, FedRAMP.",
3
+ "version": "5.2.5",
4
+ "description": "MCP server + live dashboard for AI code governance — OWASP LLM Top 10 (10/10), real-time MCP App UI, 25+ security patterns, Bayesian learning Brain, hallucinated import detection, multi-agent governance. Works with Claude, Cursor, VS Code, ChatGPT, Goose, Windsurf. Industry presets for HIPAA, SOC2, FedRAMP.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
7
7
  "icon": "https://raw.githubusercontent.com/rigour-labs/rigour/main/docs/assets/icon.svg",
8
8
  "keywords": [
9
+ "ai",
10
+ "llm",
9
11
  "mcp",
12
+ "mcp-app",
13
+ "mcp-server",
10
14
  "model-context-protocol",
11
- "owasp-llm-top-10",
15
+ "ai-agent",
12
16
  "ai-code-governance",
17
+ "owasp-llm-top-10",
18
+ "bayesian-learning",
13
19
  "real-time-hooks",
14
20
  "security-patterns",
15
21
  "hallucinated-imports",
16
- "industry-presets",
17
- "hipaa",
18
- "soc2",
19
- "fedramp",
22
+ "vibe-coding",
20
23
  "claude",
21
24
  "cursor",
22
25
  "cline",
23
26
  "windsurf",
27
+ "copilot",
28
+ "gemini",
29
+ "codex",
30
+ "chatgpt",
31
+ "goose",
24
32
  "multi-agent-governance",
25
33
  "quality-gates",
26
- "static-analysis",
27
- "code-review"
34
+ "fix-packets",
35
+ "agent-governance",
36
+ "drift-detection",
37
+ "live-dashboard",
38
+ "code-quality",
39
+ "ai-safety",
40
+ "deep-learning",
41
+ "rlaif"
28
42
  ],
29
43
  "type": "module",
30
44
  "mcpName": "io.github.rigour-labs/rigour",
@@ -48,7 +62,7 @@
48
62
  "execa": "^8.0.1",
49
63
  "fs-extra": "^11.2.0",
50
64
  "yaml": "^2.8.2",
51
- "@rigour-labs/core": "5.2.3"
65
+ "@rigour-labs/core": "5.2.5"
52
66
  },
53
67
  "devDependencies": {
54
68
  "@types/node": "^25.0.3",