diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go index 902734481a..9f84c02118 100644 --- a/pkg/github/context_tools.go +++ b/pkg/github/context_tools.go @@ -6,6 +6,7 @@ import ( "time" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/ifc" "github.com/github/github-mcp-server/pkg/inventory" "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" @@ -103,7 +104,14 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool { }, } - return MarshalledTextResult(minimalUser), nil, nil + result := MarshalledTextResult(minimalUser) + if deps.GetFlags(ctx).InsidersMode { + if result.Meta == nil { + result.Meta = mcp.Meta{} + } + result.Meta["ifc"] = ifc.LabelGetMe() + } + return result, nil, nil }, ) } diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go index 39f2058bec..365a019ab6 100644 --- a/pkg/github/context_tools_test.go +++ b/pkg/github/context_tools_test.go @@ -139,6 +139,66 @@ func Test_GetMe(t *testing.T) { } } +func Test_GetMe_IFC_InsidersMode(t *testing.T) { + t.Parallel() + + serverTool := GetMe(translations.NullTranslationHelper) + + mockUser := &github.User{ + Login: github.Ptr("testuser"), + HTMLURL: github.Ptr("https://github.com/testuser"), + CreatedAt: &github.Timestamp{Time: time.Now()}, + } + mockedHTTPClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + GetUser: mockResponse(t, http.StatusOK, mockUser), + }) + + t.Run("insiders mode disabled omits ifc label from result meta", func(t *testing.T) { + deps := BaseDeps{ + Client: github.NewClient(mockedHTTPClient), + Flags: FeatureFlags{InsidersMode: 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 insiders mode is disabled") + }) + + t.Run("insiders mode enabled includes ifc label in result meta", func(t *testing.T) { + deps := BaseDeps{ + Client: github.NewClient(mockedHTTPClient), + Flags: FeatureFlags{InsidersMode: 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 insiders mode is 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"]) + confList, ok := ifcMap["confidentiality"].([]any) + require.True(t, ok, "confidentiality should be a list") + require.Len(t, confList, 1) + assert.Equal(t, "public", confList[0]) + }) +} + func Test_GetTeams(t *testing.T) { t.Parallel() diff --git a/pkg/ifc/ifc.go b/pkg/ifc/ifc.go new file mode 100644 index 0000000000..cf0d72114f --- /dev/null +++ b/pkg/ifc/ifc.go @@ -0,0 +1,29 @@ +// Package ifc provides Information Flow Control labels for annotating MCP tool outputs. +// The actual IFC enforcement engine lives in a separate service; this package only +// defines the label schema used for annotations. +package ifc + +type Integrity string + +const ( + IntegrityTrusted Integrity = "trusted" + IntegrityUntrusted Integrity = "untrusted" +) + +type Confidentiality string + +const ( + ConfidentialityPublic Confidentiality = "public" +) + +type SecurityLabel struct { + Integrity Integrity `json:"integrity"` + Confidentiality []Confidentiality `json:"confidentiality"` +} + +func LabelGetMe() SecurityLabel { + return SecurityLabel{ + Integrity: IntegrityTrusted, + Confidentiality: []Confidentiality{ConfidentialityPublic}, + } +} diff --git a/script/get-me b/script/get-me index 954f57cec0..ffd24a357f 100755 --- a/script/get-me +++ b/script/get-me @@ -6,12 +6,12 @@ output=$( echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"get-me-script","version":"1.0.0"}}}' echo '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_me","arguments":{}}}' - sleep 1 - ) | go run cmd/github-mcp-server/main.go stdio 2>/dev/null | tail -1 + sleep 3 + ) | go run cmd/github-mcp-server/main.go stdio "$@" 2>/dev/null | grep '"id":2' ) if command -v jq &> /dev/null; then - echo "$output" | jq '.result.content[0].text | fromjson' + echo "$output" | jq '{_meta: .result._meta, content: (.result.content[0].text | fromjson)}' else echo "$output" fi