mirror-github.paniser.workers.dev/github/github-mcp-server
Advanced tools
| package github | ||
| import ( | ||
| "os" | ||
| "testing" | ||
| "github.com/github/github-mcp-server/pkg/toolvalidation" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
| // TestAllToolRegistrationsExplicitlySetReadOnlyHint statically scans every | ||
| // non-test Go source file in this package and asserts that every mcp.Tool | ||
| // composite literal explicitly sets Annotations.ReadOnlyHint. | ||
| // | ||
| // The AST scan itself lives in pkg/toolvalidation so downstream packages | ||
| // (e.g. github/github-mcp-server-remote) can apply the same guardrail to | ||
| // their own tool registrations without duplicating the parser logic. | ||
| // | ||
| // This complements TestAllToolsHaveRequiredMetadata, which can only check | ||
| // that Annotations is non-nil at runtime: Go cannot distinguish an unset | ||
| // bool field from one explicitly set to false. Source-level validation | ||
| // closes that gap and prevents future tool registrations from silently | ||
| // defaulting ReadOnlyHint to false (which has caused downstream agents to | ||
| // prompt for human approval on read-intent tools). | ||
| // | ||
| // Related issue: github/github-mcp-server#2483 | ||
| func TestAllToolRegistrationsExplicitlySetReadOnlyHint(t *testing.T) { | ||
| pkgDir, err := os.Getwd() | ||
| require.NoError(t, err, "must be able to resolve package directory") | ||
| violations, err := toolvalidation.ScanReadOnlyHint(pkgDir) | ||
| require.NoError(t, err) | ||
| if len(violations) > 0 { | ||
| t.Fatal(toolvalidation.FormatReadOnlyHintViolations(violations)) | ||
| } | ||
| } |
| package github | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/github/github-mcp-server/pkg/inventory" | ||
| "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
| // TestRegisterUIResources_ReadableViaClient verifies that each UI resource URI | ||
| // advertised by an MCP App-enabled tool (e.g. issue_write, create_pull_request, | ||
| // get_me) actually resolves to a registered resource on the server. | ||
| // | ||
| // Regression test for the "Error loading MCP App: MPC -32002: Resource not | ||
| // found" bug reported in issue #2467, where the HTTP/remote server returned a | ||
| // resource URI in the tool's _meta.ui block but never registered the matching | ||
| // resource — so the follow-up resources/read call from the client failed. | ||
| func TestRegisterUIResources_ReadableViaClient(t *testing.T) { | ||
| t.Parallel() | ||
| if !UIAssetsAvailable() { | ||
| t.Skip("UI assets not built; run script/build-ui to enable this test") | ||
| } | ||
| srv := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil) | ||
| RegisterUIResources(srv) | ||
| // Connect an in-memory client/server pair and read each advertised URI. | ||
| st, ct := mcp.NewInMemoryTransports() | ||
| type clientResult struct { | ||
| session *mcp.ClientSession | ||
| err error | ||
| } | ||
| clientCh := make(chan clientResult, 1) | ||
| go func() { | ||
| client := mcp.NewClient(&mcp.Implementation{Name: "test-client"}, nil) | ||
| cs, err := client.Connect(context.Background(), ct, nil) | ||
| clientCh <- clientResult{session: cs, err: err} | ||
| }() | ||
| ss, err := srv.Connect(context.Background(), st, nil) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { _ = ss.Close() }) | ||
| got := <-clientCh | ||
| require.NoError(t, got.err) | ||
| t.Cleanup(func() { _ = got.session.Close() }) | ||
| uris := []string{ | ||
| GetMeUIResourceURI, | ||
| IssueWriteUIResourceURI, | ||
| PullRequestWriteUIResourceURI, | ||
| } | ||
| for _, uri := range uris { | ||
| t.Run(uri, func(t *testing.T) { | ||
| res, err := got.session.ReadResource(context.Background(), &mcp.ReadResourceParams{URI: uri}) | ||
| require.NoError(t, err, "resource %s should be registered (got -32002 means it isn't)", uri) | ||
| require.NotNil(t, res) | ||
| require.NotEmpty(t, res.Contents) | ||
| assert.Equal(t, uri, res.Contents[0].URI) | ||
| assert.Equal(t, MCPAppMIMEType, res.Contents[0].MIMEType) | ||
| assert.NotEmpty(t, res.Contents[0].Text, "UI resource should return HTML body") | ||
| }) | ||
| } | ||
| } | ||
| // TestNewMCPServer_RegistersUIResources verifies that NewMCPServer — the | ||
| // shared constructor used by both the stdio and HTTP entry points — registers | ||
| // the UI resources when UI assets are embedded. Previously this registration | ||
| // only happened in the stdio bootstrap, so remote/HTTP clients hit -32002. | ||
| func TestNewMCPServer_RegistersUIResources(t *testing.T) { | ||
| t.Parallel() | ||
| if !UIAssetsAvailable() { | ||
| t.Skip("UI assets not built; run script/build-ui to enable this test") | ||
| } | ||
| srv, err := NewMCPServer(context.Background(), &MCPServerConfig{ | ||
| Version: "test", | ||
| Translator: stubTranslator, | ||
| }, stubDeps{t: stubTranslator}, mustEmptyInventory(t)) | ||
| require.NoError(t, err) | ||
| st, ct := mcp.NewInMemoryTransports() | ||
| type clientResult struct { | ||
| session *mcp.ClientSession | ||
| err error | ||
| } | ||
| clientCh := make(chan clientResult, 1) | ||
| go func() { | ||
| client := mcp.NewClient(&mcp.Implementation{Name: "test-client"}, nil) | ||
| cs, err := client.Connect(context.Background(), ct, nil) | ||
| clientCh <- clientResult{session: cs, err: err} | ||
| }() | ||
| ss, err := srv.Connect(context.Background(), st, nil) | ||
| require.NoError(t, err) | ||
| t.Cleanup(func() { _ = ss.Close() }) | ||
| got := <-clientCh | ||
| require.NoError(t, got.err) | ||
| t.Cleanup(func() { _ = got.session.Close() }) | ||
| res, err := got.session.ReadResource(context.Background(), &mcp.ReadResourceParams{URI: IssueWriteUIResourceURI}) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, res) | ||
| require.NotEmpty(t, res.Contents) | ||
| assert.Equal(t, MCPAppMIMEType, res.Contents[0].MIMEType) | ||
| } | ||
| // mustEmptyInventory builds an empty inventory for tests that only care about | ||
| // resources/prompts registered outside the inventory (such as the UI resources). | ||
| func mustEmptyInventory(t *testing.T) *inventory.Inventory { | ||
| t.Helper() | ||
| inv, err := NewInventory(stubTranslator).WithToolsets([]string{}).Build() | ||
| require.NoError(t, err) | ||
| return inv | ||
| } |
| package toolvalidation_test | ||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
| "github.com/github/github-mcp-server/pkg/toolvalidation" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
| // writePackage writes a single Go source file into a fresh temp directory and | ||
| // returns that directory, suitable for passing to ScanReadOnlyHint. | ||
| func writePackage(t *testing.T, filename, source string) string { | ||
| t.Helper() | ||
| dir := t.TempDir() | ||
| require.NoError(t, os.WriteFile(filepath.Join(dir, filename), []byte(source), 0o600)) | ||
| return dir | ||
| } | ||
| func TestScanReadOnlyHint(t *testing.T) { | ||
| t.Parallel() | ||
| const compliant = `package fixture | ||
| import "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| var Tool = mcp.Tool{ | ||
| Name: "compliant_tool", | ||
| Annotations: &mcp.ToolAnnotations{ | ||
| ReadOnlyHint: true, | ||
| }, | ||
| } | ||
| ` | ||
| const missingHint = `package fixture | ||
| import "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| var Tool = mcp.Tool{ | ||
| Name: "missing_hint", | ||
| Annotations: &mcp.ToolAnnotations{ | ||
| Title: "no hint", | ||
| }, | ||
| } | ||
| ` | ||
| const missingAnnotations = `package fixture | ||
| import "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| var Tool = mcp.Tool{ | ||
| Name: "missing_annotations", | ||
| } | ||
| ` | ||
| const nonLiteralAnnotations = `package fixture | ||
| import "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| func annotations() *mcp.ToolAnnotations { return &mcp.ToolAnnotations{ReadOnlyHint: true} } | ||
| var Tool = mcp.Tool{ | ||
| Name: "non_literal", | ||
| Annotations: annotations(), | ||
| } | ||
| ` | ||
| const unkeyedTool = `package fixture | ||
| import "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| var Tool = mcp.Tool{"unkeyed", "desc", nil, nil, nil, nil} | ||
| ` | ||
| const aliasedImport = `package fixture | ||
| import sdk "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| var Tool = sdk.Tool{ | ||
| Name: "aliased", | ||
| Annotations: &sdk.ToolAnnotations{ | ||
| ReadOnlyHint: false, | ||
| }, | ||
| } | ||
| ` | ||
| const noMCPImport = `package fixture | ||
| import "fmt" | ||
| var _ = fmt.Sprintln("nothing to scan here") | ||
| ` | ||
| cases := []struct { | ||
| name string | ||
| source string | ||
| expectCount int | ||
| expectReason string | ||
| expectToolName string | ||
| }{ | ||
| {name: "compliant literal passes", source: compliant, expectCount: 0}, | ||
| {name: "aliased import is detected", source: aliasedImport, expectCount: 0}, | ||
| {name: "file without mcp import is skipped", source: noMCPImport, expectCount: 0}, | ||
| { | ||
| name: "missing ReadOnlyHint is flagged", | ||
| source: missingHint, | ||
| expectCount: 1, | ||
| expectReason: "does not explicitly set ReadOnlyHint", | ||
| expectToolName: "missing_hint", | ||
| }, | ||
| { | ||
| name: "missing Annotations is flagged", | ||
| source: missingAnnotations, | ||
| expectCount: 1, | ||
| expectReason: "missing an Annotations field", | ||
| expectToolName: "missing_annotations", | ||
| }, | ||
| { | ||
| name: "non-literal Annotations is flagged", | ||
| source: nonLiteralAnnotations, | ||
| expectCount: 1, | ||
| expectReason: "not an &mcp.ToolAnnotations{...} literal", | ||
| expectToolName: "non_literal", | ||
| }, | ||
| { | ||
| name: "positional Tool fields are flagged", | ||
| source: unkeyedTool, | ||
| expectCount: 1, | ||
| expectReason: "positional (unkeyed) fields", | ||
| expectToolName: "<unknown>", | ||
| }, | ||
| } | ||
| for _, tc := range cases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| dir := writePackage(t, "fixture.go", tc.source) | ||
| violations, err := toolvalidation.ScanReadOnlyHint(dir) | ||
| require.NoError(t, err) | ||
| require.Len(t, violations, tc.expectCount) | ||
| if tc.expectCount == 0 { | ||
| return | ||
| } | ||
| v := violations[0] | ||
| assert.Equal(t, "fixture.go", v.File) | ||
| assert.Greater(t, v.Line, 0) | ||
| assert.Equal(t, tc.expectToolName, v.ToolName) | ||
| assert.Contains(t, v.Reason, tc.expectReason) | ||
| }) | ||
| } | ||
| } | ||
| func TestFormatReadOnlyHintViolations(t *testing.T) { | ||
| t.Parallel() | ||
| assert.Empty(t, toolvalidation.FormatReadOnlyHintViolations(nil)) | ||
| msg := toolvalidation.FormatReadOnlyHintViolations([]toolvalidation.ReadOnlyHintViolation{{ | ||
| File: "issues.go", | ||
| Line: 42, | ||
| ToolName: "issue_read", | ||
| Reason: "ToolAnnotations literal does not explicitly set ReadOnlyHint", | ||
| }}) | ||
| assert.True(t, strings.HasPrefix(msg, "Found tool registrations that do not explicitly set ReadOnlyHint:")) | ||
| assert.Contains(t, msg, "issues.go:42 tool=issue_read") | ||
| assert.Contains(t, msg, "true for read-only tools, false for tools with side effects") | ||
| } | ||
| func TestScanReadOnlyHint_ReturnsErrorForMissingDirectory(t *testing.T) { | ||
| t.Parallel() | ||
| _, err := toolvalidation.ScanReadOnlyHint(filepath.Join(t.TempDir(), "does-not-exist")) | ||
| require.Error(t, err) | ||
| } |
| // Package toolvalidation provides source-level (AST) validators for MCP tool | ||
| // registrations. It is intended to be consumed from _test.go files in any | ||
| // package that registers mcp.Tool literals (including downstream repositories | ||
| // such as github-mcp-server-remote) so the same guardrails apply everywhere | ||
| // without duplicating the parsing logic. | ||
| package toolvalidation | ||
| import ( | ||
| "fmt" | ||
| "go/ast" | ||
| "go/parser" | ||
| "go/token" | ||
| "os" | ||
| "path/filepath" | ||
| "strconv" | ||
| "strings" | ||
| ) | ||
| // MCPImportPath is the canonical module path of the MCP go-sdk. Source files | ||
| // that import this path under any alias (including the default `mcp`) are | ||
| // candidates for tool-literal validation. | ||
| const MCPImportPath = "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| // ReadOnlyHintViolation describes a single mcp.Tool composite literal that | ||
| // failed the ReadOnlyHint check. | ||
| type ReadOnlyHintViolation struct { | ||
| // File is the path to the offending source file, made relative to the | ||
| // scan directory when possible. | ||
| File string | ||
| // Line is the 1-indexed line number of the offending literal. | ||
| Line int | ||
| // ToolName is the value of the Name field on the mcp.Tool literal, or | ||
| // "<unknown>" when it cannot be statically extracted. | ||
| ToolName string | ||
| // Reason is a human-readable explanation of why the literal failed. | ||
| Reason string | ||
| } | ||
| // String renders a violation in the format used by FormatReadOnlyHintViolations: | ||
| // "<file>:<line> tool=<name>: <reason>". | ||
| func (v ReadOnlyHintViolation) String() string { | ||
| return fmt.Sprintf("%s:%d tool=%s: %s", v.File, v.Line, v.ToolName, v.Reason) | ||
| } | ||
| // ScanReadOnlyHint parses every non-test .go file in dir (a single package | ||
| // directory) and returns a violation for each mcp.Tool composite literal that | ||
| // does not explicitly set Annotations.ReadOnlyHint. | ||
| // | ||
| // The Go runtime cannot distinguish an unset bool field from one explicitly | ||
| // set to false, so this AST-level check exists to prevent future tool | ||
| // registrations from silently defaulting ReadOnlyHint to false — which has | ||
| // triggered downstream agents to prompt for human approval on safe read | ||
| // operations. | ||
| // | ||
| // Callers typically invoke this from a _test.go file: | ||
| // | ||
| // dir, _ := os.Getwd() | ||
| // violations, err := toolvalidation.ScanReadOnlyHint(dir) | ||
| func ScanReadOnlyHint(dir string) ([]ReadOnlyHintViolation, error) { | ||
| fset := token.NewFileSet() | ||
| pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool { | ||
| // Skip test files: they are allowed to construct mcp.Tool literals | ||
| // for fixtures or mocks where ReadOnlyHint is not meaningful. | ||
| return !strings.HasSuffix(info.Name(), "_test.go") | ||
| }, parser.ParseComments) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("parse package directory %q: %w", dir, err) | ||
| } | ||
| var violations []ReadOnlyHintViolation | ||
| for _, pkg := range pkgs { | ||
| for filename, file := range pkg.Files { | ||
| aliases := mcpAliasesFor(file) | ||
| if len(aliases) == 0 { | ||
| continue | ||
| } | ||
| rel, relErr := filepath.Rel(dir, filename) | ||
| if relErr != nil || rel == "" { | ||
| rel = filepath.Base(filename) | ||
| } | ||
| ast.Inspect(file, func(n ast.Node) bool { | ||
| cl, ok := n.(*ast.CompositeLit) | ||
| if !ok { | ||
| return true | ||
| } | ||
| if !isQualifiedType(cl.Type, aliases, "Tool") { | ||
| return true | ||
| } | ||
| violations = append(violations, checkToolLiteral(cl, aliases, rel, fset.Position(cl.Pos()).Line)...) | ||
| return true | ||
| }) | ||
| } | ||
| } | ||
| return violations, nil | ||
| } | ||
| // FormatReadOnlyHintViolations renders a single multi-line error message | ||
| // suitable for passing to t.Fatal. Returns "" when violations is empty. | ||
| func FormatReadOnlyHintViolations(violations []ReadOnlyHintViolation) string { | ||
| if len(violations) == 0 { | ||
| return "" | ||
| } | ||
| var msg strings.Builder | ||
| msg.WriteString("Found tool registrations that do not explicitly set ReadOnlyHint:\n") | ||
| for _, v := range violations { | ||
| msg.WriteString(" - ") | ||
| msg.WriteString(v.String()) | ||
| msg.WriteByte('\n') | ||
| } | ||
| msg.WriteString("\nEvery mcp.Tool registration must declare Annotations.ReadOnlyHint explicitly ") | ||
| msg.WriteString("(true for read-only tools, false for tools with side effects). ") | ||
| msg.WriteString("See pkg/toolvalidation.ScanReadOnlyHint.") | ||
| return msg.String() | ||
| } | ||
| func checkToolLiteral(cl *ast.CompositeLit, aliases map[string]struct{}, file string, line int) []ReadOnlyHintViolation { | ||
| toolName := extractToolName(cl) | ||
| if toolName == "" { | ||
| toolName = "<unknown>" | ||
| } | ||
| mk := func(reason string) ReadOnlyHintViolation { | ||
| return ReadOnlyHintViolation{File: file, Line: line, ToolName: toolName, Reason: reason} | ||
| } | ||
| if hasUnkeyedFields(cl) { | ||
| return []ReadOnlyHintViolation{mk("mcp.Tool literal uses positional (unkeyed) fields; this check requires keyed fields so Annotations.ReadOnlyHint can be verified")} | ||
| } | ||
| annotations := findFieldValue(cl, "Annotations") | ||
| if annotations == nil { | ||
| return []ReadOnlyHintViolation{mk("mcp.Tool literal is missing an Annotations field")} | ||
| } | ||
| annoLit := unwrapAnnotationsLiteral(annotations, aliases) | ||
| if annoLit == nil { | ||
| return []ReadOnlyHintViolation{mk("Annotations is not an &mcp.ToolAnnotations{...} literal; ReadOnlyHint cannot be statically verified")} | ||
| } | ||
| if hasUnkeyedFields(annoLit) { | ||
| return []ReadOnlyHintViolation{mk("mcp.ToolAnnotations literal uses positional (unkeyed) fields; use keyed fields so ReadOnlyHint can be verified")} | ||
| } | ||
| if findFieldValue(annoLit, "ReadOnlyHint") == nil { | ||
| return []ReadOnlyHintViolation{mk("ToolAnnotations literal does not explicitly set ReadOnlyHint")} | ||
| } | ||
| return nil | ||
| } | ||
| // mcpAliasesFor returns the set of local identifiers under which the given | ||
| // file imports the MCP go-sdk (MCPImportPath). The default unaliased import | ||
| // resolves to the package name "mcp". Blank (`_`) and dot (`.`) imports are | ||
| // skipped because tool literals cannot meaningfully be qualified through them. | ||
| func mcpAliasesFor(file *ast.File) map[string]struct{} { | ||
| aliases := map[string]struct{}{} | ||
| for _, imp := range file.Imports { | ||
| path, err := strconv.Unquote(imp.Path.Value) | ||
| if err != nil || path != MCPImportPath { | ||
| continue | ||
| } | ||
| if imp.Name != nil { | ||
| if imp.Name.Name == "_" || imp.Name.Name == "." { | ||
| continue | ||
| } | ||
| aliases[imp.Name.Name] = struct{}{} | ||
| continue | ||
| } | ||
| aliases["mcp"] = struct{}{} | ||
| } | ||
| return aliases | ||
| } | ||
| // isQualifiedType reports whether expr is a SelectorExpr of the form | ||
| // <alias>.<typeName> where alias is in the provided alias set. | ||
| func isQualifiedType(expr ast.Expr, aliases map[string]struct{}, typeName string) bool { | ||
| sel, ok := expr.(*ast.SelectorExpr) | ||
| if !ok { | ||
| return false | ||
| } | ||
| ident, ok := sel.X.(*ast.Ident) | ||
| if !ok { | ||
| return false | ||
| } | ||
| if _, ok := aliases[ident.Name]; !ok { | ||
| return false | ||
| } | ||
| return sel.Sel != nil && sel.Sel.Name == typeName | ||
| } | ||
| // hasUnkeyedFields reports whether the composite literal has any positional | ||
| // (non-key/value) elements. The static check cannot reliably map positional | ||
| // fields without full type information, so such literals are rejected with a | ||
| // dedicated diagnostic rather than producing false "missing field" violations. | ||
| func hasUnkeyedFields(cl *ast.CompositeLit) bool { | ||
| for _, elt := range cl.Elts { | ||
| if _, ok := elt.(*ast.KeyValueExpr); !ok { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| // findFieldValue returns the value expression for the named keyed field of a | ||
| // composite literal, or nil if the field is absent. | ||
| func findFieldValue(cl *ast.CompositeLit, name string) ast.Expr { | ||
| for _, elt := range cl.Elts { | ||
| kv, ok := elt.(*ast.KeyValueExpr) | ||
| if !ok { | ||
| continue | ||
| } | ||
| key, ok := kv.Key.(*ast.Ident) | ||
| if !ok { | ||
| continue | ||
| } | ||
| if key.Name == name { | ||
| return kv.Value | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| // unwrapAnnotationsLiteral attempts to extract the *ast.CompositeLit for | ||
| // &mcp.ToolAnnotations{...} or mcp.ToolAnnotations{...} from an expression, | ||
| // resolving the MCP package's local alias per file. | ||
| func unwrapAnnotationsLiteral(expr ast.Expr, aliases map[string]struct{}) *ast.CompositeLit { | ||
| if u, ok := expr.(*ast.UnaryExpr); ok && u.Op == token.AND { | ||
| expr = u.X | ||
| } | ||
| cl, ok := expr.(*ast.CompositeLit) | ||
| if !ok { | ||
| return nil | ||
| } | ||
| if !isQualifiedType(cl.Type, aliases, "ToolAnnotations") { | ||
| return nil | ||
| } | ||
| return cl | ||
| } | ||
| // extractToolName returns the literal value of the Name field of an mcp.Tool | ||
| // composite literal, or empty string if the value is not a basic string literal. | ||
| // Interpreted ("...") and raw (`...`) string literals are handled via | ||
| // strconv.Unquote so embedded escapes are decoded correctly; the raw | ||
| // literal value is returned as a best-effort fallback if unquoting fails. | ||
| func extractToolName(cl *ast.CompositeLit) string { | ||
| v := findFieldValue(cl, "Name") | ||
| if v == nil { | ||
| return "" | ||
| } | ||
| bl, ok := v.(*ast.BasicLit) | ||
| if !ok || bl.Kind != token.STRING { | ||
| return "" | ||
| } | ||
| if unq, err := strconv.Unquote(bl.Value); err == nil { | ||
| return unq | ||
| } | ||
| return bl.Value | ||
| } |
+1
-1
@@ -13,3 +13,3 @@ module github.com/github/github-mcp-server | ||
| github.com/microcosm-cc/bluemonday v1.0.27 | ||
| github.com/modelcontextprotocol/go-sdk v1.6.0 | ||
| github.com/modelcontextprotocol/go-sdk v1.6.1 | ||
| github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 | ||
@@ -16,0 +16,0 @@ github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 |
+2
-2
@@ -42,4 +42,4 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= | ||
| github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= | ||
| github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY= | ||
| github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= | ||
| github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= | ||
| github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= | ||
| github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 h1:31Y+Yu373ymebRdJN1cWLLooHH8xAr0MhKTEJGV/87g= | ||
@@ -46,0 +46,0 @@ github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021/go.mod h1:WERUkUryfUWlrHnFSO/BEUZ+7Ns8aZy7iVOGewxKzcc= |
@@ -176,11 +176,2 @@ package ghmcp | ||
| // Register MCP App UI resources if the remote_mcp_ui_apps feature flag is enabled | ||
| // and UI assets are available (requires running script/build-ui). | ||
| // We check availability to allow the feature flag to be enabled without | ||
| // requiring a UI build (graceful degradation). | ||
| mcpAppsEnabled, _ := featureChecker(context.Background(), github.MCPAppsFeatureFlag) | ||
| if mcpAppsEnabled && github.UIAssetsAvailable() { | ||
| github.RegisterUIResources(ghServer) | ||
| } | ||
| ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, clients.restUATransp, clients.gqlHTTP)) | ||
@@ -187,0 +178,0 @@ |
@@ -151,3 +151,3 @@ package github | ||
| expectedFlags: nil, | ||
| unexpectedFlags: []string{MCPAppsFeatureFlag, FeatureFlagIFCLabels}, | ||
| unexpectedFlags: []string{MCPAppsFeatureFlag}, | ||
| }, | ||
@@ -172,6 +172,5 @@ { | ||
| { | ||
| name: "internal-only flags are not directly enabled", | ||
| name: "ifc_labels can be directly enabled", | ||
| enabledFeatures: []string{FeatureFlagIFCLabels}, | ||
| expectedFlags: nil, | ||
| unexpectedFlags: []string{FeatureFlagIFCLabels}, | ||
| expectedFlags: []string{FeatureFlagIFCLabels}, | ||
| }, | ||
@@ -178,0 +177,0 @@ { |
@@ -26,2 +26,3 @@ package github | ||
| FeatureFlagCSVOutput, | ||
| FeatureFlagIFCLabels, | ||
| FeatureFlagIssueFields, | ||
@@ -28,0 +29,0 @@ FeatureFlagIssuesGranular, |
+12
-0
@@ -105,2 +105,14 @@ package github | ||
| // Register MCP App UI resources whenever the embedded UI assets are | ||
| // available. The resources are static HTML and are only referenced by | ||
| // tools when the remote_mcp_ui_apps feature flag is enabled for the | ||
| // request (the inventory strips the _meta.ui block otherwise via | ||
| // stripMCPAppsMetadata), so registering them unconditionally is safe. | ||
| // Registering here — rather than in the stdio bootstrap — ensures the | ||
| // remote/HTTP server also serves them, fixing the "-32002 Resource not | ||
| // found" error clients hit after the tool returns a ui:// URI. | ||
| if UIAssetsAvailable() { | ||
| RegisterUIResources(ghServer) | ||
| } | ||
| return ghServer, nil | ||
@@ -107,0 +119,0 @@ } |
@@ -226,12 +226,11 @@ package http | ||
| // Bypass cross-origin protection: this server uses bearer tokens (not | ||
| // cookies), so Sec-Fetch-Site CSRF checks are unnecessary. See PR #2359. | ||
| crossOriginProtection := http.NewCrossOriginProtection() | ||
| crossOriginProtection.AddInsecureBypassPattern("/") | ||
| // Cross-origin protection is intentionally left unset: this server | ||
| // authenticates via bearer tokens (not cookies), so Sec-Fetch-Site CSRF | ||
| // checks are unnecessary and would block browser-based MCP clients. As of | ||
| // go-sdk v1.6.0 a nil CrossOriginProtection disables the check by default; | ||
| // see also PR #2359. | ||
| mcpHandler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server { | ||
| return ghServer | ||
| }, &mcp.StreamableHTTPOptions{ | ||
| Stateless: true, | ||
| CrossOriginProtection: crossOriginProtection, | ||
| Stateless: true, | ||
| }) | ||
@@ -238,0 +237,0 @@ |
@@ -91,8 +91,2 @@ package http | ||
| { | ||
| name: "internal-only flag in header is ignored", | ||
| flagName: github.FeatureFlagIFCLabels, | ||
| headerFeatures: []string{github.FeatureFlagIFCLabels}, | ||
| wantEnabled: false, | ||
| }, | ||
| { | ||
| name: "static insiders enables insiders flags without route context", | ||
@@ -99,0 +93,0 @@ staticInsiders: true, |
@@ -27,4 +27,4 @@ # GitHub MCP Server dependencies | ||
| - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.0/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.0/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.1/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.1/LICENSE)) | ||
| - [github.com/muesli/cache2go](https://pkg.go.dev/github.com/muesli/cache2go) ([BSD-3-Clause](https://github.com/muesli/cache2go/blob/518229cd8021/LICENSE.txt)) | ||
@@ -31,0 +31,0 @@ - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE)) |
@@ -27,4 +27,4 @@ # GitHub MCP Server dependencies | ||
| - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.0/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.0/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.1/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.1/LICENSE)) | ||
| - [github.com/muesli/cache2go](https://pkg.go.dev/github.com/muesli/cache2go) ([BSD-3-Clause](https://github.com/muesli/cache2go/blob/518229cd8021/LICENSE.txt)) | ||
@@ -31,0 +31,0 @@ - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE)) |
@@ -28,4 +28,4 @@ # GitHub MCP Server dependencies | ||
| - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.0/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.0/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.1/LICENSE)) | ||
| - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.6.1/LICENSE)) | ||
| - [github.com/muesli/cache2go](https://pkg.go.dev/github.com/muesli/cache2go) ([BSD-3-Clause](https://github.com/muesli/cache2go/blob/518229cd8021/LICENSE.txt)) | ||
@@ -32,0 +32,0 @@ - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE)) |