syntax-map-mcp 0.1.2 → 0.1.3

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
+ ## 0.1.3 - 2026-05-04
6
+
7
+ - README에 `summarize_file`, 인덱스 검색 도구, `get_index_status`의 입력/응답 예시를 추가했습니다.
8
+ - Python 파일의 `summarize_file.exports`가 top-level `__all__`을 기준으로 반환되는 동작을 문서화했습니다.
9
+ - 배포 전 `typecheck`, 테스트, 빌드, 패키징 dry-run을 한 번에 실행하는 `release:check` 스크립트를 추가했습니다.
10
+ - Python 파일의 top-level `__all__` 문자열 이름을 `summarize_file.exports`로 반환하도록 했습니다.
11
+ - 인덱스 검색 도구에 `includePreview` 옵션을 추가해 `previewMarkdown`을 반환할 수 있게 했습니다.
12
+ - `build_context`가 `indexedSearch` 입력으로 인덱스 검색 결과 기반 markdown 컨텍스트를 만들 수 있게 했습니다.
13
+ - 패키지 버전을 `0.1.3`으로 올렸습니다.
14
+
5
15
  ## 0.1.2 - 2026-05-04
6
16
 
7
17
  - `summarize_file`의 `exports` 추출을 AST 기반으로 변경해 문자열 내부 텍스트가 export로 잘못 반환되지 않도록 했습니다.
package/README.md CHANGED
@@ -47,10 +47,192 @@ npx -y syntax-map-mcp --workspace-root /path/to/workspace
47
47
 
48
48
  `index_workspace`는 `workspaceRoot` 아래의 `.syntax-map-mcp/index.sqlite`에 인덱스를 저장합니다. 인덱싱 대상은 `.js`, `.jsx`, `.ts`, `.tsx`, `.py` 파일이며, `.git`, `.syntax-map-mcp`, `dist`, `node_modules` 디렉터리는 제외합니다.
49
49
 
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 모드는 아직 포함하지 않았습니다.
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 주변 라인도 함께 반환합니다. `includePreview: true`를 전달하면 `path:line` 헤더와 코드블록으로 구성된 `previewMarkdown`도 함께 반환합니다. 자동 watch 모드는 아직 포함하지 않았습니다.
51
51
 
52
52
  `summarize_file`은 `sources` 필드로 `symbols`, `imports`, `exports`가 어떤 방식으로 추출되었는지 반환합니다.
53
53
 
