syntax-map-mcp 1.0.0 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.2.0 - 2026-05-07
6
+
7
+ - `lsp_diagnostics` 도구를 추가해 tree-sitter parse error 기반의 가벼운 문법 진단을 LSP Diagnostic 형태로 반환하도록 했습니다.
8
+ - diagnostics 분석 계층에 provider 인터페이스를 추가해 이후 언어별 LSP 서버 진단 결과를 결합할 수 있게 했습니다.
9
+
10
+ ## 1.1.0 - 2026-05-07
11
+
12
+ - Rust `.rs` 파일 파싱을 지원하고, `get_ast_tree`와 LSP document symbols, definition, references, hover, workspace symbols, completion, signature help에서 Rust 심볼을 처리하도록 했습니다.
13
+ - Rust `struct`, `enum`, `trait`, `type`, `const`, `static`, `fn`, `impl` method, trait method signature를 심볼로 추출하도록 했습니다.
14
+
5
15
  ## 1.0.0 - 2026-05-07
6
16
 
7
17
  - AST tree 조회와 LSP document symbols, definition, references, hover, workspace symbols, completion, signature help 도구를 포함한 1.0.0 기준 기능 구성을 확정했습니다.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # syntax-map-mcp
2
2
 
3
- Tree-sitter 기반 코드 분석 MCP 서버입니다. 지정한 `workspaceRoot` 아래의 JavaScript, TypeScript, TSX, Python 소스 파일을 읽고 심볼, 정의, 참조, 요약, tree-sitter query, 컨텍스트 markdown을 제공합니다.
3
+ Tree-sitter 기반 코드 분석 MCP 서버입니다. 지정한 `workspaceRoot` 아래의 JavaScript, TypeScript, TSX, Python, Rust 소스 파일을 읽고 심볼, 정의, 참조, 요약, tree-sitter query, 컨텍스트 markdown을 제공합니다.
4
4
 
5
5
  ## 설치와 빌드
6
6
 
@@ -37,6 +37,7 @@ npx -y syntax-map-mcp --workspace-root /path/to/workspace
37
37
  - `run_query`: 파일 하나에 tree-sitter query 실행
38
38
  - `get_ast_tree`: 파일 하나의 tree-sitter AST를 depth 제한 JSON 트리로 반환
39
39
  - `lsp_document_symbols`: 파일 하나의 심볼을 LSP DocumentSymbol 형태로 반환
40
+ - `lsp_diagnostics`: 파일 하나의 tree-sitter parse error를 LSP Diagnostic 형태로 반환
40
41
  - `lsp_definition`: 파일의 LSP 위치에 있는 식별자의 정의 위치를 반환
41
42
  - `lsp_references`: 파일의 LSP 위치에 있는 식별자의 참조 위치를 반환
42
43
  - `lsp_hover`: 파일의 LSP 위치에 있는 식별자의 hover markdown을 반환
@@ -53,7 +54,7 @@ npx -y syntax-map-mcp --workspace-root /path/to/workspace
53
54
 
54
55
  ## SQLite 인덱스
55
56
 
56
- `index_workspace`는 `workspaceRoot` 아래의 `.syntax-map-mcp/index.sqlite`에 인덱스를 저장합니다. 인덱싱 대상은 `.js`, `.jsx`, `.ts`, `.tsx`, `.py` 파일이며, `.git`, `.syntax-map-mcp`, `dist`, `node_modules` 디렉터리와 `workspaceRoot` 아래의 `.gitignore` 패턴에 매칭되는 파일은 제외합니다. 하위 디렉터리의 `.gitignore`는 해당 디렉터리 기준으로 적용합니다.
57
+ `index_workspace`는 `workspaceRoot` 아래의 `.syntax-map-mcp/index.sqlite`에 인덱스를 저장합니다. 인덱싱 대상은 `.js`, `.jsx`, `.ts`, `.tsx`, `.py`, `.rs` 파일이며, `.git`, `.syntax-map-mcp`, `dist`, `node_modules` 디렉터리와 `workspaceRoot` 아래의 `.gitignore` 패턴에 매칭되는 파일은 제외합니다. 하위 디렉터리의 `.gitignore`는 해당 디렉터리 기준으로 적용합니다.
57
58
 
