septum 0.1.3 → 0.1.5

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 (74) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -102
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -63
  5. package/src/cli/commands/check.ts +161 -161
  6. package/src/cli/commands/hook.ts +188 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +211 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +64 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +48 -90
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/commands/topology.ts +119 -0
  15. package/src/cli/index.ts +210 -199
  16. package/src/cli/templates/hooks.ts +75 -75
  17. package/src/cli/templates/rules.ts +38 -38
  18. package/src/cli/templates/skill.ts +69 -69
  19. package/src/core/boundary/evaluator.ts +232 -210
  20. package/src/core/config/loader.ts +97 -97
  21. package/src/core/config/schema.ts +39 -39
  22. package/src/core/database/client.ts +44 -44
  23. package/src/core/database/repositories/dependency.repository.ts +366 -349
  24. package/src/core/database/repositories/domain.repository.ts +218 -198
  25. package/src/core/database/repositories/environment.repository.ts +207 -0
  26. package/src/core/database/repositories/file.repository.ts +116 -106
  27. package/src/core/database/repositories/meta.repository.ts +33 -33
  28. package/src/core/database/repositories/symbol.repository.ts +443 -424
  29. package/src/core/database/repository.ts +264 -255
  30. package/src/core/database/schema.ts +34 -1
  31. package/src/core/discovery/topology-detector.ts +486 -486
  32. package/src/core/ingestion/hasher.ts +5 -5
  33. package/src/core/ingestion/pipeline.ts +584 -535
  34. package/src/core/parser/behavioral-analyzer.ts +119 -0
  35. package/src/core/parser/boundary-tracker.ts +288 -128
  36. package/src/core/parser/extractors/base.ts +62 -61
  37. package/src/core/parser/extractors/frontend.ts +194 -194
  38. package/src/core/parser/extractors/go.ts +229 -176
  39. package/src/core/parser/extractors/laravel-semantic.ts +1039 -585
  40. package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -199
  41. package/src/core/parser/extractors/nestjs-semantic.ts +119 -119
  42. package/src/core/parser/extractors/php.ts +153 -149
  43. package/src/core/parser/extractors/python.ts +228 -226
  44. package/src/core/parser/extractors/registry.ts +70 -70
  45. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  46. package/src/core/parser/extractors/typescript.ts +181 -177
  47. package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
  48. package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
  49. package/src/core/parser/schema/schema-provider-registry.ts +43 -0
  50. package/src/core/parser/schema/schema-provider.interface.ts +33 -0
  51. package/src/core/parser/tree-sitter.ts +41 -41
  52. package/src/core/resolver/call-graph-tracer.ts +666 -668
  53. package/src/core/resolver/module-resolver.ts +408 -342
  54. package/src/core/resolver/symbol-locator.ts +543 -543
  55. package/src/core/resolver/vertical-slice-tracer.ts +301 -252
  56. package/src/core/session/session-manager.ts +56 -56
  57. package/src/core/telemetry/telemetry.ts +86 -86
  58. package/src/core/topology/environment-detector.ts +860 -0
  59. package/src/core/topology/types.ts +83 -0
  60. package/src/index.ts +21 -18
  61. package/src/mcp/schemas.ts +578 -238
  62. package/src/mcp/server.ts +875 -656
  63. package/src/mcp/tools/check-boundary.ts +171 -152
  64. package/src/mcp/tools/get-domain-catalog.ts +145 -121
  65. package/src/mcp/tools/get-environment-topology.ts +141 -0
  66. package/src/mcp/tools/get-feature-context.ts +212 -116
  67. package/src/mcp/tools/get-symbol-hotspots.ts +60 -49
  68. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  69. package/src/mcp/tools/get-symbol.ts +65 -63
  70. package/src/mcp/tools/locate-symbol.ts +171 -190
  71. package/src/mcp/tools/register-domain.ts +68 -64
  72. package/src/mcp/tools/register-feature.ts +100 -0
  73. package/src/mcp/tools/trace-vertical-slice.ts +74 -103
  74. package/src/types/index.ts +335 -299
