Skip to content

Commit 2eb1887

Browse files
committed
Add UpdateGist tool
1 parent 3c47964 commit 2eb1887

File tree

3 files changed

+245
-0
lines changed

3 files changed

+245
-0
lines changed

pkg/github/gists.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,83 @@ func CreateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (to
165165
return mcp.NewToolResultText(string(r)), nil
166166
}
167167
}
168+
169+
// UpdateGist creates a tool to edit an existing gist
170+
func UpdateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
171+
return mcp.NewTool("update_gist",
172+
mcp.WithDescription(t("TOOL_UPDATE_GIST_DESCRIPTION", "Update an existing gist")),
173+
mcp.WithString("gist_id",
174+
mcp.Required(),
175+
mcp.Description("ID of the gist to update"),
176+
),
177+
mcp.WithString("description",
178+
mcp.Description("Updated description of the gist"),
179+
),
180+
mcp.WithString("filename",
181+
mcp.Required(),
182+
mcp.Description("Filename to update or create"),
183+
),
184+
mcp.WithString("content",
185+
mcp.Required(),
186+
mcp.Description("Content for the file"),
187+
),
188+
),
189+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
190+
gistID, err := requiredParam[string](request, "gist_id")
191+
if err != nil {
192+
return mcp.NewToolResultError(err.Error()), nil
193+
}
194+
195+
description, err := OptionalParam[string](request, "description")
196+
if err != nil {
197+
return mcp.NewToolResultError(err.Error()), nil
198+
}
199+
200+
filename, err := requiredParam[string](request, "filename")
201+
if err != nil {
202+
return mcp.NewToolResultError(err.Error()), nil
203+
}
204+
205+
content, err := requiredParam[string](request, "content")
206+
if err != nil {
207+
return mcp.NewToolResultError(err.Error()), nil
208+
}
209+
210+
files := make(map[github.GistFilename]github.GistFile)
211+
files[github.GistFilename(filename)] = github.GistFile{
212+
Filename: github.Ptr(filename),
213+
Content: github.Ptr(content),
214+
}
215+
216+
gist := &github.Gist{
217+
Files: files,
218+
Description: github.Ptr(description),
219+
}
220+
221+
client, err := getClient(ctx)
222+
if err != nil {
223+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
224+
}
225+
226+
updatedGist, resp, err := client.Gists.Edit(ctx, gistID, gist)
227+
if err != nil {
228+
return nil, fmt.Errorf("failed to update gist: %w", err)
229+
}
230+
defer func() { _ = resp.Body.Close() }()
231+
232+
if resp.StatusCode != http.StatusOK {
233+
body, err := io.ReadAll(resp.Body)
234+
if err != nil {
235+
return nil, fmt.Errorf("failed to read response body: %w", err)
236+
}
237+
return mcp.NewToolResultError(fmt.Sprintf("failed to update gist: %s", string(body))), nil
238+
}
239+
240+
r, err := json.Marshal(updatedGist)
241+
if err != nil {
242+
return nil, fmt.Errorf("failed to marshal response: %w", err)
243+
}
244+
245+
return mcp.NewToolResultText(string(r)), nil
246+
}
247+
}

