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