package github
import (
"context"
"encoding/json"
"fmt"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// DynamicToolDependencies contains dependencies for dynamic toolset management tools.
// It includes the managed Inventory, the server for registration, and the deps
// that will be passed to tools when they are dynamically enabled.
type DynamicToolDependencies struct {
// Server is the MCP server to register tools with
Server *mcp.Server
// Inventory contains all available tools, resources and prompts that can be enabled dynamically
Inventory *inventory.Inventory
// ToolDeps are the dependencies passed to tools when they are registered
ToolDeps any
// T is the translation helper function
T translations.TranslationHelperFunc
}
// NewDynamicTool creates a ServerTool with fully-typed DynamicToolDependencies.
// Dynamic tools use a different dependency structure (DynamicToolDependencies) than regular
// tools (ToolDependencies), so they intentionally use the closure pattern.
func NewDynamicTool(toolset inventory.ToolsetMetadata, tool mcp.Tool, handler func(deps DynamicToolDependencies) mcp.ToolHandlerFor[map[string]any, any]) inventory.ServerTool {
//nolint:staticcheck // SA1019: Dynamic tools use a different deps structure, closure pattern is intentional
return inventory.NewServerTool(tool, toolset, func(d any) mcp.ToolHandlerFor[map[string]any, any] {
return handler(d.(DynamicToolDependencies))
})
}
// toolsetIDsEnum returns the list of toolset IDs as an enum for JSON Schema.
func toolsetIDsEnum(r *inventory.Inventory) []any {
toolsetIDs := r.ToolsetIDs()
result := make([]any, len(toolsetIDs))
for i, id := range toolsetIDs {
result[i] = id
}
return result
}
// DynamicTools returns the tools for dynamic toolset management.
// These tools allow runtime discovery and enablement of inventory.
// The r parameter provides the available toolset IDs for JSON Schema enums.
func DynamicTools(r *inventory.Inventory) []inventory.ServerTool {
return []inventory.ServerTool{
ListAvailableToolsets(),
GetToolsetsTools(r),
EnableToolset(r),
}
}
// EnableToolset creates a tool that enables a toolset at runtime.
func EnableToolset(r *inventory.Inventory) inventory.ServerTool {
return NewDynamicTool(
ToolsetMetadataDynamic,
mcp.Tool{
Name: "enable_toolset",
Description: "Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable",
Annotations: &mcp.ToolAnnotations{
Title: "Enable a toolset",
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"toolset": {
Type: "string",
Description: "The name of the toolset to enable",
Enum: toolsetIDsEnum(r),
},
},
Required: []string{"toolset"},
},
},
func(deps DynamicToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
return func(_ context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
toolsetName, err := RequiredParam[string](args, "toolset")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
toolsetID := inventory.ToolsetID(toolsetName)
if !deps.Inventory.HasToolset(toolsetID) {
return utils.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil, nil
}
if deps.Inventory.IsToolsetEnabled(toolsetID) {
return utils.NewToolResultText(fmt.Sprintf("Toolset %s is already enabled", toolsetName)), nil, nil
}
// Mark the toolset as enabled so IsToolsetEnabled returns true
deps.Inventory.EnableToolset(toolsetID)
// Get tools for this toolset and register them with the managed deps
toolsForToolset := deps.Inventory.ToolsForToolset(toolsetID)
for _, st := range toolsForToolset {
st.RegisterFunc(deps.Server, deps.ToolDeps)
}
return utils.NewToolResultText(fmt.Sprintf("Toolset %s enabled with %d tools", toolsetName, len(toolsForToolset))), nil, nil
}
},
)
}
// ListAvailableToolsets creates a tool that lists all available inventory.
func ListAvailableToolsets() inventory.ServerTool {
return NewDynamicTool(
ToolsetMetadataDynamic,
mcp.Tool{
Name: "list_available_toolsets",
Description: "List all available toolsets this GitHub MCP server can offer, providing the enabled status of each. Use this when a task could be achieved with a GitHub tool and the currently available tools aren't enough. Call get_toolset_tools with these toolset names to discover specific tools you can call",
Annotations: &mcp.ToolAnnotations{
Title: "List available toolsets",
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{},
},
},
func(deps DynamicToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
return func(_ context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
toolsetIDs := deps.Inventory.ToolsetIDs()
descriptions := deps.Inventory.ToolsetDescriptions()
payload := make([]map[string]string, 0, len(toolsetIDs))
for _, id := range toolsetIDs {
t := map[string]string{
"name": string(id),
"description": descriptions[id],
"can_enable": "true",
"currently_enabled": fmt.Sprintf("%t", deps.Inventory.IsToolsetEnabled(id)),
}
payload = append(payload, t)
}
r, err := json.Marshal(payload)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal features: %w", err)
}
return utils.NewToolResultText(string(r)), nil, nil
}
},
)
}
// GetToolsetsTools creates a tool that lists all tools in a specific toolset.
func GetToolsetsTools(r *inventory.Inventory) inventory.ServerTool {
return NewDynamicTool(
ToolsetMetadataDynamic,
mcp.Tool{
Name: "get_toolset_tools",
Description: "Lists all the capabilities that are enabled with the specified toolset, use this to get clarity on whether enabling a toolset would help you to complete a task",
Annotations: &mcp.ToolAnnotations{
Title: "List all tools in a toolset",
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"toolset": {
Type: "string",
Description: "The name of the toolset you want to get the tools for",
Enum: toolsetIDsEnum(r),
},
},
Required: []string{"toolset"},
},
},
func(deps DynamicToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
return func(_ context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
toolsetName, err := RequiredParam[string](args, "toolset")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
toolsetID := inventory.ToolsetID(toolsetName)
if !deps.Inventory.HasToolset(toolsetID) {
return utils.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil, nil
}
// Get all tools for this toolset (ignoring current filters for discovery)
toolsInToolset := deps.Inventory.ToolsForToolset(toolsetID)
payload := make([]map[string]string, 0, len(toolsInToolset))
for _, st := range toolsInToolset {
tool := map[string]string{
"name": st.Tool.Name,
"description": st.Tool.Description,
"can_enable": "true",
"toolset": toolsetName,
}
payload = append(payload, tool)
}
r, err := json.Marshal(payload)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal features: %w", err)
}
return utils.NewToolResultText(string(r)), nil, nil
}
},
)
}