syntax-map-mcp 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.2 - 2026-05-04
6
+
7
+ - `summarize_file`의 `exports` 추출을 AST 기반으로 변경해 문자열 내부 텍스트가 export로 잘못 반환되지 않도록 했습니다.
8
+ - `summarize_file` 응답에 `sources`를 추가해 `symbols`, `imports`, `exports` 추출 방식이 AST 기반임을 확인할 수 있게 했습니다.
9
+ - 인덱스 검색 도구에 `contextBefore`, `contextAfter` 옵션을 추가해 snippet 주변 라인을 함께 조회할 수 있게 했습니다.
10
+ - 패키지 버전을 `0.1.2`로 올렸습니다.
11
+ - `search_symbols` 결과에 현재 파일의 해당 줄 `snippet`을 추가했습니다.
12
+ - `find_indexed_references`를 추가해 SQLite 인덱스에서 식별자 참조를 검색할 수 있게 했습니다.
13
+ - `search_symbols`, `find_indexed_definition`, `find_indexed_references`가 `isStale`, `staleFiles`, `refreshed`를 반환하도록 했습니다.
14
+ - 인덱스 검색 도구에 `refreshIfStale` 옵션을 추가했습니다.
15
+ - `index_workspace`와 `get_index_status` 응답에 참조 인덱스 개수 `references`를 추가했습니다.
16
+
17
+ ## 0.1.1 - 2026-05-04
18
+
19
+ - `find_indexed_definition`을 추가해 SQLite 인덱스에서 정확한 심볼 정의와 `snippet`을 조회할 수 있게 했습니다.
20
+ - README의 실행 예시와 MCP 설정 예시를 `npx -y syntax-map-mcp` 기반으로 변경했습니다.
21
+ - npm 페이지에서 GitHub 저장소가 노출되도록 `repository`, `homepage`, `bugs`, `license` 메타데이터를 추가했습니다.
22
+ - npm publish 경고를 피하도록 `bin` 경로를 `dist/cli.js` 형식으로 정규화했습니다.
23
+
24
+ ## 0.1.0 - 2026-05-04
25
+
26
+ - Tree-sitter 기반 MCP 서버 초기 버전을 배포했습니다.
27
+ - JavaScript, TypeScript, TSX, Python 파일의 심볼, 정의, 참조, 요약, tree-sitter query, 컨텍스트 생성을 지원했습니다.
28
+ - SQLite 기반 workspace 심볼 인덱스를 추가했습니다.
package/README.md CHANGED
@@ -19,7 +19,7 @@ npm run typecheck
19
19
  ## 실행
20
20
 
21
21
  ```bash
22
- node dist/cli.js --workspace-root /path/to/workspace
22
+ npx -y syntax-map-mcp --workspace-root /path/to/workspace
23
23
  ```
24
24
 
25
25
  `workspaceRoot` 결정 순서:
@@ -33,19 +33,27 @@ node dist/cli.js --workspace-root /path/to/workspace
33
33
  - `list_symbols`: 파일 하나의 top-level 심볼 목록 반환
34
34
  - `find_definition`: 여러 파일에서 이름과 선택적 kind로 정의 검색
35
35
  - `find_references`: 여러 파일에서 식별자 참조 검색
36
- - `summarize_file`: 파일 언어, 라인 수, imports, exports, symbols 요약
36
+ - `summarize_file`: 파일 언어, 라인 수, AST 기반 imports, exports, symbols 요약
37
37
  - `run_query`: 파일 하나에 tree-sitter query 실행
38
38
  - `build_context`: 여러 파일 요약을 markdown 컨텍스트로 구성
39
- - `index_workspace`: 지원 소스 파일을 파싱해 SQLite 심볼 인덱스 생성 또는 갱신
40
- - `search_symbols`: SQLite 인덱스에서 심볼 이름 검색
41
- - `get_index_status`: 인덱스 경로, 인덱싱된 파일 수, 심볼 수, stale 파일 반환
39
+ - `index_workspace`: 지원 소스 파일을 파싱해 SQLite 심볼/참조 인덱스 생성 또는 갱신
40
+ - `search_symbols`: SQLite 인덱스에서 심볼 이름 검색 및 snippet 반환
41
+ - `find_indexed_definition`: SQLite 인덱스에서 정확한 심볼 정의 검색 snippet 반환
42
+ - `find_indexed_references`: SQLite 인덱스에서 식별자 참조 검색 및 snippet 반환
43
+ - `get_index_status`: 인덱스 경로, 인덱싱된 파일 수, 심볼 수, 참조 수, stale 파일 수 반환
42
44
  - `clear_index`: SQLite 인덱스 파일 삭제
