@rigour-labs/mcp 4.0.4 → 4.1.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/README.md CHANGED
@@ -24,7 +24,7 @@ Rigour moves code quality enforcement from the "Post-Commit" phase to the "In-Pr
24
24
  - **Security Audits**: Real-time CVE detection for dependencies the AI is suggesting.
25
25
  - **Multi-Agent Governance**: Agent registration, scope isolation, checkpoint supervision, and verified handoffs for multi-agent workflows.
26
26
  - **Industry Presets**: SOC2, HIPAA, FedRAMP-ready gate configurations.
27
- - **Zero Cloud**: 100% local analysis. Your code never leaves your machine.
27
+ - **Local-First**: Deterministic gates run locally. If deep analysis is configured with a cloud provider, code context may be sent to that provider.
28
28
 
29
29
  ---
30
30
 
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
13
13
  import { randomUUID } from "crypto";
14
14
  import { GateRunner } from "@rigour-labs/core";
15
15
  // Utils
16
- import { loadConfig, logStudioEvent } from './utils/config.js';
16
+ import { loadConfig, loadMcpSettings, logStudioEvent } from './utils/config.js';
17
17
  // Tool definitions
18
18
  import { TOOL_DEFINITIONS } from './tools/definitions.js';
19
19
  // Tool handlers
@@ -25,6 +25,7 @@ import { handleAgentRegister, handleCheckpoint, handleHandoff, handleAgentDeregi
25
25
  import { handleReview } from './tools/review-handler.js';
26
26
  import { handleHooksCheck, handleHooksInit } from './tools/hooks-handler.js';
27
27
  import { handleCheckDeep, handleDeepStats } from './tools/deep-handlers.js';
28
+ import { handleMcpGetSettings, handleMcpSetSettings } from './tools/mcp-settings-handler.js';
28
29
  // ─── Server Setup ─────────────────────────────────────────────────
29
30
  const server = new Server({ name: "rigour-mcp", version: "3.0.1" }, { capabilities: { tools: {}, prompts: {} } });
30
31
  // ─── Tool Listing ─────────────────────────────────────────────────
@@ -43,9 +44,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
43
44
  let result;
44
45
  switch (name) {
45
46
  // Quality gates
46
- case "rigour_check":
47
- result = await handleCheck(runner, cwd);
47
+ case "rigour_check": {
48
+ const { files, deep, pro, apiKey, provider, apiBaseUrl, modelName } = args;
49
+ const mcpSettings = await loadMcpSettings(cwd);
50
+ result = await handleCheck(runner, cwd, {
51
+ files,
52
+ deep: deep ?? mcpSettings.deep_default_mode,
53
+ pro,
54
+ apiKey,
55
+ provider,
56
+ apiBaseUrl,
57
+ modelName,
58
+ });
48
59
  break;
60
+ }
49
61
  case "rigour_explain":
50
62
  result = await handleExplain(runner, cwd);
51
63
  break;
@@ -61,6 +73,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
61
73
  case "rigour_get_config":
62
74
  result = handleGetConfig(config);
63
75
  break;
76
+ case "rigour_mcp_get_settings":
77
+ result = await handleMcpGetSettings(cwd);
78
+ break;
79
+ case "rigour_mcp_set_settings":
80
+ result = await handleMcpSetSettings(cwd, args);
81
+ break;
64
82
  // Memory
65
83
  case "rigour_remember":
66
84
  result = await handleRemember(cwd, args.key, args.value);
@@ -156,6 +174,8 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
156
174
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
157
175
  const { name, arguments: promptArgs } = request.params;
158
176
  const cwd = promptArgs?.cwd || process.env.RIGOUR_CWD || process.cwd();
177
+ const mcpSettings = await loadMcpSettings(cwd);
178
+ const deepMode = mcpSettings.deep_default_mode;
159
179
  const prompt = PROMPT_DEFINITIONS.find((p) => p.name === name);
160
180
  if (!prompt) {
161
181
  throw new Error(`Unknown prompt: ${name}`);
@@ -170,7 +190,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
170
190
  role: "user",
171
191
  content: {
172
192
  type: "text",
173
- text: `Initialize Rigour quality gates for the project at ${cwd}. Run \`rigour_check\` to see current quality score, then use \`rigour_hooks_init\` to install real-time hooks for the detected AI coding tool. Report the score breakdown (overall, AI health, structural) and any critical violations.`,
193
+ text: `Initialize Rigour quality gates for the project at ${cwd}. Run \`rigour_check\` to see current quality score, then use \`rigour_hooks_init\` to install real-time hooks for the detected AI coding tool. MCP default deep mode for \`rigour_check\` is "${deepMode}". Report the score breakdown (overall, AI health, structural) and any critical violations.`,
174
194
  },
175
195
  },
176
196
  ],
@@ -184,7 +204,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
184
204
  role: "user",
185
205
  content: {
186
206
  type: "text",
187
- text: `Run \`rigour_check\` on ${cwd}. If the project FAILS, retrieve the fix packet with \`rigour_get_fix_packet\` and fix every violation in priority order (critical → high → medium → low). After each fix, re-run \`rigour_check\` to verify. Repeat until PASS. Do NOT skip any violation. Report progress after each iteration.`,
207
+ text: `Run \`rigour_check\` on ${cwd}. MCP default deep mode for \`rigour_check\` is "${deepMode}". If the project FAILS, retrieve the fix packet with \`rigour_get_fix_packet\` and fix every violation in priority order (critical → high → medium → low). After each fix, re-run \`rigour_check\` to verify. Repeat until PASS. Do NOT skip any violation. Report progress after each iteration.`,
188
208
  },
189
209
  },
