Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/describe

Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `state`: Alert state (string, optional)
- `severity`: Alert severity (string, optional)

### Version

- **get_latest_version** - Get the latest version of the GitHub MCP server to see if you are up to date

- No parameters required for this tool

## Resources

### Repository Content
Expand Down
53 changes: 53 additions & 0 deletions pkg/github/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"time"

"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v69/github"
Expand Down Expand Up @@ -77,6 +78,10 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati
// Add GitHub tools - Code Scanning
s.AddTool(getCodeScanningAlert(client, t))
s.AddTool(listCodeScanningAlerts(client, t))

// Add GitHub tools - latest version
s.AddTool(getLatestVersion(client, version, t))

return s
}

Expand Down Expand Up @@ -112,6 +117,54 @@ func getMe(client *github.Client, t translations.TranslationHelperFunc) (tool mc
}
}

// getLatestVersion creates a tool to get the latest version of the server.
// getMe creates a tool to get details of the authenticated user.
func getLatestVersion(client *github.Client, currentVersion string, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_latest_version",
mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Checks the latest available version of the GitHub MCP server to see if it is up to date.")),
mcp.WithString("reason",
mcp.Description("Optional: reason the session was created"),
),
),
func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Get the latest release from GitHub API
release, resp, err := client.Repositories.GetLatestRelease(ctx, "github", "github-mcp-server")
if err != nil {
return nil, fmt.Errorf("failed to get latest release: %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 get latest release: %s", string(body))), nil
}

latestVersion := release.GetTagName()
if latestVersion == "" {
latestVersion = release.GetName()
}

// Compare versions and create response
result := map[string]interface{}{
"current_version": currentVersion,
"latest_version": latestVersion,
"up_to_date": currentVersion == latestVersion,
"release_url": release.GetHTMLURL(),
"published_at": release.GetPublishedAt().Format(time.RFC3339),
}

r, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal result: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}

// isAcceptedError checks if the error is an accepted error.
func isAcceptedError(err error) bool {
var acceptedError *github.AcceptedError
Expand Down
116 changes: 116 additions & 0 deletions pkg/github/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,119 @@ func TestOptionalPaginationParams(t *testing.T) {
})
}
}

func Test_GetLatestVersion(t *testing.T) {
// Verify tool definition
mockClient := github.NewClient(nil)
tool, _ := getLatestVersion(mockClient, "v1.0.0", translations.NullTranslationHelper)

assert.Equal(t, "get_latest_version", tool.Name)
assert.NotEmpty(t, tool.Description)

// Setup mock release response
mockRelease := &github.RepositoryRelease{
TagName: github.Ptr("v1.1.0"),
Name: github.Ptr("Release v1.1.0"),
HTMLURL: github.Ptr("https://github.com/github/github-mcp-server/releases/tag/v1.1.0"),
PublishedAt: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)},
}

tests := []struct {
name string
mockedClient *http.Client
currentVersion string
expectError bool
expectedResult map[string]interface{}
expectedErrMsg string
}{
{
name: "successful get latest version - up to date",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposReleasesLatestByOwnerByRepo,
mockResponse(t, http.StatusOK, mockRelease),
),
),
currentVersion: "v1.1.0",
expectError: false,
expectedResult: map[string]interface{}{
"current_version": "v1.1.0",
"latest_version": "v1.1.0",
"up_to_date": true,
"release_url": "https://github.com/github/github-mcp-server/releases/tag/v1.1.0",
// We can't test exact published_at since it's dynamic
},
},
{
name: "successful get latest version - outdated",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposReleasesLatestByOwnerByRepo,
mockResponse(t, http.StatusOK, mockRelease),
),
),
currentVersion: "v1.0.0",
expectError: false,
expectedResult: map[string]interface{}{
"current_version": "v1.0.0",
"latest_version": "v1.1.0",
"up_to_date": false,
"release_url": "https://github.com/github/github-mcp-server/releases/tag/v1.1.0",
// We can't test exact published_at since it's dynamic
},
},
{
name: "API request fails",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposReleasesLatestByOwnerByRepo,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
),
),
currentVersion: "v1.0.0",
expectError: true,
expectedErrMsg: "failed to get latest release",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := getLatestVersion(client, tc.currentVersion, translations.NullTranslationHelper)

// Create call request with empty parameters (none needed for this API)
request := createMCPRequest(map[string]interface{}{})

// Call handler
result, err := handler(context.Background(), request)

// Verify results
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErrMsg)
return
}

require.NoError(t, err)

// Parse result and get text content
textContent := getTextResult(t, result)

// Unmarshal and verify the result
var resultMap map[string]interface{}
err = json.Unmarshal([]byte(textContent.Text), &resultMap)
require.NoError(t, err)

// Verify expected fields
assert.Equal(t, tc.expectedResult["current_version"], resultMap["current_version"])
assert.Equal(t, tc.expectedResult["latest_version"], resultMap["latest_version"])
assert.Equal(t, tc.expectedResult["up_to_date"], resultMap["up_to_date"])
assert.Equal(t, tc.expectedResult["release_url"], resultMap["release_url"])
assert.NotEmpty(t, resultMap["published_at"])
})
}
}