ast-mcp-server 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +47 -15
  3. package/SECURITY.md +31 -0
  4. package/dist/cli.js +0 -0
  5. package/dist/index.js +129 -10
  6. package/dist/server.js +21 -2
  7. package/dist/services/agent-setup.js +36 -117
  8. package/dist/services/agent-targets.js +122 -0
  9. package/dist/services/context-builder.js +52 -24
  10. package/dist/services/diagnostics.js +10 -5
  11. package/dist/services/file-fingerprints.js +104 -0
  12. package/dist/services/file-snapshot.js +8 -1
  13. package/dist/services/impact.js +334 -0
  14. package/dist/services/operations.js +343 -254
  15. package/dist/services/outline.js +94 -21
  16. package/dist/services/pagination.js +10 -4
  17. package/dist/services/project-operation-scheduler.js +475 -0
  18. package/dist/services/project-status.js +134 -52
  19. package/dist/services/project-watcher.js +230 -0
  20. package/dist/services/project.js +646 -66
  21. package/dist/services/public-errors.js +255 -0
  22. package/dist/services/references.js +7 -1
  23. package/dist/services/relationships.js +1198 -0
  24. package/dist/services/request-context.js +42 -0
  25. package/dist/services/runtime-activity.js +65 -0
  26. package/dist/services/runtime-logger.js +50 -0
  27. package/dist/services/runtime-policy.js +47 -0
  28. package/dist/services/setup-wizard.js +4 -7
  29. package/dist/services/shutdown.js +93 -0
  30. package/dist/services/symbol-index-limits.js +5 -0
  31. package/dist/services/symbol-index-order.js +13 -0
  32. package/dist/services/symbol-index-policy.js +91 -0
  33. package/dist/services/symbol-index-sqlite.js +850 -0
  34. package/dist/services/symbol-index.js +447 -0
  35. package/dist/services/symbols.js +403 -79
  36. package/dist/services/test-candidates.js +209 -0
  37. package/dist/services/workspace.js +18 -7
  38. package/dist/tools/apply_operation.js +8 -4
  39. package/dist/tools/explore.js +8 -6
  40. package/dist/tools/find_references.js +12 -13
  41. package/dist/tools/get_diagnostics.js +10 -7
  42. package/dist/tools/get_file.js +23 -11
  43. package/dist/tools/get_impact.js +134 -0
  44. package/dist/tools/get_operation_preview.js +7 -4
  45. package/dist/tools/get_outline.js +8 -6
  46. package/dist/tools/get_project_status.js +77 -8
  47. package/dist/tools/get_symbol_source.js +8 -6
  48. package/dist/tools/list_files.js +11 -6
  49. package/dist/tools/rename_symbol.js +6 -4
  50. package/dist/tools/replace_symbol_body.js +6 -4
  51. package/dist/tools/result.js +27 -17
  52. package/dist/tools/scaffold_class.js +6 -4
  53. package/dist/tools/search_symbols.js +15 -14
  54. package/docs/support.md +80 -0
  55. package/package.json +16 -5
  56. package/skills/structural-code-editing/SKILL.md +37 -10
package/CHANGELOG.md CHANGED
@@ -4,13 +4,38 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.0] — release candidate (unreleased, 2026-08-11)
8
+
9
+ ### Added
10
+
11
+ - Compiler-backed, freshness-aware impact traversal with bounded direct/transitive relationships and fail-closed test-candidate evidence.
12
+ - Opt-in SQLite symbol-index canary with corruption/write-failure fallback, restart reuse, explicit recovery, and bounded project-status observability; default and reserved `enabled` policies remain memory-only.
13
+ - Bounded per-project scheduling, cancellation/deadline semantics, idempotent graceful shutdown, and a closed public-error envelope with correlation IDs.
14
+ - Deterministic Linux x64 production-readiness evidence across Node.js 22.5.0 and Node.js 24 for this repository and `x-scraper`.
15
+ - Exact-SHA staged release workflows for `next` publication, public-registry verification, and separately authorized `latest` promotion.
16
+
17
+ ### Changed
18
+
19
+ - Symbol search and impact discovery now remain compiler-authoritative while applying deterministic limits before globally materializing expensive relationships.
20
+ - The supported release target is explicitly Linux x64 with the required GNU coreutils publication primitive; other platforms and architectures remain unverified.
21
+ - The package now ships its support and security policies alongside the README, changelog, binaries, and bundled structural-editing skill.
22
+
23
+ ### Security
24
+
25
+ - Release preparation, GitHub authorization, OIDC publication, and npm-token promotion use physically separated least-authority phases with lifecycle scripts disabled.
26
+ - Workflow actions, permissions, inputs, conditions, commands, environments, and credential placement are checked against a closed immutable policy.
27
+ - Package publication binds exact tarball bytes and packed `gitHead` to the authorized SHA; consumer verification independently proves preview/apply/replay postimages and no-write failure paths.
28
+ - Public MCP and stderr errors are bounded and sanitized against source, path, stack, environment, and credential disclosure.
29
+
7
30
  ## [0.6.0] - 2026-08-05
8
31
 
9
32
  ### Added
10
33
 
11
34
  - Read-only `ast_get_file` with bounded exact source lines, SHA-256 byte hashes, compiler snapshot state, and `symbols_only` body-free output.