43
45
 
44
46
  ## SQLite 인덱스
45
47
 
46
48
  `index_workspace`는 `workspaceRoot` 아래의 `.syntax-map-mcp/index.sqlite`에 인덱스를 저장합니다. 인덱싱 대상은 `.js`, `.jsx`, `.ts`, `.tsx`, `.py` 파일이며, `.git`, `.syntax-map-mcp`, `dist`, `node_modules` 디렉터리는 제외합니다.
47
49
 
48
- 파일 변경 여부는 `mtimeMs`와 `size`로 판단합니다. 다시 `index_workspace`를 호출하면 변경된 파일만 재파싱하고, 삭제된 파일은 인덱스에서 제거합니다. 자동 watch 모드는 아직 포함하지 않았습니다.
50
+ 파일 변경 여부는 `mtimeMs`와 `size`로 판단합니다. 다시 `index_workspace`를 호출하면 변경된 파일만 재파싱하고, 삭제된 파일은 인덱스에서 제거합니다. `search_symbols`, `find_indexed_definition`, `find_indexed_references`는 `isStale`, `staleFiles`, `refreshed`를 반환해 검색 결과가 최신 인덱스 기반인지 알려줍니다. 세 도구에 `refreshIfStale: true`를 전달하면 stale 파일이 있을 때 먼저 인덱스를 갱신한 뒤 검색합니다. 인덱스 검색 도구는 인덱스에 저장된 위치를 조회한 뒤 현재 파일에서 해당 줄 snippet을 읽어 반환합니다. `contextBefore`, `contextAfter`를 0-10 사이 정수로 전달하면 snippet 주변 라인도 함께 반환합니다. 자동 watch 모드는 아직 포함하지 않았습니다.
51
+
52
+ `summarize_file`은 `sources` 필드로 `symbols`, `imports`, `exports`가 어떤 방식으로 추출되었는지 반환합니다.
53
+
54
+ ## 변경 이력
55
+
56
+ 버전별 변경 내용은 [CHANGELOG.md](./CHANGELOG.md)를 참고하세요.
49
57
 
50
58
  ## MCP 설정 예시
51
59
 
