Skip to content

Commit 39ff840

Browse files
committed
add experimental get_latest_version tool
1 parent 973fb5d commit 39ff840

File tree

3 files changed

+175
-0
lines changed

3 files changed

+175
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
382382
- `state`: Alert state (string, optional)
383383
- `severity`: Alert severity (string, optional)
384384

385+
### Version
386+
387+
- **get_latest_version** - Get the latest version of the GitHub MCP server to see if you are up to date
388+
389+
- No parameters required for this tool
390+
385391
## Resources
386392

387393
### Repository Content

pkg/github/server.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"net/http"
10+
"time"
1011

1112
"github.com/github/github-mcp-server/pkg/translations"
1213
"github.com/google/go-github/v69/github"
@@ -77,6 +78,10 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati
7778
// Add GitHub tools - Code Scanning
7879
s.AddTool(getCodeScanningAlert(client, t))
7980
s.AddTool(listCodeScanningAlerts(client, t))
81+
82+
// Add GitHub tools - latest version
83+
s.AddTool(getLatestVersion(client, version, t))
84+
8085
return s
8186
}
8287

@@ -112,6 +117,54 @@ func getMe(client *github.Client, t translations.TranslationHelperFunc) (tool mc
112117
}
113118
}
114119

120+
// getLatestVersion creates a tool to get the latest version of the server.
121+
// getMe creates a tool to get details of the authenticated user.
122+
func getLatestVersion(client *github.Client, currentVersion string, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
123+
return mcp.NewTool("get_latest_version",
124+
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.")),
125+
mcp.WithString("reason",
126+
mcp.Description("Optional: reason the session was created"),
127+
),
128+
),
129+
func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
130+
// Get the latest release from GitHub API
131+
release, resp, err := client.Repositories.GetLatestRelease(ctx, "github", "github-mcp-server")
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to get latest release: %w", err)
134+
}
135+
defer func() { _ = resp.Body.Close() }()
136+
137+
if resp.StatusCode != http.StatusOK {
138+
body, err := io.ReadAll(resp.Body)
139+
if err != nil {
140+
return nil, fmt.Errorf("failed to read response body: %w", err)
141+
}
142+
return mcp.NewToolResultError(fmt.Sprintf("failed to get latest release: %s", string(body))), nil
143+
}
144+
145+
latestVersion := release.GetTagName()
146+
if latestVersion == "" {
147+
latestVersion = release.GetName()
148+
}
149+
150+
// Compare versions and create response
151+
result := map[string]interface{}{
152+
"current_version": currentVersion,
153+
"latest_version": latestVersion,
154+
"up_to_date": currentVersion == latestVersion,
155+
"release_url": release.GetHTMLURL(),
156+
"published_at": release.GetPublishedAt().Format(time.RFC3339),
157+
}
158+
159+
r, err := json.Marshal(result)
160+
if err != nil {
161+
return nil, fmt.Errorf("failed to marshal result: %w", err)
162+
}
163+
164+
return mcp.NewToolResultText(string(r)), nil
165+
}
166+
}
167+
115168
// isAcceptedError checks if the error is an accepted error.
116169
func isAcceptedError(err error) bool {
117170
var acceptedError *github.AcceptedError

pkg/github/server_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,3 +634,119 @@ func TestOptionalPaginationParams(t *testing.T) {
634634
})
635635
}
636636
}
637+
638+
func Test_GetLatestVersion(t *testing.T) {
639+
// Verify tool definition
640+
mockClient := github.NewClient(nil)
641+
tool, _ := getLatestVersion(mockClient, "v1.0.0", translations.NullTranslationHelper)
642+
643+
assert.Equal(t, "get_latest_version", tool.Name)
644+
assert.NotEmpty(t, tool.Description)
645+
646+
// Setup mock release response
647+
mockRelease := &github.RepositoryRelease{
648+
TagName: github.Ptr("v1.1.0"),
649+
Name: github.Ptr("Release v1.1.0"),
650+
HTMLURL: github.Ptr("https://github.com/github/github-mcp-server/releases/tag/v1.1.0"),
651+
PublishedAt: &github.Timestamp{Time: time.Now().Add(-24 * time.Hour)},
652+
}
653+
654+
tests := []struct {
655+
name string
656+
mockedClient *http.Client
657+
currentVersion string
658+
expectError bool
659+
expectedResult map[string]interface{}
660+
expectedErrMsg string
661+
}{
662+
{
663+
name: "successful get latest version - up to date",
664+
mockedClient: mock.NewMockedHTTPClient(
665+
mock.WithRequestMatchHandler(
666+
mock.GetReposReleasesLatestByOwnerByRepo,
667+
mockResponse(t, http.StatusOK, mockRelease),
668+
),
669+
),
670+
currentVersion: "v1.1.0",
671+
expectError: false,
672+
expectedResult: map[string]interface{}{
673+
"current_version": "v1.1.0",
674+
"latest_version": "v1.1.0",
675+
"up_to_date": true,
676+
"release_url": "https://github.com/github/github-mcp-server/releases/tag/v1.1.0",
677+
// We can't test exact published_at since it's dynamic
678+
},
679+
},
680+
{
681+
name: "successful get latest version - outdated",
682+
mockedClient: mock.NewMockedHTTPClient(
683+
mock.WithRequestMatchHandler(
684+
mock.GetReposReleasesLatestByOwnerByRepo,
685+
mockResponse(t, http.StatusOK, mockRelease),
686+
),
687+
),
688+
currentVersion: "v1.0.0",
689+
expectError: false,
690+
expectedResult: map[string]interface{}{
691+
"current_version": "v1.0.0",
692+
"latest_version": "v1.1.0",
693+
"up_to_date": false,
694+
"release_url": "https://github.com/github/github-mcp-server/releases/tag/v1.1.0",
695+
// We can't test exact published_at since it's dynamic
696+
},
697+
},
698+
{
699+
name: "API request fails",
700+
mockedClient: mock.NewMockedHTTPClient(
701+
mock.WithRequestMatchHandler(
702+
mock.GetReposReleasesLatestByOwnerByRepo,
703+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
704+
w.WriteHeader(http.StatusNotFound)
705+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
706+
}),
707+
),
708+
),
709+
currentVersion: "v1.0.0",
710+
expectError: true,
711+
expectedErrMsg: "failed to get latest release",
712+
},
713+
}
714+
715+
for _, tc := range tests {
716+
t.Run(tc.name, func(t *testing.T) {
717+
// Setup client with mock
718+
client := github.NewClient(tc.mockedClient)
719+
_, handler := getLatestVersion(client, tc.currentVersion, translations.NullTranslationHelper)
720+
721+
// Create call request with empty parameters (none needed for this API)
722+
request := createMCPRequest(map[string]interface{}{})
723+
724+
// Call handler
725+
result, err := handler(context.Background(), request)
726+
727+
// Verify results
728+
if tc.expectError {
729+
require.Error(t, err)
730+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
731+
return
732+
}
733+
734+
require.NoError(t, err)
735+
736+
// Parse result and get text content
737+
textContent := getTextResult(t, result)
738+
739+
// Unmarshal and verify the result
740+
var resultMap map[string]interface{}
741+
err = json.Unmarshal([]byte(textContent.Text), &resultMap)
742+
require.NoError(t, err)
743+
744+
// Verify expected fields
745+
assert.Equal(t, tc.expectedResult["current_version"], resultMap["current_version"])
746+
assert.Equal(t, tc.expectedResult["latest_version"], resultMap["latest_version"])
747+
assert.Equal(t, tc.expectedResult["up_to_date"], resultMap["up_to_date"])
748+
assert.Equal(t, tc.expectedResult["release_url"], resultMap["release_url"])
749+
assert.NotEmpty(t, resultMap["published_at"])
750+
})
751+
}
752+
}

0 commit comments

Comments
 (0)