syntax-map-mcp 1.2.0 → 1.2.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.
- package/CHANGELOG.md +7 -0
- package/README.md +3 -1
- package/dist/analysis/index-watcher.js +80 -0
- package/dist/analysis/index.js +7 -4
- package/dist/analysis/lsp.js +2 -2
- package/dist/parser.js +2 -1
- package/dist/server.js +22 -3
- package/dist/tools.js +5 -3
- package/docs/tools.md +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.2.1 - 2026-05-07
|
|
6
|
+
|
|
7
|
+
- 큰 TypeScript 파일이 workspace 기본 `paths` 조회에 포함될 때 LSP definition, references, hover, workspace symbols, completion, signature help가 `Invalid argument`로 실패하던 문제를 수정했습니다.
|
|
8
|
+
- `lsp_signature_help`가 TypeScript 제네릭 함수의 파라미터를 추출하지 못하던 문제를 수정했습니다.
|
|
9
|
+
- `get_index_status`가 기본 응답에서 `staleReasons` 전체 배열을 생략하고, `includeStaleReasons: true`일 때만 상세 사유를 반환하도록 변경했습니다.
|
|
10
|
+
|
|
5
11
|
## 1.2.0 - 2026-05-07
|
|
6
12
|
|
|
13
|
+
- MCP 서버 실행 중 지원 소스 파일 변경을 감지해 SQLite 인덱스를 자동 갱신하는 watcher를 추가했습니다.
|
|
7
14
|
- `lsp_diagnostics` 도구를 추가해 tree-sitter parse error 기반의 가벼운 문법 진단을 LSP Diagnostic 형태로 반환하도록 했습니다.
|
|
8
15
|
- diagnostics 분석 계층에 provider 인터페이스를 추가해 이후 언어별 LSP 서버 진단 결과를 결합할 수 있게 했습니다.
|
|
9
16
|
|
package/README.md
CHANGED
|
@@ -52,11 +52,13 @@ npx -y syntax-map-mcp --workspace-root /path/to/workspace
|
|
|
52
52
|
- `get_index_status`: 인덱스 경로, 인덱싱된 파일 수, 심볼 수, 참조 수, stale 파일 수 반환
|
|
53
53
|
- `clear_index`: SQLite 인덱스 파일 삭제
|
|
54
54
|
|
|
55
|
+
`lsp_definition`, `lsp_references`, `lsp_hover`, `lsp_workspace_symbols`, `lsp_completion`, `lsp_signature_help`의 `paths` 입력은 선택 사항입니다. 생략하면 `workspaceRoot` 아래의 지원 소스 파일 전체를 대상으로 조회합니다.
|
|
56
|
+
|
|
55
57
|
## SQLite 인덱스
|
|
56
58
|
|
|
57
59
|
`index_workspace`는 `workspaceRoot` 아래의 `.syntax-map-mcp/index.sqlite`에 인덱스를 저장합니다. 인덱싱 대상은 `.js`, `.jsx`, `.ts`, `.tsx`, `.py`, `.rs` 파일이며, `.git`, `.syntax-map-mcp`, `dist`, `node_modules` 디렉터리와 `workspaceRoot` 아래의 `.gitignore` 패턴에 매칭되는 파일은 제외합니다. 하위 디렉터리의 `.gitignore`는 해당 디렉터리 기준으로 적용합니다.
|
|
58
60
|
|
|
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`도 함께 반환합니다.
|
|
61
|
+
파일 변경 여부는 `mtimeMs`와 `size`로 판단합니다. 다시 `index_workspace`를 호출하면 변경된 파일만 재파싱하고, 삭제된 파일은 인덱스에서 제거합니다. MCP 서버를 실행하면 지원 소스 파일 변경을 감지하는 watcher가 함께 시작되어 SQLite 인덱스를 자동 갱신합니다. `.syntax-map-mcp`, `.git`, `dist`, `node_modules`와 지원하지 않는 확장자 변경은 watcher refresh 대상에서 제외합니다. `get_index_status`는 기본적으로 stale 파일 개수만 반환하고, 상세 목록이 필요하면 `includeStaleReasons: true`를 전달합니다. `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`도 함께 반환합니다.
|
|
60
62
|
|
|
61
63
|
`summarize_file`은 `sources` 필드로 `symbols`, `imports`, `exports`가 어떤 방식으로 추출되었는지 반환합니다.
|
|
62
64
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { indexWorkspace } from './index.js';
|
|
4
|
+
const DEFAULT_DEBOUNCE_MS = 250;
|
|
5
|
+
const SUPPORTED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.py', '.rs']);
|
|
6
|
+
const EXCLUDED_DIRECTORIES = new Set(['.git', '.syntax-map-mcp', 'dist', 'node_modules']);
|
|
7
|
+
/* v8 ignore next -- default fs.watch wiring is exercised by CLI/manual MCP runs. */
|
|
8
|
+
const defaultWatchFactory = (root, options, listener) => watch(root, options, listener);
|
|
9
|
+
export function startIndexWatcher(workspace, options = {}) {
|
|
10
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
11
|
+
const refreshIndex = options.indexWorkspace ?? indexWorkspace;
|
|
12
|
+
const setTimer = options.setTimer ?? setTimeout;
|
|
13
|
+
const clearTimer = options.clearTimer ?? clearTimeout;
|
|
14
|
+
let stopped = false;
|
|
15
|
+
let timer;
|
|
16
|
+
let refreshChain = Promise.resolve();
|
|
17
|
+
function runRefresh() {
|
|
18
|
+
refreshChain = refreshChain
|
|
19
|
+
.then(async () => {
|
|
20
|
+
if (stopped)
|
|
21
|
+
return;
|
|
22
|
+
const result = await refreshIndex(workspace);
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
throw new Error(result.error.message);
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.catch(error => {
|
|
28
|
+
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
29
|
+
});
|
|
30
|
+
return refreshChain;
|
|
31
|
+
}
|
|
32
|
+
function scheduleRefresh(filename) {
|
|
33
|
+
if (stopped || !isRelevantWatchPath(filename))
|
|
34
|
+
return;
|
|
35
|
+
if (timer) {
|
|
36
|
+
clearTimer(timer);
|
|
37
|
+
}
|
|
38
|
+
timer = setTimer(() => {
|
|
39
|
+
timer = undefined;
|
|
40
|
+
void runRefresh();
|
|
41
|
+
}, debounceMs);
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
}
|
|
44
|
+
/* v8 ignore next -- tests inject watchFactory to avoid platform-specific fs.watch behavior. */
|
|
45
|
+
const watchFactory = options.watchFactory ??
|
|
46
|
+
/* v8 ignore next -- default fs.watch wiring is exercised by CLI/manual MCP runs. */
|
|
47
|
+
defaultWatchFactory;
|
|
48
|
+
const watcher = watchFactory(workspace.root, { recursive: true }, (_eventType, filename) => {
|
|
49
|
+
scheduleRefresh(filename);
|
|
50
|
+
});
|
|
51
|
+
watcher.unref?.();
|
|
52
|
+
return {
|
|
53
|
+
ready: runRefresh(),
|
|
54
|
+
async flush() {
|
|
55
|
+
if (timer) {
|
|
56
|
+
clearTimer(timer);
|
|
57
|
+
timer = undefined;
|
|
58
|
+
await runRefresh();
|
|
59
|
+
}
|
|
60
|
+
await refreshChain;
|
|
61
|
+
},
|
|
62
|
+
stop() {
|
|
63
|
+
stopped = true;
|
|
64
|
+
if (timer) {
|
|
65
|
+
clearTimer(timer);
|
|
66
|
+
timer = undefined;
|
|
67
|
+
}
|
|
68
|
+
watcher.close();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function isRelevantWatchPath(filename) {
|
|
73
|
+
if (filename === null)
|
|
74
|
+
return true;
|
|
75
|
+
const normalized = filename.toString().split(path.sep).join('/');
|
|
76
|
+
const parts = normalized.split('/');
|
|
77
|
+
if (parts.some(part => EXCLUDED_DIRECTORIES.has(part)))
|
|
78
|
+
return false;
|
|
79
|
+
return SUPPORTED_EXTENSIONS.has(path.extname(normalized));
|
|
80
|
+
}
|
package/dist/analysis/index.js
CHANGED
|
@@ -747,22 +747,25 @@ export async function searchSymbols(workspace, input) {
|
|
|
747
747
|
readState?.database.close();
|
|
748
748
|
}
|
|
749
749
|
}
|
|
750
|
-
export async function getIndexStatus(workspace) {
|
|
750
|
+
export async function getIndexStatus(workspace, input = {}) {
|
|
751
751
|
const indexPath = indexPathForWorkspace(workspace);
|
|
752
752
|
const { database } = await openCompatibleDatabase(indexPath);
|
|
753
753
|
try {
|
|
754
754
|
initSchema(database);
|
|
755
755
|
const staleReasons = await collectStaleReasons(workspace, database);
|
|
756
|
-
|
|
756
|
+
const result = {
|
|
757
757
|
ok: true,
|
|
758
758
|
indexPath,
|
|
759
759
|
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
760
760
|
indexedFiles: scalarCount(database, 'SELECT COUNT(*) FROM files WHERE parse_status = "ok"'),
|
|
761
761
|
symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols'),
|
|
762
762
|
references: scalarCount(database, 'SELECT COUNT(*) FROM reference_captures'),
|
|
763
|
-
staleFiles: staleReasons.length
|
|
764
|
-
staleReasons
|
|
763
|
+
staleFiles: staleReasons.length
|
|
765
764
|
};
|
|
765
|
+
if (input.includeStaleReasons === true) {
|
|
766
|
+
result.staleReasons = staleReasons;
|
|
767
|
+
}
|
|
768
|
+
return result;
|
|
766
769
|
/* v8 ignore next -- status read failures require a corrupted sqlite runtime path. */
|
|
767
770
|
}
|
|
768
771
|
catch (error) {
|
package/dist/analysis/lsp.js
CHANGED
|
@@ -120,8 +120,8 @@ function splitParameters(parameters) {
|
|
|
120
120
|
}
|
|
121
121
|
function signatureFromSnippet(name, snippet) {
|
|
122
122
|
const firstLine = snippet.split(/\r?\n/)[0].trim();
|
|
123
|
-
const functionMatch = firstLine.match(/^(?:export\s+)?(?:async\s+)?function\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(([^)]*)\)\s*([^{:]*(?::\s*[^{]+)?)[{:]?/);
|
|
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*[^{]+)?)[{:]?/);
|
|
123
|
+
const functionMatch = firstLine.match(/^(?:export\s+)?(?:async\s+)?function\s+[A-Za-z_$][A-Za-z0-9_$]*\s*(?:<[^>]+>)?\s*\(([^)]*)\)\s*([^{:]*(?::\s*[^{]+)?)[{:]?/);
|
|
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*([^{:]*(?::\s*[^{]+)?)[{:]?/);
|
|
125
125
|
const pythonMatch = firstLine.match(/^def\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(([^)]*)\)\s*([^:]*)/);
|
|
126
126
|
const rustMatch = firstLine.match(/^(?:pub\s+)?(?:async\s+)?fn\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(([^)]*)\)\s*([^{]*)/);
|
|
127
127
|
const match = functionMatch ?? pythonMatch ?? rustMatch ?? methodMatch;
|
package/dist/parser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import Parser from 'tree-sitter';
|
|
3
3
|
import { languageForName } from './languages.js';
|
|
4
|
+
const PARSE_CHUNK_SIZE = 4096;
|
|
4
5
|
export function detectLanguage(filePath) {
|
|
5
6
|
const extension = path.extname(filePath);
|
|
6
7
|
switch (extension) {
|
|
@@ -32,7 +33,7 @@ export function parseSourceFile(file, resolveLanguage = languageForName) {
|
|
|
32
33
|
try {
|
|
33
34
|
const parser = new Parser();
|
|
34
35
|
parser.setLanguage(resolveLanguage(detected.language));
|
|
35
|
-
const tree = parser.parse(file.text);
|
|
36
|
+
const tree = parser.parse(offset => file.text.slice(offset, offset + PARSE_CHUNK_SIZE));
|
|
36
37
|
return {
|
|
37
38
|
ok: true,
|
|
38
39
|
file,
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { startIndexWatcher } from './analysis/index-watcher.js';
|
|
6
7
|
import { registerTools } from './tools.js';
|
|
7
8
|
import { createWorkspace } from './workspace.js';
|
|
8
9
|
function defaultPackageJsonPath() {
|
|
@@ -18,17 +19,35 @@ export async function createServerInfo(packageJsonPath = defaultPackageJsonPath(
|
|
|
18
19
|
version: packageJson.version
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
export async function createServer(options) {
|
|
22
|
+
export async function createServer(options, dependencies = {}) {
|
|
22
23
|
const workspace = await createWorkspace(options.workspaceRoot);
|
|
23
24
|
const server = new McpServer(await createServerInfo(), {
|
|
24
25
|
instructions: 'Analyze JavaScript, TypeScript, Python, and Rust source files under the configured workspaceRoot only.'
|
|
25
26
|
});
|
|
26
27
|
registerTools(server, workspace);
|
|
28
|
+
if (options.autoIndex === true) {
|
|
29
|
+
attachIndexWatcher(server, (dependencies.startIndexWatcher ?? startIndexWatcher)(workspace, {
|
|
30
|
+
debounceMs: options.indexDebounceMs
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
27
33
|
return server;
|
|
28
34
|
}
|
|
29
35
|
export async function runServer(options, dependencies = {}) {
|
|
30
36
|
/* v8 ignore next 2 -- default CLI wiring is exercised by the package smoke test in a child process. */
|
|
31
|
-
const server = await (dependencies.createServer ?? createServer)(
|
|
32
|
-
|
|
37
|
+
const server = await (dependencies.createServer ?? createServer)({
|
|
38
|
+
...options,
|
|
39
|
+
autoIndex: options.autoIndex ?? true
|
|
40
|
+
});
|
|
41
|
+
const createTransport = dependencies.createTransport ??
|
|
42
|
+
/* v8 ignore next -- default stdio transport wiring is exercised by the package smoke test. */
|
|
43
|
+
(() => new StdioServerTransport());
|
|
44
|
+
const transport = createTransport();
|
|
33
45
|
await server.connect(transport);
|
|
34
46
|
}
|
|
47
|
+
function attachIndexWatcher(server, watcher) {
|
|
48
|
+
const closeServer = server.close.bind(server);
|
|
49
|
+
server.close = async () => {
|
|
50
|
+
watcher.stop();
|
|
51
|
+
await closeServer();
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -148,8 +148,8 @@ export function createToolHandlers(workspace) {
|
|
|
148
148
|
return toolFailure(result.error.code, result.error.message);
|
|
149
149
|
return jsonResult(result);
|
|
150
150
|
},
|
|
151
|
-
async getIndexStatus(
|
|
152
|
-
const result = await getWorkspaceIndexStatus(workspace);
|
|
151
|
+
async getIndexStatus(input) {
|
|
152
|
+
const result = await getWorkspaceIndexStatus(workspace, input);
|
|
153
153
|
/* v8 ignore next -- getIndexStatus read failures are defensive and covered at index layer. */
|
|
154
154
|
if (!result.ok)
|
|
155
155
|
return toolFailure(result.error.code, result.error.message);
|
|
@@ -365,7 +365,9 @@ export function registerTools(server, workspace) {
|
|
|
365
365
|
server.registerTool('get_index_status', {
|
|
366
366
|
title: 'Get index status',
|
|
367
367
|
description: 'Return SQLite index path, indexed file count, symbol count, and stale file count.',
|
|
368
|
-
inputSchema: {
|
|
368
|
+
inputSchema: {
|
|
369
|
+
includeStaleReasons: z.boolean().optional()
|
|
370
|
+
}
|
|
369
371
|
}, handlers.getIndexStatus);
|
|
370
372
|
server.registerTool('clear_index', {
|
|
371
373
|
title: 'Clear index',
|
package/docs/tools.md
CHANGED
|
@@ -504,7 +504,9 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
|
|
|
504
504
|
입력:
|
|
505
505
|
|
|
506
506
|
```json
|
|
507
|
-
{
|
|
507
|
+
{
|
|
508
|
+
"includeStaleReasons": true
|
|
509
|
+
}
|
|
508
510
|
```
|
|
509
511
|
|
|
510
512
|
응답 일부:
|
|
@@ -647,6 +649,8 @@ syntax-map-mcp의 주요 MCP 도구 입력과 응답 예시입니다. 응답 예
|
|
|
647
649
|
}
|
|
648
650
|
```
|
|
649
651
|
|
|
652
|
+
`includeStaleReasons`를 생략하거나 `false`로 전달하면 응답에는 `staleFiles` 개수만 포함됩니다.
|
|
653
|
+
|
|
650
654
|
## clear_index
|
|
651
655
|
|
|
652
656
|
입력:
|