35
+ - `ast_get_file` now also returns bounded project freshness metadata, distinguishing fresh, pending, stale, rebuilding, and degraded session state from the file-level snapshot state.
12
36
  - Project-scoped file snapshot validation with UTF-8 checks, traversal rejection, symlink containment checks, and ambiguous-path failures.
13
37
  - Read-only `ast_explore` with query/file/symbol routing, progressive source and compiler-reference evidence, freshness/completeness metadata, unresolved selectors, and serialized byte budgets.
38
+ - Public trust guidance for compiler, syntax, heuristic and derived-index evidence, including the fail-closed rules for stale/degraded reads and compiler-backed test candidates.
14
39
  - A deterministic context workflow benchmark comparing full-file, primitive, and composed exploration workflows.
15
40
 
16
41
  ## [0.5.1] - 2026-08-05
@@ -25,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
25
50
 
26
51
  - Source synchronization verifies stability across refresh, snapshot, refresh, and verification before reporting fresh state.
27
52
  - Phase 1 keeps the symbol index disabled while exposing explicit stale/degraded recovery state.
53
+ - Documentation now distinguishes file snapshot freshness from project freshness, reports bounded read budgets/truncation, and defines the prepare-review-apply mutation boundary.
28
54
 
29
55
  ### Security
30
56
 
@@ -93,6 +119,7 @@ Initial public release.
93
119
  - Apply rejects stale workspaces, changed configs/sources, mismatched plan hashes, unsafe plan files, conflicting MCP registrations, and conflicting skill content.
94
120
  - Inputs, outputs, operation stores, subprocesses, batch fan-out, plan lifetime, and filesystem access are bounded.
95
121
 
122
+ [0.6.0]: https://github.com/yailPeralta/ast-mcp-server/releases/tag/v0.6.0
96
123
  [0.5.1]: https://github.com/yailPeralta/ast-mcp-server/releases/tag/v0.5.1
97
124
  [0.5.0]: https://github.com/yailPeralta/ast-mcp-server/releases/tag/v0.5.0
98
125
  [0.4.0]: https://github.com/yailPeralta/ast-mcp-server/releases/tag/v0.4.0
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
+ <p align="center">
2
+ <img src="assets/header.jpg" alt="Abstract network of connected code symbols with the text AST MCP Server." width="100%" />
3
+ </p>
4
+
1
5
  # ast-mcp-server
2
6
 
