Skip to content

Commit 5ec44fb

Browse files
olaservoalmaleksia
authored andcommitted
Add instruction generation for enabled toolsets and corresponding tests
1 parent f1e0e8f commit 5ec44fb

3 files changed

Lines changed: 279 additions & 2 deletions

File tree

internal/ghmcp/server.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
105105
},
106106
}
107107

108-
ghServer := github.NewServer(cfg.Version, server.WithHooks(hooks))
109-
110108
enabledToolsets := cfg.EnabledToolsets
111109
if cfg.DynamicToolsets {
112110
// filter "all" from the enabled toolsets
@@ -118,6 +116,16 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
118116
}
119117
}
120118

119+
// Generate instructions based on enabled toolsets
120+
instructions := github.GenerateInstructions(enabledToolsets)
121+
var serverOpts []server.ServerOption
122+
if instructions != "" {
123+
serverOpts = append(serverOpts, server.WithInstructions(instructions))
124+
}
125+
serverOpts = append(serverOpts, server.WithHooks(hooks))
126+
127+
ghServer := github.NewServer(cfg.Version, serverOpts...)
128+
121129
getClient := func(_ context.Context) (*gogithub.Client, error) {
122130
return restClient, nil // closing over client
123131
}

pkg/github/instructions.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package github
2+
3+
import "strings"
4+
5+
// GenerateInstructions creates server instructions based on enabled toolsets
6+
func GenerateInstructions(enabledToolsets []string) string {
7+
var instructions []string
8+
9+
// Core instruction - always included if context toolset enabled
10+
if contains(enabledToolsets, "context") {
11+
instructions = append(instructions, "Always call 'get_me' first to understand current user permissions and context.")
12+
}
13+
14+
// Individual toolset instructions
15+
for _, toolset := range enabledToolsets {
16+
if inst := getToolsetInstructions(toolset); inst != "" {
17+
instructions = append(instructions, inst)
18+
}
19+
}
20+
21+
// Cross-toolset conditional instructions
22+
if contains(enabledToolsets, "pull_requests") && contains(enabledToolsets, "context") {
23+
instructions = append(instructions, "For team workflows: Use 'get_teams' and 'get_team_members' before assigning PR reviewers.")
24+
}
25+
26+
if hasAnySecurityToolset(enabledToolsets) {
27+
instructions = append(instructions, "Security alert priority: secret_scanning → dependabot → code_scanning alerts.")
28+
}
29+
30+
if contains(enabledToolsets, "issues") && contains(enabledToolsets, "pull_requests") {
31+
instructions = append(instructions, "Link issues to PRs using 'closes #123' or 'fixes #123' in PR descriptions.")
32+
}
33+
34+
if contains(enabledToolsets, "repos") && contains(enabledToolsets, "actions") {
35+
instructions = append(instructions, "For repository operations: Check workflow status before major changes using 'list_workflow_runs'.")
36+
}
37+
38+
// Only add base instruction if we have specific instructions
39+
if len(instructions) > 0 {
40+
baseInstruction := "GitHub MCP Server provides GitHub API tools. Common parameters: 'owner' (repo owner), 'repo' (repo name), 'page'/'perPage' (pagination)."
41+
instructions = append([]string{baseInstruction}, instructions...)
42+
}
43+
44+
return strings.Join(instructions, " ")
45+
}
46+
47+
// getToolsetInstructions returns specific instructions for individual toolsets
48+
func getToolsetInstructions(toolset string) string {
49+
switch toolset {
50+
case "pull_requests":
51+
return "PR review workflow: Use 'create_pending_pull_request_review' → 'add_comment_to_pending_review' → 'submit_pending_pull_request_review' for complex reviews with line-specific comments."
52+
case "actions":
53+
return "CI/CD debugging: Use 'get_job_logs' with 'failed_only=true' and 'return_content=true' for immediate log analysis. Use 'rerun_failed_jobs' instead of 'rerun_workflow_run' to save resources."
54+
case "issues":
55+
return "Issue workflow: Check 'list_issue_types' first for organizations to use proper issue types. Use 'search_issues' before creating new issues to avoid duplicates. Always set 'state_reason' when closing issues."
56+
case "repos":
57+
return "File operations: Use 'get_file_contents' to check if file exists before 'create_or_update_file'. Always specify 'sha' parameter when updating existing files. Use 'push_files' for multiple file operations in single commit."
58+
case "notifications":
59+
return "Notifications: Filter by 'participating' for issues/PRs you're involved in. Use 'mark_all_notifications_read' with repository filters to avoid marking unrelated notifications."
60+
case "gists":
61+
return "Gists: Use 'list_gists' with 'since' parameter to find recent gists. Specify 'public=false' for private gists in 'create_gist'."
62+
case "discussions":
63+
return "Discussions: Use 'list_discussion_categories' to understand available categories before creating discussions. Filter by category for better organization."
64+
default:
65+
return ""
66+
}
67+
}
68+
69+
// hasAnySecurityToolset checks if any security-related toolsets are enabled
70+
func hasAnySecurityToolset(toolsets []string) bool {
71+
securityToolsets := []string{"code_security", "secret_protection", "dependabot"}
72+
for _, security := range securityToolsets {
73+
if contains(toolsets, security) {
74+
return true
75+
}
76+
}
77+
return false
78+
}
79+
80+
// contains checks if a slice contains a specific string
81+
func contains(slice []string, item string) bool {
82+
for _, s := range slice {
83+
if s == item {
84+
return true
85+
}
86+
}
87+
return false
88+
}

