septum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/bin/septum.ts +4 -0
  4. package/package.json +62 -0
  5. package/src/cli/commands/check.ts +161 -0
  6. package/src/cli/commands/hook.ts +188 -0
  7. package/src/cli/commands/ingest.ts +29 -0
  8. package/src/cli/commands/init.ts +211 -0
  9. package/src/cli/commands/locate.ts +61 -0
  10. package/src/cli/commands/query.ts +64 -0
  11. package/src/cli/commands/serve.ts +5 -0
  12. package/src/cli/commands/slice.ts +90 -0
  13. package/src/cli/commands/sync.ts +31 -0
  14. package/src/cli/index.ts +178 -0
  15. package/src/cli/templates/hooks.ts +75 -0
  16. package/src/cli/templates/rules.ts +38 -0
  17. package/src/cli/templates/skill.ts +66 -0
  18. package/src/core/boundary/evaluator.ts +210 -0
  19. package/src/core/config/loader.ts +84 -0
  20. package/src/core/config/schema.ts +39 -0
  21. package/src/core/database/client.ts +132 -0
  22. package/src/core/database/repositories/dependency.repository.ts +279 -0
  23. package/src/core/database/repositories/domain.repository.ts +198 -0
  24. package/src/core/database/repositories/file.repository.ts +99 -0
  25. package/src/core/database/repositories/meta.repository.ts +33 -0
  26. package/src/core/database/repositories/symbol.repository.ts +424 -0
  27. package/src/core/database/repository.ts +242 -0
  28. package/src/core/database/schema.sql +92 -0
  29. package/src/core/discovery/topology-detector.ts +418 -0
  30. package/src/core/ingestion/hasher.ts +5 -0
  31. package/src/core/ingestion/pipeline.ts +438 -0
  32. package/src/core/parser/boundary-tracker.ts +128 -0
  33. package/src/core/parser/extractors/base.ts +60 -0
  34. package/src/core/parser/extractors/frontend.ts +194 -0
  35. package/src/core/parser/extractors/go.ts +176 -0
  36. package/src/core/parser/extractors/laravel-semantic.ts +426 -0
  37. package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -0
  38. package/src/core/parser/extractors/nestjs-semantic.ts +119 -0
  39. package/src/core/parser/extractors/php.ts +149 -0
  40. package/src/core/parser/extractors/python.ts +226 -0
  41. package/src/core/parser/extractors/registry.ts +49 -0
  42. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -0
  43. package/src/core/parser/extractors/typescript.ts +177 -0
  44. package/src/core/parser/tree-sitter.ts +39 -0
  45. package/src/core/resolver/call-graph-tracer.ts +668 -0
  46. package/src/core/resolver/module-resolver.ts +323 -0
  47. package/src/core/resolver/symbol-locator.ts +405 -0
  48. package/src/core/resolver/vertical-slice-tracer.ts +236 -0
  49. package/src/core/session/session-manager.ts +56 -0
  50. package/src/core/telemetry/telemetry.ts +86 -0
  51. package/src/index.ts +18 -0
  52. package/src/mcp/schemas.ts +68 -0
  53. package/src/mcp/server.ts +602 -0
  54. package/src/mcp/tools/check-boundary.ts +102 -0
  55. package/src/mcp/tools/get-domain-catalog.ts +91 -0
  56. package/src/mcp/tools/get-feature-context.ts +116 -0
  57. package/src/mcp/tools/get-symbol-hotspots.ts +49 -0
  58. package/src/mcp/tools/get-symbol-impact.ts +26 -0
  59. package/src/mcp/tools/get-symbol.ts +63 -0
  60. package/src/mcp/tools/locate-symbol.ts +74 -0
  61. package/src/mcp/tools/register-domain.ts +64 -0
  62. package/src/mcp/tools/trace-vertical-slice.ts +102 -0
  63. package/src/types/index.ts +296 -0