@@ -53,9 +61,10 @@ node dist/cli.js --workspace-root /path/to/workspace
53
61
  {
54
62
  "mcpServers": {
55
63
  "syntax-map-mcp": {
56
- "command": "node",
64
+ "command": "npx",
57
65
  "args": [
58
- "/Users/hantaekim/my-project/tree-sitter/dist/cli.js",
66
+ "-y",
67
+ "syntax-map-mcp",
59
68
  "--workspace-root",
60
69
  "/path/to/workspace"
61
70
  ]
@@ -3,6 +3,8 @@ import path from 'node:path';
3
3
  import initSqlJs from 'sql.js';
4
4
  import { listSymbols } from './symbols.js';
5
5
  import { parseSourceFile } from '../parser.js';
6
+ import { runTreeSitterQuery } from './query.js';
7
+ import { referenceQueryForLanguage } from './references.js';
6
8
  const INDEX_DIRECTORY = '.syntax-map-mcp';
7
9
  const INDEX_FILE = 'index.sqlite';
8
10
  function indexPathForWorkspace(workspace) {
@@ -63,8 +65,22 @@ function initSchema(database) {
63
65
  FOREIGN KEY(file_path) REFERENCES files(path) ON DELETE CASCADE
64
66
  );
65
67
 
68
+ CREATE TABLE IF NOT EXISTS reference_captures (
69
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
70
+ file_path TEXT NOT NULL,
71
+ language TEXT NOT NULL,
72
+ name TEXT NOT NULL,
73
+ node_type TEXT NOT NULL,
74
+ start_row INTEGER NOT NULL,
75
+ start_column INTEGER NOT NULL,
76
+ end_row INTEGER NOT NULL,
77
+ end_column INTEGER NOT NULL,
78
+ FOREIGN KEY(file_path) REFERENCES files(path) ON DELETE CASCADE
79
+ );
80
+
66
81
  CREATE INDEX IF NOT EXISTS symbols_name_idx ON symbols(name);
67
82
  CREATE INDEX IF NOT EXISTS symbols_kind_idx ON symbols(kind);
83
+ CREATE INDEX IF NOT EXISTS reference_captures_name_idx ON reference_captures(name);
68
84
  `);
69
85
  }
70
86
  function selectStoredFiles(database) {
@@ -90,6 +106,53 @@ function isCurrent(stored, current) {
90
106
  stored.size === current.size &&
91
107
  stored.mtimeMs === current.mtimeMs);
92
108
  }
109
+ async function countStaleFiles(workspace, database) {
110
+ const storedFiles = selectStoredFiles(database);
111
+ const currentFiles = await workspace.listSourceFiles();
112
+ const currentPaths = new Set(currentFiles.map(file => file.relativePath));
113
+ let staleFiles = 0;
114
+ for (const file of currentFiles) {
115
+ if (!isCurrent(storedFiles.get(file.relativePath), file)) {
116
+ staleFiles += 1;
117
+ }
118
+ }
119
+ for (const storedPath of storedFiles.keys()) {
120
+ if (!currentPaths.has(storedPath)) {
121
+ staleFiles += 1;
122
+ }
123
+ }
124
+ return staleFiles;
125
+ }
126
+ async function openIndexForRead(workspace, input) {
127
+ const indexPath = indexPathForWorkspace(workspace);
128
+ let database = await openDatabase(indexPath);
129
+ initSchema(database);
130
+ const initialStaleFiles = await countStaleFiles(workspace, database);
131
+ if (!input.refreshIfStale || initialStaleFiles === 0) {
132
+ return {
133
+ database,
134
+ indexPath,
135
+ isStale: initialStaleFiles > 0,
136
+ staleFiles: initialStaleFiles,
137
+ refreshed: false
138
+ };
139
+ }
140
+ database.close();
141
+ const refreshedIndex = await indexWorkspace(workspace);
142
+ if (!refreshedIndex.ok) {
143
+ throw new Error(refreshedIndex.error.message);
144
+ }
145
+ database = await openDatabase(indexPath);
146
+ initSchema(database);
147
+ const staleFiles = await countStaleFiles(workspace, database);
148
+ return {
149
+ database,
150
+ indexPath,
151
+ isStale: staleFiles > 0,
152
+ staleFiles,
153
+ refreshed: true
154
+ };
155
+ }
93
156
  function upsertFile(database, input) {
94
157
  database.run(`
95
158
  INSERT INTO files (path, language, size, mtime_ms, parse_status, error_message, indexed_at)
@@ -145,12 +208,39 @@ function insertSymbol(database, input) {
145
208
  input.symbol.selectionRange?.end.column ?? null
146
209
  ]);
147
210
  }
211
+ function insertReference(database, input) {
212
+ database.run(`
213
+ INSERT INTO reference_captures (
214
+ file_path,
215
+ language,
216
+ name,
217
+ node_type,
218
+ start_row,
219
+ start_column,
220
+ end_row,
221
+ end_column
222
+ )
223
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
224
+ `, [
225
+ input.filePath,
226
+ input.language,
227
+ input.name,
228
+ input.nodeType,
229
+ input.range.start.row,
230
+ input.range.start.column,
231
+ input.range.end.row,
232
+ input.range.end.column
233
+ ]);
234
+ }
148
235
  function deleteFile(database, filePath) {
149
236
  database.run('DELETE FROM files WHERE path = ?', [filePath]);
150
237
  }
151
238
  function deleteSymbolsForFile(database, filePath) {
152
239
  database.run('DELETE FROM symbols WHERE file_path = ?', [filePath]);
153
240
  }
241
+ function deleteReferencesForFile(database, filePath) {
242
+ database.run('DELETE FROM reference_captures WHERE file_path = ?', [filePath]);
243
+ }
154
244
  function scalarCount(database, sql) {
155
245
  const result = database.exec(sql)[0];
156
246
  if (!result)
@@ -163,6 +253,24 @@ function sqlLikePattern(query) {
163
253
  function rowValue(row, key) {
164
254
  return row[key];
165
255
  }
256
+ function snippetDetails(text, row, options) {
257
+ if (text === undefined)
258
+ return { snippet: '' };
259
+ const lines = text.split(/\r?\n/);
260
+ const snippet = lines[row] ?? '';
261
+ const beforeCount = options.contextBefore ?? 0;
262
+ const afterCount = options.contextAfter ?? 0;
263
+ if (beforeCount === 0 && afterCount === 0) {
264
+ return { snippet };
265
+ }
266
+ return {
267
+ snippet,
268
+ context: {
269
+ before: lines.slice(Math.max(0, row - beforeCount), row),
270
+ after: lines.slice(row + 1, row + 1 + afterCount)
271
+ }
272
+ };
273
+ }
166
274
  export async function indexWorkspace(workspace) {
167
275
  const indexPath = indexPathForWorkspace(workspace);
168
276
  const database = await openDatabase(indexPath);
@@ -194,6 +302,7 @@ export async function indexWorkspace(workspace) {
194
302
  errorMessage: file.error.message
195
303
  });
196
304
  deleteSymbolsForFile(database, fileInfo.relativePath);
305
+ deleteReferencesForFile(database, fileInfo.relativePath);
197
306
  indexedFiles += 1;
198
307
  continue;
199
308
  }
@@ -206,6 +315,7 @@ export async function indexWorkspace(workspace) {
206
315
  errorMessage: parsed.error.message
207
316
  });
208
317
  deleteSymbolsForFile(database, fileInfo.relativePath);
318
+ deleteReferencesForFile(database, fileInfo.relativePath);
209
319
  indexedFiles += 1;
210
320
  continue;
211
321
  }
@@ -216,6 +326,7 @@ export async function indexWorkspace(workspace) {
216
326
  errorMessage: null
217
327
  });