190
210
  ],
@@ -198,7 +218,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
198
218
  role: "user",
199
219
  content: {
200
220
  type: "text",
201
- text: `Perform a full security review on ${cwd}. First run \`rigour_security_audit\` for CVE checks on dependencies. Then run \`rigour_check\` and filter for security-provenance violations (hardcoded secrets, SQL injection, XSS, command injection, path traversal). Report all findings with severity, file locations, and remediation instructions.`,
221
+ text: `Perform a full security review on ${cwd}. First run \`rigour_security_audit\` for CVE checks on dependencies. Then run \`rigour_check\` (default deep mode "${deepMode}") and filter for security-provenance violations (hardcoded secrets, SQL injection, XSS, command injection, path traversal). Report all findings with severity, file locations, and remediation instructions.`,
202
222
  },
203
223
  },
204
224
  ],
@@ -212,7 +232,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
212
232
  role: "user",
213
233
  content: {
214
234
  type: "text",
215
- text: `Run a pre-commit quality check on ${cwd}. Execute \`rigour_check\` and \`rigour_hooks_check\` on all staged files. If any critical or high severity violations exist, list them and block the commit. For medium/low violations, warn but allow. Provide a one-line summary: PASS (safe to commit) or FAIL (must fix first).`,
235
+ text: `Run a pre-commit quality check on ${cwd}. Execute \`rigour_check\` (default deep mode "${deepMode}") and \`rigour_hooks_check\` on all staged files. If any critical or high severity violations exist, list them and block the commit. For medium/low violations, warn but allow. Provide a one-line summary: PASS (safe to commit) or FAIL (must fix first).`,
216
236
  },
217
237
  },
218
238
  ],
@@ -226,7 +246,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
226
246
  role: "user",
227
247
  content: {
228
248
  type: "text",
229
- text: `Generate an AI code health report for ${cwd}. Run \`rigour_check\` and focus on AI-drift provenance violations: hallucinated imports, duplication drift, context window artifacts, inconsistent error handling, and promise safety. Compare AI health score vs structural score. Provide a summary table of AI-specific issues and concrete next steps to improve the AI health score.`,
249
+ text: `Generate an AI code health report for ${cwd}. Run \`rigour_check\` (default deep mode "${deepMode}") and focus on AI-drift provenance violations: hallucinated imports, duplication drift, context window artifacts, inconsistent error handling, and promise safety. Compare AI health score vs structural score. Provide a summary table of AI-specific issues and concrete next steps to improve the AI health score.`,
230
250
  },
231
251
  },
232
252
  ],
@@ -9,12 +9,23 @@
9
9
  import fs from "fs-extra";
10
10
  import path from "path";
11
11
  import { logStudioEvent } from '../utils/config.js';
