@systemdox/mcp-server 0.1.11
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 +138 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1891 -0
- package/dist/http.d.ts +30 -0
- package/dist/http.js +1623 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1561 -0
- package/dist/lib/api-client.d.ts +14 -0
- package/dist/lib/api-client.js +118 -0
- package/dist/lib/cache.d.ts +9 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/context-builder.d.ts +78 -0
- package/dist/lib/context-builder.js +575 -0
- package/dist/lib/deprecation.d.ts +11 -0
- package/dist/lib/language.d.ts +7 -0
- package/dist/lib/scoring.d.ts +35 -0
- package/dist/server.d.ts +8 -0
- package/dist/tools/create-guardrail.d.ts +3 -0
- package/dist/tools/create-project-rule.d.ts +3 -0
- package/dist/tools/create-prompt-template.d.ts +3 -0
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-docs.d.ts +3 -0
- package/dist/tools/get-guardrails.d.ts +15 -0
- package/dist/tools/get-planning-context.d.ts +3 -0
- package/dist/tools/get-project-rules.d.ts +9 -0
- package/dist/tools/get-prompt-templates.d.ts +10 -0
- package/dist/tools/get-specs.d.ts +3 -0
- package/dist/tools/get-tech-stack.d.ts +3 -0
- package/dist/tools/report-decision.d.ts +3 -0
- package/dist/tools/update-spec.d.ts +3 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1561 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/config.ts
|
|
7
|
+
function loadConfig() {
|
|
8
|
+
const apiKey = process.env.SYSTEMDOX_API_KEY;
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"SYSTEMDOX_API_KEY is required. Generate one at your SystemDox settings page."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
apiKey,
|
|
16
|
+
apiUrl: process.env.SYSTEMDOX_API_URL ?? "https://api.puglieseweb.com/systemdox",
|
|
17
|
+
cacheTtlMs: parseInt(process.env.SYSTEMDOX_CACHE_TTL ?? "300", 10) * 1e3,
|
|
18
|
+
defaultOrg: process.env.SYSTEMDOX_DEFAULT_ORG
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/lib/cache.ts
|
|
23
|
+
var ResponseCache = class {
|
|
24
|
+
store = /* @__PURE__ */ new Map();
|
|
25
|
+
ttlMs;
|
|
26
|
+
constructor(ttlMs) {
|
|
27
|
+
this.ttlMs = ttlMs;
|
|
28
|
+
}
|
|
29
|
+
get(key) {
|
|
30
|
+
const entry = this.store.get(key);
|
|
31
|
+
if (!entry) return void 0;
|
|
32
|
+
if (Date.now() > entry.expiresAt) {
|
|
33
|
+
this.store.delete(key);
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
return entry.data;
|
|
37
|
+
}
|
|
38
|
+
set(key, data) {
|
|
39
|
+
this.store.set(key, {
|
|
40
|
+
data,
|
|
41
|
+
expiresAt: Date.now() + this.ttlMs
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
invalidate(key) {
|
|
45
|
+
this.store.delete(key);
|
|
46
|
+
}
|
|
47
|
+
clear() {
|
|
48
|
+
this.store.clear();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/lib/api-client.ts
|
|
53
|
+
var SystemDoxApiClient = class {
|
|
54
|
+
config;
|
|
55
|
+
cache;
|
|
56
|
+
constructor(config2) {
|
|
57
|
+
this.config = config2;
|
|
58
|
+
this.cache = new ResponseCache(config2.cacheTtlMs);
|
|
59
|
+
}
|
|
60
|
+
async validate() {
|
|
61
|
+
const res = await this.fetchRaw("/v1/health");
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async get(path, params) {
|
|
69
|
+
const url = this.buildUrl(path, params);
|
|
70
|
+
const cacheKey = url.toString();
|
|
71
|
+
const cached = this.cache.get(cacheKey);
|
|
72
|
+
if (cached !== void 0) return cached;
|
|
73
|
+
const res = await this.fetchRaw(path, params);
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const body = await res.text();
|
|
76
|
+
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
77
|
+
}
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
this.cache.set(cacheKey, data);
|
|
80
|
+
return data;
|
|
81
|
+
}
|
|
82
|
+
async post(path, body) {
|
|
83
|
+
return this.mutate("POST", path, body);
|
|
84
|
+
}
|
|
85
|
+
async put(path, body) {
|
|
86
|
+
return this.mutate("PUT", path, body);
|
|
87
|
+
}
|
|
88
|
+
async mutate(method, path, body) {
|
|
89
|
+
const url = this.buildUrl(path);
|
|
90
|
+
const res = await fetch(url, {
|
|
91
|
+
method,
|
|
92
|
+
headers: {
|
|
93
|
+
// The API Gateway authorizer only reads the Authorization header;
|
|
94
|
+
// X-API-Key is kept for backwards compatibility.
|
|
95
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
96
|
+
"X-API-Key": this.config.apiKey,
|
|
97
|
+
"Content-Type": "application/json"
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify(body)
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
throw new Error(`SystemDox API error ${res.status}: ${text}`);
|
|
104
|
+
}
|
|
105
|
+
return await res.json();
|
|
106
|
+
}
|
|
107
|
+
invalidateCache(path) {
|
|
108
|
+
if (path) {
|
|
109
|
+
this.cache.invalidate(this.buildUrl(path).toString());
|
|
110
|
+
} else {
|
|
111
|
+
this.cache.clear();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
buildUrl(path, params) {
|
|
115
|
+
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
116
|
+
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
117
|
+
if (params) {
|
|
118
|
+
for (const [key, value] of Object.entries(params)) {
|
|
119
|
+
url.searchParams.set(key, value);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return url;
|
|
123
|
+
}
|
|
124
|
+
async fetchRaw(path, params) {
|
|
125
|
+
const url = this.buildUrl(path, params);
|
|
126
|
+
return fetch(url, {
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
129
|
+
"X-API-Key": this.config.apiKey,
|
|
130
|
+
Accept: "application/json"
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/server.ts
|
|
137
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
138
|
+
|
|
139
|
+
// src/tools/get-guardrails.ts
|
|
140
|
+
import { z } from "zod";
|
|
141
|
+
|
|
142
|
+
// src/lib/language.ts
|
|
143
|
+
var EXTENSION_MAP = {
|
|
144
|
+
".py": { language: "python", categories: ["api", "backend", "data-access"] },
|
|
145
|
+
".ts": { language: "typescript", categories: ["frontend", "api"] },
|
|
146
|
+
".tsx": { language: "typescript", categories: ["frontend"] },
|
|
147
|
+
".js": { language: "javascript", categories: ["frontend"] },
|
|
148
|
+
".jsx": { language: "javascript", categories: ["frontend"] },
|
|
149
|
+
".yaml": {
|
|
150
|
+
language: "cloudformation",
|
|
151
|
+
categories: ["infrastructure", "architecture"]
|
|
152
|
+
},
|
|
153
|
+
".yml": {
|
|
154
|
+
language: "cloudformation",
|
|
155
|
+
categories: ["infrastructure", "architecture"]
|
|
156
|
+
},
|
|
157
|
+
".json": { language: "json", categories: ["configuration"] },
|
|
158
|
+
".css": { language: "css", categories: ["frontend"] },
|
|
159
|
+
".html": { language: "html", categories: ["frontend"] },
|
|
160
|
+
".sql": { language: "sql", categories: ["data-access"] },
|
|
161
|
+
".sh": { language: "shell", categories: ["infrastructure"] },
|
|
162
|
+
".bash": { language: "shell", categories: ["infrastructure"] },
|
|
163
|
+
".tf": { language: "terraform", categories: ["infrastructure"] },
|
|
164
|
+
".md": { language: "markdown", categories: ["documentation"] }
|
|
165
|
+
};
|
|
166
|
+
var PATH_CATEGORY_RULES = [
|
|
167
|
+
{ pattern: /backend\/functions\//, categories: ["api", "backend"] },
|
|
168
|
+
{ pattern: /backend\/layers\//, categories: ["backend"] },
|
|
169
|
+
{ pattern: /backend\//, categories: ["backend"] },
|
|
170
|
+
{ pattern: /apps\/app\//, categories: ["frontend"] },
|
|
171
|
+
{ pattern: /apps\/website\//, categories: ["frontend"] },
|
|
172
|
+
{ pattern: /apps\/portal\//, categories: ["frontend"] },
|
|
173
|
+
{ pattern: /packages\/ui\//, categories: ["frontend"] },
|
|
174
|
+
{ pattern: /infra\//, categories: ["infrastructure"] },
|
|
175
|
+
{ pattern: /\.github\//, categories: ["infrastructure"] },
|
|
176
|
+
{ pattern: /tests?\//, categories: ["testing"] },
|
|
177
|
+
{ pattern: /spec/, categories: ["testing"] },
|
|
178
|
+
{ pattern: /docs\/adr/, categories: ["architecture"] },
|
|
179
|
+
{ pattern: /template\.yaml$/, categories: ["infrastructure", "architecture"] }
|
|
180
|
+
];
|
|
181
|
+
function detectLanguage(filePath) {
|
|
182
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
183
|
+
return EXTENSION_MAP[ext];
|
|
184
|
+
}
|
|
185
|
+
function detectCategories(filePaths) {
|
|
186
|
+
const categories = /* @__PURE__ */ new Set();
|
|
187
|
+
for (const filePath of filePaths) {
|
|
188
|
+
const langInfo = detectLanguage(filePath);
|
|
189
|
+
if (langInfo) {
|
|
190
|
+
for (const cat of langInfo.categories) {
|
|
191
|
+
categories.add(cat);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const rule of PATH_CATEGORY_RULES) {
|
|
195
|
+
if (rule.pattern.test(filePath)) {
|
|
196
|
+
for (const cat of rule.categories) {
|
|
197
|
+
categories.add(cat);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return Array.from(categories);
|
|
203
|
+
}
|
|
204
|
+
function detectLanguages(filePaths) {
|
|
205
|
+
const languages = /* @__PURE__ */ new Set();
|
|
206
|
+
for (const filePath of filePaths) {
|
|
207
|
+
const info = detectLanguage(filePath);
|
|
208
|
+
if (info) languages.add(info.language);
|
|
209
|
+
}
|
|
210
|
+
return Array.from(languages);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/lib/deprecation.ts
|
|
214
|
+
var LEGACY_TOOL_REMOVAL_EARLIEST = "2027-01-31";
|
|
215
|
+
function legacyToolDescription(canonical, description) {
|
|
216
|
+
return `DEPRECATED \u2014 use \`${canonical}\` instead. This name still works and will not be removed before ${LEGACY_TOOL_REMOVAL_EARLIEST}. ${description}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/tools/get-guardrails.ts
|
|
220
|
+
var DESCRIPTION = "Retrieve SystemDox checks (banned patterns, required patterns, coding rules). Filter by language, category, or file path to get only relevant checks.";
|
|
221
|
+
var inputSchema = () => ({
|
|
222
|
+
language: z.string().optional().describe(
|
|
223
|
+
"Filter by programming language (python, typescript, javascript, cloudformation)"
|
|
224
|
+
),
|
|
225
|
+
category: z.string().optional().describe(
|
|
226
|
+
"Filter by category (imports, auth, error-handling, architecture, testing, security, conventions, data-access)"
|
|
227
|
+
),
|
|
228
|
+
file_path: z.string().optional().describe(
|
|
229
|
+
"File path being edited \u2014 language is auto-detected from the extension"
|
|
230
|
+
)
|
|
231
|
+
});
|
|
232
|
+
function registerGetGuardrails(server2, apiClient2) {
|
|
233
|
+
const handler = async ({
|
|
234
|
+
language,
|
|
235
|
+
category,
|
|
236
|
+
file_path
|
|
237
|
+
}) => {
|
|
238
|
+
const guardrails = await apiClient2.get("/v1/guardrails");
|
|
239
|
+
let filtered = guardrails;
|
|
240
|
+
const lang = language ?? (file_path ? detectLanguage(file_path)?.language : void 0);
|
|
241
|
+
if (lang) {
|
|
242
|
+
filtered = filtered.filter(
|
|
243
|
+
(g) => g.description.toLowerCase().includes(lang.toLowerCase()) || g.category.toLowerCase().includes(lang.toLowerCase()) || g.title.toLowerCase().includes(lang.toLowerCase())
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (category) {
|
|
247
|
+
filtered = filtered.filter(
|
|
248
|
+
(g) => g.category.toLowerCase() === category.toLowerCase()
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (filtered.length === 0) {
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const text = filtered.map(
|
|
262
|
+
(g) => `## ${g.title} [${g.severity}]
|
|
263
|
+
|
|
264
|
+
${g.description}
|
|
265
|
+
|
|
266
|
+
` + (g.pattern ? `**Bad pattern:**
|
|
267
|
+
\`\`\`
|
|
268
|
+
${g.pattern}
|
|
269
|
+
\`\`\`
|
|
270
|
+
|
|
271
|
+
` : "") + (g.fix ? `**Correct pattern:**
|
|
272
|
+
\`\`\`
|
|
273
|
+
${g.fix}
|
|
274
|
+
\`\`\`
|
|
275
|
+
|
|
276
|
+
` : "") + (g.fitnessTest ? `**Detection:** \`${g.fitnessTest}\`
|
|
277
|
+
` : "")
|
|
278
|
+
).join("\n---\n\n");
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: `Found ${filtered.length} check(s):
|
|
284
|
+
|
|
285
|
+
${text}`
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
server2.registerTool(
|
|
291
|
+
"get_checks",
|
|
292
|
+
{
|
|
293
|
+
title: "Get Checks",
|
|
294
|
+
description: DESCRIPTION,
|
|
295
|
+
inputSchema: inputSchema()
|
|
296
|
+
},
|
|
297
|
+
handler
|
|
298
|
+
);
|
|
299
|
+
server2.registerTool(
|
|
300
|
+
"get_guardrails",
|
|
301
|
+
{
|
|
302
|
+
title: "Get Checks (legacy name)",
|
|
303
|
+
description: legacyToolDescription("get_checks", DESCRIPTION),
|
|
304
|
+
inputSchema: inputSchema()
|
|
305
|
+
},
|
|
306
|
+
handler
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/tools/get-specs.ts
|
|
311
|
+
import { z as z2 } from "zod";
|
|
312
|
+
function registerGetSpecs(server2, apiClient2) {
|
|
313
|
+
server2.registerTool(
|
|
314
|
+
"get_specs",
|
|
315
|
+
{
|
|
316
|
+
title: "Get Specs",
|
|
317
|
+
description: "Retrieve SystemDox specifications (proposal, requirements with RFC 2119 keywords, Given/When/Then scenarios). Get a specific spec by ID, filter by status, or search by keyword.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
spec_id: z2.string().optional().describe("Specific spec ID to retrieve with full content"),
|
|
320
|
+
status: z2.enum(["draft", "ready", "archived"]).optional().describe("Filter by status"),
|
|
321
|
+
search: z2.string().optional().describe("Keyword search across spec names")
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
async ({ spec_id, status, search }) => {
|
|
325
|
+
if (spec_id) {
|
|
326
|
+
const spec = await apiClient2.get(
|
|
327
|
+
`/v1/specs/${spec_id}`
|
|
328
|
+
);
|
|
329
|
+
const sections = [`# ${spec.name}
|
|
330
|
+
|
|
331
|
+
**Status:** ${spec.status}`];
|
|
332
|
+
if (spec.proposalContent) {
|
|
333
|
+
sections.push(`## Proposal
|
|
334
|
+
|
|
335
|
+
${spec.proposalContent}`);
|
|
336
|
+
}
|
|
337
|
+
if (spec.requirements.length > 0) {
|
|
338
|
+
const reqs = spec.requirements.map((r) => `### ${r.name}
|
|
339
|
+
|
|
340
|
+
${r.content}`).join("\n\n");
|
|
341
|
+
sections.push(`## Requirements
|
|
342
|
+
|
|
343
|
+
${reqs}`);
|
|
344
|
+
}
|
|
345
|
+
if (spec.taskContent) {
|
|
346
|
+
sections.push(`## Tasks
|
|
347
|
+
|
|
348
|
+
${spec.taskContent}`);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: sections.join("\n\n") }]
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const specs = await apiClient2.get("/v1/specs");
|
|
355
|
+
let filtered = specs;
|
|
356
|
+
if (status) {
|
|
357
|
+
filtered = filtered.filter((s) => s.status === status);
|
|
358
|
+
}
|
|
359
|
+
if (search) {
|
|
360
|
+
const term = search.toLowerCase();
|
|
361
|
+
filtered = filtered.filter(
|
|
362
|
+
(s) => s.name.toLowerCase().includes(term)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (filtered.length === 0) {
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: `No specs found${status ? ` with status "${status}"` : ""}${search ? ` matching "${search}"` : ""}.`
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const text = filtered.map(
|
|
376
|
+
(s) => `- **${s.name}** (${s.specId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
377
|
+
).join("\n");
|
|
378
|
+
return {
|
|
379
|
+
content: [
|
|
380
|
+
{
|
|
381
|
+
type: "text",
|
|
382
|
+
text: `Found ${filtered.length} spec(s):
|
|
383
|
+
|
|
384
|
+
${text}`
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/tools/get-docs.ts
|
|
393
|
+
import { z as z3 } from "zod";
|
|
394
|
+
function matchGlob(path, pattern) {
|
|
395
|
+
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
396
|
+
return new RegExp(`^${regex}$`).test(path);
|
|
397
|
+
}
|
|
398
|
+
function registerGetDocs(server2, apiClient2) {
|
|
399
|
+
server2.registerTool(
|
|
400
|
+
"get_docs",
|
|
401
|
+
{
|
|
402
|
+
title: "Get Docs",
|
|
403
|
+
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search.",
|
|
404
|
+
inputSchema: {
|
|
405
|
+
repo: z3.string().optional().describe("Filter by repo (owner/repo format)"),
|
|
406
|
+
path: z3.string().optional().describe("File path glob pattern (e.g., docs/adr/*.md)"),
|
|
407
|
+
search: z3.string().optional().describe("Keyword search across document content"),
|
|
408
|
+
doc_id: z3.string().optional().describe(
|
|
409
|
+
"Specific document ID to retrieve full content"
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
async ({ repo, path, search, doc_id }) => {
|
|
414
|
+
if (doc_id) {
|
|
415
|
+
const doc = await apiClient2.get(
|
|
416
|
+
`/v1/docs/${encodeURIComponent(doc_id)}/content`
|
|
417
|
+
);
|
|
418
|
+
if (doc.error) {
|
|
419
|
+
return {
|
|
420
|
+
content: [
|
|
421
|
+
{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: `Error retrieving document: ${doc.error}`
|
|
424
|
+
}
|
|
425
|
+
],
|
|
426
|
+
isError: true
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
content: [{ type: "text", text: doc.content }]
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const params = {};
|
|
434
|
+
if (repo) params.repo = repo;
|
|
435
|
+
if (search) params.q = search;
|
|
436
|
+
const response = await apiClient2.get(
|
|
437
|
+
"/v1/docs",
|
|
438
|
+
params
|
|
439
|
+
);
|
|
440
|
+
let items = response.items;
|
|
441
|
+
if (path) {
|
|
442
|
+
items = items.filter((item) => matchGlob(item.path, path));
|
|
443
|
+
}
|
|
444
|
+
if (items.length === 0) {
|
|
445
|
+
return {
|
|
446
|
+
content: [
|
|
447
|
+
{
|
|
448
|
+
type: "text",
|
|
449
|
+
text: `No documents found${repo ? ` in ${repo}` : ""}${path ? ` matching "${path}"` : ""}${search ? ` for "${search}"` : ""}.`
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const text = items.map(
|
|
455
|
+
(d) => `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})` + (d.snippet ? `
|
|
456
|
+
> ${d.snippet}` : "")
|
|
457
|
+
).join("\n");
|
|
458
|
+
return {
|
|
459
|
+
content: [
|
|
460
|
+
{
|
|
461
|
+
type: "text",
|
|
462
|
+
text: `Found ${items.length} document(s) (${response.total} total):
|
|
463
|
+
|
|
464
|
+
${text}
|
|
465
|
+
|
|
466
|
+
Use doc_id to retrieve full content.`
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/tools/get-tech-stack.ts
|
|
475
|
+
function registerGetTechStack(server2, apiClient2) {
|
|
476
|
+
server2.registerTool(
|
|
477
|
+
"get_tech_stack",
|
|
478
|
+
{
|
|
479
|
+
title: "Get Tech Stack",
|
|
480
|
+
description: "Retrieve the configured technology stack (languages, frameworks, databases, infrastructure) with versions and notes."
|
|
481
|
+
},
|
|
482
|
+
async () => {
|
|
483
|
+
const stack = await apiClient2.get(
|
|
484
|
+
"/v1/ai/settings/stack"
|
|
485
|
+
);
|
|
486
|
+
if (stack.length === 0) {
|
|
487
|
+
return {
|
|
488
|
+
content: [
|
|
489
|
+
{
|
|
490
|
+
type: "text",
|
|
491
|
+
text: "No tech stack entries configured."
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const entry of stack) {
|
|
498
|
+
const list = grouped.get(entry.category) ?? [];
|
|
499
|
+
list.push(entry);
|
|
500
|
+
grouped.set(entry.category, list);
|
|
501
|
+
}
|
|
502
|
+
const sections = Array.from(grouped.entries()).map(([category, entries]) => {
|
|
503
|
+
const items = entries.map(
|
|
504
|
+
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
505
|
+
).join("\n");
|
|
506
|
+
return `### ${category}
|
|
507
|
+
${items}`;
|
|
508
|
+
}).join("\n\n");
|
|
509
|
+
return {
|
|
510
|
+
content: [
|
|
511
|
+
{
|
|
512
|
+
type: "text",
|
|
513
|
+
text: `Tech stack (${stack.length} entries):
|
|
514
|
+
|
|
515
|
+
${sections}`
|
|
516
|
+
}
|
|
517
|
+
]
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/tools/get-planning-context.ts
|
|
524
|
+
import { z as z4 } from "zod";
|
|
525
|
+
function registerGetPlanningContext(server2, apiClient2) {
|
|
526
|
+
server2.registerTool(
|
|
527
|
+
"get_planning_context",
|
|
528
|
+
{
|
|
529
|
+
title: "Get Planning Context",
|
|
530
|
+
description: "Retrieve org-wide planning context: connected repos with summaries, cross-repo dependencies, ADRs, and active specs. Optionally focus on a target repo.",
|
|
531
|
+
inputSchema: {
|
|
532
|
+
target_repo: z4.string().optional().describe(
|
|
533
|
+
"Focus on a specific repo (owner/repo format) to get its CLAUDE.md and relevant context"
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
async ({ target_repo }) => {
|
|
538
|
+
const params = {};
|
|
539
|
+
if (target_repo) params.targetRepo = target_repo;
|
|
540
|
+
const ctx = await apiClient2.get(
|
|
541
|
+
"/v1/org/planning-context",
|
|
542
|
+
params
|
|
543
|
+
);
|
|
544
|
+
const sections = [];
|
|
545
|
+
if (ctx.repoSummaries.length > 0) {
|
|
546
|
+
const repos = ctx.repoSummaries.map(
|
|
547
|
+
(r) => `#### ${r.repo}
|
|
548
|
+
${r.purpose}
|
|
549
|
+
|
|
550
|
+
${r.summary}` + (r.conventions.length > 0 ? `
|
|
551
|
+
|
|
552
|
+
**Conventions:**
|
|
553
|
+
${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
|
|
554
|
+
).join("\n\n");
|
|
555
|
+
sections.push(`## Repositories (${ctx.repoSummaries.length})
|
|
556
|
+
|
|
557
|
+
${repos}`);
|
|
558
|
+
}
|
|
559
|
+
if (ctx.dependencies.length > 0) {
|
|
560
|
+
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
|
|
561
|
+
sections.push(`## Cross-Repo Dependencies
|
|
562
|
+
|
|
563
|
+
${deps}`);
|
|
564
|
+
}
|
|
565
|
+
if (ctx.adrs.length > 0) {
|
|
566
|
+
const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
|
|
567
|
+
sections.push(`## Architecture Decision Records (${ctx.adrs.length})
|
|
568
|
+
|
|
569
|
+
${adrs}`);
|
|
570
|
+
}
|
|
571
|
+
if (ctx.specs.length > 0) {
|
|
572
|
+
const specs = ctx.specs.map((s) => `- ${s.name} [${s.status}]`).join("\n");
|
|
573
|
+
sections.push(`## Specs (${ctx.specs.length})
|
|
574
|
+
|
|
575
|
+
${specs}`);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
content: [
|
|
579
|
+
{
|
|
580
|
+
type: "text",
|
|
581
|
+
text: sections.join("\n\n") || "No planning context available."
|
|
582
|
+
}
|
|
583
|
+
]
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/tools/get-context.ts
|
|
590
|
+
import { z as z5 } from "zod";
|
|
591
|
+
|
|
592
|
+
// src/lib/scoring.ts
|
|
593
|
+
var WEIGHT_PATH = 0.4;
|
|
594
|
+
var WEIGHT_KEYWORD = 0.3;
|
|
595
|
+
var WEIGHT_RECENCY = 0.15;
|
|
596
|
+
var WEIGHT_SEVERITY = 0.15;
|
|
597
|
+
var SEVERITY_SCORES = {
|
|
598
|
+
critical: 1,
|
|
599
|
+
high: 0.75,
|
|
600
|
+
medium: 0.5,
|
|
601
|
+
low: 0.25
|
|
602
|
+
};
|
|
603
|
+
var CHARS_PER_TOKEN = 4;
|
|
604
|
+
function estimateTokens(text) {
|
|
605
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
606
|
+
}
|
|
607
|
+
function truncateToTokens(text, maxTokens) {
|
|
608
|
+
const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
|
|
609
|
+
if (text.length <= maxChars) return { text, truncated: false };
|
|
610
|
+
const head = text.slice(0, maxChars);
|
|
611
|
+
const floor = Math.floor(maxChars * 0.75);
|
|
612
|
+
const paragraph = head.lastIndexOf("\n\n");
|
|
613
|
+
const line = head.lastIndexOf("\n");
|
|
614
|
+
let cut = maxChars;
|
|
615
|
+
if (paragraph >= floor) cut = paragraph;
|
|
616
|
+
else if (line >= floor) cut = line;
|
|
617
|
+
return { text: head.slice(0, cut).trimEnd(), truncated: true };
|
|
618
|
+
}
|
|
619
|
+
function scorePathMatch(item, languages, categories) {
|
|
620
|
+
if (languages.length === 0 && categories.length === 0) return 0.5;
|
|
621
|
+
let score = 0;
|
|
622
|
+
const itemLang = item.language?.toLowerCase() ?? "";
|
|
623
|
+
const itemCat = item.category?.toLowerCase() ?? "";
|
|
624
|
+
if (itemLang && languages.some((l) => itemLang.includes(l))) {
|
|
625
|
+
score += 0.6;
|
|
626
|
+
}
|
|
627
|
+
if (itemCat && categories.some((c) => itemCat.includes(c))) {
|
|
628
|
+
score += 0.4;
|
|
629
|
+
}
|
|
630
|
+
return Math.min(score, 1);
|
|
631
|
+
}
|
|
632
|
+
function scoreKeywordMatch(item, keywords) {
|
|
633
|
+
if (keywords.length === 0) return 0;
|
|
634
|
+
const searchable = `${item.title} ${item.content}`.toLowerCase();
|
|
635
|
+
let matches = 0;
|
|
636
|
+
for (const kw of keywords) {
|
|
637
|
+
if (searchable.includes(kw)) matches++;
|
|
638
|
+
}
|
|
639
|
+
return Math.min(matches / keywords.length, 1);
|
|
640
|
+
}
|
|
641
|
+
function scoreRecency(item) {
|
|
642
|
+
if (!item.updatedAt) return 0.5;
|
|
643
|
+
const now = Date.now();
|
|
644
|
+
const updated = new Date(item.updatedAt).getTime();
|
|
645
|
+
const daysSince = (now - updated) / (1e3 * 60 * 60 * 24);
|
|
646
|
+
if (daysSince <= 7) return 1;
|
|
647
|
+
if (daysSince <= 30) return 0.75;
|
|
648
|
+
if (daysSince <= 90) return 0.5;
|
|
649
|
+
return 0.25;
|
|
650
|
+
}
|
|
651
|
+
function scoreSeverity(item) {
|
|
652
|
+
if (!item.severity) return 0.5;
|
|
653
|
+
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
654
|
+
}
|
|
655
|
+
function extractKeywords(text) {
|
|
656
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
657
|
+
"the",
|
|
658
|
+
"a",
|
|
659
|
+
"an",
|
|
660
|
+
"is",
|
|
661
|
+
"are",
|
|
662
|
+
"was",
|
|
663
|
+
"were",
|
|
664
|
+
"be",
|
|
665
|
+
"been",
|
|
666
|
+
"being",
|
|
667
|
+
"have",
|
|
668
|
+
"has",
|
|
669
|
+
"had",
|
|
670
|
+
"do",
|
|
671
|
+
"does",
|
|
672
|
+
"did",
|
|
673
|
+
"will",
|
|
674
|
+
"would",
|
|
675
|
+
"could",
|
|
676
|
+
"should",
|
|
677
|
+
"may",
|
|
678
|
+
"might",
|
|
679
|
+
"can",
|
|
680
|
+
"shall",
|
|
681
|
+
"to",
|
|
682
|
+
"of",
|
|
683
|
+
"in",
|
|
684
|
+
"for",
|
|
685
|
+
"on",
|
|
686
|
+
"with",
|
|
687
|
+
"at",
|
|
688
|
+
"by",
|
|
689
|
+
"from",
|
|
690
|
+
"as",
|
|
691
|
+
"into",
|
|
692
|
+
"about",
|
|
693
|
+
"between",
|
|
694
|
+
"through",
|
|
695
|
+
"after",
|
|
696
|
+
"before",
|
|
697
|
+
"during",
|
|
698
|
+
"without",
|
|
699
|
+
"this",
|
|
700
|
+
"that",
|
|
701
|
+
"these",
|
|
702
|
+
"those",
|
|
703
|
+
"it",
|
|
704
|
+
"its",
|
|
705
|
+
"and",
|
|
706
|
+
"or",
|
|
707
|
+
"but",
|
|
708
|
+
"not",
|
|
709
|
+
"no",
|
|
710
|
+
"all",
|
|
711
|
+
"each",
|
|
712
|
+
"every",
|
|
713
|
+
"both",
|
|
714
|
+
"few",
|
|
715
|
+
"more",
|
|
716
|
+
"most",
|
|
717
|
+
"other",
|
|
718
|
+
"some",
|
|
719
|
+
"such",
|
|
720
|
+
"than",
|
|
721
|
+
"too",
|
|
722
|
+
"very",
|
|
723
|
+
"just",
|
|
724
|
+
"also",
|
|
725
|
+
"only",
|
|
726
|
+
"new",
|
|
727
|
+
"add",
|
|
728
|
+
"update",
|
|
729
|
+
"fix",
|
|
730
|
+
"change",
|
|
731
|
+
"make",
|
|
732
|
+
"use",
|
|
733
|
+
"get",
|
|
734
|
+
"set",
|
|
735
|
+
"need",
|
|
736
|
+
"want",
|
|
737
|
+
"implement",
|
|
738
|
+
"create"
|
|
739
|
+
]);
|
|
740
|
+
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
741
|
+
}
|
|
742
|
+
function scoreItems(items, taskDescription, filePaths) {
|
|
743
|
+
const languages = detectLanguages(filePaths);
|
|
744
|
+
const categories = detectCategories(filePaths);
|
|
745
|
+
const keywords = extractKeywords(taskDescription);
|
|
746
|
+
return items.map((item) => {
|
|
747
|
+
const pathScore = scorePathMatch(item, languages, categories);
|
|
748
|
+
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
749
|
+
const recencyScore = scoreRecency(item);
|
|
750
|
+
const severityScore = scoreSeverity(item);
|
|
751
|
+
const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
752
|
+
return {
|
|
753
|
+
type: item.type,
|
|
754
|
+
id: item.id,
|
|
755
|
+
title: item.title,
|
|
756
|
+
content: item.content,
|
|
757
|
+
score,
|
|
758
|
+
tokenEstimate: estimateTokens(item.content)
|
|
759
|
+
};
|
|
760
|
+
}).sort((a, b) => b.score - a.score);
|
|
761
|
+
}
|
|
762
|
+
function selectWithinBudget(items, maxTokens) {
|
|
763
|
+
const selected = [];
|
|
764
|
+
let tokensUsed = 0;
|
|
765
|
+
for (const item of items) {
|
|
766
|
+
if (tokensUsed + item.tokenEstimate > maxTokens) continue;
|
|
767
|
+
selected.push(item);
|
|
768
|
+
tokensUsed += item.tokenEstimate;
|
|
769
|
+
}
|
|
770
|
+
return { selected, tokensUsed, totalItems: items.length };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/lib/context-builder.ts
|
|
774
|
+
var DEFAULT_BUDGET = 4e3;
|
|
775
|
+
var RESERVE_TOKENS = 200;
|
|
776
|
+
var GUARDRAIL_SHARE = 0.35;
|
|
777
|
+
var MIN_BODY_TOKENS = 150;
|
|
778
|
+
var MAX_HYDRATED = 8;
|
|
779
|
+
var MAX_TAIL = 10;
|
|
780
|
+
var TAIL_SHARE = 0.15;
|
|
781
|
+
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
782
|
+
function formatGuardrail(g) {
|
|
783
|
+
let text = `## ${g.title} [${g.severity}]
|
|
784
|
+
|
|
785
|
+
${g.description}`;
|
|
786
|
+
if (g.pattern) text += `
|
|
787
|
+
|
|
788
|
+
**Bad:** \`\`\`
|
|
789
|
+
${g.pattern}
|
|
790
|
+
\`\`\``;
|
|
791
|
+
if (g.fix) text += `
|
|
792
|
+
|
|
793
|
+
**Good:** \`\`\`
|
|
794
|
+
${g.fix}
|
|
795
|
+
\`\`\``;
|
|
796
|
+
return text;
|
|
797
|
+
}
|
|
798
|
+
function formatSpecHeadline(s) {
|
|
799
|
+
return `### ${s.name}
|
|
800
|
+
\`spec_id: ${s.specId}\` \xB7 ${s.status} \xB7 ${s.requirementCount} requirements`;
|
|
801
|
+
}
|
|
802
|
+
function formatDocHeadline(d) {
|
|
803
|
+
return `### ${d.title}
|
|
804
|
+
\`doc_id: ${d.id}\` \xB7 \`${d.repo_full_name}/${d.path}\` \xB7 ${d.doc_type}`;
|
|
805
|
+
}
|
|
806
|
+
function formatStack(entries) {
|
|
807
|
+
return entries.map(
|
|
808
|
+
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
809
|
+
).join("\n");
|
|
810
|
+
}
|
|
811
|
+
function formatRules(rules) {
|
|
812
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const r of rules) {
|
|
814
|
+
const cat = r.category || "other";
|
|
815
|
+
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
816
|
+
byCategory.get(cat).push(r);
|
|
817
|
+
}
|
|
818
|
+
let text = "";
|
|
819
|
+
for (const [cat, catRules] of byCategory) {
|
|
820
|
+
text += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
821
|
+
`;
|
|
822
|
+
for (const r of catRules) {
|
|
823
|
+
text += `- ${r.text}
|
|
824
|
+
`;
|
|
825
|
+
}
|
|
826
|
+
text += "\n";
|
|
827
|
+
}
|
|
828
|
+
return `## Project Rules (${rules.length})
|
|
829
|
+
|
|
830
|
+
${text.trim()}`;
|
|
831
|
+
}
|
|
832
|
+
async function fetchSpecBody(api, specId) {
|
|
833
|
+
const spec = await api.get(
|
|
834
|
+
`/v1/specs/${encodeURIComponent(specId)}`
|
|
835
|
+
);
|
|
836
|
+
const parts = [];
|
|
837
|
+
if (spec.proposalContent) {
|
|
838
|
+
parts.push(`**Proposal**
|
|
839
|
+
|
|
840
|
+
${spec.proposalContent}`);
|
|
841
|
+
}
|
|
842
|
+
for (const r of spec.requirements ?? []) {
|
|
843
|
+
parts.push(`**${r.name}**
|
|
844
|
+
|
|
845
|
+
${r.content}`);
|
|
846
|
+
}
|
|
847
|
+
if (spec.taskContent) {
|
|
848
|
+
parts.push(`**Tasks**
|
|
849
|
+
|
|
850
|
+
${spec.taskContent}`);
|
|
851
|
+
}
|
|
852
|
+
const body = parts.join("\n\n").trim();
|
|
853
|
+
if (!body) throw new Error("spec has no content");
|
|
854
|
+
return body;
|
|
855
|
+
}
|
|
856
|
+
async function fetchDocBody(api, docId) {
|
|
857
|
+
const doc = await api.get(
|
|
858
|
+
`/v1/docs/${encodeURIComponent(docId)}/content`
|
|
859
|
+
);
|
|
860
|
+
if (doc.error) throw new Error(doc.error);
|
|
861
|
+
const body = (doc.content ?? "").trim();
|
|
862
|
+
if (!body) throw new Error("document is empty");
|
|
863
|
+
return body;
|
|
864
|
+
}
|
|
865
|
+
async function hydrate(api, item, meta) {
|
|
866
|
+
try {
|
|
867
|
+
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id);
|
|
868
|
+
return { item, meta, body };
|
|
869
|
+
} catch (err) {
|
|
870
|
+
return {
|
|
871
|
+
item,
|
|
872
|
+
meta,
|
|
873
|
+
error: err instanceof Error ? err.message : String(err)
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
|
|
878
|
+
const budget = maxTokens ?? DEFAULT_BUDGET;
|
|
879
|
+
const [guardrails, specs, docs, stack, rules] = await Promise.all([
|
|
880
|
+
api.get("/v1/guardrails").catch(() => []),
|
|
881
|
+
api.get("/v1/specs").catch(() => []),
|
|
882
|
+
api.get("/v1/docs").catch(() => ({ items: [] })),
|
|
883
|
+
api.get("/v1/ai/settings/stack").catch(() => []),
|
|
884
|
+
api.get("/v1/standards/rules").catch(() => [])
|
|
885
|
+
]);
|
|
886
|
+
const refs = /* @__PURE__ */ new Map();
|
|
887
|
+
const key = (type, id) => `${type}:${id}`;
|
|
888
|
+
const scorable = [
|
|
889
|
+
...guardrails.map((g) => ({
|
|
890
|
+
type: "guardrail",
|
|
891
|
+
id: g.id,
|
|
892
|
+
title: g.title,
|
|
893
|
+
content: formatGuardrail(g),
|
|
894
|
+
category: g.category,
|
|
895
|
+
severity: g.severity,
|
|
896
|
+
updatedAt: g.updatedAt
|
|
897
|
+
})),
|
|
898
|
+
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
899
|
+
refs.set(key("spec", s.specId), {
|
|
900
|
+
type: "spec",
|
|
901
|
+
id: s.specId,
|
|
902
|
+
title: s.name,
|
|
903
|
+
listLine: `- **${s.name}** \u2014 \`spec_id: ${s.specId}\` (${s.status}, ${s.requirementCount} requirements)`,
|
|
904
|
+
fetchHint: `get_specs(spec_id="${s.specId}")`
|
|
905
|
+
});
|
|
906
|
+
return {
|
|
907
|
+
type: "spec",
|
|
908
|
+
id: s.specId,
|
|
909
|
+
title: s.name,
|
|
910
|
+
content: formatSpecHeadline(s),
|
|
911
|
+
updatedAt: s.updatedAt
|
|
912
|
+
};
|
|
913
|
+
}),
|
|
914
|
+
...docs.items.map((d) => {
|
|
915
|
+
refs.set(key("doc", d.id), {
|
|
916
|
+
type: "doc",
|
|
917
|
+
id: d.id,
|
|
918
|
+
title: d.title,
|
|
919
|
+
listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)`,
|
|
920
|
+
fetchHint: `get_docs(doc_id="${d.id}")`
|
|
921
|
+
});
|
|
922
|
+
return {
|
|
923
|
+
type: "doc",
|
|
924
|
+
id: d.id,
|
|
925
|
+
title: d.title,
|
|
926
|
+
content: formatDocHeadline(d),
|
|
927
|
+
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
928
|
+
updatedAt: d.synced_at
|
|
929
|
+
};
|
|
930
|
+
})
|
|
931
|
+
];
|
|
932
|
+
const ranked = scoreItems(scorable, taskDescription, filePaths);
|
|
933
|
+
const sections = [];
|
|
934
|
+
let overhead = 0;
|
|
935
|
+
if (stack.length > 0) {
|
|
936
|
+
const section = `## Tech Stack
|
|
937
|
+
|
|
938
|
+
${formatStack(stack)}`;
|
|
939
|
+
sections.push(section);
|
|
940
|
+
overhead += estimateTokens(section);
|
|
941
|
+
}
|
|
942
|
+
if (rules.length > 0) {
|
|
943
|
+
const section = formatRules(rules);
|
|
944
|
+
sections.push(section);
|
|
945
|
+
overhead += estimateTokens(section);
|
|
946
|
+
}
|
|
947
|
+
let remaining = budget - RESERVE_TOKENS - overhead;
|
|
948
|
+
const rankedGuardrails = ranked.filter((i) => i.type === "guardrail");
|
|
949
|
+
const { selected: selectedGuardrails, tokensUsed: guardrailTokens } = selectWithinBudget(
|
|
950
|
+
rankedGuardrails,
|
|
951
|
+
Math.max(0, Math.floor(remaining * GUARDRAIL_SHARE))
|
|
952
|
+
);
|
|
953
|
+
remaining -= guardrailTokens;
|
|
954
|
+
const rankedRefs = ranked.filter(
|
|
955
|
+
(i) => i.type === "spec" || i.type === "doc"
|
|
956
|
+
);
|
|
957
|
+
const avgHeadline = rankedRefs.length ? Math.ceil(
|
|
958
|
+
rankedRefs.slice(0, MAX_HYDRATED).reduce((sum, i) => sum + i.tokenEstimate, 0) / Math.min(rankedRefs.length, MAX_HYDRATED)
|
|
959
|
+
) : 0;
|
|
960
|
+
const affordable = Math.floor(
|
|
961
|
+
Math.max(0, remaining) / (MIN_BODY_TOKENS + avgHeadline)
|
|
962
|
+
);
|
|
963
|
+
const shortlist = rankedRefs.slice(
|
|
964
|
+
0,
|
|
965
|
+
Math.max(0, Math.min(MAX_HYDRATED, affordable))
|
|
966
|
+
);
|
|
967
|
+
for (const item of shortlist) {
|
|
968
|
+
remaining -= item.tokenEstimate;
|
|
969
|
+
}
|
|
970
|
+
remaining = Math.max(0, remaining);
|
|
971
|
+
const hydrated = await Promise.all(
|
|
972
|
+
shortlist.map(
|
|
973
|
+
(item) => hydrate(api, item, refs.get(key(item.type, item.id)))
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
const tailCandidates = rankedRefs.slice(shortlist.length, shortlist.length + MAX_TAIL).map((i) => refs.get(key(i.type, i.id)));
|
|
977
|
+
const tailCost = tailCandidates.reduce(
|
|
978
|
+
(sum, m) => sum + estimateTokens(m.listLine),
|
|
979
|
+
0
|
|
980
|
+
);
|
|
981
|
+
const tailReserve = Math.min(
|
|
982
|
+
tailCost,
|
|
983
|
+
Math.floor(remaining * TAIL_SHARE)
|
|
984
|
+
);
|
|
985
|
+
const bodyBudgetStart = Math.max(0, remaining - tailReserve);
|
|
986
|
+
let bodyBudget = bodyBudgetStart;
|
|
987
|
+
let itemsLeft = hydrated.length;
|
|
988
|
+
let itemsWithBody = 0;
|
|
989
|
+
let bodyFailures = 0;
|
|
990
|
+
const blocks = [];
|
|
991
|
+
for (const h of hydrated) {
|
|
992
|
+
itemsLeft--;
|
|
993
|
+
const emit = (block) => {
|
|
994
|
+
bodyBudget -= Math.max(
|
|
995
|
+
0,
|
|
996
|
+
estimateTokens(block) - h.item.tokenEstimate
|
|
997
|
+
);
|
|
998
|
+
blocks.push(block);
|
|
999
|
+
};
|
|
1000
|
+
if (h.error !== void 0) {
|
|
1001
|
+
bodyFailures++;
|
|
1002
|
+
emit(
|
|
1003
|
+
`${h.item.content}
|
|
1004
|
+
|
|
1005
|
+
_Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
1006
|
+
);
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
const available = Math.max(0, bodyBudget);
|
|
1010
|
+
const allowance = Math.min(
|
|
1011
|
+
Math.max(MIN_BODY_TOKENS, Math.floor(available / (itemsLeft + 1))),
|
|
1012
|
+
available
|
|
1013
|
+
);
|
|
1014
|
+
if (allowance < MIN_BODY_TOKENS) {
|
|
1015
|
+
emit(
|
|
1016
|
+
`${h.item.content}
|
|
1017
|
+
|
|
1018
|
+
_Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
|
|
1019
|
+
);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const { text: body2, truncated } = truncateToTokens(
|
|
1023
|
+
h.body,
|
|
1024
|
+
allowance - TRUNCATION_NOTICE_TOKENS
|
|
1025
|
+
);
|
|
1026
|
+
itemsWithBody++;
|
|
1027
|
+
emit(
|
|
1028
|
+
`${h.item.content}
|
|
1029
|
+
|
|
1030
|
+
${body2}` + (truncated ? `
|
|
1031
|
+
|
|
1032
|
+
_[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`]_` : "")
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
remaining = Math.max(0, remaining - (bodyBudgetStart - bodyBudget));
|
|
1036
|
+
if (selectedGuardrails.length > 0) {
|
|
1037
|
+
sections.push(
|
|
1038
|
+
`## Checks (${selectedGuardrails.length})
|
|
1039
|
+
|
|
1040
|
+
` + selectedGuardrails.map((i) => i.content).join("\n\n---\n\n")
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
if (blocks.length > 0) {
|
|
1044
|
+
sections.push(
|
|
1045
|
+
`## Relevant Specs & Docs (${blocks.length})
|
|
1046
|
+
|
|
1047
|
+
_Full text of the highest-ranked items. Every item carries the ID to fetch it in full._
|
|
1048
|
+
|
|
1049
|
+
` + blocks.join("\n\n---\n\n")
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
const tailLines = [];
|
|
1053
|
+
for (const meta of tailCandidates) {
|
|
1054
|
+
const cost = estimateTokens(meta.listLine);
|
|
1055
|
+
if (cost > remaining) break;
|
|
1056
|
+
tailLines.push(meta.listLine);
|
|
1057
|
+
remaining -= cost;
|
|
1058
|
+
}
|
|
1059
|
+
if (tailLines.length > 0) {
|
|
1060
|
+
sections.push(
|
|
1061
|
+
`## Also Indexed (${tailLines.length})
|
|
1062
|
+
|
|
1063
|
+
_Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
1064
|
+
|
|
1065
|
+
` + tailLines.join("\n")
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const itemsListed = blocks.length - itemsWithBody + tailLines.length;
|
|
1069
|
+
const itemsOmitted = ranked.length - selectedGuardrails.length - blocks.length - tailLines.length;
|
|
1070
|
+
const body = sections.join("\n\n");
|
|
1071
|
+
const tokensUsed = estimateTokens(body);
|
|
1072
|
+
const stats = {
|
|
1073
|
+
budget,
|
|
1074
|
+
tokensUsed,
|
|
1075
|
+
itemsWithBody,
|
|
1076
|
+
bodyFailures,
|
|
1077
|
+
itemsListed,
|
|
1078
|
+
itemsOmitted: Math.max(0, itemsOmitted),
|
|
1079
|
+
guardrails: selectedGuardrails.length
|
|
1080
|
+
};
|
|
1081
|
+
if (sections.length === 0) {
|
|
1082
|
+
return { text: "No relevant context found for this task.", stats };
|
|
1083
|
+
}
|
|
1084
|
+
const footer = `
|
|
1085
|
+
|
|
1086
|
+
---
|
|
1087
|
+
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
1088
|
+
return { text: body + footer, stats };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/tools/get-context.ts
|
|
1092
|
+
function registerGetContext(server2, apiClient2) {
|
|
1093
|
+
server2.registerTool(
|
|
1094
|
+
"get_context",
|
|
1095
|
+
{
|
|
1096
|
+
title: "Get Context",
|
|
1097
|
+
description: "Intelligent context aggregator: fetches checks, specs, docs, and tech stack, ranks them by relevance to your current task, and returns the full text of the highest-ranked items within a token budget \u2014 each with the ID needed to fetch the rest. Use this instead of calling individual tools when you want curated, relevant context.",
|
|
1098
|
+
inputSchema: {
|
|
1099
|
+
task_description: z5.string().describe(
|
|
1100
|
+
"What you're working on (e.g., 'add a new REST endpoint for user preferences')"
|
|
1101
|
+
),
|
|
1102
|
+
file_paths: z5.array(z5.string()).optional().describe(
|
|
1103
|
+
"File paths being edited \u2014 used for language/category detection"
|
|
1104
|
+
),
|
|
1105
|
+
max_tokens: z5.number().optional().describe("Maximum token budget for the response (default: 4000)")
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
async ({ task_description, file_paths, max_tokens }) => {
|
|
1109
|
+
const { text } = await buildContext(apiClient2, {
|
|
1110
|
+
taskDescription: task_description,
|
|
1111
|
+
filePaths: file_paths,
|
|
1112
|
+
maxTokens: max_tokens
|
|
1113
|
+
});
|
|
1114
|
+
return {
|
|
1115
|
+
content: [{ type: "text", text }]
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/tools/create-guardrail.ts
|
|
1122
|
+
import { z as z6 } from "zod";
|
|
1123
|
+
var DESCRIPTION2 = "Create a new check in SystemDox. Use this when you discover a pattern that should be enforced (banned pattern, required pattern, coding rule).";
|
|
1124
|
+
var CATEGORIES = [
|
|
1125
|
+
"imports",
|
|
1126
|
+
"auth",
|
|
1127
|
+
"error-handling",
|
|
1128
|
+
"architecture",
|
|
1129
|
+
"testing",
|
|
1130
|
+
"security",
|
|
1131
|
+
"conventions",
|
|
1132
|
+
"data-access"
|
|
1133
|
+
];
|
|
1134
|
+
var inputSchema2 = () => ({
|
|
1135
|
+
title: z6.string().describe("Short title for the check"),
|
|
1136
|
+
description: z6.string().describe("Detailed description of what the check enforces and why"),
|
|
1137
|
+
severity: z6.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
|
|
1138
|
+
category: z6.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
|
|
1139
|
+
pattern: z6.string().optional().describe("Bad code pattern that should be avoided"),
|
|
1140
|
+
fix: z6.string().optional().describe("Correct code pattern to use instead"),
|
|
1141
|
+
fitness_test: z6.string().optional().describe("Shell command to detect violations in CI")
|
|
1142
|
+
});
|
|
1143
|
+
function registerCreateGuardrail(server2, apiClient2) {
|
|
1144
|
+
const handler = async ({
|
|
1145
|
+
title,
|
|
1146
|
+
description,
|
|
1147
|
+
severity,
|
|
1148
|
+
category,
|
|
1149
|
+
pattern,
|
|
1150
|
+
fix,
|
|
1151
|
+
fitness_test
|
|
1152
|
+
}) => {
|
|
1153
|
+
const result = await apiClient2.post(
|
|
1154
|
+
"/v1/guardrails",
|
|
1155
|
+
{
|
|
1156
|
+
entries: [
|
|
1157
|
+
{
|
|
1158
|
+
title,
|
|
1159
|
+
description,
|
|
1160
|
+
severity,
|
|
1161
|
+
category: category ?? "conventions",
|
|
1162
|
+
pattern: pattern ?? "",
|
|
1163
|
+
fix: fix ?? "",
|
|
1164
|
+
fitnessTest: fitness_test ?? ""
|
|
1165
|
+
}
|
|
1166
|
+
]
|
|
1167
|
+
}
|
|
1168
|
+
);
|
|
1169
|
+
apiClient2.invalidateCache("/v1/guardrails");
|
|
1170
|
+
const saved = result[0];
|
|
1171
|
+
return {
|
|
1172
|
+
content: [
|
|
1173
|
+
{
|
|
1174
|
+
type: "text",
|
|
1175
|
+
text: `Check created: **${saved.title}** (id: ${saved.id}, severity: ${saved.severity}, category: ${saved.category})`
|
|
1176
|
+
}
|
|
1177
|
+
]
|
|
1178
|
+
};
|
|
1179
|
+
};
|
|
1180
|
+
server2.registerTool(
|
|
1181
|
+
"create_check",
|
|
1182
|
+
{
|
|
1183
|
+
title: "Create Check",
|
|
1184
|
+
description: DESCRIPTION2,
|
|
1185
|
+
inputSchema: inputSchema2(),
|
|
1186
|
+
annotations: {
|
|
1187
|
+
readOnlyHint: false
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
handler
|
|
1191
|
+
);
|
|
1192
|
+
server2.registerTool(
|
|
1193
|
+
"create_guardrail",
|
|
1194
|
+
{
|
|
1195
|
+
title: "Create Check (legacy name)",
|
|
1196
|
+
description: legacyToolDescription("create_check", DESCRIPTION2),
|
|
1197
|
+
inputSchema: inputSchema2(),
|
|
1198
|
+
annotations: {
|
|
1199
|
+
readOnlyHint: false
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
handler
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/tools/update-spec.ts
|
|
1207
|
+
import { z as z7 } from "zod";
|
|
1208
|
+
function registerUpdateSpec(server2, apiClient2) {
|
|
1209
|
+
server2.registerTool(
|
|
1210
|
+
"update_spec",
|
|
1211
|
+
{
|
|
1212
|
+
title: "Update Spec",
|
|
1213
|
+
description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
|
|
1214
|
+
inputSchema: {
|
|
1215
|
+
spec_id: z7.string().describe("ID of the spec to update"),
|
|
1216
|
+
section: z7.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
|
|
1217
|
+
content: z7.string().describe(
|
|
1218
|
+
"New markdown content for the section. For requirements, provide the full requirement content."
|
|
1219
|
+
),
|
|
1220
|
+
requirement_name: z7.string().optional().describe(
|
|
1221
|
+
"Name of the requirement (required when section is 'requirements')"
|
|
1222
|
+
)
|
|
1223
|
+
},
|
|
1224
|
+
annotations: {
|
|
1225
|
+
readOnlyHint: false
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
async ({ spec_id, section, content, requirement_name }) => {
|
|
1229
|
+
const spec = await apiClient2.get(`/v1/specs/${spec_id}`);
|
|
1230
|
+
if (spec.status !== "draft") {
|
|
1231
|
+
return {
|
|
1232
|
+
content: [
|
|
1233
|
+
{
|
|
1234
|
+
type: "text",
|
|
1235
|
+
text: `Cannot modify spec "${spec.name}" \u2014 status is "${spec.status}". Only draft specs can be updated.`
|
|
1236
|
+
}
|
|
1237
|
+
],
|
|
1238
|
+
isError: true
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const body = {};
|
|
1242
|
+
if (section === "proposal") {
|
|
1243
|
+
body.proposalContent = content;
|
|
1244
|
+
} else if (section === "tasks") {
|
|
1245
|
+
body.taskContent = content;
|
|
1246
|
+
} else if (section === "requirements") {
|
|
1247
|
+
const existing = spec.requirements;
|
|
1248
|
+
const newReq = {
|
|
1249
|
+
name: requirement_name ?? "Untitled Requirement",
|
|
1250
|
+
content
|
|
1251
|
+
};
|
|
1252
|
+
body.requirements = [...existing, newReq];
|
|
1253
|
+
}
|
|
1254
|
+
await apiClient2.put(`/v1/specs/${spec_id}`, body);
|
|
1255
|
+
apiClient2.invalidateCache(`/v1/specs/${spec_id}`);
|
|
1256
|
+
apiClient2.invalidateCache("/v1/specs");
|
|
1257
|
+
return {
|
|
1258
|
+
content: [
|
|
1259
|
+
{
|
|
1260
|
+
type: "text",
|
|
1261
|
+
text: `Updated spec "${spec.name}" \u2014 ${section} section modified.`
|
|
1262
|
+
}
|
|
1263
|
+
]
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/tools/report-decision.ts
|
|
1270
|
+
import { z as z8 } from "zod";
|
|
1271
|
+
function registerReportDecision(server2, apiClient2) {
|
|
1272
|
+
server2.registerTool(
|
|
1273
|
+
"report_decision",
|
|
1274
|
+
{
|
|
1275
|
+
title: "Report Decision",
|
|
1276
|
+
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
1277
|
+
inputSchema: {
|
|
1278
|
+
title: z8.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
|
|
1279
|
+
context: z8.string().describe("What prompted this decision \u2014 the problem or constraint"),
|
|
1280
|
+
decision: z8.string().describe("What was decided and why"),
|
|
1281
|
+
consequences: z8.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
|
|
1282
|
+
related_files: z8.array(z8.string()).optional().describe("File paths related to this decision")
|
|
1283
|
+
},
|
|
1284
|
+
annotations: {
|
|
1285
|
+
readOnlyHint: false
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
async ({ title, context, decision, consequences, related_files }) => {
|
|
1289
|
+
const adrTitle = `ADR: ${title}`;
|
|
1290
|
+
const created = await apiClient2.post("/v1/specs", {
|
|
1291
|
+
name: adrTitle
|
|
1292
|
+
});
|
|
1293
|
+
const relatedSection = related_files && related_files.length > 0 ? `
|
|
1294
|
+
|
|
1295
|
+
## Related Files
|
|
1296
|
+
|
|
1297
|
+
${related_files.map((f) => `- \`${f}\``).join("\n")}` : "";
|
|
1298
|
+
const adrContent = `# ${adrTitle}
|
|
1299
|
+
|
|
1300
|
+
**Date:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
1301
|
+
**Status:** Draft (pending review)
|
|
1302
|
+
|
|
1303
|
+
## Context
|
|
1304
|
+
|
|
1305
|
+
${context}
|
|
1306
|
+
|
|
1307
|
+
## Decision
|
|
1308
|
+
|
|
1309
|
+
${decision}
|
|
1310
|
+
|
|
1311
|
+
## Consequences
|
|
1312
|
+
|
|
1313
|
+
${consequences}` + relatedSection;
|
|
1314
|
+
await apiClient2.put(`/v1/specs/${created.specId}`, {
|
|
1315
|
+
proposalContent: adrContent
|
|
1316
|
+
});
|
|
1317
|
+
apiClient2.invalidateCache("/v1/specs");
|
|
1318
|
+
return {
|
|
1319
|
+
content: [
|
|
1320
|
+
{
|
|
1321
|
+
type: "text",
|
|
1322
|
+
text: `Architecture decision recorded: **${adrTitle}** (spec id: ${created.specId})
|
|
1323
|
+
|
|
1324
|
+
Status: draft \u2014 review in SystemDox before publishing.`
|
|
1325
|
+
}
|
|
1326
|
+
]
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/tools/get-project-rules.ts
|
|
1333
|
+
import { z as z9 } from "zod";
|
|
1334
|
+
var CATEGORY_LABELS = {
|
|
1335
|
+
deployment: "Deployment",
|
|
1336
|
+
architecture: "Architecture",
|
|
1337
|
+
security: "Security",
|
|
1338
|
+
testing: "Testing",
|
|
1339
|
+
process: "Process",
|
|
1340
|
+
other: "Other"
|
|
1341
|
+
};
|
|
1342
|
+
function registerGetProjectRules(server2, apiClient2) {
|
|
1343
|
+
server2.registerTool(
|
|
1344
|
+
"get_project_rules",
|
|
1345
|
+
{
|
|
1346
|
+
title: "Get Project Rules",
|
|
1347
|
+
description: "Retrieve project rules defined in SystemDox Standards. These are team-defined principles covering deployment, architecture, security, testing, and process. Filter by category to get only relevant rules.",
|
|
1348
|
+
inputSchema: {
|
|
1349
|
+
category: z9.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
async ({ category }) => {
|
|
1353
|
+
const rules = await apiClient2.get(
|
|
1354
|
+
"/v1/standards/rules"
|
|
1355
|
+
);
|
|
1356
|
+
let filtered = rules;
|
|
1357
|
+
if (category) {
|
|
1358
|
+
filtered = filtered.filter(
|
|
1359
|
+
(r) => r.category.toLowerCase() === category.toLowerCase()
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
if (filtered.length === 0) {
|
|
1363
|
+
return {
|
|
1364
|
+
content: [
|
|
1365
|
+
{
|
|
1366
|
+
type: "text",
|
|
1367
|
+
text: `No project rules found${category ? ` in category "${category}"` : ""}.`
|
|
1368
|
+
}
|
|
1369
|
+
]
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1373
|
+
for (const rule of filtered) {
|
|
1374
|
+
const cat = rule.category || "other";
|
|
1375
|
+
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1376
|
+
grouped.get(cat).push(rule);
|
|
1377
|
+
}
|
|
1378
|
+
let text = `Found ${filtered.length} project rule(s):
|
|
1379
|
+
|
|
1380
|
+
`;
|
|
1381
|
+
for (const [cat, catRules] of grouped) {
|
|
1382
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
1383
|
+
text += `## ${label}
|
|
1384
|
+
|
|
1385
|
+
`;
|
|
1386
|
+
for (const rule of catRules) {
|
|
1387
|
+
text += `- ${rule.text}
|
|
1388
|
+
`;
|
|
1389
|
+
}
|
|
1390
|
+
text += "\n";
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
content: [
|
|
1394
|
+
{
|
|
1395
|
+
type: "text",
|
|
1396
|
+
text
|
|
1397
|
+
}
|
|
1398
|
+
]
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/tools/create-project-rule.ts
|
|
1405
|
+
import { z as z10 } from "zod";
|
|
1406
|
+
function registerCreateProjectRule(server2, apiClient2) {
|
|
1407
|
+
server2.registerTool(
|
|
1408
|
+
"create_project_rule",
|
|
1409
|
+
{
|
|
1410
|
+
title: "Create Project Rule",
|
|
1411
|
+
description: "Create a new project rule in SystemDox Standards. Use this when you discover a team principle or convention that should be enforced across the project.",
|
|
1412
|
+
inputSchema: {
|
|
1413
|
+
text: z10.string().describe(
|
|
1414
|
+
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
1415
|
+
),
|
|
1416
|
+
category: z10.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
1417
|
+
},
|
|
1418
|
+
annotations: {
|
|
1419
|
+
readOnlyHint: false
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
async ({ text, category }) => {
|
|
1423
|
+
const saved = await apiClient2.post(
|
|
1424
|
+
"/v1/standards/rules",
|
|
1425
|
+
{ text, category }
|
|
1426
|
+
);
|
|
1427
|
+
apiClient2.invalidateCache("/v1/standards/rules");
|
|
1428
|
+
return {
|
|
1429
|
+
content: [
|
|
1430
|
+
{
|
|
1431
|
+
type: "text",
|
|
1432
|
+
text: `Project rule created: **${saved.text}** (id: ${saved.id}, category: ${saved.category})`
|
|
1433
|
+
}
|
|
1434
|
+
]
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/tools/get-prompt-templates.ts
|
|
1441
|
+
function registerGetPromptTemplates(server2, apiClient2) {
|
|
1442
|
+
server2.registerTool(
|
|
1443
|
+
"get_prompt_templates",
|
|
1444
|
+
{
|
|
1445
|
+
title: "Get Prompt Templates",
|
|
1446
|
+
description: "Retrieve reusable prompt templates from SystemDox Standards. These are preconfigured prompts that Gen AI should use when generating content \u2014 e.g. code review checklists, documentation templates, or analysis frameworks.",
|
|
1447
|
+
inputSchema: {}
|
|
1448
|
+
},
|
|
1449
|
+
async () => {
|
|
1450
|
+
const templates = await apiClient2.get(
|
|
1451
|
+
"/v1/standards/prompt-templates"
|
|
1452
|
+
);
|
|
1453
|
+
if (templates.length === 0) {
|
|
1454
|
+
return {
|
|
1455
|
+
content: [
|
|
1456
|
+
{
|
|
1457
|
+
type: "text",
|
|
1458
|
+
text: "No prompt templates found."
|
|
1459
|
+
}
|
|
1460
|
+
]
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
const text = templates.map(
|
|
1464
|
+
(t) => `## ${t.name}
|
|
1465
|
+
|
|
1466
|
+
` + (t.description ? `*${t.description}*
|
|
1467
|
+
|
|
1468
|
+
` : "") + `\`\`\`
|
|
1469
|
+
${t.prompt}
|
|
1470
|
+
\`\`\``
|
|
1471
|
+
).join("\n\n---\n\n");
|
|
1472
|
+
return {
|
|
1473
|
+
content: [
|
|
1474
|
+
{
|
|
1475
|
+
type: "text",
|
|
1476
|
+
text: `Found ${templates.length} prompt template(s):
|
|
1477
|
+
|
|
1478
|
+
${text}`
|
|
1479
|
+
}
|
|
1480
|
+
]
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/tools/create-prompt-template.ts
|
|
1487
|
+
import { z as z11 } from "zod";
|
|
1488
|
+
function registerCreatePromptTemplate(server2, apiClient2) {
|
|
1489
|
+
server2.registerTool(
|
|
1490
|
+
"create_prompt_template",
|
|
1491
|
+
{
|
|
1492
|
+
title: "Create Prompt Template",
|
|
1493
|
+
description: "Create a new reusable prompt template in SystemDox Standards. Use this to store preconfigured prompts for common AI tasks like code reviews, documentation generation, or analysis frameworks.",
|
|
1494
|
+
inputSchema: {
|
|
1495
|
+
name: z11.string().describe("Short name for the template"),
|
|
1496
|
+
description: z11.string().optional().describe("Brief description of when to use this template"),
|
|
1497
|
+
prompt: z11.string().describe("The full prompt text that Gen AI will use when generating content")
|
|
1498
|
+
},
|
|
1499
|
+
annotations: {
|
|
1500
|
+
readOnlyHint: false
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
async ({ name, description, prompt }) => {
|
|
1504
|
+
const saved = await apiClient2.post(
|
|
1505
|
+
"/v1/standards/prompt-templates",
|
|
1506
|
+
{
|
|
1507
|
+
name,
|
|
1508
|
+
description: description ?? "",
|
|
1509
|
+
prompt
|
|
1510
|
+
}
|
|
1511
|
+
);
|
|
1512
|
+
apiClient2.invalidateCache("/v1/standards/prompt-templates");
|
|
1513
|
+
return {
|
|
1514
|
+
content: [
|
|
1515
|
+
{
|
|
1516
|
+
type: "text",
|
|
1517
|
+
text: `Prompt template created: **${saved.name}** (id: ${saved.id})`
|
|
1518
|
+
}
|
|
1519
|
+
]
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/server.ts
|
|
1526
|
+
function createServer(apiClient2) {
|
|
1527
|
+
const server2 = new McpServer({
|
|
1528
|
+
name: "systemdox-mcp-server",
|
|
1529
|
+
version: "0.1.0"
|
|
1530
|
+
});
|
|
1531
|
+
registerGetGuardrails(server2, apiClient2);
|
|
1532
|
+
registerGetSpecs(server2, apiClient2);
|
|
1533
|
+
registerGetDocs(server2, apiClient2);
|
|
1534
|
+
registerGetTechStack(server2, apiClient2);
|
|
1535
|
+
registerGetPlanningContext(server2, apiClient2);
|
|
1536
|
+
registerGetContext(server2, apiClient2);
|
|
1537
|
+
registerGetProjectRules(server2, apiClient2);
|
|
1538
|
+
registerGetPromptTemplates(server2, apiClient2);
|
|
1539
|
+
registerCreateGuardrail(server2, apiClient2);
|
|
1540
|
+
registerUpdateSpec(server2, apiClient2);
|
|
1541
|
+
registerReportDecision(server2, apiClient2);
|
|
1542
|
+
registerCreateProjectRule(server2, apiClient2);
|
|
1543
|
+
registerCreatePromptTemplate(server2, apiClient2);
|
|
1544
|
+
return server2;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/index.ts
|
|
1548
|
+
var config = loadConfig();
|
|
1549
|
+
var apiClient = new SystemDoxApiClient(config);
|
|
1550
|
+
var server = createServer(apiClient);
|
|
1551
|
+
async function main() {
|
|
1552
|
+
await apiClient.validate();
|
|
1553
|
+
const transport = new StdioServerTransport();
|
|
1554
|
+
await server.connect(transport);
|
|
1555
|
+
}
|
|
1556
|
+
main();
|
|
1557
|
+
export {
|
|
1558
|
+
apiClient,
|
|
1559
|
+
config,
|
|
1560
|
+
createServer
|
|
1561
|
+
};
|