ast-mcp-server 0.5.1 → 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 (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +63 -14
  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 +25 -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 +262 -0
  10. package/dist/services/diagnostics.js +10 -5
  11. package/dist/services/file-fingerprints.js +104 -0
  12. package/dist/services/file-snapshot.js +103 -0
  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 +54 -0
  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 +430 -47
  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 +176 -0
  40. package/dist/tools/find_references.js +17 -55
  41. package/dist/tools/get_diagnostics.js +10 -7
  42. package/dist/tools/get_file.js +107 -0
  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 -63
  54. package/docs/support.md +80 -0
  55. package/package.json +18 -6
  56. package/skills/structural-code-editing/SKILL.md +42 -8
  57. package/benchmark/README.md +0 -129
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_FILE_LINE_LIMIT, MAX_FILE_LINE_LIMIT, readFileSnapshot, } from "../services/file-snapshot.js";
3
+ import { buildFileOutline } from "../services/outline.js";
4
+ import { getSourceFileOrThrow, withProject } from "../services/project.js";
5
+ import { createRequestContext } from "../services/request-context.js";
6
+ import { FRESHNESS_CAUSES, SNAPSHOT_STATES } from "../services/read-contracts.js";
7
+ import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
8
+ const AstGetFileInputSchema = z.object({
9
+ project_root: z
10
+ .string()
11
+ .describe("Absolute project directory containing tsconfig.json, or the config path."),
12
+ file_path: z
13
+ .string()
14
+ .describe("Project-relative or unambiguous source file path included by the active tsconfig."),
15
+ offset: z.number().int().min(0).default(0).describe("Zero-based line offset for source mode."),
16
+ limit: z
17
+ .number()
18
+ .int()
19
+ .min(1)
20
+ .max(MAX_FILE_LINE_LIMIT)
21
+ .default(DEFAULT_FILE_LINE_LIMIT)
22
+ .describe(`Maximum source lines to return, capped at ${MAX_FILE_LINE_LIMIT}.`),
23
+ symbols_only: z
24
+ .boolean()
25
+ .default(false)
26
+ .describe("Return body-free symbol selectors and signatures instead of source lines."),
27
+ });
28
+ const FileRangeSchema = z.object({
29
+ offset: z.number().int().min(0),
30
+ limit: z.number().int().min(1).max(MAX_FILE_LINE_LIMIT),
31
+ total_lines: z.number().int().min(0),
32
+ });
33
+ const FileLineSchema = z.object({
34
+ line: z.number().int().positive(),
35
+ text: z.string(),
36
+ });
37
+ const OutlineSymbolSchema = z.object({
38
+ symbolPath: z.string(),
39
+ name: z.string(),
40
+ kind: z.string(),
41
+ signature: z.string(),
42
+ startLine: z.number().int().positive(),
43
+ endLine: z.number().int().positive(),
44
+ });
45
+ const FileSnapshotBaseSchema = {
46
+ file: z.string(),
47
+ file_hash: z.string().regex(/^[0-9a-f]{64}$/),
48
+ snapshot_state: z.enum(["fresh", "stale"]),
49
+ freshness: z.object({
50
+ state: z.enum(SNAPSHOT_STATES),
51
+ causes: z.array(z.enum(FRESHNESS_CAUSES)),
52
+ checked_at: z.string().nullable(),
53
+ }),
54
+ };
55
+ const AstGetFileOutputSchema = z.object({
56
+ mode: z.enum(["source", "symbols_only"]),
57
+ ...FileSnapshotBaseSchema,
58
+ range: FileRangeSchema.optional(),
59
+ lines: z.array(FileLineSchema).optional(),
60
+ symbols: z.array(OutlineSymbolSchema).optional(),
61
+ });
62
+ export function registerGetFile(server) {
63
+ server.registerTool("ast_get_file", {
64
+ title: "Get bounded source file",
65
+ description: "Returns exact bounded source lines for one TypeScript/JavaScript file, or body-free symbols when symbols_only is true. Read-only; file snapshot freshness and project freshness are reported separately from compiler diagnostics.",
66
+ inputSchema: AstGetFileInputSchema,
67
+ outputSchema: AstGetFileOutputSchema,
68
+ annotations: {
69
+ readOnlyHint: true,
70
+ destructiveHint: false,
71
+ idempotentHint: true,
72
+ openWorldHint: false,
73
+ },
74
+ }, async ({ project_root, file_path, offset, limit, symbols_only }, extra) => {
75
+ const requestContext = createRequestContext(extra.signal);
76
+ try {
77
+ const structuredContent = await withProject(project_root, async ({ project, projectRoot, status }, operationContext) => {
78
+ const snapshot = await readFileSnapshot(project, projectRoot, file_path, { offset, limit }, operationContext);
79
+ const freshness = {
80
+ state: status.state,
81
+ causes: status.causes,
82
+ checked_at: status.lastSuccessfulSyncAt,
83
+ };
84
+ if (!symbols_only) {
85
+ return {
86
+ mode: "source",
87
+ ...snapshot,
88
+ freshness,
89
+ };
90
+ }
91
+ const sourceFile = getSourceFileOrThrow(project, file_path);
92
+ return {
93
+ mode: "symbols_only",
94
+ file: snapshot.file,
95
+ file_hash: snapshot.file_hash,
96
+ snapshot_state: snapshot.snapshot_state,
97
+ freshness,
98
+ symbols: buildFileOutline(sourceFile, operationContext).symbols,
99
+ };
100
+ }, requestContext);
101
+ return structuredResult(structuredContent);
102
+ }
103
+ catch (error) {
104
+ return errorResult(error, createToolErrorContext("ast_get_file", project_root));
105
+ }
106
+ });
107
+ }
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_IMPACT_MAX_DEPTH, DEFAULT_IMPACT_MAX_EDGES, DEFAULT_IMPACT_MAX_NODES, IMPACT_DIRECTIONS, MAX_IMPACT_DEPTH, MAX_IMPACT_EDGES, MAX_IMPACT_NODES, resolveImpactRoot, traverseCompilerImpact, } from "../services/impact.js";
3
+ import { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
4
+ import { withProject } from "../services/project.js";
5
+ import { createRequestContext } from "../services/request-context.js";
6
+ import { RELATIONSHIP_CONFIDENCES, RELATIONSHIP_EDGE_KINDS, RELATIONSHIP_PROVENANCES, RELATIONSHIP_RESOLUTIONS, } from "../services/relationships.js";
7
+ import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
8
+ const AstGetImpactInputSchema = z.object({
9
+ project_root: z
10
+ .string()
11
+ .describe("Absolute project directory containing tsconfig.json, or the config path."),
12
+ file_path: z.string().describe("Project-relative file containing the exact impact root."),
13
+ symbol_path: z.string().describe("Exact symbol path returned by an outline or symbol search."),
14
+ direction: z
15
+ .enum(IMPACT_DIRECTIONS)
16
+ .default("both")
17
+ .describe("Traversal direction relative to the root symbol."),
18
+ max_depth: z
19
+ .number()
20
+ .int()
21
+ .min(0)
22
+ .max(MAX_IMPACT_DEPTH)
23
+ .default(DEFAULT_IMPACT_MAX_DEPTH)
24
+ .describe("Maximum relationship depth, including zero for the root only."),
25
+ max_nodes: z
26
+ .number()
27
+ .int()
28
+ .min(1)
29
+ .max(MAX_IMPACT_NODES)
30
+ .default(DEFAULT_IMPACT_MAX_NODES)
31
+ .describe("Maximum returned impact nodes, including the root."),
32
+ max_edges: z
33
+ .number()
34
+ .int()
35
+ .min(1)
36
+ .max(MAX_IMPACT_EDGES)
37
+ .default(DEFAULT_IMPACT_MAX_EDGES)
38
+ .describe("Maximum returned relationship edges."),
39
+ relationship_kinds: z
40
+ .array(z.enum(RELATIONSHIP_EDGE_KINDS))
41
+ .optional()
42
+ .describe("Optional relationship kinds to traverse; defaults to all normalized kinds."),
43
+ ...ToolOutputFormatInputSchema,
44
+ });
45
+ const SourceRangeSchema = z.object({
46
+ start_line: z.number().int().positive(),
47
+ start_column: z.number().int().positive().optional(),
48
+ end_line: z.number().int().positive().optional(),
49
+ end_column: z.number().int().positive().optional(),
50
+ });
51
+ const RelationshipEndpointSchema = z.object({
52
+ file: z.string(),
53
+ symbol_path: z.string(),
54
+ selector: z.string(),
55
+ range: SourceRangeSchema.optional(),
56
+ });
57
+ const FreshnessSchema = z.object({
58
+ state: z.enum(SNAPSHOT_STATES),
59
+ causes: z.array(z.enum(FRESHNESS_CAUSES)),
60
+ checked_at: z.string().nullable(),
61
+ });
62
+ const RelationshipEdgeSchema = z.object({
63
+ relationship_id: z.string(),
64
+ source: RelationshipEndpointSchema,
65
+ target: RelationshipEndpointSchema,
66
+ kind: z.enum(RELATIONSHIP_EDGE_KINDS),
67
+ provenance: z.enum(RELATIONSHIP_PROVENANCES),
68
+ confidence: z.enum(RELATIONSHIP_CONFIDENCES),
69
+ resolution: z.enum(RELATIONSHIP_RESOLUTIONS),
70
+ compiler_authoritative: z.boolean(),
71
+ freshness: FreshnessSchema,
72
+ });
73
+ const ImpactOutputSchema = z.object({
74
+ root: RelationshipEndpointSchema,
75
+ direction: z.enum(IMPACT_DIRECTIONS),
76
+ relationship_kinds: z.array(z.enum(RELATIONSHIP_EDGE_KINDS)),
77
+ nodes: z.array(z.object({
78
+ endpoint: RelationshipEndpointSchema,
79
+ depth: z.number().int().min(0),
80
+ direct: z.boolean(),
81
+ })),
82
+ edges: z.array(RelationshipEdgeSchema),
83
+ visited_nodes: z.number().int().min(0),
84
+ visited_edges: z.number().int().min(0),
85
+ max_depth_reached: z.number().int().min(0),
86
+ max_depth: z.number().int().min(0),
87
+ max_nodes: z.number().int().positive(),
88
+ max_edges: z.number().int().positive(),
89
+ incomplete: z.boolean(),
90
+ truncation: z.object({
91
+ truncated: z.boolean(),
92
+ reason: z.enum(TRUNCATION_REASONS).nullable(),
93
+ }),
94
+ truncation_reasons: z.array(z.enum(TRUNCATION_REASONS)),
95
+ freshness: FreshnessSchema,
96
+ });
97
+ export function registerGetImpact(server) {
98
+ server.registerTool("ast_get_impact", {
99
+ title: "Get bounded compiler-backed impact",
100
+ description: "Traverses exact compiler-backed relationships around a symbol with explicit direction, depth, node and edge budgets. Returns bounded session freshness metadata and fails closed when exact relationships are not fresh. Read-only evidence only; it never prepares or applies mutations.",
101
+ inputSchema: AstGetImpactInputSchema,
102
+ outputSchema: ImpactOutputSchema,
103
+ annotations: {
104
+ readOnlyHint: true,
105
+ destructiveHint: false,
106
+ idempotentHint: true,
107
+ openWorldHint: false,
108
+ },
109
+ }, async ({ project_root, file_path, symbol_path, direction, max_depth, max_nodes, max_edges, relationship_kinds, output_format, }, extra) => {
110
+ const requestContext = createRequestContext(extra.signal);
111
+ try {
112
+ const structuredContent = await withProject(project_root, (context, operationContext) => {
113
+ if (context.status.state !== "fresh") {
114
+ throw new Error("Compiler-backed impact evidence is not fresh. Retry the read.");
115
+ }
116
+ const freshness = {
117
+ state: context.status.state,
118
+ causes: context.status.causes,
119
+ checked_at: context.status.lastSuccessfulSyncAt,
120
+ };
121
+ const root = resolveImpactRoot(context.project, context.projectRoot, { file_path, symbol_path }, operationContext);
122
+ const impact = traverseCompilerImpact(context.project, context.projectRoot, root, freshness, { direction, max_depth, max_nodes, max_edges, relationship_kinds }, operationContext);
123
+ return {
124
+ ...impact,
125
+ freshness,
126
+ };
127
+ }, requestContext);
128
+ return formattedResult(ImpactOutputSchema, structuredContent, output_format);
129
+ }
130
+ catch (error) {
131
+ return errorResult(error, createToolErrorContext("ast_get_impact", project_root));
132
+ }
133
+ });
134
+ }
@@ -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,