218
328
  deleteSymbolsForFile(database, fileInfo.relativePath);
329
+ deleteReferencesForFile(database, fileInfo.relativePath);
219
330
  for (const symbol of listSymbols(parsed)) {
220
331
  insertSymbol(database, {
221
332
  filePath: fileInfo.relativePath,
@@ -223,6 +334,19 @@ export async function indexWorkspace(workspace) {
223
334
  symbol
224
335
  });
225
336
  }
337
+ const references = runTreeSitterQuery(parsed, referenceQueryForLanguage(parsed.language));
338
+ if (!references.ok) {
339
+ throw new Error(references.error.message);
340
+ }
341
+ for (const reference of references.captures) {
342
+ insertReference(database, {
343
+ filePath: fileInfo.relativePath,
344
+ language: parsed.language,
345
+ name: reference.text,
346
+ nodeType: reference.nodeType,
347
+ range: reference.range
348
+ });
349
+ }
226
350
  indexedFiles += 1;
227
351
  }
228
352
  await saveDatabase(database, indexPath);
@@ -232,7 +356,8 @@ export async function indexWorkspace(workspace) {
232
356
  indexedFiles,
233
357
  skippedFiles,
234
358
  removedFiles,
235
- symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols')
359
+ symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols'),
360
+ references: scalarCount(database, 'SELECT COUNT(*) FROM reference_captures')
236
361
  };
237
362
  }
238
363
  catch (error) {
@@ -242,11 +367,177 @@ export async function indexWorkspace(workspace) {
242
367
  database.close();
243
368
  }
244
369
  }
