Skip to content

feat: support Go modules as a package type#1321

Open
koriyoshi2041 wants to merge 2 commits into
modelcontextprotocol:mainfrom
koriyoshi2041:feat/go-package-type
Open

feat: support Go modules as a package type#1321
koriyoshi2041 wants to merge 2 commits into
modelcontextprotocol:mainfrom
koriyoshi2041:feat/go-package-type

Conversation

@koriyoshi2041
Copy link
Copy Markdown

Closes #1307.

Adds "registryType": "go" so MCP servers distributed as go install-able modules can be published to the registry. Design discussed in #1307.

Ownership reuses the publisher's GitHub namespace rather than inventing an mcpName-style marker (which wouldn't be idiomatic in a go.mod). A Go module path is its source location, so a server published under io.github.<owner> must use a module path rooted at github.com/<owner>/.... The owner is compared case-insensitively (GitHub owners are case-insensitive). Since the publisher already authenticates for the io.github.<owner> namespace, matching the module owner proves ownership. Non-GitHub module paths are intentionally left for a follow-up (a sentinel-file/go.mod-directive check can be added later without changing this).

Existence is verified against the Go module proxy (https://proxy.golang.org) — the canonical source of truth, with no auth/rate-limit concerns. The module path and version are case-encoded per the goproxy protocol (uppercase → !+lowercase), and ./.. path elements are rejected so a crafted identifier can't escape the proxy path.

Changes

  • pkg/model/constants.go: RegistryTypeGo + RegistryURLGoProxy.
  • internal/validators/package.go: dispatch go to ValidateGo.
  • internal/validators/registries/go.go: ValidateGo + ValidateGoModuleOwnership + module-proxy existence check + EscapeGoModulePath.
  • internal/validators/registries/go_test.go: ownership matrix (incl. case-insensitivity and mismatch), path-escaping/traversal, and ValidateGo guard errors — all deterministic, no network.
  • docs/.../package-types.mdx: Go section.

Example

{
  "registryType": "go",
  "identifier": "github.com/username/audit-mcp",
  "version": "v1.0.0"
}

go test ./internal/validators/... passes; gofmt/go vet clean. The existence path is exercised against the live proxy on a real publish; the unit tests cover the ownership and escaping logic without network. Happy to adjust the identifier semantics (module path vs. go install sub-path) if you'd prefer a different convention.

Adds "registryType": "go" so MCP servers distributed as go-install-able
modules can be published. Ownership reuses the publisher's GitHub namespace:
a server under io.github.<owner> must use a module path rooted at
github.com/<owner>/... (compared case-insensitively, since GitHub owners are
case-insensitive). Existence is verified against the Go module proxy
(proxy.golang.org). Non-GitHub module paths are left for a follow-up.

Closes modelcontextprotocol#1307
@P4ST4S
Copy link
Copy Markdown

P4ST4S commented May 30, 2026

This looks excellent, thanks for moving so fast on this. A few notes from a careful read, none of them blocking:

Things I especially liked:

  • The case-encoding matches the official Go module proxy spec exactly ("uppercase letter → '!' + lowercase"), and the test cases mirror the canonical examples from golang.org/x/mod/module#EscapePath.
  • The . / .. path element rejection and control-character guard are exactly the right paranoia for an identifier that flows into a proxy URL.
  • Running ownership validation before the network call (verified by the ownership failure returns before network test) is the correct order: a malicious identifier never triggers an outbound request, and we don't leak existence of private modules to namespace-squatters.
  • Case-insensitive owner comparison is exactly what GitHub semantics require, appreciate that you picked this up from the issue discussion.

Small things worth considering, not blocking:

  1. golang.org/x/mod/module.EscapePath does effectively the same case-encoding plus the additional CheckPath validation (Windows reserved names like CON/com1, semver /vN suffix structure, etc.). The inline implementation is correct for current spec, but a future edge case in module path validation would need to be patched here manually. Trade-off: pulling in x/mod adds a dependency for one function. Either way is defensible, just noting the alternative exists.

  2. No coverage on the HTTP path in validateGoModuleExists. Totally understandable (mocking would require extracting the client; hitting the real proxy.golang.org would be flaky), but a future regression where someone adds, say, case 503: return nil would slip through. If you want, that's solvable with a small httptest.Server fixture, happy to follow up in a separate PR if useful.

  3. Hardcoded 10 * time.Second timeout, minor, but pulling it to a package-level const would make it tweakable without grep.

Thanks again, this is high-quality work and the design choice to anchor ownership in GitHub namespace authentication is exactly the right shape for v1.

Add an httptest-backed test for ValidateGoModuleExists covering 200/404/410/500
responses and asserting the case-encoded proxy request path. Exporting the
function makes it testable from the external test package without hitting the
real proxy. Also lift the hardcoded 10s client timeout to a package const.
@koriyoshi2041
Copy link
Copy Markdown
Author

Thanks for the careful read — really appreciate the detailed notes.

I pushed two of these:

  1. HTTP path coverage — added TestValidateGoModuleExists, an httptest.Server-backed test covering 200/404/410/500 and asserting the case-encoded proxy request path (!alice etc.). To make it testable from the external _test package without hitting the real proxy I exported validateGoModuleExistsValidateGoModuleExists. So the case 503: return nil-style regression you mentioned would now fail the suite.

  2. Timeout — lifted to a package-level goProxyRequestTimeout const.

On (1), golang.org/x/mod/module.EscapePath: I went with the inline encoder deliberately to keep this from pulling x/mod into the API server for a single function, and the ./.. + control-char guards cover the path-injection surface that matters here. You're right that CheckPath's extra validation (Windows reserved names, /vN suffix structure) isn't replicated — but that's belt-and-suspenders on top of the proxy itself rejecting bad paths. If maintainers would rather lean on x/mod for the future-proofing, I'm happy to swap it; just didn't want to add the dep unilaterally.

Pushed in 30310e7.

@P4ST4S
Copy link
Copy Markdown

P4ST4S commented May 31, 2026

All three responses look great:

  • HTTP path coverage: the new TestValidateGoModuleExists does exactly what's needed, and it goes one better than I suggested by asserting the case-encoded proxy path (/github.com/!alice/mcp-foo/@v/v1.0.0.info) in the same fixture. That verifies the encode step and the status handling together, which is tighter than two separate assertions would be.
  • Timeout const: clean.
  • x/mod decision: your reasoning is sound. The ./.. + control-char guards do cover the actual path-injection surface; CheckPath is belt-and-suspenders against rules the proxy itself enforces. Happy to defer to maintainers if they have a stronger view on the dependency trade-off.

LGTM from an external reviewer's perspective. Thanks for the thorough turnaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Go modules (go install) as a package type

2 participants