Files
github--github-mcp-server/pkg/github/ifc_labels.go
T
2026-07-13 12:37:57 +08:00

172 lines
5.9 KiB
Go

package github
import (
"context"
"github.com/github/github-mcp-server/pkg/ifc"
"github.com/google/go-github/v89/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// setIFCLabel writes the given IFC security label into a tool result's _meta
// under the "ifc" key, allocating the Meta map if necessary.
func setIFCLabel(r *mcp.CallToolResult, label ifc.SecurityLabel) {
if r.Meta == nil {
r.Meta = mcp.Meta{}
}
r.Meta["ifc"] = label
}
func shouldAttachIFCLabel(ctx context.Context, deps ToolDependencies, r *mcp.CallToolResult) bool {
return r != nil && !r.IsError && deps.IsFeatureEnabled(ctx, FeatureFlagIFCLabels)
}
// attachStaticIFCLabel attaches a fixed IFC label to a successful tool result
// when IFC labels are enabled. It is used by tools whose label does not depend
// on any repository visibility lookup (e.g. security alerts, global
// advisories, team membership, notification subjects).
//
// Error results are left untouched, and the label is omitted entirely when the
// IFC feature flag is disabled.
func attachStaticIFCLabel(ctx context.Context, deps ToolDependencies, r *mcp.CallToolResult, label ifc.SecurityLabel) *mcp.CallToolResult {
if !shouldAttachIFCLabel(ctx, deps, r) {
return r
}
setIFCLabel(r, label)
return r
}
// attachRepoVisibilityIFCLabel attaches an IFC label derived from a single
// repository's visibility to a successful tool result when IFC labels are
// enabled. The concrete label is produced by labelFn, which receives whether
// the repository is private.
//
// The repository visibility is resolved via FetchRepoIsPrivate. Consistent
// with the other IFC-labeled tools, if the visibility lookup fails the label
// is omitted rather than risking a misclassification. Error results and the
// disabled-feature case are left untouched.
func attachRepoVisibilityIFCLabel(
ctx context.Context,
deps ToolDependencies,
client *github.Client,
owner, repo string,
r *mcp.CallToolResult,
labelFn func(isPrivate bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
if !shouldAttachIFCLabel(ctx, deps, r) {
return r
}
isPrivate, err := FetchRepoIsPrivate(ctx, client, owner, repo)
if err != nil {
return r
}
setIFCLabel(r, labelFn(isPrivate))
return r
}
// ifcSearchPostProcessOption returns a searchOption that attaches IFC labels to
// a multi-repository search result. The feature-flag check is centralized here
// (mirroring the attach* helpers above) rather than in each search tool
// handler: when IFC labels are disabled it returns a no-op option, so callers
// can pass it unconditionally to searchHandler.
func ifcSearchPostProcessOption(ctx context.Context, deps ToolDependencies) searchOption {
if !deps.IsFeatureEnabled(ctx, FeatureFlagIFCLabels) {
return func(*searchConfig) {}
}
return withSearchPostProcess(searchIssuesIFCPostProcess(deps))
}
// attachRepoVisibilityIFCLabelLazy is like attachRepoVisibilityIFCLabel but
// resolves the REST client itself, only when IFC labels are enabled. It is used
// by tools whose handler holds a GraphQL client (or no client yet) and would
// otherwise have to acquire a REST client solely to compute the label. The
// feature-flag check is centralized here so callers can invoke it
// unconditionally; if the client cannot be obtained or the visibility lookup
// fails, the label is omitted rather than risking a misclassification.
func attachRepoVisibilityIFCLabelLazy(
ctx context.Context,
deps ToolDependencies,
owner, repo string,
r *mcp.CallToolResult,
labelFn func(isPrivate bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
if !shouldAttachIFCLabel(ctx, deps, r) {
return r
}
client, err := deps.GetClient(ctx)
if err != nil {
return r
}
return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, labelFn)
}
// attachJoinedIFCLabel attaches an IFC label computed by joining a set of
// per-item visibilities (true == private) when IFC labels are enabled. joinFn
// is the lattice join for the relevant item kind (e.g. ifc.LabelSearchIssues or
// ifc.LabelProjectList). The visibility slice is cheap to build from an
// already-fetched response, so callers may construct it unconditionally and let
// this helper own the feature-flag gate.
func attachJoinedIFCLabel(
ctx context.Context,
deps ToolDependencies,
r *mcp.CallToolResult,
visibilities []bool,
joinFn func([]bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
if !shouldAttachIFCLabel(ctx, deps, r) {
return r
}
setIFCLabel(r, joinFn(visibilities))
return r
}
func attachProjectVisibilityIFCLabel(
ctx context.Context,
deps ToolDependencies,
r *mcp.CallToolResult,
isPrivate bool,
labelFn func(isPrivate bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
if !shouldAttachIFCLabel(ctx, deps, r) {
return r
}
setIFCLabel(r, labelFn(isPrivate))
return r
}
// newRepoVisibilityIFCLabeler returns a closure that attaches a repo-visibility
// IFC label to a tool result, for handlers that have several return paths and
// want to label each one. The returned function owns the feature-flag gate (so
// callers invoke it unconditionally) and caches the repository visibility
// lookup across calls, so a handler that returns from many branches only pays
// for one FetchRepoIsPrivate call. A failed visibility lookup is not cached, so
// a later return path can retry; on persistent failure the label is omitted
// rather than risking a misclassification.
func newRepoVisibilityIFCLabeler(
ctx context.Context,
deps ToolDependencies,
client *github.Client,
owner, repo string,
labelFn func(isPrivate bool) ifc.SecurityLabel,
) func(*mcp.CallToolResult) *mcp.CallToolResult {
var (
known bool
isPrivate bool
)
return func(r *mcp.CallToolResult) *mcp.CallToolResult {
if r == nil || r.IsError || !deps.IsFeatureEnabled(ctx, FeatureFlagIFCLabels) {
return r
}
if !known {
p, err := FetchRepoIsPrivate(ctx, client, owner, repo)
if err != nil {
return r
}
isPrivate = p
known = true
}
setIFCLabel(r, labelFn(isPrivate))
return r
}
}