Skip to content

Commit 668de2c

Browse files
committed
Add UpdateGist tool
1 parent ebbda93 commit 668de2c

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
@@ -81,6 +81,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
8181
).
8282
AddWriteTools(
8383
toolsets.NewServerTool(CreateGist(getClient, t)),
84+
toolsets.NewServerTool(UpdateGist(getClient, t)),
8485
)
8586

8687
// Add toolsets to the group

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy