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
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { getOperationPreview } from "../services/operations.js";
3
- import { errorResult, structuredResult } from "./result.js";
3
+ import { createRequestContext } from "../services/request-context.js";
4
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
4
5
  const AstGetOperationPreviewInputSchema = z.object({
5
6
  operation_id: z.string().uuid(),
6
7
  file: z.string().optional().describe("Optional affected file path to retrieve only its diff."),
@@ -22,12 +23,14 @@ export function registerGetOperationPreview(server) {
22
23
  idempotentHint: true,
23
24
  openWorldHint: false,
24
25
  },
25
- }, async ({ operation_id, file }) => {
26
+ }, async ({ operation_id, file }, extra) => {
27
+ const requestContext = createRequestContext(extra.signal);
26
28
  try {
27
- return structuredResult(getOperationPreview(operation_id, file));
29
+ const output = await getOperationPreview(operation_id, file, requestContext);
30
+ return structuredResult(output);
28
31
  }
29
32
  catch (error) {
30
- return errorResult(error);
33
+ return errorResult(error, createToolErrorContext("ast_get_operation_preview"));
31
34
  }
32
35
  });
33
36
  }
@@ -2,7 +2,8 @@ import path from "node:path";
2
2
  import { z } from "zod";
3
3
  import { buildFileOutline } from "../services/outline.js";
4
4
  import { getSourceFileOrThrow, withProject } from "../services/project.js";
5
- import { errorResult, structuredResult } from "./result.js";
5
+ import { createRequestContext } from "../services/request-context.js";
6
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
6
7
  const AstGetOutlineInputSchema = z.object({
7
8
  project_root: z
8
9
  .string()
@@ -38,21 +39,22 @@ export function registerGetOutline(server) {
38
39
  idempotentHint: true,
39
40
  openWorldHint: false,
40
41
  },
41
- }, async ({ project_root, file_path, include_symbols }) => {
42
+ }, async ({ project_root, file_path, include_symbols }, extra) => {
43
+ const requestContext = createRequestContext(extra.signal);
42
44
  try {
43
- const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
45
+ const structuredContent = await withProject(project_root, ({ project, projectRoot }, operationContext) => {
44
46
  const sourceFile = getSourceFileOrThrow(project, file_path);
45
- const outline = buildFileOutline(sourceFile);
47
+ const outline = buildFileOutline(sourceFile, operationContext);
46
48
  return {
47
49
  file: path.relative(projectRoot, sourceFile.getFilePath()),
48
50
  outline: outline.text,
49
51
  ...(include_symbols ? { symbols: outline.symbols } : {}),
50
52
  };
51
- });
53
+ }, requestContext);
52
54
  return structuredResult(structuredContent);
53
55
  }
54
56
  catch (error) {
55
- return errorResult(error);
57
+ return errorResult(error, createToolErrorContext("ast_get_outline", project_root));
56
58
  }
57
59
  });
58
60
  }
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
3
3
  import { getProjectStatus } from "../services/project.js";
