Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
42 changes: 20 additions & 22 deletions cmd/github-mcp-server/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import (
"github.com/github/github-mcp-server/pkg/toolsets"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v79/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -224,7 +225,12 @@ func generateToolDoc(tool mcp.Tool) string {
lines = append(lines, fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title))

// Parameters
schema := tool.InputSchema
schema, ok := tool.InputSchema.(*jsonschema.Schema)
if !ok {
lines = append(lines, " - No parameters required")
return strings.Join(lines, "\n")
}

if len(schema.Properties) > 0 {
// Get parameter names and sort them for deterministic order
var paramNames []string
Expand All @@ -241,30 +247,22 @@ func generateToolDoc(tool mcp.Tool) string {
requiredStr = "required"
}

// Get the type and description
typeStr := "unknown"
description := ""

if propMap, ok := prop.(map[string]interface{}); ok {
if typeVal, ok := propMap["type"].(string); ok {
if typeVal == "array" {
if items, ok := propMap["items"].(map[string]interface{}); ok {
if itemType, ok := items["type"].(string); ok {
typeStr = itemType + "[]"
}
} else {
typeStr = "array"
}
} else {
typeStr = typeVal
}
}
var typeStr, description string

if desc, ok := propMap["description"].(string); ok {
description = desc
// Get the type and description
switch prop.Type {
case "array":
if prop.Items != nil {
typeStr = prop.Items.Type + "[]"
} else {
typeStr = "array"
}
default:
typeStr = prop.Type
}

description = prop.Description

paramLine := fmt.Sprintf(" - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
lines = append(lines, paramLine)
}
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.0

require (
github.com/google/go-github/v79 v79.0.0
github.com/google/jsonschema-go v0.3.0
github.com/josephburnett/jd v1.9.2
github.com/mark3labs/mcp-go v0.36.0
github.com/microcosm-cc/bluemonday v1.0.27
Expand Down Expand Up @@ -40,6 +41,7 @@ require (
github.com/google/go-querystring v1.1.0
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/modelcontextprotocol/go-sdk v1.1.0
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
Expand All @@ -52,7 +54,7 @@ require (
github.com/spf13/pflag v1.0.10
github.com/subosito/gotenv v1.6.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.5.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ github.com/google/go-github/v79 v79.0.0 h1:MdodQojuFPBhmtwHiBcIGLw/e/wei2PvFX9nd
github.com/google/go-github/v79 v79.0.0/go.mod h1:OAFbNhq7fQwohojb06iIIQAB9CBGYLq999myfUFnrS4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
Expand Down Expand Up @@ -63,6 +65,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88=
github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY=
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
Expand Down Expand Up @@ -112,6 +116,8 @@ golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
Expand Down
134 changes: 77 additions & 57 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"context"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/url"
Expand All @@ -20,8 +19,7 @@
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "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/shurcooL/githubv4"
)

Expand Down Expand Up @@ -54,11 +52,14 @@

// LockdownMode indicates if we should enable lockdown mode
LockdownMode bool

// Logger is used for logging within the server
Logger *slog.Logger
}

const stdioServerLogPrefix = "stdioserver"

Check failure on line 60 in internal/ghmcp/server.go

View workflow job for this annotation

GitHub Actions / lint

const stdioServerLogPrefix is unused (unused)

Check failure on line 60 in internal/ghmcp/server.go

View workflow job for this annotation

GitHub Actions / lint

const stdioServerLogPrefix is unused (unused)

func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
apiHost, err := parseAPIHost(cfg.Host)
if err != nil {
return nil, fmt.Errorf("failed to parse API host: %w", err)
Expand All @@ -81,34 +82,6 @@
} // We're going to wrap the Transport later in beforeInit
gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient)

// When a client send an initialize request, update the user agent to include the client info.
beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) {
userAgent := fmt.Sprintf(
"github-mcp-server/%s (%s/%s)",
cfg.Version,
message.Params.ClientInfo.Name,
message.Params.ClientInfo.Version,
)

restClient.UserAgent = userAgent

gqlHTTPClient.Transport = &userAgentTransport{
transport: gqlHTTPClient.Transport,
agent: userAgent,
}
}

hooks := &server.Hooks{
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
OnBeforeAny: []server.BeforeAnyHookFunc{
func(ctx context.Context, _ any, _ mcp.MCPMethod, _ any) {
// Ensure the context is cleared of any previous errors
// as context isn't propagated through middleware
errors.ContextWithGitHubErrors(ctx)
},
},
}

enabledToolsets := cfg.EnabledToolsets

// If dynamic toolsets are enabled, remove "all" from the enabled toolsets
Expand All @@ -135,10 +108,14 @@
// Generate instructions based on enabled toolsets
instructions := github.GenerateInstructions(enabledToolsets)

ghServer := github.NewServer(cfg.Version,
server.WithInstructions(instructions),
server.WithHooks(hooks),
)
ghServer := github.NewServer(cfg.Version, &mcp.ServerOptions{
Instructions: instructions,
Logger: cfg.Logger,
})

// Add middlewares
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, restClient, gqlHTTPClient))

getClient := func(_ context.Context) (*gogithub.Client, error) {
return restClient, nil // closing over client
Expand Down Expand Up @@ -229,23 +206,6 @@

t, dumpTranslations := translations.TranslationHelper()

ghServer, err := NewMCPServer(MCPServerConfig{
Version: cfg.Version,
Host: cfg.Host,
Token: cfg.Token,
EnabledToolsets: cfg.EnabledToolsets,
DynamicToolsets: cfg.DynamicToolsets,
ReadOnly: cfg.ReadOnly,
Translator: t,
ContentWindowSize: cfg.ContentWindowSize,
LockdownMode: cfg.LockdownMode,
})
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
}

stdioServer := server.NewStdioServer(ghServer)

var slogHandler slog.Handler
var logOutput io.Writer
if cfg.LogFilePath != "" {
Expand All @@ -261,8 +221,22 @@
}
logger := slog.New(slogHandler)
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly, "lockdownEnabled", cfg.LockdownMode)
stdLogger := log.New(logOutput, stdioServerLogPrefix, 0)
stdioServer.SetErrorLogger(stdLogger)