@@ -0,0 +1,602 @@
1
+ import { watch, type FSWatcher } from "node:fs";
2
+ import { extname } from "node:path";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { BoundaryEvaluator } from "../core/boundary/evaluator.ts";
10
+ import { ConfigLoader } from "../core/config/loader.ts";
11
+ import { SeptumDatabase } from "../core/database/client.ts";
12
+ import { SeptumRepository } from "../core/database/repository.ts";
13
+ import { IngestionPipeline } from "../core/ingestion/pipeline.ts";
14
+ import { SessionManager } from "../core/session/session-manager.ts";
15
+ import { SeptumTelemetry } from "../core/telemetry/telemetry.ts";
16
+ import {
17
+ handleCheckBoundary,
18
+ type CheckBoundaryArgs,
19
+ } from "./tools/check-boundary.ts";
20
+ import {
21
+ handleGetDomainCatalog,
22
+ type GetDomainCatalogArgs,
23
+ } from "./tools/get-domain-catalog.ts";
24
+ import {
25
+ handleClearFeatureContext,
26
+ handleGetFeatureContext,
27
+ type GetFeatureContextArgs,
28
+ } from "./tools/get-feature-context.ts";
29
+ import {
30
+ handleLocateSymbol,
31
+ type LocateSymbolArgs,
32
+ } from "./tools/locate-symbol.ts";
33
+ import {
34
+ handleTraceVerticalSlice,
35
+ type TraceVerticalSliceArgs,
36
+ } from "./tools/trace-vertical-slice.ts";
37
+ import {
38
+ handleGetSymbolHotspots,
39
+ type GetSymbolHotspotsArgs,
40
+ } from "./tools/get-symbol-hotspots.ts";
41
+ import { handleGetSymbol } from "./tools/get-symbol.ts";
42
+ import { handleGetSymbolImpact } from "./tools/get-symbol-impact.ts";
43
+ import { handleRegisterDomain } from "./tools/register-domain.ts";
44
+ import type { GetSymbolArgs, GetSymbolImpactArgs } from "../types/index.ts";
45
+ import {
46
+ CheckBoundarySchema,
47
+ GetDomainCatalogSchema,
48
+ GetFeatureContextSchema,
49
+ GetSymbolHotspotsSchema,
50
+ GetSymbolImpactSchema,
51
+ GetSymbolSchema,
52
+ LocateSymbolSchema,
53
+ RegisterDomainSchema,
54
+ TraceVerticalSliceSchema,
55
+ } from "./schemas.ts";
56
+
57
+ export async function runMCPServer(): Promise<void> {
58
+ const config = ConfigLoader.load();
59
+ const db = new SeptumDatabase(config.settings.db_path);
60
+ const repo = new SeptumRepository(db.raw);
61
+ const evaluator = new BoundaryEvaluator(repo);
62
+
63
+ const server = new Server(
64
+ {
65
+ name: "septum-mcp-server",
66
+ version: "0.1.0",
67
+ },
68
+ {
69
+ capabilities: {
70
+ tools: {},
71
+ },
72
+ }
73
+ );
74
+
75
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
76
+ return {
77
+ tools: [
78
+ {
79
+ name: "septum_get_domain_catalog",
80
+ description:
81
+ "Returns the authoritative structural catalog of a domain (files, archetypes, public signatures, and boundary rules). If 'domain' is omitted, returns the complete Macro Semantic Map (Telescope View) of the whole project. Token-efficient (< 400 tokens). Use this INSTEAD of browsing raw files.",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ domain: {
86
+ type: "string",
87
+ description: "Optional domain to inspect (e.g. 'orders', 'core'). If omitted, returns the project-wide Macro Semantic Map.",
88
+ },
89
+ archetype_filter: {
90
+ type: "string",
91
+ description: "Optional filter: 'service' | 'model' | 'controller' | 'repository'",
92
+ },
93
+ },
94
+ required: [],
95
+ },
96
+ },
97
+ {
98
+ name: "septum_get_feature_context",
99
+ description:
100
+ "Returns authoritative task/feature context (< 300 tokens) including allowed_touchpoints, reusable symbols with signatures, and contracts. Use this when implementing a specific feature.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {
104
+ feature: {
105
+ type: "string",
106
+ description: "Key/name of the feature to inspect (e.g. 'checkout_flow')",
107
+ },
108
+ },
109
+ required: ["feature"],
110
+ },
111
+ },
112
+ {
113
+ name: "septum_check_boundary",
114
+ description:
115
+ "Pre-flight boundary check to verify proposed file changes and imports against domain rules and feature touchpoints. Rejects illegal cross-domain dependencies before writing to disk.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ file_path: {
120
+ type: "string",
121
+ description: "Path of the file being edited or created (single file check)",
122
+ },
123
+ file_paths: {
124
+ type: "array",
125
+ items: { type: "string" },
126
+ description: "List of file paths being edited or created in a batch check",
127
+ },
128
+ proposed_imports: {
129
+ type: "array",
130
+ items: { type: "string" },
131
+ description: "List of import statements or target symbols proposed for the file",
132
+ },
133
+ feature_key: {
134
+ type: "string",
135
+ description: "Optional feature key to validate touchpoints against.",
136
+ },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ name: "septum_get_symbol_hotspots",
142
+ description:
143
+ "Discovers oversized functions, methods, and classes exceeding a specified line count threshold (default: 30 lines). Essential for Single Responsibility Principle (SRP) enforcement, finding God Functions, and isolating refactoring targets deterministically without reading raw files.",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ min_lines: {
148
+ type: "number",
149
+ description: "Minimum physical lines of code (LoC) threshold (default: 30).",
150
+ },
151
+ kind: {
152
+ type: "string",
153
+ description: "Filter by symbol kind ('function', 'method', 'class', 'struct', 'interface').",
154
+ },
155
+ domain: {
156
+ type: "string",
157
+ description: "Filter by domain name.",
158
+ },
159
+ limit: {
160
+ type: "number",
161
+ description: "Maximum results to return (default: 30).",
162
+ },
163
+ },
164
+ },
165
+ },
166
+ {
167
+ name: "septum_clear_feature_context",
168
+ description:
169
+ "Releases the active feature session lock (.septum/session.json) once feature implementation or editing is complete.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {},
173
+ },
174
+ },
175
+ {
176
+ name: "septum_locate_symbol",
177
+ description:
178
+ "Locates a symbol or resolves diagnostic error logs (e.g. 'OrderController::calculateTotal()' or 'Method X::y() does not exist'). Returns target file path, existing sibling methods, and ranked alternative suggestions when a method doesn't exist.",
179
+ inputSchema: {
180
+ type: "object",
181
+ properties: {
182
+ query: {
183
+ type: "string",
184
+ description:
185
+ "Symbol name, call syntax (e.g. 'OrderController::calculateTotal'), or error log snippet (e.g. 'Method OrderController::calculateTotal() does not exist')",
186
+ },
187
+ domain: {
188
+ type: "string",
189
+ description: "Optional domain filter (e.g. 'orders')",
190
+ },
191
+ },
192
+ required: ["query"],
193
+ },
194
+ },
195
+ {
196
+ name: "septum_trace_vertical_slice",
197
+ description:
198
+ "Traces the end-to-end vertical slice of an MVC/Inertia application (Route -> Request Validation -> Controller Action -> Model -> Frontend View/Component). Accepts route URIs, controller actions, or high-level feature intents without requiring manual file guessing.",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ query: {
203
+ type: "string",
204
+ description:
205
+ "Route URI (e.g. 'POST /orders/{id}/status'), action (e.g. 'OrderController@updateStatus'), or intent (e.g. 'Ubah status order di dashboard')",
206
+ },
207
+ },
208
+ required: ["query"],
209
+ },
210
+ },
211
+ {
212
+ name: "septum_get_symbol",
213
+ description:
214
+ "Retrieves physical coordinates and structural metadata of a symbol (exact start_line, end_line, total_lines, signature, visibility, injected dependencies, and outbound calls). Returns deterministic JSON for direct piping into view_file without line guessing.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ symbol: {
219
+ type: "string",
220
+ description:
221
+ "Symbol name, e.g. 'OrderController::cancelOrder', 'OrderController', or 'cancelOrder'",
222
+ },
223
+ domain: {
224
+ type: "string",
225
+ description: "Optional domain filter (e.g. 'orders')",
226
+ },
227
+ include_dependencies: {
228
+ type: "boolean",
229
+ description: "Whether to include injected dependencies and outbound calls (default: true)",
230
+ },
231
+ },
232
+ required: ["symbol"],
233
+ },
234
+ },
235
+ {
236
+ name: "septum_get_symbol_impact",
237
+ description:
238
+ "Calculates blast radius and maps all inbound callers/dependents of a symbol across domains. Prevents Local Scope Myopia by reporting all files, callers, and line numbers that depend on the target symbol before refactoring.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ symbol: {
243
+ type: "string",
244
+ description:
245
+ "Target symbol to inspect, e.g. 'OrderService::cancelOrder' or 'OrderService'",
246
+ },
247
+ },
248
+ required: ["symbol"],
249
+ },
250
+ },
251
+ {
252
+ name: "septum_register_domain",
253
+ description:
254
+ "Directly registers or updates a bounded-context domain in SQLite SSOT (Zero-Config). Use this to dynamically teach Septum about custom domain boundaries, allowed dependencies, and archetypes without touching any YAML files.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ name: {
259
+ type: "string",
260
+ description: "Unique domain name (e.g. 'orders', 'checkout', 'billing')",
261
+ },
262
+ root: {
263
+ type: "string",
264
+ description: "Root directory path for this domain (e.g. 'src/domains/orders', 'app/Domain/Orders')",
265
+ },
266
+ description: {
267
+ type: "string",
268
+ description: "Optional high-level architectural purpose of the domain",
269
+ },
270
+ allowed_dependencies: {
271
+ type: "array",
272
+ items: { type: "string" },
273
+ description: "List of domain names this domain is allowed to import",
274
+ },
275
+ forbidden_dependencies: {
276
+ type: "array",
277
+ items: { type: "string" },
278
+ description: "List of domain names strictly forbidden from being imported",
279
+ },
280
+ archetypes: {
281
+ type: "object",
282
+ description: "Layer archetypes (e.g. { 'service': 'src/services/**', 'model': 'src/models/**' })",
283
+ },
284
+ ingest_now: {
285
+ type: "boolean",
286
+ description: "Whether to immediately run AST ingestion on this domain's files (default: false)",
287
+ },
288
+ },
289
+ required: ["name", "root"],
290
+ },
291
+ },
292
+ ],
293
+ };
294
+ });
295
+
296
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
297
+ const { name, arguments: args } = request.params;
298
+ const startTime = performance.now();
299
+ const activeSession = SessionManager.getActiveSession(process.cwd());
300
+ const sessionPayload = activeSession
301
+ ? { feature_key: activeSession.feature_key, domain: activeSession.domain }
302
+ : null;
303
+
304
+ const executeTool = async () => {
305
+ if (name === "septum_get_domain_catalog") {
306
+ const parsed = GetDomainCatalogSchema.safeParse(args ?? {});
307
+ if (!parsed.success) {
308
+ return {
309
+ isError: true,
310
+ content: [
311
+ {
312
+ type: "text",
313
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
314
+ },
315
+ ],
316
+ };
317
+ }
318
+ return handleGetDomainCatalog(repo, config, parsed.data);
319
+ } else if (name === "septum_get_feature_context") {
320
+ const parsed = GetFeatureContextSchema.safeParse(args ?? {});
321
+ if (!parsed.success) {
322
+ return {
323
+ isError: true,
324
+ content: [
325
+ {
326
+ type: "text",
327
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
328
+ },
329
+ ],
330
+ };
331
+ }
332
+ return handleGetFeatureContext(repo, config, parsed.data);
333
+ } else if (name === "septum_locate_symbol") {
334
+ const parsed = LocateSymbolSchema.safeParse(args ?? {});
335
+ if (!parsed.success) {
336
+ return {
337
+ isError: true,
338
+ content: [
339
+ {
340
+ type: "text",
341
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
342
+ },
343
+ ],
344
+ };
345
+ }
346
+ return handleLocateSymbol(repo, config, parsed.data);
347
+ } else if (name === "septum_get_symbol") {
348
+ const parsed = GetSymbolSchema.safeParse(args ?? {});
349
+ if (!parsed.success) {
350
+ return {
351
+ isError: true,
352
+ content: [
353
+ {
354
+ type: "text",
355
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
356
+ },
357
+ ],
358
+ };
359
+ }
360
+ return handleGetSymbol(repo, config, parsed.data);
361
+ } else if (name === "septum_get_symbol_impact") {
362
+ const parsed = GetSymbolImpactSchema.safeParse(args ?? {});
363
+ if (!parsed.success) {
364
+ return {
365
+ isError: true,
366
+ content: [
367
+ {
368
+ type: "text",
369
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
370
+ },
371
+ ],
372
+ };
373
+ }
374
+ return handleGetSymbolImpact(repo, config, parsed.data);
375
+ } else if (name === "septum_trace_vertical_slice") {
376
+ const parsed = TraceVerticalSliceSchema.safeParse(args ?? {});
377
+ if (!parsed.success) {
378
+ return {
379
+ isError: true,
380
+ content: [
381
+ {
382
+ type: "text",
383
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
384
+ },
385
+ ],
386
+ };
387
+ }
388
+ return handleTraceVerticalSlice(repo, config, parsed.data);
389
+ } else if (name === "septum_clear_feature_context") {
390
+ const cleared = handleClearFeatureContext();
391
+ return {
392
+ content: [
393
+ {
394
+ type: "text",
395
+ text: cleared
396
+ ? "Active feature session successfully released."
397
+ : "No active feature session found to release.",
398
+ },
399
+ ],
400
+ };
401
+ } else if (name === "septum_check_boundary") {
402
+ const parsed = CheckBoundarySchema.safeParse(args ?? {});
403
+ if (!parsed.success) {
404
+ return {
405
+ isError: true,
406
+ content: [
407
+ {
408
+ type: "text",
409
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
410
+ },
411
+ ],
412
+ };
413
+ }
414
+ return handleCheckBoundary(evaluator, config, parsed.data);
415
+ } else if (name === "septum_get_symbol_hotspots") {
416
+ const parsed = GetSymbolHotspotsSchema.safeParse(args ?? {});
417
+ if (!parsed.success) {
418
+ return {
419
+ isError: true,
420
+ content: [
421
+ {
422
+ type: "text",
423
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
424
+ },
425
+ ],
426
+ };
427
+ }
428
+ return handleGetSymbolHotspots(repo, config, parsed.data);
429
+ } else if (name === "septum_register_domain") {
430
+ const parsed = RegisterDomainSchema.safeParse(args ?? {});
431
+ if (!parsed.success) {
432
+ return {
433
+ isError: true,
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
438
+ },
439
+ ],
440
+ };
441
+ }
442
+ return await handleRegisterDomain(repo, config, parsed.data);
443
+ } else {
444
+ throw new Error(`Unknown tool: '${name}'`);
445
+ }
446
+ };
447
+
448
+ try {
449
+ const response = await executeTool();
450
+ const durationMs = Math.round(performance.now() - startTime);
451
+
452
+ let bytesOut = 0;
453
+ let linesOut = 0;
454
+ if (Array.isArray(response?.content)) {
455
+ for (const item of response.content) {
456
+ if (typeof item.text === "string") {
457
+ bytesOut += Buffer.byteLength(item.text, "utf8");
458
+ linesOut += item.text.split("\n").length;
459
+ }
460
+ }
461
+ }
462
+
463
+ SeptumTelemetry.recordToolCall({
464
+ event: "tool_call_completed",
465
+ tool: name,
466
+ duration_ms: durationMs,
467
+ input: (args as Record<string, any>) ?? {},
468
+ metrics: { bytes_out: bytesOut, lines_out: linesOut },
469
+ session: sessionPayload,
470
+ status: response?.isError ? "error" : "success",
471
+ });
472
+
473
+ return response;
474
+ } catch (err) {
475
+ const durationMs = Math.round(performance.now() - startTime);
476
+ const errorInstance = err instanceof Error ? err : new Error(String(err));
477
+
478
+ SeptumTelemetry.recordToolCall({
479
+ event: "tool_call_failed",
480
+ tool: name,
481
+ duration_ms: durationMs,
482
+ input: (args as Record<string, any>) ?? {},
483
+ session: sessionPayload,
484
+ status: "error",
485
+ error: {
486
+ message: errorInstance.message,
487
+ stack: errorInstance.stack,
488
+ },
489
+ });
490
+
491
+ return {
492
+ isError: true,
493
+ content: [
494
+ {
495
+ type: "text",
496
+ text: `[Septum MCP Error] ${errorInstance.message}`,
497
+ },
498
+ ],
499
+ };
500
+ }
501
+ });
502
+
503
+ const transport = new StdioServerTransport();
504
+
505
+ // In-process transparent background watcher for auto-syncing codebase changes
506
+ let fsWatcher: FSWatcher | null = null;
507
+ let debounceTimer: NodeJS.Timeout | null = null;
508
+ const SUPPORTED_EXTENSIONS = new Set([".ts", ".js", ".tsx", ".jsx", ".php", ".py", ".go"]);
509
+ const IGNORED_SEGMENTS = new Set([
510
+ "node_modules",
511
+ ".git",
512
+ ".septum",
513
+ ".strata",
514
+ "dist",
515
+ ".output",
516
+ ".nuxt",
517
+ ".next",
518
+ ".cache",
519
+ "coverage",
520
+ ".turbo",
521
+ ]);
522
+
523
+ try {
524
+ const bgPipeline = new IngestionPipeline(repo);
525
+ const cwd = process.cwd();
526
+
527
+ fsWatcher = watch(cwd, { recursive: true }, (_event, filename) => {
528
+ if (!filename) return;
529
+
530
+ const norm = filename.replace(/\\/g, "/");
531
+ const parts = norm.split("/");
532
+ if (parts.some((p) => IGNORED_SEGMENTS.has(p))) return;
533
+
534
+ const ext = extname(norm).toLowerCase();
535
+ if (!SUPPORTED_EXTENSIONS.has(ext)) return;
536
+
537
+ if (debounceTimer) clearTimeout(debounceTimer);
538
+ debounceTimer = setTimeout(async () => {
539
+ try {
540
+ const freshConfig = ConfigLoader.load();
541
+ const metrics = await bgPipeline.run(freshConfig);
542
+ if (metrics.files_updated > 0) {
543
+ console.error(
544
+ `[Septum Watcher] Auto-synced ${metrics.files_updated} modified file(s) in ${metrics.duration_ms.toFixed(0)}ms.`
545
+ );
546
+ }
547
+ } catch (err: unknown) {
548
+ const msg = err instanceof Error ? err.message : String(err);
549
+ console.error(`[Septum Watcher Warning] Background auto-sync failed: ${msg}`);
550
+ }
551
+ }, 300);
552
+ });
553
+
554
+ fsWatcher.on("error", (err: Error) => {
555
+ console.error(`[Septum Watcher] File watcher error: ${err.message}`);
556
+ });
557
+ } catch (err: unknown) {
558
+ const msg = err instanceof Error ? err.message : String(err);
559
+ console.error(`[Septum Watcher Warning] Could not start filesystem watcher: ${msg}`);
560
+ }
561
+
562
+ const cleanup = () => {
563
+ if (debounceTimer) clearTimeout(debounceTimer);
564
+ if (fsWatcher) {
565
+ try {
566
+ fsWatcher.close();
567
+ } catch {
568
+ // Ignore watcher close errors during shutdown
569
+ }
570
+ }
571
+ try {
572
+ db.close();
573
+ console.error("[Septum MCP] Database connection closed cleanly.");
574
+ } catch (err) {
575
+ const msg = err instanceof Error ? err.message : String(err);
576
+ console.error(`[Septum MCP] Error closing database during cleanup: ${msg}`);
577
+ }
578
+ process.exit(0);
579
+ };
580
+
581
+ process.on("SIGINT", cleanup);
582
+ process.on("SIGTERM", cleanup);
583
+ process.on("beforeExit", () => {
584
+ if (debounceTimer) clearTimeout(debounceTimer);
585
+ if (fsWatcher) {
586
+ try {
587
+ fsWatcher.close();
588
+ } catch {
589
+ // Ignore watcher close errors
590
+ }
591
+ }
592
+ try {
593
+ db.close();
594
+ } catch (err) {
595
+ const msg = err instanceof Error ? err.message : String(err);
596
+ console.error(`[Septum MCP] Error closing database on beforeExit: ${msg}`);
597
+ }
598
+ });
599
+
600
+ await server.connect(transport);
601
+ console.error("[Septum MCP] Server started successfully on stdio transport.");
602
+ }
@@ -0,0 +1,102 @@
1
+ import type { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
2
+ import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
3
+
4
+ export interface CheckBoundaryArgs {
5
+ file_path?: string;
6
+ file_paths?: string[];
7
+ proposed_imports?: string[];
8
+ feature_key?: string;
9
+ }
10
+
11
+ export function handleCheckBoundary(
12
+ evaluator: BoundaryEvaluator,
13
+ config: ValidatedSeptumConfig,
14
+ args: CheckBoundaryArgs
15
+ ) {
16
+ const filesToCheck: string[] = [];
17
+ if (args.file_paths && Array.isArray(args.file_paths) && args.file_paths.length > 0) {
18
+ filesToCheck.push(...args.file_paths);
19
+ } else if (args.file_path) {
20
+ filesToCheck.push(args.file_path);
21
+ } else {
22
+ throw new Error("Missing required argument: specify either 'file_path' or 'file_paths'");
23
+ }
24
+
25
+ // Multi-file batch evaluation
26
+ if (filesToCheck.length > 1) {
27
+ const results = filesToCheck.map((filePath) => {
28
+ const violations = evaluator.evaluate(filePath, args.proposed_imports ?? [], args.feature_key, config);
29
+ return {
30
+ file: filePath,
31
+ status: violations.length > 0 ? "rejected" : "approved",
32
+ violations,
33
+ };
34
+ });
35
+
36
+ const totalViolations = results.reduce((acc, r) => acc + r.violations.length, 0);
37
+ const hasViolations = totalViolations > 0;
38
+
39
+ return {
40
+ content: [
41
+ {
42
+ type: "text" as const,
43
+ text: JSON.stringify(
44
+ {
45
+ status: hasViolations ? "rejected" : "approved",
46
+ total_checked: filesToCheck.length,
47
+ total_violations: totalViolations,
48
+ results,
49
+ message: hasViolations
50
+ ? `${totalViolations} boundary violation(s) detected across ${filesToCheck.length} file(s).`
51
+ : `All ${filesToCheck.length} file(s) approved. No boundary violations detected.`,
52
+ },
53
+ null,
54
+ 2
55
+ ),
56
+ },
57
+ ],
58
+ };
59
+ }
60
+
61
+ // Single file evaluation (backward-compatible output)
62
+ const singleFile = filesToCheck[0];
63
+ const violations = evaluator.evaluate(singleFile, args.proposed_imports ?? [], args.feature_key, config);
64
+
65
+ if (violations.length > 0) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text" as const,
70
+ text: JSON.stringify(
71
+ {
72
+ status: "rejected",
73
+ file: singleFile,
74
+ violations_count: violations.length,
75
+ violations,
76
+ message: "Boundary violation detected. Proposed imports violate bounded context architecture.",
77
+ },
78
+ null,
79
+ 2
80
+ ),
81
+ },
82
+ ],
83
+ };
84
+ }
85
+
86
+ return {
87
+ content: [
88
+ {
89
+ type: "text" as const,
90
+ text: JSON.stringify(
91
+ {
92
+ status: "approved",
93
+ file: singleFile,
94
+ message: "No boundary violations detected. Proposed imports are permissible.",
95
+ },
96
+ null,
97
+ 2
98
+ ),
99
+ },
100
+ ],
101
+ };
102
+ }