12
+ function parseJsonOrNull(raw) {
13
+ try {
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
12
20
  // ─── Agent Register ───────────────────────────────────────────────
13
21
  export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
14
22
  const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
15
23
  let session = { agents: [], startedAt: new Date().toISOString() };
16
24
  if (await fs.pathExists(sessionPath)) {
17
- session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
25
+ const parsed = parseJsonOrNull(await fs.readFile(sessionPath, 'utf-8'));
26
+ if (parsed) {
27
+ session = parsed;
28
+ }
18
29
  }
19
30
  const existingIdx = session.agents.findIndex((a) => a.agentId === agentId);
20
31
  if (existingIdx >= 0) {
@@ -63,7 +74,10 @@ export async function handleCheckpoint(cwd, progressPct, filesChanged, summary,
63
74
  status: 'active',
64
75
  };
65
76
  if (await fs.pathExists(checkpointPath)) {
66
- session = JSON.parse(await fs.readFile(checkpointPath, 'utf-8'));
77
+ const parsed = parseJsonOrNull(await fs.readFile(checkpointPath, 'utf-8'));
78
+ if (parsed) {
79
+ session = parsed;
80
+ }
67
81
  }
68
82
  const checkpointId = `cp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
69
83
  const warnings = [];
@@ -128,7 +142,10 @@ export async function handleAgentDeregister(cwd, agentId, requestId) {
128
142
  if (!await fs.pathExists(sessionPath)) {
129
143
  return { content: [{ type: "text", text: `❌ No active agent session found.` }] };
130
144
  }
131
- const session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
145
+ const session = parseJsonOrNull(await fs.readFile(sessionPath, 'utf-8'));
146
+ if (!session || !Array.isArray(session.agents)) {
147
+ return { content: [{ type: "text", text: `❌ Agent session file is malformed.` }], isError: true };
148
+ }
132
149
  const initialCount = session.agents.length;
133
150
  session.agents = session.agents.filter((a) => a.agentId !== agentId);
134
151
  if (session.agents.length === initialCount) {
@@ -150,7 +167,12 @@ export async function handleHandoffAccept(cwd, handoffId, agentId, requestId) {
150
167
  return { content: [{ type: "text", text: `❌ No handoffs found.` }] };
151
168
  }
152
169
  const content = await fs.readFile(handoffPath, 'utf-8');
153
- const handoffs = content.trim().split('\n').filter(l => l).map(line => JSON.parse(line));
170
+ const handoffs = content
171
+ .trim()
172
+ .split('\n')
173
+ .filter(l => l)
174
+ .map(line => parseJsonOrNull(line))
175
+ .filter((entry) => !!entry);
154
176
  const handoff = handoffs.find((h) => h.handoffId === handoffId);
155
177
  if (!handoff) {
156
178
  return { content: [{ type: "text", text: `❌ Handoff "${handoffId}" not found.` }] };
@@ -6,15 +6,25 @@
6
6
  * @since v4.0.0
7
7
  */
8
8
  import path from "path";
9
+ function resolveDeepExecution(args) {
10
+ const requestedProvider = (args.provider || '').toLowerCase();
11
+ const isForcedLocal = requestedProvider === 'local';
12
+ const isLocal = !args.apiKey || isForcedLocal;
13
+ return {
14
+ isLocal,
15
+ provider: isLocal ? 'local' : (args.provider || 'claude'),
16
+ };
17
+ }
9
18
  /**
10
19
  * Run quality gates with deep LLM-powered analysis.
11
20
  */
12
21
  export async function handleCheckDeep(runner, cwd, config, args) {
22
+ const execution = resolveDeepExecution(args);
13
23
  const deepOpts = {
14
24
  enabled: true,
15
25
  pro: !!args.pro,
16
26
  apiKey: args.apiKey,
17
- provider: args.apiKey ? (args.provider || 'claude') : 'local',
27
+ provider: execution.provider,
18
28
  apiBaseUrl: args.apiBaseUrl,
19
29
  modelName: args.modelName,
20
30
  };
@@ -26,7 +36,7 @@ export async function handleCheckDeep(runner, cwd, config, args) {
26
36
  if (db) {
27
37
  const repoName = path.basename(cwd);
28
38
  const scanId = insertScan(db, repoName, report, {
29
- deepTier: args.pro ? 'pro' : (args.apiKey ? 'cloud' : 'deep'),
39
+ deepTier: args.pro ? 'pro' : (execution.isLocal ? 'deep' : 'cloud'),
30
40
  deepModel: report.stats.deep?.model,
31
41
  });
32
42
  insertFindings(db, scanId, report.failures);
@@ -41,19 +51,19 @@ export async function handleCheckDeep(runner, cwd, config, args) {
41
51
  const aiHealth = stats.ai_health_score ?? 100;
42
52
  const codeQuality = stats.code_quality_score ?? stats.structural_score ?? 100;
43
53
  const overall = stats.score ?? 100;
44
- const isLocal = !args.apiKey;
54
+ const isLocal = execution.isLocal;
45
55
  let text = `RIGOUR DEEP ANALYSIS: ${report.status}\n\n`;
46
56
  text += `AI Health: ${aiHealth}/100\n`;
47
57
  text += `Code Quality: ${codeQuality}/100\n`;
48
58
  text += `Overall: ${overall}/100\n\n`;
49
59
  if (isLocal) {
50
- text += `🔒 100% local. Code never left this machine.\n`;
60
+ text += `🔒 Local sidecar/model execution. Code remains on this machine.\n`;
51
61
  }
52
62
  else {
53
- text += `☁️ Code was sent to ${args.provider || 'cloud'} API.\n`;
63
+ text += `☁️ Cloud provider execution. Code context may be sent to ${execution.provider} API.\n`;
54
64
  }
55
65
  if (stats.deep) {
56
- const tier = stats.deep.tier === 'cloud' ? (args.provider || 'cloud') : stats.deep.tier;
66
+ const tier = stats.deep.tier === 'cloud' ? execution.provider : stats.deep.tier;
57
67
  const model = stats.deep.model || 'unknown';
58
68
  const inferenceSec = stats.deep.total_ms ? (stats.deep.total_ms / 1000).toFixed(1) + 's' : '';
59
69
  text += `Model: ${model} (${tier}) ${inferenceSec}\n`;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { handleCheckDeep } from './deep-handlers.js';
3
+ describe('handleCheckDeep privacy routing', () => {
4
+ const baseReport = {
5
+ status: 'PASS',
6
+ summary: { 'ast-analysis': 'PASS' },
7
+ failures: [],
8
+ stats: {
9
+ duration_ms: 10,
10
+ score: 100,
11
+ ai_health_score: 100,
12
+ structural_score: 100,
13
+ deep: { enabled: true, tier: 'deep', model: 'Qwen2.5-Coder-0.5B', total_ms: 1000 },
14
+ },
15
+ };
16
+ it('reports local execution by default', async () => {
17
+ const run = vi.fn().mockResolvedValue(baseReport);
18
+ const runner = { run };
19
+ const result = await handleCheckDeep(runner, '/repo', {}, {});
20
+ expect(run).toHaveBeenCalledWith('/repo', undefined, {
21
+ enabled: true,
22
+ pro: false,
23
+ apiKey: undefined,
24
+ provider: 'local',
25
+ apiBaseUrl: undefined,
26
+ modelName: undefined,
27
+ });
28
+ expect(result.content[0].text).toContain('Local sidecar/model execution');
29
+ });
30
+ it('respects provider=local even when apiKey exists', async () => {
31
+ const run = vi.fn().mockResolvedValue(baseReport);
32
+ const runner = { run };
33
+ const result = await handleCheckDeep(runner, '/repo', {}, {
34
+ apiKey: 'sk-test',
35
+ provider: 'local',
36
+ });
37
+ expect(run).toHaveBeenCalledWith('/repo', undefined, {
38
+ enabled: true,
39
+ pro: false,
40
+ apiKey: 'sk-test',
41
+ provider: 'local',
42
+ apiBaseUrl: undefined,
43
+ modelName: undefined,
44
+ });
45
+ expect(result.content[0].text).toContain('Local sidecar/model execution');
46
+ expect(result.content[0].text).not.toContain('Code context may be sent');
47
+ });
48
+ });
@@ -21,6 +21,83 @@ export declare const TOOL_DEFINITIONS: ({
21
21
  inputSchema: {
22
22
  type: string;
23
23
  properties: {
24
+ files: {
25
+ type: string;
26
+ items: {
27
+ type: string;
28
+ };
29
+ description: string;
30
+ };
31
+ deep: {
32
+ type: string;
33
+ enum: string[];
34
+ description: string;
35
+ };
36
+ pro: {
37
+ type: string;
38
+ description: string;
39
+ };
40
+ apiKey: {
41
+ type: string;
42
+ description: string;
43
+ };
44
+ provider: {
45
+ type: string;
46
+ description: string;
47
+ };
48
+ apiBaseUrl: {
49
+ type: string;
50
+ description: string;
51
+ };
52
+ modelName: {
53
+ type: string;
54
+ description: string;
55
+ };
56
+ cwd: {
57
+ type: "string";
58
+ description: string;
59
+ };
60
+ };
61
+ required: string[];
62
+ };
63
+ annotations: {
64
+ title: string;
65
+ readOnlyHint: boolean;
66
+ destructiveHint: boolean;
67
+ idempotentHint: boolean;
68
+ openWorldHint: boolean;
69
+ };
70
+ } | {
71
+ name: string;
72
+ description: string;
73
+ inputSchema: {
74
+ type: string;
75
+ properties: {
76
+ cwd: {
77
+ type: "string";
78
+ description: string;
79
+ };
80
+ };
81
+ required: string[];
82
+ };
83
+ annotations: {
84
+ title: string;
85
+ readOnlyHint: boolean;
86
+ destructiveHint: boolean;
87
+ idempotentHint: boolean;
88
+ openWorldHint: boolean;
89
+ };
90
+ } | {
91
+ name: string;
92
+ description: string;
93
+ inputSchema: {
94
+ type: string;
95
+ properties: {
96
+ deep_default_mode: {
97
+ type: string;
98
+ enum: string[];
99
+ description: string;
100
+ };
24
101
  cwd: {
25
102
  type: "string";
26
103
  description: string;
@@ -27,10 +27,19 @@ 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. Matches the CLI 'check' command.",
30
+ description: "Run quality gate checks on the project. Deep modes: off (fast deterministic gates only), quick (deep enabled with standard local tier unless cloud provider is configured), full (deep enabled, optional pro model).",
31
31
  inputSchema: {
32
32
  type: "object",
33
- properties: cwdParam(),
33
+ properties: {
34
+ ...cwdParam(),
35
+ files: { type: "array", items: { type: "string" }, description: "Optional file paths (relative to cwd) to limit scan scope for both deterministic and deep checks." },
36
+ deep: { type: "string", enum: ["off", "quick", "full"], description: "Deep mode: 'off' (default), 'quick' (deep enabled with standard model), 'full' (deep enabled, combine with pro=true for larger local model)." },
37
+ pro: { type: "boolean", description: "Use larger local deep model tier when deep is enabled." },
38
+ apiKey: { type: "string", description: "Optional cloud API key for deep analysis." },
39
+ provider: { type: "string", description: "Cloud provider for deep analysis (claude, openai, gemini, groq, mistral, together, deepseek, ollama, etc.)." },
40
+ apiBaseUrl: { type: "string", description: "Custom API base URL for self-hosted/proxy deep endpoints." },
41
+ modelName: { type: "string", description: "Override cloud model name for deep analysis." },
42
+ },
34
43
  required: ["cwd"],
35
44
  },
36
45
  annotations: {
@@ -121,6 +130,45 @@ export const TOOL_DEFINITIONS = [
121
130
  openWorldHint: false,
122
131
  },
123
132
  },
133
+ {
134
+ name: "rigour_mcp_get_settings",
135
+ description: "Get Rigour MCP runtime settings for this repository (.rigour/mcp-settings.json).",
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: cwdParam(),
139
+ required: ["cwd"],
140
+ },
141
+ annotations: {
142
+ title: "Get MCP Settings",
143
+ readOnlyHint: true,
144
+ destructiveHint: false,
145
+ idempotentHint: true,
146
+ openWorldHint: false,
147
+ },
148
+ },
149
+ {
150
+ name: "rigour_mcp_set_settings",
151
+ description: "Set Rigour MCP runtime settings for this repository. Currently supports deep_default_mode: off | quick | full.",
152
+ inputSchema: {
153
+ type: "object",
154
+ properties: {
155
+ ...cwdParam(),
156
+ deep_default_mode: {
157
+ type: "string",
158
+ enum: ["off", "quick", "full"],
159
+ description: "Default deep mode applied to rigour_check when deep is not passed.",
160
+ },
161
+ },
162
+ required: ["cwd", "deep_default_mode"],
163
+ },
164
+ annotations: {
165
+ title: "Set MCP Settings",
166
+ readOnlyHint: false,
167
+ destructiveHint: false,
168
+ idempotentHint: true,
169
+ openWorldHint: false,
170
+ },
171
+ },
124
172
  // ─── Memory Persistence ───────────────────────────────
125
173
  {
126
174
  name: "rigour_remember",
@@ -122,7 +122,13 @@ async function pollArbitration(cwd, rid, timeout) {
122
122
  const content = await fs.readFile(eventsPath, 'utf-8');
123
123
  const lines = content.split('\n').filter(l => l.trim());
124
124
  for (const line of lines.reverse()) {
125
- const event = JSON.parse(line);
125
+ let event;
126
+ try {
127
+ event = JSON.parse(line);
128
+ }
129
+ catch {
130
+ continue;
131
+ }
126
132
  if (event.tool === 'human_arbitration' && event.requestId === rid) {
127
133
  return event.decision;
128
134
  }
@@ -40,8 +40,8 @@ export async function handleHooksCheck(cwd, files, timeout) {
40
40
  export async function handleHooksInit(cwd, tool, force = false, dryRun = false) {
41
41
  try {
42
42
  const hookTool = tool;
43
- const checkerPath = 'npx @rigour-labs/cli hooks check';
44
- const files = generateHookFiles(hookTool, checkerPath);
43
+ const checkerCommand = 'rigour hooks check';
44
+ const files = generateHookFiles(hookTool, checkerCommand);
45
45
  if (dryRun) {
46
46
  const preview = files.map(f => `${f.path}:\n${f.content}`).join('\n\n');
47
47
  return {
@@ -61,6 +61,9 @@ export async function handleHooksInit(cwd, tool, force = false, dryRun = false)
61
61
  }
62
62
  await fs.ensureDir(path.dirname(fullPath));
63
63
  await fs.writeFile(fullPath, file.content);
64
+ if (file.executable) {
65
+ await fs.chmod(fullPath, 0o755);
66
+ }
64
67
  written.push(file.path);
65
68
  }
66
69
  const parts = [];
@@ -0,0 +1,12 @@
1
+ type ToolResult = {
2
+ content: {
3
+ type: string;
4
+ text: string;
5
+ }[];
6
+ isError?: boolean;
7
+ };
8
+ export declare function handleMcpGetSettings(cwd: string): Promise<ToolResult>;
9
+ export declare function handleMcpSetSettings(cwd: string, settings: {
10
+ deep_default_mode?: string;
11
+ }): Promise<ToolResult>;
12
+ export {};
@@ -0,0 +1,20 @@
1
+ import { loadMcpSettings, saveMcpSettings } from "../utils/config.js";
2
+ export async function handleMcpGetSettings(cwd) {
3
+ const settings = await loadMcpSettings(cwd);
4
+ return {
5
+ content: [{ type: "text", text: JSON.stringify(settings, null, 2) }],
6
+ };
7
+ }
8
+ export async function handleMcpSetSettings(cwd, settings) {
9
+ const value = settings.deep_default_mode;
10
+ if (value !== "off" && value !== "quick" && value !== "full") {
11
+ return {
12
+ content: [{ type: "text", text: "Invalid deep_default_mode. Use one of: off, quick, full." }],
13
+ isError: true,
14
+ };
15
+ }
16
+ await saveMcpSettings(cwd, { deep_default_mode: value });
17
+ return {
18
+ content: [{ type: "text", text: `Saved MCP settings: deep_default_mode=${value}` }],
19
+ };
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { handleMcpGetSettings, handleMcpSetSettings } from './mcp-settings-handler.js';
6
+ describe('mcp settings handlers', () => {
7
+ let testDir;
8
+ beforeEach(async () => {
9
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-mcp-settings-'));
10
+ await fs.ensureDir(path.join(testDir, '.rigour'));
11
+ });
12
+ afterEach(async () => {
13
+ await fs.remove(testDir);
14
+ });
15
+ it('returns default settings when file is missing', async () => {
16
+ const result = await handleMcpGetSettings(testDir);
17
+ const parsed = JSON.parse(result.content[0].text);
18
+ expect(parsed.deep_default_mode).toBe('off');
19
+ });
20
+ it('persists deep_default_mode through set/get', async () => {
21
+ const setResult = await handleMcpSetSettings(testDir, { deep_default_mode: 'quick' });
22
+ expect(setResult.isError).toBeUndefined();
23
+ const getResult = await handleMcpGetSettings(testDir);
24
+ const parsed = JSON.parse(getResult.content[0].text);
25
+ expect(parsed.deep_default_mode).toBe('quick');
26
+ });
27
+ it('rejects invalid deep_default_mode', async () => {
28
+ const result = await handleMcpSetSettings(testDir, { deep_default_mode: 'invalid' });
29
+ expect(result.isError).toBe(true);
30
+ });
31
+ });
@@ -64,7 +64,7 @@ export const PROMPT_DEFINITIONS = [
64
64
  },
65
65
  {
66
66
  name: "rigour-deep-analysis",
67
- description: "Run deep LLM-powered code quality analysis. AST extracts facts, LLM interprets patterns (SOLID violations, code smells, architecture issues), AST verifies findings. 100% local by default.",
67
+ description: "Run deep LLM-powered code quality analysis. AST extracts facts, LLM interprets patterns (SOLID violations, code smells, architecture issues), AST verifies findings. Local sidecar by default; cloud provider mode when configured.",
68
68
  arguments: [
69
69
  {
70
70
  name: "cwd",
@@ -16,7 +16,17 @@ type ToolResult = {
16
16
  isError?: boolean;
17
17
  _rigour_report?: Report;
18
18
  };
19
- export declare function handleCheck(runner: GateRunner, cwd: string): Promise<ToolResult>;
19
+ type DeepMode = 'off' | 'quick' | 'full';
20
+ export interface CheckArgs {
21
+ files?: string[];
22
+ deep?: DeepMode;
23
+ pro?: boolean;
24
+ apiKey?: string;
25
+ provider?: string;
26
+ apiBaseUrl?: string;
27
+ modelName?: string;
28
+ }
29
+ export declare function handleCheck(runner: GateRunner, cwd: string, args?: CheckArgs): Promise<ToolResult>;
20
30
  export declare function handleExplain(runner: GateRunner, cwd: string): Promise<ToolResult>;
21
31
  export declare function handleStatus(runner: GateRunner, cwd: string): Promise<ToolResult>;
22
32
  export declare function handleGetFixPacket(runner: GateRunner, cwd: string, config: Config): Promise<ToolResult>;
@@ -1,3 +1,12 @@
1
+ function resolveDeepExecution(args) {
2
+ const requestedProvider = (args.provider || '').toLowerCase();
3
+ const isForcedLocal = requestedProvider === 'local';
4
+ const isLocal = !args.apiKey || isForcedLocal;
5
+ return {
6
+ isLocal,
7
+ provider: isLocal ? 'local' : (args.provider || 'claude'),
8
+ };
9
+ }
1
10
  // ─── Score / Severity Formatters ──────────────────────────────────
2
11
  function formatScoreText(stats) {
3
12
  let text = '';
@@ -17,14 +26,35 @@ function formatSeverityText(stats) {
17
26
  return parts.length > 0 ? `\nSeverity: ${parts.join(', ')}` : '';
18
27
  }
19
28
  // ─── Handlers ─────────────────────────────────────────────────────
20
- export async function handleCheck(runner, cwd) {
21
- const report = await runner.run(cwd);
29
+ export async function handleCheck(runner, cwd, args = {}) {
30
+ const deepMode = args.deep || 'off';
31
+ const fileTargets = args.files && args.files.length > 0 ? args.files : undefined;
32
+ const execution = resolveDeepExecution(args);
33
+ let deepOpts;
34
+ if (deepMode !== 'off') {
35
+ deepOpts = {
36
+ enabled: true,
37
+ // full mode always means pro-depth analysis in MCP.
38
+ pro: deepMode === 'full' ? true : !!args.pro,
39
+ apiKey: args.apiKey,
40
+ provider: execution.provider,
41
+ apiBaseUrl: args.apiBaseUrl,
42
+ modelName: args.modelName,
43
+ };
44
+ }
45
+ const report = await runner.run(cwd, fileTargets, deepOpts);
22
46
  const scoreText = formatScoreText(report.stats);
23
47
  const sevText = formatSeverityText(report.stats);
48
+ const deepText = deepMode === 'off'
49
+ ? ''
50
+ : `\nDeep: ${deepMode} | Execution: ${execution.isLocal ? 'local' : 'cloud'}${report.stats.deep?.model ? ` | Model: ${report.stats.deep.model}` : ''}` +
51
+ `${execution.isLocal
52
+ ? '\nPrivacy: Local sidecar/model execution. Code remains on this machine.'
53
+ : `\nPrivacy: Cloud provider execution. Code context may be sent to ${execution.provider} API.`}`;
24
54
  const result = {
25
55
  content: [{
26
56
  type: "text",
27
- text: `RIGOUR AUDIT RESULT: ${report.status}${scoreText}${sevText}\n\nSummary:\n${Object.entries(report.summary).map(([k, v]) => `- ${k}: ${v}`).join("\n")}`,
57
+ text: `RIGOUR AUDIT RESULT: ${report.status}${scoreText}${sevText}${deepText}\n\nSummary:\n${Object.entries(report.summary).map(([k, v]) => `- ${k}: ${v}`).join("\n")}`,
28
58
  }],
29
59
  };
30
60
  result._rigour_report = report;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { handleCheck } from './quality-handlers.js';
3
+ describe('handleCheck deep routing', () => {
4
+ const baseReport = {
5
+ status: 'PASS',
6
+ summary: { 'ast-analysis': 'PASS' },
7
+ failures: [],
8
+ stats: {
9
+ duration_ms: 10,
10
+ score: 100,
11
+ ai_health_score: 100,
12
+ structural_score: 100,
13
+ },
14
+ };
15
+ it('runs standard check by default', async () => {
16
+ const run = vi.fn().mockResolvedValue(baseReport);
17
+ const runner = { run };
18
+ await handleCheck(runner, '/repo');
19
+ expect(run).toHaveBeenCalledWith('/repo', undefined, undefined);
20
+ });
21
+ it('maps quick deep mode and file scope', async () => {
22
+ const run = vi.fn().mockResolvedValue({
23
+ ...baseReport,
24
+ stats: {
25
+ ...baseReport.stats,
26
+ deep: { enabled: true, tier: 'deep', model: 'Qwen2.5-Coder-0.5B' },
27
+ },
28
+ });
29
+ const runner = { run };
30
+ const result = await handleCheck(runner, '/repo', {
31
+ deep: 'quick',
32
+ files: ['src/a.ts', 'src/b.ts'],
33
+ });
34
+ expect(run).toHaveBeenCalledWith('/repo', ['src/a.ts', 'src/b.ts'], {
35
+ enabled: true,
36
+ pro: false,
37
+ apiKey: undefined,
38
+ provider: 'local',
39
+ apiBaseUrl: undefined,
40
+ modelName: undefined,
41
+ });
42
+ expect(result.content[0].text).toContain('Deep: quick');
43
+ expect(result.content[0].text).toContain('Execution: local');
44
+ expect(result.content[0].text).toContain('Code remains on this machine');
45
+ });
46
+ it('maps full deep mode with cloud provider', async () => {
47
+ const run = vi.fn().mockResolvedValue({
48
+ ...baseReport,
49
+ stats: {
50
+ ...baseReport.stats,
51
+ deep: { enabled: true, tier: 'cloud', model: 'claude-sonnet' },
52
+ },
53
+ });
54
+ const runner = { run };
55
+ await handleCheck(runner, '/repo', {
56
+ deep: 'full',
57
+ pro: true,
58
+ apiKey: 'sk-test',
59
+ provider: 'openai',
60
+ modelName: 'gpt-4o-mini',
61
+ apiBaseUrl: 'https://example.com/v1',
62
+ });
63
+ expect(run).toHaveBeenCalledWith('/repo', undefined, {
64
+ enabled: true,
65
+ pro: true,
66
+ apiKey: 'sk-test',
67
+ provider: 'openai',
68
+ apiBaseUrl: 'https://example.com/v1',
69
+ modelName: 'gpt-4o-mini',
70
+ });
71
+ });
72
+ it('treats full deep mode as pro even when pro flag is omitted', async () => {
73
+ const run = vi.fn().mockResolvedValue(baseReport);
74
+ const runner = { run };
75
+ await handleCheck(runner, '/repo', {
76
+ deep: 'full',
77
+ apiKey: 'sk-test',
78
+ provider: 'openai',
79
+ });
80
+ expect(run).toHaveBeenCalledWith('/repo', undefined, {
81
+ enabled: true,
82
+ pro: true,
83
+ apiKey: 'sk-test',
84
+ provider: 'openai',
85
+ apiBaseUrl: undefined,
86
+ modelName: undefined,
87
+ });
88
+ });
89
+ it('forces local execution when provider=local even if apiKey is present', async () => {
90
+ const run = vi.fn().mockResolvedValue(baseReport);
91
+ const runner = { run };
92
+ const result = await handleCheck(runner, '/repo', {
93
+ deep: 'full',
94
+ apiKey: 'sk-test',
95
+ provider: 'local',
96
+ });
97
+ expect(run).toHaveBeenCalledWith('/repo', undefined, {
98
+ enabled: true,
99
+ pro: true,
100
+ apiKey: 'sk-test',
101
+ provider: 'local',
102
+ apiBaseUrl: undefined,
103
+ modelName: undefined,
104
+ });
105
+ expect(result.content[0].text).toContain('Execution: local');
106
+ expect(result.content[0].text).toContain('Code remains on this machine');
107
+ });
108
+ });
@@ -202,5 +202,11 @@ export interface MemoryStore {
202
202
  export declare function getMemoryPath(cwd: string): Promise<string>;
203
203
  export declare function loadMemory(cwd: string): Promise<MemoryStore>;
204
204
  export declare function saveMemory(cwd: string, store: MemoryStore): Promise<void>;
205
+ export interface McpSettings {
206
+ deep_default_mode: 'off' | 'quick' | 'full';
207
+ }
208
+ export declare function getMcpSettingsPath(cwd: string): Promise<string>;
209
+ export declare function loadMcpSettings(cwd: string): Promise<McpSettings>;
210
+ export declare function saveMcpSettings(cwd: string, settings: McpSettings): Promise<void>;
205
211
  export declare function logStudioEvent(cwd: string, event: any): Promise<void>;
206
212
  export declare function parseDiff(diff: string): Record<string, Set<number>>;
@@ -37,7 +37,15 @@ export async function loadMemory(cwd) {
37
37
  const memPath = await getMemoryPath(cwd);
38
38
  if (await fs.pathExists(memPath)) {
39
39
  const content = await fs.readFile(memPath, "utf-8");
40
- return JSON.parse(content);
40
+ try {
41
+ const parsed = JSON.parse(content);
42
+ if (parsed && typeof parsed === 'object' && parsed.memories && typeof parsed.memories === 'object') {
43
+ return parsed;
44
+ }
45
+ }
46
+ catch {
47
+ // fall through to default
48
+ }
41
49
  }
42
50
  return { memories: {} };
43
51
  }
@@ -45,6 +53,35 @@ export async function saveMemory(cwd, store) {
45
53
  const memPath = await getMemoryPath(cwd);
46
54
  await fs.writeFile(memPath, JSON.stringify(store, null, 2));
47
55
  }
56
+ const DEFAULT_MCP_SETTINGS = {
57
+ deep_default_mode: 'off',
58
+ };
59
+ export async function getMcpSettingsPath(cwd) {
60
+ const rigourDir = path.join(cwd, ".rigour");
61
+ await fs.ensureDir(rigourDir);
62
+ return path.join(rigourDir, "mcp-settings.json");
63
+ }
64
+ export async function loadMcpSettings(cwd) {
65
+ const settingsPath = await getMcpSettingsPath(cwd);
66
+ if (!(await fs.pathExists(settingsPath))) {
67
+ return DEFAULT_MCP_SETTINGS;
68
+ }
69
+ try {
70
+ const raw = await fs.readJson(settingsPath);
71
+ const deepMode = raw?.deep_default_mode;
72
+ if (deepMode === 'quick' || deepMode === 'full' || deepMode === 'off') {
73
+ return { deep_default_mode: deepMode };
74
+ }
75
+ }
76
+ catch {
77
+ // Fall through to defaults.
78
+ }
79
+ return DEFAULT_MCP_SETTINGS;
80
+ }
81
+ export async function saveMcpSettings(cwd, settings) {
82
+ const settingsPath = await getMcpSettingsPath(cwd);
83
+ await fs.writeJson(settingsPath, settings, { spaces: 2 });
84
+ }
48
85
  // ─── Studio Event Logging ─────────────────────────────────────────
49
86
  export async function logStudioEvent(cwd, event) {
50
87
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/mcp",
3
- "version": "4.0.4",
3
+ "version": "4.1.0",
4
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.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -48,7 +48,7 @@
48
48
  "execa": "^8.0.1",
49
49
  "fs-extra": "^11.2.0",
50
50
  "yaml": "^2.8.2",
51
- "@rigour-labs/core": "4.0.4"
51
+ "@rigour-labs/core": "4.1.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^25.0.3",