@toolbaux/guardian 0.1.7 → 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 +10 -0
- package/dist/commands/mcp-serve.js +314 -0
- package/dist/extract/context-block.js +13 -13
- package/package.json +1 -1
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
|
+
}
|
|
@@ -131,26 +131,26 @@ export function renderContextBlock(architecture, ux, options) {
|
|
|
131
131
|
lines.push("");
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
// Deep intelligence
|
|
135
|
-
lines.push("###
|
|
134
|
+
// Deep intelligence — directive instructions for AI agents
|
|
135
|
+
lines.push("### How to Use This Context");
|
|
136
136
|
lines.push("");
|
|
137
|
-
lines.push("
|
|
137
|
+
lines.push("> **Before reading source files**, run `guardian search --query \"<keyword>\"` to find relevant endpoints, models, components, and modules. This is faster than file exploration.");
|
|
138
138
|
lines.push("");
|
|
139
|
-
lines.push("**
|
|
140
|
-
lines.push("- `.specs/machine/architecture.snapshot.yaml` —
|
|
141
|
-
lines.push("- `.specs/machine/codebase-intelligence.json` — API registry
|
|
142
|
-
lines.push("- `.specs/machine/structural-intelligence.json` — depth
|
|
139
|
+
lines.push("**Deeper analysis files** (read when you need specifics):");
|
|
140
|
+
lines.push("- `.specs/machine/architecture.snapshot.yaml` — every file, export symbol, and import edge per module");
|
|
141
|
+
lines.push("- `.specs/machine/codebase-intelligence.json` — API registry: handlers, service calls, request/response schemas");
|
|
142
|
+
lines.push("- `.specs/machine/structural-intelligence.json` — depth and complexity classification per module");
|
|
143
143
|
if (architecture.dependencies.file_graph.length > 0) {
|
|
144
|
-
lines.push("-
|
|
144
|
+
lines.push("- File-level dependency graph available in `architecture.snapshot.yaml → dependencies.file_graph`");
|
|
145
145
|
}
|
|
146
146
|
if (architecture.analysis?.circular_dependencies?.length > 0) {
|
|
147
|
-
lines.push(`-
|
|
147
|
+
lines.push(`- ⚠ ${architecture.analysis.circular_dependencies.length} circular dependency cycle(s) detected — check snapshot before refactoring`);
|
|
148
148
|
}
|
|
149
149
|
lines.push("");
|
|
150
|
-
lines.push("**Commands
|
|
151
|
-
lines.push("- `guardian search --query \"
|
|
152
|
-
lines.push("- `guardian context --focus \"
|
|
153
|
-
lines.push("- `guardian drift` — check
|
|
150
|
+
lines.push("**Commands:**");
|
|
151
|
+
lines.push("- `guardian search --query \"auth\"` — find everything related to a feature");
|
|
152
|
+
lines.push("- `guardian context --focus \"auth\"` — generate AI context focused on one area");
|
|
153
|
+
lines.push("- `guardian drift` — check if architecture has shifted since last baseline");
|
|
154
154
|
lines.push("");
|
|
155
155
|
lines.push("<!-- /guardian:context -->");
|
|
156
156
|
return lines.join("\n");
|
package/package.json
CHANGED