Skip to content

Commit 0a1d6db

Browse files
olaservoalmaleksia
andauthored
Add server instructions based on toolsets (#1091)
* Add instruction generation for enabled toolsets and corresponding tests * Refactor instruction generation to always include base and context management instructions * Refactor base instruction for clarity and adjust context management instruction formatting * Simplify changes for now * Remove unused toolset instructions and simplify test cases for clarity * Add test cases for issues, notifications, and discussions toolsets in instruction generation * Update base instruction and test expectations for clarity on tool selection and context management * Add support for disabling instructions via environment variable * Clarify PR review workflow instruction for consistency * Apply suggestions from code review Co-authored-by: Ksenia Bobrova <[email protected]> * Refactor instruction generation and testing for clarity and consistency --------- Co-authored-by: Ksenia Bobrova <[email protected]>
1 parent 2efa8e8 commit 0a1d6db

File tree

3 files changed

+234
-2
lines changed

3 files changed

+234
-2
lines changed

internal/ghmcp/server.go

Lines changed: 8 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,14 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
118116
}
119117
}
120118

119+
// Generate instructions based on enabled toolsets
120+
instructions := github.GenerateInstructions(enabledToolsets)
121+
122+
ghServer := github.NewServer(cfg.Version,
123+
server.WithInstructions(instructions),
124+
server.WithHooks(hooks),
125+
)
126+
121127
getClient := func(_ context.Context) (*gogithub.Client, error) {
122128
return restClient, nil // closing over client
123129
}

pkg/github/instructions.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package github
2+
3+
import (
4+
"os"
5+
"slices"
6+
"strings"
7+
)
8+
9+
// GenerateInstructions creates server instructions based on enabled toolsets
10+
func GenerateInstructions(enabledToolsets []string) string {
11+
// For testing - add a flag to disable instructions
12+
if os.Getenv("DISABLE_INSTRUCTIONS") == "true" {
13+
return "" // Baseline mode
14+
}
15+
16+
var instructions []string
17+
18+
// Core instruction - always included if context toolset enabled
19+
if slices.Contains(enabledToolsets, "context") {
20+
instructions = append(instructions, "Always call 'get_me' first to understand current user permissions and context.")
21+
}
22+
23+
// Individual toolset instructions
24+
for _, toolset := range enabledToolsets {
25+
if inst := getToolsetInstructions(toolset); inst != "" {
26+
instructions = append(instructions, inst)
27+
}
28+
}
29+
30+
// Base instruction with context management
31+
baseInstruction := `The GitHub MCP Server provides tools to interact with GitHub platform.
32+
33+
Tool selection guidance:
34+
1. Use 'list_*' tools for broad, simple retrieval and pagination of all items of a type (e.g., all issues, all PRs, all branches) with basic filtering.
35+
2. Use 'search_*' tools for targeted queries with specific criteria, keywords, or complex filters (e.g., issues with certain text, PRs by author, code containing functions).
36+
37+
Context management:
38+
1. Use pagination whenever possible with batches of 5-10 items.
39+
2. Use minimal_output parameter set to true if the full information is not needed to accomplish a task.`
40+
41+
allInstructions := []string{baseInstruction}
42+
allInstructions = append(allInstructions, instructions...)
43+
44+
return strings.Join(allInstructions, " ")
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 "## Pull Requests\n\nPR review workflow: Always 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 "issues":
53+
return "## Issues\n\nCheck '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."
54+
case "discussions":
55+
return "## Discussions\n\nUse 'list_discussion_categories' to understand available categories before creating discussions. Filter by category for better organization."
56+
default:
57+
return ""
58+
}
59+
}
60+