package/src/mcp/server.ts CHANGED
@@ -1,656 +1,875 @@
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
- import { SEPTUM_VERSION } from "../version.ts";
57
- import { resolveWorkspaceRoot } from "../core/resolver/path-resolver.ts";
58
- import type { ValidatedSeptumConfig } from "../core/config/schema.ts";
59
-
60
- interface WorkspaceContext {
61
- root: string;
62
- config: ValidatedSeptumConfig;
63
- db: SeptumDatabase;
64
- repo: SeptumRepository;
65
- evaluator: BoundaryEvaluator;
66
- pipeline: IngestionPipeline;
67
- }
68
-
69
- const workspaceContextCache = new Map<string, WorkspaceContext>();
70
-
71
- function extractPathHint(args: Record<string, any> | undefined): string | undefined {
72
- if (!args) return undefined;
73
- if (typeof args.workspace_path === "string" && args.workspace_path) return args.workspace_path;
74
- if (typeof args.target_path === "string" && args.target_path) return args.target_path;
75
- if (typeof args.project_root === "string" && args.project_root) return args.project_root;
76
- if (typeof args.file_path === "string" && args.file_path) return args.file_path;
77
- if (Array.isArray(args.file_paths) && args.file_paths[0]) return args.file_paths[0];
78
- if (typeof args.root === "string" && args.root) return args.root;
79
- return undefined;
80
- }
81
-
82
- function getWorkspaceContext(pathHint?: string): WorkspaceContext {
83
- const root = resolveWorkspaceRoot(pathHint);
84
- let ctx = workspaceContextCache.get(root);
85
- if (ctx) {
86
- return ctx;
87
- }
88
-
89
- const config = ConfigLoader.load(undefined, root);
90
- const db = new SeptumDatabase(config.settings.db_path);
91
- const repo = new SeptumRepository(db.raw);
92
- const evaluator = new BoundaryEvaluator(repo);
93
- const pipeline = new IngestionPipeline(repo, root);
94
-
95
- ctx = { root, config, db, repo, evaluator, pipeline };
96
- workspaceContextCache.set(root, ctx);
97
- return ctx;
98
- }
99
-
100
- export async function runMCPServer(): Promise<void> {
101
- const initialCtx = getWorkspaceContext();
102
-
103
- const server = new Server(
104
- {
105
- name: "septum-mcp-server",
106
- version: SEPTUM_VERSION,
107
- },
108
- {
109
- capabilities: {
110
- tools: {},
111
- },
112
- }
113
- );
114
-
115
- server.setRequestHandler(ListToolsRequestSchema, async () => {
116
- return {
117
- tools: [
118
- {
119
- name: "septum_get_domain_catalog",
120
- description:
121
- "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.",
122
- inputSchema: {
123
- type: "object",
124
- properties: {
125
- domain: {
126
- type: "string",
127
- description: "Optional domain to inspect (e.g. 'orders', 'core'). If omitted, returns the project-wide Macro Semantic Map.",
128
- },
129
- archetype_filter: {
130
- type: "string",
131
- description: "Optional filter: 'service' | 'model' | 'controller' | 'repository'",
132
- },
133
- },
134
- required: [],
135
- },
136
- },
137
- {
138
- name: "septum_get_feature_context",
139
- description:
140
- "Returns authoritative task/feature context (< 300 tokens) including allowed_touchpoints, reusable symbols with signatures, and contracts. Use this when implementing a specific feature.",
141
- inputSchema: {
142
- type: "object",
143
- properties: {
144
- feature: {
145
- type: "string",
146
- description: "Key/name of the feature to inspect (e.g. 'checkout_flow')",
147
- },
148
- },
149
- required: ["feature"],
150
- },
151
- },
152
- {
153
- name: "septum_check_boundary",
154
- description:
155
- "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.",
156
- inputSchema: {
157
- type: "object",
158
- properties: {
159
- file_path: {
160
- type: "string",
161
- description: "Path of the file being edited or created (single file check)",
162
- },
163
- file_paths: {
164
- type: "array",
165
- items: { type: "string" },
166
- description: "List of file paths being edited or created in a batch check",
167
- },
168
- proposed_imports: {
169
- type: "array",
170
- items: { type: "string" },
171
- description: "List of import statements or target symbols proposed for the file",
172
- },
173
- feature_key: {
174
- type: "string",
175
- description: "Optional feature key to validate touchpoints against.",
176
- },
177
- },
178
- },
179
- },
180
- {
181
- name: "septum_get_symbol_hotspots",
182
- description:
183
- "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.",
184
- inputSchema: {
185
- type: "object",
186
- properties: {
187
- min_lines: {
188
- type: "number",
189
- description: "Minimum physical lines of code (LoC) threshold (default: 30).",
190
- },
191
- kind: {
192
- type: "string",
193
- description: "Filter by symbol kind ('function', 'method', 'class', 'struct', 'interface').",
194
- },
195
- domain: {
196
- type: "string",
197
- description: "Filter by domain name.",
198
- },
199
- limit: {
200
- type: "number",
201
- description: "Maximum results to return (default: 30).",
202
- },
203
- },
204
- },
205
- },
206
- {
207
- name: "septum_clear_feature_context",
208
- description:
209
- "Releases the active feature session lock (.septum/session.json) once feature implementation or editing is complete.",
210
- inputSchema: {
211
- type: "object",
212
- properties: {},
213
- },
214
- },
215
- {
216
- name: "septum_locate_symbol",
217
- description:
218
- "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.",
219
- inputSchema: {
220
- type: "object",
221
- properties: {
222
- query: {
223
- type: "string",
224
- description:
225
- "Symbol name, call syntax (e.g. 'OrderController::calculateTotal'), or error log snippet (e.g. 'Method OrderController::calculateTotal() does not exist')",
226
- },
227
- domain: {
228
- type: "string",
229
- description: "Optional domain filter (e.g. 'orders')",
230
- },
231
- },
232
- required: ["query"],
233
- },
234
- },
235
- {
236
- name: "septum_trace_vertical_slice",
237
- description:
238
- "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.",
239
- inputSchema: {
240
- type: "object",
241
- properties: {
242
- query: {
243
- type: "string",
244
- description:
245
- "Route URI (e.g. 'POST /orders/{id}/status'), action (e.g. 'OrderController@updateStatus'), or intent (e.g. 'Ubah status order di dashboard')",
246
- },
247
- },
248
- required: ["query"],
249
- },
250
- },
251
- {
252
- name: "septum_get_symbol",
253
- description:
254
- "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.",
255
- inputSchema: {
256
- type: "object",
257
- properties: {
258
- symbol: {
259
- type: "string",
260
- description:
261
- "Symbol name, e.g. 'OrderController::cancelOrder', 'OrderController', or 'cancelOrder'",
262
- },
263
- domain: {
264
- type: "string",
265
- description: "Optional domain filter (e.g. 'orders')",
266
- },
267
- include_dependencies: {
268
- type: "boolean",
269
- description: "Whether to include injected dependencies and outbound calls (default: true)",
270
- },
271
- },
272
- required: ["symbol"],
273
- },
274
- },
275
- {
276
- name: "septum_get_symbol_impact",
277
- description:
278
- "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.",
279
- inputSchema: {
280
- type: "object",
281
- properties: {
282
- symbol: {
283
- type: "string",
284
- description:
285
- "Target symbol to inspect, e.g. 'OrderService::cancelOrder' or 'OrderService'",
286
- },
287
- },
288
- required: ["symbol"],
289
- },
290
- },
291
- {
292
- name: "septum_register_domain",
293
- description:
294
- "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.",
295
- inputSchema: {
296
- type: "object",
297
- properties: {
298
- name: {
299
- type: "string",
300
- description: "Unique domain name (e.g. 'orders', 'checkout', 'billing')",
301
- },
302
- root: {
303
- type: "string",
304
- description: "Root directory path for this domain (e.g. 'src/domains/orders', 'app/Domain/Orders')",
305
- },
306
- description: {
307
- type: "string",
308
- description: "Optional high-level architectural purpose of the domain",
309
- },
310
- allowed_dependencies: {
311
- type: "array",
312
- items: { type: "string" },
313
- description: "List of domain names this domain is allowed to import",
314
- },
315
- forbidden_dependencies: {
316
- type: "array",
317
- items: { type: "string" },
318
- description: "List of domain names strictly forbidden from being imported",
319
- },
320
- archetypes: {
321
- type: "object",
322
- description: "Layer archetypes (e.g. { 'service': 'src/services/**', 'model': 'src/models/**' })",
323
- },
324
- ingest_now: {
325
- type: "boolean",
326
- description: "Whether to immediately run AST ingestion on this domain's files (default: false)",
327
- },
328
- },
329
- required: ["name", "root"],
330
- },
331
- },
332
- ],
333
- };
334
- });
335
-
336
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
337
- const { name, arguments: args } = request.params;
338
- const startTime = performance.now();
339
- const pathHint = extractPathHint(args as Record<string, any>);
340
- const ctx = getWorkspaceContext(pathHint);
341
-
342
- const activeSession = SessionManager.getActiveSession(ctx.root);
343
- const sessionPayload = activeSession
344
- ? { feature_key: activeSession.feature_key, domain: activeSession.domain }
345
- : null;
346
-
347
- const executeTool = async () => {
348
- if (name === "septum_get_domain_catalog") {
349
- const parsed = GetDomainCatalogSchema.safeParse(args ?? {});
350
- if (!parsed.success) {
351
- return {
352
- isError: true,
353
- content: [
354
- {
355
- type: "text",
356
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
357
- },
358
- ],
359
- };
360
- }
361
- return handleGetDomainCatalog(ctx.repo, ctx.config, parsed.data, ctx.root);
362
- } else if (name === "septum_get_feature_context") {
363
- const parsed = GetFeatureContextSchema.safeParse(args ?? {});
364
- if (!parsed.success) {
365
- return {
366
- isError: true,
367
- content: [
368
- {
369
- type: "text",
370
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
371
- },
372
- ],
373
- };
374
- }
375
- return handleGetFeatureContext(ctx.repo, ctx.config, parsed.data);
376
- } else if (name === "septum_locate_symbol") {
377
- const parsed = LocateSymbolSchema.safeParse(args ?? {});
378
- if (!parsed.success) {
379
- return {
380
- isError: true,
381
- content: [
382
- {
383
- type: "text",
384
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
385
- },
386
- ],
387
- };
388
- }
389
- return await handleLocateSymbol(ctx.repo, ctx.config, parsed.data, ctx.pipeline, ctx.root);
390
- } else if (name === "septum_get_symbol") {
391
- const parsed = GetSymbolSchema.safeParse(args ?? {});
392
- if (!parsed.success) {
393
- return {
394
- isError: true,
395
- content: [
396
- {
397
- type: "text",
398
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
399
- },
400
- ],
401
- };
402
- }
403
- return handleGetSymbol(ctx.repo, ctx.config, parsed.data);
404
- } else if (name === "septum_get_symbol_impact") {
405
- const parsed = GetSymbolImpactSchema.safeParse(args ?? {});
406
- if (!parsed.success) {
407
- return {
408
- isError: true,
409
- content: [
410
- {
411
- type: "text",
412
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
413
- },
414
- ],
415
- };
416
- }
417
- return handleGetSymbolImpact(ctx.repo, ctx.config, parsed.data);
418
- } else if (name === "septum_trace_vertical_slice") {
419
- const parsed = TraceVerticalSliceSchema.safeParse(args ?? {});
420
- if (!parsed.success) {
421
- return {
422
- isError: true,
423
- content: [
424
- {
425
- type: "text",
426
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
427
- },
428
- ],
429
- };
430
- }
431
- return handleTraceVerticalSlice(ctx.repo, ctx.config, parsed.data);
432
- } else if (name === "septum_clear_feature_context") {
433
- const cleared = handleClearFeatureContext();
434
- return {
435
- content: [
436
- {
437
- type: "text",
438
- text: cleared
439
- ? "Active feature session successfully released."
440
- : "No active feature session found to release.",
441
- },
442
- ],
443
- };
444
- } else if (name === "septum_check_boundary") {
445
- const parsed = CheckBoundarySchema.safeParse(args ?? {});
446
- if (!parsed.success) {
447
- return {
448
- isError: true,
449
- content: [
450
- {
451
- type: "text",
452
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
453
- },
454
- ],
455
- };
456
- }
457
- return handleCheckBoundary(ctx.evaluator, ctx.config, parsed.data, ctx.repo);
458
- } else if (name === "septum_get_symbol_hotspots") {
459
- const parsed = GetSymbolHotspotsSchema.safeParse(args ?? {});
460
- if (!parsed.success) {
461
- return {
462
- isError: true,
463
- content: [
464
- {
465
- type: "text",
466
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
467
- },
468
- ],
469
- };
470
- }
471
- return handleGetSymbolHotspots(ctx.repo, ctx.config, parsed.data);
472
- } else if (name === "septum_register_domain") {
473
- const parsed = RegisterDomainSchema.safeParse(args ?? {});
474
- if (!parsed.success) {
475
- return {
476
- isError: true,
477
- content: [
478
- {
479
- type: "text",
480
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
481
- },
482
- ],
483
- };
484
- }
485
- return await handleRegisterDomain(ctx.repo, ctx.config, parsed.data);
486
- } else {
487
- throw new Error(`Unknown tool: '${name}'`);
488
- }
489
- };
490
-
491
- try {
492
- const response = await executeTool();
493
- const durationMs = Math.round(performance.now() - startTime);
494
-
495
- let bytesOut = 0;
496
- let linesOut = 0;
497
- if (Array.isArray(response?.content)) {
498
- for (const item of response.content) {
499
- if (typeof item.text === "string") {
500
- bytesOut += Buffer.byteLength(item.text, "utf8");
501
- linesOut += item.text.split("\n").length;
502
- }
503
- }
504
- }
505
-
506
- SeptumTelemetry.recordToolCall({
507
- event: "tool_call_completed",
508
- tool: name,
509
- duration_ms: durationMs,
510
- input: (args as Record<string, any>) ?? {},
511
- metrics: { bytes_out: bytesOut, lines_out: linesOut },
512
- session: sessionPayload,
513
- status: response?.isError ? "error" : "success",
514
- });
515
-
516
- return response;
517
- } catch (err) {
518
- const durationMs = Math.round(performance.now() - startTime);
519
- const errorInstance = err instanceof Error ? err : new Error(String(err));
520
-
521
- SeptumTelemetry.recordToolCall({
522
- event: "tool_call_failed",
523
- tool: name,
524
- duration_ms: durationMs,
525
- input: (args as Record<string, any>) ?? {},
526
- session: sessionPayload,
527
- status: "error",
528
- error: {
529
- message: errorInstance.message,
530
- stack: errorInstance.stack,
531
- },
532
- });
533
-
534
- return {
535
- isError: true,
536
- content: [
537
- {
538
- type: "text",
539
- text: `[Septum MCP Error] ${errorInstance.message}`,
540
- },
541
- ],
542
- };
543
- }
544
- });
545
-
546
- const transport = new StdioServerTransport();
547
-
548
- // In-process transparent background watcher for auto-syncing codebase changes
549
- let fsWatcher: FSWatcher | null = null;
550
- let debounceTimer: NodeJS.Timeout | null = null;
551
- const SUPPORTED_EXTENSIONS = new Set([".ts", ".js", ".tsx", ".jsx", ".php", ".py", ".go"]);
552
- const IGNORED_SEGMENTS = new Set([
553
- "node_modules",
554
- ".git",
555
- ".septum",
556
- ".strata",
557
- "dist",
558
- ".output",
559
- ".nuxt",
560
- ".next",
561
- ".cache",
562
- "coverage",
563
- ".turbo",
564
- ]);
565
-
566
- try {
567
- const bgPipeline = initialCtx.pipeline;
568
- const cwd = process.cwd();
569
-
570
- fsWatcher = watch(cwd, { recursive: true }, (_event, filename) => {
571
- if (!filename) return;
572
-
573
- const norm = filename.replace(/\\/g, "/");
574
- const parts = norm.split("/");
575
- if (parts.some((p) => IGNORED_SEGMENTS.has(p))) return;
576
-
577
- const ext = extname(norm).toLowerCase();
578
- if (!SUPPORTED_EXTENSIONS.has(ext)) return;
579
-
580
- if (debounceTimer) clearTimeout(debounceTimer);
581
- debounceTimer = setTimeout(async () => {
582
- try {
583
- const freshConfig = ConfigLoader.load();
584
- const metrics = await bgPipeline.run(freshConfig);
585
- if (metrics.files_updated > 0) {
586
- console.error(
587
- `[Septum Watcher] Auto-synced ${metrics.files_updated} modified file(s) in ${metrics.duration_ms.toFixed(0)}ms.`
588
- );
589
- }
590
- } catch (err: unknown) {
591
- const msg = err instanceof Error ? err.message : String(err);
592
- console.error(`[Septum Watcher Warning] Background auto-sync failed: ${msg}`);
593
- }
594
- }, 300);
595
- });
596
-
597
- fsWatcher.on("error", (err: Error) => {
598
- console.error(`[Septum Watcher] File watcher error: ${err.message}`);
599
- });
600
- } catch (err: unknown) {
601
- const msg = err instanceof Error ? err.message : String(err);
602
- console.error(`[Septum Watcher Warning] Could not start filesystem watcher: ${msg}`);
603
- }
604
-
605
- const cleanup = () => {
606
- if (debounceTimer) clearTimeout(debounceTimer);
607
- if (fsWatcher) {
608
- try {
609
- fsWatcher.close();
610
- } catch {
611
- // Ignore watcher close errors during shutdown
612
- }
613
- }
614
- for (const ctx of workspaceContextCache.values()) {
615
- try {
616
- ctx.db.close();
617
- } catch (err) {
618
- // Ignore close error during shutdown
619
- }
620
- }
621
- console.error("[Septum MCP] All database connections closed cleanly.");
622
- process.exit(0);
623
- };
624
-
625
- process.on("SIGINT", cleanup);
626
- process.on("SIGTERM", cleanup);
627
- process.on("beforeExit", () => {
628
- if (debounceTimer) clearTimeout(debounceTimer);
629
- if (fsWatcher) {
630
- try {
631
- fsWatcher.close();
632
- } catch {
633
- // Ignore watcher close errors
634
- }
635
- }
636
- for (const ctx of workspaceContextCache.values()) {
637
- try {
638
- ctx.db.close();
639
- } catch {
640
- // Ignore close error
641
- }
642
- }
643
- });
644
-
645
- await server.connect(transport);
646
- if (process.stdin.isTTY) {
647
- console.error(
648
- `\x1b[36m[Septum MCP]\x1b[0m Server daemon running on stdio (PID: ${process.pid}, v${SEPTUM_VERSION}).`
649
- );
650
- console.error(
651
- `\x1b[36m[Septum MCP]\x1b[0m Listening for JSON-RPC client messages. Press Ctrl+C to terminate.`
652
- );
653
- } else {
654
- console.error(`[Septum MCP] Server started successfully on stdio transport (v${SEPTUM_VERSION}).`);
655
- }
656
- }
1
+ import { watch, type FSWatcher } from "node:fs";
2
+ import { extname, resolve } 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 { ModuleResolver } from "../core/resolver/module-resolver.ts";
11
+ import { ConfigLoader } from "../core/config/loader.ts";
12
+ import { SeptumDatabase } from "../core/database/client.ts";
13
+ import { SeptumRepository } from "../core/database/repository.ts";
14
+ import { IngestionPipeline } from "../core/ingestion/pipeline.ts";
15
+ import { SessionManager } from "../core/session/session-manager.ts";
16
+ import { SeptumTelemetry } from "../core/telemetry/telemetry.ts";
17
+ import {
18
+ handleCheckBoundary,
19
+ type CheckBoundaryArgs,
20
+ } from "./tools/check-boundary.ts";
21
+ import {
22
+ handleGetDomainCatalog,
23
+ type GetDomainCatalogArgs,
24
+ } from "./tools/get-domain-catalog.ts";
25
+ import {
26
+ handleClearFeatureContext,
27
+ handleGetFeatureContext,
28
+ type GetFeatureContextArgs,
29
+ } from "./tools/get-feature-context.ts";
30
+ import {
31
+ handleLocateSymbol,
32
+ type LocateSymbolArgs,
33
+ } from "./tools/locate-symbol.ts";
34
+ import {
35
+ handleTraceVerticalSlice,
36
+ type TraceVerticalSliceArgs,
37
+ } from "./tools/trace-vertical-slice.ts";
38
+ import {
39
+ handleGetSymbolHotspots,
40
+ type GetSymbolHotspotsArgs,
41
+ } from "./tools/get-symbol-hotspots.ts";
42
+ import { handleGetSymbol } from "./tools/get-symbol.ts";
43
+ import { handleGetSymbolImpact } from "./tools/get-symbol-impact.ts";
44
+ import { handleRegisterDomain } from "./tools/register-domain.ts";
45
+ import { handleRegisterFeature } from "./tools/register-feature.ts";
46
+ import { handleGetEnvironmentTopology } from "./tools/get-environment-topology.ts";
47
+ import type { GetSymbolArgs, GetSymbolImpactArgs } from "../types/index.ts";
48
+ import {
49
+ CheckBoundarySchema,
50
+ GetDomainCatalogSchema,
51
+ GetEnvironmentTopologySchema,
52
+ GetFeatureContextSchema,
53
+ GetSymbolHotspotsSchema,
54
+ GetSymbolImpactSchema,
55
+ GetSymbolSchema,
56
+ LocateSymbolSchema,
57
+ RegisterDomainSchema,
58
+ RegisterFeatureSchema,
59
+ TraceVerticalSliceSchema,
60
+ } from "./schemas.ts";
61
+ import { SEPTUM_VERSION } from "../version.ts";
62
+ import { resolveWorkspaceRoot } from "../core/resolver/path-resolver.ts";
63
+ import type { ValidatedSeptumConfig } from "../core/config/schema.ts";
64
+
65
+ interface WorkspaceContext {
66
+ root: string;
67
+ config: ValidatedSeptumConfig;
68
+ db: SeptumDatabase;
69
+ repo: SeptumRepository;
70
+ evaluator: BoundaryEvaluator;
71
+ pipeline: IngestionPipeline;
72
+ }
73
+
74
+ const workspaceContextCache = new Map<string, WorkspaceContext>();
75
+
76
+ export function extractPathHint(args: unknown): string | undefined {
77
+ if (!args) return undefined;
78
+
79
+ if (typeof args === "string") {
80
+ const trimmed = args.trim();
81
+ if (trimmed.length > 0) return trimmed;
82
+ return undefined;
83
+ }
84
+
85
+ if (Array.isArray(args)) {
86
+ if (typeof args[0] === "string" && args[0].trim()) {
87
+ return args[0].trim();
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ if (typeof args === "object" && args !== null) {
93
+ const record = args as Record<string, any>;
94
+
95
+ const explicitWorkspace =
96
+ record.workspace_path ??
97
+ record.workspacePath ??
98
+ record.workspace ??
99
+ record.project_root ??
100
+ record.projectRoot ??
101
+ record.target_path ??
102
+ record.targetPath;
103
+ if (typeof explicitWorkspace === "string" && explicitWorkspace.trim()) {
104
+ return explicitWorkspace.trim();
105
+ }
106
+
107
+ const filePath =
108
+ record.file_path ??
109
+ record.filePath ??
110
+ record.from_file ??
111
+ record.fromFile ??
112
+ record.source_file ??
113
+ record.sourceFile ??
114
+ record.entry_file ??
115
+ record.entryFile;
116
+ if (typeof filePath === "string" && filePath.trim()) {
117
+ return filePath.trim();
118
+ }
119
+
120
+ const filePaths =
121
+ record.file_paths ??
122
+ record.filePaths ??
123
+ record.files ??
124
+ record.paths;
125
+ if (Array.isArray(filePaths) && typeof filePaths[0] === "string" && filePaths[0].trim()) {
126
+ return filePaths[0].trim();
127
+ }
128
+ if (typeof filePaths === "string" && filePaths.trim()) {
129
+ return filePaths.trim();
130
+ }
131
+
132
+ if (
133
+ !record.domain &&
134
+ !record.name &&
135
+ !record.domain_name &&
136
+ !record.domainName &&
137
+ typeof record.root === "string" &&
138
+ record.root.trim()
139
+ ) {
140
+ return record.root.trim();
141
+ }
142
+ }
143
+
144
+ return undefined;
145
+ }
146
+
147
+ function getWorkspaceContext(pathHint?: string): WorkspaceContext {
148
+ const root = resolveWorkspaceRoot(pathHint);
149
+ let ctx = workspaceContextCache.get(root);
150
+ if (ctx) {
151
+ return ctx;
152
+ }
153
+
154
+ const config = ConfigLoader.load(undefined, root);
155
+ const db = new SeptumDatabase(config.settings.db_path);
156
+ const repo = new SeptumRepository(db.raw);
157
+ const evaluator = new BoundaryEvaluator(repo, new ModuleResolver(root, repo));
158
+ const pipeline = new IngestionPipeline(repo, root);
159
+
160
+ ctx = { root, config, db, repo, evaluator, pipeline };
161
+ workspaceContextCache.set(root, ctx);
162
+ return ctx;
163
+ }
164
+
165
+ export async function runMCPServer(): Promise<void> {
166
+ const initialCtx = getWorkspaceContext();
167
+
168
+ const server = new Server(
169
+ {
170
+ name: "septum-mcp-server",
171
+ version: SEPTUM_VERSION,
172
+ },
173
+ {
174
+ capabilities: {
175
+ tools: {},
176
+ },
177
+ }
178
+ );
179
+
180
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
181
+ return {
182
+ tools: [
183
+ {
184
+ name: "septum_get_domain_catalog",
185
+ description:
186
+ "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.",
187
+ inputSchema: {
188
+ type: "object",
189
+ properties: {
190
+ domain: {
191
+ type: "string",
192
+ description: "Optional domain to inspect (e.g. 'orders', 'core'). If omitted, returns the project-wide Macro Semantic Map.",
193
+ },
194
+ archetype_filter: {
195
+ type: "string",
196
+ description: "Optional filter: 'service' | 'model' | 'controller' | 'repository'",
197
+ },
198
+ },
199
+ required: [],
200
+ },
201
+ },
202
+ {
203
+ name: "septum_get_feature_context",
204
+ description:
205
+ "Returns authoritative task/feature context (< 300 tokens) including allowed_touchpoints, reusable symbols with signatures, and contracts. Use this when implementing a specific feature.",
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ feature_key: {
210
+ type: "string",
211
+ description:
212
+ "Unique identifier for the feature to inspect (e.g. 'checkout_flow'). Symmetrically accepts 'feature'.",
213
+ },
214
+ feature: {
215
+ type: "string",
216
+ description: "Alias for 'feature_key'.",
217
+ },
218
+ },
219
+ required: [],
220
+ },
221
+ },
222
+ {
223
+ name: "septum_check_boundary",
224
+ description:
225
+ "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.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ file_path: {
230
+ type: "string",
231
+ description: "Path of the file being edited or created (single file check)",
232
+ },
233
+ file_paths: {
234
+ type: "array",
235
+ items: { type: "string" },
236
+ description: "List of file paths being edited or created in a batch check",
237
+ },
238
+ proposed_imports: {
239
+ type: "array",
240
+ items: { type: "string" },
241
+ description: "List of import statements or target symbols proposed for the file",
242
+ },
243
+ feature_key: {
244
+ type: "string",
245
+ description: "Optional feature key to validate touchpoints against.",
246
+ },
247
+ },
248
+ },
249
+ },
250
+ {
251
+ name: "septum_get_symbol_hotspots",
252
+ description:
253
+ "Discovers oversized functions, methods, and classes exceeding a specified line count threshold or cyclomatic control flow nesting depth (> 4). Essential for Single Responsibility Principle (SRP) enforcement, finding God Functions / bug magnets, and isolating refactoring targets deterministically without reading raw files.",
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ min_lines: {
258
+ type: "number",
259
+ description: "Minimum physical lines of code (LoC) threshold (default: 30).",
260
+ },
261
+ min_nesting: {
262
+ type: "number",
263
+ description: "Minimum control-flow nesting depth (e.g. nested if/for/while/try > 4).",
264
+ },
265
+ kind: {
266
+ type: "string",
267
+ description: "Filter by symbol kind ('function', 'method', 'class', 'struct', 'interface').",
268
+ },
269
+ domain: {
270
+ type: "string",
271
+ description: "Filter by domain name.",
272
+ },
273
+ limit: {
274
+ type: "number",
275
+ description: "Maximum results to return (default: 30).",
276
+ },
277
+ sort_by: {
278
+ type: "string",
279
+ enum: ["lines", "nesting", "risk_score"],
280
+ description: "Sorting criteria: 'risk_score' (combines lines & nesting depth), 'nesting', or 'lines'. Default: 'risk_score'.",
281
+ },
282
+ },
283
+ },
284
+ },
285
+ {
286
+ name: "septum_clear_feature_context",
287
+ description:
288
+ "Releases the active feature session lock (.septum/session.json) once feature implementation or editing is complete.",
289
+ inputSchema: {
290
+ type: "object",
291
+ properties: {},
292
+ },
293
+ },
294
+ {
295
+ name: "septum_locate_symbol",
296
+ description:
297
+ "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.",
298
+ inputSchema: {
299
+ type: "object",
300
+ properties: {
301
+ query: {
302
+ type: "string",
303
+ description:
304
+ "Symbol name, call syntax (e.g. 'OrderController::calculateTotal'), or error log snippet. Symmetrically accepts 'symbol'.",
305
+ },
306
+ symbol: {
307
+ type: "string",
308
+ description: "Alias for 'query'.",
309
+ },
310
+ domain: {
311
+ type: "string",
312
+ description: "Optional domain filter (e.g. 'orders')",
313
+ },
314
+ },
315
+ required: [],
316
+ },
317
+ },
318
+ {
319
+ name: "septum_trace_vertical_slice",
320
+ description:
321
+ "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.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ query: {
326
+ type: "string",
327
+ description:
328
+ "Route URI (e.g. 'POST /orders/{id}/status'), action (e.g. 'OrderController@updateStatus'), or intent (e.g. 'Ubah status order di dashboard'). Symmetrically accepts 'symbol'.",
329
+ },
330
+ symbol: {
331
+ type: "string",
332
+ description: "Alias for 'query'.",
333
+ },
334
+ max_depth: {
335
+ type: "number",
336
+ description: "Maximum depth to trace (default: 5).",
337
+ },
338
+ },
339
+ required: [],
340
+ },
341
+ },
342
+ {
343
+ name: "septum_get_symbol",
344
+ description:
345
+ "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.",
346
+ inputSchema: {
347
+ type: "object",
348
+ properties: {
349
+ symbol: {
350
+ type: "string",
351
+ description:
352
+ "Symbol name, e.g. 'OrderController::cancelOrder', 'OrderController', or 'cancelOrder'. Symmetrically accepts 'query'.",
353
+ },
354
+ query: {
355
+ type: "string",
356
+ description: "Alias for 'symbol'.",
357
+ },
358
+ domain: {
359
+ type: "string",
360
+ description: "Optional domain filter (e.g. 'orders')",
361
+ },
362
+ include_dependencies: {
363
+ type: "boolean",
364
+ description: "Whether to include injected dependencies and outbound calls (default: true)",
365
+ },
366
+ },
367
+ required: [],
368
+ },
369
+ },
370
+ {
371
+ name: "septum_get_symbol_impact",
372
+ description:
373
+ "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.",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ symbol: {
378
+ type: "string",
379
+ description:
380
+ "Target symbol to inspect, e.g. 'OrderService::cancelOrder' or 'OrderService'. Symmetrically accepts 'query'.",
381
+ },
382
+ query: {
383
+ type: "string",
384
+ description: "Alias for 'symbol'.",
385
+ },
386
+ },
387
+ required: [],
388
+ },
389
+ },
390
+ {
391
+ name: "septum_register_domain",
392
+ description:
393
+ "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.",
394
+ inputSchema: {
395
+ type: "object",
396
+ properties: {
397
+ domain: {
398
+ type: "string",
399
+ description:
400
+ "Unique domain name/identifier (e.g. 'orders', 'checkout', 'billing'). Symmetrically accepts 'name'.",
401
+ },
402
+ name: {
403
+ type: "string",
404
+ description: "Alias for 'domain'.",
405
+ },
406
+ root: {
407
+ type: "string",
408
+ description: "Root directory path for this domain (e.g. 'src/domains/orders', 'app/Domain/Orders')",
409
+ },
410
+ description: {
411
+ type: "string",
412
+ description: "Optional high-level architectural purpose of the domain",
413
+ },
414
+ allowed_dependencies: {
415
+ type: "array",
416
+ items: { type: "string" },
417
+ description: "List of domain names this domain is allowed to import",
418
+ },
419
+ forbidden_dependencies: {
420
+ type: "array",
421
+ items: { type: "string" },
422
+ description: "List of domain names strictly forbidden from being imported",
423
+ },
424
+ archetypes: {
425
+ type: "object",
426
+ description: "Layer archetypes (e.g. { 'service': 'src/services/**', 'model': 'src/models/**' })",
427
+ },
428
+ ingest_now: {
429
+ type: "boolean",
430
+ description: "Whether to immediately run AST ingestion on this domain's files (default: false)",
431
+ },
432
+ },
433
+ required: ["domain", "root"],
434
+ },
435
+ },
436
+ {
437
+ name: "septum_register_feature",
438
+ description:
439
+ "Directly registers or updates an ephemeral or persistent feature/task context in Septum. Establishes an active session lease (.septum/session.json) with allowed_touchpoints and reuse_symbols to enforce surgical blast-radius boundaries during code modifications.",
440
+ inputSchema: {
441
+ type: "object",
442
+ properties: {
443
+ feature_key: {
444
+ type: "string",
445
+ description:
446
+ "Unique identifier for the feature/task (e.g. 'penjualan', 'checkout-pos'). Symmetrically accepts 'feature'.",
447
+ },
448
+ feature: {
449
+ type: "string",
450
+ description: "Alias for 'feature_key'.",
451
+ },
452
+ domain: {
453
+ type: "string",
454
+ description: "Bounded-context domain to which this feature belongs",
455
+ },
456
+ description: {
457
+ type: "string",
458
+ description: "Scope and rationale for the feature",
459
+ },
460
+ allowed_touchpoints: {
461
+ type: "array",
462
+ items: { type: "string" },
463
+ description: "List of file paths or glob patterns permitted to be edited for this feature",
464
+ },
465
+ reuse_symbols: {
466
+ type: "array",
467
+ items: { type: "string" },
468
+ description: "Symbols/services/methods that must be reused rather than duplicated",
469
+ },
470
+ input_contract: {
471
+ type: "object",
472
+ description: "Expected input payload / DTO schema",
473
+ },
474
+ output_contract: {
475
+ type: "object",
476
+ description: "Expected output response / return contract",
477
+ },
478
+ persist_to_config: {
479
+ type: "boolean",
480
+ description: "If true, persists this feature specification to septum.config.json",
481
+ default: false,
482
+ },
483
+ },
484
+ required: ["feature_key", "domain"],
485
+ },
486
+ },
487
+ {
488
+ name: "septum_get_environment_topology",
489
+ description:
490
+ "Self-Aware Production Topology Engine: Maps and verifies the 6-layer environment topology (Edge/WAF, Gateway/Web Server, Host Platform, Runtime, Storage/Cache, Telemetry). Identifies cross-layer constraints (e.g. Cloudflare + LiteSpeed + Laravel throttle), surfaces pending architectural inquiries if layers are ambiguous, and allows recording verified environment answers to SQLite SSOT.",
491
+ inputSchema: {
492
+ type: "object",
493
+ properties: {
494
+ resolve: {
495
+ type: "object",
496
+ properties: {
497
+ layer: {
498
+ type: "string",
499
+ enum: ["edge", "gateway", "host", "runtime", "storage", "telemetry"],
500
+ description: "Target layer to manually resolve/verify",
501
+ },
502
+ platform: {
503
+ type: "string",
504
+ description: "Confirmed platform name (e.g. 'Hostinger Shared Hosting', 'Cloudflare WAF', 'LiteSpeed')",
505
+ },
506
+ constraints: {
507
+ type: "array",
508
+ items: { type: "string" },
509
+ description: "Optional list of architectural constraints for this layer",
510
+ },
511
+ },
512
+ required: ["layer", "platform"],
513
+ description: "Optional argument to record and verify a specific layer into SQLite SSOT",
514
+ },
515
+ refresh: {
516
+ type: "boolean",
517
+ description: "Force re-detection from codebase files, ignoring cached status",
518
+ },
519
+ },
520
+ },
521
+ },
522
+ ],
523
+ };
524
+ });
525
+
526
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
527
+ const { name, arguments: args } = request.params;
528
+ const startTime = performance.now();
529
+ const pathHint = extractPathHint(args as Record<string, any>);
530
+ const ctx = getWorkspaceContext(pathHint);
531
+
532
+ const activeSession = SessionManager.getActiveSession(ctx.root);
533
+ const sessionPayload = activeSession
534
+ ? { feature_key: activeSession.feature_key, domain: activeSession.domain }
535
+ : null;
536
+
537
+ const executeTool = async () => {
538
+ if (name === "septum_get_domain_catalog") {
539
+ const parsed = GetDomainCatalogSchema.safeParse(args ?? {});
540
+ if (!parsed.success) {
541
+ return {
542
+ isError: true,
543
+ content: [
544
+ {
545
+ type: "text",
546
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
547
+ },
548
+ ],
549
+ };
550
+ }
551
+ return handleGetDomainCatalog(ctx.repo, ctx.config, parsed.data, ctx.root);
552
+ } else if (name === "septum_get_feature_context") {
553
+ const parsed = GetFeatureContextSchema.safeParse(args ?? {});
554
+ if (!parsed.success) {
555
+ return {
556
+ isError: true,
557
+ content: [
558
+ {
559
+ type: "text",
560
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
561
+ },
562
+ ],
563
+ };
564
+ }
565
+ return handleGetFeatureContext(ctx.repo, ctx.config, parsed.data, ctx.root);
566
+ } else if (name === "septum_locate_symbol") {
567
+ const parsed = LocateSymbolSchema.safeParse(args ?? {});
568
+ if (!parsed.success) {
569
+ return {
570
+ isError: true,
571
+ content: [
572
+ {
573
+ type: "text",
574
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
575
+ },
576
+ ],
577
+ };
578
+ }
579
+ return await handleLocateSymbol(ctx.repo, ctx.config, parsed.data, ctx.pipeline, ctx.root);
580
+ } else if (name === "septum_get_symbol") {
581
+ const parsed = GetSymbolSchema.safeParse(args ?? {});
582
+ if (!parsed.success) {
583
+ return {
584
+ isError: true,
585
+ content: [
586
+ {
587
+ type: "text",
588
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
589
+ },
590
+ ],
591
+ };
592
+ }
593
+ return handleGetSymbol(ctx.repo, ctx.config, parsed.data, ctx.root);
594
+ } else if (name === "septum_get_symbol_impact") {
595
+ const parsed = GetSymbolImpactSchema.safeParse(args ?? {});
596
+ if (!parsed.success) {
597
+ return {
598
+ isError: true,
599
+ content: [
600
+ {
601
+ type: "text",
602
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
603
+ },
604
+ ],
605
+ };
606
+ }
607
+ return handleGetSymbolImpact(ctx.repo, ctx.config, parsed.data, ctx.root);
608
+ } else if (name === "septum_trace_vertical_slice") {
609
+ const parsed = TraceVerticalSliceSchema.safeParse(args ?? {});
610
+ if (!parsed.success) {
611
+ return {
612
+ isError: true,
613
+ content: [
614
+ {
615
+ type: "text",
616
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
617
+ },
618
+ ],
619
+ };
620
+ }
621
+ return handleTraceVerticalSlice(ctx.repo, ctx.config, parsed.data, ctx.root);
622
+ } else if (name === "septum_clear_feature_context") {
623
+ const cleared = handleClearFeatureContext(ctx.root);
624
+ return {
625
+ content: [
626
+ {
627
+ type: "text",
628
+ text: cleared
629
+ ? "Active feature session successfully released."
630
+ : "No active feature session found to release.",
631
+ },
632
+ ],
633
+ };
634
+ } else if (name === "septum_check_boundary") {
635
+ const parsed = CheckBoundarySchema.safeParse(args ?? {});
636
+ if (!parsed.success) {
637
+ return {
638
+ isError: true,
639
+ content: [
640
+ {
641
+ type: "text",
642
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
643
+ },
644
+ ],
645
+ };
646
+ }
647
+ return handleCheckBoundary(ctx.evaluator, ctx.config, parsed.data, ctx.repo, ctx.root);
648
+ } else if (name === "septum_get_symbol_hotspots") {
649
+ const parsed = GetSymbolHotspotsSchema.safeParse(args ?? {});
650
+ if (!parsed.success) {
651
+ return {
652
+ isError: true,
653
+ content: [
654
+ {
655
+ type: "text",
656
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
657
+ },
658
+ ],
659
+ };
660
+ }
661
+ return handleGetSymbolHotspots(ctx.repo, ctx.config, parsed.data, ctx.root);
662
+ } else if (name === "septum_register_domain") {
663
+ const parsed = RegisterDomainSchema.safeParse(args ?? {});
664
+ if (!parsed.success) {
665
+ return {
666
+ isError: true,
667
+ content: [
668
+ {
669
+ type: "text",
670
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
671
+ },
672
+ ],
673
+ };
674
+ }
675
+ return await handleRegisterDomain(ctx.repo, ctx.config, parsed.data, ctx.root);
676
+ } else if (name === "septum_register_feature") {
677
+ const parsed = RegisterFeatureSchema.safeParse(args ?? {});
678
+ if (!parsed.success) {
679
+ return {
680
+ isError: true,
681
+ content: [
682
+ {
683
+ type: "text",
684
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
685
+ },
686
+ ],
687
+ };
688
+ }
689
+ return await handleRegisterFeature(ctx.repo, ctx.config, parsed.data, ctx.root);
690
+ } else if (name === "septum_get_environment_topology") {
691
+ const parsed = GetEnvironmentTopologySchema.safeParse(args ?? {});
692
+ if (!parsed.success) {
693
+ return {
694
+ isError: true,
695
+ content: [
696
+ {
697
+ type: "text",
698
+ text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
699
+ },
700
+ ],
701
+ };
702
+ }
703
+ return handleGetEnvironmentTopology(ctx.repo, ctx.config, parsed.data, ctx.root);
704
+ } else {
705
+ throw new Error(`Unknown tool: '${name}'`);
706
+ }
707
+ };
708
+
709
+ try {
710
+ const response = await executeTool();
711
+ const durationMs = Math.round(performance.now() - startTime);
712
+
713
+ let bytesOut = 0;
714
+ let linesOut = 0;
715
+ if (Array.isArray(response?.content)) {
716
+ for (const item of response.content) {
717
+ if (typeof item.text === "string") {
718
+ bytesOut += Buffer.byteLength(item.text, "utf8");
719
+ linesOut += item.text.split("\n").length;
720
+ }
721
+ }
722
+ }
723
+
724
+ SeptumTelemetry.recordToolCall({
725
+ event: "tool_call_completed",
726
+ tool: name,
727
+ duration_ms: durationMs,
728
+ input: (args as Record<string, any>) ?? {},
729
+ metrics: { bytes_out: bytesOut, lines_out: linesOut },
730
+ session: sessionPayload,
731
+ status: (response as { isError?: boolean } | undefined)?.isError ? "error" : "success",
732
+ });
733
+
734
+ return response;
735
+ } catch (err) {
736
+ const durationMs = Math.round(performance.now() - startTime);
737
+ const errorInstance = err instanceof Error ? err : new Error(String(err));
738
+
739
+ SeptumTelemetry.recordToolCall({
740
+ event: "tool_call_failed",
741
+ tool: name,
742
+ duration_ms: durationMs,
743
+ input: (args as Record<string, any>) ?? {},
744
+ session: sessionPayload,
745
+ status: "error",
746
+ error: {
747
+ message: errorInstance.message,
748
+ stack: errorInstance.stack,
749
+ },
750
+ });
751
+
752
+ return {
753
+ isError: true,
754
+ content: [
755
+ {
756
+ type: "text",
757
+ text: `[Septum MCP Error] ${errorInstance.message}`,
758
+ },
759
+ ],
760
+ };
761
+ }
762
+ });
763
+
764
+ const transport = new StdioServerTransport();
765
+
766
+ // In-process transparent background watcher for auto-syncing codebase changes
767
+ let fsWatcher: FSWatcher | null = null;
768
+ let debounceTimer: NodeJS.Timeout | null = null;
769
+ const SUPPORTED_EXTENSIONS = new Set([".ts", ".js", ".tsx", ".jsx", ".php", ".py", ".go"]);
770
+ const IGNORED_SEGMENTS = new Set([
771
+ "node_modules",
772
+ ".git",
773
+ ".septum",
774
+ ".strata",
775
+ "dist",
776
+ ".output",
777
+ ".nuxt",
778
+ ".next",
779
+ ".cache",
780
+ "coverage",
781
+ ".turbo",
782
+ ]);
783
+
784
+ try {
785
+ const bgPipeline = initialCtx.pipeline;
786
+ const cwd = process.cwd();
787
+
788
+ fsWatcher = watch(cwd, { recursive: true }, (_event, filename) => {
789
+ if (!filename) return;
790
+
791
+ const norm = filename.replace(/\\/g, "/");
792
+ const parts = norm.split("/");
793
+ if (parts.some((p) => IGNORED_SEGMENTS.has(p))) return;
794
+
795
+ const ext = extname(norm).toLowerCase();
796
+ if (!SUPPORTED_EXTENSIONS.has(ext)) return;
797
+
798
+ if (debounceTimer) clearTimeout(debounceTimer);
799
+ debounceTimer = setTimeout(async () => {
800
+ try {
801
+ const freshConfig = ConfigLoader.load();
802
+ const targetPath = resolve(cwd, filename);
803
+ const updated = await bgPipeline.ingestFile(targetPath, freshConfig);
804
+ if (updated) {
805
+ console.error(
806
+ `[Septum Watcher] Auto-synced modified file '${filename}' via incremental delta sync.`
807
+ );
808
+ }
809
+ } catch (err: unknown) {
810
+ const msg = err instanceof Error ? err.message : String(err);
811
+ console.error(`[Septum Watcher Warning] Background auto-sync failed: ${msg}`);
812
+ }
813
+ }, 300);
814
+ });
815
+
816
+ fsWatcher.on("error", (err: Error) => {
817
+ console.error(`[Septum Watcher] File watcher error: ${err.message}`);
818
+ });
819
+ } catch (err: unknown) {
820
+ const msg = err instanceof Error ? err.message : String(err);
821
+ console.error(`[Septum Watcher Warning] Could not start filesystem watcher: ${msg}`);
822
+ }
823
+
824
+ const cleanup = () => {
825
+ if (debounceTimer) clearTimeout(debounceTimer);
826
+ if (fsWatcher) {
827
+ try {
828
+ fsWatcher.close();
829
+ } catch {
830
+ // Ignore watcher close errors during shutdown
831
+ }
832
+ }
833
+ for (const ctx of workspaceContextCache.values()) {
834
+ try {
835
+ ctx.db.close();
836
+ } catch (err) {
837
+ // Ignore close error during shutdown
838
+ }
839
+ }
840
+ console.error("[Septum MCP] All database connections closed cleanly.");
841
+ process.exit(0);
842
+ };
843
+
844
+ process.on("SIGINT", cleanup);
845
+ process.on("SIGTERM", cleanup);
846
+ process.on("beforeExit", () => {
847
+ if (debounceTimer) clearTimeout(debounceTimer);
848
+ if (fsWatcher) {
849
+ try {
850
+ fsWatcher.close();
851
+ } catch {
852
+ // Ignore watcher close errors
853
+ }
854
+ }
855
+ for (const ctx of workspaceContextCache.values()) {
856
+ try {
857
+ ctx.db.close();
858
+ } catch {
859
+ // Ignore close error
860
+ }
861
+ }
862
+ });
863
+
864
+ await server.connect(transport);
865
+ if (process.stdin.isTTY) {
866
+ console.error(
867
+ `\x1b[36m[Septum MCP]\x1b[0m Server daemon running on stdio (PID: ${process.pid}, v${SEPTUM_VERSION}).`
868
+ );
869
+ console.error(
870
+ `\x1b[36m[Septum MCP]\x1b[0m Listening for JSON-RPC client messages. Press Ctrl+C to terminate.`
871
+ );
872
+ } else {
873
+ console.error(`[Septum MCP] Server started successfully on stdio transport (v${SEPTUM_VERSION}).`);
874
+ }
875
+ }