Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ This MCP server enables AI assistants (like Cursor, Claude Desktop, etc.) to sea
brew install ast-grep
nix-shell -p ast-grep
cargo install ast-grep --locked

# Or install via uv (Python package manager)
uv add ast-grep-cli
```

2. **Install uv**: Python package manager
Expand Down Expand Up @@ -100,6 +103,31 @@ You can provide the config file in two ways (in order of precedence):
1. **Command-line argument**: `--config /path/to/sgconfig.yaml`
2. **Environment variable**: `AST_GREP_CONFIG=/path/to/sgconfig.yaml`

### Custom ast-grep Command Path

If ast-grep is not in your PATH or you need to run it with a wrapper command (e.g., via `uv`), you can configure the command using the `AST_GREP_PATH` environment variable:

```json
{
"mcpServers": {
"ast-grep": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/ast-grep-mcp", "run", "main.py"],
"env": {
"AST_GREP_PATH": "uv run ast-grep"
}
}
}
}
```

**Examples:**
- `AST_GREP_PATH="uv run ast-grep"` - Run ast-grep via uv
- `AST_GREP_PATH="/custom/path/to/ast-grep"` - Use ast-grep from a custom location
- `AST_GREP_PATH="npx ast-grep"` - Run ast-grep via npx

If not set, defaults to `ast-grep` (expects ast-grep to be in PATH).

## Usage

This repository includes comprehensive ast-grep rule documentation in [ast-grep.mdc](https://github.com/ast-grep/ast-grep-mcp/blob/main/ast-grep.mdc). The documentation covers all aspects of writing effective ast-grep rules, from simple patterns to complex multi-condition searches.
Expand Down
19 changes: 16 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ def sigterm_handler(signum, frame):
CONFIG_PATH = None
TRANSPORT_TYPE = "stdio"
SERVER_PORT = 8000
AST_GREP_COMMAND = None


def parse_args_and_get_config():
"""Parse command-line arguments and determine config path and transport."""
global CONFIG_PATH, TRANSPORT_TYPE, SERVER_PORT
global CONFIG_PATH, TRANSPORT_TYPE, SERVER_PORT, AST_GREP_COMMAND

# Determine how the script was invoked
prog = None
Expand All @@ -58,6 +59,7 @@ def parse_args_and_get_config():
epilog="""
environment variables:
AST_GREP_CONFIG Path to sgconfig.yaml file (overridden by --config flag)
AST_GREP_PATH Custom command to run ast-grep (e.g., 'uv run ast-grep')

For more information, see: https://github.com/ast-grep/ast-grep-mcp
""",
Expand Down Expand Up @@ -92,6 +94,9 @@ def parse_args_and_get_config():
sys.exit(1)
CONFIG_PATH = env_config

# Determine ast-grep command from AST_GREP_PATH env variable
AST_GREP_COMMAND = os.environ.get("AST_GREP_PATH", "ast-grep")


# Initialize FastMCP server
mcp = FastMCP("ast-grep")
Expand Down Expand Up @@ -345,7 +350,14 @@ def run_command(args: List[str], input_text: Optional[str] = None) -> subprocess
try:
# On Windows, if ast-grep is installed via npm, it's a batch file
# that requires shell=True to execute properly
use_shell = sys.platform == "win32" and args[0] == "ast-grep"
# Also use shell if the command contains spaces (e.g., "uv run ast-grep")
use_shell = sys.platform == "win32" and args[0] in ("ast-grep", AST_GREP_COMMAND)
if not use_shell and len(args) > 0 and " " in args[0]:
# Command has spaces, need to split it into separate arguments
use_shell = False
split_cmd = args[0].split()
args = split_cmd + args[1:]

need_check = len(args) < 2 or args[1] != "run"

result = subprocess.run(
Expand Down Expand Up @@ -390,7 +402,8 @@ def run_command(args: List[str], input_text: Optional[str] = None) -> subprocess
def run_ast_grep(command: str, args: List[str], input_text: Optional[str] = None) -> subprocess.CompletedProcess:
if CONFIG_PATH:
args = ["--config", CONFIG_PATH] + args
return run_command(["ast-grep", command] + args, input_text)
ast_grep_cmd = AST_GREP_COMMAND or "ast-grep"
return run_command([ast_grep_cmd, command] + args, input_text)


def run_mcp_server() -> None:
Expand Down
38 changes: 38 additions & 0 deletions tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class TestRunAstGrep:

@patch("main.run_command")
@patch("main.CONFIG_PATH", None)
@patch("main.AST_GREP_COMMAND", None)
def test_without_config(self, mock_run):
"""Test running ast-grep without config"""
mock_result = Mock()
Expand All @@ -367,6 +368,7 @@ def test_without_config(self, mock_run):

@patch("main.run_command")
@patch("main.CONFIG_PATH", "/path/to/config.yaml")
@patch("main.AST_GREP_COMMAND", None)
def test_with_config(self, mock_run):
"""Test running ast-grep with config"""
mock_result = Mock()
Expand All @@ -387,6 +389,42 @@ def test_with_config(self, mock_run):
None,
)

@patch("main.run_command")
@patch("main.CONFIG_PATH", None)
@patch("main.AST_GREP_COMMAND", "uv run ast-grep")
def test_with_custom_command(self, mock_run):
"""Test running ast-grep with custom command path"""
mock_result = Mock()
mock_run.return_value = mock_result

result = run_ast_grep("run", ["--pattern", "test"])

assert result == mock_result
mock_run.assert_called_once_with(["uv run ast-grep", "run", "--pattern", "test"], None)

@patch("main.run_command")
@patch("main.CONFIG_PATH", "/path/to/config.yaml")
@patch("main.AST_GREP_COMMAND", "/custom/path/to/ast-grep")
def test_with_config_and_custom_command(self, mock_run):
"""Test running ast-grep with both config and custom command"""
mock_result = Mock()
mock_run.return_value = mock_result

result = run_ast_grep("scan", ["--inline-rules", "rule"])

assert result == mock_result
mock_run.assert_called_once_with(
[
"/custom/path/to/ast-grep",
"scan",
"--config",
"/path/to/config.yaml",
"--inline-rules",
"rule",
],
None,
)


if __name__ == "__main__":
pytest.main([__file__, "-v"])
Loading