syntax-map-mcp 0.1.7 → 0.1.9

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,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.9 - 2026-05-07
6
+
7
+ - 잘못된 인덱스 검색 옵션 오류 메시지에 실제 입력값을 포함하도록 개선했습니다.
8
+ - 인덱스 검색 옵션의 `limit`, `contextBefore`, `contextAfter` 범위를 분석 계층에서도 검증하도록 했습니다.
9
+ - 인덱스 저장 실패 시 tool failure 응답 shape를 검증하는 테스트를 추가했습니다.
10
+ - 주요 인덱스 도구의 `structuredContent` 응답 shape를 검증하는 테스트를 추가했습니다.
11
+ - `get_index_status` 응답에 stale 파일별 이유(`changed`, `missing`)를 반환하는 `staleReasons`를 추가했습니다.
12
+ - SQLite 인덱스 DB에 schema version metadata를 저장하고, 호환되지 않는 기존 인덱스는 자동 재생성하도록 했습니다.
13
+
14
+ ## 0.1.8 - 2026-05-07
15
+
16
+ - `docs/tools.md`의 도구 목록이 실제 MCP `listTools()` 응답과 일치하는지 검증하도록 했습니다.
17
+ - MCP `listTools()` 응답의 공개 tool 이름과 주요 input schema 필드를 검증하는 테스트를 추가했습니다.
18
+ - `release:check`가 npm 패키지를 tarball로 설치한 뒤 MCP 초기화 응답을 확인하는 smoke test를 실행하도록 했습니다.
19
+
5
20
  ## 0.1.7 - 2026-05-07
6
21
 
7
22
  - `release:check`가 npm 패키징 dry-run 결과의 필수 파일 포함 여부를 자동 검증하도록 했습니다.
@@ -7,6 +7,10 @@ import { runTreeSitterQuery } from './query.js';
7
7
  import { referenceQueryForLanguage } from './references.js';
8
8
  const INDEX_DIRECTORY = '.syntax-map-mcp';
9
9
  const INDEX_FILE = 'index.sqlite';
10
+ const INDEX_SCHEMA_VERSION = 1;
11
+ const DEFAULT_SEARCH_LIMIT = 50;
12
+ const MAX_SEARCH_LIMIT = 500;
13
+ const MAX_CONTEXT_LINES = 10;
10
14
  function indexPathForWorkspace(workspace) {
11
15
  return path.join(workspace.root, INDEX_DIRECTORY, INDEX_FILE);
12
16
  }
@@ -19,15 +23,49 @@ function failure(message) {
19
23
  }
20
24
  };
21
25
  }
26
+ async function createDatabase() {
27
+ const SQL = await initSqlJs();
28
+ return new SQL.Database();
29
+ }
22
30
  async function openDatabase(indexPath) {
23
31
  const SQL = await initSqlJs();
24
32
  try {
25
33
  const data = await readFile(indexPath);
26
- return new SQL.Database(data);
34
+ return { database: new SQL.Database(data), exists: true };
27
35
  }
28
36
  catch {
29
- return new SQL.Database();
37
+ return { database: new SQL.Database(), exists: false };
38
+ }
39
+ }
40
+ function tableExists(database, tableName) {
41
+ const result = database.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", [tableName])[0];
42
+ return result !== undefined && result.values.length > 0;
43
+ }
44
+ function storedSchemaVersion(database) {
45
+ if (!tableExists(database, 'metadata')) {
46
+ return undefined;
47
+ }
48
+ const result = database.exec('SELECT value FROM metadata WHERE key = ?', ['schema_version'])[0];
49
+ const value = result?.values[0]?.[0];
50
+ if (value === undefined || value === null) {
51
+ return undefined;
52
+ }
53
+ const version = Number(value);
54
+ return Number.isInteger(version) ? version : undefined;
55
+ }
56
+ function hasCurrentSchemaVersion(database) {
57
+ return storedSchemaVersion(database) === INDEX_SCHEMA_VERSION;
58
+ }
59
+ async function openCompatibleDatabase(indexPath) {
60
+ let { database, exists } = await openDatabase(indexPath);
61
+ let reset = false;
62
+ if (exists && !hasCurrentSchemaVersion(database)) {
63
+ database.close();
64
+ await rm(indexPath, { force: true });
65
+ database = await createDatabase();
66
+ reset = true;
30
67
  }
68
+ return { database, reset };
31
69
  }