58
59
  파일 변경 여부는 `mtimeMs`와 `size`로 판단합니다. 다시 `index_workspace`를 호출하면 변경된 파일만 재파싱하고, 삭제된 파일은 인덱스에서 제거합니다. `search_symbols`, `find_indexed_definition`, `find_indexed_references`는 `isStale`, `staleFiles`, `refreshed`를 반환해 검색 결과가 최신 인덱스 기반인지 알려줍니다. 세 도구에 `refreshIfStale: true`를 전달하면 stale 파일이 있을 때 먼저 인덱스를 갱신한 뒤 검색합니다. 인덱스 검색 도구는 인덱스에 저장된 위치를 조회한 뒤 현재 파일에서 해당 줄 snippet을 읽어 반환합니다. `contextBefore`, `contextAfter`를 0-10 사이 정수로 전달하면 snippet 주변 라인도 함께 반환합니다. `includePreview: true`를 전달하면 `path:line` 헤더와 코드블록으로 구성된 `previewMarkdown`도 함께 반환합니다. 자동 watch 모드는 아직 포함하지 않았습니다.
59
60
 
@@ -89,4 +90,4 @@ Python은 JavaScript/TypeScript처럼 명시적인 `export` 문법이 없으므
89
90
 
90
91
  ## 보안 경계
91
92
 
