diff --git a/README.md b/README.md index c68eda2f..16c53486 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,59 @@ and set it as the GITHUB_PERSONAL_ACCESS_TOKEN environment variable. - `state`: Alert state (string, optional) - `severity`: Alert severity (string, optional) +## Resources + +### Repository Content + +- **Get Repository Content** + Retrieves the content of a repository at a specific path. + + - **Template**: `repo://{owner}/{repo}/contents{/path*}` + - **Parameters**: + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `path`: File or directory path (string, optional) + +- **Get Repository Content for a Specific Branch** + Retrieves the content of a repository at a specific path for a given branch. + + - **Template**: `repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}` + - **Parameters**: + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `branch`: Branch name (string, required) + - `path`: File or directory path (string, optional) + +- **Get Repository Content for a Specific Commit** + Retrieves the content of a repository at a specific path for a given commit. + + - **Template**: `repo://{owner}/{repo}/sha/{sha}/contents{/path*}` + - **Parameters**: + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `sha`: Commit SHA (string, required) + - `path`: File or directory path (string, optional) + +- **Get Repository Content for a Specific Tag** + Retrieves the content of a repository at a specific path for a given tag. + + - **Template**: `repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}` + - **Parameters**: + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `tag`: Tag name (string, required) + - `path`: File or directory path (string, optional) + +- **Get Repository Content for a Specific Pull Request** + Retrieves the content of a repository at a specific path for a given pull request. + + - **Template**: `repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}` + - **Parameters**: + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pr_number`: Pull request number (string, required) + - `path`: File or directory path (string, optional) + ## Standard input/output server ```sh @@ -216,7 +269,7 @@ GitHub MCP Server running on stdio ## Testing on VS Code Insiders -First of all, install `github-mcp-server` with: +First of all, install `github-mcp-server` with: ```bash go install ./cmd/github-mcp-server @@ -231,18 +284,16 @@ Go to settings, find the MCP related settings, and set them to: ```json { - "mcp": { - "inputs": [], - "servers": { - "mcp-github-server": { - "command": "path-to-your/github-mcp-server", - "args": [ - "stdio" - ], - "env": {} - } - } + "mcp": { + "inputs": [], + "servers": { + "mcp-github-server": { + "command": "path-to-your/github-mcp-server", + "args": ["stdio"], + "env": {} + } } + } } ``` @@ -255,7 +306,6 @@ Try something like the following prompt to verify that it works: I'd like to know more about my GitHub profile. ``` - ## TODO Lots of things! diff --git a/go.mod b/go.mod index 4338a69d..0bb0ee9a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.7 require ( github.com/aws/smithy-go v1.22.3 github.com/google/go-github/v69 v69.2.0 - github.com/mark3labs/mcp-go v0.11.2 + github.com/mark3labs/mcp-go v0.14.1 github.com/migueleliasweb/go-github-mock v1.1.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 @@ -34,10 +34,11 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5aa0482d..24df4db9 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mark3labs/mcp-go v0.11.2 h1:mCxWFUTrcXOtJIn9t7F8bxAL8rpE/ZZTTnx3PU/VNdA= -github.com/mark3labs/mcp-go v0.11.2/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE= +github.com/mark3labs/mcp-go v0.14.1 h1:NsieyFbuWQaeZSWSHPvJ5TwJdQwu+1jmivAIVljeouY= +github.com/mark3labs/mcp-go v0.14.1/go.mod h1:xBB350hekQsJAK7gJAii8bcEoWemboLm2mRm5/+KBaU= github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -77,6 +77,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -84,10 +86,10 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go new file mode 100644 index 00000000..dd8597e3 --- /dev/null +++ b/pkg/github/repository_resource.go @@ -0,0 +1,121 @@ +package github + +import ( + "context" + "encoding/base64" + "mime" + "path/filepath" + "strings" + + "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// getRepositoryContent defines the resource template and handler for the Repository Content API. +func getRepositoryContent(client *github.Client) (mainTemplate mcp.ResourceTemplate, reftemplate mcp.ResourceTemplate, shaTemplate mcp.ResourceTemplate, tagTemplate mcp.ResourceTemplate, prTemplate mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) { + + return mcp.NewResourceTemplate( + "repo://{owner}/{repo}/contents{/path*}", // Resource template + "Repository Content", // Description + ), mcp.NewResourceTemplate( + "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template + "Repository Content for specific branch", // Description + ), mcp.NewResourceTemplate( + "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template + "Repository Content for specific commit", // Description + ), mcp.NewResourceTemplate( + "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template + "Repository Content for specific tag", // Description + ), mcp.NewResourceTemplate( + "repo://{owner}/{repo}/refs/pull/{pr_number}/head/contents{/path*}", // Resource template + "Repository Content for specific pull request", // Description + ), func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + // Extract parameters from request.Params.URI + + owner := request.Params.Arguments["owner"].([]string)[0] + repo := request.Params.Arguments["repo"].([]string)[0] + // path should be a joined list of the path parts + path := strings.Join(request.Params.Arguments["path"].([]string), "/") + + opts := &github.RepositoryContentGetOptions{} + + sha, ok := request.Params.Arguments["sha"].([]string) + if ok { + opts.Ref = sha[0] + } + + branch, ok := request.Params.Arguments["branch"].([]string) + if ok { + opts.Ref = "refs/heads/" + branch[0] + } + + tag, ok := request.Params.Arguments["tag"].([]string) + if ok { + opts.Ref = "refs/tags/" + tag[0] + } + prNumber, ok := request.Params.Arguments["pr_number"].([]string) + if ok { + opts.Ref = "refs/pull/" + prNumber[0] + "/head" + } + + // Use the GitHub client to fetch repository content + fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + if err != nil { + return nil, err + } + + if directoryContent != nil { + // Process the directory content and return it as resource contents + var resources []mcp.ResourceContents + for _, entry := range directoryContent { + mimeType := "text/directory" + if entry.GetType() == "file" { + mimeType = mime.TypeByExtension(filepath.Ext(entry.GetName())) + } + resources = append(resources, mcp.TextResourceContents{ + URI: entry.GetHTMLURL(), + MIMEType: mimeType, + Text: entry.GetName(), + }) + + } + return resources, nil + + } else if fileContent != nil { + // Process the file content and return it as a binary resource + + if fileContent.Content != nil { + decodedContent, err := fileContent.GetContent() + if err != nil { + return nil, err + } + + mimeType := mime.TypeByExtension(filepath.Ext(fileContent.GetName())) + + // Check if the file is text-based + if strings.HasPrefix(mimeType, "text") { + // Return as TextResourceContents + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: request.Params.URI, + MIMEType: mimeType, + Text: decodedContent, + }, + }, nil + } + + // Otherwise, return as BlobResourceContents + return []mcp.ResourceContents{ + mcp.BlobResourceContents{ + URI: request.Params.URI, + MIMEType: mimeType, + Blob: base64.StdEncoding.EncodeToString([]byte(decodedContent)), // Encode content as Base64 + }, + }, nil + } + } + + return nil, nil + } +} diff --git a/pkg/github/server.go b/pkg/github/server.go index 0a90b4d1..c804ca28 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -22,6 +22,15 @@ func NewServer(client *github.Client) *server.MCPServer { server.WithResourceCapabilities(true, true), server.WithLogging()) + // Add GitHub Resources + defaultTemplate, branchTemplate, tagTemplate, shaTemplate, prTemplate, handler := getRepositoryContent(client) + + s.AddResourceTemplate(defaultTemplate, handler) + s.AddResourceTemplate(branchTemplate, handler) + s.AddResourceTemplate(tagTemplate, handler) + s.AddResourceTemplate(shaTemplate, handler) + s.AddResourceTemplate(prTemplate, handler) + // Add GitHub tools - Issues s.AddTool(getIssue(client)) s.AddTool(addIssueComment(client)) diff --git a/script/get-me b/script/get-me index fa6675a8..46339ae5 100755 --- a/script/get-me +++ b/script/get-me @@ -1,3 +1,3 @@ #!/bin/bash -echo '{"jsonrpc":"2.0","id":3,"params":{"name":"get_me"},"method":"tools/call"}' | go run cmd/server/main.go stdio | jq . +echo '{"jsonrpc":"2.0","id":3,"params":{"name":"get_me"},"method":"tools/call"}' | go run cmd/github-mcp-server/main.go stdio | jq .