1- from typing import Any , List , Optional
2- from mcp .server .fastmcp import FastMCP
3- import subprocess
4- from pydantic import Field
5- import json
6- from enum import Enum
71import argparse
2+ import json
83import os
4+ import subprocess
95import sys
6+ from typing import Any , List , Literal , Optional
107
11- # Determine how the script was invoked
12- if sys .argv [0 ].endswith ('main.py' ):
13- # Direct execution: python main.py
14- prog = 'python main.py'
15- else :
16- # Installed script execution (via uvx, pip install, etc.)
17- prog = None # Let argparse use the default
18-
19- # Parse command-line arguments
20- parser = argparse .ArgumentParser (
21- prog = prog ,
22- description = 'ast-grep MCP Server - Provides structural code search capabilities via Model Context Protocol' ,
23- epilog = '''
8+ from mcp .server .fastmcp import FastMCP
9+ from pydantic import Field
10+
11+ # Global variable for config path (will be set by parse_args_and_get_config)
12+ CONFIG_PATH = None
13+
14+ def parse_args_and_get_config ():
15+ """Parse command-line arguments and determine config path."""
16+ global CONFIG_PATH
17+
18+ # Determine how the script was invoked
19+ prog = None
20+ if sys .argv [0 ].endswith ('main.py' ):
21+ # Direct execution: python main.py
22+ prog = 'python main.py'
23+
24+ # Parse command-line arguments
25+ parser = argparse .ArgumentParser (
26+ prog = prog ,
27+ description = 'ast-grep MCP Server - Provides structural code search capabilities via Model Context Protocol' ,
28+ epilog = '''
2429environment variables:
2530 AST_GREP_CONFIG Path to sgconfig.yaml file (overridden by --config flag)
2631
2732For more information, see: https://github.com/ast-grep/ast-grep-mcp
28- ''' ,
29- formatter_class = argparse .RawDescriptionHelpFormatter
30- )
31- parser .add_argument (
32- '--config' ,
33- type = str ,
34- metavar = 'PATH' ,
35- help = 'Path to sgconfig.yaml file for customizing ast-grep behavior (language mappings, rule directories, etc.)'
36- )
37- args = parser .parse_args ()
38-
39- # Determine config path with precedence: --config flag > AST_GREP_CONFIG env > None
40- CONFIG_PATH = None
41- if args .config :
42- if not os .path .exists (args .config ):
43- print (f"Error: Config file '{ args .config } ' does not exist" )
44- sys .exit (1 )
45- CONFIG_PATH = args .config
46- elif os .environ .get ('AST_GREP_CONFIG' ):
47- env_config = os .environ .get ('AST_GREP_CONFIG' )
48- if not os .path .exists (env_config ):
49- print (f"Error: Config file '{ env_config } ' specified in AST_GREP_CONFIG does not exist" )
50- sys .exit (1 )
51- CONFIG_PATH = env_config
33+ ''' ,
34+ formatter_class = argparse .RawDescriptionHelpFormatter
35+ )
36+ parser .add_argument (
37+ '--config' ,
38+ type = str ,
39+ metavar = 'PATH' ,
40+ help = 'Path to sgconfig.yaml file for customizing ast-grep behavior (language mappings, rule directories, etc.)'
41+ )
42+ args = parser .parse_args ()
43+
44+ # Determine config path with precedence: --config flag > AST_GREP_CONFIG env > None
45+ if args .config :
46+ if not os .path .exists (args .config ):
47+ print (f"Error: Config file '{ args .config } ' does not exist" )
48+ sys .exit (1 )
49+ CONFIG_PATH = args .config
50+ elif os .environ .get ('AST_GREP_CONFIG' ):
51+ env_config = os .environ .get ('AST_GREP_CONFIG' )
52+ if env_config and not os .path .exists (env_config ):
53+ print (f"Error: Config file '{ env_config } ' specified in AST_GREP_CONFIG does not exist" )
54+ sys .exit (1 )
55+ CONFIG_PATH = env_config
5256
5357# Initialize FastMCP server
5458mcp = FastMCP ("ast-grep" )
5559
56- class DumpFormat (Enum ):
57- Pattern = "pattern"
58- CST = "cst"
59- AST = "ast"
60+ DumpFormat = Literal ["pattern" , "cst" , "ast" ]
6061
6162@mcp .tool ()
6263def dump_syntax_tree (
@@ -74,8 +75,8 @@ def dump_syntax_tree(
7475
7576 Internally calls: ast-grep run --pattern <code> --lang <language> --debug-query=<format>
7677 """
77- result = run_ast_grep ("run" , ["--pattern" , code , "--lang" , language , f"--debug-query={ format . value } " ])
78- return result .stderr .strip ()
78+ result = run_ast_grep ("run" , ["--pattern" , code , "--lang" , language , f"--debug-query={ format } " ])
79+ return result .stderr .strip () # type: ignore[no-any-return]
7980
8081@mcp .tool ()
8182def test_match_code_rule (
@@ -92,7 +93,7 @@ def test_match_code_rule(
9293 matches = json .loads (result .stdout .strip ())
9394 if not matches :
9495 raise ValueError ("No matches found for the given code and rule. Try adding `stopBy: end` to your inside/has rule." )
95- return matches
96+ return matches # type: ignore[no-any-return]
9697
9798@mcp .tool ()
9899def find_code (
@@ -131,7 +132,7 @@ def find_code(
131132 # Limit results if max_results is specified
132133 if max_results is not None and len (matches ) > max_results :
133134 matches = matches [:max_results ]
134- return matches
135+ return matches # type: ignore[no-any-return]
135136 else :
136137 # Text format - return plain text output
137138 result = run_ast_grep ("run" , args + [project_folder ])
@@ -150,7 +151,7 @@ def find_code(
150151 else :
151152 header = f"Found { len (non_empty_lines )} matches:\n "
152153 output = header + output
153- return output
154+ return output # type: ignore[no-any-return]
154155
155156@mcp .tool ()
156157def find_code_by_rule (
@@ -188,7 +189,7 @@ def find_code_by_rule(
188189 # Limit results if max_results is specified
189190 if max_results is not None and len (matches ) > max_results :
190191 matches = matches [:max_results ]
191- return matches
192+ return matches # type: ignore[no-any-return]
192193 else :
193194 # Text format - return plain text output
194195 result = run_ast_grep ("scan" , args + [project_folder ])
@@ -207,16 +208,21 @@ def find_code_by_rule(
207208 else :
208209 header = f"Found { len (non_empty_lines )} matches:\n "
209210 output = header + output
210- return output
211+ return output # type: ignore[no-any-return]
211212
212213def run_command (args : List [str ], input_text : Optional [str ] = None ) -> subprocess .CompletedProcess :
213214 try :
215+ # On Windows, if ast-grep is installed via npm, it's a batch file
216+ # that requires shell=True to execute properly
217+ use_shell = (sys .platform == "win32" and args [0 ] == "ast-grep" )
218+
214219 result = subprocess .run (
215220 args ,
216221 capture_output = True ,
217222 input = input_text ,
218223 text = True ,
219- check = True # Raises CalledProcessError if return code is non-zero
224+ check = True , # Raises CalledProcessError if return code is non-zero
225+ shell = use_shell
220226 )
221227 return result
222228 except subprocess .CalledProcessError as e :
@@ -237,6 +243,7 @@ def run_mcp_server() -> None:
237243 Run the MCP server.
238244 This function is used to start the MCP server when this script is run directly.
239245 """
246+ parse_args_and_get_config ()
240247 mcp .run (transport = "stdio" )
241248
242249if __name__ == "__main__" :
0 commit comments