pkg/github/instructions_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package github
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestGenerateInstructions(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
enabledToolsets []string
12+
expectedEmpty bool
13+
}{
14+
{
15+
name: "empty toolsets",
16+
enabledToolsets: []string{},
17+
expectedEmpty: false,
18+
},
19+
{
20+
name: "only context toolset",
21+
enabledToolsets: []string{"context"},
22+
expectedEmpty: false,
23+
},
24+
{
25+
name: "pull requests toolset",
26+
enabledToolsets: []string{"pull_requests"},
27+
expectedEmpty: false,
28+
},
29+
{
30+
name: "issues toolset",
31+
enabledToolsets: []string{"issues"},
32+
expectedEmpty: false,
33+
},
34+
{
35+
name: "discussions toolset",
36+
enabledToolsets: []string{"discussions"},
37+
expectedEmpty: false,
38+
},
39+
{
40+
name: "multiple toolsets (context + pull_requests)",
41+
enabledToolsets: []string{"context", "pull_requests"},
42+
expectedEmpty: false,
43+
},
44+
{
45+
name: "multiple toolsets (issues + pull_requests)",
46+
enabledToolsets: []string{"issues", "pull_requests"},
47+
expectedEmpty: false,
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
result := GenerateInstructions(tt.enabledToolsets)
54+
55+
if tt.expectedEmpty {
56+
if result != "" {
57+
t.Errorf("Expected empty instructions but got: %s", result)
58+
}
59+
} else {
60+
if result == "" {
61+
t.Errorf("Expected non-empty instructions but got empty result")
62+
}
63+
}
64+
})
65+
}
66+
}
67+
68+
func TestGenerateInstructionsWithDisableFlag(t *testing.T) {
69+
tests := []struct {
70+
name string
71+
disableEnvValue string
72+
enabledToolsets []string
73+
expectedEmpty bool
74+
}{
75+
{
76+
name: "DISABLE_INSTRUCTIONS=true returns empty",
77+
disableEnvValue: "true",
78+
enabledToolsets: []string{"context", "issues", "pull_requests"},
79+
expectedEmpty: true,
80+
},
81+
{
82+
name: "DISABLE_INSTRUCTIONS=false returns normal instructions",
83+
disableEnvValue: "false",
84+
enabledToolsets: []string{"context"},
85+
expectedEmpty: false,
86+
},
87+
{
88+
name: "DISABLE_INSTRUCTIONS unset returns normal instructions",
89+
disableEnvValue: "",
90+
enabledToolsets: []string{"issues"},
91+
expectedEmpty: false,
92+
},
93+
}
94+
95+
for _, tt := range tests {
96+
t.Run(tt.name, func(t *testing.T) {
97+
// Save original env value
98+
originalValue := os.Getenv("DISABLE_INSTRUCTIONS")
99+
defer func() {
100+
if originalValue == "" {
101+
os.Unsetenv("DISABLE_INSTRUCTIONS")
102+
} else {
103+
os.Setenv("DISABLE_INSTRUCTIONS", originalValue)
104+
}
105+
}()
106+
107+
// Set test env value
108+
if tt.disableEnvValue == "" {
109+
os.Unsetenv("DISABLE_INSTRUCTIONS")
110+
} else {
111+
os.Setenv("DISABLE_INSTRUCTIONS", tt.disableEnvValue)
112+
}
113+
114+
result := GenerateInstructions(tt.enabledToolsets)
115+
116+
if tt.expectedEmpty {
117+
if result != "" {
118+
t.Errorf("Expected empty instructions but got: %s", result)
119+
}
120+
} else {
121+
if result == "" {
122+
t.Errorf("Expected non-empty instructions but got empty result")
123+
}
124+
}
125+
})
126+
}
127+
}
128+
129+
func TestGetToolsetInstructions(t *testing.T) {
130+
tests := []struct {
131+
toolset string
132+
expectedEmpty bool
133+
}{
134+
{
135+
toolset: "pull_requests",
136+
expectedEmpty: false,
137+
},
138+
{
139+
toolset: "issues",
140+
expectedEmpty: false,
141+
},
142+
{
143+
toolset: "discussions",
144+
expectedEmpty: false,
145+
},
146+
{
147+
toolset: "nonexistent",
148+
expectedEmpty: true,
149+
},
150+
}
151+
152+
for _, tt := range tests {
153+
t.Run(tt.toolset, func(t *testing.T) {
154+
result := getToolsetInstructions(tt.toolset)
155+
if tt.expectedEmpty {
156+
if result != "" {
157+
t.Errorf("Expected empty result for toolset '%s', but got: %s", tt.toolset, result)
158+
}
159+
} else {
160+
if result == "" {
161+
t.Errorf("Expected non-empty result for toolset '%s', but got empty", tt.toolset)
162+
}
163+
}
164+
})
165+
}
166+
}

0 commit comments

Comments
 (0)