Skip to content

Commit bc897a1

Browse files
Fix merge conflicts
1 parent e396227 commit bc897a1

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

pkg/github/server.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package github
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/google/go-github/v69/github"
8+
"github.com/mark3labs/mcp-go/mcp"
9+
"github.com/mark3labs/mcp-go/server"
10+
)
11+
12+
// NewServer creates a new GitHub MCP server with the specified GH client and logger.
13+
14+
func NewServer(version string, opts ...server.ServerOption) *server.MCPServer {
15+
// Add default options
16+
defaultOpts := []server.ServerOption{
17+
server.WithToolCapabilities(true),
18+
server.WithResourceCapabilities(true, true),
19+
server.WithLogging(),
20+
}
21+
opts = append(defaultOpts, opts...)
22+
23+
// Create a new MCP server
24+
s := server.NewMCPServer(
25+
"github-mcp-server",
26+
version,
27+
opts...,
28+
)
29+
return s
30+
}
31+
32+
// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request.
33+
// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong.
34+
func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool, err error) {
35+
// Check if the parameter is present in the request
36+
val, exists := r.Params.Arguments[p]
37+
if !exists {
38+
// Not present, return zero value, false, no error
39+
return
40+
}
41+
42+
// Check if the parameter is of the expected type
43+
value, ok = val.(T)
44+
if !ok {
45+
// Present but wrong type
46+
err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val)
47+
ok = true // Set ok to true because the parameter *was* present, even if wrong type
48+
return
49+
}
50+
51+
// Present and correct type
52+
ok = true
53+
return
54+
}
55+
56+
// isAcceptedError checks if the error is an accepted error.
57+
func isAcceptedError(err error) bool {
58+
var acceptedError *github.AcceptedError
59+
return errors.As(err, &acceptedError)
60+
}
61+
62+
// requiredParam is a helper function that can be used to fetch a requested parameter from the request.
63+
// It does the following checks:
64+
// 1. Checks if the parameter is present in the request.
65+
// 2. Checks if the parameter is of the expected type.
66+
// 3. Checks if the parameter is not empty, i.e: non-zero value
67+
func requiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) {
68+
var zero T
69+
70+
// Check if the parameter is present in the request
71+
if _, ok := r.Params.Arguments[p]; !ok {
72+
return zero, fmt.Errorf("missing required parameter: %s", p)
73+
}
74+
75+
// Check if the parameter is of the expected type
76+
if _, ok := r.Params.Arguments[p].(T); !ok {
77+
return zero, fmt.Errorf("parameter %s is not of type %T", p, zero)
78+
}
79+
80+
if r.Params.Arguments[p].(T) == zero {
81+
return zero, fmt.Errorf("missing required parameter: %s", p)
82+
83+
}
84+
85+
return r.Params.Arguments[p].(T), nil
86+
}
87+
88+
// RequiredInt is a helper function that can be used to fetch a requested parameter from the request.
89+
// It does the following checks:
90+
// 1. Checks if the parameter is present in the request.
91+
// 2. Checks if the parameter is of the expected type.
92+
// 3. Checks if the parameter is not empty, i.e: non-zero value
93+
func RequiredInt(r mcp.CallToolRequest, p string) (int, error) {
94+
v, err := requiredParam[float64](r, p)
95+
if err != nil {
96+
return 0, err
97+
}
98+
return int(v), nil
99+
}
100+
101+
// OptionalParam is a helper function that can be used to fetch a requested parameter from the request.
102+
// It does the following checks:
103+
// 1. Checks if the parameter is present in the request, if not, it returns its zero-value
104+
// 2. If it is present, it checks if the parameter is of the expected type and returns it
105+
func OptionalParam[T any](r mcp.CallToolRequest, p string) (T, error) {
106+
var zero T
107+
108+
// Check if the parameter is present in the request
109+
if _, ok := r.Params.Arguments[p]; !ok {
110+
return zero, nil
111+
}
112+
113+
// Check if the parameter is of the expected type
114+
if _, ok := r.Params.Arguments[p].(T); !ok {
115+
return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, r.Params.Arguments[p])
116+
}
117+
118+
return r.Params.Arguments[p].(T), nil
119+
}
120+
121+
// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request.
122+
// It does the following checks:
123+
// 1. Checks if the parameter is present in the request, if not, it returns its zero-value
124+
// 2. If it is present, it checks if the parameter is of the expected type and returns it
125+
func OptionalIntParam(r mcp.CallToolRequest, p string) (int, error) {
126+
v, err := OptionalParam[float64](r, p)
127+
if err != nil {
128+
return 0, err
129+
}
130+
return int(v), nil
131+
}
132+
133+
// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request
134+
// similar to optionalIntParam, but it also takes a default value.
135+
func OptionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, error) {
136+
v, err := OptionalIntParam(r, p)
137+
if err != nil {
138+
return 0, err
139+
}
140+
if v == 0 {
141+
return d, nil
142+
}
143+
return v, nil
144+
}
145+
146+
// OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request
147+
// similar to optionalParam, but it also takes a default value.
148+
func OptionalBoolParamWithDefault(r mcp.CallToolRequest, p string, d bool) (bool, error) {
149+
v, err := OptionalParam[bool](r, p)
150+
if err != nil {
151+
return false, err
152+
}
153+
if !v {
154+
return d, nil
155+
}
156+
return v, nil
157+
}
158+
159+
// OptionalStringParam is a helper function that can be used to fetch a requested parameter from the request.
160+
// It does the following checks:
161+
// 1. Checks if the parameter is present in the request, if not, it returns its zero-value
162+
// 2. If it is present, it checks if the parameter is of the expected type and returns it
163+
func OptionalStringParam(r mcp.CallToolRequest, p string) (string, error) {
164+
v, err := OptionalParam[string](r, p)
165+
if err != nil {
166+
return "", err
167+
}
168+
if v == "" {
169+
return "", nil
170+
}
171+
return v, nil
172+
}
173+
174+
// OptionalStringParamWithDefault is a helper function that can be used to fetch a requested parameter from the request
175+
// similar to optionalParam, but it also takes a default value.
176+
func OptionalStringParamWithDefault(r mcp.CallToolRequest, p string, d string) (string, error) {
177+
v, err := OptionalParam[string](r, p)
178+
if err != nil {
179+
return "", err
180+
}
181+
if v == "" {
182+
return d, nil
183+
}
184+
return v, nil
185+
}
186+
187+
// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request.
188+
// It does the following checks:
189+
// 1. Checks if the parameter is present in the request, if not, it returns its zero-value
190+
// 2. If it is present, iterates the elements and checks each is a string
191+
func OptionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) {
192+
// Check if the parameter is present in the request
193+
if _, ok := r.Params.Arguments[p]; !ok {
194+
return []string{}, nil
195+
}
196+
197+
switch v := r.Params.Arguments[p].(type) {
198+
case nil:
199+
return []string{}, nil
200+
case []string:
201+
return v, nil
202+
case []any:
203+
strSlice := make([]string, len(v))
204+
for i, v := range v {
205+
s, ok := v.(string)
206+
if !ok {
207+
return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v)
208+
}
209+
strSlice[i] = s
210+
}
211+
return strSlice, nil
212+
default:
213+
return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, r.Params.Arguments[p])
214+
}
215+
}
216+
217+
// WithPagination returns a ToolOption that adds "page" and "perPage" parameters to the tool.
218+
// The "page" parameter is optional, min 1. The "perPage" parameter is optional, min 1, max 100.
219+
func WithPagination() mcp.ToolOption {
220+
return func(tool *mcp.Tool) {
221+
mcp.WithNumber("page",
222+
mcp.Description("Page number for pagination (min 1)"),
223+
mcp.Min(1),
224+
)(tool)
225+
226+
mcp.WithNumber("perPage",
227+
mcp.Description("Results per page for pagination (min 1, max 100)"),
228+
mcp.Min(1),
229+
mcp.Max(100),
230+
)(tool)
231+
}
232+
}
233+
234+
type PaginationParams struct {
235+
page int
236+
perPage int
237+
}
238+
239+
// OptionalPaginationParams returns the "page" and "perPage" parameters from the request,
240+
// or their default values if not present, "page" default is 1, "perPage" default is 30.
241+
// In future, we may want to make the default values configurable, or even have this
242+
// function returned from `withPagination`, where the defaults are provided alongside
243+
// the min/max values.
244+
func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) {
245+
page, err := OptionalIntParamWithDefault(r, "page", 1)
246+
if err != nil {
247+
return PaginationParams{}, err
248+
}
249+
perPage, err := OptionalIntParamWithDefault(r, "perPage", 30)
250+
if err != nil {
251+
return PaginationParams{}, err
252+
}
253+
return PaginationParams{
254+
page: page,
255+
perPage: perPage,
256+
}, nil
257+
}

pkg/github/tools.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
7878
toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)),
7979
toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)),
8080
)
81+
82+
notifications := toolsets.NewToolset("notifications", "GitHub Notifications related tools").
83+
AddReadTools(
84+
85+
toolsets.NewServerTool(MarkNotificationRead(getClient, t)),
86+
toolsets.NewServerTool(MarkAllNotificationsRead(getClient, t)),
87+
toolsets.NewServerTool(MarkNotificationDone(getClient, t)),
88+
).
89+
AddWriteTools(
90+
toolsets.NewServerTool(GetNotifications(getClient, t)),
91+
toolsets.NewServerTool(GetNotificationThread(getClient, t)),
92+
)
93+
8194
// Keep experiments alive so the system doesn't error out when it's always enabled
8295
experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet")
8396

@@ -88,6 +101,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
88101
tsg.AddToolset(pullRequests)
89102
tsg.AddToolset(codeSecurity)
90103
tsg.AddToolset(secretProtection)
104+
tsg.AddToolset(notifications)
91105
tsg.AddToolset(experiments)
92106
// Enable the requested features
93107

0 commit comments

Comments
 (0)