Skip to content
Open
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
2 changes: 1 addition & 1 deletion internal/ghmcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func TestStdioGraphQLUserAgent(t *testing.T) {
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers <- r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"repository":{"isPrivate":false,"issues":{"nodes":[],"totalCount":0,"pageInfo":{"hasNextPage":false,"endCursor":""}}}}}`))
_, _ = w.Write([]byte(`{"data":{"repository":{"issues":{"nodes":[],"totalCount":0,"pageInfo":{"hasNextPage":false,"endCursor":""}}}}}`))
}))
defer api.Close()

Expand Down
61 changes: 12 additions & 49 deletions pkg/github/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/github/github-mcp-server/v2/internal/profiler"
buffer "github.com/github/github-mcp-server/v2/pkg/buffer"
ghErrors "github.com/github/github-mcp-server/v2/pkg/errors"
"github.com/github/github-mcp-server/v2/pkg/ifc"
"github.com/github/github-mcp-server/v2/pkg/inventory"
"github.com/github/github-mcp-server/v2/pkg/scopes"
"github.com/github/github-mcp-server/v2/pkg/translations"
Expand Down Expand Up @@ -324,14 +323,6 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

// attachIFC adds the IFC label to a successful Actions result when
// IFC labels are enabled. Workflow definitions, runs, jobs,
// artifacts and logs echo attacker-influenceable run output, so
// integrity is untrusted; confidentiality follows repo visibility.
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, ifc.LabelActionsResult)
}

var resourceIDInt int64
var parseErr error
switch method {
Expand All @@ -354,17 +345,13 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an

switch method {
case actionsMethodListWorkflows:
result, payload, err := listWorkflows(ctx, client, owner, repo, pagination)
return attachIFC(result), payload, err
return listWorkflows(ctx, client, owner, repo, pagination)
case actionsMethodListWorkflowRuns:
result, payload, err := listWorkflowRuns(ctx, client, args.WorkflowRunsFilter, owner, repo, resourceID, pagination)
return attachIFC(result), payload, err
return listWorkflowRuns(ctx, client, args.WorkflowRunsFilter, owner, repo, resourceID, pagination)
case actionsMethodListWorkflowJobs:
result, payload, err := listWorkflowJobs(ctx, client, args.WorkflowJobsFilter, owner, repo, resourceIDInt, pagination)
return attachIFC(result), payload, err
return listWorkflowJobs(ctx, client, args.WorkflowJobsFilter, owner, repo, resourceIDInt, pagination)
case actionsMethodListWorkflowArtifacts:
result, payload, err := listWorkflowArtifacts(ctx, client, owner, repo, resourceIDInt, pagination)
return attachIFC(result), payload, err
return listWorkflowArtifacts(ctx, client, owner, repo, resourceIDInt, pagination)
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
Expand Down Expand Up @@ -426,14 +413,6 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

// attachIFC adds the IFC label to a successful Actions result when
// IFC labels are enabled. Workflow runs, jobs, artifacts, usage,
// and log URLs reflect attacker-influenceable run output, so
// integrity is untrusted; confidentiality follows repo visibility.
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, ifc.LabelActionsResult)
}

var resourceIDInt int64
var parseErr error
switch method {
Expand All @@ -449,23 +428,17 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an

switch method {
case actionsMethodGetWorkflow:
result, payload, err := getWorkflow(ctx, client, owner, repo, resourceID)
return attachIFC(result), payload, err
return getWorkflow(ctx, client, owner, repo, resourceID)
case actionsMethodGetWorkflowRun:
result, payload, err := getWorkflowRun(ctx, client, owner, repo, resourceIDInt)
return attachIFC(result), payload, err
return getWorkflowRun(ctx, client, owner, repo, resourceIDInt)
case actionsMethodGetWorkflowJob:
result, payload, err := getWorkflowJob(ctx, client, owner, repo, resourceIDInt)
return attachIFC(result), payload, err
return getWorkflowJob(ctx, client, owner, repo, resourceIDInt)
case actionsMethodDownloadWorkflowArtifact:
result, payload, err := downloadWorkflowArtifact(ctx, client, owner, repo, resourceIDInt)
return attachIFC(result), payload, err
return downloadWorkflowArtifact(ctx, client, owner, repo, resourceIDInt)
case actionsMethodGetWorkflowRunUsage:
result, payload, err := getWorkflowRunUsage(ctx, client, owner, repo, resourceIDInt)
return attachIFC(result), payload, err
return getWorkflowRunUsage(ctx, client, owner, repo, resourceIDInt)
case actionsMethodGetWorkflowRunLogsURL:
result, payload, err := getWorkflowRunLogsURL(ctx, client, owner, repo, resourceIDInt)
return attachIFC(result), payload, err
return getWorkflowRunLogsURL(ctx, client, owner, repo, resourceIDInt)
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
Expand Down Expand Up @@ -636,22 +609,12 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i
return utils.NewToolResultError("job_id is required when failed_only is false"), nil, nil
}

// attachIFC adds the IFC label to a successful result when IFC
// labels are enabled. Job logs echo attacker-influenceable run
// output, so integrity is untrusted; confidentiality follows repo
// visibility.
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, ifc.LabelActionsResult)
}

if failedOnly && runID > 0 {
// Handle failed-only mode: get logs for all failed jobs in the workflow run
result, payload, err := handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize())
return attachIFC(result), payload, err
return handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize())
} else if jobID > 0 {
// Handle single job mode
result, payload, err := handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent, tailLines, deps.GetContentWindowSize())
return attachIFC(result), payload, err
return handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent, tailLines, deps.GetContentWindowSize())
}

return utils.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil, nil
Expand Down
15 changes: 2 additions & 13 deletions pkg/github/code_scanning.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"net/http"

ghErrors "github.com/github/github-mcp-server/v2/pkg/errors"
"github.com/github/github-mcp-server/v2/pkg/ifc"
"github.com/github/github-mcp-server/v2/pkg/inventory"
"github.com/github/github-mcp-server/v2/pkg/scopes"
"github.com/github/github-mcp-server/v2/pkg/translations"
Expand Down Expand Up @@ -86,12 +85,7 @@ func GetCodeScanningAlert(t translations.TranslationHelperFunc) inventory.Server
return utils.NewToolResultErrorFromErr("failed to marshal alert", err), nil, nil
}

result := utils.NewToolResultText(string(r))
// Code scanning alerts are access-restricted regardless of repo
// visibility and embed attacker-influenceable code snippets, so the
// label is always private-untrusted.
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelSecurityAlert())
return result, codeScanningAlertOutput(alert), nil
return utils.NewToolResultText(string(r)), codeScanningAlertOutput(alert), nil
},
normalizeSecurityIntegerArguments("alertNumber"),
)
Expand Down Expand Up @@ -193,12 +187,7 @@ func ListCodeScanningAlerts(t translations.TranslationHelperFunc) inventory.Serv
return utils.NewToolResultErrorFromErr("failed to marshal alerts", err), nil, nil
}

result := utils.NewToolResultText(string(r))
// Code scanning alerts are access-restricted regardless of repo
// visibility and embed attacker-influenceable code snippets, so the
// label is always private-untrusted.
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelSecurityAlert())
return result, mapSecurityOutputs(alerts, codeScanningAlertOutput), nil
return utils.NewToolResultText(string(r)), mapSecurityOutputs(alerts, codeScanningAlertOutput), nil
},
normalizeSecurityIntegerArguments("page", "perPage"),
normalizeTypedReadArguments(nil, false),
Expand Down
13 changes: 1 addition & 12 deletions pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"time"

ghErrors "github.com/github/github-mcp-server/v2/pkg/errors"
"github.com/github/github-mcp-server/v2/pkg/ifc"
"github.com/github/github-mcp-server/v2/pkg/inventory"
"github.com/github/github-mcp-server/v2/pkg/scopes"
"github.com/github/github-mcp-server/v2/pkg/translations"
Expand Down Expand Up @@ -178,9 +177,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
},
}

result := MarshalledTextResult(minimalUser)
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelGetMe())
return result, minimalUser, nil
return MarshalledTextResult(minimalUser), minimalUser, nil
},
)
}
Expand Down Expand Up @@ -283,10 +280,6 @@ func GetTeams(t translations.TranslationHelperFunc) inventory.ServerTool {
}

result := MarshalledTextResult(organizations)
// Team membership is maintained by GitHub and cannot be forged by
// outside contributors (trusted). Org team rosters are visible only
// to org members, so confidentiality is private.
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelTeam())
if organizations == nil {
organizations = []OrganizationTeams{}
}
Expand Down Expand Up @@ -355,10 +348,6 @@ func GetTeamMembers(t translations.TranslationHelperFunc) inventory.ServerTool {
}

result := MarshalledTextResult(members)
// Team membership is maintained by GitHub and cannot be forged by
// outside contributors (trusted). A team's member roster is visible
// only to org members, so confidentiality is private.
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelTeam())
if members == nil {
members = []string{}
}
Expand Down
66 changes: 0 additions & 66 deletions pkg/github/context_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,72 +460,6 @@ func TestContextToolsTypedRegistration(t *testing.T) {
assert.Nil(t, result.StructuredContent, "handler errors must not expose a successful typed output")
}

func Test_GetMe_IFC_FeatureFlag(t *testing.T) {
t.Parallel()

serverTool := GetMe(translations.NullTranslationHelper)

mockUser := &github.User{
Login: new("testuser"),
HTMLURL: new("https://github.com/testuser"),
CreatedAt: &github.Timestamp{Time: time.Now()},
}
mockedHTTPClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetUser: mockResponse(t, http.StatusOK, mockUser),
})

depsWithIFCFeature := func(enabled bool) *BaseDeps {
return NewBaseDeps(
mustNewGHClient(t, mockedHTTPClient), nil, nil, nil,
translations.NullTranslationHelper,
FeatureFlags{},
0,
func(_ context.Context, flagName string) (bool, error) {
return flagName == FeatureFlagIFCLabels && enabled, nil
},
stubExporters(),
)
}

t.Run("feature disabled omits ifc label from result meta", func(t *testing.T) {
deps := depsWithIFCFeature(false)
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

assert.Nil(t, result.Meta, "result meta should be nil when IFC labels are disabled")
})

t.Run("feature enabled includes ifc label in result meta", func(t *testing.T) {
deps := depsWithIFCFeature(true)
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta, "result meta should be set when IFC labels are enabled")
ifcLabel, ok := result.Meta["ifc"]
require.True(t, ok, "result meta should contain ifc key")

ifcJSON, err := json.Marshal(ifcLabel)
require.NoError(t, err)

var ifcMap map[string]any
err = json.Unmarshal(ifcJSON, &ifcMap)
require.NoError(t, err)

assert.Equal(t, "trusted", ifcMap["integrity"])
// get_me returns the caller's private repo/gist counts, which are not
// part of the public profile, so confidentiality is private.
assert.Equal(t, "private", ifcMap["confidentiality"])
})
}

func Test_GetTeams(t *testing.T) {
t.Parallel()

Expand Down
15 changes: 2 additions & 13 deletions pkg/github/dependabot.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/http"

ghErrors "github.com/github/github-mcp-server/v2/pkg/errors"
"github.com/github/github-mcp-server/v2/pkg/ifc"
"github.com/github/github-mcp-server/v2/pkg/inventory"
"github.com/github/github-mcp-server/v2/pkg/scopes"
"github.com/github/github-mcp-server/v2/pkg/translations"
Expand Down Expand Up @@ -87,12 +86,7 @@ func GetDependabotAlert(t translations.TranslationHelperFunc) inventory.ServerTo
return utils.NewToolResultErrorFromErr("failed to marshal alert", err), nil, err
}

result := utils.NewToolResultText(string(r))
// Dependabot alerts are access-restricted regardless of repo
// visibility and embed attacker-influenceable advisory text, so the
// label is always private-untrusted.
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelSecurityAlert())
return result, dependabotAlertOutput(alert), nil
return utils.NewToolResultText(string(r)), dependabotAlertOutput(alert), nil
},
normalizeSecurityIntegerArguments("alertNumber"),
)
Expand Down Expand Up @@ -198,12 +192,7 @@ func ListDependabotAlerts(t translations.TranslationHelperFunc) inventory.Server
return utils.NewToolResultErrorFromErr("failed to marshal alerts", err), DependabotAlertsOutput{}, err
}

result := utils.NewToolResultText(string(r))
// Dependabot alerts are access-restricted regardless of repo
// visibility and embed attacker-influenceable advisory text, so the
// label is always private-untrusted.
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelSecurityAlert())
return result, output, nil
return utils.NewToolResultText(string(r)), output, nil
},
normalizeTypedReadArguments(nil, false),
)
Expand Down
16 changes: 1 addition & 15 deletions pkg/github/discussions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"strings"

"github.com/github/github-mcp-server/v2/pkg/ifc"
"github.com/github/github-mcp-server/v2/pkg/inventory"
"github.com/github/github-mcp-server/v2/pkg/sanitize"
"github.com/github/github-mcp-server/v2/pkg/scopes"
Expand Down Expand Up @@ -279,9 +278,6 @@ func ListDiscussions(t translations.TranslationHelperFunc) inventory.ServerTool
return nil, nil, fmt.Errorf("failed to marshal discussions: %w", err)
}
result := utils.NewToolResultText(string(out))
// Discussion content is user-authored (untrusted); confidentiality
// follows repo visibility.
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, owner, repo, result, ifc.LabelRepoUserContent)
items := make([]*DiscussionListItemOutput, 0, len(discussions))
for _, discussion := range discussions {
items = append(items, &DiscussionListItemOutput{
Expand Down Expand Up @@ -395,9 +391,6 @@ func GetDiscussion(t translations.TranslationHelperFunc) inventory.ServerTool {
}

result := utils.NewToolResultText(string(out))
// Discussion content is user-authored (untrusted); confidentiality
// follows repo visibility.
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, params.Owner, params.Repo, result, ifc.LabelRepoUserContent)
output := &DiscussionOutput{
Number: int(d.Number), Title: sanitize.PlainText(string(d.Title)),
Body: sanitize.Content(string(d.Body)), HTMLURL: string(d.URL),
Expand Down Expand Up @@ -596,11 +589,7 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve
return nil, nil, fmt.Errorf("failed to marshal comments: %w", err)
}

result := utils.NewToolResultText(string(out))
// Discussion comments are user-authored (untrusted); confidentiality
// follows repo visibility.
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, params.Owner, params.Repo, result, ifc.LabelRepoUserContent)
return result, &DiscussionCommentsOutput{
return utils.NewToolResultText(string(out)), &DiscussionCommentsOutput{
Comments: discussionCommentOutputs(comments),
PageInfo: DiscussionPageInfoOutput{
HasNextPage: bool(pageInfo.HasNextPage), HasPreviousPage: bool(pageInfo.HasPreviousPage),
Expand Down Expand Up @@ -1126,9 +1115,6 @@ func ListDiscussionCategories(t translations.TranslationHelperFunc) inventory.Se
return nil, nil, fmt.Errorf("failed to marshal discussion categories: %w", err)
}
result := utils.NewToolResultText(string(out))
// Discussion categories are repo-defined structural metadata
// (trusted); confidentiality follows repo visibility.
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, owner, repo, result, ifc.LabelRepoMetadata)
items := make([]DiscussionCategoryItemOutput, 0, len(categories))
for _, category := range categories {
items = append(items, DiscussionCategoryItemOutput{ID: category["id"], Name: category["name"]})
Expand Down
4 changes: 0 additions & 4 deletions pkg/github/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ const MCPAppsDisableFormDeferralFeatureFlag = "mcp_apps_disable_form_deferral"
// FeatureFlagCSVOutput is the feature flag name for CSV output on list tools.
const FeatureFlagCSVOutput = "csv_output"

// FeatureFlagIFCLabels is the feature flag name for IFC security labels in tool results.
const FeatureFlagIFCLabels = "ifc_labels"

// FeatureFlagFileBlame is the feature flag name for the get_file_blame tool,
// which exposes git blame information for a file. It is gated so the extra tool
// is not advertised by default, keeping the tool surface small unless opted in.
Expand Down Expand Up @@ -46,7 +43,6 @@ const FeatureFlagThreadResolutionReason = "thread_resolution_reason"
var AllowedFeatureFlags = []string{
MCPAppsDisableFormDeferralFeatureFlag,
FeatureFlagCSVOutput,
FeatureFlagIFCLabels,
FeatureFlagIssuesGranular,
FeatureFlagPullRequestsGranular,
FeatureFlagFileBlame,
Expand Down
Loading
Loading