Skip to content
Merged
112 changes: 112 additions & 0 deletions README.md

Large diffs are not rendered by default.

43 changes: 37 additions & 6 deletions cmd/github-mcp-server/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -189,7 +188,7 @@ func generateToolsDoc(r *inventory.Inventory) string {
currentToolsetID = tool.Toolset.ID
currentToolsetIcon = tool.Toolset.Icon
}
writeToolDoc(&toolBuf, tool.Tool)
writeToolDoc(&toolBuf, tool)
toolBuf.WriteString("\n\n")
}

Expand Down Expand Up @@ -223,16 +222,26 @@ func formatToolsetName(name string) string {
}
}

func writeToolDoc(buf *strings.Builder, tool mcp.Tool) {
func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
// Tool name (no icon - section header already has the toolset icon)
fmt.Fprintf(buf, "- **%s** - %s\n", tool.Name, tool.Annotations.Title)
fmt.Fprintf(buf, "- **%s** - %s\n", tool.Tool.Name, tool.Tool.Annotations.Title)

// OAuth scopes if present
if len(tool.RequiredScopes) > 0 {
fmt.Fprintf(buf, " - **Required OAuth Scopes**: `%s`\n", strings.Join(tool.RequiredScopes, "`, `"))

// Only show accepted scopes if they differ from required scopes
if len(tool.AcceptedScopes) > 0 && !scopesEqual(tool.RequiredScopes, tool.AcceptedScopes) {
fmt.Fprintf(buf, " - **Accepted OAuth Scopes**: `%s`\n", strings.Join(tool.AcceptedScopes, "`, `"))
}
}

// Parameters
if tool.InputSchema == nil {
if tool.Tool.InputSchema == nil {
buf.WriteString(" - No parameters required")
return
}
schema, ok := tool.InputSchema.(*jsonschema.Schema)
schema, ok := tool.Tool.InputSchema.(*jsonschema.Schema)
if !ok || schema == nil {
buf.WriteString(" - No parameters required")
return
Expand Down Expand Up @@ -281,6 +290,28 @@ func writeToolDoc(buf *strings.Builder, tool mcp.Tool) {
}
}

// scopesEqual checks if two scope slices contain the same elements (order-independent)
func scopesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}

// Create a map for quick lookup
aMap := make(map[string]bool, len(a))
for _, scope := range a {
aMap[scope] = true
}

// Check if all elements in b are in a
for _, scope := range b {
if !aMap[scope] {
return false
}
}

return true
}

