Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
174 changes: 101 additions & 73 deletions pkg/github/repository_resource.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package github

import (
"bytes"
"context"
"encoding/base64"
"errors"
Expand All @@ -15,107 +16,120 @@
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v79/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/yosida95/uritemplate/v3"
)

var (
repositoryResourceContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/contents{/path*}")
repositoryResourceBranchContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}")
repositoryResourceCommitContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/sha/{sha}/contents{/path*}")
repositoryResourceTagContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}")
repositoryResourcePrContentURITemplate = uritemplate.MustNew("repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}")
)

// GetRepositoryResourceContent defines the resource template and handler for getting repository content.
func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
return mcp.ResourceTemplate{
Name: "repository_content",
URITemplate: repositoryResourceContentURITemplate.Raw(), // Resource template
Description: t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
},
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceContentURITemplate)
}

// GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch.
func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
return mcp.ResourceTemplate{
Name: "repository_content_branch",
URITemplate: repositoryResourceBranchContentURITemplate.Raw(), // Resource template
Description: t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
},
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceBranchContentURITemplate)
}

// GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit.
func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
return mcp.ResourceTemplate{
Name: "repository_content_commit",
URITemplate: repositoryResourceCommitContentURITemplate.Raw(), // Resource template
Description: t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
},
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceCommitContentURITemplate)
}

// GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag.
func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
return mcp.ResourceTemplate{
Name: "repository_content_tag",
URITemplate: repositoryResourceTagContentURITemplate.Raw(), // Resource template
Description: t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
},
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourceTagContentURITemplate)
}

// GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request.
func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
),
RepositoryResourceContentsHandler(getClient, getRawClient)
func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) {
return mcp.ResourceTemplate{
Name: "repository_content_pr",
URITemplate: repositoryResourcePrContentURITemplate.Raw(), // Resource template
Description: t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
},
RepositoryResourceContentsHandler(getClient, getRawClient, repositoryResourcePrContentURITemplate)
}

// RepositoryResourceContentsHandler returns a handler function for repository content requests.
func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.GetRawClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// the matcher will give []string with one element
// https://github.com/mark3labs/mcp-go/pull/54
o, ok := request.Params.Arguments["owner"].([]string)
if !ok || len(o) == 0 {
func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.GetRawClientFn, resourceUriTemplate *uritemplate.Template) mcp.ResourceHandler {

Check failure on line 82 in pkg/github/repository_resource.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: func parameter resourceUriTemplate should be resourceURITemplate (revive)
return func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
// Match the URI to extract parameters
uriValues := resourceUriTemplate.Match(request.Params.URI)
if uriValues == nil {
return nil, fmt.Errorf("failed to match URI: %s", request.Params.URI)
}

// Extract required vars
owner := uriValues.Get("owner").String()
repo := uriValues.Get("repo").String()

if owner == "" {
return nil, errors.New("owner is required")
}
owner := o[0]

r, ok := request.Params.Arguments["repo"].([]string)
if !ok || len(r) == 0 {
if repo == "" {
return nil, errors.New("repo is required")
}
repo := r[0]

// path should be a joined list of the path parts
path := ""
p, ok := request.Params.Arguments["path"].([]string)
if ok {
path = strings.Join(p, "/")
}
path := uriValues.Get("path").String()

opts := &github.RepositoryContentGetOptions{}
rawOpts := &raw.ContentOpts{}

sha, ok := request.Params.Arguments["sha"].([]string)
if ok && len(sha) > 0 {
opts.Ref = sha[0]
rawOpts.SHA = sha[0]
sha := uriValues.Get("sha").String()
if sha != "" {
opts.Ref = sha
rawOpts.SHA = sha
}

branch, ok := request.Params.Arguments["branch"].([]string)
if ok && len(branch) > 0 {
opts.Ref = "refs/heads/" + branch[0]
rawOpts.Ref = "refs/heads/" + branch[0]
branch := uriValues.Get("branch").String()
if branch != "" {
opts.Ref = "refs/heads/" + branch
rawOpts.Ref = "refs/heads/" + branch
}

tag, ok := request.Params.Arguments["tag"].([]string)
if ok && len(tag) > 0 {
opts.Ref = "refs/tags/" + tag[0]
rawOpts.Ref = "refs/tags/" + tag[0]
tag := uriValues.Get("tag").String()
if tag != "" {
opts.Ref = "refs/tags/" + tag
rawOpts.Ref = "refs/tags/" + tag
}
prNumber, ok := request.Params.Arguments["prNumber"].([]string)
if ok && len(prNumber) > 0 {

prNumber := uriValues.Get("prNumber").String()
if prNumber != "" {
// fetch the PR from the API to get the latest commit and use SHA
githubClient, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
prNum, err := strconv.Atoi(prNumber[0])
prNum, err := strconv.Atoi(prNumber)
if err != nil {
return nil, fmt.Errorf("invalid pull request number: %w", err)
}
Expand Down Expand Up @@ -161,19 +175,33 @@

switch {
case strings.HasPrefix(mimeType, "text"), strings.HasPrefix(mimeType, "application"):
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: request.Params.URI,
MIMEType: mimeType,
Text: string(content),
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{
{
URI: request.Params.URI,
MIMEType: mimeType,
Text: string(content),
},
},
}, nil
default:
return []mcp.ResourceContents{
mcp.BlobResourceContents{
URI: request.Params.URI,
MIMEType: mimeType,
Blob: base64.StdEncoding.EncodeToString(content),
var buf bytes.Buffer
base64Encoder := base64.NewEncoder(base64.StdEncoding, &buf)
_, err := base64Encoder.Write(content)
if err != nil {
return nil, fmt.Errorf("failed to base64 encode content: %w", err)
}
if err := base64Encoder.Close(); err != nil {
return nil, fmt.Errorf("failed to close base64 encoder: %w", err)
}

return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{
{
URI: request.Params.URI,
MIMEType: mimeType,
Blob: buf.Bytes(),
},
},
}, nil
}
Expand Down
Loading
Loading