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
10 changes: 10 additions & 0 deletions internal/toolsnaps/toolsnaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ func Test(toolName string, tool any) error {
if err != nil {
return fmt.Errorf("failed to marshal tool %s: %w", toolName, err)
}
toolJSON, err = sortJSONKeys(toolJSON)
if err != nil {
return fmt.Errorf("failed to normalize tool %s: %w", toolName, err)
}

snapPath := fmt.Sprintf("__toolsnaps__/%s.snap", toolName)

Expand Down Expand Up @@ -97,5 +101,11 @@ func sortJSONKeys(jsonData []byte) ([]byte, error) {
return nil, err
}

// outputSchema is feature-gated at registration time, so default tool
// snapshots continue to represent the non-opt-in tool surface.
if object, ok := data.(map[string]any); ok {
delete(object, "outputSchema")
}

return json.MarshalIndent(data, "", " ")
}
31 changes: 25 additions & 6 deletions pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
},
}

result := MarshalledTextResult(minimalUser)
result, err := structuredTextResult(ctx, deps, minimalUser, minimalUser)
if err != nil {
return nil, nil, err
}
if deps.GetFlags(ctx).InsidersMode {
if result.Meta == nil {
result.Meta = mcp.Meta{}
Expand All @@ -113,7 +116,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
}
return result, nil, nil
},
)
).WithOutputSchema(MustOutputSchema[MinimalUser]())
}

type TeamInfo struct {
Expand All @@ -127,6 +130,14 @@ type OrganizationTeams struct {
Teams []TeamInfo `json:"teams"`
}

type TeamsResponse struct {
Teams []OrganizationTeams `json:"teams"`
}

type TeamMembersResponse struct {
Members []string `json:"members"`
}

func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataContext,
Expand Down Expand Up @@ -220,9 +231,13 @@ func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
organizations = append(organizations, orgTeams)
}

return MarshalledTextResult(organizations), nil, nil
result, err := structuredTextResult(ctx, deps, organizations, TeamsResponse{Teams: organizations})
if err != nil {
return nil, nil, err
}
return result, nil, nil
},
)
).WithOutputSchema(MustOutputSchema[TeamsResponse]())
}

func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
Expand Down Expand Up @@ -291,7 +306,11 @@ func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
members = append(members, string(member.Login))
}

return MarshalledTextResult(members), nil, nil
result, err := structuredTextResult(ctx, deps, members, TeamMembersResponse{Members: members})
if err != nil {
return nil, nil, err
}
return result, nil, nil
},
)
).WithOutputSchema(MustOutputSchema[TeamMembersResponse]())
}
4 changes: 4 additions & 0 deletions pkg/github/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package github
// MCPAppsFeatureFlag is the feature flag name for MCP Apps (interactive UI forms).
const MCPAppsFeatureFlag = "remote_mcp_ui_apps"

// FeatureFlagOutputSchemas is the feature flag name for MCP tool output schemas.
const FeatureFlagOutputSchemas = "output_schemas"

// AllowedFeatureFlags is the allowlist of feature flags that can be enabled
// by users via --features CLI flag or X-MCP-Features HTTP header.
// Only flags in this list are accepted; unknown flags are silently ignored.
Expand All @@ -11,6 +14,7 @@ var AllowedFeatureFlags = []string{
MCPAppsFeatureFlag,
FeatureFlagIssuesGranular,
FeatureFlagPullRequestsGranular,
FeatureFlagOutputSchemas,
}

// InsidersFeatureFlags is the list of feature flags that insiders mode enables.
Expand Down
24 changes: 17 additions & 7 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,13 +593,20 @@ func ListIssueTypes(t translations.TranslationHelperFunc) inventory.ServerTool {
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list issue types", resp, body), nil, nil
}

r, err := json.Marshal(issueTypes)
minimalIssueTypes := make([]MinimalIssueType, 0, len(issueTypes))
for _, issueType := range issueTypes {
if issueType != nil {
minimalIssueTypes = append(minimalIssueTypes, convertToMinimalIssueType(issueType))
}
}

result, err := structuredTextResult(ctx, deps, issueTypes, MinimalIssueTypesResponse{IssueTypes: minimalIssueTypes})
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal issue types", err), nil, nil
}

return utils.NewToolResultText(string(r)), nil, nil
})
return result, nil, nil
}).WithOutputSchema(MustOutputSchema[MinimalIssueTypesResponse]())
}

// AddIssueComment creates a tool to add a comment to an issue.
Expand Down Expand Up @@ -978,9 +985,9 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
result, err := searchHandler(ctx, deps.GetClient, args, "issue", "failed to search issues")
result, err := searchHandler(ctx, deps, args, "issue", "failed to search issues")
return result, nil, err
})
}).WithOutputSchema(MustOutputSchema[MinimalSearchIssuesResult]())
}

// IssueWrite creates a tool to create a new or update an existing issue in a GitHub repository.
Expand Down Expand Up @@ -1590,7 +1597,10 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
isPrivate = queryResult.GetIsPrivate()
}

result := MarshalledTextResult(resp)
result, err := structuredTextResult(ctx, deps, resp, resp)
if err != nil {
return nil, nil, err
}
if deps.GetFlags(ctx).InsidersMode {
if result.Meta == nil {
result.Meta = mcp.Meta{}
Expand All @@ -1613,7 +1623,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
result.Meta["ifc"] = ifc.LabelListIssues(isPrivate, readers)
}
return result, nil, nil
})
}).WithOutputSchema(MustOutputSchema[MinimalIssuesResponse]())
}

// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object.
Expand Down
16 changes: 8 additions & 8 deletions pkg/github/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,21 @@ func GetLabel(t translations.TranslationHelperFunc) inventory.ServerTool {
return utils.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil, nil
}

label := map[string]any{
"id": fmt.Sprintf("%v", query.Repository.Label.ID),
"name": string(query.Repository.Label.Name),
"color": string(query.Repository.Label.Color),
"description": string(query.Repository.Label.Description),
label := MinimalLabel{
ID: fmt.Sprintf("%v", query.Repository.Label.ID),
Name: string(query.Repository.Label.Name),
Color: string(query.Repository.Label.Color),
Description: string(query.Repository.Label.Description),
}

out, err := json.Marshal(label)
result, err := structuredTextResult(ctx, deps, label, label)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal label: %w", err)
}

return utils.NewToolResultText(string(out)), nil, nil
return result, nil, nil
},
)
).WithOutputSchema(MustOutputSchema[MinimalLabel]())
}

// GetLabelForLabelsToolset returns the same GetLabel tool but registered in the labels toolset.
Expand Down
Loading
Loading