Skip to content

Commit 3369489

Browse files
committed
Add supported languages to MCP tool parameter description
1 parent 6659a60 commit 3369489

File tree

3 files changed

+211
-149
lines changed

3 files changed

+211
-149
lines changed

main.py

Lines changed: 178 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
from typing import Any, List, Literal, Optional
77

8+
import yaml
89
from mcp.server.fastmcp import FastMCP
910
from pydantic import Field
1011

@@ -59,168 +60,172 @@ def parse_args_and_get_config():
5960

6061
DumpFormat = Literal["pattern", "cst", "ast"]
6162

62-
@mcp.tool()
63-
def dump_syntax_tree(
64-
code: str = Field(description = "The code you need"),
65-
language: str = Field(description = "The language of the code"),
66-
format: DumpFormat = Field(description = "Code dump format. Available values: pattern, ast, cst", default = "cst"),
67-
) -> str:
68-
"""
69-
Dump code's syntax structure or dump a query's pattern structure.
70-
This is useful to discover correct syntax kind and syntax tree structure. Call it when debugging a rule.
71-
The tool requires three arguments: code, language and format. The first two are self-explanatory.
72-
`format` is the output format of the syntax tree.
73-
use `format=cst` to inspect the code's concrete syntax tree structure, useful to debug target code.
74-
use `format=pattern` to inspect how ast-grep interprets a pattern, useful to debug pattern rule.
75-
76-
Internally calls: ast-grep run --pattern <code> --lang <language> --debug-query=<format>
77-
"""
78-
result = run_ast_grep("run", ["--pattern", code, "--lang", language, f"--debug-query={format}"])
79-
return result.stderr.strip() # type: ignore[no-any-return]
80-
81-
@mcp.tool()
82-
def test_match_code_rule(
83-
code: str = Field(description="The code to test against the rule"),
84-
yaml: str = Field(description="The ast-grep YAML rule to search. It must have id, language, rule fields."),
85-
) -> List[dict[str, Any]]:
86-
"""
87-
Test a code against an ast-grep YAML rule.
88-
This is useful to test a rule before using it in a project.
89-
90-
Internally calls: ast-grep scan --inline-rules <yaml> --json --stdin
91-
"""
92-
result = run_ast_grep("scan", ["--inline-rules", yaml, "--json", "--stdin"], input_text = code)
93-
matches = json.loads(result.stdout.strip())
94-
if not matches:
95-
raise ValueError("No matches found for the given code and rule. Try adding `stopBy: end` to your inside/has rule.")
96-
return matches # type: ignore[no-any-return]
97-
98-
@mcp.tool()
99-
def find_code(
100-
project_folder: str = Field(description="The absolute path to the project folder. It must be absolute path."),
101-
pattern: str = Field(description="The ast-grep pattern to search for. Note, the pattern must have valid AST structure."),
102-
language: str = Field(description="The language of the query", default=""),
103-
max_results: Optional[int] = Field(default=None, description="Maximum results to return"),
104-
output_format: str = Field(default="text", description="'text' or 'json'"),
105-
) -> str | List[dict[str, Any]]:
106-
"""
107-
Find code in a project folder that matches the given ast-grep pattern.
108-
Pattern is good for simple and single-AST node result.
109-
For more complex usage, please use YAML by `find_code_by_rule`.
110-
111-
Internally calls: ast-grep run --pattern <pattern> [--json] <project_folder>
112-
113-
Output formats:
114-
- text (default): Compact text format with file:line-range headers and complete match text
115-
Example:
116-
Found 2 matches:
117-
118-
path/to/file.py:10-15
119-
def example_function():
120-
# function body
121-
return result
122-
123-
path/to/file.py:20-22
124-
def another_function():
125-
pass
126-
127-
- json: Full match objects with metadata including ranges, meta-variables, etc.
128-
129-
The max_results parameter limits the number of complete matches returned (not individual lines).
130-
When limited, the header shows "Found X matches (showing first Y of Z)".
131-
132-
Example usage:
133-
find_code(pattern="class $NAME", max_results=20) # Returns text format
134-
find_code(pattern="class $NAME", output_format="json") # Returns JSON with metadata
135-
"""
136-
if output_format not in ["text", "json"]:
137-
raise ValueError(f"Invalid output_format: {output_format}. Must be 'text' or 'json'.")
138-
139-
args = ["--pattern", pattern]
140-
if language:
141-
args.extend(["--lang", language])
142-
143-
# Always get JSON internally for accurate match limiting
144-
result = run_ast_grep("run", args + ["--json", project_folder])
145-
matches = json.loads(result.stdout.strip() or "[]")
146-
147-
# Apply max_results limit to complete matches
148-
total_matches = len(matches)
149-
if max_results is not None and total_matches > max_results:
150-
matches = matches[:max_results]
151-
152-
if output_format == "text":
63+
def register_mcp_tools() -> None:
64+
65+
@mcp.tool()
66+
def dump_syntax_tree(
67+
code: str = Field(description = "The code you need"),
68+
language: str = Field(description = f"The language of the code. Supported: {', '.join(get_supported_languages())}"),
69+
format: DumpFormat = Field(description = "Code dump format. Available values: pattern, ast, cst", default = "cst"),
70+
) -> str:
71+
"""
72+
Dump code's syntax structure or dump a query's pattern structure.
73+
This is useful to discover correct syntax kind and syntax tree structure. Call it when debugging a rule.
74+
The tool requires three arguments: code, language and format. The first two are self-explanatory.
75+
`format` is the output format of the syntax tree.
76+
use `format=cst` to inspect the code's concrete syntax tree structure, useful to debug target code.
77+
use `format=pattern` to inspect how ast-grep interprets a pattern, useful to debug pattern rule.
78+
79+
Internally calls: ast-grep run --pattern <code> --lang <language> --debug-query=<format>
80+
"""
81+
result = run_ast_grep("run", ["--pattern", code, "--lang", language, f"--debug-query={format}"])
82+
return result.stderr.strip() # type: ignore[no-any-return]
83+
84+
@mcp.tool()
85+
def test_match_code_rule(
86+
code: str = Field(description = "The code to test against the rule"),
87+
yaml: str = Field(description = "The ast-grep YAML rule to search. It must have id, language, rule fields."),
88+
) -> List[dict[str, Any]]:
89+
"""
90+
Test a code against an ast-grep YAML rule.
91+
This is useful to test a rule before using it in a project.
92+
93+
Internally calls: ast-grep scan --inline-rules <yaml> --json --stdin
94+
"""
95+
result = run_ast_grep("scan", ["--inline-rules", yaml, "--json", "--stdin"], input_text = code)
96+
matches = json.loads(result.stdout.strip())
15397
if not matches:
154-
return "No matches found"
155-
text_output = format_matches_as_text(matches)
156-
header = f"Found {len(matches)} matches"
157-
if max_results is not None and total_matches > max_results:
158-
header += f" (showing first {max_results} of {total_matches})"
159-
return header + ":\n\n" + text_output
160-
return matches # type: ignore[no-any-return]
161-
162-
@mcp.tool()
163-
def find_code_by_rule(
164-
project_folder: str = Field(description="The absolute path to the project folder. It must be absolute path."),
165-
yaml: str = Field(description="The ast-grep YAML rule to search. It must have id, language, rule fields."),
166-
max_results: Optional[int] = Field(default=None, description="Maximum results to return"),
167-
output_format: str = Field(default="text", description="'text' or 'json'"),
98+
raise ValueError("No matches found for the given code and rule. Try adding `stopBy: end` to your inside/has rule.")
99+
return matches # type: ignore[no-any-return]
100+
101+
@mcp.tool()
102+
def find_code(
103+
project_folder: str = Field(description = "The absolute path to the project folder. It must be absolute path."),
104+
pattern: str = Field(description = "The ast-grep pattern to search for. Note, the pattern must have valid AST structure."),
105+
language: str = Field(description = f"The language of the code. Supported: {', '.join(get_supported_languages())}."
106+
"If not specified, will be auto-detected based on file extensions.", default = ""),
107+
max_results: Optional[int] = Field(default = None, description = "Maximum results to return"),
108+
output_format: str = Field(default = "text", description = "'text' or 'json'"),
168109
) -> str | List[dict[str, Any]]:
169-
"""
170-
Find code using ast-grep's YAML rule in a project folder.
171-
YAML rule is more powerful than simple pattern and can perform complex search like find AST inside/having another AST.
172-
It is a more advanced search tool than the simple `find_code`.
110+
"""
111+
Find code in a project folder that matches the given ast-grep pattern.
112+
Pattern is good for simple and single-AST node result.
113+
For more complex usage, please use YAML by `find_code_by_rule`.
173114
174-
Tip: When using relational rules (inside/has), add `stopBy: end` to ensure complete traversal.
115+
Internally calls: ast-grep run --pattern <pattern> [--json] <project_folder>
175116
176-
Internally calls: ast-grep scan --inline-rules <yaml> [--json] <project_folder>
117+
Output formats:
118+
- text (default): Compact text format with file:line-range headers and complete match text
119+
Example:
120+
Found 2 matches:
177121
178-
Output formats:
179-
- text (default): Compact text format with file:line-range headers and complete match text
180-
Example:
181-
Found 2 matches:
122+
path/to/file.py:10-15
123+
def example_function():
124+
# function body
125+
return result
182126
183-
src/models.py:45-52
184-
class UserModel:
185-
def __init__(self):
186-
self.id = None
187-
self.name = None
127+
path/to/file.py:20-22
128+
def another_function():
129+
pass
188130
189-
src/views.py:12
190-
class SimpleView: pass
131+
- json: Full match objects with metadata including ranges, meta-variables, etc.
191132
192-
- json: Full match objects with metadata including ranges, meta-variables, etc.
133+
The max_results parameter limits the number of complete matches returned (not individual lines).
134+
When limited, the header shows "Found X matches (showing first Y of Z)".
193135
194-
The max_results parameter limits the number of complete matches returned (not individual lines).
195-
When limited, the header shows "Found X matches (showing first Y of Z)".
136+
Example usage:
137+
find_code(pattern="class $NAME", max_results=20) # Returns text format
138+
find_code(pattern="class $NAME", output_format="json") # Returns JSON with metadata
139+
"""
140+
if output_format not in ["text", "json"]:
141+
raise ValueError(f"Invalid output_format: {output_format}. Must be 'text' or 'json'.")
196142

197-
Example usage:
198-
find_code_by_rule(yaml="id: x\\nlanguage: python\\nrule: {pattern: 'class $NAME'}", max_results=20)
199-
find_code_by_rule(yaml="...", output_format="json") # For full metadata
200-
"""
201-
if output_format not in ["text", "json"]:
202-
raise ValueError(f"Invalid output_format: {output_format}. Must be 'text' or 'json'.")
143+
args = ["--pattern", pattern]
144+
if language:
145+
args.extend(["--lang", language])
203146

204-
args = ["--inline-rules", yaml]
147+
# Always get JSON internally for accurate match limiting
148+
result = run_ast_grep("run", args + ["--json", project_folder])
149+
matches = json.loads(result.stdout.strip() or "[]")
205150

206-
# Always get JSON internally for accurate match limiting
207-
result = run_ast_grep("scan", args + ["--json", project_folder])
208-
matches = json.loads(result.stdout.strip() or "[]")
151+
# Apply max_results limit to complete matches
152+
total_matches = len(matches)
153+
if max_results is not None and total_matches > max_results:
154+
matches = matches[:max_results]
155+
156+
if output_format == "text":
157+
if not matches:
158+
return "No matches found"
159+
text_output = format_matches_as_text(matches)
160+
header = f"Found {len(matches)} matches"
161+
if max_results is not None and total_matches > max_results:
162+
header += f" (showing first {max_results} of {total_matches})"
163+
return header + ":\n\n" + text_output
164+
return matches # type: ignore[no-any-return]
165+
166+
@mcp.tool()
167+
def find_code_by_rule(
168+
project_folder: str = Field(description = "The absolute path to the project folder. It must be absolute path."),
169+
yaml: str = Field(description = "The ast-grep YAML rule to search. It must have id, language, rule fields."),
170+
max_results: Optional[int] = Field(default = None, description = "Maximum results to return"),
171+
output_format: str = Field(default = "text", description = "'text' or 'json'"),
172+
) -> str | List[dict[str, Any]]:
173+
"""
174+
Find code using ast-grep's YAML rule in a project folder.
175+
YAML rule is more powerful than simple pattern and can perform complex search like find AST inside/having another AST.
176+
It is a more advanced search tool than the simple `find_code`.
177+
178+
Tip: When using relational rules (inside/has), add `stopBy: end` to ensure complete traversal.
179+
180+
Internally calls: ast-grep scan --inline-rules <yaml> [--json] <project_folder>
181+
182+
Output formats:
183+
- text (default): Compact text format with file:line-range headers and complete match text
184+
Example:
185+
Found 2 matches:
186+
187+
src/models.py:45-52
188+
class UserModel:
189+
def __init__(self):
190+
self.id = None
191+
self.name = None
192+
193+
src/views.py:12
194+
class SimpleView: pass
195+
196+
- json: Full match objects with metadata including ranges, meta-variables, etc.
197+
198+
The max_results parameter limits the number of complete matches returned (not individual lines).
199+
When limited, the header shows "Found X matches (showing first Y of Z)".
200+
201+
Example usage:
202+
find_code_by_rule(yaml="id: x\\nlanguage: python\\nrule: {pattern: 'class $NAME'}", max_results=20)
203+
find_code_by_rule(yaml="...", output_format="json") # For full metadata
204+
"""
205+
if output_format not in ["text", "json"]:
206+
raise ValueError(f"Invalid output_format: {output_format}. Must be 'text' or 'json'.")
207+
208+
args = ["--inline-rules", yaml]
209+
210+
# Always get JSON internally for accurate match limiting
211+
result = run_ast_grep("scan", args + ["--json", project_folder])
212+
matches = json.loads(result.stdout.strip() or "[]")
213+
214+
# Apply max_results limit to complete matches
215+
total_matches = len(matches)
216+
if max_results is not None and total_matches > max_results:
217+
matches = matches[:max_results]
209218

210-
# Apply max_results limit to complete matches
211-
total_matches = len(matches)
212-
if max_results is not None and total_matches > max_results:
213-
matches = matches[:max_results]
219+
if output_format == "text":
220+
if not matches:
221+
return "No matches found"
222+
text_output = format_matches_as_text(matches)
223+
header = f"Found {len(matches)} matches"
224+
if max_results is not None and total_matches > max_results:
225+
header += f" (showing first {max_results} of {total_matches})"
226+
return header + ":\n\n" + text_output
227+
return matches # type: ignore[no-any-return]
214228

215-
if output_format == "text":
216-
if not matches:
217-
return "No matches found"
218-
text_output = format_matches_as_text(matches)
219-
header = f"Found {len(matches)} matches"
220-
if max_results is not None and total_matches > max_results:
221-
header += f" (showing first {max_results} of {total_matches})"
222-
return header + ":\n\n" + text_output
223-
return matches # type: ignore[no-any-return]
224229

225230
def format_matches_as_text(matches: List[dict]) -> str:
226231
"""Convert JSON matches to LLM-friendly text format.
@@ -248,6 +253,29 @@ def format_matches_as_text(matches: List[dict]) -> str:
248253

249254
return '\n\n'.join(output_blocks)
250255

256+
def get_supported_languages() -> List[str]:
257+
"""Get all supported languages as a field description string."""
258+
languages = [ # https://ast-grep.github.io/reference/languages.html
259+
"bash", "c", "cpp", "csharp", "css", "elixir", "go", "haskell",
260+
"html", "java", "javascript", "json", "jsx", "kotlin", "lua",
261+
"nix", "php", "python", "ruby", "rust", "scala", "solidity",
262+
"swift", "tsx", "typescript", "yaml"
263+
]
264+
265+
# Check for custom languages in config file
266+
# https://ast-grep.github.io/advanced/custom-language.html#register-language-in-sgconfig-yml
267+
if CONFIG_PATH and os.path.exists(CONFIG_PATH):
268+
try:
269+
with open(CONFIG_PATH, 'r') as f:
270+
config = yaml.safe_load(f)
271+
if config and 'customLanguages' in config:
272+
custom_langs = list(config['customLanguages'].keys())
273+
languages += custom_langs
274+
except Exception:
275+
pass
276+
277+
return sorted(set(languages))
278+
251279
def run_command(args: List[str], input_text: Optional[str] = None) -> subprocess.CompletedProcess:
252280
try:
253281
# On Windows, if ast-grep is installed via npm, it's a batch file
@@ -281,7 +309,8 @@ def run_mcp_server() -> None:
281309
Run the MCP server.
282310
This function is used to start the MCP server when this script is run directly.
283311
"""
284-
parse_args_and_get_config()
312+
parse_args_and_get_config() # sets CONFIG_PATH
313+
register_mcp_tools() # tools defined *after* CONFIG_PATH is known
285314
mcp.run(transport="stdio")
286315

287316
if __name__ == "__main__":

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ requires-python = ">=3.13"
77
dependencies = [
88
"pydantic>=2.11.0",
99
"mcp[cli]>=1.6.0",
10+
"pyyaml>=6.0.2",
1011
]
1112

1213
[project.optional-dependencies]
@@ -16,6 +17,7 @@ dev = [
1617
"pytest-mock>=3.14.0",
1718
"ruff>=0.7.0",
1819
"mypy>=1.13.0",
20+
"types-pyyaml>=6.0.12.20250809",
1921
]
2022

2123
[project.scripts]
@@ -55,3 +57,4 @@ warn_return_any = true
5557
warn_unused_configs = true
5658
disallow_untyped_defs = false
5759
ignore_missing_imports = true
60+

0 commit comments

Comments
 (0)