Skip to content

Commit 1c6171b

Browse files
Feat: Add initial Gist tools (#340)
* Add initial Gist tools: ListGists, CreateGist * Add UpdateGist tool * Add documentation for initial Gist tools --------- Co-authored-by: Matt Holloway <mattdholloway@github.com>
1 parent 4b64121 commit 1c6171b

File tree

5 files changed

+803
-1
lines changed

5 files changed

+803
-1
lines changed

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ The following sets of tools are available (all are on by default):
287287
| `dependabot` | Dependabot tools |
288288
| `discussions` | GitHub Discussions related tools |
289289
| `experiments` | Experimental features that are not considered stable yet |
290+
| `gists` | GitHub Gist related tools |
290291
| `issues` | GitHub Issues related tools |
291292
| `notifications` | GitHub Notifications related tools |
292293
| `orgs` | GitHub Organization related tools |
@@ -472,6 +473,30 @@ The following sets of tools are available (all are on by default):
472473

473474
<details>
474475

476+
<summary>Gists</summary>
477+
478+
- **create_gist** - Create Gist
479+
- `content`: Content for simple single-file gist creation (string, required)
480+
- `description`: Description of the gist (string, optional)
481+
- `filename`: Filename for simple single-file gist creation (string, required)
482+
- `public`: Whether the gist is public (boolean, optional)
483+
484+
- **list_gists** - List Gists
485+
- `page`: Page number for pagination (min 1) (number, optional)
486+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
487+
- `since`: Only gists updated after this time (ISO 8601 timestamp) (string, optional)
488+
- `username`: GitHub username (omit for authenticated user's gists) (string, optional)
489+
490+
- **update_gist** - Update Gist
491+
- `content`: Content for the file (string, required)
492+
- `description`: Updated description of the gist (string, optional)
493+
- `filename`: Filename to update or create (string, required)
494+
- `gist_id`: ID of the gist to update (string, required)
495+
496+
</details>
497+
498+
<details>
499+
475500
<summary>Issues</summary>
476501

477502
- **add_issue_comment** - Add comment to issue
@@ -1049,4 +1074,4 @@ The exported Go API of this module should currently be considered unstable, and
10491074

10501075
## License
10511076

1052-
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.
1077+
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.

docs/remote-server.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
2525
| Dependabot | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) |
2626
| Discussions | GitHub Discussions related tools | https://api.githubcopilot.com/mcp/x/discussions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/discussions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%2Freadonly%22%7D) |
2727
| Experiments | Experimental features that are not considered stable yet | https://api.githubcopilot.com/mcp/x/experiments | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/experiments/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%2Freadonly%22%7D) |
28+
| Gists | GitHub Gist related tools | https://api.githubcopilot.com/mcp/x/gists | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/gists/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%2Freadonly%22%7D) |
2829
| Issues | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) |
2930
| Notifications | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D) |
3031
| Organizations | GitHub Organization related tools | https://api.githubcopilot.com/mcp/x/orgs | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/orgs/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%2Freadonly%22%7D) |

