septum 0.1.4 → 0.1.6

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