32
70
  async function saveDatabase(database, indexPath) {
33
71
  await mkdir(path.dirname(indexPath), { recursive: true });
@@ -81,7 +119,17 @@ function initSchema(database) {
81
119
  CREATE INDEX IF NOT EXISTS symbols_name_idx ON symbols(name);
82
120
  CREATE INDEX IF NOT EXISTS symbols_kind_idx ON symbols(kind);
83
121
  CREATE INDEX IF NOT EXISTS reference_captures_name_idx ON reference_captures(name);
122
+
123
+ CREATE TABLE IF NOT EXISTS metadata (
124
+ key TEXT PRIMARY KEY,
125
+ value TEXT NOT NULL
126
+ );
84
127
  `);
128
+ database.run(`
129
+ INSERT INTO metadata (key, value)
130
+ VALUES ('schema_version', ?)
131
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
132
+ `, [String(INDEX_SCHEMA_VERSION)]);
85
133
  }
86
134
  function selectStoredFiles(database) {
87
135
  const rows = database.exec('SELECT path, size, mtime_ms, parse_status FROM files');
@@ -106,29 +154,38 @@ function isCurrent(stored, current) {
106
154
  stored.size === current.size &&
107
155
  stored.mtimeMs === current.mtimeMs);
108
156
  }
109
- async function countStaleFiles(workspace, database) {
157
+ async function collectStaleReasons(workspace, database) {
110
158
  const storedFiles = selectStoredFiles(database);
111
159
  const currentFiles = await workspace.listSourceFiles();
112
160
  const currentPaths = new Set(currentFiles.map(file => file.relativePath));
113
- let staleFiles = 0;
161
+ const staleReasons = [];
114
162
  for (const file of currentFiles) {
115
163
  if (!isCurrent(storedFiles.get(file.relativePath), file)) {
116
- staleFiles += 1;
164
+ staleReasons.push({
165
+ path: file.relativePath,
166
+ reason: 'changed'
167
+ });
117
168
  }
118
169
  }
119
170
  for (const storedPath of storedFiles.keys()) {
120
171
  if (!currentPaths.has(storedPath)) {
121
- staleFiles += 1;
172
+ staleReasons.push({
173
+ path: storedPath,
174
+ reason: 'missing'
175
+ });
122
176
  }
123
177
  }
124
- return staleFiles;
178
+ return staleReasons.sort((left, right) => left.path.localeCompare(right.path));
179
+ }
180
+ async function countStaleFiles(workspace, database) {
181
+ return (await collectStaleReasons(workspace, database)).length;
125
182
  }
126
183
  async function openIndexForRead(workspace, input) {
127
184
  const indexPath = indexPathForWorkspace(workspace);
128
- let database = await openDatabase(indexPath);
185
+ let { database, reset } = await openCompatibleDatabase(indexPath);
129
186
  initSchema(database);
130
187
  const initialStaleFiles = await countStaleFiles(workspace, database);
131
- if (!input.refreshIfStale || initialStaleFiles === 0) {
188
+ if (!reset && (!input.refreshIfStale || initialStaleFiles === 0)) {
132
189
  return {
133
190
  database,
134
191
  indexPath,
@@ -142,7 +199,7 @@ async function openIndexForRead(workspace, input) {
142
199
  if (!refreshedIndex.ok) {
143
200
  throw new Error(refreshedIndex.error.message);
144
201
  }
145
- database = await openDatabase(indexPath);
202
+ database = (await openCompatibleDatabase(indexPath)).database;
146
203
  initSchema(database);
147
204
  const staleFiles = await countStaleFiles(workspace, database);
148
205
  return {
@@ -253,6 +310,18 @@ function sqlLikePattern(query) {
253
310
  function rowValue(row, key) {
254
311
  return row[key];
255
312
  }
313
+ function assertIntegerRange(name, value, min, max) {
314
+ if (value === undefined)
315
+ return;
316
+ if (!Number.isInteger(value) || value < min || value > max) {
317
+ throw new Error(`${name} must be an integer between ${min} and ${max} (received ${String(value)})`);
318
+ }
319
+ }
320
+ function validateSearchOptions(input) {
321
+ assertIntegerRange('limit', input.limit, 1, MAX_SEARCH_LIMIT);
322
+ assertIntegerRange('contextBefore', input.contextBefore, 0, MAX_CONTEXT_LINES);
323
+ assertIntegerRange('contextAfter', input.contextAfter, 0, MAX_CONTEXT_LINES);
324
+ }
256
325
  function snippetDetails(path, language, text, row, options) {
257
326
  if (text === undefined)
258
327
  return { snippet: '' };
@@ -289,7 +358,7 @@ function previewMarkdown(filePath, language, row, snippet, context) {
289
358
  }
290
359
  export async function indexWorkspace(workspace) {
291
360
  const indexPath = indexPathForWorkspace(workspace);
292
- const database = await openDatabase(indexPath);
361
+ const { database } = await openCompatibleDatabase(indexPath);
293
362
  try {
294
363
  initSchema(database);
295
364
  const currentFiles = await workspace.listSourceFiles();
@@ -372,6 +441,7 @@ export async function indexWorkspace(workspace) {
372
441
  indexedFiles,
373
442
  skippedFiles,
374
443
  removedFiles,
444
+ schemaVersion: INDEX_SCHEMA_VERSION,
375
445
  symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols'),
376
446
  references: scalarCount(database, 'SELECT COUNT(*) FROM reference_captures')
377
447
  };
@@ -386,6 +456,7 @@ export async function indexWorkspace(workspace) {
386
456
  export async function findIndexedDefinitions(workspace, input) {
387
457
  let readState;
388
458
  try {
459
+ validateSearchOptions(input);
389
460
  readState = await openIndexForRead(workspace, input);
390
461
  const { database } = readState;
391
462
  const where = ['name = ?'];
@@ -394,7 +465,7 @@ export async function findIndexedDefinitions(workspace, input) {
394
465
  where.push(`kind IN (${input.kinds.map(() => '?').join(', ')})`);
395
466
  params.push(...input.kinds);
396
467
  }
397
- const limit = input.limit ?? 50;
468
+ const limit = input.limit ?? DEFAULT_SEARCH_LIMIT;
398
469
  params.push(limit);
399
470
  const statement = database.prepare(`
400
471
  SELECT
@@ -485,9 +556,10 @@ export async function findIndexedDefinitions(workspace, input) {
485
556
  export async function findIndexedReferences(workspace, input) {
486
557
  let readState;
487
558
  try {
559
+ validateSearchOptions(input);
488
560
  readState = await openIndexForRead(workspace, input);
489
561
  const { database } = readState;
490
- const limit = input.limit ?? 50;
562
+ const limit = input.limit ?? DEFAULT_SEARCH_LIMIT;
491
563
  const statement = database.prepare(`
492
564
  SELECT
493
565
  file_path,
@@ -552,6 +624,7 @@ export async function findIndexedReferences(workspace, input) {
552
624
  export async function searchSymbols(workspace, input) {
553
625
  let readState;
554
626
  try {
627
+ validateSearchOptions(input);
555
628
  readState = await openIndexForRead(workspace, input);
556
629
  const { database } = readState;
557
630
  const where = ['name LIKE ? ESCAPE "\\"'];
@@ -560,7 +633,7 @@ export async function searchSymbols(workspace, input) {
560
633
  where.push(`kind IN (${input.kinds.map(() => '?').join(', ')})`);
561
634
  params.push(...input.kinds);
562
635
  }
563
- const limit = input.limit ?? 50;
636
+ const limit = input.limit ?? DEFAULT_SEARCH_LIMIT;
564
637
  params.push(limit);
565
638
  const statement = database.prepare(`
566
639
  SELECT
@@ -650,17 +723,19 @@ export async function searchSymbols(workspace, input) {
650
723
  }
651
724
  export async function getIndexStatus(workspace) {
652
725
  const indexPath = indexPathForWorkspace(workspace);
653
- const database = await openDatabase(indexPath);
726
+ const { database } = await openCompatibleDatabase(indexPath);
654
727
  try {
655
728
  initSchema(database);
656
- const staleFiles = await countStaleFiles(workspace, database);
729
+ const staleReasons = await collectStaleReasons(workspace, database);
657
730
  return {
658
731
  ok: true,
659
732
  indexPath,
733
+ schemaVersion: INDEX_SCHEMA_VERSION,
660
734
  indexedFiles: scalarCount(database, 'SELECT COUNT(*) FROM files WHERE parse_status = "ok"'),
661
735
  symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols'),
662
736
  references: scalarCount(database, 'SELECT COUNT(*) FROM reference_captures'),
663
- staleFiles
737
+ staleFiles: staleReasons.length,
738
+ staleReasons
664
739
  };
665
740
  }
666
741
  catch (error) {
package/docs/tools.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예시는 핵심 필드만 보여줍니다.
4
4
 
5
- ## summarize_file
5
+ ## list_symbols
6
6
 
7
7
  입력:
8
8
 
@@ -19,28 +19,26 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
19
19
  "ok": true,
20
20
  "path": "src/index.ts",
21
21
  "language": "typescript",
22
- "imports": ["import { createServer } from './server.js';"],
23
- "exports": ["export async function main() {"],
24
- "sources": {
25
- "symbols": "ast",
26
- "imports": "ast",
27
- "exports": "ast"
28
- }
22
+ "symbols": [
23
+ {
24
+ "name": "main",
25
+ "kind": "function",
26
+ "line": 3,
27
+ "column": 1
28
+ }
29
+ ]
29
30
  }
30
31
  ```
31
32
 
32
- ## search_symbols
33
+ ## find_definition
33
34
 
34
35
  입력:
35
36
 
36
37
  ```json
37
38
  {
38
- "query": "UserService",
39
- "kinds": ["class"],
40
- "refreshIfStale": true,
41
- "contextBefore": 2,
42
- "contextAfter": 2,
43
- "includePreview": true
39
+ "name": "UserService",
40
+ "paths": ["src/users.ts", "src/index.ts"],
41
+ "kinds": ["class"]
44
42
  }
45
43
  ```
46
44
 
@@ -49,34 +47,24 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
49
47
  ```json
50
48
  {
51
49
  "ok": true,
52
- "isStale": false,
53
- "refreshed": true,
54
- "symbols": [
50
+ "definitions": [
55
51
  {
56
52
  "path": "src/users.ts",
57
53
  "name": "UserService",
58
- "kind": "class",
59
- "snippet": "export class UserService {",
60
- "context": {
61
- "before": ["export type UserId = User['id'];", ""],
62
- "after": [" constructor(private readonly users: User[]) {}", ""]
63
- },
64
- "previewMarkdown": "src/users.ts:8\n\n```typescript\nexport type UserId = User['id'];\n\nexport class UserService {\n constructor(private readonly users: User[]) {}\n\n```"
54
+ "kind": "class"
65
55
  }
66
56
  ]
67
57
  }
68
58
  ```
69
59
 
70
- ## find_indexed_definition
60
+ ## find_references
71
61
 
72
62
  입력:
73
63
 
74
64
  ```json
75
65
  {
76
- "name": "UserService",
77
- "refreshIfStale": true,
78
- "contextBefore": 1,
79
- "contextAfter": 1
66
+ "name": "formatUser",
67
+ "paths": ["src/users.ts", "src/index.ts"]
80
68
  }
81
69
  ```
82
70
 
@@ -85,27 +73,23 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
85
73
  ```json
86
74
  {
87
75
  "ok": true,
88
- "total": 1,
89
- "definitions": [
76
+ "references": [
90
77
  {
91
- "path": "src/users.ts",
92
- "name": "UserService",
93
- "kind": "class",
94
- "snippet": "export class UserService {"
78
+ "path": "src/index.ts",
79
+ "name": "formatUser",
80
+ "nodeType": "identifier"
95
81
  }
96
82
  ]
97
83
  }
98
84
  ```
99
85
 
100
- ## find_indexed_references
86
+ ## summarize_file
101
87
 
102
88
  입력:
103
89
 
104
90
  ```json
105
91
  {
106
- "name": "formatUser",
107
- "limit": 20,
108
- "refreshIfStale": true
92
+ "path": "src/index.ts"
109
93
  }
110
94
  ```
111
95
 
@@ -114,12 +98,43 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
114
98
  ```json
115
99
  {
116
100
  "ok": true,
117
- "references": [
101
+ "path": "src/index.ts",
102
+ "language": "typescript",
103
+ "imports": ["import { createServer } from './server.js';"],
104
+ "exports": ["export async function main() {"],
105
+ "sources": {
106
+ "symbols": "ast",
107
+ "imports": "ast",
108
+ "exports": "ast"
109
+ }
110
+ }
111
+ ```
112
+
113
+ ## run_query
114
+
115
+ 입력:
116
+
117
+ ```json
118
+ {
119
+ "path": "src/users.ts",
120
+ "query": "(class_declaration name: (type_identifier) @class.name)"
121
+ }
122
+ ```
123
+
124
+ 응답 일부:
125
+
126
+ ```json
127
+ {
128
+ "ok": true,
129
+ "matches": [
118
130
  {
119
- "path": "src/users.ts",
120
- "name": "formatUser",
121
- "nodeType": "identifier",
122
- "snippet": "formatUser(defaultUser);"
131
+ "pattern": 0,
132
+ "captures": [
133
+ {
134
+ "name": "class.name",
135
+ "text": "UserService"
136
+ }
137
+ ]
123
138
  }
124
139
  ]
125
140
  }
@@ -187,6 +202,123 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
187
202
  }
188
203
  ```
189
204
 
205
+ ## index_workspace
206
+
207
+ 입력:
208
+
209
+ ```json
210
+ {}
211
+ ```
212
+
213
+ 응답 일부:
214
+
215
+ ```json
216
+ {
217
+ "ok": true,
218
+ "schemaVersion": 1,
219
+ "indexedFiles": 12,
220
+ "symbols": 84,
221
+ "references": 231,
222
+ "indexPath": "/workspace/.syntax-map-mcp/index.sqlite"
223
+ }
224
+ ```
225
+
226
+ ## search_symbols
227
+
228
+ 입력:
229
+
230
+ ```json
231
+ {
232
+ "query": "UserService",
233
+ "kinds": ["class"],
234
+ "refreshIfStale": true,
235
+ "contextBefore": 2,
236
+ "contextAfter": 2,
237
+ "includePreview": true
238
+ }
239
+ ```
240
+
241
+ 응답 일부:
242
+
243
+ ```json
244
+ {
245
+ "ok": true,
246
+ "isStale": false,
247
+ "refreshed": true,
248
+ "symbols": [
249
+ {
250
+ "path": "src/users.ts",
251
+ "name": "UserService",
252
+ "kind": "class",
253
+ "snippet": "export class UserService {",
254
+ "context": {
255
+ "before": ["export type UserId = User['id'];", ""],
256
+ "after": [" constructor(private readonly users: User[]) {}", ""]
257
+ },
258
+ "previewMarkdown": "src/users.ts:8\n\n```typescript\nexport type UserId = User['id'];\n\nexport class UserService {\n constructor(private readonly users: User[]) {}\n\n```"
259
+ }
260
+ ]
261
+ }
262
+ ```
263
+
264
+ ## find_indexed_definition
265
+
266
+ 입력:
267
+
268
+ ```json
269
+ {
270
+ "name": "UserService",
271
+ "refreshIfStale": true,
272
+ "contextBefore": 1,
273
+ "contextAfter": 1
274
+ }
275
+ ```
276
+
277
+ 응답 일부:
278
+
279
+ ```json
280
+ {
281
+ "ok": true,
282
+ "total": 1,
283
+ "definitions": [
284
+ {
285
+ "path": "src/users.ts",
286
+ "name": "UserService",
287
+ "kind": "class",
288
+ "snippet": "export class UserService {"
289
+ }
290
+ ]
291
+ }
292
+ ```
293
+
294
+ ## find_indexed_references
295
+
296
+ 입력:
297
+
298
+ ```json
299
+ {
300
+ "name": "formatUser",
301
+ "limit": 20,
302
+ "refreshIfStale": true
303
+ }
304
+ ```
305
+
306
+ 응답 일부:
307
+
308
+ ```json
309
+ {
310
+ "ok": true,
311
+ "references": [
312
+ {
313
+ "path": "src/users.ts",
314
+ "name": "formatUser",
315
+ "nodeType": "identifier",
316
+ "snippet": "formatUser(defaultUser);"
317
+ }
318
+ ]
319
+ }
320
+ ```
321
+
190
322
  ## get_index_status
191
323
 
192
324
  입력:
@@ -200,9 +332,38 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
200
332
  ```json
201
333
  {
202
334
  "ok": true,
335
+ "schemaVersion": 1,
203
336
  "indexedFiles": 12,
204
337
  "symbols": 84,
205
338
  "references": 231,
206
- "staleFiles": 0
339
+ "staleFiles": 2,
340
+ "staleReasons": [
341
+ {
342
+ "path": "src/users.ts",
343
+ "reason": "changed"
344
+ },
345
+ {
346
+ "path": "src/old.ts",
347
+ "reason": "missing"
348
+ }
349
+ ]
350
+ }
351
+ ```
352
+
353
+ ## clear_index
354
+
355
+ 입력:
356
+
357
+ ```json
358
+ {}
359
+ ```
360
+
361
+ 응답 일부:
362
+
363
+ ```json
364
+ {
365
+ "ok": true,
366
+ "indexPath": "/workspace/.syntax-map-mcp/index.sqlite",
367
+ "deleted": true
207
368
  }
208
369
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntax-map-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Tree-sitter based code analysis MCP server",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "test": "vitest run",
32
32
  "test:watch": "vitest",
33
33
  "typecheck": "tsc -p tsconfig.json --noEmit",
34
- "release:check": "npm run typecheck && npm test && npm run build && node scripts/check-package-files.mjs"
34
+ "release:check": "npm run typecheck && npm test && npm run build && node scripts/check-package-files.mjs && node scripts/smoke-package-install.mjs"
35
35
  },
36
36
  "dependencies": {
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",