diff --git a/cmd/github-mcp-server/generate_docs.go b/cmd/github-mcp-server/generate_docs.go index 359370760..546bd716b 100644 --- a/cmd/github-mcp-server/generate_docs.go +++ b/cmd/github-mcp-server/generate_docs.go @@ -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" ) @@ -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 @@ -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) } diff --git a/go.mod b/go.mod index 02b9ad252..cb0d0e89c 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 1ac8b7606..56a066492 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 1067a222f..39a6726ce 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "log" "log/slog" "net/http" "net/url" @@ -20,8 +19,7 @@ import ( "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" ) @@ -54,11 +52,14 @@ type MCPServerConfig struct { // LockdownMode indicates if we should enable lockdown mode LockdownMode bool + + // Logger is used for logging within the server + Logger *slog.Logger } const stdioServerLogPrefix = "stdioserver" -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) @@ -81,34 +82,6 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { } // 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 @@ -135,10 +108,14 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { // 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 @@ -229,23 +206,6 @@ func RunStdioServer(cfg StdioServerConfig) error { 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 != "" { @@ -261,8 +221,22 @@ func RunStdioServer(cfg StdioServerConfig) error { } 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 @@ -272,15 +246,20 @@ func RunStdioServer(cfg StdioServerConfig) error { // 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 @@ -497,3 +476,44 @@ func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro 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) + } + } +} diff --git a/pkg/errors/error.go b/pkg/errors/error.go index 57e4a0d97..be2cf58f9 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -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 { @@ -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 @@ -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) } diff --git a/pkg/github/__toolsnaps__/get_me.snap b/pkg/github/__toolsnaps__/get_me.snap index 13b061741..2ccdeda5b 100644 --- a/pkg/github/__toolsnaps__/get_me.snap +++ b/pkg/github/__toolsnaps__/get_me.snap @@ -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" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_team_members.snap b/pkg/github/__toolsnaps__/get_team_members.snap index 2d91bb5ea..5b7f090fe 100644 --- a/pkg/github/__toolsnaps__/get_team_members.snap +++ b/pkg/github/__toolsnaps__/get_team_members.snap @@ -1,25 +1,25 @@ { "annotations": { - "title": "Get team members", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Get team members" }, "description": "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials", "inputSchema": { + "type": "object", + "required": [ + "org", + "team_slug" + ], "properties": { "org": { - "description": "Organization login (owner) that contains the team.", - "type": "string" + "type": "string", + "description": "Organization login (owner) that contains the team." }, "team_slug": { - "description": "Team slug", - "type": "string" + "type": "string", + "description": "Team slug" } - }, - "required": [ - "org", - "team_slug" - ], - "type": "object" + } }, "name": "get_team_members" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_teams.snap b/pkg/github/__toolsnaps__/get_teams.snap index 39ed4db35..595dd262d 100644 --- a/pkg/github/__toolsnaps__/get_teams.snap +++ b/pkg/github/__toolsnaps__/get_teams.snap @@ -1,17 +1,17 @@ { "annotations": { - "title": "Get teams", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Get teams" }, "description": "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials", "inputSchema": { + "type": "object", "properties": { "user": { - "description": "Username to get teams for. If not provided, uses the authenticated user.", - "type": "string" + "type": "string", + "description": "Username to get teams for. If not provided, uses the authenticated user." } - }, - "type": "object" + } }, "name": "get_teams" } \ No newline at end of file diff --git a/pkg/github/actions.go b/pkg/github/actions.go index ecf00021a..8dee61deb 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go index 2f82ceafd..f09f8082b 100644 --- a/pkg/github/actions_test.go +++ b/pkg/github/actions_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 979d98ff6..a087bf0bf 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go index 3c6a8325f..dc2d66446 100644 --- a/pkg/github/code_scanning_test.go +++ b/pkg/github/code_scanning_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go index 06642aa15..5f248934b 100644 --- a/pkg/github/context_tools.go +++ b/pkg/github/context_tools.go @@ -6,8 +6,9 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/github/github-mcp-server/pkg/utils" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" ) @@ -34,20 +35,20 @@ type UserDetails struct { } // GetMe creates a tool to get details of the authenticated user. -func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - tool := mcp.NewTool("get_me", - mcp.WithDescription(t("TOOL_GET_ME_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.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "get_me", + Description: t("TOOL_GET_ME_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."), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"), - ReadOnlyHint: ToBoolPtr(true), - }), - ) + ReadOnlyHint: true, + }, + } - type args struct{} - handler := mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, _ args) (*mcp.CallToolResult, error) { + handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) { client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultErrorFromErr("failed to get GitHub client", err), nil + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err } user, res, err := client.Users.Get(ctx, "") @@ -56,7 +57,7 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Too "failed to get user", res, err, - ), nil + ), nil, err } // Create minimal user representation instead of returning full user object @@ -86,7 +87,7 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Too }, } - return MarshalledTextResult(minimalUser), nil + return MarshalledTextResult(minimalUser), nil, nil }) return tool, handler @@ -103,21 +104,28 @@ type OrganizationTeams struct { Teams []TeamInfo `json:"teams"` } -func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("get_teams", - mcp.WithDescription(t("TOOL_GET_TEAMS_DESCRIPTION", "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials")), - mcp.WithString("user", - mcp.Description(t("TOOL_GET_TEAMS_USER_DESCRIPTION", "Username to get teams for. If not provided, uses the authenticated user.")), - ), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "get_teams", + Description: t("TOOL_GET_TEAMS_DESCRIPTION", "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_TEAMS_TITLE", "Get teams"), - ReadOnlyHint: ToBoolPtr(true), - }), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - user, err := OptionalParam[string](request, "user") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "user": { + Type: "string", + Description: t("TOOL_GET_TEAMS_USER_DESCRIPTION", "Username to get teams for. If not provided, uses the authenticated user."), + }, + }, + }, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + user, err := OptionalParam[string](args, "user") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } var username string @@ -126,7 +134,7 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations } else { client, err := getClient(ctx) if err != nil { - return mcp.NewToolResultErrorFromErr("failed to get GitHub client", err), nil + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } userResp, res, err := client.Users.Get(ctx, "") @@ -135,14 +143,14 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations "failed to get user", res, err, - ), nil + ), nil, nil } username = userResp.GetLogin() } gqlClient, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil + return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil } var q struct { @@ -165,7 +173,7 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations "login": githubv4.String(username), } if err := gqlClient.Query(ctx, &q, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find teams", err), nil + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find teams", err), nil, nil } var organizations []OrganizationTeams @@ -186,40 +194,47 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations organizations = append(organizations, orgTeams) } - return MarshalledTextResult(organizations), nil + return MarshalledTextResult(organizations), nil, nil } } -func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("get_team_members", - mcp.WithDescription(t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials")), - mcp.WithString("org", - mcp.Description(t("TOOL_GET_TEAM_MEMBERS_ORG_DESCRIPTION", "Organization login (owner) that contains the team.")), - mcp.Required(), - ), - mcp.WithString("team_slug", - mcp.Description(t("TOOL_GET_TEAM_MEMBERS_TEAM_SLUG_DESCRIPTION", "Team slug")), - mcp.Required(), - ), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + return mcp.Tool{ + Name: "get_team_members", + Description: t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials"), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_TEAM_MEMBERS_TITLE", "Get team members"), - ReadOnlyHint: ToBoolPtr(true), - }), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := RequiredParam[string](request, "org") + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "org": { + Type: "string", + Description: t("TOOL_GET_TEAM_MEMBERS_ORG_DESCRIPTION", "Organization login (owner) that contains the team."), + }, + "team_slug": { + Type: "string", + Description: t("TOOL_GET_TEAM_MEMBERS_TEAM_SLUG_DESCRIPTION", "Team slug"), + }, + }, + Required: []string{"org", "team_slug"}, + }, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + org, err := RequiredParam[string](args, "org") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - teamSlug, err := RequiredParam[string](request, "team_slug") + teamSlug, err := RequiredParam[string](args, "team_slug") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } gqlClient, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil + return utils.NewToolResultErrorFromErr("failed to get GitHub GQL client", err), nil, nil } var q struct { @@ -238,7 +253,7 @@ func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelpe "teamSlug": githubv4.String(teamSlug), } if err := gqlClient.Query(ctx, &q, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get team members", err), nil, nil } var members []string @@ -246,6 +261,6 @@ func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelpe members = append(members, string(member.Login)) } - return MarshalledTextResult(members), nil + return MarshalledTextResult(members), nil, nil } } diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go index d3d5d0797..13992fb91 100644 --- a/pkg/github/context_tools_test.go +++ b/pkg/github/context_tools_test.go @@ -25,7 +25,7 @@ func Test_GetMe(t *testing.T) { // Verify some basic very important properties assert.Equal(t, "get_me", tool.Name) - assert.True(t, *tool.Annotations.ReadOnlyHint, "get_me tool should be read-only") + assert.True(t, tool.Annotations.ReadOnlyHint, "get_me tool should be read-only") // Setup mock user response mockUser := &github.User{ @@ -111,11 +111,11 @@ func Test_GetMe(t *testing.T) { _, handler := GetMe(tc.stubbedGetClientFn, translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) - require.NoError(t, err) + result, _, err := handler(context.Background(), &request, tc.requestArgs) textContent := getTextResult(t, result) if tc.expectToolError { + assert.Error(t, err) assert.True(t, result.IsError, "expected tool call result to be an error") assert.Contains(t, textContent.Text, tc.expectedToolErrMsg) return @@ -150,7 +150,7 @@ func Test_GetTeams(t *testing.T) { require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_teams", tool.Name) - assert.True(t, *tool.Annotations.ReadOnlyHint, "get_teams tool should be read-only") + assert.True(t, tool.Annotations.ReadOnlyHint, "get_teams tool should be read-only") mockUser := &github.User{ Login: github.Ptr("testuser"), @@ -335,7 +335,7 @@ func Test_GetTeams(t *testing.T) { _, handler := GetTeams(tc.stubbedGetClientFn, tc.stubbedGetGQLClientFn, translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) @@ -377,7 +377,7 @@ func Test_GetTeamMembers(t *testing.T) { require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_team_members", tool.Name) - assert.True(t, *tool.Annotations.ReadOnlyHint, "get_team_members tool should be read-only") + assert.True(t, tool.Annotations.ReadOnlyHint, "get_team_members tool should be read-only") mockTeamMembersResponse := githubv4mock.DataResponse(map[string]any{ "organization": map[string]any{ @@ -471,7 +471,7 @@ func Test_GetTeamMembers(t *testing.T) { _, handler := GetTeamMembers(tc.stubbedGetGQLClientFn, translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) textContent := getTextResult(t, result) diff --git a/pkg/github/dependabot.go b/pkg/github/dependabot.go index ebd295aad..2823daf3e 100644 --- a/pkg/github/dependabot.go +++ b/pkg/github/dependabot.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/dependabot_test.go b/pkg/github/dependabot_test.go index 57b421db3..2aa02981e 100644 --- a/pkg/github/dependabot_test.go +++ b/pkg/github/dependabot_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index d37794db4..ac4077952 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/discussions_test.go b/pkg/github/discussions_test.go index 05789b606..03dd4ae1d 100644 --- a/pkg/github/discussions_test.go +++ b/pkg/github/discussions_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/dynamic_tools.go b/pkg/github/dynamic_tools.go index e703a885e..45a481576 100644 --- a/pkg/github/dynamic_tools.go +++ b/pkg/github/dynamic_tools.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/gists.go b/pkg/github/gists.go index 5183f353e..ccda72486 100644 --- a/pkg/github/gists.go +++ b/pkg/github/gists.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/gists_test.go b/pkg/github/gists_test.go index fc4a2c692..fa40ba1af 100644 --- a/pkg/github/gists_test.go +++ b/pkg/github/gists_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/git.go b/pkg/github/git.go index e0207ac8d..cbbc8e3d7 100644 --- a/pkg/github/git.go +++ b/pkg/github/git.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go index bc1ae412f..1e4627544 100644 --- a/pkg/github/helper_test.go +++ b/pkg/github/helper_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -110,56 +110,67 @@ func mockResponse(t *testing.T, code int, body interface{}) http.HandlerFunc { // createMCPRequest is a helper function to create a MCP request with the given arguments. func createMCPRequest(args any) mcp.CallToolRequest { + // convert args to map[string]interface{} and serialize to JSON + argsMap, ok := args.(map[string]interface{}) + if !ok { + argsMap = make(map[string]interface{}) + } + + argsJSON, err := json.Marshal(argsMap) + require.NoError(nil, err) + + jsonRawMessage := json.RawMessage(argsJSON) + return mcp.CallToolRequest{ - Params: struct { - Name string `json:"name"` - Arguments any `json:"arguments,omitempty"` - Meta *mcp.Meta `json:"_meta,omitempty"` - }{ - Arguments: args, + Params: &mcp.CallToolParamsRaw{ + Arguments: jsonRawMessage, }, } } // getTextResult is a helper function that returns a text result from a tool call. -func getTextResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent { +func getTextResult(t *testing.T, result *mcp.CallToolResult) *mcp.TextContent { t.Helper() assert.NotNil(t, result) require.Len(t, result.Content, 1) - require.IsType(t, mcp.TextContent{}, result.Content[0]) - textContent := result.Content[0].(mcp.TextContent) - assert.Equal(t, "text", textContent.Type) + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected content to be of type TextContent") return textContent } -func getErrorResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent { +func getErrorResult(t *testing.T, result *mcp.CallToolResult) *mcp.TextContent { res := getTextResult(t, result) require.True(t, result.IsError, "expected tool call result to be an error") return res } // getTextResourceResult is a helper function that returns a text result from a tool call. -func getTextResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.TextResourceContents { +func getTextResourceResult(t *testing.T, result *mcp.CallToolResult) *mcp.ResourceContents { t.Helper() assert.NotNil(t, result) require.Len(t, result.Content, 2) content := result.Content[1] require.IsType(t, mcp.EmbeddedResource{}, content) - resource := content.(mcp.EmbeddedResource) - require.IsType(t, mcp.TextResourceContents{}, resource.Resource) - return resource.Resource.(mcp.TextResourceContents) + resource, ok := content.(*mcp.EmbeddedResource) + require.True(t, ok, "expected content to be of type EmbeddedResource") + + require.IsType(t, mcp.ResourceContents{}, resource.Resource) + require.NotEmpty(t, resource.Resource.Text) + return resource.Resource } // getBlobResourceResult is a helper function that returns a blob result from a tool call. -func getBlobResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.BlobResourceContents { +func getBlobResourceResult(t *testing.T, result *mcp.CallToolResult) *mcp.ResourceContents { t.Helper() assert.NotNil(t, result) require.Len(t, result.Content, 2) content := result.Content[1] require.IsType(t, mcp.EmbeddedResource{}, content) - resource := content.(mcp.EmbeddedResource) - require.IsType(t, mcp.BlobResourceContents{}, resource.Resource) - return resource.Resource.(mcp.BlobResourceContents) + + resource := content.(*mcp.EmbeddedResource) + require.IsType(t, mcp.ResourceContents{}, resource.Resource) + require.NotEmpty(t, resource.Resource.Blob) + return resource.Resource } func TestOptionalParamOK(t *testing.T) { @@ -226,11 +237,9 @@ func TestOptionalParamOK(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.args) - // Test with string type assertion if _, isString := tc.expectedVal.(string); isString || tc.errorMsg == "parameter myParam is not of type string, is bool" { - val, ok, err := OptionalParamOK[string](request, tc.paramName) + val, ok, err := OptionalParamOK[string, map[string]any](tc.args, tc.paramName) if tc.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tc.errorMsg) @@ -245,7 +254,7 @@ func TestOptionalParamOK(t *testing.T) { // Test with bool type assertion if _, isBool := tc.expectedVal.(bool); isBool || tc.errorMsg == "parameter myParam is not of type bool, is string" { - val, ok, err := OptionalParamOK[bool](request, tc.paramName) + val, ok, err := OptionalParamOK[bool, map[string]any](tc.args, tc.paramName) if tc.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tc.errorMsg) @@ -260,7 +269,7 @@ func TestOptionalParamOK(t *testing.T) { // Test with float64 type assertion (for number case) if _, isFloat := tc.expectedVal.(float64); isFloat { - val, ok, err := OptionalParamOK[float64](request, tc.paramName) + val, ok, err := OptionalParamOK[float64, map[string]any](tc.args, tc.paramName) if tc.expectError { // This case shouldn't happen for float64 in the defined tests require.Fail(t, "Unexpected error case for float64") diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 1c4f9514c..a37583a18 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( @@ -237,8 +239,8 @@ func IssueRead(getClient GetClientFn, getGQLClient GetGQLClientFn, t translation }), mcp.WithString("method", mcp.Required(), - mcp.Description(`The read operation to perform on a single issue. -Options are: + mcp.Description(`The read operation to perform on a single issue. +Options are: 1. get - Get details of a specific issue. 2. get_comments - Get issue comments. 3. get_sub_issues - Get sub-issues of the issue. @@ -862,8 +864,8 @@ func IssueWrite(getClient GetClientFn, getGQLClient GetGQLClientFn, t translatio mcp.WithString("method", mcp.Required(), mcp.Description(`Write operation to perform on a single issue. -Options are: -- 'create' - creates a new issue. +Options are: +- 'create' - creates a new issue. - 'update' - updates an existing issue. `), mcp.Enum("create", "update"), diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 4cc3a1302..60e6f57de 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/labels.go b/pkg/github/labels.go index c9be7be75..42b53fc6d 100644 --- a/pkg/github/labels.go +++ b/pkg/github/labels.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/labels_test.go b/pkg/github/labels_test.go index 6bb91da26..5055364f0 100644 --- a/pkg/github/labels_test.go +++ b/pkg/github/labels_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/notifications.go b/pkg/github/notifications.go index 8bf862006..ac2dcec6b 100644 --- a/pkg/github/notifications.go +++ b/pkg/github/notifications.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/notifications_test.go b/pkg/github/notifications_test.go index 53a25076b..4920589e1 100644 --- a/pkg/github/notifications_test.go +++ b/pkg/github/notifications_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 4a2a68bf2..6710a4f6f 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index ac0019ac0..6cc4f6cc4 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index e64ae03e4..69af69af7 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( @@ -28,8 +30,8 @@ func PullRequestRead(getClient GetClientFn, t translations.TranslationHelperFunc }), mcp.WithString("method", mcp.Required(), - mcp.Description(`Action to specify what pull request data needs to be retrieved from GitHub. -Possible options: + mcp.Description(`Action to specify what pull request data needs to be retrieved from GitHub. +Possible options: 1. get - Get details of a specific pull request. 2. get_diff - Get the diff of a pull request. 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks. diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 347bce672..02eaadf32 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index b1fe5bf72..4ece85e91 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index b9628eee5..ea784fe43 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/search.go b/pkg/github/search.go index 147b16402..c909c10ca 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index d31abc154..d823e2070 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/search_utils.go b/pkg/github/search_utils.go index 9f2b1f5c3..4a710236b 100644 --- a/pkg/github/search_utils.go +++ b/pkg/github/search_utils.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/search_utils_test.go b/pkg/github/search_utils_test.go index 85f953eed..7b68c4ca2 100644 --- a/pkg/github/search_utils_test.go +++ b/pkg/github/search_utils_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index 1c5da12f9..192e0a410 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go index 74d0d382b..8f665ba8a 100644 --- a/pkg/github/secret_scanning_test.go +++ b/pkg/github/secret_scanning_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/security_advisories.go b/pkg/github/security_advisories.go index 027203687..2c4be1bb3 100644 --- a/pkg/github/security_advisories.go +++ b/pkg/github/security_advisories.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/security_advisories_test.go b/pkg/github/security_advisories_test.go index 7975dc145..f4dcf7f50 100644 --- a/pkg/github/security_advisories_test.go +++ b/pkg/github/security_advisories_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package github import ( diff --git a/pkg/github/server.go b/pkg/github/server.go index 439f93346..f474d06b4 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -6,36 +6,37 @@ import ( "fmt" "strconv" + "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // NewServer creates a new GitHub MCP server with the specified GH client and logger. -func NewServer(version string, opts ...server.ServerOption) *server.MCPServer { +func NewServer(version string, opts *mcp.ServerOptions) *mcp.Server { // Add default options - defaultOpts := []server.ServerOption{ - server.WithToolCapabilities(true), - server.WithResourceCapabilities(true, true), - server.WithLogging(), + opts = &mcp.ServerOptions{ + HasTools: true, + HasResources: true, + Logger: opts.Logger, } - opts = append(defaultOpts, opts...) // Create a new MCP server - s := server.NewMCPServer( - "github-mcp-server", - version, - opts..., - ) + s := mcp.NewServer(&mcp.Implementation{ + Name: "github-mcp-server", + Title: "GitHub MCP Server", + Version: version, + }, opts) + return s } // OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. // It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. -func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool, err error) { +func OptionalParamOK[T any, A map[string]any](args A, p string) (value T, ok bool, err error) { // Check if the parameter is present in the request - val, exists := r.GetArguments()[p] + val, exists := args[p] if !exists { // Not present, return zero value, false, no error return @@ -66,16 +67,16 @@ func isAcceptedError(err error) bool { // 1. Checks if the parameter is present in the request. // 2. Checks if the parameter is of the expected type. // 3. Checks if the parameter is not empty, i.e: non-zero value -func RequiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) { +func RequiredParam[T comparable](args map[string]any, p string) (T, error) { var zero T // Check if the parameter is present in the request - if _, ok := r.GetArguments()[p]; !ok { + if _, ok := args[p]; !ok { return zero, fmt.Errorf("missing required parameter: %s", p) } // Check if the parameter is of the expected type - val, ok := r.GetArguments()[p].(T) + val, ok := args[p].(T) if !ok { return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) } @@ -92,8 +93,8 @@ func RequiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) { // 1. Checks if the parameter is present in the request. // 2. Checks if the parameter is of the expected type. // 3. Checks if the parameter is not empty, i.e: non-zero value -func RequiredInt(r mcp.CallToolRequest, p string) (int, error) { - v, err := RequiredParam[float64](r, p) +func RequiredInt(args map[string]any, p string) (int, error) { + v, err := RequiredParam[float64](args, p) if err != nil { return 0, err } @@ -106,8 +107,8 @@ func RequiredInt(r mcp.CallToolRequest, p string) (int, error) { // 2. Checks if the parameter is of the expected type (float64). // 3. Checks if the parameter is not empty, i.e: non-zero value. // 4. Validates that the float64 value can be safely converted to int64 without truncation. -func RequiredBigInt(r mcp.CallToolRequest, p string) (int64, error) { - v, err := RequiredParam[float64](r, p) +func RequiredBigInt(args map[string]any, p string) (int64, error) { + v, err := RequiredParam[float64](args, p) if err != nil { return 0, err } @@ -124,28 +125,28 @@ func RequiredBigInt(r mcp.CallToolRequest, p string) (int64, error) { // It does the following checks: // 1. Checks if the parameter is present in the request, if not, it returns its zero-value // 2. If it is present, it checks if the parameter is of the expected type and returns it -func OptionalParam[T any](r mcp.CallToolRequest, p string) (T, error) { +func OptionalParam[T any](args map[string]any, p string) (T, error) { var zero T // Check if the parameter is present in the request - if _, ok := r.GetArguments()[p]; !ok { + if _, ok := args[p]; !ok { return zero, nil } // Check if the parameter is of the expected type - if _, ok := r.GetArguments()[p].(T); !ok { - return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, r.GetArguments()[p]) + if _, ok := args[p].(T); !ok { + return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, args[p]) } - return r.GetArguments()[p].(T), nil + return args[p].(T), nil } // OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. // It does the following checks: // 1. Checks if the parameter is present in the request, if not, it returns its zero-value // 2. If it is present, it checks if the parameter is of the expected type and returns it -func OptionalIntParam(r mcp.CallToolRequest, p string) (int, error) { - v, err := OptionalParam[float64](r, p) +func OptionalIntParam(args map[string]any, p string) (int, error) { + v, err := OptionalParam[float64](args, p) if err != nil { return 0, err } @@ -154,8 +155,8 @@ func OptionalIntParam(r mcp.CallToolRequest, p string) (int, error) { // OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request // similar to optionalIntParam, but it also takes a default value. -func OptionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, error) { - v, err := OptionalIntParam(r, p) +func OptionalIntParamWithDefault(args map[string]any, p string, d int) (int, error) { + v, err := OptionalIntParam(args, p) if err != nil { return 0, err } @@ -167,10 +168,9 @@ func OptionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, e // OptionalBoolParamWithDefault is a helper function that can be used to fetch a requested parameter from the request // similar to optionalBoolParam, but it also takes a default value. -func OptionalBoolParamWithDefault(r mcp.CallToolRequest, p string, d bool) (bool, error) { - args := r.GetArguments() +func OptionalBoolParamWithDefault(args map[string]any, p string, d bool) (bool, error) { _, ok := args[p] - v, err := OptionalParam[bool](r, p) + v, err := OptionalParam[bool](args, p) if err != nil { return false, err } @@ -184,13 +184,13 @@ func OptionalBoolParamWithDefault(r mcp.CallToolRequest, p string, d bool) (bool // It does the following checks: // 1. Checks if the parameter is present in the request, if not, it returns its zero-value // 2. If it is present, iterates the elements and checks each is a string -func OptionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) { +func OptionalStringArrayParam(args map[string]any, p string) ([]string, error) { // Check if the parameter is present in the request - if _, ok := r.GetArguments()[p]; !ok { + if _, ok := args[p]; !ok { return []string{}, nil } - switch v := r.GetArguments()[p].(type) { + switch v := args[p].(type) { case nil: return []string{}, nil case []string: @@ -206,7 +206,7 @@ func OptionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) } return strSlice, nil default: - return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, r.GetArguments()[p]) + return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, args[p]) } } @@ -234,13 +234,13 @@ func convertStringToBigInt(s string, def int64) (int64, error) { // It does the following checks: // 1. Checks if the parameter is present in the request, if not, it returns an empty slice // 2. If it is present, iterates the elements, checks each is a string, and converts them to int64 values -func OptionalBigIntArrayParam(r mcp.CallToolRequest, p string) ([]int64, error) { +func OptionalBigIntArrayParam(args map[string]any, p string) ([]int64, error) { // Check if the parameter is present in the request - if _, ok := r.GetArguments()[p]; !ok { + if _, ok := args[p]; !ok { return []int64{}, nil } - switch v := r.GetArguments()[p].(type) { + switch v := args[p].(type) { case nil: return []int64{}, nil case []string: @@ -260,61 +260,68 @@ func OptionalBigIntArrayParam(r mcp.CallToolRequest, p string) ([]int64, error) } return int64Slice, nil default: - return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, r.GetArguments()[p]) + return []int64{}, fmt.Errorf("parameter %s could not be coerced to []int64, is %T", p, args[p]) } } // WithPagination adds REST API pagination parameters to a tool. // https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api -func WithPagination() mcp.ToolOption { - return func(tool *mcp.Tool) { - mcp.WithNumber("page", - mcp.Description("Page number for pagination (min 1)"), - mcp.Min(1), - )(tool) - - mcp.WithNumber("perPage", - mcp.Description("Results per page for pagination (min 1, max 100)"), - mcp.Min(1), - mcp.Max(100), - )(tool) +func WithPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "Number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "Number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), } + + return schema } // WithUnifiedPagination adds REST API pagination parameters to a tool. // GraphQL tools will use this and convert page/perPage to GraphQL cursor parameters internally. -func WithUnifiedPagination() mcp.ToolOption { - return func(tool *mcp.Tool) { - mcp.WithNumber("page", - mcp.Description("Page number for pagination (min 1)"), - mcp.Min(1), - )(tool) - - mcp.WithNumber("perPage", - mcp.Description("Results per page for pagination (min 1, max 100)"), - mcp.Min(1), - mcp.Max(100), - )(tool) - - mcp.WithString("after", - mcp.Description("Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs."), - )(tool) +func WithUnifiedPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["page"] = &jsonschema.Schema{ + Type: "Number", + Description: "Page number for pagination (min 1)", + Minimum: jsonschema.Ptr(1.0), + } + + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "Number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "String", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema } // WithCursorPagination adds only cursor-based pagination parameters to a tool (no page parameter). -func WithCursorPagination() mcp.ToolOption { - return func(tool *mcp.Tool) { - mcp.WithNumber("perPage", - mcp.Description("Results per page for pagination (min 1, max 100)"), - mcp.Min(1), - mcp.Max(100), - )(tool) - - mcp.WithString("after", - mcp.Description("Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs."), - )(tool) +func WithCursorPagination(schema *jsonschema.Schema) *jsonschema.Schema { + schema.Properties["perPage"] = &jsonschema.Schema{ + Type: "Number", + Description: "Results per page for pagination (min 1, max 100)", + Minimum: jsonschema.Ptr(1.0), + Maximum: jsonschema.Ptr(100.0), } + + schema.Properties["after"] = &jsonschema.Schema{ + Type: "String", + Description: "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + } + + return schema } type PaginationParams struct { @@ -328,16 +335,16 @@ type PaginationParams struct { // In future, we may want to make the default values configurable, or even have this // function returned from `withPagination`, where the defaults are provided alongside // the min/max values. -func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) { - page, err := OptionalIntParamWithDefault(r, "page", 1) +func OptionalPaginationParams(args map[string]any) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(args, "page", 1) if err != nil { return PaginationParams{}, err } - perPage, err := OptionalIntParamWithDefault(r, "perPage", 30) + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) if err != nil { return PaginationParams{}, err } - after, err := OptionalParam[string](r, "after") + after, err := OptionalParam[string](args, "after") if err != nil { return PaginationParams{}, err } @@ -350,12 +357,12 @@ func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) { // OptionalCursorPaginationParams returns the "perPage" and "after" parameters from the request, // without the "page" parameter, suitable for cursor-based pagination only. -func OptionalCursorPaginationParams(r mcp.CallToolRequest) (CursorPaginationParams, error) { - perPage, err := OptionalIntParamWithDefault(r, "perPage", 30) +func OptionalCursorPaginationParams(args map[string]any) (CursorPaginationParams, error) { + perPage, err := OptionalIntParamWithDefault(args, "perPage", 30) if err != nil { return CursorPaginationParams{}, err } - after, err := OptionalParam[string](r, "after") + after, err := OptionalParam[string](args, "after") if err != nil { return CursorPaginationParams{}, err } @@ -411,8 +418,8 @@ func (p PaginationParams) ToGraphQLParams() (*GraphQLPaginationParams, error) { func MarshalledTextResult(v any) *mcp.CallToolResult { data, err := json.Marshal(v) if err != nil { - return mcp.NewToolResultErrorFromErr("failed to marshal text result to json", err) + return utils.NewToolResultErrorFromErr("failed to marshal text result to json", err) } - return mcp.NewToolResultText(string(data)) + return utils.NewToolResultText(string(data)) } diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 3bfc1ef94..599be18ce 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -141,8 +141,7 @@ func Test_RequiredStringParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := RequiredParam[string](request, tc.paramName) + result, err := RequiredParam[string](tc.params, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -194,8 +193,7 @@ func Test_OptionalStringParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := OptionalParam[string](request, tc.paramName) + result, err := OptionalParam[string](tc.params, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -240,8 +238,7 @@ func Test_RequiredInt(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := RequiredInt(request, tc.paramName) + result, err := RequiredInt(tc.params, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -292,8 +289,7 @@ func Test_OptionalIntParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := OptionalIntParam(request, tc.paramName) + result, err := OptionalIntParam(tc.params, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -350,8 +346,7 @@ func Test_OptionalNumberParamWithDefault(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := OptionalIntParamWithDefault(request, tc.paramName, tc.defaultVal) + result, err := OptionalIntParamWithDefault(tc.params, tc.paramName, tc.defaultVal) if tc.expectError { assert.Error(t, err) @@ -403,8 +398,7 @@ func Test_OptionalBooleanParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := OptionalParam[bool](request, tc.paramName) + result, err := OptionalParam[bool](tc.params, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -471,8 +465,7 @@ func TestOptionalStringArrayParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := OptionalStringArrayParam(request, tc.paramName) + result, err := OptionalStringArrayParam(tc.params, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -554,8 +547,7 @@ func TestOptionalPaginationParams(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - request := createMCPRequest(tc.params) - result, err := OptionalPaginationParams(request) + result, err := OptionalPaginationParams(tc.params) if tc.expectError { assert.Error(t, err) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 0594f2f94..c024a31e9 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" ) @@ -164,147 +164,147 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // Define all available features with their default state (disabled) // Create toolsets - repos := toolsets.NewToolset(ToolsetMetadataRepos.ID, ToolsetMetadataRepos.Description). - AddReadTools( - toolsets.NewServerTool(SearchRepositories(getClient, t)), - toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)), - toolsets.NewServerTool(ListCommits(getClient, t)), - toolsets.NewServerTool(SearchCode(getClient, t)), - toolsets.NewServerTool(GetCommit(getClient, t)), - toolsets.NewServerTool(ListBranches(getClient, t)), - toolsets.NewServerTool(ListTags(getClient, t)), - toolsets.NewServerTool(GetTag(getClient, t)), - toolsets.NewServerTool(ListReleases(getClient, t)), - toolsets.NewServerTool(GetLatestRelease(getClient, t)), - toolsets.NewServerTool(GetReleaseByTag(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)), - toolsets.NewServerTool(CreateRepository(getClient, t)), - toolsets.NewServerTool(ForkRepository(getClient, t)), - toolsets.NewServerTool(CreateBranch(getClient, t)), - toolsets.NewServerTool(PushFiles(getClient, t)), - toolsets.NewServerTool(DeleteFile(getClient, t)), - ). - AddResourceTemplates( - toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceBranchContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceCommitContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceTagContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, getRawClient, t)), - ) - git := toolsets.NewToolset(ToolsetMetadataGit.ID, ToolsetMetadataGit.Description). - AddReadTools( - toolsets.NewServerTool(GetRepositoryTree(getClient, t)), - ) - issues := toolsets.NewToolset(ToolsetMetadataIssues.ID, ToolsetMetadataIssues.Description). - AddReadTools( - toolsets.NewServerTool(IssueRead(getClient, getGQLClient, t, flags)), - toolsets.NewServerTool(SearchIssues(getClient, t)), - toolsets.NewServerTool(ListIssues(getGQLClient, t)), - toolsets.NewServerTool(ListIssueTypes(getClient, t)), - toolsets.NewServerTool(GetLabel(getGQLClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(IssueWrite(getClient, getGQLClient, t)), - toolsets.NewServerTool(AddIssueComment(getClient, t)), - toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)), - toolsets.NewServerTool(SubIssueWrite(getClient, t)), - ).AddPrompts( - toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), - toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), - ) - users := toolsets.NewToolset(ToolsetMetadataUsers.ID, ToolsetMetadataUsers.Description). - AddReadTools( - toolsets.NewServerTool(SearchUsers(getClient, t)), - ) - orgs := toolsets.NewToolset(ToolsetMetadataOrgs.ID, ToolsetMetadataOrgs.Description). - AddReadTools( - toolsets.NewServerTool(SearchOrgs(getClient, t)), - ) - pullRequests := toolsets.NewToolset(ToolsetMetadataPullRequests.ID, ToolsetMetadataPullRequests.Description). - AddReadTools( - toolsets.NewServerTool(PullRequestRead(getClient, t, flags)), - toolsets.NewServerTool(ListPullRequests(getClient, t)), - toolsets.NewServerTool(SearchPullRequests(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(MergePullRequest(getClient, t)), - toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), - toolsets.NewServerTool(CreatePullRequest(getClient, t)), - toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)), - toolsets.NewServerTool(RequestCopilotReview(getClient, t)), - - // Reviews - toolsets.NewServerTool(PullRequestReviewWrite(getGQLClient, t)), - toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)), - ) - codeSecurity := toolsets.NewToolset(ToolsetMetadataCodeSecurity.ID, ToolsetMetadataCodeSecurity.Description). - AddReadTools( - toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), - toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), - ) - secretProtection := toolsets.NewToolset(ToolsetMetadataSecretProtection.ID, ToolsetMetadataSecretProtection.Description). - AddReadTools( - toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), - toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), - ) - dependabot := toolsets.NewToolset(ToolsetMetadataDependabot.ID, ToolsetMetadataDependabot.Description). - AddReadTools( - toolsets.NewServerTool(GetDependabotAlert(getClient, t)), - toolsets.NewServerTool(ListDependabotAlerts(getClient, t)), - ) - - notifications := toolsets.NewToolset(ToolsetMetadataNotifications.ID, ToolsetMetadataNotifications.Description). - AddReadTools( - toolsets.NewServerTool(ListNotifications(getClient, t)), - toolsets.NewServerTool(GetNotificationDetails(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(DismissNotification(getClient, t)), - toolsets.NewServerTool(MarkAllNotificationsRead(getClient, t)), - toolsets.NewServerTool(ManageNotificationSubscription(getClient, t)), - toolsets.NewServerTool(ManageRepositoryNotificationSubscription(getClient, t)), - ) - - discussions := toolsets.NewToolset(ToolsetMetadataDiscussions.ID, ToolsetMetadataDiscussions.Description). - AddReadTools( - toolsets.NewServerTool(ListDiscussions(getGQLClient, t)), - toolsets.NewServerTool(GetDiscussion(getGQLClient, t)), - toolsets.NewServerTool(GetDiscussionComments(getGQLClient, t)), - toolsets.NewServerTool(ListDiscussionCategories(getGQLClient, t)), - ) - - actions := toolsets.NewToolset(ToolsetMetadataActions.ID, ToolsetMetadataActions.Description). - AddReadTools( - toolsets.NewServerTool(ListWorkflows(getClient, t)), - toolsets.NewServerTool(ListWorkflowRuns(getClient, t)), - toolsets.NewServerTool(GetWorkflowRun(getClient, t)), - toolsets.NewServerTool(GetWorkflowRunLogs(getClient, t)), - toolsets.NewServerTool(ListWorkflowJobs(getClient, t)), - toolsets.NewServerTool(GetJobLogs(getClient, t, contentWindowSize)), - toolsets.NewServerTool(ListWorkflowRunArtifacts(getClient, t)), - toolsets.NewServerTool(DownloadWorkflowRunArtifact(getClient, t)), - toolsets.NewServerTool(GetWorkflowRunUsage(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(RunWorkflow(getClient, t)), - toolsets.NewServerTool(RerunWorkflowRun(getClient, t)), - toolsets.NewServerTool(RerunFailedJobs(getClient, t)), - toolsets.NewServerTool(CancelWorkflowRun(getClient, t)), - toolsets.NewServerTool(DeleteWorkflowRunLogs(getClient, t)), - ) - - securityAdvisories := toolsets.NewToolset(ToolsetMetadataSecurityAdvisories.ID, ToolsetMetadataSecurityAdvisories.Description). - AddReadTools( - toolsets.NewServerTool(ListGlobalSecurityAdvisories(getClient, t)), - toolsets.NewServerTool(GetGlobalSecurityAdvisory(getClient, t)), - toolsets.NewServerTool(ListRepositorySecurityAdvisories(getClient, t)), - toolsets.NewServerTool(ListOrgRepositorySecurityAdvisories(getClient, t)), - ) - - // Keep experiments alive so the system doesn't error out when it's always enabled - experiments := toolsets.NewToolset(ToolsetMetadataExperiments.ID, ToolsetMetadataExperiments.Description) + // repos := toolsets.NewToolset(ToolsetMetadataRepos.ID, ToolsetMetadataRepos.Description). + // AddReadTools( + // toolsets.NewServerTool(SearchRepositories(getClient, t)), + // toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)), + // toolsets.NewServerTool(ListCommits(getClient, t)), + // toolsets.NewServerTool(SearchCode(getClient, t)), + // toolsets.NewServerTool(GetCommit(getClient, t)), + // toolsets.NewServerTool(ListBranches(getClient, t)), + // toolsets.NewServerTool(ListTags(getClient, t)), + // toolsets.NewServerTool(GetTag(getClient, t)), + // toolsets.NewServerTool(ListReleases(getClient, t)), + // toolsets.NewServerTool(GetLatestRelease(getClient, t)), + // toolsets.NewServerTool(GetReleaseByTag(getClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)), + // toolsets.NewServerTool(CreateRepository(getClient, t)), + // toolsets.NewServerTool(ForkRepository(getClient, t)), + // toolsets.NewServerTool(CreateBranch(getClient, t)), + // toolsets.NewServerTool(PushFiles(getClient, t)), + // toolsets.NewServerTool(DeleteFile(getClient, t)), + // ). + // AddResourceTemplates( + // toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, getRawClient, t)), + // toolsets.NewServerResourceTemplate(GetRepositoryResourceBranchContent(getClient, getRawClient, t)), + // toolsets.NewServerResourceTemplate(GetRepositoryResourceCommitContent(getClient, getRawClient, t)), + // toolsets.NewServerResourceTemplate(GetRepositoryResourceTagContent(getClient, getRawClient, t)), + // toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, getRawClient, t)), + // ) + // git := toolsets.NewToolset(ToolsetMetadataGit.ID, ToolsetMetadataGit.Description). + // AddReadTools( + // toolsets.NewServerTool(GetRepositoryTree(getClient, t)), + // ) + // issues := toolsets.NewToolset(ToolsetMetadataIssues.ID, ToolsetMetadataIssues.Description). + // AddReadTools( + // toolsets.NewServerTool(IssueRead(getClient, getGQLClient, t, flags)), + // toolsets.NewServerTool(SearchIssues(getClient, t)), + // toolsets.NewServerTool(ListIssues(getGQLClient, t)), + // toolsets.NewServerTool(ListIssueTypes(getClient, t)), + // toolsets.NewServerTool(GetLabel(getGQLClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(IssueWrite(getClient, getGQLClient, t)), + // toolsets.NewServerTool(AddIssueComment(getClient, t)), + // toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)), + // toolsets.NewServerTool(SubIssueWrite(getClient, t)), + // ).AddPrompts( + // toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), + // toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), + // ) + // users := toolsets.NewToolset(ToolsetMetadataUsers.ID, ToolsetMetadataUsers.Description). + // AddReadTools( + // toolsets.NewServerTool(SearchUsers(getClient, t)), + // ) + // orgs := toolsets.NewToolset(ToolsetMetadataOrgs.ID, ToolsetMetadataOrgs.Description). + // AddReadTools( + // toolsets.NewServerTool(SearchOrgs(getClient, t)), + // ) + // pullRequests := toolsets.NewToolset(ToolsetMetadataPullRequests.ID, ToolsetMetadataPullRequests.Description). + // AddReadTools( + // toolsets.NewServerTool(PullRequestRead(getClient, t, flags)), + // toolsets.NewServerTool(ListPullRequests(getClient, t)), + // toolsets.NewServerTool(SearchPullRequests(getClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(MergePullRequest(getClient, t)), + // toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), + // toolsets.NewServerTool(CreatePullRequest(getClient, t)), + // toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)), + // toolsets.NewServerTool(RequestCopilotReview(getClient, t)), + + // // Reviews + // toolsets.NewServerTool(PullRequestReviewWrite(getGQLClient, t)), + // toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)), + // ) + // codeSecurity := toolsets.NewToolset(ToolsetMetadataCodeSecurity.ID, ToolsetMetadataCodeSecurity.Description). + // AddReadTools( + // toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), + // toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), + // ) + // secretProtection := toolsets.NewToolset(ToolsetMetadataSecretProtection.ID, ToolsetMetadataSecretProtection.Description). + // AddReadTools( + // toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), + // toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), + // ) + // dependabot := toolsets.NewToolset(ToolsetMetadataDependabot.ID, ToolsetMetadataDependabot.Description). + // AddReadTools( + // toolsets.NewServerTool(GetDependabotAlert(getClient, t)), + // toolsets.NewServerTool(ListDependabotAlerts(getClient, t)), + // ) + + // notifications := toolsets.NewToolset(ToolsetMetadataNotifications.ID, ToolsetMetadataNotifications.Description). + // AddReadTools( + // toolsets.NewServerTool(ListNotifications(getClient, t)), + // toolsets.NewServerTool(GetNotificationDetails(getClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(DismissNotification(getClient, t)), + // toolsets.NewServerTool(MarkAllNotificationsRead(getClient, t)), + // toolsets.NewServerTool(ManageNotificationSubscription(getClient, t)), + // toolsets.NewServerTool(ManageRepositoryNotificationSubscription(getClient, t)), + // ) + + // discussions := toolsets.NewToolset(ToolsetMetadataDiscussions.ID, ToolsetMetadataDiscussions.Description). + // AddReadTools( + // toolsets.NewServerTool(ListDiscussions(getGQLClient, t)), + // toolsets.NewServerTool(GetDiscussion(getGQLClient, t)), + // toolsets.NewServerTool(GetDiscussionComments(getGQLClient, t)), + // toolsets.NewServerTool(ListDiscussionCategories(getGQLClient, t)), + // ) + + // actions := toolsets.NewToolset(ToolsetMetadataActions.ID, ToolsetMetadataActions.Description). + // AddReadTools( + // toolsets.NewServerTool(ListWorkflows(getClient, t)), + // toolsets.NewServerTool(ListWorkflowRuns(getClient, t)), + // toolsets.NewServerTool(GetWorkflowRun(getClient, t)), + // toolsets.NewServerTool(GetWorkflowRunLogs(getClient, t)), + // toolsets.NewServerTool(ListWorkflowJobs(getClient, t)), + // toolsets.NewServerTool(GetJobLogs(getClient, t, contentWindowSize)), + // toolsets.NewServerTool(ListWorkflowRunArtifacts(getClient, t)), + // toolsets.NewServerTool(DownloadWorkflowRunArtifact(getClient, t)), + // toolsets.NewServerTool(GetWorkflowRunUsage(getClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(RunWorkflow(getClient, t)), + // toolsets.NewServerTool(RerunWorkflowRun(getClient, t)), + // toolsets.NewServerTool(RerunFailedJobs(getClient, t)), + // toolsets.NewServerTool(CancelWorkflowRun(getClient, t)), + // toolsets.NewServerTool(DeleteWorkflowRunLogs(getClient, t)), + // ) + + // securityAdvisories := toolsets.NewToolset(ToolsetMetadataSecurityAdvisories.ID, ToolsetMetadataSecurityAdvisories.Description). + // AddReadTools( + // toolsets.NewServerTool(ListGlobalSecurityAdvisories(getClient, t)), + // toolsets.NewServerTool(GetGlobalSecurityAdvisory(getClient, t)), + // toolsets.NewServerTool(ListRepositorySecurityAdvisories(getClient, t)), + // toolsets.NewServerTool(ListOrgRepositorySecurityAdvisories(getClient, t)), + // ) + + // // Keep experiments alive so the system doesn't error out when it's always enabled + // experiments := toolsets.NewToolset(ToolsetMetadataExperiments.ID, ToolsetMetadataExperiments.Description) contextTools := toolsets.NewToolset(ToolsetMetadataContext.ID, ToolsetMetadataContext.Description). AddReadTools( @@ -313,82 +313,83 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetTeamMembers(getGQLClient, t)), ) - gists := toolsets.NewToolset(ToolsetMetadataGists.ID, ToolsetMetadataGists.Description). - AddReadTools( - toolsets.NewServerTool(ListGists(getClient, t)), - toolsets.NewServerTool(GetGist(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(CreateGist(getClient, t)), - toolsets.NewServerTool(UpdateGist(getClient, t)), - ) + // gists := toolsets.NewToolset(ToolsetMetadataGists.ID, ToolsetMetadataGists.Description). + // AddReadTools( + // toolsets.NewServerTool(ListGists(getClient, t)), + // toolsets.NewServerTool(GetGist(getClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(CreateGist(getClient, t)), + // toolsets.NewServerTool(UpdateGist(getClient, t)), + // ) + + // projects := toolsets.NewToolset(ToolsetMetadataProjects.ID, ToolsetMetadataProjects.Description). + // AddReadTools( + // toolsets.NewServerTool(ListProjects(getClient, t)), + // toolsets.NewServerTool(GetProject(getClient, t)), + // toolsets.NewServerTool(ListProjectFields(getClient, t)), + // toolsets.NewServerTool(GetProjectField(getClient, t)), + // toolsets.NewServerTool(ListProjectItems(getClient, t)), + // toolsets.NewServerTool(GetProjectItem(getClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(AddProjectItem(getClient, t)), + // toolsets.NewServerTool(DeleteProjectItem(getClient, t)), + // toolsets.NewServerTool(UpdateProjectItem(getClient, t)), + // ) + // stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). + // AddReadTools( + // toolsets.NewServerTool(ListStarredRepositories(getClient, t)), + // ). + // AddWriteTools( + // toolsets.NewServerTool(StarRepository(getClient, t)), + // toolsets.NewServerTool(UnstarRepository(getClient, t)), + // ) + // labels := toolsets.NewToolset(ToolsetLabels.ID, ToolsetLabels.Description). + // AddReadTools( + // // get + // toolsets.NewServerTool(GetLabel(getGQLClient, t)), + // // list labels on repo or issue + // toolsets.NewServerTool(ListLabels(getGQLClient, t)), + // ). + // AddWriteTools( + // // create or update + // toolsets.NewServerTool(LabelWrite(getGQLClient, t)), + // ) - projects := toolsets.NewToolset(ToolsetMetadataProjects.ID, ToolsetMetadataProjects.Description). - AddReadTools( - toolsets.NewServerTool(ListProjects(getClient, t)), - toolsets.NewServerTool(GetProject(getClient, t)), - toolsets.NewServerTool(ListProjectFields(getClient, t)), - toolsets.NewServerTool(GetProjectField(getClient, t)), - toolsets.NewServerTool(ListProjectItems(getClient, t)), - toolsets.NewServerTool(GetProjectItem(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(AddProjectItem(getClient, t)), - toolsets.NewServerTool(DeleteProjectItem(getClient, t)), - toolsets.NewServerTool(UpdateProjectItem(getClient, t)), - ) - stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). - AddReadTools( - toolsets.NewServerTool(ListStarredRepositories(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(StarRepository(getClient, t)), - toolsets.NewServerTool(UnstarRepository(getClient, t)), - ) - labels := toolsets.NewToolset(ToolsetLabels.ID, ToolsetLabels.Description). - AddReadTools( - // get - toolsets.NewServerTool(GetLabel(getGQLClient, t)), - // list labels on repo or issue - toolsets.NewServerTool(ListLabels(getGQLClient, t)), - ). - AddWriteTools( - // create or update - toolsets.NewServerTool(LabelWrite(getGQLClient, t)), - ) // Add toolsets to the group tsg.AddToolset(contextTools) - tsg.AddToolset(repos) - tsg.AddToolset(git) - tsg.AddToolset(issues) - tsg.AddToolset(orgs) - tsg.AddToolset(users) - tsg.AddToolset(pullRequests) - tsg.AddToolset(actions) - tsg.AddToolset(codeSecurity) - tsg.AddToolset(secretProtection) - tsg.AddToolset(dependabot) - tsg.AddToolset(notifications) - tsg.AddToolset(experiments) - tsg.AddToolset(discussions) - tsg.AddToolset(gists) - tsg.AddToolset(securityAdvisories) - tsg.AddToolset(projects) - tsg.AddToolset(stargazers) - tsg.AddToolset(labels) + // tsg.AddToolset(repos) + // tsg.AddToolset(git) + // tsg.AddToolset(issues) + // tsg.AddToolset(orgs) + // tsg.AddToolset(users) + // tsg.AddToolset(pullRequests) + // tsg.AddToolset(actions) + // tsg.AddToolset(codeSecurity) + // tsg.AddToolset(secretProtection) + // tsg.AddToolset(dependabot) + // tsg.AddToolset(notifications) + // tsg.AddToolset(experiments) + // tsg.AddToolset(discussions) + // tsg.AddToolset(gists) + // tsg.AddToolset(securityAdvisories) + // tsg.AddToolset(projects) + // tsg.AddToolset(stargazers) + // tsg.AddToolset(labels) return tsg } // InitDynamicToolset creates a dynamic toolset that can be used to enable other toolsets, and so requires the server and toolset group as arguments -func InitDynamicToolset(s *server.MCPServer, tsg *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) *toolsets.Toolset { +func InitDynamicToolset(s *mcp.Server, tsg *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) *toolsets.Toolset { // Create a new dynamic toolset // Need to add the dynamic toolset last so it can be used to enable other toolsets dynamicToolSelection := toolsets.NewToolset(ToolsetMetadataDynamic.ID, ToolsetMetadataDynamic.Description). AddReadTools( - toolsets.NewServerTool(ListAvailableToolsets(tsg, t)), - toolsets.NewServerTool(GetToolsetsTools(tsg, t)), - toolsets.NewServerTool(EnableToolset(s, tsg, t)), + // toolsets.NewServerTool(ListAvailableToolsets(tsg, t)), + // toolsets.NewServerTool(GetToolsetsTools(tsg, t)), + // toolsets.NewServerTool(EnableToolset(s, tsg, t)), ) dynamicToolSelection.Enabled = true diff --git a/pkg/log/io.go b/pkg/log/io.go index 44b8dc17a..0f034c2a4 100644 --- a/pkg/log/io.go +++ b/pkg/log/io.go @@ -9,6 +9,8 @@ import ( // IOLogger is a wrapper around io.Reader and io.Writer that can be used // to log the data being read and written from the underlying streams type IOLogger struct { + io.ReadWriteCloser + reader io.Reader writer io.Writer logger *slog.Logger diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go index 96f1fc3ca..7ff7a8d41 100644 --- a/pkg/toolsets/toolsets.go +++ b/pkg/toolsets/toolsets.go @@ -3,8 +3,7 @@ package toolsets import ( "fmt" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) type ToolsetDoesNotExistError struct { @@ -29,19 +28,36 @@ func NewToolsetDoesNotExistError(name string) *ToolsetDoesNotExistError { return &ToolsetDoesNotExistError{Name: name} } -func NewServerTool(tool mcp.Tool, handler server.ToolHandlerFunc) server.ServerTool { - return server.ServerTool{Tool: tool, Handler: handler} +type ServerTool struct { + Tool mcp.Tool + RegisterFunc func(s *mcp.Server) } -func NewServerResourceTemplate(resourceTemplate mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) server.ServerResourceTemplate { - return server.ServerResourceTemplate{ +func NewServerTool[In, Out any](tool mcp.Tool, handler mcp.ToolHandlerFor[In, Out]) ServerTool { + return ServerTool{Tool: tool, RegisterFunc: func(s *mcp.Server) { + mcp.AddTool(s, &tool, handler) + }} +} + +type ServerResourceTemplate struct { + Template mcp.ResourceTemplate + Handler mcp.ResourceHandler +} + +func NewServerResourceTemplate(resourceTemplate mcp.ResourceTemplate, handler mcp.ResourceHandler) ServerResourceTemplate { + return ServerResourceTemplate{ Template: resourceTemplate, Handler: handler, } } -func NewServerPrompt(prompt mcp.Prompt, handler server.PromptHandlerFunc) server.ServerPrompt { - return server.ServerPrompt{ +type ServerPrompt struct { + Prompt mcp.Prompt + Handler mcp.PromptHandler +} + +func NewServerPrompt(prompt mcp.Prompt, handler mcp.PromptHandler) ServerPrompt { + return ServerPrompt{ Prompt: prompt, Handler: handler, } @@ -53,16 +69,16 @@ type Toolset struct { Description string Enabled bool readOnly bool - writeTools []server.ServerTool - readTools []server.ServerTool + writeTools []ServerTool + readTools []ServerTool // resources are not tools, but the community seems to be moving towards namespaces as a broader concept // and in order to have multiple servers running concurrently, we want to avoid overlapping resources too. - resourceTemplates []server.ServerResourceTemplate + resourceTemplates []ServerResourceTemplate // prompts are also not tools but are namespaced similarly - prompts []server.ServerPrompt + prompts []ServerPrompt } -func (t *Toolset) GetActiveTools() []server.ServerTool { +func (t *Toolset) GetActiveTools() []ServerTool { if t.Enabled { if t.readOnly { return t.readTools @@ -72,63 +88,63 @@ func (t *Toolset) GetActiveTools() []server.ServerTool { return nil } -func (t *Toolset) GetAvailableTools() []server.ServerTool { +func (t *Toolset) GetAvailableTools() []ServerTool { if t.readOnly { return t.readTools } return append(t.readTools, t.writeTools...) } -func (t *Toolset) RegisterTools(s *server.MCPServer) { +func (t *Toolset) RegisterTools(s *mcp.Server) { if !t.Enabled { return } for _, tool := range t.readTools { - s.AddTool(tool.Tool, tool.Handler) + tool.RegisterFunc(s) } if !t.readOnly { for _, tool := range t.writeTools { - s.AddTool(tool.Tool, tool.Handler) + tool.RegisterFunc(s) } } } -func (t *Toolset) AddResourceTemplates(templates ...server.ServerResourceTemplate) *Toolset { +func (t *Toolset) AddResourceTemplates(templates ...ServerResourceTemplate) *Toolset { t.resourceTemplates = append(t.resourceTemplates, templates...) return t } -func (t *Toolset) AddPrompts(prompts ...server.ServerPrompt) *Toolset { +func (t *Toolset) AddPrompts(prompts ...ServerPrompt) *Toolset { t.prompts = append(t.prompts, prompts...) return t } -func (t *Toolset) GetActiveResourceTemplates() []server.ServerResourceTemplate { +func (t *Toolset) GetActiveResourceTemplates() []ServerResourceTemplate { if !t.Enabled { return nil } return t.resourceTemplates } -func (t *Toolset) GetAvailableResourceTemplates() []server.ServerResourceTemplate { +func (t *Toolset) GetAvailableResourceTemplates() []ServerResourceTemplate { return t.resourceTemplates } -func (t *Toolset) RegisterResourcesTemplates(s *server.MCPServer) { +func (t *Toolset) RegisterResourcesTemplates(s *mcp.Server) { if !t.Enabled { return } for _, resource := range t.resourceTemplates { - s.AddResourceTemplate(resource.Template, resource.Handler) + s.AddResourceTemplate(&resource.Template, resource.Handler) } } -func (t *Toolset) RegisterPrompts(s *server.MCPServer) { +func (t *Toolset) RegisterPrompts(s *mcp.Server) { if !t.Enabled { return } for _, prompt := range t.prompts { - s.AddPrompt(prompt.Prompt, prompt.Handler) + s.AddPrompt(&prompt.Prompt, prompt.Handler) } } @@ -137,10 +153,10 @@ func (t *Toolset) SetReadOnly() { t.readOnly = true } -func (t *Toolset) AddWriteTools(tools ...server.ServerTool) *Toolset { +func (t *Toolset) AddWriteTools(tools ...ServerTool) *Toolset { // Silently ignore if the toolset is read-only to avoid any breach of that contract for _, tool := range tools { - if *tool.Tool.Annotations.ReadOnlyHint { + if tool.Tool.Annotations.ReadOnlyHint { panic(fmt.Sprintf("tool (%s) is incorrectly annotated as read-only", tool.Tool.Name)) } } @@ -150,9 +166,9 @@ func (t *Toolset) AddWriteTools(tools ...server.ServerTool) *Toolset { return t } -func (t *Toolset) AddReadTools(tools ...server.ServerTool) *Toolset { +func (t *Toolset) AddReadTools(tools ...ServerTool) *Toolset { for _, tool := range tools { - if !*tool.Tool.Annotations.ReadOnlyHint { + if !tool.Tool.Annotations.ReadOnlyHint { panic(fmt.Sprintf("tool (%s) must be annotated as read-only", tool.Tool.Name)) } } @@ -248,7 +264,7 @@ func (tg *ToolsetGroup) EnableToolset(name string) error { return nil } -func (tg *ToolsetGroup) RegisterAll(s *server.MCPServer) { +func (tg *ToolsetGroup) RegisterAll(s *mcp.Server) { for _, toolset := range tg.Toolsets { toolset.RegisterTools(s) toolset.RegisterResourcesTemplates(s) diff --git a/pkg/utils/result.go b/pkg/utils/result.go new file mode 100644 index 000000000..c90a911de --- /dev/null +++ b/pkg/utils/result.go @@ -0,0 +1,49 @@ +package utils + +import "github.com/modelcontextprotocol/go-sdk/mcp" + +func NewToolResultText(message string) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: message, + }, + }, + } +} + +func NewToolResultError(message string) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: message, + }, + }, + IsError: true, + } +} + +func NewToolResultErrorFromErr(message string, err error) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: message + ": " + err.Error(), + }, + }, + IsError: true, + } +} + +func NewToolResultResource(message string, contents *mcp.ResourceContents) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: message, + }, + &mcp.EmbeddedResource{ + Resource: contents, + }, + }, + IsError: false, + } +} diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index eecc6faa8..3823082a9 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -18,6 +18,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v79.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) + - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.3.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE)) @@ -28,6 +29,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.36.0/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) + - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.1.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE)) - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index eecc6faa8..3823082a9 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -18,6 +18,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v79.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) + - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.3.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE)) @@ -28,6 +29,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.36.0/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) + - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.1.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE)) - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 75fe8172a..fa5cba4e0 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -18,6 +18,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE)) - [github.com/google/go-github/v79/github](https://pkg.go.dev/github.com/google/go-github/v79/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v79.0.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) + - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.3.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE)) @@ -29,6 +30,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.36.0/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE)) + - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.1.0/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE)) - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE)) diff --git a/third-party/github.com/google/jsonschema-go/jsonschema/LICENSE b/third-party/github.com/google/jsonschema-go/jsonschema/LICENSE new file mode 100644 index 000000000..1cb53e9df --- /dev/null +++ b/third-party/github.com/google/jsonschema-go/jsonschema/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 JSON Schema Go Project Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third-party/github.com/modelcontextprotocol/go-sdk/LICENSE b/third-party/github.com/modelcontextprotocol/go-sdk/LICENSE new file mode 100644 index 000000000..508be9266 --- /dev/null +++ b/third-party/github.com/modelcontextprotocol/go-sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Go MCP SDK Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.