syntax-map-mcp 0.1.8 → 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 +9 -0
- package/dist/analysis/index.js +92 -17
- package/docs/tools.md +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
|
|
5
14
|
## 0.1.8 - 2026-05-07
|
|
6
15
|
|
|
7
16
|
- `docs/tools.md`의 도구 목록이 실제 MCP `listTools()` 응답과 일치하는지 검증하도록 했습니다.
|
package/dist/analysis/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
161
|
+
const staleReasons = [];
|
|
114
162
|
for (const file of currentFiles) {
|
|
115
163
|
if (!isCurrent(storedFiles.get(file.relativePath), file)) {
|
|
116
|
-
|
|
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
|
-
|
|
172
|
+
staleReasons.push({
|
|
173
|
+
path: storedPath,
|
|
174
|
+
reason: 'missing'
|
|
175
|
+
});
|
|
122
176
|
}
|
|
123
177
|
}
|
|
124
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
|
726
|
+
const { database } = await openCompatibleDatabase(indexPath);
|
|
654
727
|
try {
|
|
655
728
|
initSchema(database);
|
|
656
|
-
const
|
|
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
|
@@ -215,6 +215,7 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
|
|
|
215
215
|
```json
|
|
216
216
|
{
|
|
217
217
|
"ok": true,
|
|
218
|
+
"schemaVersion": 1,
|
|
218
219
|
"indexedFiles": 12,
|
|
219
220
|
"symbols": 84,
|
|
220
221
|
"references": 231,
|
|
@@ -331,10 +332,21 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
|
|
|
331
332
|
```json
|
|
332
333
|
{
|
|
333
334
|
"ok": true,
|
|
335
|
+
"schemaVersion": 1,
|
|
334
336
|
"indexedFiles": 12,
|
|
335
337
|
"symbols": 84,
|
|
336
338
|
"references": 231,
|
|
337
|
-
"staleFiles":
|
|
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
|
+
]
|
|
338
350
|
}
|
|
339
351
|
```
|
|
340
352
|
|