54
+ Python은 JavaScript/TypeScript처럼 명시적인 `export` 문법이 없으므로, top-level `__all__`에 선언된 문자열 이름을 `exports`로 반환합니다. `__all__`이 없으면 `exports`는 빈 배열입니다.
55
+
56
+ ## 도구 사용 예시
57
+
58
+ ### summarize_file
59
+
60
+ 입력:
61
+
62
+ ```json
63
+ {
64
+ "path": "src/index.ts"
65
+ }
66
+ ```
67
+
68
+ 응답 일부:
69
+
70
+ ```json
71
+ {
72
+ "ok": true,
73
+ "path": "src/index.ts",
74
+ "language": "typescript",
75
+ "imports": ["import { createServer } from './server.js';"],
76
+ "exports": ["export async function main() {"],
77
+ "sources": {
78
+ "symbols": "ast",
79
+ "imports": "ast",
80
+ "exports": "ast"
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### search_symbols
86
+
87
+ 입력:
88
+
89
+ ```json
90
+ {
91
+ "query": "UserService",
92
+ "kinds": ["class"],
93
+ "refreshIfStale": true,
94
+ "contextBefore": 2,
95
+ "contextAfter": 2,
96
+ "includePreview": true
97
+ }
98
+ ```
99
+
100
+ 응답 일부:
101
+
102
+ ```json
103
+ {
104
+ "ok": true,
105
+ "isStale": false,
106
+ "refreshed": true,
107
+ "symbols": [
108
+ {
109
+ "path": "src/users.ts",
110
+ "name": "UserService",
111
+ "kind": "class",
112
+ "snippet": "export class UserService {",
113
+ "context": {
114
+ "before": ["export type UserId = User['id'];", ""],
115
+ "after": [" constructor(private readonly users: User[]) {}", ""]
116
+ },
117
+ "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```"
118
+ }
119
+ ]
120
+ }
121
+ ```
122
+
123
+ ### find_indexed_definition
124
+
125
+ 입력:
126
+
127
+ ```json
128
+ {
129
+ "name": "UserService",
130
+ "refreshIfStale": true,
131
+ "contextBefore": 1,
132
+ "contextAfter": 1
133
+ }
134
+ ```
135
+
136
+ 응답 일부:
137
+
138
+ ```json
139
+ {
140
+ "ok": true,
141
+ "total": 1,
142
+ "definitions": [
143
+ {
144
+ "path": "src/users.ts",
145
+ "name": "UserService",
146
+ "kind": "class",
147
+ "snippet": "export class UserService {"
148
+ }
149
+ ]
150
+ }
151
+ ```
152
+
153
+ ### find_indexed_references
154
+
155
+ 입력:
156
+
157
+ ```json
158
+ {
159
+ "name": "formatUser",
160
+ "limit": 20,
161
+ "refreshIfStale": true
162
+ }
163
+ ```
164
+
165
+ 응답 일부:
166
+
167
+ ```json
168
+ {
169
+ "ok": true,
170
+ "references": [
171
+ {
172
+ "path": "src/users.ts",
173
+ "name": "formatUser",
174
+ "nodeType": "identifier",
175
+ "snippet": "formatUser(defaultUser);"
176
+ }
177
+ ]
178
+ }
179
+ ```
180
+
181
+ ### build_context
182
+
183
+ 파일 경로 기반 입력:
184
+
185
+ ```json
186
+ {
187
+ "paths": ["src/users.ts", "src/index.ts"],
188
+ "detail": "compact"
189
+ }
190
+ ```
191
+
192
+ 인덱스 검색 기반 입력:
193
+
194
+ ```json
195
+ {
196
+ "detail": "compact",
197
+ "indexedSearch": {
198
+ "query": "UserService",
199
+ "kinds": ["class"],
200
+ "refreshIfStale": true,
201
+ "contextBefore": 1,
202
+ "contextAfter": 1
203
+ }
204
+ }
205
+ ```
206
+
207
+ 응답 일부:
208
+
209
+ ```json
210
+ {
211
+ "ok": true,
212
+ "markdown": "# Code Context\n\n## Indexed Search Results\n\n### UserService\n\nsrc/users.ts:8\n\n```typescript\nexport class UserService {\n```"
213
+ }
214
+ ```
215
+
216
+ ### get_index_status
217
+
218
+ 입력:
219
+
220
+ ```json
221
+ {}
222
+ ```
223
+
224
+ 응답 일부:
225
+
226
+ ```json
227
+ {
228
+ "ok": true,
229
+ "indexedFiles": 12,
230
+ "symbols": 84,
231
+ "references": 231,
232
+ "staleFiles": 0
233
+ }
234
+ ```
235
+
54
236
  ## 변경 이력
55
237
 
56
238
  버전별 변경 내용은 [CHANGELOG.md](./CHANGELOG.md)를 참고하세요.
@@ -1,5 +1,15 @@
1
+ import { searchSymbols } from './index.js';
1
2
  import { summarizeFile } from './summary.js';
2
3
  export async function buildContext(workspace, input) {
4
+ if (input.indexedSearch) {
5
+ return buildIndexedSearchContext(workspace, {
6
+ detail: input.detail,
7
+ indexedSearch: input.indexedSearch
8
+ });
9
+ }
10
+ if (!input.paths) {
11
+ return failure('Either paths or indexedSearch must be provided');
12
+ }
3
13
  const summaries = [];
4
14
  for (const filePath of input.paths) {
5
15
  const summary = await summarizeFile(workspace, filePath);
@@ -12,8 +22,50 @@ export async function buildContext(workspace, input) {
12
22
  markdown: renderMarkdown(summaries, input.detail)
13
23
  };
14
24
  }
15
- function renderMarkdown(summaries, detail) {
16
- return ['# Code Context', ...summaries.map(summary => renderFile(summary, detail))].join('\n\n');
25
+ async function buildIndexedSearchContext(workspace, input) {
26
+ const search = await searchSymbols(workspace, {
27
+ ...input.indexedSearch,
28
+ includePreview: true
29
+ });
30
+ if (!search.ok)
31
+ return search;
32
+ const paths = [...new Set(search.symbols.map(symbol => symbol.path))];
33
+ const summaries = [];
34
+ for (const filePath of paths) {
35
+ const summary = await summarizeFile(workspace, filePath);
36
+ if (!summary.ok)
37
+ return summary;
38
+ summaries.push(summary);
39
+ }
40
+ return {
41
+ ok: true,
42
+ markdown: renderMarkdown(summaries, input.detail, renderIndexedSearchResults(search.symbols))
43
+ };
44
+ }
45
+ function renderMarkdown(summaries, detail, intro) {
46
+ return ['# Code Context', intro, ...summaries.map(summary => renderFile(summary, detail))]
47
+ .filter(Boolean)
48
+ .join('\n\n');
49
+ }
50
+ function renderIndexedSearchResults(symbols) {
51
+ const lines = ['## Indexed Search Results'];
52
+ if (symbols.length === 0) {
53
+ lines.push('', '- None');
54
+ return lines.join('\n');
55
+ }
56
+ for (const symbol of symbols) {
57
+ lines.push('', `### ${symbol.name}`, '', symbol.previewMarkdown ?? symbol.path);
58
+ }
59
+ return lines.join('\n');
60
+ }
61
+ function failure(message) {
62
+ return {
63
+ ok: false,
64
+ error: {
65
+ code: 'INDEX_ERROR',
66
+ message
67
+ }
68
+ };
17
69
  }