4
- import { errorResult, structuredResult } from "./result.js";
4
+ import { createRequestContext } from "../services/request-context.js";
5
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
5
6
  const AstGetProjectStatusInputSchema = z.object({
6
7
  project_root: z
7
8
  .string()
@@ -11,6 +12,54 @@ const TruncationMetadataSchema = z.object({
11
12
  truncated: z.boolean(),
12
13
  reason: z.enum(TRUNCATION_REASONS).nullable(),
13
14
  });
15
+ const IndexObservabilitySchema = z.object({
16
+ policy: z.enum(["disabled", "canary", "enabled"]),
17
+ policy_reason: z.enum([
18
+ "default",
19
+ "invalid_mode",
20
+ "enabled_not_released",
21
+ "cache_root_missing",
22
+ "cache_root_invalid",
23
+ ]),
24
+ backend: z.enum(["memory", "sqlite"]),
25
+ state: z.enum(["disabled", "ready", "rebuilding", "failed"]),
26
+ operation: z.enum([
27
+ "disabled",
28
+ "hit",
29
+ "miss",
30
+ "rebuild",
31
+ "fallback",
32
+ "migration",
33
+ "corruption",
34
+ "read_failure",
35
+ "write_failure",
36
+ ]),
37
+ last_operation: z.enum([
38
+ "disabled",
39
+ "hit",
40
+ "miss",
41
+ "rebuild",
42
+ "fallback",
43
+ "migration",
44
+ "corruption",
45
+ "read_failure",
46
+ "write_failure",
47
+ ]),
48
+ loaded_entries: z.number().int().min(0),
49
+ accepted_entries: z.number().int().min(0),
50
+ rejected_entries: z.number().int().min(0),
51
+ cache_hits: z.number().int().min(0),
52
+ cache_misses: z.number().int().min(0),
53
+ fallback_count: z.number().int().min(0),
54
+ migration_count: z.number().int().min(0),
55
+ corruption_count: z.number().int().min(0),
56
+ write_failure_count: z.number().int().min(0),
57
+ rebuilt_files: z.number().int().min(0),
58
+ reused_files: z.number().int().min(0),
59
+ removed_files: z.number().int().min(0),
60
+ last_error: z.string().nullable(),
61
+ last_successful_persistence_at: z.string().nullable(),
62
+ });
14
63
  const ProjectStatusOutputSchema = z.object({
15
64
  project: z.object({
16
65
  project_id: z.string().regex(/^project_[0-9a-f]{20}$/),
@@ -28,21 +77,40 @@ const ProjectStatusOutputSchema = z.object({
28
77
  pending_files_truncation: TruncationMetadataSchema,
29
78
  pending_files_filename_truncation: TruncationMetadataSchema,
30
79
  compiler: z.object({ state: z.enum(["ready", "rebuilding", "failed"]) }),
31
- index: z.object({ state: z.literal("disabled") }),
80
+ index: z.object({ state: z.enum(["disabled", "ready", "rebuilding", "failed"]) }),
32
81
  operation_queue: z.object({
33
82
  state: z.enum(["idle", "queued", "running"]),
34
- active_operations: z.number().int().min(0),
35
- queued_operations: z.number().int().min(0),
83
+ admission: z.enum(["open", "closed"]),
84
+ queue_capacity: z.number().int().min(1).max(256),
85
+ active_operations: z.number().int().min(0).max(1),
86
+ queued_operations: z.number().int().min(0).max(256),
87
+ rejected_operations: z.number().int().min(0).max(2_147_483_647),
88
+ cancelled_operations: z.number().int().min(0).max(2_147_483_647),
89
+ queue_timeout_operations: z.number().int().min(0).max(2_147_483_647),
90
+ deadline_exceeded_operations: z.number().int().min(0).max(2_147_483_647),
91
+ last_outcome: z.enum([
92
+ "none",
93
+ "succeeded",
94
+ "failed",
95
+ "rejected",
96
+ "cancelled",
97
+ "queue_timeout",
98
+ "deadline_exceeded",
99
+ "internal_error",
100
+ ]),
101
+ max_queue_wait_ms: z.number().int().min(0).max(86_400_000),
102
+ max_execution_ms: z.number().int().min(0).max(86_400_000),
36
103
  }),
37
104
  watcher: z.object({ state: z.enum(["disabled", "ready", "failed"]) }),
38
105
  last_successful_sync_at: z.string().nullable(),
39
- last_successful_index_at: z.null(),
106
+ last_successful_index_at: z.string().nullable(),
40
107
  source_snapshot_fingerprint: z.string().nullable(),
41
108
  config_snapshot_fingerprint: z.string().nullable(),
42
109
  canonical_snapshot_fingerprint: z.string().nullable(),
43
110
  degraded_errors: z.array(z.string()),
44
111
  degraded_errors_truncation: TruncationMetadataSchema,
45
112
  degraded_errors_text_truncation: TruncationMetadataSchema,
113
+ index_observability: IndexObservabilitySchema,
46
114
  });
47
115
  export function registerGetProjectStatus(server) {
48
116
  server.registerTool("ast_get_project_status", {
@@ -56,13 +124,14 @@ export function registerGetProjectStatus(server) {
56
124
  idempotentHint: true,
57
125
  openWorldHint: false,
58
126
  },
59
- }, async ({ project_root }) => {
127
+ }, async ({ project_root }, extra) => {
128
+ const requestContext = createRequestContext(extra.signal);
60
129
  try {
61
- const output = await getProjectStatus(project_root);
130
+ const output = await getProjectStatus(project_root, requestContext);
62
131
  return structuredResult({ ...output });
63
132
  }
64
133
  catch (error) {
65
- return errorResult(error);
134
+ return errorResult(error, createToolErrorContext("ast_get_project_status", project_root));
66
135
  }
67
136
  });
68
137
  }
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { nodeSourceWithLocation } from "../services/outline.js";
3
3
  import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
4
- import { errorResult, structuredResult } from "./result.js";
4
+ import { createRequestContext } from "../services/request-context.js";
5
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
5
6
  const AstGetSymbolSourceInputSchema = z.object({
6
7
  project_root: z
7
8
  .string()
@@ -27,17 +28,18 @@ export function registerGetSymbolSource(server) {
27
28
  idempotentHint: true,
28
29
  openWorldHint: false,
29
30
  },
30
- }, async ({ project_root, file_path, symbol_path }) => {
31
+ }, async ({ project_root, file_path, symbol_path }, extra) => {
32
+ const requestContext = createRequestContext(extra.signal);
31
33
  try {
32
- const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
34
+ const structuredContent = await withProject(project_root, ({ project, projectRoot }, operationContext) => {
33
35
  const sourceFile = getSourceFileOrThrow(project, file_path);
34
36
  const node = findDeclarationByName(sourceFile, symbol_path);
35
- return nodeSourceWithLocation(node, projectRoot);
36
- });
37
+ return nodeSourceWithLocation(node, projectRoot, operationContext);
38
+ }, requestContext);
37
39
  return structuredResult(structuredContent);
38
40
  }
39
41
  catch (error) {
40
- return errorResult(error);
42
+ return errorResult(error, createToolErrorContext("ast_get_symbol_source", project_root));
41
43
  }
42
44
  });
43
45
  }
@@ -2,7 +2,8 @@ import path from "node:path";
2
2
  import { z } from "zod";
3
3
  import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
4
4
  import { withProject } from "../services/project.js";
5
- import { errorResult, structuredResult } from "./result.js";
5
+ import { createRequestContext } from "../services/request-context.js";
6
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
6
7
  const AstListFilesInputSchema = z.object({
7
8
  project_root: z
8
9
  .string()
@@ -29,23 +30,27 @@ export function registerListFiles(server) {
29
30
  idempotentHint: true,
30
31
  openWorldHint: false,
31
32
  },
32
- }, async ({ project_root, glob_filter, offset, limit }) => {
33
+ }, async ({ project_root, glob_filter, offset, limit }, extra) => {
34
+ const requestContext = createRequestContext(extra.signal);
33
35
  try {
34
- const output = await withProject(project_root, ({ project, projectRoot }) => {
36
+ const output = await withProject(project_root, ({ project, projectRoot }, operationContext) => {
35
37
  const filter = glob_filter?.toLowerCase();
36
38
  const files = project
37
39
  .getSourceFiles()
38
- .map((sourceFile) => path.relative(projectRoot, sourceFile.getFilePath()))
40
+ .map((sourceFile) => {
41
+ operationContext.checkpoint();
42
+ return path.relative(projectRoot, sourceFile.getFilePath());
43
+ })
39
44
  .filter((file) => !filter || file.toLowerCase().includes(filter))
40
45
  .sort((left, right) => left.localeCompare(right));
41
46
  const page = paginate(files, offset, limit);
42
47
  const { items, ...metadata } = page;
43
48
  return { files: items, ...metadata };
44
- });
49
+ }, requestContext);
45
50
  return structuredResult(output);
46
51
  }
47
52
  catch (error) {
48
- return errorResult(error);
53
+ return errorResult(error, createToolErrorContext("ast_list_files", project_root));
49
54
  }
50
55
  });
51
56
  }
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { prepareRename } from "../services/operations.js";
3
+ import { createRequestContext } from "../services/request-context.js";
3
4
  import { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
4
- import { errorResult, structuredResult } from "./result.js";
5
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
5
6
  const AstRenameSymbolInputSchema = z.object({
6
7
  project_root: z.string(),
7
8
  file_path: z.string(),
@@ -28,7 +29,8 @@ export function registerRenameSymbol(server) {
28
29
  idempotentHint: false,
29
30
  openWorldHint: false,
30
31
  },
31
- }, async ({ project_root, file_path, symbol_path, new_name, dry_run, allow_new_errors }) => {
32
+ }, async ({ project_root, file_path, symbol_path, new_name, dry_run, allow_new_errors }, extra) => {
33
+ const requestContext = createRequestContext(extra.signal);
32
34
  try {
33
35
  if (!dry_run) {
34
36
  throw new Error("Direct rename application is disabled. Prepare with dry_run=true, then call ast_apply_operation with the returned operation_id.");
@@ -39,11 +41,11 @@ export function registerRenameSymbol(server) {
39
41
  symbolPath: symbol_path,
40
42
  newName: new_name,
41
43
  allowNewErrors: allow_new_errors,
42
- });
44
+ }, requestContext);
43
45
  return structuredResult(serializePreparedOperation(operation));
44
46
  }
45
47
  catch (error) {
46
- return errorResult(error);
48
+ return errorResult(error, createToolErrorContext("ast_rename_symbol", project_root));
47
49
  }
48
50
  });
49
51
  }
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { prepareReplaceBody } from "../services/operations.js";
3
+ import { createRequestContext } from "../services/request-context.js";
3
4
  import { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
4
- import { errorResult, structuredResult } from "./result.js";
5
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
5
6
  const AstReplaceSymbolBodyInputSchema = z.object({
6
7
  project_root: z.string(),
7
8
  file_path: z.string(),
@@ -28,7 +29,8 @@ export function registerReplaceSymbolBody(server) {
28
29
  idempotentHint: false,
29
30
  openWorldHint: false,
30
31
  },
31
- }, async ({ project_root, file_path, symbol_path, new_body, dry_run, allow_new_errors }) => {
32
+ }, async ({ project_root, file_path, symbol_path, new_body, dry_run, allow_new_errors }, extra) => {
33
+ const requestContext = createRequestContext(extra.signal);
32
34
  try {
33
35
  if (!dry_run) {
34
36
  throw new Error("Direct body replacement is disabled. Prepare with dry_run=true, then call ast_apply_operation with the returned operation_id.");
@@ -39,11 +41,11 @@ export function registerReplaceSymbolBody(server) {
39
41
  symbolPath: symbol_path,
40
42
  newBody: new_body,
41
43
  allowNewErrors: allow_new_errors,
42
- });
44
+ }, requestContext);
43
45
  return structuredResult(serializePreparedOperation(operation));
44
46
  }
45
47
  catch (error) {
46
- return errorResult(error);
48
+ return errorResult(error, createToolErrorContext("ast_replace_symbol_body", project_root));
47
49
  }
48
50
  });
