Repository: github/github-mcp-server

Issue:
GIST tools
### Describe the feature or problem you’d like to solve

To let `github-mcp-server` be able to work with gists

### Proposed solution

Implement gist RWD tools

### Additional context

N/A

Discussion:

Solution patch:
diff --git a/README.md b/README.md
index 165f336..3c9eb67 100644
--- a/README.md
+++ b/README.md
@@ -287,6 +287,7 @@ The following sets of tools are available (all are on by default):
 | `dependabot` | Dependabot tools |
 | `discussions` | GitHub Discussions related tools |
 | `experiments` | Experimental features that are not considered stable yet |
+| `gists` | GitHub Gist related tools |
 | `issues` | GitHub Issues related tools |
 | `notifications` | GitHub Notifications related tools |
 | `orgs` | GitHub Organization related tools |
@@ -472,6 +473,30 @@ The following sets of tools are available (all are on by default):
 
 <details>
 
+<summary>Gists</summary>
+
+- **create_gist** - Create Gist
+  - `content`: Content for simple single-file gist creation (string, required)
+  - `description`: Description of the gist (string, optional)
+  - `filename`: Filename for simple single-file gist creation (string, required)
+  - `public`: Whether the gist is public (boolean, optional)
+
+- **list_gists** - List Gists
+  - `page`: Page number for pagination (min 1) (number, optional)
+  - `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
+  - `since`: Only gists updated after this time (ISO 8601 timestamp) (string, optional)
+  - `username`: GitHub username (omit for authenticated user's gists) (string, optional)
+
+- **update_gist** - Update Gist
+  - `content`: Content for the file (string, required)
+  - `description`: Updated description of the gist (string, optional)
+  - `filename`: Filename to update or create (string, required)
+  - `gist_id`: ID of the gist to update (string, required)
+
+</details>
+
+<details>
+
 <summary>Issues</summary>
 
 - **add_issue_comment** - Add comment to issue
@@ -1049,4 +1074,4 @@ The exported Go API of this module should currently be considered unstable, and
 
 ## License
 
-This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.
+This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.
\ No newline at end of file
diff --git a/docs/remote-server.md b/docs/remote-server.md
index 49794c6..5f57f49 100644
--- a/docs/remote-server.md
+++ b/docs/remote-server.md
@@ -25,6 +25,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
 | 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)                                                                    |
 | 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)                                                                  |
 | 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)                                                                  |
+| 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)                                                                              |
 | 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)                                                                            |
 | 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)                                                              |
 | 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)                                                                                |
diff --git a/pkg/github/gists.go b/pkg/github/gists.go
new file mode 100644
index 0000000..403804c
--- /dev/null
+++ b/pkg/github/gists.go
@@ -0,0 +1,259 @@
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/github/github-mcp-server/pkg/translations"
+	"github.com/google/go-github/v73/github"
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+)
+
+// ListGists creates a tool to list gists for a user
+func ListGists(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool("list_gists",
+			mcp.WithDescription(t("TOOL_LIST_GISTS_DESCRIPTION", "List gists for a user")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_GISTS", "List Gists"),
+				ReadOnlyHint: ToBoolPtr(true),
+			}),
+			mcp.WithString("username",
+				mcp.Description("GitHub username (omit for authenticated user's gists)"),
+			),
+			mcp.WithString("since",
+				mcp.Description("Only gists updated after this time (ISO 8601 timestamp)"),
+			),
+			WithPagination(),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			username, err := OptionalParam[string](request, "username")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			since, err := OptionalParam[string](request, "since")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			pagination, err := OptionalPaginationParams(request)
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			opts := &github.GistListOptions{
+				ListOptions: github.ListOptions{
+					Page:    pagination.Page,
+					PerPage: pagination.PerPage,
+				},
+			}
+
+			// Parse since timestamp if provided
+			if since != "" {
+				sinceTime, err := parseISOTimestamp(since)
+				if err != nil {
+					return mcp.NewToolResultError(fmt.Sprintf("invalid since timestamp: %v", err)), nil
+				}
+				opts.Since = sinceTime
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			gists, resp, err := client.Gists.List(ctx, username, opts)
+			if err != nil {
+				return nil, fmt.Errorf("failed to list gists: %w", err)
+			}
+			defer func() { _ = resp.Body.Close() }()
+
+			if resp.StatusCode != http.StatusOK {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					return nil, fmt.Errorf("failed to read response body: %w", err)
+				}
+				return mcp.NewToolResultError(fmt.Sprintf("failed to list gists: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(gists)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
+
+// CreateGist creates a tool to create a new gist
+func CreateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool("create_gist",
+			mcp.WithDescription(t("TOOL_CREATE_GIST_DESCRIPTION", "Create a new gist")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_CREATE_GIST", "Create Gist"),
+				ReadOnlyHint: ToBoolPtr(false),
+			}),
+			mcp.WithString("description",
+				mcp.Description("Description of the gist"),
+			),
+			mcp.WithString("filename",
+				mcp.Required(),
+				mcp.Description("Filename for simple single-file gist creation"),
+			),
+			mcp.WithString("content",
+				mcp.Required(),
+				mcp.Description("Content for simple single-file gist creation"),
+			),
+			mcp.WithBoolean("public",
+				mcp.Description("Whether the gist is public"),
+				mcp.DefaultBool(false),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			description, err := OptionalParam[string](request, "description")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			filename, err := RequiredParam[string](request, "filename")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			content, err := RequiredParam[string](request, "content")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			public, err := OptionalParam[bool](request, "public")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			files := make(map[github.GistFilename]github.GistFile)
+			files[github.GistFilename(filename)] = github.GistFile{
+				Filename: github.Ptr(filename),
+				Content:  github.Ptr(content),
+			}
+
+			gist := &github.Gist{
+				Files:       files,
+				Public:      github.Ptr(public),
+				Description: github.Ptr(description),
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			createdGist, resp, err := client.Gists.Create(ctx, gist)
+			if err != nil {
+				return nil, fmt.Errorf("failed to create gist: %w", err)
+			}
+			defer func() { _ = resp.Body.Close() }()
+
+			if resp.StatusCode != http.StatusCreated {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					return nil, fmt.Errorf("failed to read response body: %w", err)
+				}
+				return mcp.NewToolResultError(fmt.Sprintf("failed to create gist: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(createdGist)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
+
+// UpdateGist creates a tool to edit an existing gist
+func UpdateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool("update_gist",
+			mcp.WithDescription(t("TOOL_UPDATE_GIST_DESCRIPTION", "Update an existing gist")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_UPDATE_GIST", "Update Gist"),
+				ReadOnlyHint: ToBoolPtr(false),
+			}),
+			mcp.WithString("gist_id",
+				mcp.Required(),
+				mcp.Description("ID of the gist to update"),
+			),
+			mcp.WithString("description",
+				mcp.Description("Updated description of the gist"),
+			),
+			mcp.WithString("filename",
+				mcp.Required(),
+				mcp.Description("Filename to update or create"),
+			),
+			mcp.WithString("content",
+				mcp.Required(),
+				mcp.Description("Content for the file"),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			gistID, err := RequiredParam[string](request, "gist_id")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			description, err := OptionalParam[string](request, "description")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			filename, err := RequiredParam[string](request, "filename")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			content, err := RequiredParam[string](request, "content")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			files := make(map[github.GistFilename]github.GistFile)
+			files[github.GistFilename(filename)] = github.GistFile{
+				Filename: github.Ptr(filename),
+				Content:  github.Ptr(content),
+			}
+
+			gist := &github.Gist{
+				Files:       files,
+				Description: github.Ptr(description),
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			updatedGist, resp, err := client.Gists.Edit(ctx, gistID, gist)
+			if err != nil {
+				return nil, fmt.Errorf("failed to update gist: %w", err)
+			}
+			defer func() { _ = resp.Body.Close() }()
+
+			if resp.StatusCode != http.StatusOK {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					return nil, fmt.Errorf("failed to read response body: %w", err)
+				}
+				return mcp.NewToolResultError(fmt.Sprintf("failed to update gist: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(updatedGist)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 499defc..7fb1d39 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -164,6 +164,15 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
 			toolsets.NewServerTool(GetMe(getClient, t)),
 		)
 
+	gists := toolsets.NewToolset("gists", "GitHub Gist related tools").
+		AddReadTools(
+			toolsets.NewServerTool(ListGists(getClient, t)),
+		).
+		AddWriteTools(
+			toolsets.NewServerTool(CreateGist(getClient, t)),
+			toolsets.NewServerTool(UpdateGist(getClient, t)),
+		)
+
 	// Add toolsets to the group
 	tsg.AddToolset(contextTools)
 	tsg.AddToolset(repos)
@@ -178,6 +187,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
 	tsg.AddToolset(notifications)
 	tsg.AddToolset(experiments)
 	tsg.AddToolset(discussions)
+	tsg.AddToolset(gists)
 
 	return tsg
 }



Test patch:
diff --git a/pkg/github/gists_test.go b/pkg/github/gists_test.go
new file mode 100644
index 0000000..4234229
--- /dev/null
+++ b/pkg/github/gists_test.go
@@ -0,0 +1,507 @@
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/github/github-mcp-server/pkg/translations"
+	"github.com/google/go-github/v73/github"
+	"github.com/migueleliasweb/go-github-mock/src/mock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_ListGists(t *testing.T) {
+	// Verify tool definition
+	mockClient := github.NewClient(nil)
+	tool, _ := ListGists(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+	assert.Equal(t, "list_gists", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "username")
+	assert.Contains(t, tool.InputSchema.Properties, "since")
+	assert.Contains(t, tool.InputSchema.Properties, "page")
+	assert.Contains(t, tool.InputSchema.Properties, "perPage")
+	assert.Empty(t, tool.InputSchema.Required)
+
+	// Setup mock gists for success case
+	mockGists := []*github.Gist{
+		{
+			ID:          github.Ptr("gist1"),
+			Description: github.Ptr("First Gist"),
+			HTMLURL:     github.Ptr("https://gist.github.com/user/gist1"),
+			Public:      github.Ptr(true),
+			CreatedAt:   &github.Timestamp{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)},
+			Owner:       &github.User{Login: github.Ptr("user")},
+			Files: map[github.GistFilename]github.GistFile{
+				"file1.txt": {
+					Filename: github.Ptr("file1.txt"),
+					Content:  github.Ptr("content of file 1"),
+				},
+			},
+		},
+		{
+			ID:          github.Ptr("gist2"),
+			Description: github.Ptr("Second Gist"),
+			HTMLURL:     github.Ptr("https://gist.github.com/testuser/gist2"),
+			Public:      github.Ptr(false),
+			CreatedAt:   &github.Timestamp{Time: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)},
+			Owner:       &github.User{Login: github.Ptr("testuser")},
+			Files: map[github.GistFilename]github.GistFile{
+				"file2.js": {
+					Filename: github.Ptr("file2.js"),
+					Content:  github.Ptr("console.log('hello');"),
+				},
+			},
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedGists  []*github.Gist
+		expectedErrMsg string
+	}{
+		{
+			name: "list authenticated user's gists",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatch(
+					mock.GetGists,
+					mockGists,
+				),
+			),
+			requestArgs:   map[string]interface{}{},
+			expectError:   false,
+			expectedGists: mockGists,
+		},
+		{
+			name: "list specific user's gists",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetUsersGistsByUsername,
+					mockResponse(t, http.StatusOK, mockGists),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"username": "testuser",
+			},
+			expectError:   false,
+			expectedGists: mockGists,
+		},
+		{
+			name: "list gists with pagination and since parameter",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetGists,
+					expectQueryParams(t, map[string]string{
+						"since":    "2023-01-01T00:00:00Z",
+						"page":     "2",
+						"per_page": "5",
+					}).andThen(
+						mockResponse(t, http.StatusOK, mockGists),
+					),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"since":   "2023-01-01T00:00:00Z",
+				"page":    float64(2),
+				"perPage": float64(5),
+			},
+			expectError:   false,
+			expectedGists: mockGists,
+		},
+		{
+			name: "invalid since parameter",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatch(
+					mock.GetGists,
+					mockGists,
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"since": "invalid-date",
+			},
+			expectError:    true,
+			expectedErrMsg: "invalid since timestamp",
+		},
+		{
+			name: "list gists fails with error",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetGists,
+					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+						w.WriteHeader(http.StatusUnauthorized)
+						_, _ = w.Write([]byte(`{"message": "Requires authentication"}`))
+					}),
+				),
+			),
+			requestArgs:    map[string]interface{}{},
+			expectError:    true,
+			expectedErrMsg: "failed to list gists",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := ListGists(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				if err != nil {
+					assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				} else {
+					// For errors returned as part of the result, not as an error
+					assert.NotNil(t, result)
+					textContent := getTextResult(t, result)
+					assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+				}
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var returnedGists []*github.Gist
+			err = json.Unmarshal([]byte(textContent.Text), &returnedGists)
+			require.NoError(t, err)
+
+			assert.Len(t, returnedGists, len(tc.expectedGists))
+			for i, gist := range returnedGists {
+				assert.Equal(t, *tc.expectedGists[i].ID, *gist.ID)
+				assert.Equal(t, *tc.expectedGists[i].Description, *gist.Description)
+				assert.Equal(t, *tc.expectedGists[i].HTMLURL, *gist.HTMLURL)
+				assert.Equal(t, *tc.expectedGists[i].Public, *gist.Public)
+			}
+		})
+	}
+}
+
+func Test_CreateGist(t *testing.T) {
+	// Verify tool definition
+	mockClient := github.NewClient(nil)
+	tool, _ := CreateGist(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+	assert.Equal(t, "create_gist", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "description")
+	assert.Contains(t, tool.InputSchema.Properties, "filename")
+	assert.Contains(t, tool.InputSchema.Properties, "content")
+	assert.Contains(t, tool.InputSchema.Properties, "public")
+
+	// Verify required parameters
+	assert.Contains(t, tool.InputSchema.Required, "filename")
+	assert.Contains(t, tool.InputSchema.Required, "content")
+
+	// Setup mock data for test cases
+	createdGist := &github.Gist{
+		ID:          github.Ptr("new-gist-id"),
+		Description: github.Ptr("Test Gist"),
+		HTMLURL:     github.Ptr("https://gist.github.com/user/new-gist-id"),
+		Public:      github.Ptr(false),
+		CreatedAt:   &github.Timestamp{Time: time.Now()},
+		Owner:       &github.User{Login: github.Ptr("user")},
+		Files: map[github.GistFilename]github.GistFile{
+			"test.go": {
+				Filename: github.Ptr("test.go"),
+				Content:  github.Ptr("package main\n\nfunc main() {\n\tfmt.Println(\"Hello, Gist!\")\n}"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedErrMsg string
+		expectedGist   *github.Gist
+	}{
+		{
+			name: "create gist successfully",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostGists,
+					mockResponse(t, http.StatusCreated, createdGist),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"filename":    "test.go",
+				"content":     "package main\n\nfunc main() {\n\tfmt.Println(\"Hello, Gist!\")\n}",
+				"description": "Test Gist",
+				"public":      false,
+			},
+			expectError:  false,
+			expectedGist: createdGist,
+		},
+		{
+			name:         "missing required filename",
+			mockedClient: mock.NewMockedHTTPClient(),
+			requestArgs: map[string]interface{}{
+				"content":     "test content",
+				"description": "Test Gist",
+			},
+			expectError:    true,
+			expectedErrMsg: "missing required parameter: filename",
+		},
+		{
+			name:         "missing required content",
+			mockedClient: mock.NewMockedHTTPClient(),
+			requestArgs: map[string]interface{}{
+				"filename":    "test.go",
+				"description": "Test Gist",
+			},
+			expectError:    true,
+			expectedErrMsg: "missing required parameter: content",
+		},
+		{
+			name: "api returns error",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PostGists,
+					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+						w.WriteHeader(http.StatusUnauthorized)
+						_, _ = w.Write([]byte(`{"message": "Requires authentication"}`))
+					}),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"filename":    "test.go",
+				"content":     "package main",
+				"description": "Test Gist",
+			},
+			expectError:    true,
+			expectedErrMsg: "failed to create gist",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := CreateGist(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				if err != nil {
+					assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				} else {
+					// For errors returned as part of the result, not as an error
+					assert.NotNil(t, result)
+					textContent := getTextResult(t, result)
+					assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+				}
+				return
+			}
+
+			require.NoError(t, err)
+			assert.NotNil(t, result)
+
+			// Parse the result and get the text content
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var gist *github.Gist
+			err = json.Unmarshal([]byte(textContent.Text), &gist)
+			require.NoError(t, err)
+
+			assert.Equal(t, *tc.expectedGist.ID, *gist.ID)
+			assert.Equal(t, *tc.expectedGist.Description, *gist.Description)
+			assert.Equal(t, *tc.expectedGist.HTMLURL, *gist.HTMLURL)
+			assert.Equal(t, *tc.expectedGist.Public, *gist.Public)
+
+			// Verify file content
+			for filename, expectedFile := range tc.expectedGist.Files {
+				actualFile, exists := gist.Files[filename]
+				assert.True(t, exists)
+				assert.Equal(t, *expectedFile.Filename, *actualFile.Filename)
+				assert.Equal(t, *expectedFile.Content, *actualFile.Content)
+			}
+		})
+	}
+}
+
+func Test_UpdateGist(t *testing.T) {
+	// Verify tool definition
+	mockClient := github.NewClient(nil)
+	tool, _ := UpdateGist(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+	assert.Equal(t, "update_gist", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "gist_id")
+	assert.Contains(t, tool.InputSchema.Properties, "description")
+	assert.Contains(t, tool.InputSchema.Properties, "filename")
+	assert.Contains(t, tool.InputSchema.Properties, "content")
+
+	// Verify required parameters
+	assert.Contains(t, tool.InputSchema.Required, "gist_id")
+	assert.Contains(t, tool.InputSchema.Required, "filename")
+	assert.Contains(t, tool.InputSchema.Required, "content")
+
+	// Setup mock data for test cases
+	updatedGist := &github.Gist{
+		ID:          github.Ptr("existing-gist-id"),
+		Description: github.Ptr("Updated Test Gist"),
+		HTMLURL:     github.Ptr("https://gist.github.com/user/existing-gist-id"),
+		Public:      github.Ptr(true),
+		UpdatedAt:   &github.Timestamp{Time: time.Now()},
+		Owner:       &github.User{Login: github.Ptr("user")},
+		Files: map[github.GistFilename]github.GistFile{
+			"updated.go": {
+				Filename: github.Ptr("updated.go"),
+				Content:  github.Ptr("package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedErrMsg string
+		expectedGist   *github.Gist
+	}{
+		{
+			name: "update gist successfully",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchGistsByGistId,
+					mockResponse(t, http.StatusOK, updatedGist),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"gist_id":     "existing-gist-id",
+				"filename":    "updated.go",
+				"content":     "package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}",
+				"description": "Updated Test Gist",
+			},
+			expectError:  false,
+			expectedGist: updatedGist,
+		},
+		{
+			name:         "missing required gist_id",
+			mockedClient: mock.NewMockedHTTPClient(),
+			requestArgs: map[string]interface{}{
+				"filename":    "updated.go",
+				"content":     "updated content",
+				"description": "Updated Test Gist",
+			},
+			expectError:    true,
+			expectedErrMsg: "missing required parameter: gist_id",
+		},
+		{
+			name:         "missing required filename",
+			mockedClient: mock.NewMockedHTTPClient(),
+			requestArgs: map[string]interface{}{
+				"gist_id":     "existing-gist-id",
+				"content":     "updated content",
+				"description": "Updated Test Gist",
+			},
+			expectError:    true,
+			expectedErrMsg: "missing required parameter: filename",
+		},
+		{
+			name:         "missing required content",
+			mockedClient: mock.NewMockedHTTPClient(),
+			requestArgs: map[string]interface{}{
+				"gist_id":     "existing-gist-id",
+				"filename":    "updated.go",
+				"description": "Updated Test Gist",
+			},
+			expectError:    true,
+			expectedErrMsg: "missing required parameter: content",
+		},
+		{
+			name: "api returns error",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.PatchGistsByGistId,
+					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+						w.WriteHeader(http.StatusNotFound)
+						_, _ = w.Write([]byte(`{"message": "Not Found"}`))
+					}),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"gist_id":     "nonexistent-gist-id",
+				"filename":    "updated.go",
+				"content":     "package main",
+				"description": "Updated Test Gist",
+			},
+			expectError:    true,
+			expectedErrMsg: "failed to update gist",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := UpdateGist(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				if err != nil {
+					assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				} else {
+					// For errors returned as part of the result, not as an error
+					assert.NotNil(t, result)
+					textContent := getTextResult(t, result)
+					assert.Contains(t, textContent.Text, tc.expectedErrMsg)
+				}
+				return
+			}
+
+			require.NoError(t, err)
+			assert.NotNil(t, result)
+
+			// Parse the result and get the text content
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var gist *github.Gist
+			err = json.Unmarshal([]byte(textContent.Text), &gist)
+			require.NoError(t, err)
+
+			assert.Equal(t, *tc.expectedGist.ID, *gist.ID)
+			assert.Equal(t, *tc.expectedGist.Description, *gist.Description)
+			assert.Equal(t, *tc.expectedGist.HTMLURL, *gist.HTMLURL)
+
+			// Verify file content
+			for filename, expectedFile := range tc.expectedGist.Files {
+				actualFile, exists := gist.Files[filename]
+				assert.True(t, exists)
+				assert.Equal(t, *expectedFile.Filename, *actualFile.Filename)
+				assert.Equal(t, *expectedFile.Content, *actualFile.Content)
+			}
+		})
+	}
+}