Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 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 @@ -11,7 +11,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 @@ -190,7 +189,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 @@ -224,16 +223,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 @@ -282,6 +291,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
37 changes: 37 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,8 @@ func ListWorkflows(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo"},
}),
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +203,8 @@ func ListWorkflowRuns(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "workflow_id"},
}),
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +316,8 @@ func RunWorkflow(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo", "workflow_id", "ref"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +422,8 @@ func GetWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo", "run_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +492,8 @@ func GetWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.ServerTo
Required: []string{"owner", "repo", "run_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +577,8 @@ func ListWorkflowJobs(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "run_id"},
}),
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +691,8 @@ func GetJobLogs(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"owner", "repo"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +941,8 @@ func RerunWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "run_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1018,8 @@ func RerunFailedJobs(t translations.TranslationHelperFunc) inventory.ServerTool
Required: []string{"owner", "repo", "run_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1095,8 @@ func CancelWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerToo
Required: []string{"owner", "repo", "run_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1174,8 @@ func ListWorkflowRunArtifacts(t translations.TranslationHelperFunc) inventory.Se
Required: []string{"owner", "repo", "run_id"},
}),
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1256,8 @@ func DownloadWorkflowRunArtifact(t translations.TranslationHelperFunc) inventory
Required: []string{"owner", "repo", "artifact_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1336,8 @@ func DeleteWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.Serve
Required: []string{"owner", "repo", "run_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1413,8 @@ func GetWorkflowRunUsage(t translations.TranslationHelperFunc) inventory.ServerT
Required: []string{"owner", "repo", "run_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1579,8 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an
Required: []string{"method", "owner", "repo"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1699,8 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an
Required: []string{"method", "owner", "repo", "resource_id"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1814,8 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo
Required: []string{"method", "owner", "repo"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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 +1930,8 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i
Required: []string{"owner", "repo"},
},
},
scopes.ToStringSlice(scopes.Repo),
scopes.ToStringSlice(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
5 changes: 5 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,8 @@ func GetCodeScanningAlert(t translations.TranslationHelperFunc) inventory.Server
Required: []string{"owner", "repo", "alertNumber"},
},
},
scopes.ToStringSlice(scopes.SecurityEvents),
scopes.ToStringSlice(scopes.SecurityEvents, 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 @@ -135,6 +138,8 @@ func ListCodeScanningAlerts(t translations.TranslationHelperFunc) inventory.Serv
Required: []string{"owner", "repo"},
},
},
scopes.ToStringSlice(scopes.SecurityEvents),
scopes.ToStringSlice(scopes.SecurityEvents, 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
7 changes: 7 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,8 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
// OpenAI strict mode requires the properties field to be present.
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
},
nil, // no required scopes
nil, // no accepted scopes
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 +132,8 @@ func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
},
},
},
scopes.ToStringSlice(scopes.ReadOrg),
scopes.ToStringSlice(scopes.ReadOrg, scopes.WriteOrg, scopes.AdminOrg),
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 +236,8 @@ func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
Required: []string{"org", "team_slug"},
},
},
scopes.ToStringSlice(scopes.ReadOrg),
scopes.ToStringSlice(scopes.ReadOrg, scopes.WriteOrg, scopes.AdminOrg),
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
5 changes: 5 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,8 @@ func GetDependabotAlert(t translations.TranslationHelperFunc) inventory.ServerTo
Required: []string{"owner", "repo", "alertNumber"},
},
},
scopes.ToStringSlice(scopes.SecurityEvents),
scopes.ToStringSlice(scopes.SecurityEvents, 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 @@ -128,6 +131,8 @@ func ListDependabotAlerts(t translations.TranslationHelperFunc) inventory.Server
Required: []string{"owner", "repo"},
},
},
scopes.ToStringSlice(scopes.SecurityEvents),
scopes.ToStringSlice(scopes.SecurityEvents, 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
30 changes: 26 additions & 4 deletions pkg/github/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,21 +148,43 @@ 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) {
//
// All tools must explicitly specify their OAuth scope requirements, even if empty (nil).
func NewTool[In, Out any](
toolset inventory.ToolsetMetadata,
tool mcp.Tool,
requiredScopes []string,
acceptedScopes []string,
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 = requiredScopes
st.AcceptedScopes = acceptedScopes
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) {
//
// All tools must explicitly specify their OAuth scope requirements, even if empty (nil).
func NewToolFromHandler(
toolset inventory.ToolsetMetadata,
tool mcp.Tool,
requiredScopes []string,
acceptedScopes []string,
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 = requiredScopes
st.AcceptedScopes = acceptedScopes
return st
}
Loading