49
51
  }
@@ -2,7 +2,9 @@ import { Buffer } from "node:buffer";
2
2
  import { isDeepStrictEqual } from "node:util";
3
3
  import { decode, encode } from "@toon-format/toon";
4
4
  import { z } from "zod";
5
- export const MAX_TOOL_ERROR_BYTES = 64 * 1024;
5
+ import { createProjectIdentity } from "../services/project-status.js";
6
+ import { renderPublicError } from "../services/public-errors.js";
7
+ import { emitToolFailureEvent } from "../services/runtime-logger.js";
6
8
  export const MAX_TOON_RESULT_BYTES = 10 * 1024 * 1024;
7
9
  export const ToolOutputFormatSchema = z.enum(["json", "toon"]).default("json");
8
10
  export const ToolOutputFormatInputSchema = {
@@ -12,6 +14,16 @@ export const ToonResultSchema = z.object({
12
14
  format: z.literal("toon"),
13
15
  data: z.string(),
14
16
  });
17
+ export function createToolErrorContext(toolName, projectRoot) {
18
+ if (typeof projectRoot !== "string")
19
+ return { toolName };
20
+ try {
21
+ return { toolName, projectIdentity: createProjectIdentity({ projectRoot }) };
22
+ }
23
+ catch {
24
+ return { toolName };
25
+ }
26
+ }
15
27
  export function structuredResult(structuredContent) {
16
28
  return { content: [], structuredContent };
17
29
  }
@@ -30,27 +42,25 @@ export function formattedResult(outputSchema, candidate, outputFormat, maxToonBy
30
42
  }
31
43
  return structuredResult({ format: "toon", data: text });
32
44
  }