18
70
  function renderFile(summary, detail) {
19
71
  const lines = [
@@ -253,23 +253,39 @@ function sqlLikePattern(query) {
253
253
  function rowValue(row, key) {
254
254
  return row[key];
255
255
  }
256
- function snippetDetails(text, row, options) {
256
+ function snippetDetails(path, language, text, row, options) {
257
257
  if (text === undefined)
258
258
  return { snippet: '' };
259
259
  const lines = text.split(/\r?\n/);
260
260
  const snippet = lines[row] ?? '';
261
261
  const beforeCount = options.contextBefore ?? 0;
262
262
  const afterCount = options.contextAfter ?? 0;
263
- if (beforeCount === 0 && afterCount === 0) {
264
- return { snippet };
265
- }
266
- return {
267
- snippet,
268
- context: {
263
+ const context = beforeCount === 0 && afterCount === 0
264
+ ? undefined
265
+ : {
269
266
  before: lines.slice(Math.max(0, row - beforeCount), row),
270
267
  after: lines.slice(row + 1, row + 1 + afterCount)
271
- }
268
+ };
269
+ const details = {
270
+ snippet,
272
271
  };
272
+ if (context)
273
+ details.context = context;
274
+ if (options.includePreview) {
275
+ details.previewMarkdown = previewMarkdown(path, language, row, snippet, context);
276
+ }
277
+ return details;
278
+ }
279
+ function previewMarkdown(filePath, language, row, snippet, context) {
280
+ return [
281
+ `${filePath}:${row + 1}`,
282
+ '',
283
+ `\`\`\`${language}`,
284
+ ...(context?.before ?? []),
285
+ snippet,
286
+ ...(context?.after ?? []),
287
+ '```'
288
+ ].join('\n');
273
289
  }
274
290
  export async function indexWorkspace(workspace) {
275
291
  const indexPath = indexPathForWorkspace(workspace);
@@ -442,7 +458,7 @@ export async function findIndexedDefinitions(workspace, input) {
442
458
  column: Number(selectionEndColumn)
443
459
  }
444
460
  },
445
- ...snippetDetails(file.ok ? file.text : undefined, startRow, input)
461
+ ...snippetDetails(filePath, rowValue(row, 'language'), file.ok ? file.text : undefined, startRow, input)
446
462
  });
447
463
  }
448
464
  }
@@ -509,7 +525,7 @@ export async function findIndexedReferences(workspace, input) {
509
525
  column: Number(rowValue(row, 'end_column'))
510
526
  }
511
527
  },
512
- ...snippetDetails(file.ok ? file.text : undefined, startRow, input)
528
+ ...snippetDetails(filePath, rowValue(row, 'language'), file.ok ? file.text : undefined, startRow, input)
513
529
  });
514
530
  }
515
531
  }
@@ -608,7 +624,7 @@ export async function searchSymbols(workspace, input) {
608
624
  column: Number(selectionEndColumn)
609
625
  }
610
626
  },
611
- ...snippetDetails(file.ok ? file.text : undefined, startRow, input)
627
+ ...snippetDetails(filePath, rowValue(row, 'language'), file.ok ? file.text : undefined, startRow, input)
612
628
  });
613
629
  }
614
630
  }
@@ -43,6 +43,9 @@ function isImportNode(language, nodeType) {
43
43
  }
44
44
  }