370
+ export async function findIndexedDefinitions(workspace, input) {
371
+ let readState;
372
+ try {
373
+ readState = await openIndexForRead(workspace, input);
374
+ const { database } = readState;
375
+ const where = ['name = ?'];
376
+ const params = [input.name];
377
+ if (input.kinds && input.kinds.length > 0) {
378
+ where.push(`kind IN (${input.kinds.map(() => '?').join(', ')})`);
379
+ params.push(...input.kinds);
380
+ }
381
+ const limit = input.limit ?? 50;
382
+ params.push(limit);
383
+ const statement = database.prepare(`
384
+ SELECT
385
+ file_path,
386
+ language,
387
+ name,
388
+ kind,
389
+ parent_name,
390
+ start_row,
391
+ start_column,
392
+ end_row,
393
+ end_column,
394
+ selection_start_row,
395
+ selection_start_column,
396
+ selection_end_row,
397
+ selection_end_column
398
+ FROM symbols
399
+ WHERE ${where.join(' AND ')}
400
+ ORDER BY file_path ASC, start_row ASC, name ASC
401
+ LIMIT ?
402
+ `, params);
403
+ const definitions = [];
404
+ try {
405
+ while (statement.step()) {
406
+ const row = statement.getAsObject();
407
+ const selectionStartRow = rowValue(row, 'selection_start_row');
408
+ const selectionStartColumn = rowValue(row, 'selection_start_column');
409
+ const selectionEndRow = rowValue(row, 'selection_end_row');
410
+ const selectionEndColumn = rowValue(row, 'selection_end_column');
411
+ const filePath = String(rowValue(row, 'file_path'));
412
+ const startRow = Number(rowValue(row, 'start_row'));
413
+ const file = await workspace.readSourceFile(filePath);
414
+ definitions.push({
415
+ path: filePath,
416
+ language: rowValue(row, 'language'),
417
+ name: String(rowValue(row, 'name')),
418
+ kind: rowValue(row, 'kind'),
419
+ parentName: rowValue(row, 'parent_name') === null ? undefined : String(rowValue(row, 'parent_name')),
420
+ range: {
421
+ start: {
422
+ row: startRow,
423
+ column: Number(rowValue(row, 'start_column'))
424
+ },
425
+ end: {
426
+ row: Number(rowValue(row, 'end_row')),
427
+ column: Number(rowValue(row, 'end_column'))
428
+ }
429
+ },
430
+ selectionRange: selectionStartRow === null ||
431
+ selectionStartColumn === null ||
432
+ selectionEndRow === null ||
433
+ selectionEndColumn === null
434
+ ? undefined
435
+ : {
436
+ start: {
437
+ row: Number(selectionStartRow),
438
+ column: Number(selectionStartColumn)
439
+ },
440
+ end: {
441
+ row: Number(selectionEndRow),
442
+ column: Number(selectionEndColumn)
443
+ }
444
+ },
445
+ ...snippetDetails(file.ok ? file.text : undefined, startRow, input)
446
+ });
447
+ }
448
+ }
449
+ finally {
450
+ statement.free();
451
+ }
452
+ return {
453
+ ok: true,
454
+ indexPath: readState.indexPath,
455
+ isStale: readState.isStale,
456
+ staleFiles: readState.staleFiles,
457
+ refreshed: readState.refreshed,
458
+ total: definitions.length,
459
+ definitions
460
+ };
461
+ }
462
+ catch (error) {
463
+ return failure(error instanceof Error ? error.message : String(error));
464
+ }
465
+ finally {
466
+ readState?.database.close();
467
+ }
468
+ }
469
+ export async function findIndexedReferences(workspace, input) {
470
+ let readState;
471
+ try {
472
+ readState = await openIndexForRead(workspace, input);
473
+ const { database } = readState;
474
+ const limit = input.limit ?? 50;
475
+ const statement = database.prepare(`
476
+ SELECT
477
+ file_path,
478
+ language,
479
+ name,
480
+ node_type,
481
+ start_row,
482
+ start_column,
483
+ end_row,
484
+ end_column
485
+ FROM reference_captures
486
+ WHERE name = ?
487
+ ORDER BY file_path ASC, start_row ASC
488
+ LIMIT ?
489
+ `, [input.name, limit]);
490
+ const references = [];
491
+ try {
492
+ while (statement.step()) {
493
+ const row = statement.getAsObject();
494
+ const filePath = String(rowValue(row, 'file_path'));
495
+ const startRow = Number(rowValue(row, 'start_row'));
496
+ const file = await workspace.readSourceFile(filePath);
497
+ references.push({
498
+ path: filePath,
499
+ language: rowValue(row, 'language'),
500
+ name: String(rowValue(row, 'name')),
501
+ nodeType: String(rowValue(row, 'node_type')),
502
+ range: {
503
+ start: {
504
+ row: startRow,
505
+ column: Number(rowValue(row, 'start_column'))
506
+ },
507
+ end: {
508
+ row: Number(rowValue(row, 'end_row')),
509
+ column: Number(rowValue(row, 'end_column'))
510
+ }
511
+ },
512
+ ...snippetDetails(file.ok ? file.text : undefined, startRow, input)
513
+ });
514
+ }
515
+ }
516
+ finally {
517
+ statement.free();
518
+ }
519
+ return {
520
+ ok: true,
521
+ indexPath: readState.indexPath,
522
+ isStale: readState.isStale,
523
+ staleFiles: readState.staleFiles,
524
+ refreshed: readState.refreshed,
525
+ total: references.length,
526
+ references
527
+ };
528
+ }
529
+ catch (error) {
530
+ return failure(error instanceof Error ? error.message : String(error));
531
+ }
532
+ finally {
533
+ readState?.database.close();
534
+ }
535
+ }
245
536
  export async function searchSymbols(workspace, input) {
246
- const indexPath = indexPathForWorkspace(workspace);
247
- const database = await openDatabase(indexPath);
537
+ let readState;
248
538
  try {
249
- initSchema(database);
539
+ readState = await openIndexForRead(workspace, input);
540
+ const { database } = readState;
250
541
  const where = ['name LIKE ? ESCAPE "\\"'];
251
542
  const params = [sqlLikePattern(input.query)];
252
543
  if (input.kinds && input.kinds.length > 0) {
@@ -283,15 +574,18 @@ export async function searchSymbols(workspace, input) {
283
574
  const selectionStartColumn = rowValue(row, 'selection_start_column');
284
575
  const selectionEndRow = rowValue(row, 'selection_end_row');
285
576
  const selectionEndColumn = rowValue(row, 'selection_end_column');
577
+ const filePath = String(rowValue(row, 'file_path'));
578
+ const startRow = Number(rowValue(row, 'start_row'));
579
+ const file = await workspace.readSourceFile(filePath);
286
580
  symbols.push({
287
- path: String(rowValue(row, 'file_path')),
581
+ path: filePath,
288
582
  language: rowValue(row, 'language'),
289
583
  name: String(rowValue(row, 'name')),
290
584
  kind: rowValue(row, 'kind'),
291
585
  parentName: rowValue(row, 'parent_name') === null ? undefined : String(rowValue(row, 'parent_name')),
292
586
  range: {
293
587
  start: {
294
- row: Number(rowValue(row, 'start_row')),
588
+ row: startRow,
295
589
  column: Number(rowValue(row, 'start_column'))
296
590
  },
297
591
  end: {
@@ -313,7 +607,8 @@ export async function searchSymbols(workspace, input) {
313
607
  row: Number(selectionEndRow),
314
608
  column: Number(selectionEndColumn)
315
609
  }
316
- }
610
+ },
611
+ ...snippetDetails(file.ok ? file.text : undefined, startRow, input)
317
612
  });
318
613
  }
319
614
  }
@@ -322,7 +617,10 @@ export async function searchSymbols(workspace, input) {
322
617
  }
323
618
  return {
324
619
  ok: true,
325
- indexPath,
620
+ indexPath: readState.indexPath,
621
+ isStale: readState.isStale,
622
+ staleFiles: readState.staleFiles,
623
+ refreshed: readState.refreshed,
326
624
  total: symbols.length,
327
625
  symbols
328
626
  };
@@ -331,7 +629,7 @@ export async function searchSymbols(workspace, input) {
331
629
  return failure(error instanceof Error ? error.message : String(error));
332
630
  }
333
631
  finally {
334
- database.close();
632
+ readState?.database.close();
335
633
  }
336
634
  }
337
635
  export async function getIndexStatus(workspace) {
@@ -339,24 +637,13 @@ export async function getIndexStatus(workspace) {
339
637
  const database = await openDatabase(indexPath);
340
638
  try {
341
639
  initSchema(database);
342
- const storedFiles = selectStoredFiles(database);
343
- const currentFiles = await workspace.listSourceFiles();
344
- let staleFiles = 0;
345
- for (const file of currentFiles) {
346
- if (!isCurrent(storedFiles.get(file.relativePath), file)) {
347
- staleFiles += 1;
348
- }
349
- }
350
- for (const storedPath of storedFiles.keys()) {
351
- if (!currentFiles.some(file => file.relativePath === storedPath)) {
352
- staleFiles += 1;
353
- }
354
- }
640
+ const staleFiles = await countStaleFiles(workspace, database);
355
641
  return {
356
642
  ok: true,
357
643
  indexPath,
358
644
  indexedFiles: scalarCount(database, 'SELECT COUNT(*) FROM files WHERE parse_status = "ok"'),
359
645
  symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols'),
646
+ references: scalarCount(database, 'SELECT COUNT(*) FROM reference_captures'),
360
647
  staleFiles
361
648
  };
362
649
  }
@@ -24,7 +24,7 @@ export async function findReferences(workspace, input) {
24
24
  }
25
25
  return { ok: true, references };
26
26
  }
27
- function referenceQueryForLanguage(language) {
27
+ export function referenceQueryForLanguage(language) {
28
28
  switch (language) {
29
29
  case 'typescript':
30
30
  case 'tsx':
@@ -13,8 +13,13 @@ export async function summarizeFile(workspace, filePath) {
13
13
  language: parsed.language,
14
14
  lineCount: countLines(file.text),
15
15
  symbols: listSymbols(parsed),
16
- imports: findImports(file.text),
17
- exports: findExports(file.text)
16
+ imports: findImports(parsed),
17
+ exports: findExports(parsed),
18
+ sources: {
19
+ symbols: 'ast',
20
+ imports: 'ast',
21
+ exports: 'ast'
22
+ }
18
23
  };
19
24
  }
20
25
  function countLines(text) {
@@ -22,15 +27,36 @@ function countLines(text) {
22
27
  return 0;
23
28
  return text.replace(/\r\n|\r|\n$/, '').split(/\r\n|\r|\n/).length;
24
29
  }
25
- function findImports(text) {
26
- return trimmedLines(text).filter(line => line.startsWith('import ') || line.startsWith('from '));
30
+ function findImports(parsed) {
31
+ return parsed.tree.rootNode.namedChildren
32
+ .filter(node => isImportNode(parsed.language, node.type))
33
+ .map(node => firstLine(node.text));
27
34
  }
28
- function findExports(text) {
29
- return trimmedLines(text).filter(line => line.startsWith('export '));
35
+ function isImportNode(language, nodeType) {
36
+ switch (language) {
37
+ case 'python':
38
+ return nodeType === 'import_statement' || nodeType === 'import_from_statement';
39
+ case 'javascript':
40
+ case 'typescript':
41
+ case 'tsx':
42
+ return nodeType === 'import_statement';
43
+ }
30
44
  }
31
- function trimmedLines(text) {
32
- return text
33
- .split(/\r\n|\r|\n/)
34
- .map(line => line.trim())
35
- .filter(Boolean);
45
+ function findExports(parsed) {
46
+ return parsed.tree.rootNode.namedChildren
47
+ .filter(node => isExportNode(parsed.language, node.type))
48
+ .map(node => firstLine(node.text));
49
+ }
50
+ function isExportNode(language, nodeType) {
51
+ switch (language) {
52
+ case 'javascript':
53
+ case 'typescript':
54
+ case 'tsx':
55
+ return nodeType === 'export_statement';
56
+ case 'python':
57
+ return false;
58
+ }
59
+ }
60
+ function firstLine(text) {
61
+ return text.split(/\r\n|\r|\n/, 1)[0].trim();
36
62
  }
package/dist/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { buildContext as buildContextAnalysis } from './analysis/context.js';
3
3
  import { findDefinitions } from './analysis/definitions.js';
4
- import { clearIndex as clearWorkspaceIndex, getIndexStatus as getWorkspaceIndexStatus, indexWorkspace as indexWorkspaceAnalysis, searchSymbols as searchIndexedSymbols } from './analysis/index.js';
4
+ import { clearIndex as clearWorkspaceIndex, findIndexedDefinitions, findIndexedReferences, getIndexStatus as getWorkspaceIndexStatus, indexWorkspace as indexWorkspaceAnalysis, searchSymbols as searchIndexedSymbols } from './analysis/index.js';
5
5
  import { runTreeSitterQuery } from './analysis/query.js';
6
6
  import { findReferences as findReferencesAnalysis } from './analysis/references.js';
7
7
  import { summarizeFile as summarizeFileAnalysis } from './analysis/summary.js';
@@ -10,6 +10,7 @@ import { parseSourceFile } from './parser.js';
10
10
  import { jsonResult, toolFailure } from './result.js';
11
11
  const symbolKindSchema = z.enum(['function', 'method', 'class', 'variable', 'interface', 'type']);
12
12
  const detailSchema = z.enum(['compact', 'full']);
13
+ const contextLineCountSchema = z.number().int().min(0).max(10);
13
14
  export function createToolHandlers(workspace) {
14
15
  return {
15
16
  async listSymbols(input) {
@@ -74,6 +75,18 @@ export function createToolHandlers(workspace) {
74
75
  return toolFailure(result.error.code, result.error.message);
75
76
  return jsonResult(result);
76
77
  },
78
+ async findIndexedDefinition(input) {
79
+ const result = await findIndexedDefinitions(workspace, input);
80
+ if (!result.ok)
81
+ return toolFailure(result.error.code, result.error.message);
82
+ return jsonResult(result);
83
+ },
84
+ async findIndexedReferences(input) {
85
+ const result = await findIndexedReferences(workspace, input);
86
+ if (!result.ok)
87
+ return toolFailure(result.error.code, result.error.message);
88
+ return jsonResult(result);
89
+ },
77
90
  async getIndexStatus(_input) {
78
91
  const result = await getWorkspaceIndexStatus(workspace);
79
92
  if (!result.ok)
@@ -148,9 +161,35 @@ export function registerTools(server, workspace) {
148
161
  inputSchema: {
149
162
  query: z.string(),
150
163
  kinds: z.array(symbolKindSchema).optional(),
151
- limit: z.number().int().positive().max(500).optional()
164
+ limit: z.number().int().positive().max(500).optional(),
165
+ refreshIfStale: z.boolean().optional(),
166
+ contextBefore: contextLineCountSchema.optional(),
167
+ contextAfter: contextLineCountSchema.optional()
152
168
  }
153
169
  }, handlers.searchSymbols);
170
+ server.registerTool('find_indexed_definition', {
171
+ title: 'Find indexed definition',
172
+ description: 'Find exact symbol definitions from the SQLite workspace index with snippets.',
173
+ inputSchema: {
174
+ name: z.string(),
175
+ kinds: z.array(symbolKindSchema).optional(),
176
+ limit: z.number().int().positive().max(500).optional(),
177
+ refreshIfStale: z.boolean().optional(),
178
+ contextBefore: contextLineCountSchema.optional(),
179
+ contextAfter: contextLineCountSchema.optional()
180
+ }
181
+ }, handlers.findIndexedDefinition);
182
+ server.registerTool('find_indexed_references', {
183
+ title: 'Find indexed references',
184
+ description: 'Find identifier references from the SQLite workspace index with snippets.',
185
+ inputSchema: {
186
+ name: z.string(),
187
+ limit: z.number().int().positive().max(500).optional(),
188
+ refreshIfStale: z.boolean().optional(),
189
+ contextBefore: contextLineCountSchema.optional(),
190
+ contextAfter: contextLineCountSchema.optional()
191
+ }
192
+ }, handlers.findIndexedReferences);
154
193
  server.registerTool('get_index_status', {
155
194
  title: 'Get index status',
156
195
  description: 'Return SQLite index path, indexed file count, symbol count, and stale file count.',
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "syntax-map-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Tree-sitter based code analysis MCP server",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "bin": {
7
- "syntax-map-mcp": "./dist/cli.js"
8
+ "syntax-map-mcp": "dist/cli.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/kht6163/syntax-map-mcp.git"
13
+ },
14
+ "homepage": "https://github.com/kht6163/syntax-map-mcp#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/kht6163/syntax-map-mcp/issues"
8
17
  },
9
18
  "files": [
10
19
  "dist",
11
20
  "README.md",
21
+ "CHANGELOG.md",
12
22
  "LICENSE"
13
23
  ],
14
24
  "publishConfig": {