33
- function truncateUtf8(value, maxBytes) {
34
- if (Buffer.byteLength(value, "utf8") <= maxBytes)
35
- return value;
36
- const suffix = "... [truncated]";
37
- const source = Buffer.from(value, "utf8");
38
- let end = Math.max(0, maxBytes - Buffer.byteLength(suffix, "utf8"));
39
- while (end > 0 && (source[end] & 0xc0) === 0x80)
40
- end -= 1;
41
- let prefix = source.subarray(0, end).toString("utf8");
42
- while (Buffer.byteLength(prefix + suffix, "utf8") > maxBytes) {
43
- prefix = prefix.slice(0, -1);
45
+ export function errorResult(error, context) {
46
+ const { envelope, text } = renderPublicError(error);
47
+ try {
48
+ emitToolFailureEvent({
49
+ correlationId: envelope.error.correlation_id,
50
+ toolName: context.toolName,
51
+ code: envelope.error.code,
52
+ message: envelope.error.message,
53
+ projectIdentity: context.projectIdentity,
54
+ });
55
+ }
56
+ catch {
57
+ // A logging sink failure must not replace the bounded MCP error.
44
58
  }
45
- return prefix + suffix;
46
- }
47
- export function errorResult(error) {
48
- const message = error instanceof Error ? error.message : String(error);
49
59
  return {
50
60
  content: [
51
61
  {
52
62
  type: "text",
53
- text: truncateUtf8(message, MAX_TOOL_ERROR_BYTES),
63
+ text,
54
64
  },
55
65
  ],
56
66
  isError: true,
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { prepareScaffoldClass } from "../services/operations.js";
3
- import { errorResult, structuredResult } from "./result.js";
3
+ import { createRequestContext } from "../services/request-context.js";
4
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
4
5
  import { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
5
6
  const IdentifierSchema = z.string().min(1).max(200);
6
7
  const TypeTextSchema = z.string().min(1).max(10_000);
@@ -121,14 +122,15 @@ export function registerScaffoldClass(server) {
121
122
  idempotentHint: false,
122
123
  openWorldHint: false,
123
124
  },
124
- }, async (args) => {
125
+ }, async (args, extra) => {
126
+ const requestContext = createRequestContext(extra.signal);
125
127
  try {
126
128
  const prepared = await prepareScaffoldClass({
127
129
  projectRoot: args.project_root,
128
130
  filePath: args.file_path,
129
131
  spec: toSpec(args),
130
132
  allowNewErrors: args.allow_new_errors,
131
- });
133
+ }, requestContext);
132
134
  const structuredContent = {
133
135
  ...serializePreparedOperation(prepared.operation),
134
136
  file: prepared.file,
@@ -139,7 +141,7 @@ export function registerScaffoldClass(server) {
139
141
  return structuredResult(structuredContent);
140
142
  }
141
143
  catch (error) {
142
- return errorResult(error);
144
+ return errorResult(error, createToolErrorContext("ast_scaffold_class", args.project_root));
143
145
  }
144
146
  });
145
147
  }
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod";
2
- import { createPaginationInputSchema, PaginationOutputSchema, paginate, } from "../services/pagination.js";
3
- import { withProject } from "../services/project.js";
4
- import { searchProjectSymbols } from "../services/symbols.js";
5
- import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
2
+ import { createPaginationInputSchema, PaginationOutputSchema } from "../services/pagination.js";
3
+ import { withProject, reportSymbolIndexFailure } from "../services/project.js";
4
+ import { createRequestContext } from "../services/request-context.js";
5
+ import { searchProjectSymbolsPage, searchProjectSymbolsPageWithIndex, } from "../services/symbols.js";
6
+ import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
6
7
  const SEARCH_DEFAULT_LIMIT = 20;
7
8
  const SearchDetailSchema = z.enum(["selectors", "summary", "full"]).default("summary");
8
9
  const AstSearchSymbolsInputSchema = z.object({
@@ -86,28 +87,28 @@ export function registerSearchSymbols(server) {
86
87
  idempotentHint: true,
87
88
  openWorldHint: false,
88
89
  },
89
- }, async ({ project_root, query, kinds, file_filter, detail, output_format, offset, limit }) => {
90
+ }, async ({ project_root, query, kinds, file_filter, detail, output_format, offset, limit }, extra) => {
91
+ const requestContext = createRequestContext(extra.signal);
90
92
  try {
91
- const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
93
+ const structuredContent = await withProject(project_root, async (context, operationContext) => {
94
+ const { project, projectRoot } = context;
92
95
  const startedAt = performance.now();
93
- const matches = searchProjectSymbols(project, projectRoot, {
94
- query,
95
- kinds,
96
- fileFilter: file_filter,
97
- });
98
- const page = paginate(matches, offset, limit);
96
+ const page = (await searchProjectSymbolsPageWithIndex(project, projectRoot, context.status.project, context.symbolIndex, context.symbolIndexReady, { query, kinds, fileFilter: file_filter }, offset, limit, async (reason) => {
97
+ await reportSymbolIndexFailure(projectRoot, reason);
98
+ }, operationContext)) ??
99
+ searchProjectSymbolsPage(project, projectRoot, { query, kinds, fileFilter: file_filter }, offset, limit, operationContext);
99
100
  const { items, ...metadata } = page;
100
101
  return {
101
102
  symbols: projectSymbols(detail, items),
102
103
  duration_ms: performance.now() - startedAt,
103
104
  ...metadata,
104
105
  };
105
- });
106
+ }, requestContext);
106
107
  const outputSchema = SearchOutputSchemas[detail];
107
108
  return formattedResult(outputSchema, structuredContent, output_format);
108
109
  }
109
110
  catch (error) {
110
- return errorResult(error);
111
+ return errorResult(error, createToolErrorContext("ast_search_symbols", project_root));
111
112
  }
112
113
  });