45
45
  function findExports(parsed) {
46
+ if (parsed.language === 'python') {
47
+ return findPythonAllExports(parsed);
48
+ }
46
49
  return parsed.tree.rootNode.namedChildren
47
50
  .filter(node => isExportNode(parsed.language, node.type))
48
51
  .map(node => firstLine(node.text));
@@ -57,6 +60,25 @@ function isExportNode(language, nodeType) {
57
60
  return false;
58
61
  }
59
62
  }
63
+ function findPythonAllExports(parsed) {
64
+ return parsed.tree.rootNode.namedChildren.flatMap(node => pythonAllExportNames(node));
65
+ }
66
+ function pythonAllExportNames(node) {
67
+ if (node.type !== 'expression_statement')
68
+ return [];
69
+ const assignment = node.namedChildren[0];
70
+ if (!assignment || assignment.type !== 'assignment')
71
+ return [];
72
+ const [target, value] = assignment.namedChildren;
73
+ if (!target || !value || target.type !== 'identifier' || target.text !== '__all__')
74
+ return [];
75
+ if (value.type !== 'list' && value.type !== 'tuple')
76
+ return [];
77
+ return value.namedChildren
78
+ .filter(child => child.type === 'string')
79
+ .map(child => child.namedChildren.find(part => part.type === 'string_content')?.text ?? '')
80
+ .filter(Boolean);
81
+ }
60
82
  function firstLine(text) {
61
83
  return text.split(/\r\n|\r|\n/, 1)[0].trim();
62
84
  }
package/dist/tools.js CHANGED
@@ -146,8 +146,18 @@ export function registerTools(server, workspace) {
146
146
  title: 'Build context',
147
147
  description: 'Build markdown context for supported source files.',
148
148
  inputSchema: {
149
- paths: z.array(z.string()),
150
- detail: detailSchema
149
+ paths: z.array(z.string()).optional(),
150
+ detail: detailSchema,
151
+ indexedSearch: z
152
+ .object({
153
+ query: z.string(),
154
+ kinds: z.array(symbolKindSchema).optional(),
155
+ limit: z.number().int().positive().max(500).optional(),
156
+ refreshIfStale: z.boolean().optional(),
157
+ contextBefore: contextLineCountSchema.optional(),
158
+ contextAfter: contextLineCountSchema.optional()
159
+ })
160
+ .optional()
151
161
  }
152
162
  }, handlers.buildContext);
153
163
  server.registerTool('index_workspace', {
@@ -164,7 +174,8 @@ export function registerTools(server, workspace) {
164
174
  limit: z.number().int().positive().max(500).optional(),
165
175
  refreshIfStale: z.boolean().optional(),
166
176
  contextBefore: contextLineCountSchema.optional(),
167
- contextAfter: contextLineCountSchema.optional()
177
+ contextAfter: contextLineCountSchema.optional(),
178
+ includePreview: z.boolean().optional()
168
179
  }
169
180
  }, handlers.searchSymbols);
170
181
  server.registerTool('find_indexed_definition', {
@@ -176,7 +187,8 @@ export function registerTools(server, workspace) {
176
187
  limit: z.number().int().positive().max(500).optional(),
177
188
  refreshIfStale: z.boolean().optional(),
178
189
  contextBefore: contextLineCountSchema.optional(),
179
- contextAfter: contextLineCountSchema.optional()
190
+ contextAfter: contextLineCountSchema.optional(),
191
+ includePreview: z.boolean().optional()
180
192
  }
181
193
  }, handlers.findIndexedDefinition);
182
194
  server.registerTool('find_indexed_references', {
@@ -187,7 +199,8 @@ export function registerTools(server, workspace) {
187
199
  limit: z.number().int().positive().max(500).optional(),
188
200
  refreshIfStale: z.boolean().optional(),
189
201
  contextBefore: contextLineCountSchema.optional(),
190
- contextAfter: contextLineCountSchema.optional()
202
+ contextAfter: contextLineCountSchema.optional(),
203
+ includePreview: z.boolean().optional()
191
204
  }
192
205
  }, handlers.findIndexedReferences);
193
206
  server.registerTool('get_index_status', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntax-map-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Tree-sitter based code analysis MCP server",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,8 @@
29
29
  "build": "tsc -p tsconfig.json",
30
30
  "test": "vitest run",
31
31
  "test:watch": "vitest",
32
- "typecheck": "tsc -p tsconfig.json --noEmit"
32
+ "typecheck": "tsc -p tsconfig.json --noEmit",
33
+ "release:check": "npm run typecheck && npm test && npm run build && npm pack --dry-run"
33
34
  },
34
35
  "dependencies": {
35
36
  "@modelcontextprotocol/sdk": "^1.29.0",