3
7
  [![CI](https://github.com/yailPeralta/ast-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/yailPeralta/ast-mcp-server/actions/workflows/ci.yml)
4
8
  [![npm version](https://img.shields.io/npm/v/ast-mcp-server.svg)](https://www.npmjs.com/package/ast-mcp-server)
5
- [![Node.js 20.19+](https://img.shields.io/badge/Node.js-20.19%2B-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
9
+ [![Node.js 22.5+](https://img.shields.io/badge/Node.js-22.5%2B-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
6
10
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
7
11
 
8
12
  `ast-mcp-server` gives coding agents compact, type-aware access to TypeScript and JavaScript projects. It uses the real compiler project model through `ts-morph`, so declarations, references, rename locations, and diagnostics come from the AST instead of text-search guesses.
@@ -21,27 +25,44 @@ Coding agents often fall back to two generic operations: read files as plain tex
21
25
 
22
26
  This MCP server gives the agent structural code tools in addition to generic file reads and writes. Under the hood, `ts-morph` uses the TypeScript compiler project model, so the server can reason about declarations and references as code rather than undifferentiated text.
23
27
 
24
- | Need | Structural operation | Returned scope |
25
- | ------------------------------ | ------------------------- | ----------------------------------------------------- |
26
- | Read a bounded file | `ast_get_file` | Exact selected source lines, hash, and snapshot state |
27
- | Explore bounded context | `ast_explore` | Ranked selectors plus optional source and references |
28
- | Understand a file | `ast_get_outline` | Signatures without implementation bodies |
29
- | Inspect one declaration | `ast_get_symbol_source` | Exact source for one function, method, class, or type |
30
- | Find usages across the project | `ast_find_references` | Compiler-resolved reference locations |
31
- | Rename a symbol everywhere | `ast_rename_symbol` | A reviewed project-wide rename plan |
32
- | Change one implementation | `ast_replace_symbol_body` | A body-only plan that preserves the declaration |
28
+ | Need | Structural operation | Returned scope |
29
+ | ------------------------------ | ------------------------- | ---------------------------------------------------------- |
30
+ | Read a bounded file | `ast_get_file` | Exact selected source lines, hashes, and bounded freshness |
31
+ | Explore bounded context | `ast_explore` | Ranked selectors plus optional source and references |
32
+ | Understand a file | `ast_get_outline` | Signatures without implementation bodies |
33
+ | Inspect one declaration | `ast_get_symbol_source` | Exact source for one function, method, class, or type |
34
+ | Find usages across the project | `ast_find_references` | Compiler-resolved reference locations |
35
+ | Understand symbol impact | `ast_get_impact` | Bounded direct/transitive compiler-backed relationships |
36
+ | Rename a symbol everywhere | `ast_rename_symbol` | A reviewed project-wide rename plan |
37
+ | Change one implementation | `ast_replace_symbol_body` | A body-only plan that preserves the declaration |
33
38
 
34
39
  Reads can start with a bounded file slice, a compact outline, or exact source only for the declaration that needs inspection. Mutations are prepared in memory first, compared against baseline diagnostics, and returned as immutable, hash-bound plans. Nothing is written until the caller reviews and explicitly applies the plan.
35
40
 
36
41
  ### Choosing a read tool
37
42
 
38
- - Use `ast_get_file` when the file path is known and the agent needs exact source lines. It is read-only, uses zero-based `offset` and bounded `limit`, returns one-based line records, a SHA-256 byte hash, and `snapshot_state`.
43
+ - Use `ast_get_file` when the file path is known and the agent needs exact source lines. It is read-only, uses zero-based `offset` and bounded `limit`, returns one-based line records, a SHA-256 byte hash, file-level `snapshot_state`, and bounded project `freshness` metadata (`fresh`, `pending`, `stale`, `rebuilding`, or `degraded`).
39
44
  - Use `ast_get_file` with `symbols_only: true` when only selectors and body-free signatures are needed from one known file.
40
45
  - Use `ast_explore` when the question spans discovery and evidence. Its default summary is bounded; use `detail: "context"` for selected source and `detail: "full"` for source plus compiler references.
41
46
  - Use `ast_get_outline` for a compact body-free view of a known file without source lines.
42
47
  - Use `ast_get_symbol_source` when one declaration or implementation is the required evidence.
48
+ - Use `ast_get_impact` when the exact symbol is known and bounded direct/transitive compiler relationships are needed; it is read-only evidence, not a mutation plan.
49
+
50
+ `snapshot_state: "fresh"` means that the returned file bytes match the synchronized compiler snapshot. The separate `freshness` object describes the project/session state and preserves causes such as source changes or watcher failure. Neither field means that the project has zero TypeScript diagnostics; use `ast_get_diagnostics` for compiler errors and warnings.
51
+
52
+ ### Trust, freshness and completeness
53
+
54
+ The server exposes evidence labels instead of collapsing every result into an unqualified confidence score:
55
+
56
+ | Label | Meaning | Safe use |
57
+ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
58
+ | `provenance: "compiler"`, `confidence: "exact"`, `resolution: "resolved"`, `freshness.state: "fresh"` | A relationship resolved by the active TypeScript compiler snapshot. This is the only combination that sets `compiler_authoritative: true`. | May support bounded impact evidence and compiler-backed test candidates. |
59
+ | `provenance: "syntax"` | Syntax or AST structure without semantic symbol resolution. | Navigation and structural context only; not proof that two symbols are related. |
60
+ | `provenance: "heuristic"` | A convention or name-based suggestion. | Discovery hints only; never mutation authority or a compiler-backed test candidate. |
61
+ | index evidence | A derived query accelerator, not compiler authority. The current production status reports the index as disabled; any future ready index still requires compiler selector validation and a compiler fallback. | Faster routing only; stale, missing or mismatched entries must fail closed or fall back to the compiler. |
62
+
63
+ Freshness is orthogonal to TypeScript diagnostics. `fresh` means the evidence matches the synchronized snapshot; `pending`, `rebuilding`, `stale`, or `degraded` means the response must not be presented as current compiler evidence. Read tools expose the state, causes (`source_change`, `config_change`, `index_failure`, `watcher_failure`, or `compiler_rebuild`), and bounded `checked_at` timestamp. `ast_get_impact` refuses non-fresh compiler relationships. `ast_explore` returns the state together with `completeness`, `unresolved`, `budget`, and `truncation` metadata rather than silently dropping evidence.
43
64
 
44
- `snapshot_state: "fresh"` means that the returned file bytes match the synchronized compiler snapshot. It does not mean that the project has zero TypeScript diagnostics; use `ast_get_diagnostics` for compiler errors and warnings.
65
+ All reads are budgeted. Callers control pagination and, where applicable, `max_bytes`, `reference_limit`, `max_depth`, `max_nodes`, and `max_edges`; responses report the effective limits and whether a record, byte, depth, edge, invocation, or serialization limit truncated the result. A truncated or unresolved result is incomplete evidence, not an empty negative result. The internal test-candidate resolver follows the same rule: it accepts only fresh, exact compiler-backed impact, emits direct/transitive evidence and bounded relationship IDs, and never executes tests or guesses from filenames alone.
45
66
 
46
67
  ## Why this helps
47
68
 
@@ -55,10 +76,20 @@ The included batch benchmark records a 50% reduction in model round-trips and a
55
76
 
56
77
  ## Requirements
57
78
 
58
- - Node.js 20.19 or newer
79
+ - Node.js 22.5.0 or newer
59
80
  - Corepack with Yarn 4.15.0 (pinned by `packageManager`)
60
81
  - A target project with a `tsconfig.json`
61
82
 
83
+ ## Supported environment and trust boundary
84
+
85
+ The supported v0.7.0 target is Linux x64 with GNU coreutils `mv` supporting `--update=none-fail`, under Node.js 22.5.0 and the current Node.js 24 line. The production-readiness matrix covers both runtimes on that target; the Node 22.5 persistence canary requires `--experimental-sqlite`. Other Linux architectures or systems without the required GNU `mv`, macOS, and Windows are unverified until they pass equivalent filesystem, process, MCP/CLI/package, and mutation gates.
86
+
87
+ This is a local stdio server. It runs with the invoking user's filesystem permissions, and clients may request any `project_root` that user can access. It does not provide HTTP authentication, sandboxing, tenant isolation, or a remote-service security boundary. Remote, untrusted, and multi-tenant operation is unsupported.
88
+
89
+ Symbol-index persistence remains disabled by default. `AST_SYMBOL_INDEX_PERSISTENCE=canary` additionally requires an explicit absolute `AST_SYMBOL_INDEX_CACHE_ROOT`; the reserved `enabled` mode fails closed to memory-only with `enabled_not_released`.
90
+
91
+ See [Support policy](docs/support.md) for the complete platform, runtime, persistence, and operational contract. Report security issues through [SECURITY.md](SECURITY.md).
92
+
62
93
  ## Install
63
94
 
64
95
  Install the published CLI globally while keeping dependency lifecycle scripts disabled:
@@ -236,6 +267,7 @@ Project-scoped tools accept `project_root`, either the project directory or an e
236
267
  | `ast_get_symbol_source` | Exact source for one declaration | No |
237
268
  | `ast_search_symbols` | Paginated structural symbol discovery | No |
238
269
  | `ast_find_references` | Compiler-resolved references with bounded context | No |
270
+ | `ast_get_impact` | Bounded incoming/outgoing compiler-backed impact evidence | No |
239
271
  | `ast_get_diagnostics` | Project- or file-scoped TypeScript diagnostics | No |
240
272
  | `ast_rename_symbol` | Prepare a project-wide rename | No |
241
273
  | `ast_replace_symbol_body` | Prepare a body-only replacement while preserving the signature | No |
@@ -251,9 +283,9 @@ Symbol search is relevance-ranked and defaults to at most 20 `summary` records c
251
283
 
252
284
  ### Optional TOON results
253
285
 
254
- `ast_search_symbols`, `ast_find_references`, and `ast_get_diagnostics` accept `output_format: "toon"` for collection-heavy results consumed directly by a model. JSON remains the default and preserves the canonical structured object.
286
+ `ast_search_symbols`, `ast_find_references`, `ast_get_impact`, and `ast_get_diagnostics` accept `output_format: "toon"` for collection-heavy results consumed directly by a model. JSON remains the default and preserves the canonical structured object.
255
287
 
256
- MCP TOON is returned once as structured content shaped like `{ "format": "toon", "data": "..." }`; `data` is the lossless TOON document. The complete JSON result is not duplicated. These three tools validate their canonical Zod result and verify an encode/decode deep-equality round trip before presentation, but do not advertise a single MCP `outputSchema` because their successful structured content has two representations.
288
+ MCP TOON is returned once as structured content shaped like `{ "format": "toon", "data": "..." }`; `data` is the lossless TOON document. The complete JSON result is not duplicated. These four tools validate their canonical Zod result and verify an encode/decode deep-equality round trip before presentation, but do not advertise a single MCP `outputSchema` because their successful structured content has two representations.
257
289
 
258
290
  Do not request TOON for source, outlines, file lists, previews, or mutation results. Checked negative controls show that the MCP envelope makes those shapes larger. TOON is an explicit shape-specific optimization, not a new dialect for every object in sight.
259
291
 
package/SECURITY.md ADDED
@@ -0,0 +1,31 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are provided for the latest published minor release and the current `main` development line. During the v0.7.0 release-candidate cycle, the support window is:
6
+
7
+ | Version | Supported |
8
+ | ---------------------------------- | ---------------------------------- |
9
+ | `main` / v0.7.0 release candidates | Yes |
10
+ | 0.6.x | Yes, until v0.7.0 becomes `latest` |
11
+ | 0.5.x and earlier | No |
12
+
13
+ This project supports trusted single-user local stdio use. The current v0.7.0 target supports Linux x64 only when GNU coreutils `mv` provides `--update=none-fail`, with Node.js 22.5.0 or the current Node.js 24 line. Other Linux architectures, systems without that primitive, macOS and Windows are unverified. Remote, untrusted and multi-tenant use is outside the supported security boundary.
14
+
15
+ ## Reporting a vulnerability
16
+
17
+ Do not open a public issue for a suspected vulnerability. Email `yaildeveloper@gmail.com` with the subject `ast-mcp-server security report` and include:
18
+
19
+ - affected version or commit;
20
+ - impact and required attacker access;
21
+ - minimal reproduction steps;
22
+ - whether source mutation, credentials or local paths may have been exposed;
23
+ - a safe way to coordinate follow-up.
24
+
25
+ Do not include live API keys, tokens, credentials, private source bodies or production connection strings. Replace any necessary secret-shaped evidence with `[REDACTED]` and use a disposable fixture where possible.
26
+
27
+ A maintainer will acknowledge the report within five business days and provide an initial triage result within ten business days. Remediation and coordinated disclosure timing depend on severity, exploitability and release impact; no universal fix deadline is promised.
28
+
29
+ ## Disclosure and scope
30
+
31
+ Please allow time for a fix and package readback before public disclosure. Reports are evaluated against the documented local stdio trust boundary: the server runs with the invoking user's filesystem permissions and is not an OS sandbox. Vulnerabilities that cross the compiler-authority, reviewed mutation, path-containment, credential-redaction, package-provenance or release-integrity boundaries remain in scope.
package/dist/cli.js CHANGED
File without changes
package/dist/index.js CHANGED
@@ -1,13 +1,132 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
6
  import { createServer } from "./server.js";
4
- async function main() {
5
- const server = createServer();
6
- const transport = new StdioServerTransport();
7
- await server.connect(transport);
8
- console.error("ast-mcp-server running over stdio.");
9
- }
10
- main().catch((error) => {
11
- console.error("Fatal ast-mcp-server error:", error);
12
- process.exit(1);
13
- });
7
+ import { beginProjectShutdown, closeDrainedProjectSessions, getProjectRuntimeShutdownSnapshot, prepareProjectRuntimeForStartup, waitForProjectCompletionCriticalOperationsToDrain, waitForProjectOperationsToDrain, } from "./services/project.js";
8
+ import { RuntimeActivityTracker } from "./services/runtime-activity.js";
9
+ import { readRuntimePolicy } from "./services/runtime-policy.js";
10
+ import { emitIncompleteShutdownEvent, ShutdownCoordinator, } from "./services/shutdown.js";
11
+ function emitShutdownFailureEvent() {
12
+ process.stderr.write(`${JSON.stringify({ event: "shutdown_failed", version: 1 })}\n`);
13
+ }
14
+ export async function runStdioServer(options = {}) {
15
+ if (options.server && !options.runtimeActivity) {
16
+ throw new Error("An injected MCP server requires its runtime activity tracker.");
17
+ }
18
+ prepareProjectRuntimeForStartup();
19
+ const runtimeActivity = options.runtimeActivity ?? new RuntimeActivityTracker();
20
+ runtimeActivity.prepareForStartup();
21
+ const server = options.server ?? createServer({ runtimeActivity });
22
+ const transport = options.transport ?? new StdioServerTransport();
23
+ const send = transport.send.bind(transport);
24
+ transport.send = (message, sendOptions) => runtimeActivity.trackSend(() => send(message, sendOptions));
25
+ const runtimePolicy = options.runtimePolicy ?? readRuntimePolicy();
26
+ const installProcessHandlers = options.installProcessHandlers ?? true;
27
+ const exit = options.exit ?? ((code) => process.exit(code));
28
+ const protocol = server.server;
29
+ const previousTransportClose = protocol.onclose;
30
+ let listenersInstalled = false;
31
+ let terminalHandlerInstalled = false;
32
+ const onSigint = () => requestFromRuntime("sigint");
33
+ const onSigterm = () => requestFromRuntime("sigterm");
34
+ const onStdinEnd = () => requestFromRuntime("stdin_eof");
35
+ const onStdinClose = () => requestFromRuntime("stdin_eof");
36
+ const onTransportClose = () => {
37
+ previousTransportClose?.();
38
+ requestFromRuntime("transport_closed");
39
+ };
40
+ const removeLifecycleListeners = () => {
41
+ if (listenersInstalled) {
42
+ process.off("SIGINT", onSigint);
43
+ process.off("SIGTERM", onSigterm);
44
+ process.stdin.off("end", onStdinEnd);
45
+ process.stdin.off("close", onStdinClose);
46
+ listenersInstalled = false;
47
+ }
48
+ if (protocol.onclose === onTransportClose)
49
+ protocol.onclose = previousTransportClose;
50
+ };
51
+ const coordinator = new ShutdownCoordinator({
52
+ drainTimeoutMs: runtimePolicy.shutdownDrainTimeoutMs,
53
+ stopAdmissionAndCancel: () => {
54
+ runtimeActivity.beginShutdown();
55
+ return beginProjectShutdown();
56
+ },
57
+ getDrainSnapshot: getProjectRuntimeShutdownSnapshot,
58
+ waitForDrain: async () => {
59
+ await Promise.all([waitForProjectOperationsToDrain(), runtimeActivity.waitForIdle()]);
60
+ },
61
+ waitForCompletionCriticalOperationsToDrain: waitForProjectCompletionCriticalOperationsToDrain,
62
+ closeMcp: async () => {
63
+ await server.close();
64
+ },
65
+ closeProjects: closeDrainedProjectSessions,
66
+ emitIncompleteShutdown: emitIncompleteShutdownEvent,
67
+ onCompletionCriticalWait: options.onCompletionCriticalWait,
68
+ });
69
+ let lifecycleCompletion;
70
+ function requestShutdown(trigger) {
71
+ lifecycleCompletion ??= coordinator.shutdown(trigger).finally(removeLifecycleListeners);
72
+ return lifecycleCompletion;
73
+ }
74
+ function requestFromRuntime(trigger) {
75
+ try {
76
+ options.onRuntimeTrigger?.(trigger);
77
+ }
78
+ catch {
79
+ // Runtime observation is non-authoritative and must never interrupt shutdown.
80
+ }
81
+ const completion = requestShutdown(trigger);
82
+ if (terminalHandlerInstalled)
83
+ return;
84
+ terminalHandlerInstalled = true;
85
+ void completion
86
+ .then((result) => {
87
+ if (result.state === "forced_noncritical")
88
+ exit(1);
89
+ })
90
+ .catch(() => {
91
+ emitShutdownFailureEvent();
92
+ exit(1);
93
+ });
94
+ }
95
+ protocol.onclose = onTransportClose;
96
+ if (installProcessHandlers) {
97
+ process.on("SIGINT", onSigint);
98
+ process.on("SIGTERM", onSigterm);
99
+ process.stdin.once("end", onStdinEnd);
100
+ process.stdin.once("close", onStdinClose);
101
+ listenersInstalled = true;
102
+ }
103
+ try {
104
+ await server.connect(transport);
105
+ }
106
+ catch (error) {
107
+ await requestShutdown("requested").catch(() => undefined);
108
+ throw error;
109
+ }
110
+ if (options.logStartup ?? true)
111
+ process.stderr.write("ast-mcp-server running over stdio.\n");
112
+ return Object.freeze({
113
+ shutdown: (trigger = "requested") => requestShutdown(trigger),
114
+ });
115
+ }
116
+ function isMainModule() {
117
+ const entry = process.argv[1];
118
+ if (!entry)
119
+ return false;
120
+ try {
121
+ return fs.realpathSync(entry) === fs.realpathSync(fileURLToPath(import.meta.url));
122
+ }
123
+ catch {
124
+ return path.resolve(entry) === path.resolve(fileURLToPath(import.meta.url));
125
+ }
126
+ }
127
+ if (isMainModule()) {
128
+ runStdioServer().catch(() => {
129
+ process.stderr.write("Fatal ast-mcp-server startup error.\n");
130
+ process.exit(1);
131
+ });
132
+ }
package/dist/server.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { createRequire } from "node:module";
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { McpServer, } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { PublicOperationalError } from "./services/public-errors.js";
3
4
  import { registerApplyOperation } from "./tools/apply_operation.js";
4
5
  import { registerFindReferences } from "./tools/find_references.js";
5
6
  import { registerGetDiagnostics } from "./tools/get_diagnostics.js";
6
7
  import { registerGetFile } from "./tools/get_file.js";
8
+ import { registerGetImpact } from "./tools/get_impact.js";
7
9
  import { registerGetOperationPreview } from "./tools/get_operation_preview.js";
8
10
  import { registerGetOutline } from "./tools/get_outline.js";
9
11
  import { registerGetProjectStatus } from "./tools/get_project_status.js";
@@ -14,13 +16,29 @@ import { registerReplaceSymbolBody } from "./tools/replace_symbol_body.js";
14
16
  import { registerScaffoldClass } from "./tools/scaffold_class.js";
15
17
  import { registerSearchSymbols } from "./tools/search_symbols.js";
16
18
  import { registerExplore } from "./tools/explore.js";
19
+ import { createToolErrorContext, errorResult } from "./tools/result.js";
17
20
  const packageMetadata = createRequire(import.meta.url)("../package.json");
18
21
  export const PACKAGE_VERSION = packageMetadata.version;
19
- export function createServer() {
22
+ function installRuntimeActivityTracking(server, runtimeActivity) {
23
+ const registerTool = server.registerTool.bind(server);
24
+ function trackedRegisterTool(name, config, callback) {
25
+ const trackedCallback = (async (...args) => runtimeActivity.trackRequest(() => {
26
+ if (runtimeActivity.admission === "closed") {
27
+ return errorResult(new PublicOperationalError("SERVER_SHUTTING_DOWN", "Server is shutting down."), createToolErrorContext(name));
28
+ }
29
+ return Reflect.apply(callback, undefined, args);
30
+ }));
31
+ return registerTool(name, config, trackedCallback);
32
+ }
33
+ server.registerTool = trackedRegisterTool;
34
+ }
35
+ export function createServer(options = {}) {
20
36
  const server = new McpServer({
21
37
  name: "ast-mcp-server",
22
38
  version: PACKAGE_VERSION,
23
39
  });
40
+ if (options.runtimeActivity)
41
+ installRuntimeActivityTracking(server, options.runtimeActivity);
24
42
  registerListFiles(server);
25
43
  registerGetProjectStatus(server);
26
44
  registerExplore(server);
@@ -28,6 +46,7 @@ export function createServer() {
28
46
  registerGetSymbolSource(server);
29
47
  registerSearchSymbols(server);
30
48
  registerFindReferences(server);
49
+ registerGetImpact(server);
31
50
  registerGetDiagnostics(server);
32
51
  registerGetFile(server);
33
52
  registerRenameSymbol(server);
@@ -4,27 +4,9 @@ import { access, realpath, stat } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { installBundledSkill, SkillConflictError, } from "./skill-installer.js";
7
- import { AGENT_IDS } from "./setup-wizard.js";
8
- const MCP_SERVER_NAME = "ast";
7
+ import { AGENT_IDS, getAgentTarget, MCP_SERVER_NAME, } from "./agent-targets.js";
9
8
  const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
10
9
  const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024;
11
- const EXPECTED_HERMES_TOOLS = [
12
- "ast_list_files",
13
- "ast_get_outline",
14
- "ast_get_symbol_source",
15
- "ast_search_symbols",
16
- "ast_find_references",
17
- "ast_get_diagnostics",
18
- "ast_rename_symbol",
19
- "ast_replace_symbol_body",
20
- "ast_scaffold_class",
21
- "ast_get_operation_preview",
22
- "ast_apply_operation",
23
- ];
24
- const AGENT_DEFINITIONS = {
25
- claude: { label: "Claude Code", command: "claude" },
26
- hermes: { label: "Hermes", command: "hermes" },
27
- };
28
10
  export class AgentSetupError extends Error {
29
11
  code;
30
12
  details;
@@ -128,102 +110,44 @@ async function runCommand(executable, args, options) {
128
110
  }
129
111
  function commandFailure(agent, operation, result) {
130
112
  const output = `${result.stdout}\n${result.stderr}`.trim().slice(0, 4000);
131
- return new AgentSetupError(`${AGENT_DEFINITIONS[agent].label} ${operation} failed with exit code ${result.exitCode}.`, "AGENT_COMMAND_FAILED", { agent, operation, output });
113
+ return new AgentSetupError(`${getAgentTarget(agent).label} ${operation} failed with exit code ${result.exitCode}.`, "AGENT_COMMAND_FAILED", { agent, operation, output });
132
114
  }
133
- function isMissingClaudeMcp(output) {
134
- return (/\bno\s+mcp\s+server\b/i.test(output) &&
135
- /(?:\bfound\b|\bnamed?\b)/i.test(output) &&
136
- /\bast\b/i.test(output));
137
- }
138
- async function inspectClaudeMcp(detection, serverEntryPath, nodeExecutable, environment, timeoutMs) {
139
- const result = await runCommand(detection.executable, ["mcp", "get", MCP_SERVER_NAME], {
140
- environment,
141
- timeoutMs,
142
- });
143
- const output = `${result.stdout}\n${result.stderr}`;
144
- if (result.exitCode !== 0) {
145
- if (isMissingClaudeMcp(output)) {
146
- return { agent: "claude", status: "missing" };
147
- }
148
- throw commandFailure("claude", "MCP inspection", result);
149
- }
150
- const fields = new Map();
151
- for (const line of result.stdout.split(/\r?\n/)) {
152
- const match = line.match(/^\s*(Status|Type|Command|Args):\s*(.*)$/i);
153
- if (match !== null) {
154
- fields.set(match[1].toLowerCase(), match[2].trim());
155
- }
156
- }
157
- const current = /connected/i.test(fields.get("status") ?? "") &&
158
- fields.get("type")?.toLowerCase() === "stdio" &&
159
- fields.get("command") === nodeExecutable &&
160
- fields.get("args") === serverEntryPath;
161
- return current
162
- ? { agent: "claude", status: "current" }
163
- : {
164
- agent: "claude",
165
- status: "conflict",
166
- detail: "The existing user-scoped 'ast' registration does not match this package.",
167
- };
168
- }
169
- async function inspectHermesMcp(detection, environment, timeoutMs) {
170
- const listed = await runCommand(detection.executable, ["mcp", "list"], {
171
- environment,
172
- timeoutMs,
173
- });
174
- if (listed.exitCode !== 0) {
175
- throw commandFailure("hermes", "MCP list", listed);
176
- }
177
- if (!/^\s*ast(?:\s|$)/im.test(listed.stdout)) {
178
- return { agent: "hermes", status: "missing" };
179
- }
180
- const tested = await runCommand(detection.executable, ["mcp", "test", MCP_SERVER_NAME], {
181
- environment,
182
- timeoutMs,
183
- });
184
- const current = tested.exitCode === 0 && EXPECTED_HERMES_TOOLS.every((tool) => tested.stdout.includes(tool));
185
- return current
186
- ? { agent: "hermes", status: "current" }
187
- : {
188
- agent: "hermes",
189
- status: "conflict",
190
- detail: "The existing 'ast' registration does not expose the expected structural tools.",
191
- };
115
+ function targetRuntime(detection, environment, timeoutMs) {
116
+ return {
117
+ run: (args, input) => runCommand(detection.executable, [...args], {
118
+ environment,
119
+ timeoutMs,
120
+ input,
121
+ }),
122
+ };
192
123
  }
193
124
  async function inspectMcp(agent, detection, serverEntryPath, nodeExecutable, environment, timeoutMs) {
194
- return agent === "claude"
195
- ? inspectClaudeMcp(detection, serverEntryPath, nodeExecutable, environment, timeoutMs)
196
- : inspectHermesMcp(detection, environment, timeoutMs);
197
- }
198
- async function configureClaudeMcp(detection, serverEntryPath, nodeExecutable, environment, timeoutMs) {
199
- const added = await runCommand(detection.executable, [
200
- "mcp",
201
- "add",
202
- "--scope",
203
- "user",
204
- "--transport",
205
- "stdio",
206
- MCP_SERVER_NAME,
207
- "--",
208
- nodeExecutable,
125
+ const target = getAgentTarget(agent);
126
+ const inspection = await target.mcp.inspect(targetRuntime(detection, environment, timeoutMs), {
209
127
  serverEntryPath,
210
- ], { environment, timeoutMs });
211
- if (added.exitCode !== 0) {
212
- throw commandFailure("claude", "MCP registration", added);
213
- }
214
- const verified = await inspectClaudeMcp(detection, serverEntryPath, nodeExecutable, environment, timeoutMs);
215
- if (verified.status !== "current") {
216
- throw new AgentSetupError("Claude Code saved the MCP registration but verification did not match.", "AGENT_VERIFICATION_FAILED", { agent: "claude", inspection: verified });
128
+ nodeExecutable,
129
+ });
130
+ if (inspection.status === "error") {
131
+ throw commandFailure(agent, inspection.operation, inspection.result);
217
132
  }
133
+ return {
134
+ agent,
135
+ status: inspection.status,
136
+ ...(inspection.detail ? { detail: inspection.detail } : {}),
137
+ };
218
138
  }
219
- async function configureHermesMcp(detection, serverEntryPath, nodeExecutable, environment, timeoutMs) {
220
- const added = await runCommand(detection.executable, ["mcp", "add", MCP_SERVER_NAME, "--command", nodeExecutable, "--args", serverEntryPath], { environment, timeoutMs, input: "\n" });
221
- if (added.exitCode !== 0 || !/saved\s+'ast'/i.test(added.stdout)) {
222
- throw commandFailure("hermes", "MCP registration", added);
139
+ async function configureMcp(agent, detection, serverEntryPath, nodeExecutable, environment, timeoutMs) {
140
+ const target = getAgentTarget(agent);
141
+ const added = await target.mcp.register(targetRuntime(detection, environment, timeoutMs), {
142
+ serverEntryPath,
143
+ nodeExecutable,
144
+ });
145
+ if (!target.mcp.registrationAccepted(added)) {
146
+ throw commandFailure(agent, "MCP registration", added);
223
147
  }
224
- const verified = await inspectHermesMcp(detection, environment, timeoutMs);
148
+ const verified = await inspectMcp(agent, detection, serverEntryPath, nodeExecutable, environment, timeoutMs);
225
149
  if (verified.status !== "current") {
226
- throw new AgentSetupError("Hermes saved the MCP registration but verification did not expose the expected tools.", "AGENT_VERIFICATION_FAILED", { agent: "hermes", inspection: verified });
150
+ throw new AgentSetupError(`${target.label} saved the MCP registration but verification did not match.`, "AGENT_VERIFICATION_FAILED", { agent, inspection: verified });
227
151
  }
228
152
  }
229
153
  async function assertRegularFile(filePath, label) {
@@ -248,7 +172,7 @@ export async function detectInstalledAgents(options = {}) {
248
172
  const environment = options.environment ?? process.env;
249
173
  const timeoutMs = options.commandTimeoutMs ?? 5000;
250
174
  return Promise.all(AGENT_IDS.map(async (id) => {
251
- const definition = AGENT_DEFINITIONS[id];
175
+ const definition = getAgentTarget(id);
252
176
  const executable = await findExecutable(definition.command, environment);
253
177
  if (executable === undefined) {
254
178
  return { id, label: definition.label, installed: false };
@@ -282,7 +206,7 @@ export async function runAgentSetup(options) {
282
206
  for (const agent of requested) {
283
207
  const detection = detectionMap.get(agent);
284
208
  if (!detection?.installed || detection.executable === undefined) {
285
- throw new AgentSetupError(`${AGENT_DEFINITIONS[agent].label} is not installed or is not available in PATH.`, "AGENT_NOT_INSTALLED", { agent });
209
+ throw new AgentSetupError(`${getAgentTarget(agent).label} is not installed or is not available in PATH.`, "AGENT_NOT_INSTALLED", { agent });
286
210
  }
287
211
  }
288
212
  await assertRegularFile(options.serverEntryPath, "MCP server entrypoint");
@@ -290,9 +214,9 @@ export async function runAgentSetup(options) {
290
214
  const inspections = await Promise.all(requested.map((agent) => inspectMcp(agent, detectionMap.get(agent), options.serverEntryPath, nodeExecutable, environment, timeoutMs)));
291
215
  const conflict = inspections.find((item) => item.status === "conflict");
292
216
  if (conflict !== undefined) {
293
- throw new AgentSetupError(`Conflicting ${AGENT_DEFINITIONS[conflict.agent].label} MCP registration: ${conflict.detail}`, "AGENT_MCP_CONFLICT", { agent: conflict.agent });
217
+ throw new AgentSetupError(`Conflicting ${getAgentTarget(conflict.agent).label} MCP registration: ${conflict.detail}`, "AGENT_MCP_CONFLICT", { agent: conflict.agent });
294
218
  }
295
- const skillTarget = requested.length === 2 ? "all" : requested[0];
219
+ const skillTarget = requested.length === AGENT_IDS.length ? "all" : getAgentTarget(requested[0]).skillTarget;
296
220
  const skillResult = await installBundledSkill({
297
221
  target: skillTarget,
298
222
  scope: "user",
@@ -312,12 +236,7 @@ export async function runAgentSetup(options) {
312
236
  const inspection = inspections.find((item) => item.agent === agent);
313
237
  try {
314
238
  if (inspection.status === "missing") {
315
- if (agent === "claude") {
316
- await configureClaudeMcp(detection, options.serverEntryPath, nodeExecutable, environment, timeoutMs);
317
- }
318
- else {
319
- await configureHermesMcp(detection, options.serverEntryPath, nodeExecutable, environment, timeoutMs);
320
- }
239
+ await configureMcp(agent, detection, options.serverEntryPath, nodeExecutable, environment, timeoutMs);
321
240
  }
322
241
  completed.push({
323
242
  agent,