113
114
  }
@@ -0,0 +1,80 @@
1
+ # Support policy
2
+
3
+ This document defines the supported platform, runtime, persistence, and operational boundary for `ast-mcp-server` v0.7.0.
4
+
5
+ ## Supported release target
6
+
7
+ | Environment | Status | Contract |
8
+ | ------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | Linux x64 with required GNU coreutils, Node.js 22.5.0 | Supported | Full repository, MCP/CLI/package, lifecycle, mutation, and production-readiness gates. Persistence canary runs require `--experimental-sqlite`. |
10
+ | Linux x64 with required GNU coreutils, current Node.js 24 line | Supported | Full repository, MCP/CLI/package, lifecycle, mutation, and production-readiness gates. |
11
+ | Other Linux architectures or systems without required GNU coreutils | Unverified | Not supported until equivalent architecture and filesystem-publication gates pass. |
12
+ | macOS | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
13
+ | Windows | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
14
+ | Remote, untrusted, or multi-tenant service use | Unsupported | No network authentication, sandbox, or tenant-isolation boundary is provided. |
15
+
16
+ The package engine floor is Node.js `>=22.5.0`, but release evidence is deliberately narrower: exact Node.js 22.5.0 and the current Node.js 24 line on Linux. A newer version satisfying the engine range is not automatically a verified release target.
17
+
18
+ The checked-evidence freezer requires GNU coreutils `mv` with `--update=none-fail`, `--no-copy`, and `--no-target-directory`. Equivalent publication semantics have not been verified on other operating systems or architectures.
19
+
20
+ ## Local stdio trust boundary
21
+
22
+ `ast-mcp-server` is a local stdio process. It inherits the invoking user's filesystem authority, and an MCP client may request any `project_root` or `tsconfig.json` that user can access.
23
+
24
+ The server does not provide:
25
+
26
+ - HTTP or network authentication;
27
+ - filesystem or process sandboxing;
28
+ - privilege separation;
29
+ - tenant isolation;
30
+ - protection from a malicious process running with the same filesystem authority.
31
+
32
+ Operation locks coordinate cooperating same-user processes. They do not stop editors, network filesystems, or hostile same-user writers. Report-set freezer coordination likewise assumes cooperating same-UID processes and cannot defend checked evidence against a malicious process with the same filesystem authority. The freezer guarantees no-replace atomic visibility of the checked directory; it does not claim persistence across sudden power loss because parent-directory `fsync` durability is not established.
33
+
34
+ ## Symbol-index persistence
35
+
36
+ The supported default is memory-only indexing. When `AST_SYMBOL_INDEX_PERSISTENCE` is absent, persistence remains disabled and no persistent cache is created.
37
+
38
+ The only candidate-authorized opt-in persistence policy is `canary`; no public v0.7.0 release transition has occurred yet:
39
+
40
+ ```bash
41
+ AST_SYMBOL_INDEX_PERSISTENCE=canary \
42
+ AST_SYMBOL_INDEX_CACHE_ROOT=/absolute/isolated/cache/root \
43
+ ast-mcp-server
44
+ ```
45
+
46
+ The cache root must be explicit and absolute. Use a dedicated local directory with permissions appropriate for the invoking user. Node.js 22.5.0 additionally requires `--experimental-sqlite` for this canary path.
47
+
48
+ `AST_SYMBOL_INDEX_PERSISTENCE=enabled` is reserved and intentionally fails closed to memory-only with `policy_reason: "enabled_not_released"`. Unknown values and invalid/missing canary cache roots also fail closed. Canary evidence authorizes the opt-in capability; it does not authorize global or default activation.
49
+
50
+ ## Production-readiness acceptance
51
+
52
+ The checked Linux x64 matrix covers `ast-mcp-server` and `x-scraper` under Node.js 22.5.0 and Node.js 24. MCP-PROD-404 is normative; its complete release gate requires all four reports to pass the following contract:
53
+
54
+ - zero semantic mismatches across disabled, cold, warm, restart, and rollback reads;
55
+ - byte-identical real-repository status and worktree trees;
56
+ - compiler-equivalent corruption and write-failure fallback with explicit recovery;
57
+ - exact mutation postimages, replay, byte-exact rollback, and no cache side effect in disposable fixtures;
58
+ - bounded queue/session behavior and cancellation;
59
+ - one process running identical deterministic-fixture reads under Node.js launched with `--expose-gc`;
60
+ - exactly 10 warmups, 50 measured reads, and one explicit `global.gc()` immediately before every measured RSS sample;
61
+ - deterministic-fixture final-five median RSS no more than `max(32 MiB, 20%)` above the first-five median;
62
+ - immutable-cache growth from the first complete build to the third unchanged restart no more than `max(1 MiB, 5%)`;
63
+ - recursive cache accounting through `lstat` without following symlinks, including SQLite main, WAL, SHM, quarantine, and temporary files in a stable sorted relative-file manifest; any unreadable, symbolic, or non-regular entry fails the gate.
64
+
65
+ Any semantic mismatch, mutation-boundary effect, unrecovered fallback, repository mutation, path/secret disclosure, unexpected or accepted invalid cache entry, or failed runtime/identity check fails the gate. Deliberately injected corruption is valid only when it is rejected, falls back to the exact compiler baseline, and then recovers explicitly.
66
+
67
+ Real-repository latency and RSS are retained as observations only. They are not capacity guarantees, SLAs, or PASS/FAIL criteria for this release.
68
+
69
+ ## Compatibility and issue handling
70
+
71
+ Before reporting a compatibility problem, capture:
72
+
73
+ - operating system and architecture;
74
+ - exact `node --version` output and Node binary path;
75
+ - package version;
76
+ - MCP client and transport configuration;
77
+ - whether persistence is disabled or running in explicit canary mode;
78
+ - the bounded public error code and correlation identifier, if present.
79
+
80
+ Do not attach source code, absolute project paths, cache contents, credentials, tokens, or private configuration unless they have been intentionally sanitized. Report suspected vulnerabilities through [SECURITY.md](../SECURITY.md), not a public issue.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ast-mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Structural code MCP server and batch CLI for compact TypeScript/JavaScript reads and hash-bound AST edits.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -35,13 +35,16 @@
35
35
  "dist",