pkg/github/gists.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
10+
"github.com/github/github-mcp-server/pkg/translations"
11+
"github.com/google/go-github/v73/github"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/mark3labs/mcp-go/server"
14+
)
15+
16+
// ListGists creates a tool to list gists for a user
17+
func ListGists(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
18+
return mcp.NewTool("list_gists",
19+
mcp.WithDescription(t("TOOL_LIST_GISTS_DESCRIPTION", "List gists for a user")),
20+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
21+
Title: t("TOOL_LIST_GISTS", "List Gists"),
22+
ReadOnlyHint: ToBoolPtr(true),
23+
}),
24+
mcp.WithString("username",
25+
mcp.Description("GitHub username (omit for authenticated user's gists)"),
26+
),
27+
mcp.WithString("since",
28+
mcp.Description("Only gists updated after this time (ISO 8601 timestamp)"),
29+
),
30+
WithPagination(),
31+
),
32+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
33+
username, err := OptionalParam[string](request, "username")
34+
if err != nil {
35+
return mcp.NewToolResultError(err.Error()), nil
36+
}
37+
38+
since, err := OptionalParam[string](request, "since")
39+
if err != nil {
40+
return mcp.NewToolResultError(err.Error()), nil
41+
}
42+
43+
pagination, err := OptionalPaginationParams(request)
44+
if err != nil {
45+
return mcp.NewToolResultError(err.Error()), nil
46+
}
47+
48+
opts := &github.GistListOptions{
49+
ListOptions: github.ListOptions{
50+
Page: pagination.Page,
51+
PerPage: pagination.PerPage,
52+
},
53+
}
54+
55+
// Parse since timestamp if provided
56+
if since != "" {
57+
sinceTime, err := parseISOTimestamp(since)
58+
if err != nil {
59+
return mcp.NewToolResultError(fmt.Sprintf("invalid since timestamp: %v", err)), nil
60+
}
61+
opts.Since = sinceTime
62+
}
63+
64+
client, err := getClient(ctx)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
67+
}
68+
69+
gists, resp, err := client.Gists.List(ctx, username, opts)
70+
if err != nil {
71+
return nil, fmt.Errorf("failed to list gists: %w", err)
72+
}
73+
defer func() { _ = resp.Body.Close() }()
74+
75+
if resp.StatusCode != http.StatusOK {
76+
body, err := io.ReadAll(resp.Body)
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to read response body: %w", err)
79+
}
80+
return mcp.NewToolResultError(fmt.Sprintf("failed to list gists: %s", string(body))), nil
81+
}
82+
83+
r, err := json.Marshal(gists)
84+
if err != nil {
85+
return nil, fmt.Errorf("failed to marshal response: %w", err)
86+
}
87+
88+
return mcp.NewToolResultText(string(r)), nil
89+
}
90+
}
91+
92+
// CreateGist creates a tool to create a new gist
93+
func CreateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
94+
return mcp.NewTool("create_gist",
95+
mcp.WithDescription(t("TOOL_CREATE_GIST_DESCRIPTION", "Create a new gist")),
96+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
97+
Title: t("TOOL_CREATE_GIST", "Create Gist"),
98+
ReadOnlyHint: ToBoolPtr(false),
99+
}),
100+
mcp.WithString("description",
101+
mcp.Description("Description of the gist"),
102+
),
103+
mcp.WithString("filename",
104+
mcp.Required(),
105+
mcp.Description("Filename for simple single-file gist creation"),
106+
),
107+
mcp.WithString("content",
108+
mcp.Required(),
109+
mcp.Description("Content for simple single-file gist creation"),
110+
),
111+
mcp.WithBoolean("public",
112+
mcp.Description("Whether the gist is public"),
113+
mcp.DefaultBool(false),
114+
),
115+
),
116+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
117+
description, err := OptionalParam[string](request, "description")
118+
if err != nil {
119+
return mcp.NewToolResultError(err.Error()), nil
120+
}
121+
122+
filename, err := RequiredParam[string](request, "filename")
123+
if err != nil {
124+
return mcp.NewToolResultError(err.Error()), nil
125+
}
126+
127+
content, err := RequiredParam[string](request, "content")
128+
if err != nil {
129+
return mcp.NewToolResultError(err.Error()), nil
130+
}
131+
132+
public, err := OptionalParam[bool](request, "public")
133+
if err != nil {
134+
return mcp.NewToolResultError(err.Error()), nil
135+
}
136+
137+
files := make(map[github.GistFilename]github.GistFile)
138+
files[github.GistFilename(filename)] = github.GistFile{
139+
Filename: github.Ptr(filename),
140+
Content: github.Ptr(content),
141+
}
142+
143+
gist := &github.Gist{
144+
Files: files,
145+
Public: github.Ptr(public),
146+
Description: github.Ptr(description),
147+
}
148+
149+
client, err := getClient(ctx)
150+
if err != nil {
151+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
152+
}
153+
154+
createdGist, resp, err := client.Gists.Create(ctx, gist)
155+
if err != nil {
156+
return nil, fmt.Errorf("failed to create gist: %w", err)
157+
}
158+
defer func() { _ = resp.Body.Close() }()
159+
160+
if resp.StatusCode != http.StatusCreated {
161+
body, err := io.ReadAll(resp.Body)
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to read response body: %w", err)
164+
}
165+
return mcp.NewToolResultError(fmt.Sprintf("failed to create gist: %s", string(body))), nil
166+
}
167+
168+
r, err := json.Marshal(createdGist)
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to marshal response: %w", err)
171+
}
172+
173+
return mcp.NewToolResultText(string(r)), nil
174+
}
175+
}
176+
177+
// UpdateGist creates a tool to edit an existing gist
178+
func UpdateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
179+
return mcp.NewTool("update_gist",
180+
mcp.WithDescription(t("TOOL_UPDATE_GIST_DESCRIPTION", "Update an existing gist")),
181+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
182+
Title: t("TOOL_UPDATE_GIST", "Update Gist"),
183+
ReadOnlyHint: ToBoolPtr(false),
184+
}),
185+
mcp.WithString("gist_id",
186+
mcp.Required(),
187+
mcp.Description("ID of the gist to update"),
188+
),
189+
mcp.WithString("description",
190+
mcp.Description("Updated description of the gist"),
191+
),
192+
mcp.WithString("filename",
193+
mcp.Required(),
194+
mcp.Description("Filename to update or create"),
195+
),
196+
mcp.WithString("content",
197+
mcp.Required(),
198+
mcp.Description("Content for the file"),
199+
),
200+
),
201+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
202+
gistID, err := RequiredParam[string](request, "gist_id")
203+
if err != nil {
204+
return mcp.NewToolResultError(err.Error()), nil
205+
}
206+
207+
description, err := OptionalParam[string](request, "description")
208+
if err != nil {
209+
return mcp.NewToolResultError(err.Error()), nil
210+
}
211+
212+
filename, err := RequiredParam[string](request, "filename")
213+
if err != nil {
214+
return mcp.NewToolResultError(err.Error()), nil
215+
}
216+
217+
content, err := RequiredParam[string](request, "content")
218+
if err != nil {
219+
return mcp.NewToolResultError(err.Error()), nil
220+
}
221+
222+
files := make(map[github.GistFilename]github.GistFile)
223+
files[github.GistFilename(filename)] = github.GistFile{
224+
Filename: github.Ptr(filename),
225+
Content: github.Ptr(content),
226+
}
227+
228+
gist := &github.Gist{
229+
Files: files,
230+
Description: github.Ptr(description),
231+
}
232+
233+
client, err := getClient(ctx)
234+
if err != nil {
235+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
236+
}
237+
238+
updatedGist, resp, err := client.Gists.Edit(ctx, gistID, gist)
239+
if err != nil {
240+
return nil, fmt.Errorf("failed to update gist: %w", err)
241+
}
242+
defer func() { _ = resp.Body.Close() }()
243+
244+
if resp.StatusCode != http.StatusOK {
245+
body, err := io.ReadAll(resp.Body)
246+
if err != nil {
247+
return nil, fmt.Errorf("failed to read response body: %w", err)
248+
}
249+
return mcp.NewToolResultError(fmt.Sprintf("failed to update gist: %s", string(body))), nil
250+
}
251+
252+
r, err := json.Marshal(updatedGist)
253+
if err != nil {
254+
return nil, fmt.Errorf("failed to marshal response: %w", err)
255+
}
256+
257+
return mcp.NewToolResultText(string(r)), nil
258+
}
259+
}

0 commit comments

Comments
 (0)