func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
Expand Down
19 changes: 19 additions & 0 deletions pkg/github/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
buffer "github.com/github/github-mcp-server/pkg/buffer"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/go-github/v79/github"
Expand Down Expand Up @@ -74,6 +75,7 @@ func ListWorkflows(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo"},
}),
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -200,6 +202,7 @@ func ListWorkflowRuns(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "workflow_id"},
}),
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -311,6 +314,7 @@ func RunWorkflow(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo", "workflow_id", "ref"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -415,6 +419,7 @@ func GetWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo", "run_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -483,6 +488,7 @@ func GetWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.ServerTo
Required: []string{"owner", "repo", "run_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -566,6 +572,7 @@ func ListWorkflowJobs(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "run_id"},
}),
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -678,6 +685,7 @@ func GetJobLogs(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -926,6 +934,7 @@ func RerunWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "run_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -1001,6 +1010,7 @@ func RerunFailedJobs(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "run_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -1076,6 +1086,7 @@ func CancelWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerToo
Required: []string{"owner", "repo", "run_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -1153,6 +1164,7 @@ func ListWorkflowRunArtifacts(t translations.TranslationHelperFunc) inventory.Se
Required: []string{"owner", "repo", "run_id"},
}),
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -1233,6 +1245,7 @@ func DownloadWorkflowRunArtifact(t translations.TranslationHelperFunc) inventory
Required: []string{"owner", "repo", "artifact_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -1311,6 +1324,7 @@ func DeleteWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.Serve
Required: []string{"owner", "repo", "run_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -1386,6 +1400,7 @@ func GetWorkflowRunUsage(t translations.TranslationHelperFunc) inventory.ServerT
Required: []string{"owner", "repo", "run_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -1550,6 +1565,7 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an
Required: []string{"method", "owner", "repo"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down Expand Up @@ -1668,6 +1684,7 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an
Required: []string{"method", "owner", "repo", "resource_id"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down Expand Up @@ -1781,6 +1798,7 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo
Required: []string{"method", "owner", "repo"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down Expand Up @@ -1895,6 +1913,7 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i
Required: []string{"owner", "repo"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions pkg/github/code_scanning.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/go-github/v79/github"
Expand Down Expand Up @@ -44,6 +45,7 @@ func GetCodeScanningAlert(t translations.TranslationHelperFunc) inventory.Server
Required: []string{"owner", "repo", "alertNumber"},
},
},
[]scopes.Scope{scopes.SecurityEvents},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down Expand Up @@ -135,6 +137,7 @@ func ListCodeScanningAlerts(t translations.TranslationHelperFunc) inventory.Serv
Required: []string{"owner", "repo"},
},
},
[]scopes.Scope{scopes.SecurityEvents},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/jsonschema-go/jsonschema"
Expand Down Expand Up @@ -51,6 +52,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
// OpenAI strict mode requires the properties field to be present.
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
},
nil,
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
client, err := deps.GetClient(ctx)
if err != nil {
Expand Down Expand Up @@ -129,6 +131,7 @@ func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
},
},
},
[]scopes.Scope{scopes.ReadOrg},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
user, err := OptionalParam[string](args, "user")
if err != nil {
Expand Down Expand Up @@ -231,6 +234,7 @@ func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"org", "team_slug"},
},
},
[]scopes.Scope{scopes.ReadOrg},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
org, err := RequiredParam[string](args, "org")
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions pkg/github/dependabot.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/go-github/v79/github"
Expand Down Expand Up @@ -45,6 +46,7 @@ func GetDependabotAlert(t translations.TranslationHelperFunc) inventory.ServerTo
Required: []string{"owner", "repo", "alertNumber"},
},
},
[]scopes.Scope{scopes.SecurityEvents},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down Expand Up @@ -128,6 +130,7 @@ func ListDependabotAlerts(t translations.TranslationHelperFunc) inventory.Server
Required: []string{"owner", "repo"},
},
},
[]scopes.Scope{scopes.SecurityEvents},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
Expand Down
32 changes: 28 additions & 4 deletions pkg/github/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/lockdown"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v79/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
Expand Down Expand Up @@ -148,21 +149,44 @@ func (d BaseDeps) GetContentWindowSize() int { return d.ContentWindowSize }
//
// The handler function receives deps extracted from context via MustDepsFromContext.
// Ensure ContextWithDeps is called to inject deps before any tool handlers are invoked.
func NewTool[In, Out any](toolset inventory.ToolsetMetadata, tool mcp.Tool, handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error)) inventory.ServerTool {
return inventory.NewServerToolWithContextHandler(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error) {
//
// requiredScopes specifies the minimum OAuth scopes needed for this tool.
// AcceptedScopes are automatically derived using the scope hierarchy (e.g., if
// public_repo is required, repo is also accepted since repo grants public_repo).
func NewTool[In, Out any](
toolset inventory.ToolsetMetadata,
tool mcp.Tool,
requiredScopes []scopes.Scope,
handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error),
) inventory.ServerTool {
st := inventory.NewServerToolWithContextHandler(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error) {
deps := MustDepsFromContext(ctx)
return handler(ctx, deps, req, args)
})
st.RequiredScopes = scopes.ToStringSlice(requiredScopes...)
st.AcceptedScopes = scopes.ExpandScopes(requiredScopes...)
return st
}

// NewToolFromHandler creates a ServerTool that retrieves ToolDependencies from context at call time.
// Use this when you have a handler that conforms to mcp.ToolHandler directly.
//
// The handler function receives deps extracted from context via MustDepsFromContext.
// Ensure ContextWithDeps is called to inject deps before any tool handlers are invoked.
func NewToolFromHandler(toolset inventory.ToolsetMetadata, tool mcp.Tool, handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest) (*mcp.CallToolResult, error)) inventory.ServerTool {
return inventory.NewServerToolWithRawContextHandler(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
//
// requiredScopes specifies the minimum OAuth scopes needed for this tool.
// AcceptedScopes are automatically derived using the scope hierarchy.
func NewToolFromHandler(
toolset inventory.ToolsetMetadata,
tool mcp.Tool,
requiredScopes []scopes.Scope,
handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest) (*mcp.CallToolResult, error),
) inventory.ServerTool {
st := inventory.NewServerToolWithRawContextHandler(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
deps := MustDepsFromContext(ctx)
return handler(ctx, deps, req)
})
st.RequiredScopes = scopes.ToStringSlice(requiredScopes...)
st.AcceptedScopes = scopes.ExpandScopes(requiredScopes...)
return st
}
Loading