36
36
  "skills",
37
37
  "README.md",
38
- "CHANGELOG.md"
38
+ "CHANGELOG.md",
39
+ "SECURITY.md",
40
+ "docs/support.md"
39
41
  ],
40
42
  "engines": {
41
- "node": ">=20.19"
43
+ "node": ">=22.5.0"
42
44
  },
43
45
  "resolutions": {
44
- "hono": "4.12.34"
46
+ "hono": "4.12.34",
47
+ "nanoid": "3.3.17"
45
48
  },
46
49
  "scripts": {
47
50
  "build": "tsc",
@@ -49,14 +52,21 @@
49
52
  "test": "vitest run",
50
53
  "test:watch": "vitest",
51
54
  "test:mcp": "yarn build && node scripts/mcp-smoke.mjs",
55
+ "test:lifecycle": "yarn build && node scripts/mcp-lifecycle-smoke.mjs",
52
56
  "test:cli": "yarn build && node scripts/cli-smoke.mjs",
57
+ "test:errors": "yarn build && node scripts/public-error-smoke.mjs",
53
58
  "test:package": "node scripts/package-smoke.mjs",
59
+ "test:release-candidate": "node scripts/release-candidate-matrix.mjs",
54
60
  "benchmark": "yarn build && node scripts/benchmark.mjs",
55
61
  "benchmark:corpus": "yarn build && node scripts/benchmark-corpus.mjs",
56
62
  "benchmark:batch": "yarn build && node scripts/benchmark-batch.mjs",
57
63
  "benchmark:formats": "yarn build && node scripts/benchmark-formats.mjs",
58
64
  "benchmark:shapes": "yarn build && node scripts/benchmark-result-shapes.mjs",
59
65
  "benchmark:agent-workflows": "yarn build && node scripts/benchmark-agent-workflows.mjs",
66
+ "benchmark:index-storage": "yarn build && node scripts/benchmark-index-storage.mjs",
67
+ "benchmark:index-integration": "yarn build && node scripts/benchmark-symbol-index-integration.mjs",
68
+ "benchmark:production-readiness": "yarn build && node scripts/canary-local-mcp.mjs run",
69
+ "benchmark:freeze-production-readiness": "node scripts/canary-local-mcp.mjs freeze-report-set",
60
70
  "lint": "eslint src test scripts vitest.config.ts eslint.config.js",
61
71
  "format": "prettier --write .",
62
72
  "format:check": "prettier --check .",
@@ -82,5 +92,6 @@
82
92
  "typescript": "^5.6.0",
83
93
  "typescript-eslint": "^8.65.0",
84
94
  "vitest": "^4.1.10"
85
- }
95
+ },
96
+ "gitHead": "5fc09f4a874b8e6eca20433ba2d1a719e96aa1de"
86
97
  }