| layout | default |
|---|---|
| title | Chapter 4: Toolsets, Tools, and Dynamic Discovery |
| nav_order | 4 |
| parent | GitHub MCP Server Tutorial |
Welcome to Chapter 4: Toolsets, Tools, and Dynamic Discovery. In this part of GitHub MCP Server Tutorial: Production GitHub Operations Through MCP, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.
This chapter explains how to precisely shape the server capability surface for better reliability and safety.
- constrain tools using toolsets and explicit tool allow-lists
- use dynamic discovery to limit initial tool overload
- combine read-only mode with selective tool exposure
- prevent accidental write operations in exploratory sessions
| Control | Local | Remote |
|---|---|---|
| toolsets | --toolsets / GITHUB_TOOLSETS |
URL + X-MCP-Toolsets |
| individual tools | --tools / GITHUB_TOOLS |
header-based filtering |
| read-only | --read-only / GITHUB_READ_ONLY |
/readonly or X-MCP-Readonly |
| dynamic discovery | --dynamic-toolsets |
not available |
| lockdown mode | --lockdown-mode |
X-MCP-Lockdown |
You now know how to expose just enough capability for each task context.
Next: Chapter 5: Host Integration Patterns
The GetDiscussionComments function in pkg/github/discussions.go handles a key part of this chapter's functionality:
// The go-github library's Discussion type lacks isAnswered and answerChosenAt fields,
// so we use map[string]interface{} for the response (consistent with other functions
// like ListDiscussions and GetDiscussionComments).
response := map[string]any{
"number": int(d.Number),
"title": string(d.Title),
"body": string(d.Body),
"url": string(d.URL),
"closed": bool(d.Closed),
"isAnswered": bool(d.IsAnswered),
"createdAt": d.CreatedAt.Time,
"category": map[string]any{
"name": string(d.Category.Name),
},
}
// Add optional timestamp fields if present
if d.AnswerChosenAt != nil {
response["answerChosenAt"] = d.AnswerChosenAt.Time
}
out, err := json.Marshal(response)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal discussion: %w", err)
}
return utils.NewToolResultText(string(out)), nil, nil
},
)
}
func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.ServerTool {This function is important because it defines how GitHub MCP Server Tutorial: Production GitHub Operations Through MCP implements the patterns covered in this chapter.
The ListDiscussionCategories function in pkg/github/discussions.go handles a key part of this chapter's functionality:
}
func ListDiscussionCategories(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataDiscussions,
mcp.Tool{
Name: "list_discussion_categories",
Description: t("TOOL_LIST_DISCUSSION_CATEGORIES_DESCRIPTION", "List discussion categories with their id and name, for a repository or organisation."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_LIST_DISCUSSION_CATEGORIES_USER_TITLE", "List discussion categories"),
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner",
},
"repo": {
Type: "string",
Description: "Repository name. If not provided, discussion categories will be queried at the organisation level.",
},
},
Required: []string{"owner"},
},
},
[]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 {
return utils.NewToolResultError(err.Error()), nil, nilThis function is important because it defines how GitHub MCP Server Tutorial: Production GitHub Operations Through MCP implements the patterns covered in this chapter.
The for interface in pkg/github/discussions.go handles a key part of this chapter's functionality:
const DefaultGraphQLPageSize = 30
// Common interface for all discussion query types
type DiscussionQueryResult interface {
GetDiscussionFragment() DiscussionFragment
}
// Implement the interface for all query types
func (q *BasicNoOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
func (q *BasicWithOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
func (q *WithCategoryAndOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
func (q *WithCategoryNoOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
type DiscussionFragment struct {
Nodes []NodeFragment
PageInfo PageInfoFragment
TotalCount githubv4.Int
}
type NodeFragment struct {
Number githubv4.IntThis interface is important because it defines how GitHub MCP Server Tutorial: Production GitHub Operations Through MCP implements the patterns covered in this chapter.
The for interface in pkg/github/discussions.go handles a key part of this chapter's functionality:
const DefaultGraphQLPageSize = 30
// Common interface for all discussion query types
type DiscussionQueryResult interface {
GetDiscussionFragment() DiscussionFragment
}
// Implement the interface for all query types
func (q *BasicNoOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
func (q *BasicWithOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
func (q *WithCategoryAndOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
func (q *WithCategoryNoOrder) GetDiscussionFragment() DiscussionFragment {
return q.Repository.Discussions
}
type DiscussionFragment struct {
Nodes []NodeFragment
PageInfo PageInfoFragment
TotalCount githubv4.Int
}
type NodeFragment struct {
Number githubv4.IntThis interface is important because it defines how GitHub MCP Server Tutorial: Production GitHub Operations Through MCP implements the patterns covered in this chapter.
flowchart TD
A[GetDiscussionComments]
B[ListDiscussionCategories]
C[for]
D[for]
E[var]
A --> B
B --> C
C --> D
D --> E