Skip to content

Commit 80716ff

Browse files
committed
add projectRoot flag so the binary can be re-used and used outside of the project root directory
1 parent fca40a1 commit 80716ff

File tree

4 files changed

+105
-19
lines changed

4 files changed

+105
-19
lines changed

AGENT_DOCS/PRD/PRD.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,28 @@ As a developer using an AI coding agent, I want to:
2424

2525
The MCP server will expose the following **seven** tools to the AI agent. The agent will use the tool descriptions to decide which tool to call based on the user's request.
2626

27+
### Deployment Configuration
28+
29+
**Custom Project Root Support**: The server supports a `--projectRoot` command-line argument that allows users to specify where project files (sgconfig.yml, rules/, etc.) should be created, regardless of where the binary is located. This solves cross-platform deployment issues where the binary may be installed in a system location separate from project directories.
30+
31+
**Usage**:
32+
```bash
33+
# Command line
34+
context-sherpa --projectRoot="/path/to/project"
35+
36+
# MCP configuration
37+
{
38+
"mcpServers": {
39+
"context-sherpa": {
40+
"command": "context-sherpa",
41+
"args": ["--projectRoot", "/path/to/project"]
42+
}
43+
}
44+
}
45+
```
46+
47+
This ensures consistent file locations across different installation scenarios and platforms.
48+
2749
### Tool 1: `initialize_ast_grep`
2850

2951
- **Description**: "Initializes an ast-grep project if one is not already present. It creates the `sgconfig.yml` file and a `rules` directory. This tool should be suggested if another tool fails due to a missing configuration file."

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,44 @@ Context Sherpa is delivered as a single, self-contained binary for easy integrat
2222