ghServer, err := NewMCPServer(MCPServerConfig{
Version: cfg.Version,
Host: cfg.Host,
Token: cfg.Token,
EnabledToolsets: cfg.EnabledToolsets,
DynamicToolsets: cfg.DynamicToolsets,
ReadOnly: cfg.ReadOnly,
Translator: t,
ContentWindowSize: cfg.ContentWindowSize,
LockdownMode: cfg.LockdownMode,
Logger: logger,
})
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
}

if cfg.ExportTranslations {
// Once server is initialized, all translations are loaded
Expand All @@ -272,15 +246,20 @@
// Start listening for messages
errC := make(chan error, 1)
go func() {
in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
var in io.ReadCloser
var out io.WriteCloser

in = os.Stdin
out = os.Stdout

if cfg.EnableCommandLogging {
loggedIO := mcplog.NewIOLogger(in, out, logger)
in, out = loggedIO, loggedIO
}

// enable GitHub errors in the context
ctx := errors.ContextWithGitHubErrors(ctx)
errC <- stdioServer.Listen(ctx, in, out)
errC <- ghServer.Run(ctx, &mcp.IOTransport{Reader: in, Writer: out})
}()

// Output github-mcp-server string
Expand Down Expand Up @@ -497,3 +476,44 @@
req.Header.Set("Authorization", "Bearer "+t.token)
return t.transport.RoundTrip(req)
}

func addGitHubAPIErrorToContext(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, req mcp.Request) (result mcp.Result, err error) {
// Ensure the context is cleared of any previous errors
// as context isn't propagated through middleware
ctx = errors.ContextWithGitHubErrors(ctx)
return next(ctx, method, req)
}
}

func addUserAgentsMiddleware(cfg MCPServerConfig, restClient *gogithub.Client, gqlHTTPClient *http.Client) func(next mcp.MethodHandler) mcp.MethodHandler {
return func(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, request mcp.Request) (result mcp.Result, err error) {
if method != "initialize" {
return next(ctx, method, request)
}

initializeRequest, ok := request.(*mcp.InitializeRequest)
if !ok {
return next(ctx, method, request)
}

message := initializeRequest
userAgent := fmt.Sprintf(
"github-mcp-server/%s (%s/%s)",
cfg.Version,
message.Params.ClientInfo.Name,
message.Params.ClientInfo.Version,
)

restClient.UserAgent = userAgent

gqlHTTPClient.Transport = &userAgentTransport{
transport: gqlHTTPClient.Transport,
agent: userAgent,
}

return next(ctx, method, request)
}
}
}
7 changes: 4 additions & 3 deletions pkg/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"context"
"fmt"

"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/go-github/v79/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

type GitHubAPIError struct {
Expand Down Expand Up @@ -112,7 +113,7 @@ func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github
if ctx != nil {
_, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling
}
return mcp.NewToolResultErrorFromErr(message, err)
return utils.NewToolResultErrorFromErr(message, err)
}

// NewGitHubGraphQLErrorResponse returns an mcp.NewToolResultError and retains the error in the context for access via middleware
Expand All @@ -121,5 +122,5 @@ func NewGitHubGraphQLErrorResponse(ctx context.Context, message string, err erro
if ctx != nil {
_, _ = addGitHubGraphQLErrorToContext(ctx, graphQLErr) // Explicitly ignore error for graceful handling
}
return mcp.NewToolResultErrorFromErr(message, err)
return utils.NewToolResultErrorFromErr(message, err)
}
9 changes: 3 additions & 6 deletions pkg/github/__toolsnaps__/get_me.snap
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
{
"annotations": {
"title": "Get my user profile",
"readOnlyHint": true
"readOnlyHint": true,
"title": "Get my user profile"
},
"description": "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls.",
"inputSchema": {
"properties": {},
"type": "object"
},
"inputSchema": null,
"name": "get_me"
}
Loading
Loading