@toolbaux/guardian 0.1.8 → 0.1.9

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/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { runDocGenerate } from "./commands/doc-generate.js";
18
18
  import { runDiscrepancy } from "./commands/discrepancy.js";
19
19
  import { runDocHtml } from "./commands/doc-html.js";
20
20
  import { runInit } from "./commands/init.js";
21
+ import { runMcpServe } from "./commands/mcp-serve.js";
21
22
  import { DEFAULT_SPECS_DIR } from "./config.js";
22
23
  const program = new Command();
23
24
  program
@@ -338,6 +339,15 @@ program
338
339
  skipHook: options.skipHook ?? false,
339
340
  });
340
341
  });
342
+ program
343
+ .command("mcp-serve")
344
+ .description("Start Guardian MCP server for Claude Code / Cursor integration")
345
+ .option("--specs <dir>", "Specs directory", ".specs")
346
+ .action(async (options) => {
347
+ await runMcpServe({
348
+ specs: options.specs,
349
+ });
350
+ });
341
351
  program.parseAsync().catch((error) => {
342
352
  console.error(error);
343
353
  process.exitCode = 1;
@@ -0,0 +1,314 @@
1
+ /**
2
+ * `guardian mcp-serve` — Model Context Protocol server.
3
+ *
4
+ * Runs as a background process. Claude Code / Cursor connect via stdio.
5
+ * Exposes tools that query codebase-intelligence.json live.
6
+ *
7
+ * Tools:
8
+ * guardian_file_context — get upstream/downstream deps for a file
9
+ * guardian_search — search models, endpoints, components, modules
10
+ * guardian_endpoint_trace — trace an endpoint's full call chain
11
+ * guardian_impact_check — what files/endpoints are affected by a change
12
+ * guardian_overview — project summary and key metrics
13
+ *
14
+ * Protocol: JSON-RPC 2.0 over stdio (MCP standard)
15
+ * Spec: https://modelcontextprotocol.io
16
+ */
17
+ import fs from "node:fs/promises";
18
+ import path from "node:path";
19
+ import readline from "node:readline";
20
+ // ── Intelligence loader ──
21
+ let intel = null;
22
+ let intelPath = "";
23
+ let lastLoadTime = 0;
24
+ async function loadIntel() {
25
+ // Reload if file changed (check every 5s max)
26
+ const now = Date.now();
27
+ if (intel && now - lastLoadTime < 5000)
28
+ return intel;
29
+ try {
30
+ const raw = await fs.readFile(intelPath, "utf8");
31
+ intel = JSON.parse(raw);
32
+ lastLoadTime = now;
33
+ }
34
+ catch {
35
+ // Return cached or empty
36
+ if (!intel) {
37
+ intel = { api_registry: {}, model_registry: {}, service_map: [], frontend_pages: [], meta: { project: "unknown", counts: {} } };
38
+ }
39
+ }
40
+ return intel;
41
+ }
42
+ // ── Tool implementations ──
43
+ async function fileContext(args) {
44
+ const data = await loadIntel();
45
+ const file = args.file.replace(/^\.\//, "");
46
+ // Find which module this file belongs to
47
+ const module = data.service_map?.find((m) => m.path && file.startsWith(m.path.replace(/^\.\//, "")));
48
+ // Find endpoints in this file
49
+ const endpoints = Object.values(data.api_registry || {}).filter((ep) => ep.file && file.includes(ep.file.replace(/^\.\//, "")));
50
+ // Find models in this file
51
+ const models = Object.values(data.model_registry || {}).filter((m) => m.file && file.includes(m.file.replace(/^\.\//, "")));
52
+ // Find which endpoints call services defined in this file
53
+ const calledBy = [];
54
+ const fileName = path.basename(file, path.extname(file));
55
+ for (const [key, ep] of Object.entries(data.api_registry || {})) {
56
+ const e = ep;
57
+ if (e.service_calls?.some((s) => s.toLowerCase().includes(fileName.toLowerCase()))) {
58
+ calledBy.push(`${e.method} ${e.path} (${e.handler})`);
59
+ }
60
+ }
61
+ // Find what this file's endpoints call
62
+ const calls = endpoints.flatMap((ep) => (ep.service_calls || []).filter((s) => !["str", "dict", "int", "len", "float", "max", "join", "getattr"].includes(s)));
63
+ // Find frontend pages that use APIs from this module
64
+ const pages = (data.frontend_pages || []).filter((p) => p.api_calls?.some((call) => endpoints.some((ep) => call.includes(ep.path?.split("{")[0]))));
65
+ return JSON.stringify({
66
+ file,
67
+ module: module ? { id: module.id, layer: module.layer, file_count: module.file_count, imports: module.imports } : null,
68
+ endpoints_in_file: endpoints.map((ep) => `${ep.method} ${ep.path} → ${ep.handler}`),
69
+ models_in_file: models.map((m) => `${m.name} (${m.framework}, ${m.fields?.length || 0} fields)`),
70
+ calls_downstream: [...new Set(calls)],
71
+ called_by_upstream: calledBy.slice(0, 10),
72
+ frontend_pages_using: pages.map((p) => p.path),
73
+ coupling: module?.coupling_score ?? null,
74
+ }, null, 2);
75
+ }
76
+ async function search(args) {
77
+ const data = await loadIntel();
78
+ const q = args.query.toLowerCase();
79
+ const types = (args.types || "models,endpoints,modules").split(",").map((t) => t.trim());
80
+ const results = {};
81
+ if (types.includes("endpoints")) {
82
+ results.endpoints = Object.values(data.api_registry || {})
83
+ .filter((ep) => ep.path?.toLowerCase().includes(q) ||
84
+ ep.handler?.toLowerCase().includes(q) ||
85
+ ep.service_calls?.some((s) => s.toLowerCase().includes(q)))
86
+ .slice(0, 10)
87
+ .map((ep) => `${ep.method} ${ep.path} → ${ep.handler} [${ep.module}]`);
88
+ }
89
+ if (types.includes("models")) {
90
+ results.models = Object.values(data.model_registry || {})
91
+ .filter((m) => m.name?.toLowerCase().includes(q) ||
92
+ m.fields?.some((f) => f.toLowerCase().includes(q)))
93
+ .slice(0, 10)
94
+ .map((m) => `${m.name} (${m.framework}, ${m.fields?.length} fields, ${m.file})`);
95
+ }
96
+ if (types.includes("modules")) {
97
+ results.modules = (data.service_map || [])
98
+ .filter((m) => m.id?.toLowerCase().includes(q) ||
99
+ m.path?.toLowerCase().includes(q))
100
+ .slice(0, 10)
101
+ .map((m) => `${m.id} (${m.type}, ${m.endpoint_count} eps, ${m.file_count} files, imports: ${m.imports?.join(",") || "none"})`);
102
+ }
103
+ return JSON.stringify(results, null, 2);
104
+ }
105
+ async function endpointTrace(args) {
106
+ const data = await loadIntel();
107
+ const key = `${args.method.toUpperCase()} ${args.path}`;
108
+ const ep = data.api_registry?.[key] || Object.values(data.api_registry || {}).find((e) => e.method === args.method.toUpperCase() && e.path === args.path);
109
+ if (!ep)
110
+ return JSON.stringify({ error: `Endpoint ${key} not found` });
111
+ // Find which frontend pages call this endpoint
112
+ const frontendCallers = (data.frontend_pages || []).filter((p) => p.api_calls?.some((call) => call.includes(args.path.split("{")[0])));
113
+ // Find what models this endpoint uses
114
+ const models = Object.values(data.model_registry || {}).filter((m) => ep.request_schema === m.name || ep.response_schema === m.name);
115
+ return JSON.stringify({
116
+ endpoint: `${ep.method} ${ep.path}`,
117
+ handler: ep.handler,
118
+ file: ep.file,
119
+ module: ep.module,
120
+ request_schema: ep.request_schema,
121
+ response_schema: ep.response_schema,
122
+ service_calls: ep.service_calls,
123
+ ai_operations: ep.ai_operations,
124
+ patterns: ep.patterns,
125
+ models_used: models.map((m) => ({ name: m.name, fields: m.fields })),
126
+ frontend_callers: frontendCallers.map((p) => p.path),
127
+ }, null, 2);
128
+ }
129
+ async function impactCheck(args) {
130
+ const data = await loadIntel();
131
+ const file = args.file.replace(/^\.\//, "");
132
+ // Find all endpoints in this file
133
+ const endpoints = Object.values(data.api_registry || {}).filter((ep) => ep.file && file.includes(ep.file.replace(/^\.\//, "")));
134
+ // Find all models in this file
135
+ const models = Object.values(data.model_registry || {}).filter((m) => m.file && file.includes(m.file.replace(/^\.\//, "")));
136
+ // Find endpoints that USE these models
137
+ const modelNames = new Set(models.map((m) => m.name));
138
+ const affectedEndpoints = Object.values(data.api_registry || {}).filter((ep) => ep.request_schema && modelNames.has(ep.request_schema) ||
139
+ ep.response_schema && modelNames.has(ep.response_schema));
140
+ // Find modules that import from this file's module
141
+ const fileModule = data.service_map?.find((m) => m.path && file.startsWith(m.path.replace(/^\.\//, "")));
142
+ const dependentModules = fileModule
143
+ ? (data.service_map || []).filter((m) => m.imports?.includes(fileModule.id))
144
+ : [];
145
+ // Find frontend pages affected
146
+ const affectedPages = (data.frontend_pages || []).filter((p) => p.api_calls?.some((call) => endpoints.some((ep) => call.includes(ep.path?.split("{")[0]))));
147
+ return JSON.stringify({
148
+ file,
149
+ direct_endpoints: endpoints.map((ep) => `${ep.method} ${ep.path}`),
150
+ models_defined: models.map((m) => m.name),
151
+ endpoints_using_these_models: affectedEndpoints.map((ep) => `${ep.method} ${ep.path}`),
152
+ dependent_modules: dependentModules.map((m) => m.id),
153
+ affected_frontend_pages: affectedPages.map((p) => p.path),
154
+ risk: endpoints.length + affectedEndpoints.length + dependentModules.length > 5 ? "HIGH" : "LOW",
155
+ }, null, 2);
156
+ }
157
+ async function overview() {
158
+ const data = await loadIntel();
159
+ return JSON.stringify({
160
+ project: data.meta?.project,
161
+ counts: data.meta?.counts,
162
+ modules: (data.service_map || [])
163
+ .filter((m) => m.file_count > 0)
164
+ .map((m) => ({ id: m.id, type: m.type, layer: m.layer, endpoints: m.endpoint_count, files: m.file_count, imports: m.imports })),
165
+ pages: (data.frontend_pages || []).map((p) => ({ route: p.path, component: p.component })),
166
+ top_endpoints: Object.values(data.api_registry || {})
167
+ .sort((a, b) => (b.service_calls?.length || 0) - (a.service_calls?.length || 0))
168
+ .slice(0, 5)
169
+ .map((ep) => `${ep.method} ${ep.path} (${ep.service_calls?.length || 0} service calls)`),
170
+ }, null, 2);
171
+ }
172
+ // ── MCP protocol ──
173
+ const TOOLS = [
174
+ {
175
+ name: "guardian_file_context",
176
+ description: "Get upstream/downstream dependencies, endpoints, models, and coupling for a file. Call this BEFORE editing any file.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ file: { type: "string", description: "File path relative to project root (e.g. 'backend/service-conversation/engine.py')" },
181
+ },
182
+ required: ["file"],
183
+ },
184
+ },
185
+ {
186
+ name: "guardian_search",
187
+ description: "Search the codebase for endpoints, models, or modules matching a keyword.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ query: { type: "string", description: "Search keyword (e.g. 'session', 'auth', 'TTS')" },
192
+ types: { type: "string", description: "Comma-separated: models,endpoints,modules (default: all)" },
193
+ },
194
+ required: ["query"],
195
+ },
196
+ },
197
+ {
198
+ name: "guardian_endpoint_trace",
199
+ description: "Trace an API endpoint's full chain: frontend callers, handler, service calls, models, AI operations.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ method: { type: "string", description: "HTTP method (GET, POST, PUT, DELETE)" },
204
+ path: { type: "string", description: "Endpoint path (e.g. '/sessions/start')" },
205
+ },
206
+ required: ["method", "path"],
207
+ },
208
+ },
209
+ {
210
+ name: "guardian_impact_check",
211
+ description: "Check what endpoints, models, modules, and pages are affected if you change a file. Call this BEFORE making changes to high-coupling files.",
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ file: { type: "string", description: "File path to check impact for" },
216
+ },
217
+ required: ["file"],
218
+ },
219
+ },
220
+ {
221
+ name: "guardian_overview",
222
+ description: "Get project summary: modules, pages, top endpoints, counts. Call this at session start for orientation.",
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {},
226
+ },
227
+ },
228
+ ];
229
+ const TOOL_HANDLERS = {
230
+ guardian_file_context: fileContext,
231
+ guardian_search: search,
232
+ guardian_endpoint_trace: endpointTrace,
233
+ guardian_impact_check: impactCheck,
234
+ guardian_overview: overview,
235
+ };
236
+ function respond(id, result) {
237
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, result });
238
+ process.stdout.write(msg + "\n");
239
+ }
240
+ function respondError(id, code, message) {
241
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
242
+ process.stdout.write(msg + "\n");
243
+ }
244
+ async function handleRequest(req) {
245
+ switch (req.method) {
246
+ case "initialize":
247
+ respond(req.id, {
248
+ protocolVersion: "2024-11-05",
249
+ capabilities: { tools: {} },
250
+ serverInfo: { name: "guardian", version: "0.1.0" },
251
+ });
252
+ break;
253
+ case "initialized":
254
+ // Client acknowledgment — no response needed
255
+ break;
256
+ case "tools/list":
257
+ respond(req.id, { tools: TOOLS });
258
+ break;
259
+ case "tools/call": {
260
+ const toolName = req.params?.name;
261
+ const toolArgs = req.params?.arguments || {};
262
+ const handler = TOOL_HANDLERS[toolName];
263
+ if (!handler) {
264
+ respond(req.id, {
265
+ content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
266
+ isError: true,
267
+ });
268
+ break;
269
+ }
270
+ try {
271
+ const result = await handler(toolArgs);
272
+ respond(req.id, {
273
+ content: [{ type: "text", text: result }],
274
+ });
275
+ }
276
+ catch (err) {
277
+ respond(req.id, {
278
+ content: [{ type: "text", text: `Error: ${err.message}` }],
279
+ isError: true,
280
+ });
281
+ }
282
+ break;
283
+ }
284
+ default:
285
+ respondError(req.id, -32601, `Method not found: ${req.method}`);
286
+ }
287
+ }
288
+ // ── Entry point ──
289
+ export async function runMcpServe(options) {
290
+ const specsDir = path.resolve(options.specs);
291
+ intelPath = path.join(specsDir, "machine", "codebase-intelligence.json");
292
+ // Pre-load intelligence
293
+ await loadIntel();
294
+ // Log to stderr (stdout is for MCP protocol)
295
+ process.stderr.write(`Guardian MCP server started. Intelligence: ${intelPath}\n`);
296
+ process.stderr.write(`Tools: ${TOOLS.map((t) => t.name).join(", ")}\n`);
297
+ // Read JSON-RPC messages from stdin, line by line
298
+ const rl = readline.createInterface({ input: process.stdin });
299
+ rl.on("line", async (line) => {
300
+ if (!line.trim())
301
+ return;
302
+ try {
303
+ const req = JSON.parse(line);
304
+ await handleRequest(req);
305
+ }
306
+ catch (err) {
307
+ respondError(null, -32700, `Parse error: ${err.message}`);
308
+ }
309
+ });
310
+ rl.on("close", () => {
311
+ process.stderr.write("Guardian MCP server stopped.\n");
312
+ process.exit(0);
313
+ });
314
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolbaux/guardian",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Architectural intelligence for codebases. Verify that AI-generated code matches your architectural intent.",
6
6
  "keywords": [