2323
1. Navigate to the [releases page](https://github.com/hackafterdark/context-sherpa/releases/latest) of the GitHub repository.
2424
2. Download the binary that matches your operating system and architecture:
25-
- `context-sherpa-linux-amd64` (Linux)
26-
- `context-sherpa-darwin-amd64` or `context-sherpa-darwin-arm64` (macOS)
27-
- `context-sherpa-windows-amd64.exe` or `context-sherpa-windows-arm64.exe` (Windows)
25+
- `context-sherpa-linux-amd64` (Linux)
26+
- `context-sherpa-darwin-amd64` or `context-sherpa-darwin-arm64` (macOS)
27+
- `context-sherpa-windows-amd64.exe` or `context-sherpa-windows-arm64.exe` (Windows)
2828
3. Configure your AI coding tool (Roo Code, Cline, Cursor, etc.) to use Context Sherpa as an MCP server.
2929

3030
### Option 2: Build from Source
3131

3232
You can also build from source, though you'll need to ensure the `ast-grep` binary is available (see Contributing section below).
3333

34+
```bash
35+
go build -o context-sherpa ./cmd/server
36+
```
37+
38+
## Configuration
39+
40+
### Custom Project Root (Optional)
41+
42+
If your MCP server binary is installed in a different location than your project directory, you can specify a custom project root:
43+
44+
**Command Line:**
45+
```bash
46+
context-sherpa --projectRoot="/path/to/your/project"
47+
```
48+
49+
**MCP Configuration:**
50+
```json
51+
{
52+
"mcpServers": {
53+
"context-sherpa": {
54+
"command": "context-sherpa",
55+
"args": ["--projectRoot", "/path/to/your/project"]
56+
}
57+
}
58+
}
59+
```
60+
61+
This ensures that `sgconfig.yml`, `rules/`, and other project files are created in your actual project directory rather than where the binary is located.
62+
3463
## Features
3564

3665
- **Dynamic Rule Management**: Create, update, and remove linting rules on the fly based on natural language feedback.

cmd/server/main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
_ "embed"
5+
"flag"
56

67
"github.com/hackafterdark/context-sherpa/internal/mcp"
78
)
@@ -10,7 +11,10 @@ import (
1011
var astGrepBinary []byte
1112

1213
func main() {
13-
mcp.Start(astGrepBinary)
14+
projectRoot := flag.String("projectRoot", "", "Project root directory (defaults to current working directory)")
15+
flag.Parse()
16+
17+
mcp.Start(astGrepBinary, *projectRoot)
1418
}
1519

1620
// GetAstGrepBinary returns the embedded ast-grep binary for testing

internal/mcp/server.go

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,19 @@ var (
5858

5959
var communityRulesRepo = "https://raw.githubusercontent.com/hackafterdark/context-sherpa-community-rules/main/index.json"
6060

61+
// projectRootOverride stores the custom project root directory when specified via command-line argument
62+
var projectRootOverride string
63+
6164
// getCommunityRulesRepoURL returns the community rules repository URL (can be overridden in tests)
6265
func getCommunityRulesRepoURL() string {
6366
return communityRulesRepo
6467
}
6568

6669
// Start initializes and starts the MCP server.
67-
func Start(sgBinary []byte) {
70+
func Start(sgBinary []byte, projectRoot string) {
71+
if projectRoot != "" {
72+
projectRootOverride = projectRoot
73+
}
6874
sgBinaryData = sgBinary
6975

7076
// Create a new MCP server
@@ -551,13 +557,22 @@ func addOrUpdateRuleHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.
551557
// findProjectRoot finds the project root by searching for sgconfig.yml
552558
// in the current and parent directories.
553559
func findProjectRoot() (string, error) {
554-
dir, err := os.Getwd()
555-
if err != nil {
556-
return "", fmt.Errorf("could not get current directory: %v", err)
560+
var dir string
561+
var err error
562+
563+
if projectRootOverride != "" {
564+
// Use the specified project root as starting point
565+
dir = projectRootOverride
566+
} else {
567+
// Fall back to current behavior
568+
dir, err = os.Getwd()
569+
if err != nil {
570+
return "", fmt.Errorf("could not get current directory: %v", err)
571+
}
557572
}
558573

559574
for {
560-
configPath := dir + "/sgconfig.yml"
575+
configPath := filepath.Join(dir, "sgconfig.yml")
561576
if _, err := os.Stat(configPath); err == nil {
562577
return dir, nil
563578
}
@@ -594,13 +609,22 @@ func extractSgBinary(sgBinary []byte) (string, error) {
594609
// getRuleDir determines the directory where rules should be stored by searching
595610
// for sgconfig.yml in the current and parent directories.
596611
func getRuleDir() (string, error) {
597-
dir, err := os.Getwd()
598-
if err != nil {
599-
return "", fmt.Errorf("could not get current directory: %v", err)
612+
var dir string
613+
var err error
614+
615+
if projectRootOverride != "" {
616+
// Use the specified project root as starting point
617+
dir = projectRootOverride
618+
} else {
619+
// Fall back to current behavior
620+
dir, err = os.Getwd()
621+
if err != nil {
622+
return "", fmt.Errorf("could not get current directory: %v", err)
623+
}
600624
}
601625

602626
for {
603-
configPath := dir + "/sgconfig.yml"
627+
configPath := filepath.Join(dir, "sgconfig.yml")
604628
if _, err := os.Stat(configPath); err == nil {
605629
// Read and parse sgconfig.yml
606630
data, err := os.ReadFile(configPath)
@@ -617,7 +641,7 @@ func getRuleDir() (string, error) {
617641
return "", fmt.Errorf("ruleDirs not specified in sgconfig.yml")
618642
}
619643
// Return the first rule directory, relative to the config file's location
620-
return dir + "/" + strings.TrimSpace(config.RuleDirs[0]), nil
644+
return filepath.Join(dir, strings.TrimSpace(config.RuleDirs[0])), nil
621645
}
622646

623647
// Move to parent directory
@@ -659,10 +683,17 @@ func removeRuleHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
659683
}
660684

661685
func initializeAstGrepHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
662-
// Get the current working directory as the project root
663-
projectRoot, err := os.Getwd()
664-
if err != nil {
665-
return mcp.NewToolResultError(fmt.Sprintf("Error getting current directory: %v", err)), nil
686+
// Determine the project root to use
687+
var projectRoot string
688+
var err error
689+
690+
if projectRootOverride != "" {
691+
projectRoot = projectRootOverride
692+
} else {
693+
projectRoot, err = os.Getwd()
694+
if err != nil {
695+
return mcp.NewToolResultError(fmt.Sprintf("Error getting current directory: %v", err)), nil
696+
}
666697
}
667698

668699
// Create the rules directory if it doesn't exist
@@ -675,7 +706,7 @@ func initializeAstGrepHandler(ctx context.Context, req mcp.CallToolRequest) (*mc
675706
sgconfigPath := filepath.Join(projectRoot, "sgconfig.yml")
676707
sgconfigContent := `ruleDirs:
677708
- rules
678-
`
709+
`
679710
if err := os.WriteFile(sgconfigPath, []byte(sgconfigContent), 0644); err != nil {
680711
return mcp.NewToolResultError(fmt.Sprintf("Error creating sgconfig.yml: %v", err)), nil
681712
}

0 commit comments

Comments
 (0)