pkg/github/gists_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,167 @@ func Test_CreateGist(t *testing.T) {
341341
})
342342
}
343343
}
344+
345+
func Test_UpdateGist(t *testing.T) {
346+
// Verify tool definition
347+
mockClient := github.NewClient(nil)
348+
tool, _ := UpdateGist(stubGetClientFn(mockClient), translations.NullTranslationHelper)
349+
350+
assert.Equal(t, "update_gist", tool.Name)
351+
assert.NotEmpty(t, tool.Description)
352+
assert.Contains(t, tool.InputSchema.Properties, "gist_id")
353+
assert.Contains(t, tool.InputSchema.Properties, "description")
354+
assert.Contains(t, tool.InputSchema.Properties, "filename")
355+
assert.Contains(t, tool.InputSchema.Properties, "content")
356+
357+
// Verify required parameters
358+
assert.Contains(t, tool.InputSchema.Required, "gist_id")
359+
assert.Contains(t, tool.InputSchema.Required, "filename")
360+
assert.Contains(t, tool.InputSchema.Required, "content")
361+
362+
// Setup mock data for test cases
363+
updatedGist := &github.Gist{
364+
ID: github.Ptr("existing-gist-id"),
365+
Description: github.Ptr("Updated Test Gist"),
366+
HTMLURL: github.Ptr("https://gist.github.com/user/existing-gist-id"),
367+
Public: github.Ptr(true),
368+
UpdatedAt: &github.Timestamp{Time: time.Now()},
369+
Owner: &github.User{Login: github.Ptr("user")},
370+
Files: map[github.GistFilename]github.GistFile{
371+
"updated.go": {
372+
Filename: github.Ptr("updated.go"),
373+
Content: github.Ptr("package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}"),
374+
},
375+
},
376+
}
377+
378+
tests := []struct {
379+
name string
380+
mockedClient *http.Client
381+
requestArgs map[string]interface{}
382+
expectError bool
383+
expectedErrMsg string
384+
expectedGist *github.Gist
385+
}{
386+
{
387+
name: "update gist successfully",
388+
mockedClient: mock.NewMockedHTTPClient(
389+
mock.WithRequestMatchHandler(
390+
mock.PatchGistsByGistId,
391+
mockResponse(t, http.StatusOK, updatedGist),
392+
),
393+
),
394+
requestArgs: map[string]interface{}{
395+
"gist_id": "existing-gist-id",
396+
"filename": "updated.go",
397+
"content": "package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}",
398+
"description": "Updated Test Gist",
399+
},
400+
expectError: false,
401+
expectedGist: updatedGist,
402+
},
403+
{
404+
name: "missing required gist_id",
405+
mockedClient: mock.NewMockedHTTPClient(),
406+
requestArgs: map[string]interface{}{
407+
"filename": "updated.go",
408+
"content": "updated content",
409+
"description": "Updated Test Gist",
410+
},
411+
expectError: true,
412+
expectedErrMsg: "missing required parameter: gist_id",
413+
},
414+
{
415+
name: "missing required filename",
416+
mockedClient: mock.NewMockedHTTPClient(),
417+
requestArgs: map[string]interface{}{
418+
"gist_id": "existing-gist-id",
419+
"content": "updated content",
420+
"description": "Updated Test Gist",
421+
},
422+
expectError: true,
423+
expectedErrMsg: "missing required parameter: filename",
424+
},
425+
{
426+
name: "missing required content",
427+
mockedClient: mock.NewMockedHTTPClient(),
428+
requestArgs: map[string]interface{}{
429+
"gist_id": "existing-gist-id",
430+
"filename": "updated.go",
431+
"description": "Updated Test Gist",
432+
},
433+
expectError: true,
434+
expectedErrMsg: "missing required parameter: content",
435+
},
436+
{
437+
name: "api returns error",
438+
mockedClient: mock.NewMockedHTTPClient(
439+
mock.WithRequestMatchHandler(
440+
mock.PatchGistsByGistId,
441+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
442+
w.WriteHeader(http.StatusNotFound)
443+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
444+
}),
445+
),
446+
),
447+
requestArgs: map[string]interface{}{
448+
"gist_id": "nonexistent-gist-id",
449+
"filename": "updated.go",
450+
"content": "package main",
451+
"description": "Updated Test Gist",
452+
},
453+
expectError: true,
454+
expectedErrMsg: "failed to update gist",
455+
},
456+
}
457+
458+
for _, tc := range tests {
459+
t.Run(tc.name, func(t *testing.T) {
460+
// Setup client with mock
461+
client := github.NewClient(tc.mockedClient)
462+
_, handler := UpdateGist(stubGetClientFn(client), translations.NullTranslationHelper)
463+
464+
// Create call request
465+
request := createMCPRequest(tc.requestArgs)
466+
467+
// Call handler
468+
result, err := handler(context.Background(), request)
469+
470+
// Verify results
471+
if tc.expectError {
472+
if err != nil {
473+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
474+
} else {
475+
// For errors returned as part of the result, not as an error
476+
assert.NotNil(t, result)
477+
textContent := getTextResult(t, result)
478+
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
479+
}
480+
return
481+
}
482+
483+
require.NoError(t, err)
484+
assert.NotNil(t, result)
485+
486+
// Parse the result and get the text content
487+
textContent := getTextResult(t, result)
488+
489+
// Unmarshal and verify the result
490+
var gist *github.Gist
491+
err = json.Unmarshal([]byte(textContent.Text), &gist)
492+
require.NoError(t, err)
493+
494+
assert.Equal(t, *tc.expectedGist.ID, *gist.ID)
495+
assert.Equal(t, *tc.expectedGist.Description, *gist.Description)
496+
assert.Equal(t, *tc.expectedGist.HTMLURL, *gist.HTMLURL)
497+
498+
// Verify file content
499+
for filename, expectedFile := range tc.expectedGist.Files {
500+
actualFile, exists := gist.Files[filename]
501+
assert.True(t, exists)
502+
assert.Equal(t, *expectedFile.Filename, *actualFile.Filename)
503+
assert.Equal(t, *expectedFile.Content, *actualFile.Content)
504+
}
505+
})
506+
}
507+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
8888
).
8989
AddWriteTools(
9090
toolsets.NewServerTool(CreateGist(getClient, t)),
91+
toolsets.NewServerTool(UpdateGist(getClient, t)),
9192
)
9293

9394
// Add toolsets to the group

0 commit comments

Comments
 (0)