92
- 서버는 `workspaceRoot` 내부 파일만 읽습니다. 지원 확장자는 `.js`, `.jsx`, `.ts`, `.tsx`, `.py`뿐이며, workspace 밖으로 나가는 경로나 지원하지 않는 확장자는 오류로 처리합니다.
93
+ 서버는 `workspaceRoot` 내부 파일만 읽습니다. 지원 확장자는 `.js`, `.jsx`, `.ts`, `.tsx`, `.py`, `.rs`뿐이며, workspace 밖으로 나가는 경로나 지원하지 않는 확장자는 오류로 처리합니다.
@@ -0,0 +1,73 @@
1
+ import { parseSourceFile } from '../parser.js';
2
+ export const treeSitterDiagnosticProvider = {
3
+ name: 'tree-sitter',
4
+ getDiagnostics(context) {
5
+ return collectTreeSitterDiagnostics(context.tree.rootNode);
6
+ }
7
+ };
8
+ const defaultDiagnosticProviders = [treeSitterDiagnosticProvider];
9
+ export async function getDiagnostics(workspace, input, providers = defaultDiagnosticProviders) {
10
+ const file = await workspace.readSourceFile(input.path);
11
+ if (!file.ok)
12
+ return file;
13
+ const parsed = parseSourceFile(file);
14
+ /* v8 ignore next -- parser failures are covered by parser tests. */
15
+ if (!parsed.ok)
16
+ return parsed;
17
+ const context = diagnosticContext(parsed);
18
+ const diagnostics = (await Promise.all(providers.map(provider => Promise.resolve(provider.getDiagnostics(context))))).flat();
19
+ return {
20
+ ok: true,
21
+ path: file.relativePath,
22
+ language: parsed.language,
23
+ diagnostics
24
+ };
25
+ }
26
+ function diagnosticContext(parsed) {
27
+ return {
28
+ path: parsed.file.relativePath,
29
+ language: parsed.language,
30
+ text: parsed.file.text,
31
+ tree: parsed.tree
32
+ };
33
+ }
34
+ function collectTreeSitterDiagnostics(root) {
35
+ if (!root.hasError)
36
+ return [];
37
+ const diagnostics = [];
38
+ collectErrorNodes(root, diagnostics);
39
+ return diagnostics;
40
+ }
41
+ function collectErrorNodes(node, diagnostics) {
42
+ if (node.isError || node.isMissing) {
43
+ diagnostics.push({
44
+ range: lspRange(rangeForNode(node)),
45
+ severity: 1,
46
+ source: 'tree-sitter',
47
+ message: node.isMissing ? `Syntax error: missing ${node.type}` : 'Syntax error'
48
+ });
49
+ }
50
+ if (!node.hasError)
51
+ return;
52
+ for (const child of node.children) {
53
+ collectErrorNodes(child, diagnostics);
54
+ }
55
+ }
56
+ function lspRange(range) {
57
+ return {
58
+ start: {
59
+ line: range.start.row,
60
+ character: range.start.column
61
+ },
62
+ end: {
63
+ line: range.end.row,
64
+ character: range.end.column
65
+ }
66
+ };
67
+ }
68
+ function rangeForNode(node) {
69
+ return {
70
+ start: node.startPosition,
71
+ end: node.endPosition
72
+ };
73
+ }
@@ -123,7 +123,8 @@ function signatureFromSnippet(name, snippet) {
123
123
  const functionMatch = firstLine.match(/^(?:export\s+)?(?:async\s+)?function\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(([^)]*)\)\s*([^{:]*(?::\s*[^{]+)?)[{:]?/);
124
124
  const methodMatch = firstLine.match(/^(?:public\s+|private\s+|protected\s+|static\s+|async\s+)*[A-Za-z_$][A-Za-z0-9_$]*\s*\(([^)]*)\)\s*([^{:]*(?::\s*[^{]+)?)[{:]?/);
125
125
  const pythonMatch = firstLine.match(/^def\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(([^)]*)\)\s*([^:]*)/);
126
- const match = functionMatch ?? pythonMatch ?? methodMatch;
126
+ const rustMatch = firstLine.match(/^(?:pub\s+)?(?:async\s+)?fn\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(([^)]*)\)\s*([^{]*)/);
127
+ const match = functionMatch ?? pythonMatch ?? rustMatch ?? methodMatch;
127
128
  /* v8 ignore next 6 -- indexed function and method definitions provide recognizable signature snippets. */
128
129
  if (!match) {
129
130
  return {
@@ -34,6 +34,8 @@ export function referenceQueryForLanguage(language) {
34
34
  return '[(identifier) (type_identifier) (property_identifier)] @reference';
35
35
  case 'javascript':
36
36
  return '[(identifier) (property_identifier)] @reference';
37
+ case 'rust':
38
+ return '[(identifier) (type_identifier)] @reference';
37
39
  case 'python':
38
40
  default:
39
41
  return '(identifier) @reference';
@@ -43,6 +43,8 @@ function isImportNode(language, nodeType) {
43
43
  case 'typescript':
44
44
  case 'tsx':
45
45
  return nodeType === 'import_statement';
46
+ case 'rust':
47
+ return false;
46
48
  }
47
49
  }
48
50
  function findExports(parsed) {
@@ -61,6 +63,7 @@ function isExportNode(language, nodeType) {
61
63
  return nodeType === 'export_statement';
62
64
  /* v8 ignore next 2 -- Python exports are handled by findPythonAllExports before this switch. */
63
65
  case 'python':
66
+ case 'rust':
64
67
  return false;
65
68
  }
66
69
  }
@@ -21,6 +21,16 @@ const pythonSymbolPatterns = [
21
21
  { kind: 'function', query: '(function_definition name: (identifier) @name) @definition' },
22
22
  { kind: 'variable', query: '(module (expression_statement (assignment left: (identifier) @name) @definition))' }
23
23
  ];
24
+ const rustSymbolPatterns = [
25
+ { kind: 'class', query: '(struct_item name: (type_identifier) @name) @definition' },
26
+ { kind: 'class', query: '(enum_item name: (type_identifier) @name) @definition' },
27
+ { kind: 'interface', query: '(trait_item name: (type_identifier) @name) @definition' },
28
+ { kind: 'type', query: '(type_item name: (type_identifier) @name) @definition' },
29
+ { kind: 'function', query: '(function_item name: (identifier) @name) @definition' },
30
+ { kind: 'method', query: '(function_signature_item name: (identifier) @name) @definition' },
31
+ { kind: 'variable', query: '(const_item name: (identifier) @name) @definition' },
32
+ { kind: 'variable', query: '(static_item name: (identifier) @name) @definition' }
33
+ ];
24
34
  export function listSymbols(parsed) {
25
35
  return patternsForLanguage(parsed).flatMap(pattern => querySymbols(parsed, pattern));
26
36
  }
@@ -33,6 +43,8 @@ function patternsForLanguage(parsed) {
33
43
  return typeScriptSymbolPatterns;
34
44
  case 'python':
35
45
  return pythonSymbolPatterns;
46
+ case 'rust':
47
+ return rustSymbolPatterns;
36
48
  }
37
49
  }
38
50
  function querySymbols(parsed, pattern) {
@@ -51,6 +63,9 @@ function querySymbols(parsed, pattern) {
51
63
  !directJavaScriptLikeMethodClass(definition)) {
52
64
  return [];
53
65
  }
66
+ if (pattern.kind === 'method' && parsed.language === 'rust' && !directRustMethodParentName(definition)) {
67
+ return [];
68
+ }
54
69
  if (pattern.kind === 'method' && name.text === 'constructor')
55
70
  return [];
56
71
  const kind = symbolKind(parsed, pattern.kind, definition);
@@ -69,15 +84,20 @@ function symbolKind(parsed, kind, definition) {
69
84
  if (parsed.language === 'python' && kind === 'function' && isDirectPythonMethod(definition)) {
70
85
  return 'method';
71
86
  }
87
+ if (parsed.language === 'rust' && kind === 'function' && directRustMethodParentName(definition)) {
88
+ return 'method';
89
+ }
72
90
  return kind;
73
91
  }
74
92
  function parentNameForSymbol(parsed, kind, definition) {
75
93
  if (kind !== 'method')
76
94
  return undefined;
77
- const classNode = parsed.language === 'python'
78
- ? directPythonMethodClass(definition)
79
- : directJavaScriptLikeMethodClass(definition);
80
- return classNode?.childForFieldName('name')?.text;
95
+ const classNode = parsed.language === 'python' ? directPythonMethodClass(definition) : undefined;
96
+ if (classNode)
97
+ return classNode.childForFieldName('name')?.text;
98
+ if (parsed.language === 'rust')
99
+ return directRustMethodParentName(definition);
100
+ return directJavaScriptLikeMethodClass(definition)?.childForFieldName('name')?.text;
81
101
  }
82
102
  function isJavaScriptLikeLanguage(parsed) {
83
103
  /* v8 ignore next 5 -- language dispatch is covered by JS/TS/TSX/Python fixture tests. */
@@ -86,6 +106,8 @@ function isJavaScriptLikeLanguage(parsed) {
86
106
  parsed.language === 'tsx');
87
107
  }
88
108
  function isTopLevelVariableDefinition(definition) {
109
+ if (definition.parent?.type === 'source_file')
110
+ return true;
89
111
  const statement = definition.parent;
90
112
  /* v8 ignore next -- tree-sitter variable definitions always have a parent statement. */
91
113
  if (!statement)
@@ -117,6 +139,17 @@ function directJavaScriptLikeMethodClass(definition) {
117
139
  }
118
140
  return classNode;
119
141
  }
142
+ function directRustMethodParentName(definition) {
143
+ const declarationList = definition.parent;
144
+ const parentNode = declarationList?.parent;
145
+ if (declarationList?.type !== 'declaration_list')
146
+ return undefined;
147
+ if (parentNode?.type === 'trait_item')
148
+ return parentNode.childForFieldName('name')?.text;
149
+ if (parentNode?.type !== 'impl_item')
150
+ return undefined;
151
+ return parentNode.namedChildren.find(child => child.type === 'type_identifier')?.text;
152
+ }
120
153
  function rangeForNode(node) {
121
154
  return {
122
155
  start: node.startPosition,
package/dist/languages.js CHANGED
@@ -2,12 +2,14 @@ import { createRequire } from 'node:module';
2
2
  const require = createRequire(import.meta.url);
3
3
  const javascript = require('tree-sitter-javascript');
4
4
  const python = require('tree-sitter-python');
5
+ const rust = require('tree-sitter-rust');
5
6
  const typescript = require('tree-sitter-typescript');
6
7
  const languages = {
7
8
  javascript,
8
9
  typescript: typescript.typescript,
9
10
  tsx: typescript.tsx,
10
- python
11
+ python,
12
+ rust
11
13
  };
12
14
  export function languageForName(language) {
13
15
  return languages[language];
package/dist/parser.js CHANGED
@@ -13,6 +13,8 @@ export function detectLanguage(filePath) {
13
13
  return { ok: true, language: 'tsx' };
14
14
  case '.py':
15
15
  return { ok: true, language: 'python' };
16
+ case '.rs':
17
+ return { ok: true, language: 'rust' };
16
18
  default:
17
19
  return {
18
20
  ok: false,
package/dist/server.js CHANGED
@@ -21,7 +21,7 @@ export async function createServerInfo(packageJsonPath = defaultPackageJsonPath(
21
21
  export async function createServer(options) {
22
22
  const workspace = await createWorkspace(options.workspaceRoot);
23
23
  const server = new McpServer(await createServerInfo(), {
24
- instructions: 'Analyze JavaScript, TypeScript, and Python source files under the configured workspaceRoot only.'
24
+ instructions: 'Analyze JavaScript, TypeScript, Python, and Rust source files under the configured workspaceRoot only.'
25
25
  });
26
26
  registerTools(server, workspace);
27
27
  return server;
package/dist/tools.js CHANGED
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { getAstTree as getAstTreeAnalysis } from './analysis/ast-tree.js';
3
3
  import { buildContext as buildContextAnalysis } from './analysis/context.js';
4
4
  import { findDefinitions } from './analysis/definitions.js';
5
+ import { getDiagnostics as getLspDiagnostics } from './analysis/diagnostics.js';
5
6
  import { clearIndex as clearWorkspaceIndex, findIndexedDefinitions, findIndexedReferences, getIndexStatus as getWorkspaceIndexStatus, indexWorkspace as indexWorkspaceAnalysis, searchSymbols as searchIndexedSymbols } from './analysis/index.js';
6
7
  import { runTreeSitterQuery } from './analysis/query.js';
7
8
  import { findReferences as findReferencesAnalysis } from './analysis/references.js';
@@ -75,6 +76,12 @@ export function createToolHandlers(workspace) {
75
76
  return toolFailure(result.error.code, result.error.message);
76
77
  return jsonResult(result);
77
78
  },
79
+ async lspDiagnostics(input) {
80
+ const result = await getLspDiagnostics(workspace, input);
81
+ if (!result.ok)
82
+ return toolFailure(result.error.code, result.error.message);
83
+ return jsonResult(result);
84
+ },
78
85
  async lspDefinition(input) {
79
86
  const result = await getLspDefinition(workspace, input);
80
87
  if (!result.ok)
@@ -214,6 +221,13 @@ export function registerTools(server, workspace) {
214
221
  path: z.string()
215
222
  }
216
223
  }, handlers.lspDocumentSymbols);
224
+ server.registerTool('lsp_diagnostics', {
225
+ title: 'LSP diagnostics',
226
+ description: 'Return lightweight syntax diagnostics from tree-sitter parse errors.',
227
+ inputSchema: {
228
+ path: z.string()
229
+ }
230
+ }, handlers.lspDiagnostics);
217
231
  server.registerTool('lsp_definition', {
218
232
  title: 'LSP definition',
219
233
  description: 'Return definition locations for the identifier at a zero-based LSP position.',
package/dist/workspace.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, realpath, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import ignore from 'ignore';
4
- const SUPPORTED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.py']);
4
+ const SUPPORTED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.py', '.rs']);
5
5
  const EXCLUDED_DIRECTORIES = new Set(['.git', '.syntax-map-mcp', 'dist', 'node_modules']);
6
6
  function isInsideRoot(root, candidate) {
7
7
  const relative = path.relative(root, candidate);
package/docs/tools.md CHANGED
@@ -215,6 +215,37 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
215
215
  }
216
216
  ```
217
217
 
218
+ ## lsp_diagnostics
219
+
220
+ 입력:
221
+
222
+ ```json
223
+ {
224
+ "path": "src/users.ts"
225
+ }
226
+ ```
227
+
228
+ 응답 일부:
229
+
230
+ ```json
231
+ {
232
+ "ok": true,
233
+ "path": "src/users.ts",
234
+ "language": "typescript",
235
+ "diagnostics": [
236
+ {
237
+ "range": {
238
+ "start": { "line": 10, "character": 4 },
239
+ "end": { "line": 10, "character": 12 }
240
+ },
241
+ "severity": 1,
242
+ "source": "tree-sitter",
243
+ "message": "Syntax error"
244
+ }
245
+ ]
246
+ }
247
+ ```
248
+
218
249
  ## lsp_definition
219
250
 
220
251
  입력:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntax-map-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Tree-sitter based code analysis MCP server",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,6 +42,7 @@
42
42
  "tree-sitter": "^0.21.1",
43
43
  "tree-sitter-javascript": "^0.21.4",
44
44
  "tree-sitter-python": "^0.21.0",
45
+ "tree-sitter-rust": "^0.21.0",
45
46
  "tree-sitter-typescript": "^0.21.2",
46
47
  "zod": "^4.0.0"
47
48
  },