pkg/github/instructions_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package github
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestGenerateInstructions(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
enabledToolsets []string
12+
expectedContains []string
13+
expectedEmpty bool
14+
}{
15+
{
16+
name: "empty toolsets",
17+
enabledToolsets: []string{},
18+
expectedEmpty: true,
19+
},
20+
{
21+
name: "only context toolset",
22+
enabledToolsets: []string{"context"},
23+
expectedContains: []string{
24+
"GitHub MCP Server provides GitHub API tools",
25+
"Always call 'get_me' first",
26+
},
27+
},
28+
{
29+
name: "pull requests toolset",
30+
enabledToolsets: []string{"pull_requests"},
31+
expectedContains: []string{
32+
"create_pending_pull_request_review",
33+
"add_comment_to_pending_review",
34+
"submit_pending_pull_request_review",
35+
},
36+
},
37+
{
38+
name: "actions toolset",
39+
enabledToolsets: []string{"actions"},
40+
expectedContains: []string{
41+
"get_job_logs",
42+
"failed_only=true",
43+
"rerun_failed_jobs",
44+
},
45+
},
46+
{
47+
name: "security toolsets",
48+
enabledToolsets: []string{"code_security", "secret_protection", "dependabot"},
49+
expectedContains: []string{
50+
"Security alert priority",
51+
"secret_scanning",
52+
"dependabot",
53+
"code_scanning",
54+
},
55+
},
56+
{
57+
name: "cross-toolset instructions",
58+
enabledToolsets: []string{"context", "pull_requests"},
59+
expectedContains: []string{
60+
"get_me",
61+
"get_teams",
62+
"get_team_members",
63+
},
64+
},
65+
{
66+
name: "issues and pull_requests combination",
67+
enabledToolsets: []string{"issues", "pull_requests"},
68+
expectedContains: []string{
69+
"closes #123",
70+
"fixes #123",
71+
"search_issues",
72+
"list_issue_types",
73+
},
74+
},
75+
}
76+
77+
for _, tt := range tests {
78+
t.Run(tt.name, func(t *testing.T) {
79+
result := GenerateInstructions(tt.enabledToolsets)
80+
81+
if tt.expectedEmpty {
82+
if result != "" {
83+
t.Errorf("Expected empty instructions but got: %s", result)
84+
}
85+
return
86+
}
87+
88+
for _, expectedContent := range tt.expectedContains {
89+
if !strings.Contains(result, expectedContent) {
90+
t.Errorf("Expected instructions to contain '%s', but got: %s", expectedContent, result)
91+
}
92+
}
93+
})
94+
}
95+
}
96+
97+
func TestGetToolsetInstructions(t *testing.T) {
98+
tests := []struct {
99+
toolset string
100+
expected string
101+
}{
102+
{
103+
toolset: "pull_requests",
104+
expected: "create_pending_pull_request_review",
105+
},
106+
{
107+
toolset: "actions",
108+
expected: "get_job_logs",
109+
},
110+
{
111+
toolset: "issues",
112+
expected: "list_issue_types",
113+
},
114+
{
115+
toolset: "repos",
116+
expected: "get_file_contents",
117+
},
118+
{
119+
toolset: "nonexistent",
120+
expected: "",
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.toolset, func(t *testing.T) {
126+
result := getToolsetInstructions(tt.toolset)
127+
if tt.expected == "" {
128+
if result != "" {
129+
t.Errorf("Expected empty result for toolset '%s', but got: %s", tt.toolset, result)
130+
}
131+
} else {
132+
if !strings.Contains(result, tt.expected) {
133+
t.Errorf("Expected instructions for '%s' to contain '%s', but got: %s", tt.toolset, tt.expected, result)
134+
}
135+
}
136+
})
137+
}
138+
}
139+
140+
func TestHasAnySecurityToolset(t *testing.T) {
141+
tests := []struct {
142+
name string
143+
toolsets []string
144+
expected bool
145+
}{
146+
{
147+
name: "no security toolsets",
148+
toolsets: []string{"repos", "issues"},
149+
expected: false,
150+
},
151+
{
152+
name: "has code_security",
153+
toolsets: []string{"repos", "code_security"},
154+
expected: true,
155+
},
156+
{
157+
name: "has secret_protection",
158+
toolsets: []string{"secret_protection", "issues"},
159+
expected: true,
160+
},
161+
{
162+
name: "has dependabot",
163+
toolsets: []string{"dependabot"},
164+
expected: true,
165+
},
166+
{
167+
name: "has all security toolsets",
168+
toolsets: []string{"code_security", "secret_protection", "dependabot"},
169+
expected: true,
170+
},
171+
}
172+
173+
for _, tt := range tests {
174+
t.Run(tt.name, func(t *testing.T) {
175+
result := hasAnySecurityToolset(tt.toolsets)
176+
if result != tt.expected {
177+
t.Errorf("Expected %v for toolsets %v, but got %v", tt.expected, tt.toolsets, result)
178+
}
179+
})
180+
}
181+
}

0 commit comments

Comments
 (0)