345584c9ca
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
- Renamed deprecated NewServerTool[In, Out] to NewServerToolWithDeps[In, Out] - Updated dynamic_tools.go to use NewServerToolWithDeps (for special case with DynamicToolDependencies) - Renamed NewServerToolWithRawContextHandler to NewServerTool - Updated all call sites in dependencies.go and registry_test.go - All tests pass, linter passes Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
217 lines
7.8 KiB
Go
217 lines
7.8 KiB
Go
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 use NewServerToolWithDeps which creates closures.
|
|
func NewDynamicTool(toolset inventory.ToolsetMetadata, tool mcp.Tool, handler func(deps DynamicToolDependencies) mcp.ToolHandlerFor[map[string]any, any]) inventory.ServerTool {
|
|
return inventory.NewServerToolWithDeps(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
|
|
}
|
|
},
|
|
)
|
|
}
|