Skip to content
Merged
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
15 changes: 15 additions & 0 deletions docs/src/content/docs/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,23 @@ tools:
allowed: [create_issue, update_issue, add_issue_comment] # Optional: specific permissions
version: "latest" # Optional: MCP server version
args: ["--verbose", "--debug"] # Optional: additional command-line arguments
read-only: true # Optional: restrict to read-only operations
```

### GitHub Read-Only Mode

The `read-only` flag restricts the GitHub MCP server to read-only operations, preventing any modifications to repositories, issues, pull requests, etc.

```yaml
tools:
github:
read-only: true
```

When `read-only: true` is specified, the GitHub MCP server runs with the `GITHUB_READ_ONLY` environment variable set, which enables read-only mode at the server level.

**Default behavior**: When the GitHub tool is specified without any configuration (just `github:` with no properties), the default behavior provides read-only access with all read-only tools available.

### GitHub Args Configuration

The `args` field allows you to pass additional command-line arguments to the GitHub MCP server:
Expand Down
8 changes: 8 additions & 0 deletions pkg/parser/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ func processBuiltinMCPTool(toolName string, toolValue any, serverFilter string)

// Check for custom GitHub configuration
if toolConfig, ok := toolValue.(map[string]any); ok {
// Check for read-only mode
if readOnly, hasReadOnly := toolConfig["read-only"]; hasReadOnly {
if readOnlyBool, ok := readOnly.(bool); ok && readOnlyBool {
// When read-only is true, inline GITHUB_READ_ONLY=1 in docker args
config.Args = append(config.Args[:5], append([]string{"-e", "GITHUB_READ_ONLY=1"}, config.Args[5:]...)...)
}
}

if allowed, hasAllowed := toolConfig["allowed"]; hasAllowed {
if allowedSlice, ok := allowed.([]any); ok {
for _, item := range allowedSlice {
Expand Down
74 changes: 74 additions & 0 deletions pkg/parser/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,80 @@ func TestExtractMCPConfigurations(t *testing.T) {
expected []MCPServerConfig
expectError bool
}{
{
name: "GitHub tool with read-only true",
frontmatter: map[string]any{
"tools": map[string]any{
"github": map[string]any{
"read-only": true,
},
},
},
expected: []MCPServerConfig{
{
Name: "github",
Type: "docker",
Command: "docker",
Args: []string{
"run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"-e", "GITHUB_READ_ONLY=1",
"ghcr.io/github/github-mcp-server:sha-09deac4",
},
Env: map[string]string{
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN_REQUIRED}",
},
Allowed: []string{},
},
},
},
{
name: "GitHub tool with read-only false",
frontmatter: map[string]any{
"tools": map[string]any{
"github": map[string]any{
"read-only": false,
},
},
},
expected: []MCPServerConfig{
{
Name: "github",
Type: "docker",
Command: "docker",
Args: []string{
"run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-09deac4",
},
Env: map[string]string{
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN_REQUIRED}",
},
Allowed: []string{},
},
},
},
{
name: "GitHub tool without read-only (default behavior)",
frontmatter: map[string]any{
"tools": map[string]any{
"github": map[string]any{},
},
},
expected: []MCPServerConfig{
{
Name: "github",
Type: "docker",
Command: "docker",
Args: []string{
"run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-09deac4",
},
Env: map[string]string{
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN_REQUIRED}",
},
Allowed: []string{},
},
},
},
{
name: "New format: Custom MCP server with direct fields",
frontmatter: map[string]any{
Expand Down
8 changes: 8 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,10 @@
"github": {
"description": "GitHub API tools for repository operations (issues, pull requests, content management)",
"oneOf": [
{
"type": "null",
"description": "Empty GitHub tool configuration (enables all read-only GitHub API functions)"
},
{
"type": "string",
"description": "Simple GitHub tool configuration (enables all GitHub API functions)"
Expand All @@ -1009,6 +1013,10 @@
"items": {
"type": "string"
}
},
"read-only": {
"type": "boolean",
"description": "Enable read-only mode to restrict GitHub MCP server to read-only operations only"
}
},
"additionalProperties": false
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a
func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, githubTool any, isLast bool, workflowData *WorkflowData) {
githubDockerImageVersion := getGitHubDockerImageVersion(githubTool)
customArgs := getGitHubCustomArgs(githubTool)
readOnly := getGitHubReadOnly(githubTool)

yaml.WriteString(" \"github\": {\n")

Expand All @@ -638,6 +639,10 @@ func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, github
yaml.WriteString(" \"--rm\",\n")
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n")
if readOnly {
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_READ_ONLY=1\",\n")
}
yaml.WriteString(" \"ghcr.io/github/github-mcp-server:" + githubDockerImageVersion + "\"")

// Append custom args if present
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ func (e *CodexEngine) extractCodexTokenUsage(line string) int {
func (e *CodexEngine) renderGitHubCodexMCPConfig(yaml *strings.Builder, githubTool any, workflowData *WorkflowData) {
githubDockerImageVersion := getGitHubDockerImageVersion(githubTool)
customArgs := getGitHubCustomArgs(githubTool)
readOnly := getGitHubReadOnly(githubTool)

yaml.WriteString(" \n")
yaml.WriteString(" [mcp_servers.github]\n")
Expand All @@ -503,6 +504,10 @@ func (e *CodexEngine) renderGitHubCodexMCPConfig(yaml *strings.Builder, githubTo
yaml.WriteString(" \"--rm\",\n")
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n")
if readOnly {
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_READ_ONLY=1\",\n")
}
yaml.WriteString(" \"ghcr.io/github/github-mcp-server:" + githubDockerImageVersion + "\"")

// Append custom args if present
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]
func (e *CopilotEngine) renderGitHubCopilotMCPConfig(yaml *strings.Builder, githubTool any, isLast bool, workflowData *WorkflowData) {
githubDockerImageVersion := getGitHubDockerImageVersion(githubTool)
customArgs := getGitHubCustomArgs(githubTool)
readOnly := getGitHubReadOnly(githubTool)

yaml.WriteString(" \"github\": {\n")
yaml.WriteString(" \"type\": \"local\",\n")
Expand All @@ -267,6 +268,10 @@ func (e *CopilotEngine) renderGitHubCopilotMCPConfig(yaml *strings.Builder, gith
yaml.WriteString(" \"--rm\",\n")
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }}\",\n")
if readOnly {
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_READ_ONLY=1\",\n")
}
yaml.WriteString(" \"ghcr.io/github/github-mcp-server:" + githubDockerImageVersion + "\"")

// Append custom args if present
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/custom_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func (e *CustomEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a
func (e *CustomEngine) renderGitHubMCPConfig(yaml *strings.Builder, githubTool any, isLast bool) {
githubDockerImageVersion := getGitHubDockerImageVersion(githubTool)
customArgs := getGitHubCustomArgs(githubTool)
readOnly := getGitHubReadOnly(githubTool)

yaml.WriteString(" \"github\": {\n")

Expand All @@ -194,6 +195,10 @@ func (e *CustomEngine) renderGitHubMCPConfig(yaml *strings.Builder, githubTool a
yaml.WriteString(" \"--rm\",\n")
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n")
if readOnly {
yaml.WriteString(" \"-e\",\n")
yaml.WriteString(" \"GITHUB_READ_ONLY=1\",\n")
}
yaml.WriteString(" \"ghcr.io/github/github-mcp-server:" + githubDockerImageVersion + "\"")

// Append custom args if present
Expand Down
59 changes: 59 additions & 0 deletions pkg/workflow/github_readonly_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package workflow

import "testing"

func TestGetGitHubReadOnly(t *testing.T) {
tests := []struct {
name string
githubTool any
expected bool
}{
{
name: "read-only true",
githubTool: map[string]any{
"read-only": true,
},
expected: true,
},
{
name: "read-only false",
githubTool: map[string]any{
"read-only": false,
},
expected: false,
},
{
name: "no read-only field",
githubTool: map[string]any{},
expected: false,
},
{
name: "read-only with other fields",
githubTool: map[string]any{
"read-only": true,
"version": "latest",
"args": []string{"--verbose"},
},
expected: true,
},
{
name: "nil tool",
githubTool: nil,
expected: false,
},
{
name: "string tool (not map)",
githubTool: "github",
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getGitHubReadOnly(tt.githubTool)
if result != tt.expected {
t.Errorf("getGitHubReadOnly() = %v, want %v", result, tt.expected)
}
})
}
}
Loading