ast-mcp-server 0.6.0 → 0.7.1

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 +38 -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
@@ -0,0 +1,255 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { randomUUID } from "node:crypto";
3
+ import { types as utilTypes } from "node:util";
4
+ export const PUBLIC_ERROR_CODES = Object.freeze([
5
+ "INVALID_INPUT",
6
+ "PROJECT_NOT_FOUND",
7
+ "PROJECT_CAPACITY_EXCEEDED",
8
+ "PROJECT_QUEUE_FULL",
9
+ "QUEUE_WAIT_TIMEOUT",
10
+ "REQUEST_CANCELLED",
11
+ "OPERATION_DEADLINE_EXCEEDED",
12
+ "SERVER_SHUTTING_DOWN",
13
+ "NOT_FOUND",
14
+ "AMBIGUOUS_TARGET",
15
+ "STALE_WORKSPACE",
16
+ "MUTATION_BLOCKED",
17
+ "CONFLICT",
18
+ "INTERNAL_ERROR",
19
+ ]);
20
+ export const MAX_PUBLIC_ERROR_MESSAGE_BYTES = 2048;
21
+ export const MAX_PUBLIC_ERROR_RESPONSE_BYTES = 4096;
22
+ export const INTERNAL_ERROR_MESSAGE = "An internal error occurred.";
23
+ const MAX_PUBLIC_ERROR_INPUT_CHARS = 8192;
24
+ const TRUNCATION_SUFFIX = "... [truncated]";
25
+ const PUBLIC_ERROR_CODE_SET = new Set(PUBLIC_ERROR_CODES);
26
+ const FIXED_PUBLIC_MESSAGES = Object.freeze({
27
+ INVALID_INPUT: "The request is invalid.",
28
+ PROJECT_NOT_FOUND: "The project was not found.",
29
+ PROJECT_CAPACITY_EXCEEDED: "Project session capacity exceeded.",
30
+ PROJECT_QUEUE_FULL: "Project operation queue is full.",
31
+ QUEUE_WAIT_TIMEOUT: "Project operation queue wait timed out.",
32
+ REQUEST_CANCELLED: "Request was cancelled.",
33
+ OPERATION_DEADLINE_EXCEEDED: "Operation deadline exceeded.",
34
+ SERVER_SHUTTING_DOWN: "Server is shutting down.",
35
+ NOT_FOUND: "The requested target was not found.",
36
+ AMBIGUOUS_TARGET: "The requested target is ambiguous.",
37
+ STALE_WORKSPACE: "The workspace changed. Retry the operation.",
38
+ MUTATION_BLOCKED: "The mutation is blocked.",
39
+ CONFLICT: "The operation conflicts with current state.",
40
+ INTERNAL_ERROR: INTERNAL_ERROR_MESSAGE,
41
+ });
42
+ const LEGACY_OPERATIONAL_CODES = Object.freeze({
43
+ ProjectCapacityError: new Set(["PROJECT_CAPACITY_EXCEEDED"]),
44
+ ProjectOperationSchedulerError: new Set([
45
+ "PROJECT_QUEUE_FULL",
46
+ "QUEUE_WAIT_TIMEOUT",
47
+ "REQUEST_CANCELLED",
48
+ "OPERATION_DEADLINE_EXCEEDED",
49
+ "SERVER_SHUTTING_DOWN",
50
+ ]),
51
+ RequestContextError: new Set([
52
+ "REQUEST_CANCELLED",
53
+ "OPERATION_DEADLINE_EXCEEDED",
54
+ ]),
55
+ });
56
+ const LEGACY_MESSAGE_RULES = Object.freeze([
57
+ {
58
+ code: "PROJECT_NOT_FOUND",
59
+ pattern: /^(?:No tsconfig\.json found at\b|Project session not found\.)/,
60
+ },
61
+ {
62
+ code: "AMBIGUOUS_TARGET",
63
+ pattern: /^(?:Ambiguous source file\b|Symbol .{0,1024} is ambiguous\.)/,
64
+ },
65
+ {
66
+ code: "NOT_FOUND",
67
+ pattern: /^(?:(?:Source file|Symbol) .{0,1024} (?:was not found|does not exist)|Prepared operation .{0,1024} (?:was not found or has expired|has expired)\.|File .{0,1024} is not part of operation .{0,1024}\.)/,
68
+ },
69
+ {
70
+ code: "STALE_WORKSPACE",
71
+ pattern: /^(?:Workspace changed\b|Project (?:configuration|source) changed during synchronization\.)/,
72
+ },
73
+ {
74
+ code: "CONFLICT",
75
+ pattern: /^(?:Conflict:|Plan hash mismatch\b|Persisted plan integrity mismatch\b|Operation id collision\b|Applied receipt conflict\b|Postimage recovery conflict\b|Operation target already exists\b|Another AST apply holds\b)/,
76
+ },
77
+ {
78
+ code: "MUTATION_BLOCKED",
79
+ pattern: /^(?:The operation is blocked\b|\d{1,10} new TypeScript error\(s\) would be introduced\.|Operation target\b|Symbol .{0,1024} (?:does not support structural rename|does not expose a replaceable body)|The structural operation produced no file changes\.)/,
80
+ },
81
+ {
82
+ code: "INVALID_INPUT",
83
+ pattern: /^(?:File path traversal is not allowed\.|Windows absolute paths are not valid on this host\.|Source file resolves outside the configured project root\.|Unsupported (?:UTF-16|non-UTF-8) source encoding\b|Persisted (?:operation|applied operation|prepared operation|project_root|original hash|updated hash|affected_files|diagnostic error delta|blocked status)\b|Scaffold file_path must\b|.{0,1024} is not a valid TypeScript identifier\.|.{0,1024} must (?:be|not)\b|.{0,1024} requires\b|.{0,1024} exceeds \d+ characters\.)/,
84
+ },
85
+ ]);
86
+ export class PublicOperationalError extends Error {
87
+ code;
88
+ #brand = true;
89
+ constructor(code, message, options) {
90
+ super(message, options);
91
+ this.code = code;
92
+ this.name = "PublicOperationalError";
93
+ }
94
+ static is(value) {
95
+ try {
96
+ return typeof value === "object" && value !== null && #brand in value;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ }
103
+ function redactAbsolutePaths(value) {
104
+ const traversalMarker = "__TRAVERSAL_PATH_REDACTED__";
105
+ const quotedTraversalPath = /(?<=["'`])(?:[^"'`\n\r]*[\\/])?\.\.[\\/][^"'`\n\r]*?(?=["'`])/g;
106
+ const labelledTraversalPath = /([=:]\s+)([^"'`:\n\r;,]*[ \t]+[^"'`:\n\r;,/\\]+[\\/]\.\.[\\/][^"'`:\n\r]*?)(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
107
+ const diagnosticTraversalPath = /(\b(?:read|open|resolve|watch|parse)\s+)([^"'`:\n\r;,]*[\\/]\.\.[\\/][^"'`:\n\r]*?)(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/gi;
108
+ const spacedTraversalPath = /(?<=\s)([^"'`:\n\r;,]*[ \t]+[^"'`:\n\r;,/\\]+[\\/]\.\.[\\/][^"'`:\n\r]*?)(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
109
+ const relativeTraversalPath = /(?:[^\s"'`:/${}\\]+[\\/])+\.\.[\\/][^"'`:\n\r]*?(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
110
+ const directTraversalPath = /(?:^|(?<=[\s(]))\.\.[\\/][^"'`:\n\r]*?(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
111
+ const uncPath = /(?:\\\\|\/{2})[^"'`\n\r]*?(?=\s*(?::(?=\s|$)|[;,\n\r"'`)\]}])|(?:\.(?=\s|$))|$)/g;
112
+ const drivePath = /\b[A-Za-z]:[^"'`\n\r]*?(?=\s*(?::(?=\s|$)|[;,\n\r"'`)\]}])|(?:\.(?=\s|$))|$)/g;
113
+ const posixPath = /(?<![A-Za-z0-9_./-])\/(?!\/)[^"'`\n\r]*?(?=\s*(?::(?=\s|$)|[;,\n\r"'`)\]}])|(?:\.(?=\s|$))|$)/g;
114
+ const redacted = value
115
+ .replace(quotedTraversalPath, traversalMarker)
116
+ .replace(labelledTraversalPath, `$1${traversalMarker}`)
117
+ .replace(diagnosticTraversalPath, `$1${traversalMarker}`)
118
+ .replace(spacedTraversalPath, traversalMarker)
119
+ .replace(relativeTraversalPath, traversalMarker)
120
+ .replace(directTraversalPath, traversalMarker);
121
+ return redacted
122
+ .replace(drivePath, "[path-redacted]")
123
+ .replace(uncPath, "[path-redacted]")
124
+ .replace(posixPath, "[path-redacted]")
125
+ .replaceAll(traversalMarker, "[path-redacted]");
126
+ }
127
+ function redactCredentials(value) {
128
+ return value
129
+ .replace(/(?!(?:\bauthorization[ \t]*[:=])[ \t]*\[REDACTED\])(\bauthorization[ \t]*[:=])([ \t]*)[^\r\n;]+/gi, "$1$2[REDACTED]")
130
+ .replace(/(?!(?:\bbearer)[ \t]+\[REDACTED\])\bbearer[ \t]+[^\r\n;]+/gi, "Bearer [REDACTED]")
131
+ .replace(/\b(api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|token|password|secret|credential|connection[-_ ]?string)\b\s*[:=]\s*(?!\[REDACTED\])(?:"[^"]*"|'[^']*'|[^\s,;}\]]+)/gi, "$1=[REDACTED]")
132
+ .replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^\s"'`]+@/g, "$1[REDACTED]@")
133
+ .replace(/\b(?:sk|ghp|github_pat|xox[baprs]-|AIza)[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]");
134
+ }
135
+ export function sanitizeSensitiveText(value) {
136
+ return redactCredentials(redactAbsolutePaths(value));
137
+ }
138
+ function truncateUtf8(value, maximumBytes) {
139
+ if (Buffer.byteLength(value, "utf8") <= maximumBytes)
140
+ return value;
141
+ const suffixBytes = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
142
+ const source = Buffer.from(value, "utf8");
143
+ let end = Math.max(0, maximumBytes - suffixBytes);
144
+ while (end > 0 && (source[end] & 0xc0) === 0x80)
145
+ end -= 1;
146
+ let prefix = source.subarray(0, end).toString("utf8");
147
+ while (Buffer.byteLength(prefix + TRUNCATION_SUFFIX, "utf8") > maximumBytes) {
148
+ prefix = prefix.slice(0, -1);
149
+ }
150
+ return prefix + TRUNCATION_SUFFIX;
151
+ }
152
+ export function sanitizePublicText(value) {
153
+ const boundedInput = value.slice(0, MAX_PUBLIC_ERROR_INPUT_CHARS);
154
+ const singleLine = Array.from(boundedInput, (character) => {
155
+ const codePoint = character.codePointAt(0) ?? 0;
156
+ return codePoint <= 31 || codePoint === 127 ? " " : character;
157
+ }).join("");
158
+ return truncateUtf8(sanitizeSensitiveText(singleLine), MAX_PUBLIC_ERROR_MESSAGE_BYTES);
159
+ }
160
+ function ownString(value, property) {
161
+ const descriptor = Object.getOwnPropertyDescriptor(value, property);
162
+ return descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "string"
163
+ ? descriptor.value
164
+ : null;
165
+ }
166
+ function codedError(error) {
167
+ try {
168
+ if (PublicOperationalError.is(error)) {
169
+ const code = ownString(error, "code");
170
+ const message = ownString(error, "message");
171
+ if (code === null || message === null || !PUBLIC_ERROR_CODE_SET.has(code))
172
+ return null;
173
+ return {
174
+ code: code,
175
+ message: sanitizePublicText(message),
176
+ };
177
+ }
178
+ if (!(error instanceof Error))
179
+ return null;
180
+ const name = ownString(error, "name");
181
+ const code = ownString(error, "code");
182
+ if (name === null || code === null || !PUBLIC_ERROR_CODE_SET.has(code))
183
+ return null;
184
+ const publicCode = code;
185
+ if (!LEGACY_OPERATIONAL_CODES[name]?.has(publicCode))
186
+ return null;
187
+ return {
188
+ code: publicCode,
189
+ message: FIXED_PUBLIC_MESSAGES[publicCode],
190
+ };
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
196
+ function knownLegacyError(error) {
197
+ try {
198
+ if (!(error instanceof Error))
199
+ return null;
200
+ const message = ownString(error, "message");
201
+ if (message === null)
202
+ return null;
203
+ const boundedMessage = message.slice(0, MAX_PUBLIC_ERROR_RESPONSE_BYTES);
204
+ const match = LEGACY_MESSAGE_RULES.find(({ pattern }) => pattern.test(boundedMessage));
205
+ return match === undefined
206
+ ? null
207
+ : { code: match.code, message: FIXED_PUBLIC_MESSAGES[match.code] };
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
213
+ export function classifyPublicError(error) {
214
+ if (typeof error === "object" && error !== null && utilTypes.isProxy(error)) {
215
+ return { code: "INTERNAL_ERROR", message: INTERNAL_ERROR_MESSAGE };
216
+ }
217
+ return (codedError(error) ??
218
+ knownLegacyError(error) ?? {
219
+ code: "INTERNAL_ERROR",
220
+ message: INTERNAL_ERROR_MESSAGE,
221
+ });
222
+ }
223
+ function serializePublicError(code, message, correlationId) {
224
+ const envelope = {
225
+ error: {
226
+ code,
227
+ message,
228
+ correlation_id: correlationId,
229
+ },
230
+ };
231
+ return { envelope, text: JSON.stringify(envelope) };
232
+ }
233
+ export function renderPublicError(error) {
234
+ const classified = classifyPublicError(error);
235
+ const correlationId = randomUUID();
236
+ const initial = serializePublicError(classified.code, classified.message, correlationId);
237
+ if (Buffer.byteLength(initial.text, "utf8") <= MAX_PUBLIC_ERROR_RESPONSE_BYTES) {
238
+ return initial;
239
+ }
240
+ let lowerBudget = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
241
+ let upperBudget = Buffer.byteLength(classified.message, "utf8") - 1;
242
+ let best = serializePublicError(classified.code, TRUNCATION_SUFFIX, correlationId);
243
+ while (lowerBudget <= upperBudget) {
244
+ const candidateBudget = Math.floor((lowerBudget + upperBudget) / 2);
245
+ const candidate = serializePublicError(classified.code, truncateUtf8(classified.message, candidateBudget), correlationId);
246
+ if (Buffer.byteLength(candidate.text, "utf8") <= MAX_PUBLIC_ERROR_RESPONSE_BYTES) {
247
+ best = candidate;
248
+ lowerBudget = candidateBudget + 1;
249
+ }
250
+ else {
251
+ upperBudget = candidateBudget - 1;
252
+ }
253
+ }
254
+ return best;
255
+ }
@@ -1,12 +1,15 @@
1
1
  import path from "node:path";
2
2
  import { Node } from "ts-morph";
3
3
  import { paginate } from "./pagination.js";
4
- export function collectSymbolReferences(node, projectRoot, symbolPath, includeDeclaration, detail, offset, limit) {
4
+ import { NO_REQUEST_CONTEXT } from "./request-context.js";
5
+ export function collectSymbolReferences(node, projectRoot, symbolPath, includeDeclaration, detail, offset, limit, requestContext = NO_REQUEST_CONTEXT) {
6
+ requestContext.checkpoint();
5
7
  if (!Node.isReferenceFindable(node)) {
6
8
  throw new Error(`Symbol "${symbolPath}" (${node.getKindName()}) does not support semantic reference search.`);
7
9
  }
8
10
  const lineCache = new Map();
9
11
  const location = (referenceNode, isDeclaration) => {
12
+ requestContext.checkpoint();
10
13
  const referenceSource = referenceNode.getSourceFile();
11
14
  const absoluteFile = referenceSource.getFilePath();
12
15
  let lines = lineCache.get(absoluteFile);
@@ -26,12 +29,15 @@ export function collectSymbolReferences(node, projectRoot, symbolPath, includeDe
26
29
  : {}),
27
30
  };
28
31
  };
32
+ requestContext.checkpoint();
29
33
  const references = node.findReferencesAsNodes().map((reference) => location(reference, false));
30
34
  const declaration = location(node, true);
31
35
  const allLocations = includeDeclaration ? [declaration, ...references] : references;
36
+ requestContext.checkpoint();
32
37
  allLocations.sort((left, right) => `${left.file}:${left.line}:${left.column}:${left.is_declaration ? 0 : 1}`.localeCompare(`${right.file}:${right.line}:${right.column}:${right.is_declaration ? 0 : 1}`));
33
38
  const affectedFiles = [...new Set([declaration, ...references].map((item) => item.file))].sort();
34
39
  const page = paginate(allLocations, offset, limit);
40
+ requestContext.checkpoint();
35
41
  return {
36
42
  symbol: symbolPath,
37
43
  include_declaration: includeDeclaration,