Commit graph registry/internal
Author SHA1 Message Date
Jeff Wainwright
dd4b2939e3
test: isolates temp test fixtures (#1569)
## Motivation and Context

Replace shared temp paths with `t.TempDir()`` so each test gets its own
cleanup-scoped directory.
- This avoids leftover files and cross-test interference. No production
code changed.

## How Has This Been Tested?

GitHub CI passed.
- https://github.com/yowainwright/registry/pull/1

## Breaking Changes

No breaking changes; just tests.

## Types of changes

- [x] chore

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context

Small test-only cleanup. No production code paths are changed.

Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-09-06 00:31:12 +03:00
Radoslav Dimitrov
068a97a2f8
fix: identify submitted server version in publish diagnostics (#1621)
Publishing a manifest with a stale version currently produces a
duplicate-version error without identifying the version that was
submitted. This made a request containing `0.4.5` look like a rejected
publication of `0.4.6` in #1615.

Include the submitted `name@version` in the CLI output before sending
the request and in the API duplicate-version error. HTTP 400, the
response structure, and duplicate-rejection behavior remain unchanged.
The service wraps the existing error sentinel so `errors.Is` continues
to work.

Adds a CLI regression test for a failed publication and updates the API
duplicate-error assertion.

Validation:
- Publish command tests passed (`go test ./cmd/publisher/commands -run
TestPublishCommand -count=1`).
- `git diff --check` passed.
- API endpoint tests were attempted but could not run because PostgreSQL
was unavailable on localhost:5432; Docker was also unavailable locally.

Related to #1615.
2026-09-06 00:18:27 +03:00
Radoslav Dimitrov
8759d9d8c0
ci: bump golangci-lint to v2.13.1 to fix panic on Go 1.27 (#1561)
CI is red on `main` and on every open PR (including the Dependabot PRs
#1531, #1552–#1554) with:

```
panic: file requires newer Go version go1.27 (application built with go1.26)
golangci-lint exit with code 2
```

## Cause

`actions/setup-go` with `go-version: 'stable'` started resolving to
**go1.27.0**. The pinned golangci-lint **v2.11.4** release binary is
built with **go1.26**, and its `go/types` refuses to typecheck source
files declaring a newer language version. Nothing in the failing PRs is
at fault.

## Fix

Bump to **golangci-lint v2.13.1**, whose release binaries are built with
go1.27, so linting works under `stable` again.

That bump also pulls in newer linter versions, which surface 57 findings
this tree did not have before. Handled as follows:

| Linter | Count | Resolution |
|---|---|---|
| `goconst` | 50 | 39 were repeated literals in `_test.go` files —
goconst started counting those in v2.12.0 (golangci-lint#6480 exposed
`ignore-tests`). Fixture data like `"1.0.0"` (89 occurrences) is not a
missing constant, so `goconst.ignore-tests: true`. The other 11 were
real duplication in production code (OpenAPI tags, the `bearer` security
scheme name, `$ref`, `http`/`https`) and became named constants. |
| `gosec` G710 | 1 | **True positive, fixed.** See below. |
| `staticcheck` SA1019 | 6 | Go 1.26 deprecated `ecdsa.PublicKey.X/Y`
and `PrivateKey.D`. Annotated with the same targeted `nolint` +
rationale already used for `ScalarBaseMult` in
`cmd/publisher/auth/common.go`. |

### The open redirect is real

`TrailingSlashMiddleware` copied the entire `*url.URL` before rewriting
the path. For an absolute-form request URI — legal for a Go HTTP server
to parse, e.g. `GET http://evil.com/foo/ HTTP/1.1` — `r.URL.Scheme` and
`r.URL.Host` are populated, and they were echoed straight back in the
`Location` header. The redirect target is now built from the cleaned
path and query alone. The existing `path.Clean` hardening for
GHSA-v8vw-gw5j-w7m6 (protocol-relative `//evil.com/`) is unchanged.

### Also

Switched `gomodguard` → `gomodguard_v2`; the former is deprecated as of
v2.12.0 and was emitting a warning on every run.

## Verification

- `golangci-lint v2.13.1` against this tree: **0 issues**, no
deprecation warnings
- `make validate`: all validations passed
- `go build ./...` and tests for every touched package pass (DB-backed
tests need Postgres; left to CI)

## Note on migrating the ecdsa key paths

Moving those six sites to `PublicKey.Bytes` /
`ParseUncompressedPublicKey` / `crypto/ecdh` touches key parsing and
signature verification. It should be its own reviewed PR rather than
riding along on a CI unblock. Happy to open one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 00:14:13 +03:00
Wolfe-James
69374b08de
docs: add cargo and crates.io to schema examples (#1511)
The cargo registry type has been live since v1.8.0 (#1207).
This PR only updates the schema examples so they match reality.

- Add "cargo" to registryType.examples
- Add "https://crates.io" to registryBaseUrl.examples

No behaviour change.

Note: versioned schemas are synced from modelcontextprotocol/static.
A follow-up there keeps this durable across schema sync.

Co-authored-by: Wolfe-Jam <wolfejam77@gmail.com>
2026-08-10 15:19:06 +03:00
Sash
5c19656cb9
fix(auth): reject github.io domains in DNS/HTTP token exchange (org namespace takeover) (#1506)
## Summary

- The GitHub token method grants `io.github.<org>/*` publish rights
**only to org Owners** (active `admin` membership — `github_at.go`).
- The DNS/HTTP token methods grant `reversed(domain)/*` for any proven
domain, with no `github.io` special-casing and an empty
`BlockedNamespaces` list.
- `<org>.github.io` is served by GitHub Pages from the
`<org>/<org>.github.io` repository. **Push access to that one
repository** — routinely held by ordinary org members, a much weaker bar
than Owner — is enough to serve a key at
`/.well-known/mcp-registry-auth`, exchange it for a JWT, and publish
under the org's entire `io.github.<org>/*` namespace.
- Reject `github.io` (and subdomains) at the shared
`ValidateDomainAndTimestamp` seam used by both DNS and HTTP exchange;
`io.github.*` publishers already have the GitHub method.

## Impact if unsolved

Any org member with write access to the org's GitHub Pages repo (or an
attacker compromising such a member) can mint MCP packages under the
org's trusted namespace — supply-chain poisoning of every downstream
user who installs `<org>`'s servers from the registry, bypassing the
deliberate Owner-only gate.

## Test plan

- New `TestValidateDomainAndTimestampRejectsGitHubPages`: rejects
`my-org.github.io` (incl. mixed case and subdomains), still allows
lookalikes (`github.io.evil-example.com`,
`my-org.github.io.example.com`) and ordinary domains.
- Full `internal/api/handlers/v0/auth` package passes.

Made with [Cursor](https://cursor.com)

Signed-off-by: SashaMIT <sash@ela.city>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 01:38:34 +03:00
Thomas Thomsen
d813d2b805
fix(validators): distinguish a missing package from a missing version on PyPI/NPM (#1411)
Fixes #553.

The PyPI and npm validators fetch the version-specific metadata endpoint
(`/pypi/{name}/{version}/json` and the npm equivalent) and report any
non-200 as `<pkg> not found`. That endpoint 404s for two different
reasons though: the package doesn't exist, or it exists and only the
*version* is missing (e.g. a release that hasn't propagated yet). So a
valid package reads as gone:

```diff
- PyPI package 'requests' not found (status: 404)
+ PyPI package 'requests' exists, but version '99.99.99' was not found (status: 404). A newly published release can take a moment to appear on PyPI. Wait and retry, or publish version '99.99.99' before registering it
```

On a version 404 it probes the package-level endpoint
(`/pypi/{name}/json`, `/{name}`) with a `HEAD` to tell the two apart,
and reports 429/5xx as transient rather than "not found". `HEAD` so it
reads a status without pulling the whole packument. The probe carries
its own 3s deadline: it only refines the error message, so a hung probe
must not stretch the validator past the ~10s-per-registry budget the
publish path assumes. Same probe-and-classify shape as the existing
cargo validator.

The fetch is split into `validatePyPIPackage` / `validateNPMPackage`
behind `export_test.go` so the branches are testable with `httptest`,
the same seam cargo uses.

### Testing
- [x] hermetic `httptest` for the status matrix (version-missing,
package-missing, 5xx, 429, inconclusive probe, positive path, scoped
npm); the mocks pin the expected method per endpoint (`GET` fetch,
`HEAD` probe)
- [x] hermetic deadline test: a hung probe is cut off at ~3s and
reported as inconclusive, not "not found"
- [x] existing live package tests still pass, now also hitting the HEAD
probe; on a live 429/5xx they now `t.Skip` as inconclusive instead of
flaking (deliberate: CI runs these against the real registries with no
short-mode gating)
- [x] `gofmt` / `go vet` / `golangci-lint` clean

Out of scope: auto-retry for the propagation race, and SSRF
redirect-pinning parity for the pypi/npm clients (pre-existing, shared
with nuget).
2026-07-12 15:24:34 -04:00
Radoslav Dimitrov
5d736c7c6c
test(api): remove placeholder CORS test superseded by #1436 (#1441)
Follow-up to #1436.

`internal/api/cors_test.go` was a placeholder: both `TestCORSHeaders`
and `TestCORSHeaderValues` construct `api.NewServer(...)`, discard it,
and then assert against a locally-built `http.HandlerFunc` that has no
CORS middleware — so they never exercised the real stack.
`TestCORSHeaderValues` only `t.Log`s; `TestCORSHeaders`'s only real
assertion (preflight status in {200,204}) ran against a bare handler
that always returns 200. The comments admit as much ("this is a bit of a
hack", "verified via integration tests").

That empty coverage is exactly why the missing `PATCH` in the CORS
`AllowedMethods` (fixed in #1436) went unnoticed.

#1436 added `TestCORSPreflightAllowedMethods`, which drives the real
wrapped handler through the new `Server.Handler()` accessor and asserts
every routed method — including PATCH — is allowed. With that in place
the placeholder carries no remaining coverage, so this PR removes it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 15:18:39 -04:00
Yusufhan Saçak
3a39b665ac
fix(api): allow PATCH in CORS so browsers can call the status endpoints (#1436)
The status endpoints (PATCH /v0/servers/{name}/status and the
per-version one) are served with PATCH, but the CORS middleware's
AllowedMethods lists GET/POST/PUT/DELETE/OPTIONS and never PATCH. So a
cross-origin browser preflight for those endpoints comes back without
the allow headers and the browser blocks the actual request. One-line
fix: add PATCH to the allowed methods.

While here, the existing CORS test turned out to be a placeholder that
never reaches the real wrapped handler (its own comments say so), which
is why this went unnoticed. Added a small Handler() accessor and a
proper preflight test that asserts each routed method including PATCH is
allowed.
2026-07-12 21:53:13 +03:00
Radoslav Dimitrov
9e762b03c7
docs(auth): document fine-grained PAT permission for org publishing (+ pagination-cap test) (#1435)
Follow-up to #1383, surfaced while validating that change end-to-end
against staging.

## Why

#1383 hardened org-namespace publishing to require Owner (`admin`) role,
read via `GET /user/memberships/orgs`. Validating on staging revealed a
gap: the docs only mention the classic `read:org` scope, but a
**fine-grained PAT** (GitHub's recommended token type) needs a
*different* permission to expose org membership. Without it, GitHub
returns no org memberships for the token, so the exchange silently
grants only the personal namespace — with no hint why org publishing
"doesn't work."

This is **not** a code bug (the behavior is fail-closed and correct
given GitHub's response); it's a documentation/UX gap.

## What

- **Docs** (`authentication.mdx`, `github-actions.mdx`): spell out both
token types —
  - Classic PAT → `read:org` scope
- Fine-grained PAT → **Organization permissions → Members → Read-only**
(the equivalent), noting a fine-grained PAT is bound to a single
resource owner.
  - Also clarified `read:user` is not required.
- **Test** (`github_at_test.go`): cover the `maxOrgMembershipPages`
pagination cap — when every page is full, the exchange **fails closed**
(no token) rather than returning a possibly-truncated org grant. This
path was previously untested.
- **Comment** (`github_at.go`): qualify the `X-GitHub-SSO` 403 check as
best-effort — we have not confirmed the memberships *list* endpoint
emits that header on SSO enforcement (it may just omit the org). The
check is safe either way, but the comment no longer overstates it as a
guarantee.

## Verification (live, against staging)

Confirmed by exchanging real tokens for `rdimitrov` on
`staging.registry.modelcontextprotocol.io`:

- **Classic `read:org` PAT** across 16 real org memberships → the JWT
granted exactly the 8 **admin** orgs + personal namespace, and **zero**
of the 8 **member** orgs (incl. `stacklok`, `modelcontextprotocol`) —
the core #1383 behavior, working.
- **Fine-grained PAT**: without Members:read → personal namespace only
(fail-closed); after adding Members:read → the org (Owner) namespace is
granted. This is what the new docs describe.

No runtime behavior change here beyond the added test; the docs and
comment are the substance.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:48:56 -04:00
Tadas Antanavicius
2bc37b1999
fix(auth): grant org namespace only to org Owners, not all members (#1383)
## The bug (scoped to `github-at`)

When you exchange a GitHub OAuth/PAT token for a registry JWT, the
handler granted `io.github.<org>/*` publish permission for **every**
organization you belong to. It derived that list from [`GET
/users/{username}/orgs`](https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-a-user),
which:

1. returns only **public** org memberships, and
2. carries **no role** — an org Owner and a brand-new member look
identical.

So membership was silently treated as publishing authority. Anyone who
is (or whose public membership lists them as) a member of `acme` could
publish — and, because publish is a pure namespace-glob with no
per-server ownership check, **overwrite** — any server under
`io.github.acme/*`.

## The fix

Switch to [`GET
/user/memberships/orgs`](https://docs.github.com/en/rest/orgs/members#list-organization-memberships-for-the-authenticated-user),
which returns the **authenticated caller's role** per org (and includes
private memberships), and grant the org namespace only when the role is
`admin` (org Owner).

- **Personal namespaces are unchanged** — you always get
`io.github.<your-username>/*`.
- **Org namespaces now require Owner.** GitHub has no org-level
"maintainer" role; org membership is either `admin` (Owner) or `member`.
We grant only on `admin`.
- **`read:org` is the only scope needed.** A minimal token without it
still authenticates and still publishes to your personal namespace; a
genuine missing-scope `403` from the memberships call is treated as "no
admin orgs" rather than a hard failure, so we don't push anyone to
over-scope a token. A `403`/`429` that is actually a rate limit
(`X-RateLimit-Remaining: 0` / `Retry-After`) fails closed instead, so a
throttle can't silently strip a real Owner's org grant. Critically, the
token needs **no repository scopes** — the registry never reads or
writes code, so an org Owner can hand CI a near-powerless `read:org`
token.

The authorization decision stays stateless and delegated to GitHub — no
new storage, no new endpoints, no schema changes — consistent with
[design principle #2](docs/design/design-principles.md) (minimal
operational burden).

### Containing the CI token

The registry-side change makes the org Owner the only one who can mint
org-namespace authority. But if that Owner drops the resulting PAT into
a plain repo secret, **every repository writer** can reach it (via
branch/tag pushes that run repo-controlled build/test code in the
privileged job) — quietly re-widening publish access beyond Owners.
External fork contributors still cannot reach it by default (no secrets
on fork PRs, no branch push), barring misconfigurations like
`pull_request_target`-with-checkout or self-hosted runners on public
repos.

`docs/modelcontextprotocol-io/github-actions.mdx` documents how to
contain that blast radius:

- store the token as a **GitHub Actions Environment secret** (the PAT
example uses `environment:`),
- restrict that environment to the default branch / release tags and
require PR reviews on it, so only reviewed, maintainer-approved code can
reach the token,
- optionally add a **required environment reviewer** for explicit
per-publish approval,
- and avoid the two footguns (`pull_request_target` checkout,
self-hosted runners on public repos).

This shrinks *who holds* the org credential; it does not change the fact
that an authorized publish can still overwrite a different server in the
same namespace until per-server publisher ownership exists (the
follow-up noted below).

## Why this shape, given the alternatives

[Pree's
writeup](https://github.com/modelcontextprotocol/registry/issues/982#issuecomment-3982959559)
([Notion analysis
doc](https://www.notion.so/MCP-Registry-Authorization-Model-Analysis-Issue-982-31736f79f74980bc90c6e53de1c564c5))
is the clearest framing of the problem and the option space, laying out
three broad solutions:

- **Approach A — GitHub App as the authority layer.** Strong model, but
its natural unit of authority is a *repo*, so it needs a repo→server
mapping to authorize a namespace. The safe, flexible version of that
mapping is registry-managed state — which collapses into Approach B.
It's also GitHub-only.
- **Approach B — a registry-managed grants table** (OIDC bootstrap, or
device-flow admin check). The most capable and provider-agnostic option,
and probably where v1 wants to go. But it introduces exactly the
"registry manages an authorization database" layer we wanted to avoid
taking on right now: revocation, sync, audit, migration.
- **Approach C — scoped bearer tokens (npm-style).** Good ergonomics,
but again implies issuance/storage the registry would own.

The two-property test (does the registry control the decision? is it
granted through an act requiring real authority?) is the right lens.
This change satisfies it **without** new state: the registry makes the
decision (it filters on role), and the authority is real (live GitHub
Owner role, re-checked on every short-lived token exchange).

**What this deliberately gives up**, so reviewers can weigh it honestly:

- **Owners are always in the loop.** Only org Owners can publish org
servers; there's no delegated "this team can publish this server" — that
needs Approach B's state. Reduced flexibility in exchange for zero new
infrastructure.
- **Blast radius within an org is unchanged.** An Owner (or Owner-scoped
CI token) can still overwrite *any* server under the org namespace,
because there's still no per-server publisher ownership. That
hijack-an-existing-server problem is real but **orthogonal** to this fix
and belongs in its own change (record publisher identity per server;
enforce it on update) — a follow-up to take alongside the OIDC hardening
below.
- **OIDC is untouched and remains coarser.** The `github-oidc` path
still grants the whole `io.github.<owner>/*` namespace to any workflow
with `id-token: write`, i.e. effectively anyone with write access to any
repo in the org, and it excludes non-Actions CI (Jenkins/GitLab can't
mint GitHub Actions OIDC). The admin-PAT path here is the
higher-assurance option for org publishing; OIDC stays as a
lower-assurance Actions convenience to be tightened separately.

## Revocation

The registry JWT is **5-minute, no-refresh** (per #264/#273), so loss of
Owner status propagates within minutes on the next token exchange, with
no revocation list to maintain.

## Tests

`go test ./internal/api/handlers/v0/auth/...` passes, including:

- member-role org is **not** granted; only admin orgs are
- a missing-scope `403` (no `read:org`) still yields the personal
namespace and no error
- an admin org on a **later page** of memberships is still granted
(pagination correctness)
- a `5xx` from the memberships call **fails closed** (no token issued)
rather than degrading to personal-only
- a rate-limit `403` (`X-RateLimit-Remaining: 0` / `Retry-After`)
**fails closed** — not mistaken for a missing-scope `403`
- existing org/name-validation tests updated to the new endpoint + role
shape

`gofmt`/`go vet`/`golangci-lint` (CI-pinned v2.11.4) clean; CI green on
all checks.

## Live verification

Confirmed end-to-end against real `api.github.com` (not just mocks) by
exercising this branch's `ExchangeToken` and decoding the `permissions`
claim of the returned JWT.

A dedicated org, **Tadasant Test Org** (login `Tadasant-Test-Org`), is
Owned by one account and has a second account joined as a plain
**member**. Both memberships are `active`, verified via the same
endpoint the fix uses (`GET /user/memberships/orgs/{org}`):

| Token (role) | `io.github.Tadasant-Test-Org/*` granted? | Other
patterns granted |
|---|---|---|
| Owner (`admin`) | ✅ yes | personal `io.github.<owner>/*` |
| member | ❌ no | personal `io.github.<member>/*` + the orgs that
account actually Owns |

The member case is the meaningful one: that membership is **active and
returned** by `GET /user/memberships/orgs`, and is excluded purely
because its role is `member`, not `admin`. Under the previous `GET
/users/{username}/orgs` behavior (membership = authority, no role), the
same active membership **would have granted** the org namespace. The
role filter is also selective rather than blanket-deny — the member
account still receives the namespaces of the orgs where it genuinely
holds Owner.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-07-10 17:05:47 -04:00
Radoslav Dimitrov
fecef4f9ed
fix: don't leak internal error detail in GET /v0/servers 500 response (#1338)
## Summary
Fixes the information-disclosure regression introduced by #1335 (item 1
of #1337).

`ListServersError` passed the raw `err` into
`huma.Error500InternalServerError(...)`. huma serializes extra error
args into the response body's `errors` array (`ErrorModel.Errors`,
`json:"errors,omitempty"`), so on the **public, unauthenticated** `GET
/v0/servers` endpoint a genuine DB failure echoed the internal wrapped
error from `internal/database/postgres.go` (`error iterating rows: <pgx
error>`) back to the client. pgx/pgconn errors can carry SQLSTATE codes,
constraint/table/column names, and internal messages (CWE-209).

## Change
- Drop `err` from the 500 call so only the generic message is returned;
keep `log.Printf` for the server-side detail — matching the sibling
handlers (`servers.go:190/220`, `edit.go`, `status.go`).
- Add a regression test
(`TestListServersError_realFailureDoesNotLeakDetail`) that marshals the
500 error and asserts the internal detail is absent from the
client-visible body. Existing tests only asserted status codes, which is
why the leak slipped through.

The 499 client-cancelled path is unchanged (the client has already
disconnected, so nothing is leaked there).

## Test plan
- [x] `go test ./internal/api/handlers/v0/... -run ListServersError`
(3/3 pass)
- [x] `go build ./...`, `go vet`, `golangci-lint` (no new findings in
changed files)

## Note
This leaves the non-security items in #1337 (correcting the `superfluous
WriteHeader` claim, `statusClientClosed` const reuse, broader
cancellation-guard scope) for separate follow-up. Closing #1337 here
since the only security-impacting item is addressed; reopen/split if
you'd prefer to track the nits independently.

Closes #1337

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 03:08:33 -04:00
CK
ef29673221
fix: client-cancelled GET /v0/servers returns 499 without error log (#1335)
Maps context.Canceled from ListServers to 499 and skips the error-level log on client disconnect.

Merged as-is per maintainer decision; known follow-up tracked in #1337 (500-path error-detail leak + WriteHeader claim correction + nits).
2026-06-05 02:59:47 -04:00
Radoslav Dimitrov
04623ed925
fix(validators): harden mcp-name matching (PyPI/NuGet anchoring, comment-form safe) + cargo follow-ups (#1331)
Registry-wide hardening of the `mcp-name:` ownership-token match, plus
the cargo follow-up fixes from the #1330 review — consolidated here per
maintainer request. Rebased onto current `main` (the merged #1330 cargo
commit drops out), so the diff is just the net-new work.

### Changes
- **PyPI/NuGet: boundary-anchored token match.** Replace
`strings.Contains(readme, "mcp-name: "+name)` with the shared
`containsMCPNameToken`, so a README declaring a longer name
(`…/widget-pro`) no longer satisfies an ownership claim for a shorter
prefix (`…/widget`). (Cargo already got this in #1330; NPM is unaffected
— it compares an exact metadata field.)
- **Matcher: treat the HTML comment close as a boundary.** `<!--
mcp-name: NAME-->` / `<!--mcp-name: NAME-->` (any spacing) validate
again — the documented hidden-comment form for PyPI/NuGet — while a
genuine longer name (`…/widget--pro`) still does not.
- **Cargo hardening (review of #1330):** pin scheme+port (not just host)
on the README fetch and redirects; a rate-limited/failed crate-version
existence probe now reports *transient* instead of "not found"; a 403
with the crate present no longer flatly asserts "no README".
- **Docs:** note the token must be followed by a boundary.

### ⚠️ Behavior change for PyPI/NuGet — read before merging
The match is **strictly stricter** (verified by fuzz, 2.3M execs: it can
only flip pass→fail, never fail→pass). After the comment-close fix, the
**only** forms that newly fail are unusual *inline* ones: the token
ending a sentence (`…/my-mcp.`) or glued to a trailing `/`. The
documented forms (own line, in `<!-- … -->`) are unaffected.

**Correcting an earlier claim:** this re-validates on **edits too**, not
just new versions — `edit.go → UpdateServer → ValidateUpdateRequest →
validateRegistryOwnership` runs the token check on any edit of a live
server (only delete-transitions skip it). So an existing PyPI/NuGet
server whose README uses one of the breaking inline forms would fail on
its next publish *or* edit. Given the v0.1 API-freeze posture, this
should land as a deliberate, noted change.

### Testing
`go build`, `vet`, `gofmt`, `golangci-lint` (prod files),
`check-schema`, `validate-examples` all clean. Hermetic + **live**
PyPI/NuGet/cargo suite passes. New: `TestCargoURLAllowed`, a
combined-fixture probe-429→transient case, comment-form matcher cases,
and `FuzzContainsMCPNameToken` (the strictly-stricter property, 2.3M
execs).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:28:41 -04:00
Radoslav Dimitrov
7a6d1bc30a
fix(cargo): harden README fetch, clarify status handling, strengthen tests (#1330)
Follow-up to #1207, addressing items from the cargo validator review.
**Scoped to cargo + shared helpers — no behavior change for existing
npm/pypi/nuget/oci/mcpb publishers.** Safe to merge and promote.

### Changes
**SSRF hardening of the two-call README retrieval**
- Pin the step-2 README URL to an allowed host (`static.crates.io` in
prod; the base host under test) *before* fetching, so a metadata
response can't steer the validator at an internal/attacker host.
- `CheckRedirect` policy pins every redirect hop to the same allowlist
(the initial URL being pinned isn't enough if an upstream 3xx is
followed).
- Cap the README body with `io.LimitReader` (5 MiB).

> Not publisher-exploitable today (the URL comes from crates.io), so
this is defense-in-depth — but it makes the "host is pinned" guarantee
hold in code rather than rely on crates.io's behavior.

**Clearer status handling** (previously any non-5xx/non-200 collapsed to
"not found")
- **429** → reported as transient/retryable, at both the metadata and
README steps.
- **403** → disambiguated via the crate-version endpoint:
genuinely-missing stays "not found"; *exists-but-no-README* gets an
actionable "add a README with `mcp-name` and republish" message (mirrors
the NuGet validator). This continues the 5xx-vs-403 diagnostic split
@P4ST4S started in the #1207 review — thanks!

**Shared `containsMCPNameToken` helper**
Boundary-anchored ownership-token match (prevents prefix confusion, e.g.
a README declaring `io.github.acme/widget-pro` satisfying a claim for
`io.github.acme/widget`), used here **by the cargo validator only**.
Adopting it in PyPI/NuGet is a separate follow-up because it's a
behavior change for those already-live registries.

**Tests & docs**
Hermetic positive `ServerNameFormats`; combined fixture gains a
version-existence endpoint + cases for 403-missing vs 403-no-README,
429, and prefix-confusion rejection; new foreign-README-host (SSRF)
test; internal test for the helper. Docs: list `crates.io` in the
registry-requirements list + the `Package` model doc.

### Testing
`go build`, `go vet`, full `./internal/validators/...` suite (incl. live
cargo positive), `make check-schema`, `validate-examples` all pass. No
new `golangci-lint` findings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:34:54 -04:00
Wolfe-James
685e354b2f
feat: add cargo (crates.io) as a package registry type (#1207)
> **Update — 2026-06-01 (commit 1f3be53)**
>
> - **All @P4ST4S review feedback addressed** — positive-path hermetic
mock + live anchor against `rust-faf-mcp v0.3.1`; 5xx routed as
transient; doc comment qualified. See the [close-out
comment](https://github.com/modelcontextprotocol/registry/pull/1207#issuecomment-4589711057)
for the per-item breakdown.
> - **Runtime-model question RESOLVED** — `package-types.mdx` documents
cargo and MCPB as both first-class Rust MCP distribution paths (cargo
for source-distributed, MCPB for prebuilt-binary). The "Open question"
section below was the original draft framing; the doc now ships the
resolution.
> - **Cargo-specific gotcha discovered and documented** — crates.io
strips HTML comments during README rendering, unlike PyPI/NuGet.
`package-types.mdx` now spells this out so cargo authors don't ship
invisible mcp-name tokens (which is exactly what happened to
`rust-faf-mcp v0.3.0`; fixed in v0.3.1).
> - **Stale reference removed in this update** — the "Out of scope" line
mentioning `docs/guides/publishing/publish-cargo.md` pointed at a path
that doesn't exist. The canonical per-type doc is
`docs/modelcontextprotocol-io/package-types.mdx`; the Cargo section has
been added there directly.
> - **Two real-world Rust MCP servers queued for day-one register**:
`rust-faf-mcp v0.3.1` (live, end-to-end validated by the new live test)
and `perfetto-mcp-rs` ([@0xZOne](https://github.com/0xZOne) in #1055).
> - **Tests:** 19/19 cargo tests pass (16 existing + 2 hermetic + 1 live
anchor). `make validate` clean. `golangci-lint` 0 issues.

---

Adds support for `registryType: cargo` so Rust MCP servers published to
crates.io can be registered through the
documented validation flow rather than the MCPB binary-packaging
workaround. Closes #1055.

  ## Motivation and Context

#1055 noted that crates.io has ~1,800 MCP-related packages with no
direct path into the registry — only the MCPB
binary-packaging workaround. This PR adds first-class support for cargo
as a package registry type: schema +
API surface, validator, integration tests, and a documented example. The
publishing guide
(`docs/guides/publishing/publish-cargo.md`) follows on this branch once
the runtime-model direction in
  **Additional context** is settled.

  ## How Has This Been Tested?

- **`make validate`** — clean. Schema-vs-openapi sync verified; 17/17
examples in `generic-server-json.md`
validate against the schema; `expectedServerJSONCount` bumped 16 → 17 to
match the new Cargo example.
- **`go test ./internal/validators/...`** — passes. 16 cargo validator
sub-cases across 4 test functions, run
  against real crates.io (~2.5s wall):
    - Input validation rejection paths
- Registry-baseURL rejection (4 variants: different host, trailing
slash, http-not-https, subdomain typo)
- Ownership validation against real crates (`serde`, `tokio`, `rand`) —
all correctly rejected for missing
  `mcp-name` token
- Server-name format variations (canonical `io.github.OWNER/REPO`,
multi-hyphen, underscore, numeric suffix)
- **Positive-path test** — gated on `rust-faf-mcp` v0.2.3+ being
published with `mcp-name:
io.github.Wolfe-Jam/rust-faf-mcp` in its README. Reserved as a TODO in
`cargo_test.go`; uncomments to become the
   live anchor once that publish lands.
- **Local `make test-unit`** — not run due to a Go toolchain version
quirk on my workstation (project
auto-downloads 1.26.0; my local `go` is 1.25.6, causing compile
mismatch). CI will exercise the full
  PostgreSQL-backed suite.

  ## Breaking Changes

None. This is an additive change — `cargo` joins the supported
`registryType` enum alongside
`npm`/`pypi`/`nuget`/`oci`/`mcpb`. Existing publishes continue to work
unchanged.

  ## Types of changes

  - [x] New feature (non-breaking change which adds functionality)
- [x] Documentation update (new Cargo example in
`generic-server-json.md`)

  ## Checklist

- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
  - [x] My code follows the repository's style guidelines
  - [x] New and existing tests pass locally
  - [x] I have added appropriate error handling
  - [x] I have added or updated documentation as needed

  ## Additional context

  ### Validator design — two-call retrieval on the documented API

`internal/validators/registries/cargo.go` mirrors the PyPI validator's
README-token approach (substring-match
`mcp-name: <serverName>`), with a two-call retrieval pattern to stay on
the documented public crates.io API:

1. `GET /api/v1/crates/{name}/{version}/readme` returns 200 OK with a
JSON pointer (`{"url":
"https://static.crates.io/readmes/.../...html"}`) — crates.io hands us
the URL rather than emitting a 302.
2. Follow the pointer to the rendered HTML; substring-match for the
token.

The two-call pattern stays on the documented public surface. The CDN URL
layout is observed-stable, but treating
it as the entry point would mean depending on an undocumented path. With
two calls, crates.io controls where
the README lives — if they move it, the metadata endpoint hands us the
new URL.

Missing crates and missing versions surface as **403** from the CDN
(S3's default for missing keys), not 404.
The validator treats any non-200 as "not found" and surfaces the actual
status code in the error message.

  ### Open question — runtime model

Cargo's runtime model is genuinely different from npm/PyPI/NuGet. `cargo
install` is one-time (binary lands on
PATH at `~/.cargo/bin`), not per-invocation like `npx`, `uvx`, or `dnx`
(the new one in .NET 10 SDK Preview 6+).
The new Cargo example in `generic-server-json.md` annotates this
honestly and omits `runtimeHint`.

If a different framing is preferred — e.g. recommending **MCPB**
(prebuilt binary distribution via GitHub
Releases) as the primary path for Rust MCP servers instead of cargo —
happy to adjust the documentation
accordingly. The schema + validator code in this PR is **additive** and
doesn't force a recommendation either
way; a Rust author who chooses cargo can use it, and one who prefers
MCPB still can.

  ### Out of scope (deferred)

- **`docs/guides/publishing/publish-cargo.md`** — follows on this branch
once the runtime-model direction above
  is settled.
- **Publisher CLI `Cargo.toml` autodetect** in
`cmd/publisher/commands/init.go` — separate concern, separate PR.
- **Positive-path validator test** — gated on `rust-faf-mcp` v0.2.3+
publishing as noted in *How Has This Been
  Tested?*.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-06-03 10:56:43 -04:00
Radoslav Dimitrov
35b20719ee
fix(validators/oci): fail closed on upstream rate limiting (#1281)
## Summary

When the OCI validator's anonymous manifest fetch returns HTTP 429,
return a retryable error instead of `nil`. This aligns OCI with the
fail-closed behavior of the npm / PyPI / NuGet / MCPB validators, and
ensures the `io.modelcontextprotocol.server.name` label-match always
runs before a publish is accepted.

## Test plan

- [x] `go test ./internal/validators/registries/ -run TestValidateOCI`
passes locally
- [x] `gofmt` + `golangci-lint` clean on the changed file
- [ ] CI green

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:59:59 -04:00
Radoslav Dimitrov
4e147072f0
metrics: stop counting client-cancelled requests as server errors (#1255)
## Summary

- Remap recorded status to `499` (NGINX-style "client closed") when
`ctx.Context().Err()` is `context.Canceled` after the handler returns
- Skip the `http_errors_total` increment for `499`s so the availability
metric reflects server-visible errors only

## Why

Under scraper-driven load, clients with short timeouts paginate
`/v0.1/servers`, give up while PG is busy, and close the TCP connection.
The handler's DB iteration returns `context canceled`, the handler
converts that to `huma.Error500InternalServerError`, and tries to write
a response to the now-closed socket. NGINX records `499` (client closed)
and never delivers anything to the client — but the registry's
middleware records `status_code=500` and bumps
`mcp_registry_http_errors_total`.

That's what's been firing the `Availability dropped below 95%` alert
during the daily 17:00 UTC bursts. Recent prod numbers from one pod
(~19h uptime):

```
mcp_registry_http_errors_total{path="/v0.1/servers", status_code="500"} 7396
mcp_registry_http_errors_total{path="/v0.1/servers/{serverName}/versions", status_code="500"} 283
mcp_registry_http_errors_total{path="/v0/servers", status_code="500"} 352
```

…against zero 5xx in NGINX ingress logs over the same window. The alert
was correct *given the data it had*; the data was misclassifying client
cancellations as server errors.

## What this does

In `internal/api/router/router.go` (the metric middleware): after
`next(ctx)`, check `ctx.Context().Err()`. If it's `context.Canceled`,
override `statusCode` to `499` and skip the error counter. Requests
counter and duration histogram still record so the cancellation rate
stays visible.

`context.DeadlineExceeded` is intentionally **not** remapped — that
would indicate a server-side timeout if we ever add per-request
deadlines, and should still count as a server error.

## What this does *not* do

- Doesn't reduce the underlying load (ServiceNow + notion catalog
scrapers driving the daily peak — separate work, see the `updated_since`
outreach we're doing).
- Doesn't change handler behaviour — handlers still call
`huma.Error500…` on `context.Canceled`. We could short-circuit earlier
(return without writing) to save the work, but that's a bigger refactor
and the metric fix unblocks the alert immediately.

## Companion change (no code, just a Grafana annotation tweak)

The `Availability dropped below 95%` alert (UID `bexrc60etvhmoa`)
currently has the annotation:

> No. of 5xx are increased from acceptable limit i.e. 5% of total
throughput

The query is on `mcp_registry_http_errors_total`, not 5xx specifically.
After this change the count is meaningful again, but the wording is
still misleading. Suggest updating to:

> Internal error rate exceeded 5% of total throughput

(no PromQL change required.)

## Test plan

- [x] `go test ./internal/api/router/...` — new test passes
- [x] `golangci-lint run ./internal/...` — clean
- [ ] After merge, watch a daily 17:00 UTC burst —
`mcp_registry_http_errors_total{status_code="500",
path="/v0.1/servers"}` should stay flat while
`mcp_registry_http_requests_total{status_code="499",
path="/v0.1/servers"}` grows. Availability alert should not fire on
cancellation-only bursts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 05:58:05 -04:00
Radoslav Dimitrov
f5f40bd980
fix(auth/http): extend SSRF blocklist to IPv6 6to4/NAT64/site-local prefixes (#1250)
The HTTP namespace-verification fetcher's `isBlockedIP` composed its
blocklist from Go stdlib's `Is*` helpers plus a manual CGNAT range. That
covers IPv4 cloud metadata, RFC1918, ULA, and link-local — but Go's
classification helpers don't recognise IPv6 prefix families that embed
or tunnel to arbitrary IPv4:

  - 2002::/16       RFC 3056 6to4 — bits 16-47 are an arbitrary IPv4
  - 64:ff9b::/96    RFC 6052 NAT64 well-known — low 32 bits are IPv4
  - 64:ff9b:1::/48  RFC 8215 NAT64 local-use — same IPv4-embedding shape
  - fec0::/10       RFC 3879 site-local (deprecated, still routed by
                    some stacks)

On hosts with 6to4 routing or a NAT64 next-hop (the default on
IPv6-only GKE node pools, AWS IPv6-only EC2, Azure IPv6 VMs with
NAT64, and many corporate DNS64/NAT64 networks), an attacker who
controls DNS for a publisher domain can cause the registry to dial
`169.254.169.254`, `10.0.0.0/8`, etc. via the IPv6 wrapper. Same
SSRF class as GHSA-56c3-vfp2-5qqj on n8n-mcp.

Define the four prefixes via a new `mustCIDR` helper (panic on
malformed CIDR at init so a typo can't fail-open silently) and iterate
them in `isBlockedIP` after the stdlib gates. Refactor the existing
CGNAT IIFE to use the same helper for consistency. IPv4-mapped IPv6
(::ffff:0:0/96) needs no special-casing: Go's `Is*` helpers honour the
`To4()` fast-path, so e.g. `::ffff:10.0.0.1` is already classified as
`IsPrivate`.

Adds 17 test cases to `TestIsBlockedIP` covering each new prefix
family, IPv4-mapped IPv6 sanity checks, and just-outside-the-range
positive controls (`2001::1`, `2003::1`, `64:ff9c::1`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 09:41:23 -04:00
Radoslav Dimitrov
78b7bbde07
fix(validators): reject HTML metacharacters and whitespace in websiteUrl (#1249)
`validateWebsiteURL` only checked that the URL parses, is absolute, and
uses the `https` scheme. It accepted literal `"`, `'`, `<`, `>`, and
ASCII space — none of which are valid in a URI per RFC 3986, and all of
which broke rendering when the value flowed into the catalogue UI's
`<a href="...">` template.

Add a `strings.IndexAny` check after the scheme check that rejects any
of these characters with a clear `website-url-invalid-characters` issue
that points at the offending byte position. Control characters (`\t`,
`\n`, `\r`) are also covered: they're caught one step earlier by Go's
`url.Parse`, but the bytes are listed in the rejection set so the
behaviour is explicit and survives any future relaxation of `url.Parse`.
Already-percent-encoded URLs (e.g. `?q=hello%20world`) continue to
validate cleanly.

Adds table-driven cases for `"`, `'`, `<>`, space, newline, and a
positive case for percent-encoded special characters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 09:26:50 -04:00
Radoslav Dimitrov
fbfd63ced8
fix(ui): encode publisher-controlled URLs for href attribute context (#1248)
The catalogue page interpolates publisher-supplied URL fields
(`server.repository.url`, `server.websiteUrl`) inside `href="..."`
attributes via `innerHTML`, escaping them with a
`textContent`→`innerHTML`
helper. Per the HTML5 fragment-serialisation algorithm, that round-trip
encodes only `&`, `<`, `>`, and U+00A0 inside text nodes — it does not
encode `"` or `'`. The helper is therefore safe in element-text
contexts (where it is used for `name`, `version`, `description`, etc.)
but unsafe inside an attribute value.

Add an attribute-safe `escapeAttr` helper that follows the OWASP
attribute-encoding rule (also encodes `"` and `'`) and use it for the
two `href` interpolations. Element-text usages keep the existing
`escapeHtml` and remain unchanged. A short comment on each helper
documents the contexts they cover so future fields are wired up
correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 09:25:41 -04:00
Radoslav Dimitrov
7a4216c528
refactor: rename readBody to readErrorBody and cap body size (#1246)
## Summary

Follow-up to #1241. Two small tweaks to the `readBody` helper:

- **Rename `readBody` → `readErrorBody`.** The helper is only ever
called on the error path (after a non-2xx status check), where we want a
small string for diagnostics. The generic name made it look reusable on
the success path, where streaming JSON decoding is the right approach.
The new name pins the intended use down.
- **Cap the read with `io.LimitReader` (8 KiB).** Defense in depth —
error diagnostics never need more than a few KB, and we shouldn't buffer
an arbitrarily large body from an upstream just to format an error
message. 8 KiB is well above any realistic GitHub API or JWKS error
response.

No behavior change on the happy path. On the unhappy path, error
messages are now bounded.

## Test plan

- [x] `go build ./...`
- [x] `go test ./internal/api/handlers/v0/auth/...`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-04 07:09:57 -04:00
Mesut Oezdil
6f9bd6661c
refactor: extract readBody helper to remove duplicate io.ReadAll calls (#1241)
The same pattern for reading an error response body appeared in three
places across the auth handlers:

- `internal/api/handlers/v0/auth/github_at.go` (twice)
- `internal/api/handlers/v0/auth/github_oidc.go` (once)

A shared `readBody` helper function added to `common.go` now handles
this in one place.
The `io` import is removed from both files since it is no longer used
directly there.

---------

Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-05-04 06:57:02 -04:00
Radoslav Dimitrov
dfaa8f3b82
auth/oidc: test slice-typed extra claims comparison (#1245)
## Summary
Follow-up to #1238 (closes #988). Adds direct unit-test coverage for the
`claimMatches` helper, which was the actual change in #1238.

`claimMatches` and `toAnySlice` are unexported, so the tests live in
`oidc_internal_test.go` (`package auth`) — same pattern as
`http_internal_test.go`. Cases covered:

- scalar/scalar match and mismatch
- scalar actual against array expected (both directions of overlap)
- array actual against scalar expected
- **array/array** — pre-#1238 this combination panicked with `comparing
uncomparable type []interface {}`
- `[]string` typed slice (exercises the typed-slice branch of
`toAnySlice`, which `[]any` skips)
- non-string scalars (int, bool)
- empty actual array

Test values use single letters (`x`, `y`, `p`, `q`) rather than role
labels — only the relationships matter, and short tokens keep `goconst`
quiet.

## Test plan
- [x] `go test ./internal/api/handlers/v0/auth/...` — all 13 subtests
pass
- [x] `golangci-lint run ./internal/api/handlers/v0/auth/...` — no new
issues vs main
2026-05-04 06:52:12 -04:00
Sai Asish Y
811046c4ce
auth/oidc: support slice-typed extra claims via overlap match (#1238)
Closes #988. Direct `actualValue != expectedValue` fails when the token
claim is an array (groups, roles, scp, aud). Switch to a slice-aware
any-of overlap so OIDC list claims are validated correctly.
2026-05-04 06:24:34 -04:00
Radoslav Dimitrov
3f89fc2b1f
auth: bind GitHub OIDC token exchange to a per-deployment audience (#1229)
## What this does

The publisher CLI now derives the OIDC audience to request from the
`--registry` URL instead of using a fixed constant, and the server
validates the token's `aud` claim against a new per-deployment env var
(`MCP_REGISTRY_GITHUB_OIDC_AUDIENCE`).

Production sets this env var to
`https://registry.modelcontextprotocol.io` (and per-environment URL
elsewhere), so the deployment only accepts tokens that were requested
for it.

## Compatibility

This is a breaking change for older `mcp-publisher` binaries that still
request the legacy hardcoded audience. After this rolls to a deployment,
users on old `mcp-publisher` will see a clear `invalid audience:
expected …, got […]` error and need to upgrade. Users following the
documented `releases/latest` install pattern get the new binary
automatically.

## Test plan

- [ ] CI green
- [ ] Run a synthetic publish from a real GitHub Actions workflow
against staging after deploy
- [ ] Confirm an older `mcp-publisher` against the same staging deploy
returns the expected `invalid audience` error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 20:27:26 -04:00
Radoslav Dimitrov
1201cbd82b
security: fix open redirect and add small hardening (#1227)
## What this does

Fixes an open redirect plus a handful of small, mostly-invisible
hardening changes around it.

## What's in it

- **The fix**: trailing-slash redirects no longer let `//evil.com/`
redirect off-host.
- **SSRF block** on the DNS/HTTP namespace verification — refuses to
dial loopback, RFC1918, link-local, multicast, CGNAT, or
IP-literal/single-label \"domains\".
- **Slow-client guard**: `ReadTimeout` and `IdleTimeout` on the HTTP
server (no `WriteTimeout` — would risk cutting off legitimate
multi-package publishes).
- **Defence-in-depth headers** on the UI (`/`) and 404 responses.
- **5xx responses** stop leaking raw DB error text to clients (logged
server-side instead).
- **Search filter** stops treating `%` and `_` as wildcards.
- **Validators**: PyPI/NuGet identifiers URL-escaped before fetching;
MCPB no longer follows redirects.
- **Misc**: `Version` field bounded to 255 chars; logs quote user input;
`BlockedNamespaces` denylist now catches subdomain claimants.

Each change is its own commit so they can be reviewed (or reverted)
independently.

## Test plan

- [ ] CI green
- [ ] Synthetic publish in staging for each package type (npm, pypi,
nuget, oci, mcpb) — confirms validator changes don't regress
- [ ] Quick browser check of `/` after deploy to confirm UI still loads
with the new CSP
- [ ] `curl https://<host>//evil.com/` — confirm the open redirect is
closed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:14:00 -04:00
Radoslav Dimitrov
736d376858
fix(db): raise pgxpool MaxConns + PG max_connections + set PG resources (#1221)
## Summary

Follow-up to #1215 and #1220. Both of those addressed *individual* slow
queries; this PR addresses *concurrency* — under today's scraper load
the cursor query is fast on average (mean 42ms) but the connection pool
saturates and queue depth blows up at the Go HTTP layer.

## Diagnosis

`pg_stat_statements` (added in #1215) made this possible to see:

```
max_ms   mean_ms  calls    template
10,755     41.8   94,009   plain cursor pagination, no filter
 7,277    214.7   14,770   ILIKE substring filter
 7,638    330.8    7,656   ILIKE substring + is_latest filter
```

Mean times are healthy. But `max_exec_time` of 7–10s on the cursor
query, combined with sustained ~15 req/s from scrapers (ServiceNow's
148.139.x.x range, anonymous `node` user-agent, etc) saturates
`MaxConns=30 × 2 pods = 60`. New requests queue at the Go HTTP layer;
nginx-side p99 hits 35s; eventually scrapers time out at 60s, retry, and
amplify the queue.

Today's two ongoing alerts are both this pattern:
- 17:35–17:40 UTC `Availability dropped below 95%` — 4,526 GET requests
in 5 min, hundreds of 504s
- 18:17 UTC `Publish Endpoint Latency` re-fire — same scraper
concurrency dragging the publish path

Critically, the symptoms today were also visible during yesterday's
incident, but yesterday's broken cursor (#1215) was the dominant cause.
After #1215 the cursor is fast individually; concurrency now becomes the
next bottleneck.

## Changes

### pgxpool (`internal/database/postgres.go`)

| Setting | Before | After |
|---------|-------:|------:|
| `MaxConns` | 30 | **60** per pod |
| `MinConns` | 5 | **10** per pod |
| `MaxConnIdleTime` | 30 min | (unchanged) |
| `MaxConnLifetime` | 2 h | (unchanged) |

2 pods × 60 = 120 total app connections. Cuts queue depth roughly in
half at current scraper load.

### PG cluster (`deploy/pkg/k8s/postgres.go`)

- `max_connections: 100 → 200` — required to support the larger pool.
120 (app) + ~10 (PG internals: autovacuum, replication, admin) + 70
headroom.
- Added explicit `resources:` block — previously the pod had no resource
limits, making node-level OOM behaviour unpredictable.

| | Request | Limit |
|---|--------:|------:|
| memory | 512Mi | **4Gi** |
| cpu | 200m | 1500m |

## Resource budget

| Node | Now | After |
|------|----:|------:|
| dy89 (PG node) | 39% mem (~2.4 GiB / 6 GiB) | ~65% mem worst-case
(~3.9 GiB) |
| 2yxm | 36% mem | unchanged |

Both nodes well within capacity. CPU usage <50% on both, plenty of
headroom.

PG worst-case memory math:
- 200 conns × ~15 MB per backend = 3 GiB
- + `shared_buffers` 128 MiB
- + `maintenance_work_mem`, OS overhead, etc.
- ≈ 3–4 GiB total

## Deployment caveat

`max_connections` is a postmaster-level setting → CNPG triggers a PG
restart on the change. Same in-pod restart shape as the
pg_stat_statements deploy yesterday — registry pods see ~30s of DB
unavailability, covered by v1.7.1's retry-with-backoff (8 attempts, 1→8s
capped). One registry pod may bounce once before recovering, like
yesterday.

**Time the merge for a low-traffic UTC window.**

Order of operations matters within the deploy itself:
1. Pulumi applies the new CNPG spec → PG restarts with
`max_connections=200`
2. Rolling deploy of registry pods picks up `MaxConns=60` config
3. New conns are accepted up to 200 limit

Pulumi's standard ordering does step 1 before step 2 in this scenario
(Pulumi resource graph: CNPG cluster precedes Deployment). If for any
reason it doesn't, the worst case is `too many connections` errors
during a small window — Self-correcting once the rollout completes.

## Test plan

- [x] `go build ./...` clean for app + deploy
- [x] `make lint` clean
- [x] `go test -race ./internal/database/...` green
- [ ] On merge: staging deploy applies the spec change cleanly; PG
restarts; pool reaches 60 max
- [ ] On prod deploy: same; verify no `too many connections` errors
during the rollout window
- [ ] After deploy: query `SHOW max_connections` returns 200; query
pg_stat_statements after a scraper burst, confirm `max_exec_time` for
cursor query no longer hits 10s

## Out of scope

- **ILIKE substring search** (`server_name ILIKE '%foo%'`) is
unindexable. Three pg_stat_statements variants run with means 141–333ms
and max 4–7s. Worth replacing with a `pg_trgm` GIN index or full-text
search in a separate PR.
- **Per-IP nginx rate limiting** — defends against scraper retry storms
regardless of pool size.
- **`Cache-Control` on `/v0/servers`** — let nginx absorb
scraper-repeated cursors.
- **Pre-existing `superfluous WriteHeader` warnings** — separate Huma
framework issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:21:24 -04:00
Radoslav Dimitrov
54cbe2473c
fix(list): use JSONB containment for RemoteURL filter (GIN-indexable) (#1220)
## Summary

Same shape of bug as #1215's cursor fix, different filter. The RemoteURL
filter built its WHERE predicate using `jsonb_array_elements` + `->>`
extraction, which the planner can't translate into a GIN search.
Replaced with a JSONB containment predicate `@>` that uses
`idx_servers_json_remotes`.

## Diagnosis (from the per-phase publish slog #1215 added)

Today's first publish-latency alert at 17:08 UTC pointed straight at
this:

```
publish complete server_name=dev.storage/mcp version=1.10.1 total_ms=14822
  validate_ms=0 lock_ms=238 remotes_ms=10980 version_checks_ms=535 unmark_ms=3014 create_ms=53
```

`remotes_ms=10,980` on a server with **one** remote URL pointed at
`validateNoDuplicateRemoteURLs`, which runs the RemoteURL filter against
the database. EXPLAIN ANALYZE on prod confirmed:

```
Filter: (status != 'deleted') AND EXISTS(SubPlan 1)
Rows Removed by Filter: 21,092       ← scans the whole table
Buffers: shared hit=20,028
Execution Time: 10,005 ms
```

## Fix

`internal/database/postgres.go:99` rewritten:

```sql
-- before  (planner can't use GIN index for this)
EXISTS (SELECT 1 FROM jsonb_array_elements(value->'remotes') AS remote
        WHERE remote->>'url' = $1)

-- after   (GIN-indexable)
value -> 'remotes' @> jsonb_build_array(jsonb_build_object('url', $1::text))
```

Semantically equivalent for our schema (`url` is always a string, NOT
NULL).

## Local benchmark — 20K rows, prod-shaped JSONB (~1KB/row)

| Form | Buffer hits | Index used | Time |
|------|------------:|------------|-----:|
| OLD | 40,398 | none — full scan | 17.0 ms |
| NEW | **294** | **`idx_servers_json_remotes` GIN bitmap** | **1.6 ms**
|

Maps to prod's 10,005 ms cold-cache observation — same shape of speedup
we got from the cursor fix.

## Scope honesty

This PR addresses the **publish slowness path**
(`validateNoDuplicateRemoteURLs` in the publish transaction). It
directly resolves today's 17:08 UTC `Publish Endpoint Latency` alert.

It does **not** address:

- Today's 17:35–17:40 UTC `Availability dropped below 95%` alert.
Per-investigation: only 4 publishes happened in the entire 17:00–17:50
window and zero during the burst minutes — so my initial "concurrent
slow publishes starved the pool" hypothesis was wrong. The actual cause
was scraper-driven concurrency (4,500+ requests in 5 min) on plain
cursor pagination + ILIKE substring search, where individual queries are
fast (mean 42 ms) but tail latency under concurrency reaches 10 s
server-side and 20–35 s nginx-level due to pool queueing.
- The 18:17 UTC `Publish Endpoint Latency` re-fire (same scraper-driven
concurrency).

A follow-up PR will raise pool size + PG `max_connections` + add
explicit PG resource limits to absorb concurrency without queue blowup.
That fix has a different shape (config) and a different risk profile (PG
restart for `max_connections`); kept separate so this surgical SQL
change can ship cleanly.

## Test plan

- [x] `go build ./...` clean
- [x] `make lint` clean
- [x] `go test -race ./internal/database/... ./internal/service/...`
green — existing `TestPostgreSQL_ListServers/filter by remote URL`
covers semantic correctness
- [x] EXPLAIN ANALYZE comparison locally on 20K rows + prod-shaped JSONB
- [ ] After merge + deploy: verify on prod via `EXPLAIN ANALYZE`;
confirm `remotes_ms` in `publish complete` slog drops to single-digit ms

## Out of scope (separate follow-ups)

- pgxpool `MaxConns` / `MinConns` bump + PG `max_connections` raise + PG
`resources` limits — addresses the cursor-tail and ILIKE-search
concurrency that this PR doesn't touch
- Per-IP rate limiting at nginx — defends against scraper retry storms
regardless of query speed
- `Cache-Control` headers on `/v0/servers` — let nginx absorb scraper
repeats
- Pre-existing `superfluous WriteHeader` warnings — separate Huma
framework issue
- ILIKE substring search on `server_name` is unindexable — could be
replaced with `pg_trgm` GIN index or full-text search

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:03:21 -04:00
Radoslav Dimitrov
27af672c5e
fix(list): use row-constructor cursor + restore per-phase publish timings + enable pg_stat_statements (#1215)
## Summary

Three changes from the 2026-04-27 incident:

1. **Cursor SQL fix** — `addCursorCondition` rewritten to use a
row-constructor comparison `(server_name, version) > ($1, $2)` instead
of the OR-decomposed form. Postgres can index-seek on the row
constructor; it can't on the OR form, which scanned from the start of
the index and filtered rows before the cursor. Cost grew linearly with
cursor depth.
2. **Per-phase publish timings restored** — `createServerInTransaction`
was simplified during #1211 review to log only `validate_ms`. The
incident showed publishes spending 50+ s in `acquire_lock` /
`version_checks` / `db_create` while `validate_ms` reported a few
hundred ms — the diagnostic signal was hidden. Restored timings for
every phase, refactored into a small `runPhase` helper.
3. **`pg_stat_statements` enabled** in the CNPG cluster spec. We had no
aggregate query visibility during the incident — could only EXPLAIN
queries we happened to suspect.

## Cursor fix evidence

Local benchmark, 100K rows, cold cache:

| Form | Rows filtered | Buffer reads | Time |
|------|--------------:|-------------:|-----:|
| OLD | 80,001 | 7,679 | 31.6 ms |
| NEW | 2 | 1 | **0.05 ms** |

Maps to prod's measured 8,911 buffer hits → 760 ms. End-to-end API:
`/v0/servers?limit=100&cursor=…` returns in 4–7 ms regardless of depth.

Risk check: confirmed `(server_name, version)` index exists in that
column order (both `servers_pkey` and `idx_servers_name_version`), and
both columns are `NOT NULL` so row-constructor comparison can't silently
drop rows.

## Tests

- New
`TestPostgreSQL_PerformanceScenarios/compound_cursor_across_versions_of_same_server`
pins multi-version pagination semantics. The existing cursor tests only
exercised the fallback (single-component cursor) and degenerate (one
version per server) cases.
- `make lint` clean, `go test -race ./internal/... ./cmd/...` green.

## Deployment

The cursor + slog changes are zero-downtime. The CNPG spec change
triggers a brief PG restart (single-instance cluster). After the
restart, run once on prod:

```bash
kubectl exec -i registry-pg-1 -c postgres \
  --context gke_mcp-registry-prod_us-central1-b_mcp-registry-prod \
  -- psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements"
```

Time the merge for a low-traffic UTC window. v1.7.1's DB
retry-with-backoff covers the brief PG restart.

## Out of scope

`MaxConns` bump, per-IP rate limiting at nginx, response caching, the
pre-existing `superfluous WriteHeader` warnings — separate follow-ups.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:35:59 -04:00
Radoslav Dimitrov
b24d6c7657
fix: harden registry availability and add publish phase timings (#1211)
## Summary

Daily ~17:00 UTC `Publish Endpoint Latency` and `Availability < 95%`
alerts are caused by both registry pods exit-0'ing within seconds of
each other and racing with the still-recovering `registry-pg-rw`
endpoint on restart. Separately, the default compute SA had no project
IAM roles, so `fluentbit-gke` and the GMP collector were silently
dropping every log line and time series — which is why none of this was
visible in Cloud Logging.

All Kubernetes/IAM changes apply uniformly to staging and prod.

## Changes

- **GCP IAM** (`deploy/pkg/providers/gcp/provider.go`) — grant the
default compute SA `logging.logWriter`, `monitoring.metricWriter`,
`monitoring.viewer`, and `stackdriver.resourceMetadata.writer`. Already
applied live; this just encodes it in Pulumi.
- **Pod resilience** (`deploy/pkg/k8s/registry.go`) —
- `PodDisruptionBudget` (`minAvailable: 1`) prevents a synchronized
voluntary disruption.
- `TopologySpreadConstraints` (`ScheduleAnyway`, hostname) keeps the PDB
from deadlocking node drains when both pods co-locate.
- `StartupProbe` (30 × 5s = 150s) protects the DB-retry budget from the
liveness probe.
  - Memory bump 128→256Mi request, 256→512Mi limit.
- **Startup retry** (`cmd/registry/main.go`) — replace silent `return`
on initial DB-ping failure with bounded retry-with-backoff (8 attempts,
1→8s capped, 10s per-attempt). A brief endpoint flap no longer cascades
into a restart loop.
- **Diagnostic log** (`internal/service/registry_service.go`) — emit
`validate_ms` on each publish (success and failure). Validate fans out
to npm/PyPI/OCI with 10s per-host timeouts and is the most likely
contributor to publish latency. Total request duration is already
covered by the existing `http.request.duration` histogram.

## Calibration

The IAM, PDB, topology-spread, startup-probe, and DB-retry pieces are
mitigations — they reduce blast radius regardless of root cause. The
`validate_ms` log is the diagnostic for identifying the trigger when the
alert fires next. Single-PG SPOF (`deploy/pkg/k8s/postgres.go:57`)
remains a follow-up; today's evidence shows PG was a victim, not the
cause.

## Test plan

- [x] `go build ./...` and `go vet ./...` clean for both modules
- [x] `make lint` clean (12 pre-existing issues under `deploy/` are not
in `make lint`)
- [x] `go test -race -count=1 ./internal/... ./cmd/...` green
- [x] IAM applied live; `fluentbit-gke` and GMP no longer hitting
`IAM_PERMISSION_DENIED`
- [ ] After deploy, verify the next 17:00 UTC firing surfaces
`validate_ms` in Cloud Logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:09:50 -04:00
Radoslav Dimitrov
0cb1f6bf52
fix: recalculate isLatest after status changes (#1081) (#1201)
## Summary

Fixes #1081. When the version flagged `is_latest` was soft-deleted, no
other version was promoted, and because `GetCurrentLatestVersion` didn't
filter deleted rows, subsequent publishes with a lower semver never
became latest — leaving `/v0/servers/{name}/versions/latest` permanently
404.

This PR has two parts:

### 1. Code fix — recalculate `is_latest` on every status change

- Recalculate `is_latest` after every status mutation
(`UpdateServerStatus`, `UpdateAllVersionsStatus`, and the inline status
change in `UpdateServer`), in the same transaction that already holds
the per-server advisory lock.
- Winner selection reuses the existing `CompareVersions` logic: highest
non-deleted version gets the flag; if every version is deleted, the
highest deleted version keeps it so admin lookups via
`includeDeleted=true` still resolve the server (important for restore
flows).
- New `SetLatestVersion` DB primitive issues two statements (clear old,
set new) — `idx_unique_latest_per_server` is a non-deferrable unique
partial index, so a single `UPDATE` that flips two rows trips the
constraint.

### 2. One-shot migration — heal servers already affected (no manual SQL
needed)

`014_heal_is_latest.sql` promotes the highest non-deleted version on
every server whose `is_latest` flag is on a deleted row (or absent
entirely). Picker parses `major.minor.patch` numerically and falls back
to `published_at` for non-semver or as a tiebreaker. Heals
`io.github.containers/kubernetes-mcp-server` to `0.0.61` automatically
and catches any other servers silently affected by the original bug,
without hardcoded version numbers.

Picker behavior vs. `CompareVersions`:
- Standard `M.m.p` semver: matches exactly, including the
backport-after-delete case (e.g. published 1.0.1 then 1.0.0 hotfix after
deleting 2.0.0 → picks 1.0.1, not 1.0.0).
- Non-semver: falls through to `published_at DESC`. Reasonable.
- Prereleases (`1.0.0-alpha` vs `1.0.0`): both parse to `(1,0,0)`,
tiebreak by `published_at`. Diverges from `CompareVersions` (which
always says `1.0.0 > 1.0.0-alpha`). Edge case requires (a) server hit by
this bug AND (b) prereleases at the top of the version list. Acceptable;
the publisher can flip it with one `mcp-publisher status` call (which
now triggers proper recalc thanks to part 1).

## Verification

End-to-end curl repro of the exact scenario from the issue, run against
both `main` and this branch:

**Before (main):**
```
publish 1.0.0 → 200
publish 0.0.59 → 200
PATCH .../1.0.0/status {deleted} → 200
GET /versions/latest → 404            ← bug
publish 0.0.60 → 200
GET /versions/latest → 404            ← bug persists, 0.0.60 has isLatest=False
```

**After (this branch):**
```
publish 1.0.0 → 200
publish 0.0.59 → 200
PATCH .../1.0.0/status {deleted} → 200
GET /versions/latest → 200 (0.0.59, isLatest=true)
publish 0.0.60 → 200
GET /versions/latest → 200 (0.0.60, isLatest=true)
```

## Test plan

- [x] Service tests covering: soft-delete latest → next highest active
promoted; the exact #1081 repro end-to-end through the service;
all-versions-deleted still keeps highest addressable for admin;
restore-to-higher-version reclaims latest.
- [x] Migration tests covering: kubernetes-mcp-server scenario (heals to
highest semver); backport scenario (semver beats most-recent-published);
no-is_latest-row defensive case; all-deleted server left untouched;
healthy server left untouched; non-semver versions fall back to
`published_at`.
- [x] Full unit test suite (`./internal/... ./cmd/...`) passes against a
real Postgres.
- [x] `make lint` (0 issues) and `make validate` pass.
- [x] Manual curl repro against a running registry confirms the fix
resolves the failure mode and `main` still reproduces it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:59:28 -04:00
Radoslav Dimitrov
c5f50e817b
fix(auth): better DNS verification errors and wrong-selector hint (#1202)
## Summary

DNS auth users have repeatedly hit two failure modes that both produce
the same opaque "Ed25519 signature verification failed" error, making
them indistinguishable to debug. This PR makes both diagnosable
end-to-end without changing the auth protocol or any successful path.

The two recurring failure modes:

1. **Wrong placement** — TXT record put under a selector
(`_mcp-auth.<domain>` or `_mcp-registry.<domain>`) instead of the apex.
DKIM-style intuition, but MCP DNS auth uses SPF-style apex placement.
See #385 (PR #388 fixed the docs but the failure recurred), #1103,
#1126.
2. **Stale apex record** — old TXT record left behind after a key
rotation, silently tried first and rejected.

## Changes

**`internal/api/handlers/v0/auth/dns.go`** — when the apex has no
`v=MCPv1` record, probe `_mcp-auth.<domain>` and
`_mcp-registry.<domain>` in parallel (2s individual timeout). If a
record is found there, return a targeted error pointing the user at the
apex. Costs zero extra DNS work in the success path.

**`internal/api/handlers/v0/auth/common.go`** — when verification fails
against records that *are* present, include short fingerprints of every
key the registry tried (`ed25519:YJLsHFhu`-style 8-char prefix). A new
`ErrSignatureMismatch` sentinel keeps structural errors (wrong signature
size, unsupported algorithm) passing through unchanged via `errors.Is`,
so existing assertions on those messages still hold.

**`docs/modelcontextprotocol-io/authentication.mdx`** — anti-pattern
`<Warning>` callout in the DNS section explicitly warning against
selector placement and stale records.

**Tests** — two new tests:
- `TestDNSAuthHandler_WrongSelectorProbe` — verifies the probe surfaces
records found at `_mcp-auth.` / `_mcp-registry.` and falls through to
the standard error when nothing is found anywhere
- `TestDNSAuthHandler_StaleKeyFingerprintInError` — verifies the
fingerprint hint appears in the failure message for #1126's exact
scenario

One existing test assertion was updated from a generic `"signature
verification failed"` substring to the more specific `"invalid signature
size for ECDSA P-384"` it should always have been asserting; the new
sentinel-based logic surfaces structural errors verbatim.

## Test plan

- [x] `make lint` — clean
- [x] `make test-unit` — all auth tests pass (existing + 2 new)
- [x] Verified existing single-key and multi-key paths preserve their
original (more specific) error messages where they were already specific
- [ ] Reviewer to sanity-check the docs callout renders well in the
Mintlify theme

## Out of scope

The publisher-side preflight DNS check (cross-link in #845) is
intentionally **not** in this PR — that belongs in the broader CLI UX
work tracked by #845.

Refs #845, #385, #1103
Fixes #1126

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:22:06 -04:00
Mikel Olasagasti Uranga
7c446e75ad
feat(oci): add support for Quay.io registry (#1150)
Adds support for the Quay.io container registry to the OCI validator.
This allows MCP servers hosted on Quay.io to be registered and validated
by the official registry.

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

Quay.io is a major, trusted public OCI registry widely used by the
cloud-native and open-source communities. Currently, attempting to
publish a server using a Quay image results in an unsupported OCI
registry error. This change enables developers to use Quay.io as a
first-class hosting option for their MCP server images.

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
- **Unit Tests:** Updated `internal/validators/registries/oci_test.go`
to transition Quay.io from a rejected test case to an allowed one.
- **Pattern Matching:** Verified that the allowlist correctly handles
both specific image identifiers and general pattern matching for the
`quay.io` host.
- **Local Validation:** Ran `make check` to ensure no regressions in the
existing OCI validation logic.

## Breaking Changes
<!-- Will users need to update their code or configurations? -->
No. This is an additive change that expands the allowlist of supported
registries. Existing configurations are unaffected.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->
The implementation follows the existing pattern for OCI registries:
1. Added a `RegistryURLQuay` constant to `pkg/model` for consistency.
2. Hardcoded `quay.io` into the `allowedOCIRegistries` map in the
validator to minimize architectural complexity.
3. Updated all relevant documentation files (`package-types.mdx`,
`generic-server-json.md`, and `official-registry-requirements.md`) to
ensure the source of truth is updated across the project.
4. Validation remains scoped to public images; no private registry
authentication was added for this host.

Signed-off-by: Mikel Olasagasti Uranga <mikel@olasagasti.info>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-04-24 06:11:46 -04:00
Radoslav Dimitrov
0fc4606be2
Stop pinning Go to patch versions, use stable in CI (#1137)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
We keep bumping `go 1.26.x` across the repo every time a new Go patch
drops
## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
- **`go.mod` / `deploy/go.mod`**: `go 1.25.8` → `go 1.26` (minor only —
"this module needs Go 1.26")
- **All CI workflows**: `go-version-file: 'go.mod'` → `go-version:
'stable'` — CI always uses the latest stable Go, no more
  bump PRs
- **govulncheck**: removed hardcoded `go-version-input: '1.26.1'` →
`go-version-input: 'stable'`
## Breaking Changes
<!-- Will users need to update their code or configurations? -->

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-04-09 11:57:21 -04:00
Preeti Dewani
bb84ea69e8
Publisher-managed "deleted" support (#893)
(Updated auto-generated PR description from @tadasant / Claude on Feb
22, 2026)

## Motivation and Context

Fixes #637

Server lifecycle management (deprecation and yanking) is a standard
requirement for package registries. Use cases like rebranding, security
vulnerabilities, and end-of-life servers demand the ability to mark
servers as deprecated or deleted, with optional messaging to guide
consumers toward alternatives.

## Changes

### New API endpoints

- **`PATCH /v0/servers/{serverName}/versions/{version}/status`** —
Update the lifecycle status of a single server version
- **`PATCH /v0/servers/{serverName}/status`** — Update the lifecycle
status of all versions of a server in a single transaction

Both endpoints accept:
```json
{
  "status": "active|deprecated|deleted",
  "statusMessage": "Optional message (max 500 chars) explaining the change"
}
```

### New CLI command

- **`mcp-publisher status <serverName> [version]`** — Update server
lifecycle status from the command line
  - `--status` (required): `active`, `deprecated`, or `deleted`
  - `--message`: Optional status message
- `--all-versions`: Apply to all versions of the server (replaces the
`<version>` argument)

### API response changes

`RegistryExtensions` now includes two new fields:
- `statusChangedAt` — Timestamp of the last status change
- `statusMessage` — Optional free-form message (e.g., deprecation
reason, migration guidance)

### List endpoint changes

- New `include_deleted` query parameter on `GET /v0/servers` — defaults
to `false`, hiding deleted servers from listings
- When `updated_since` is provided, `include_deleted` is automatically
set to `true` for incremental sync

### Database

- Migration `013_add_status_fields.sql`: Adds `status_changed_at` (NOT
NULL, initialized from `published_at`) and `status_message` columns to
the `servers` table

### Design decisions (from review discussion)

- **Dedicated status endpoints** rather than overloading the existing
edit endpoint
- **Bidirectional status transitions** — all transitions between
`active`, `deprecated`, and `deleted` are allowed
- **`statusMessage` as free-form text** — no structured `nextName` or
`alternativeUrl` fields (per maintainer feedback, to avoid complexity
around chaining, trust transfer, etc.)
- **Publish permission** grants status change access (not limited to
admin/edit)
- **Status message auto-cleared** when transitioning back to `active`
Good, now I have the full picture. Here's the addendum:

### Consumer-facing API changes

The new status fields live inside
`_meta["io.modelcontextprotocol.registry/official"]` on every
`ServerResponse`. This is the existing `RegistryExtensions` object — two
new fields are added alongside the existing ones:

**Before:**
```json
{
  "server": { "name": "com.example/my-server", "...": "..." },
  "_meta": {
    "io.modelcontextprotocol.registry/official": {
      "status": "active",
      "publishedAt": "2025-06-01T00:00:00Z",
      "updatedAt": "2025-06-15T00:00:00Z",
      "isLatest": true
    }
  }
}
```

**After:**
```json
{
  "server": { "name": "com.example/my-server", "...": "..." },
  "_meta": {
    "io.modelcontextprotocol.registry/official": {
      "status": "deprecated",
      "statusChangedAt": "2026-02-01T12:00:00Z",
      "statusMessage": "Deprecated in favor of com.example/my-server-v2",
      "publishedAt": "2025-06-01T00:00:00Z",
      "updatedAt": "2026-02-01T12:00:00Z",
      "isLatest": true
    }
  }
}
```

| New field | Type | Present when |
|-----------|------|-------------|
| `statusChangedAt` | `string` (RFC3339) | Always — initialized to
`publishedAt` for existing records |
| `statusMessage` | `string` (max 500 chars) | Only when set —
`null`/omitted for active servers (auto-cleared on transition to active)
|

These fields appear on **every endpoint that returns a
`ServerResponse`**: `GET /v0/servers`, `GET /v0/servers/{name}`, `GET
/v0/servers/{name}/versions/{version}`, and the new PATCH status
endpoints.

### List endpoint filtering

`GET /v0/servers` gains an `include_deleted` query parameter:

| Scenario | Behavior |
|----------|----------|
| No `include_deleted` param | Deleted servers hidden (default `false`)
|
| `include_deleted=true` | Deleted servers included in results |
| `updated_since` provided | `include_deleted` forced to `true` for
incremental sync |

## How Has This Been Tested?

- Unit tests for status handler (`status_test.go`) covering status
transitions, permission checks, validation errors, and the all-versions
endpoint
- Unit tests for the CLI command (`status_test.go`)
- Integration with existing server/edit handler tests
- Database layer tests for the new status update and query methods
- Tested locally with `make dev-compose`

## Types of changes

- [x] New feature (non-breaking change which adds functionality)

## Checklist

- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Open items from review

- [ ] `include_deleted` silent override: When `updated_since` is
provided and `include_deleted=false` is explicitly passed, should return
a 400 error rather than silently overriding
([thread](https://github.com/modelcontextprotocol/registry/pull/893#discussion_r2100879360))
— agreed upon but may not yet be implemented
2026-02-27 09:00:07 -08:00
deerajkumar18
b5f38fcc2a
update schema url point to the actual location (#884)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
This PR ensures that the draft URL points to the actual draft schema 
improvement suggested in
https://github.com/modelcontextprotocol/registry/issues/814


## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->
2026-02-10 13:32:58 -08:00
Tadas Antanavicius
70a27717fb
feat(schema): allow template variables in transport URL field (#910)
Authored by Claude Code Opus 4.5, steered/reviewed by me.

## Summary
- Updated the URL regex pattern in `StreamableHttpTransport` and
`SseTransport` to allow template variables like `{baseUrl}` as an
alternative to requiring the `https://` prefix
- This enables more flexible URL configuration in the `remotes` section
of server.json

## Changes
- Modified the `url` field pattern from `^https?://[^\s]+$` to
`^(https?://[^\s]+|\{[a-zA-Z_][a-zA-Z0-9_]*\}[^\s]*)$`
- Updated both `openapi.yaml` and the derived `server.schema.json`
- Added clarifying documentation that URL must start with `http://`,
`https://`, or a template variable

## Regex breakdown

```
^(https?://[^\s]+|\{[a-zA-Z_][a-zA-Z0-9_]*\}[^\s]*)$
```

| Part | Meaning |
|------|---------|
| `^` | Start of string |
| `(` | Begin alternation group |
| `https?://[^\s]+` | `http://` or `https://` followed by non-whitespace
|
| `\|` | OR |
| `\{[a-zA-Z_][a-zA-Z0-9_]*\}` | Template variable: `{` + valid
identifier + `}` |
| `[^\s]*` | Optional path/query after template variable |
| `)` | End alternation group |
| `$` | End of string |

## Test cases

**Should match ✓**
- `https://api.example.com/mcp`
- `http://localhost:8080/sse`
- `https://example.com/path?query=value`
- `{baseUrl}`
- `{baseUrl}/mcp`
- `{server_url}/api/v1`
- `{API_ENDPOINT}`

**Should NOT match ✗**
- `ftp://example.com` — wrong protocol
- `example.com` — missing protocol or variable
- `/relative/path` — relative paths not allowed
- `{invalid-name}/path` — hyphens not valid in variable names
- `{123invalid}` — variables can't start with numbers

---------

Co-authored-by: Agent Orchestrator <agent@ao.pulsemcp.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-02-09 11:51:17 +02:00
dependabot[bot]
541a6c61fa
build(deps): bump go.opentelemetry.io/contrib/instrumentation/runtime from 0.64.0 to 0.65.0 (#940)
Bumps
[go.opentelemetry.io/contrib/instrumentation/runtime](https://github.com/open-telemetry/opentelemetry-go-contrib)
from 0.64.0 to 0.65.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-go-contrib/releases">go.opentelemetry.io/contrib/instrumentation/runtime's
releases</a>.</em></p>
<blockquote>
<h2>Release
v1.40.0/v2.2.0/v0.65.0/v0.34.0/v0.20.0/v0.15.0/v0.13.0/v0.12.0</h2>
<h2>Overview</h2>
<h3>Added</h3>
<ul>
<li><code>WithMetricAttributesFn</code> option in
<code>go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc</code>
to define dynamic attributes on auto-instrumented metrics. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8191">#8191</a>)</li>
<li>Add support for configuring propagators in
<code>go.opentelemetry.io/contrib/otelconf</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8281">#8281</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/prometheus</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8401">#8401</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/otelconf</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8461">#8461</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/otellogr</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8477">#8477</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/otellogrus</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8485">#8485</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/otelslog</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8480">#8480</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix panic when passing nil <code>TracerProvider</code> or
<code>MeterProvider</code> to <code>WithTracerProvider</code> or
<code>WithMeterProvider</code> in
<code>go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8323">#8323</a>)</li>
<li><code>Transport</code> in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp</code>
now supports reading request body multiple times for subsequent requests
that reuse <code>http.Request</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8352">#8352</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8142">#8142</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8302">#8302</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8317">#8317</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/zpages</code> has been replaced by
<code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8325">#8325</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8340">#8340</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8341">#8341</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/runtime</code> has
been replaced by <code>const Version</code> string. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8349">#8349</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8356">#8356</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8357">#8357</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/host</code> has been
replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8358">#8358</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8360">#8360</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/propagators/opencensus</code> has been
replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8361">#8361</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8365">#8365</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/samplers/probability/consistent</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8366">#8366</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8370">#8370</a>)</li>
<li>Set <code>error.type</code> attribute instead of adding
<code>exception</code> span events in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8386">#8386</a>)</li>
<li>Set <code>error.type</code> attribute instead of adding
<code>exception</code> span events in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8386">#8386</a>)</li>
<li>Upgrade <code>go.opentelemetry.io/otel/semconv</code> to
<code>v1.39.0</code>, including updates across all instrumentation and
detector modules. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8404">#8404</a>)
<ul>
<li>The semantic conventions v1.39.0 release introduces breaking
changes, including:
<ul>
<li><code>rpc</code> span and metric attributes have been renamed to
align with naming guidelines:
<ul>
<li><code>rpc.system</code> → <code>rpc.system.name</code> (values:
<code>grpc</code>, <code>grpc_web</code>, <code>connectrpc</code>,
<code>thrift</code>, <code>dubbo</code>, etc.)</li>
<li><code>rpc.method</code> and <code>rpc.service</code> have been
merged into a fully-qualified <code>rpc.method</code> attribute</li>
<li><code>rpc.client|server.duration</code> →
<code>rpc.client|server.call.duration</code> (unit changed to
seconds)</li>

<li><code>rpc.grpc.request.metadata</code>/<code>rpc.grpc.response.metadata</code>
→
<code>rpc.request.metadata</code>/<code>rpc.response.metadata</code></li>
<li><code>rpc.grpc.status_code</code> → deprecated in favor of
<code>rpc.response.status_code</code></li>
<li><code>rpc.jsonrpc.request_id</code> →
<code>jsonrpc.request.id</code></li>
<li><code>rpc.jsonrpc.version</code> →
<code>jsonrpc.protocol.version</code></li>
</ul>
</li>
<li><code>system</code> and <code>process</code> metrics:
<ul>
<li><code>*.linux.memory</code> metrics renamed to
<code>*.memory.linux</code></li>
<li><code>system.process.status</code> → <code>process.state</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md">go.opentelemetry.io/contrib/instrumentation/runtime's
changelog</a>.</em></p>
<blockquote>
<h2>[1.40.0/2.2.0/0.65.0/0.34.0/0.20.0/0.15.0/0.13.0/0.12.0] -
2026-02-02</h2>
<h3>Added</h3>
<ul>
<li><code>WithMetricAttributesFn</code> option in
<code>go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc</code>
to define dynamic attributes on auto-instrumented metrics. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8191">#8191</a>)</li>
<li>Add support for configuring propagators in
<code>go.opentelemetry.io/contrib/otelconf</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8281">#8281</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/prometheus</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8401">#8401</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/otelconf</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8461">#8461</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/otellogr</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8477">#8477</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/otellogrus</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8485">#8485</a>)</li>
<li>Add <code>const Version</code> in
<code>go.opentelemetry.io/contrib/bridges/otelslog</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8480">#8480</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix panic when passing nil <code>TracerProvider</code> or
<code>MeterProvider</code> to <code>WithTracerProvider</code> or
<code>WithMeterProvider</code> in
<code>go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8323">#8323</a>)</li>
<li><code>Transport</code> in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp</code>
now supports reading request body multiple times for subsequent requests
that reuse <code>http.Request</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8352">#8352</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8142">#8142</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8302">#8302</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8317">#8317</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/zpages</code> has been replaced by
<code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8325">#8325</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8340">#8340</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8341">#8341</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/runtime</code> has
been replaced by <code>const Version</code> string. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8349">#8349</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8356">#8356</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8357">#8357</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/host</code> has been
replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8358">#8358</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8360">#8360</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/propagators/opencensus</code> has been
replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8361">#8361</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8365">#8365</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/samplers/probability/consistent</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8366">#8366</a>)</li>
<li>The <code>Version()</code> function in
<code>go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo</code>
has been replaced by <code>const Version</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8370">#8370</a>)</li>
<li>Set <code>error.type</code> attribute instead of adding
<code>exception</code> span events in
<code>go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8386">#8386</a>)</li>
<li>Set <code>error.type</code> attribute instead of adding
<code>exception</code> span events in
<code>go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8386">#8386</a>)</li>
<li>Upgrade <code>go.opentelemetry.io/otel/semconv</code> to
<code>v1.39.0</code>, including updates across all instrumentation and
detector modules. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8404">#8404</a>)
<ul>
<li>The semantic conventions v1.39.0 release introduces breaking
changes, including:
<ul>
<li><code>rpc</code> span and metric attributes have been renamed to
align with naming guidelines:
<ul>
<li><code>rpc.system</code> → <code>rpc.system.name</code> (values:
<code>grpc</code>, <code>grpc_web</code>, <code>connectrpc</code>,
<code>thrift</code>, <code>dubbo</code>, etc.)</li>
<li><code>rpc.method</code> and <code>rpc.service</code> have been
merged into a fully-qualified <code>rpc.method</code> attribute</li>
<li><code>rpc.client|server.duration</code> →
<code>rpc.client|server.call.duration</code> (unit changed to
seconds)</li>

<li><code>rpc.grpc.request.metadata</code>/<code>rpc.grpc.response.metadata</code>
→
<code>rpc.request.metadata</code>/<code>rpc.response.metadata</code></li>
<li><code>rpc.grpc.status_code</code> → deprecated in favor of
<code>rpc.response.status_code</code></li>
<li><code>rpc.jsonrpc.request_id</code> →
<code>jsonrpc.request.id</code></li>
<li><code>rpc.jsonrpc.version</code> →
<code>jsonrpc.protocol.version</code></li>
</ul>
</li>
<li><code>system</code> and <code>process</code> metrics:
<ul>
<li><code>*.linux.memory</code> metrics renamed to
<code>*.memory.linux</code></li>
<li><code>system.process.status</code> → <code>process.state</code></li>
<li><code>system.paging.type</code> and
<code>process.paging.fault_type</code> →
<code>system.paging.fault.type</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a89d958e7a"><code>a89d958</code></a>
Release v1.40.0/v2.2.0/v0.65.0/v0.34.0/v0.20.0/v0.15.0/v0.13.0/v0.12.0
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8493">#8493</a>)</li>
<li><a
href="1530d71edc"><code>1530d71</code></a>
Upgrade <code>semconv</code> to <code>v1.39.0</code> (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8481">#8481</a>)</li>
<li><a
href="135bb6050e"><code>135bb60</code></a>
Upgrade autoexport to latest otel dep (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8483">#8483</a>)</li>
<li><a
href="8e921058c6"><code>8e92105</code></a>
chore(deps): update module github.com/gabriel-vasile/mimetype to v1.4.13
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8486">#8486</a>)</li>
<li><a
href="147148e2d2"><code>147148e</code></a>
fix(deps): update module github.com/shirou/gopsutil/v4 to v4.26.1 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8487">#8487</a>)</li>
<li><a
href="d387510eec"><code>d387510</code></a>
Add version file for otellogrus bridge (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8485">#8485</a>)</li>
<li><a
href="fad49c80d5"><code>fad49c8</code></a>
add version file for otelslog bridge (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8480">#8480</a>)</li>
<li><a
href="9e1e5731ae"><code>9e1e573</code></a>
fix(deps): update module go.mongodb.org/mongo-driver to v1.17.8 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8479">#8479</a>)</li>
<li><a
href="37aa4753c9"><code>37aa475</code></a>
Add version file for otellogr bridge (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go-contrib/issues/8477">#8477</a>)</li>
<li><a
href="fc8ef39de1"><code>fc8ef39</code></a>
chore(deps): update module github.com/grpc-ecosystem/grpc-gateway/v2 to
v2.27...</li>
<li>Additional commits viewable in <a
href="https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.64.0...zpages/v0.65.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=go.opentelemetry.io/contrib/instrumentation/runtime&package-manager=go_modules&previous-version=0.64.0&new-version=0.65.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-02-09 11:11:06 +02:00
Bob Dickinson
f06d1eda8a
Added /validate endpoint and modified mcp-publish so that all validation happens there (#896)
## Motivation and Context

Per discussion with @rdimitrov - all validation now happens in a new
`/validate` endpoint. For details, see below.

## Breaking Changes

None. Validation on /publish only validates schema version and semantic
checks (manual checks) as before. We can add schema validation by
changing a flag in the called to `ValidateJSONSchema` when we're ready.

## Types of changes
- [X] New feature (non-breaking change which adds functionality)

## Checklist
- [X] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [X] My code follows the repository's style guidelines
- [X] New and existing tests pass locally
- [X] I have added appropriate error handling
- [X] I have added or updated documentation as needed

## Additional context

## API Changes

### 1. New `/validate` Endpoint

A new public endpoint was added for validating `server.json` files
without publishing them.

**Details:**
- **Paths:** `/v0/validate` and `/v0.1/validate`
- **Method:** `POST`
- **Authentication:** None required (public endpoint)
- **Request:** JSON body containing `ServerJSON`
- **Response:** `200 OK` with `ValidationResult` containing:
  - `valid` (boolean)
- `issues` (array of validation issues with type, path, message,
severity, reference)

**Implementation:**
- New handler file: `internal/api/handlers/v0/validate.go`
- Performs comprehensive validation using `ValidationAll` option:
  - Schema version validation
  - Full schema validation
  - Semantic validation
- Always returns `200 OK` - validity is indicated in `result.Valid`
field
- Registered in router for both `/v0` and `/v0.1` prefixes

### 2. `/publish` Endpoint Validation Flow Changes

The publish endpoint now validates earlier in the request flow and
provides clearer error responses.

**Before:** Validation was mixed into `ValidatePublishRequest()` (which
also handled publisher extensions and registry ownership).

**After:**
1. Validates `ServerJSON` structure first using `ValidateServerJSON()`
(schema version + semantic validation)
2. Returns `422 Unprocessable Entity` with message: "Failed to publish
server, invalid schema: call /validate for details"
3. Only proceeds to publisher extensions/registry validation if schema
validation passes
4. No longer calls `ValidatePublishRequest()` internally for schema
validation

**Key Changes:**
- Validation happens before `CreateServer()` call
- Returns `422` on schema/semantic validation failures
- Error message directs users to `/validate` endpoint for details
- `ValidatePublishRequest()` no longer performs schema validation (added
note in comments)

### 3. `/edit` Endpoint Validation Flow Changes

Similar validation changes were applied to the edit endpoint.

**Changes:**
- Added `ValidateServerJSON()` call before server update
- Returns `422` with same error message format on validation failure
- Matches the `/publish` endpoint behavior

### 4. Validator Functions Refactoring

Updated validator functions to reflect that schema validation is now
done separately.

**Changes to `ValidatePublishRequest()`:**
- Removed internal `ValidateServerJSON()` call
- Added note: "ValidateServerJSON should be called separately before
this function"
- Now only validates publisher extensions and registry ownership

**Changes to `ValidateUpdateRequest()`:**
- Same change (removed schema validation, added note)
- Now only validates registry ownership

## CLI Changes

### 1. `validate` Command: Now Uses API Endpoint

**Before:** Performed local validation using
`validators.ValidateServerJSON()`.

**After:** Calls the `/v0/validate` endpoint on the registry.

**Details:**
- Reads `server.json` and sends it to the registry API
- Displays validation results returned by the server
- Uses the same error formatting as before
- Registry URL comes from token file (or default)
- No authentication required (public endpoint)

**Implementation:**
- Added `validateViaAPI()` function that POSTs to `/v0/validate`
- Parses `ValidationResult` response from server
- Uses shared `printValidationIssues()` for formatting

### 2. `publish` Command: Validation Only on 422 Errors

**Before:** Performed upfront local schema validation before publishing.

**After:** Attempts publish first, then validates only if server returns
422.

**Details:**
- Attempts publish immediately (no upfront validation)
- If publish returns 422 (Unprocessable Entity):
  - Calls `/v0/validate` endpoint to get detailed validation errors
  - Formats and displays them using same logic as `validate` command
  - Returns error with migration links
- For non-422 errors (401, 403, etc.), returns original error (no
validation call)

**Implementation Changes:**
- Removed upfront `runValidationAndPrintIssues()` call
- Modified `publishToRegistry()` to return HTTP status code alongside
response/error
- Added 422 detection logic that calls `validateViaAPI()`

### 3. Shared Validation Error Formatting

**Before:** `runValidationAndPrintIssues()` function handled both
validation and printing together.

**After:** Refactored to separate printing logic.

**Details:**
- Created `printValidationIssues()` function - just prints validation
issues (no validation logic)
- Removed `runValidationAndPrintIssues()` function (no longer used)
- Both `validate` and `publish` commands use `printValidationIssues()`
for consistent output

**Behavior:**
- Prints schema validation errors with migration guidance
- Prints all other validation issues with formatting
- Returns formatted error message string for schema issues

## Summary

Both CLI commands now use the `/validate` API endpoint for validation
instead of local validation. The `publish` command validates only after
receiving a 422 error response, and both commands share the same error
formatting via `printValidationIssues()`.

### Benefits

- **Centralized validation logic** - All validation happens on the
server
- **Always up-to-date** - CLI always uses current server validation
rules
- **Consistent experience** - Same validation behavior whether using CLI
or API directly
- **Better separation of concerns** - Schema validation separated from
publisher-specific validation
2026-02-04 15:52:07 +02:00
Bob Dickinson
fc88cf0870
Enhanced server.json validation (phase 1) (#636)
Added schema validation and support for exhaustive and more detailed
validation to existing validators. Added new `mcp-publisher validate`
command. No change to any existing tests or behavior other than the new
command.

## Motivation and Context

Many currently published servers fail schema validation. Of those that
pass, many have semantic/logical errors or other errors that make them
impossible to configure, or their configuration when applied doesn't
generate correct MCP server configuration (per their own documentation).

To address this, we need better tooling to validate servers and to
inform server publishers of errors, issues, and best practices in a way
that is clear and actionable, both during server development and at time
of publishing.

I have started a discussion about the broader topic:
https://github.com/modelcontextprotocol/registry/discussions/635

There is also a fairly detailed document describing the design,
architecture, implementation, future plans, etc. See [Enhanced
Validation Design](docs/explanations/proposed-enhanced-validation.md)

This PR is the first step in the process of improving validation. It
adds schema validation and updates all existing validators to use a new
model that allows them to track context and return rich and exhaustive
results. This has been done in a way that is backward compatible
(ValidateServerJSON is unchanged as are all existing validation unit
tests).

There is a new `mcp-publisher` command called `validate` that is
currenty the only place that schema validation and enhanced
(exhaustive/rich) validation results are exposed.

### Changes

Internal Validation improvements
- Schema validation has been implemented and is available as an option
in validation methods (defaults to off)
- Existing validators track the JSON path (context) of the attribute
they're evaluating, passing it to any child validators
- Existing validators are exhaustive (return all issues, not failing
fast on the first error)
- Existing validators return rich validation results, including:
  - type (schema, semantic, linter)
  - severity (error, warn, info)
  - path (JSON path to server.json element)
  - reference (to the schema element or rule that triggered the result)
  - message (currently the same as previous error message)

A new `validate` command has been added to `mcp-publisher` so publishers
can evaluate their server.json before publishing
- Includes new schema validation and previous semantic validation
- Shows full details of all issues encountered during validation

### Usage

Given a server.json that looks like this:

```json
{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
  "name": "io.github.BobDickinson/registry",
  "description": "An MCP server that provides [describe what your server does]",
  "repository": {
    "url": "",
    "source": ""
  },
  "version": "=1.0.0",
  "packages": [
    {
      "registryType": "oci",
      "registryBaseUrl": "https://docker.io",
      "identifier": "registry",
      "version": "1.0.0",
      "transport": {
        "type": "sse"
      },
      "packageArguments": [
        {
          "type": "named",
          "name": "--mode",
          "description": "Operation mode",
          "format": "foo"
        }
      ],
      "environmentVariables": [
        {
          "description": "Your API key for the service",
          "isRequired": true,
          "format": "string",
          "isSecret": true,
          "name": "YOUR_API_KEY"
        }
      ]
    }
  ]
}
```

Run the command: 

`mcp-publisher validate server.json`

Which will produce the output:

```bash
❌ Validation failed with 5 issue(s):

1. [error] packages.0.packageArguments.0.format (schema)
   value must be one of "string", "number", "boolean", "filepath"
   Reference: #/definitions/Input/properties/format/enum from: [#/definitions/ServerDetail]/properties/packages/items/[#/definitions/Package]/properties/packageArguments/items/[#/definitions/Argument]/else/[#/definitions/NamedArgument]/allOf/0/[#/definitions/InputWithVariables]/allOf/0/[#/definitions/Input]/properties/format/enum

2. [error] packages.0.transport (schema)
   missing required fields: 'url'
   Reference: #/definitions/SseTransport/required from: [#/definitions/ServerDetail]/properties/packages/items/[#/definitions/Package]/properties/transport/else/else/[#/definitions/SseTransport]/required

3. [error] repository.url (schema)
   '' has invalid format 'uri'
   Reference: #/definitions/Repository/properties/url/format from: [#/definitions/ServerDetail]/properties/repository/[#/definitions/Repository]/properties/url/format

4. [error] version (semantic)
   version must be a specific version, not a range: "=1.0.0"
   Reference: version-looks-like-range

5. [error] packages[0].transport.url (semantic)
   url is required for sse transport type
   Reference: streamable-transport-url-required

Error: validation failed
```

Note in the results above:
- All errors contain the JSON path of the triggering element from
server.json.
- The first three errors are schema errors detected during schema
validation. The `Reference` provided contains an absolute path to the
schema rule that was violated, followed by the full schema path that
enforced that rule (with $ref redirect values substited and enclosed in
square brackets).
- The error messages provided by the existing semantic validators are
exactly the same as the previous error messages those validators
produced.

In the final solution we would remove semantic validators that are
redundant to schema validation (as in number 5 above). We have the
option to use the structured data produced by schema validation to
replace the generic schema validation message with a more descriptive
message if that is an issue (in particular, if the only argument for an
ad-hoc validator is that is produces a better/cleaner message, we would
just move that message creation code to the schema error result
formatter and special case it as opposed to having a redundant
validator).

### What's Next

If this general direction is supported and this PR is accepted, a fast
follow would include:

- Add new validators (infliuenced by observed issues) for both semantic
errors and best practices
- Require passing schema validation (in addition to improved semantic
validation) in order to publish
- Remove existing validation code that is redundant to schema validation
(ensuring error messages remain intelligible)
- Update validation client code and tests to handle the new rich return
types natively

### Issues

Schema validation requires embedding the `server.schema.json` file into
the validators package via `go:embed`, which is restricted from
embedding files outside of the package. For this reason we copy the
schema file into a schema subdir (via a prep target in the makefile) and
we .gitignore it. I tried cleaning up the copy in the make clean target,
but then the linter would complain about the embed target being missing
if it wasn't there during development. I also considered just checking
in the schema file and assuming a separate workflow for updating the
embedded schema file, but it's not clear what that workflow would be or
how it would be maintained. I'm open to a different approach, but the
schema file does need to be embedded somehow for schema validation to
work.

## How Has This Been Tested?
All existing validation tests pass, new tests implemented and all pass.

## Breaking Changes
No breaking changes, no behavior change except new `validate` command

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [X] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [X] My code follows the repository's style guidelines
- [X] New and existing tests pass locally
- [X] I have added appropriate error handling
- [X] I have added or updated documentation as needed

---------

Co-authored-by: GitHub Action <action@github.com>
2026-01-16 00:49:39 +02:00
Radoslav Dimitrov
ad207f2390
Handle URL-encoded NUL bytes (%00) in URL path and query (#877)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
Follow up on #869 

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-01-13 11:19:49 +02:00
lif
ab6a5a6b6d
fix(api): reject requests with NUL bytes in URL (#869)
## Summary
- Added `NulByteValidationMiddleware` to validate incoming requests
- Returns 400 Bad Request when NUL bytes (%00) detected in URL path or
query
- Prevents PostgreSQL encoding errors from being exposed as 500 errors

## Before
```
GET /v0.1/servers?cursor=%00
→ 500 Internal Server Error
→ "invalid byte sequence for encoding UTF8: 0x00"
```

## After
```
GET /v0.1/servers?cursor=%00
→ 400 Bad Request
→ "Invalid request: query parameters contain null bytes"
```

Fixes #862

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
2026-01-03 19:14:44 +02:00
Benedikt Reinartz
4338ec157a
Default to the local registry in the UI (#799)
<!-- Provide a brief summary of your changes -->
Change the UI default to the locally hosted registry.

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
Was quite confusing at first :)

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [ ] I have added or updated documentation as needed
2025-12-16 21:59:39 +00:00
Tadas Antanavicius
b58836c48e
feat: Add LocalTransport/RemoteTransport with URL template variables (#570)
Co-authored with Claude Code, but I've closely reviewed/modified/vetted
all changes; ready for final review.

## Summary

This PR adds LocalTransport and RemoteTransport separation with URL
template variable support for remote servers, enabling e.g. multi-tenant
deployments with configurable endpoints.

**Key additions:**
- LocalTransport (for Packages) and RemoteTransport (for Remotes)
- URL template variables for remote transports (e.g., `{tenant_id}` in
URLs)
- Pattern validation ensuring URLs are URL-like while allowing templates
- Full test coverage via Go validator tests and documentation examples

## ⚠️ server.json Update

This PR updates the `server.json` schema. A new schema version will be
cut after this lands.

## Problem

The current schema describes URL templating with `{curly_braces}` for
transports but provides no mechanism for Remote contexts to define what
those variables should resolve to, making the feature unusable for
remote servers.

Referenced issue: #394
- Users need multi-tenant remote servers with different endpoints per
deployment
- Each tenant/region has its own URL (e.g., `us-cell1.example.com`,
`emea-cell1.example.com`)
- Current schema doesn't support parameterized remote URLs

## Solution

### Schema Architecture

**LocalTransport** (for Package context):
```typescript
LocalTransport = StdioTransport | StreamableHttpTransport | SseTransport
```
- Used in Package context (non-breaking rename of existing behavior)
- Variables in `{curly_braces}` reference parent Package
arguments/environment variables
- Supports all three transport types including stdio

**RemoteTransport** (for Remote context):
```typescript
RemoteTransport = (StreamableHttpTransport | SseTransport) + { variables: object }
```
- Extends StreamableHttp/Sse via `allOf` composition - no duplication!
- Adds `variables` object for URL templating
- Only supports streamable-http and sse (stdio not valid for remotes)

### Key Changes

**1. Schema Updates** (`openapi.yaml`, `server.schema.json`):
- Added `LocalTransport` union type for Package context
- Added `RemoteTransport` with `allOf` composition extending base
transports
- Updated Package to reference `LocalTransport`
- Updated remotes to reference `RemoteTransport`
- Added URL pattern validation: `^https?://[^\\s]+$` to both
StreamableHttp and SSE
- Removed `format: uri` from SSE (was blocking template variables)

**2. Go Types** (`pkg/model/types.go`):
- Base `Transport` struct unchanged for Package context
- `RemoteTransport` struct for remotes with `Variables` field
- Both work with existing validation infrastructure

**3. Validators** (`internal/validators/`):
- `validateRemoteTransport()` validates Remote-specific constraints
- `collectRemoteTransportVariables()` extracts available variables
- `IsValidTemplatedURL()` validates template variables reference defined
variables
- `IsValidRemoteURL()` handles template variable substitution before
localhost check
- 6 new test cases for remote transport variable validation

**4. Documentation Examples** (`generic-server-json.md`):
- Remote Server with URL Templating (StreamableHttp with `{tenant_id}`)
- SSE Remote with URL Templating (SSE with `{tenant_id}`)
- Local Server with URL Templating (Package with `{port}`)
- All examples validate via existing `validate-examples` tool

## Example: Remote Transport with Variables

```json
{
  "remotes": [
    {
      "type": "streamable-http",
      "url": "https://api.example.com/mcp/{tenant_id}/{region}",
      "variables": {
        "tenant_id": {
          "description": "Tenant identifier",
          "isRequired": true,
          "choices": ["us-cell1", "emea-cell1", "apac-cell1"]
        },
        "region": {
          "description": "Deployment region",
          "isRequired": true
        }
      }
    }
  ]
}
```

Clients configure the variables, and `{tenant_id}` and `{region}` get
replaced to connect to tenant-specific endpoints like
`https://api.example.com/mcp/us-cell1/east`.

## Architecture Details

**Context-based variable resolution:**
- **Package + LocalTransport**: `{port}` references `--port` argument or
`PORT` env var from parent Package
- **Remote + RemoteTransport**: `{tenant_id}` references
`variables.tenant_id` defined in the transport itself
- **Code reuse**: StreamableHttp/Sse definitions shared via `allOf`, not
duplicated
- **Validation**: Template variables validated against available
variables for each context

---

## Follow-on Work

Follow-on work will be done to adopt the feedback in
https://github.com/modelcontextprotocol/registry/pull/570#issuecomment-3367417636
regarding variable naming/prioritzation/scoping conventions, but should
not have any impact on the shape being introduced here (just
validations).

---------

Co-authored-by: yuna0x0 <yuna@yuna0x0.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Connor Peet <connor@peet.io>
2025-12-11 09:17:32 -08:00
Radoslav Dimitrov
c54a5ac277
Ensure tests are failing CI (#840) 2025-12-11 09:56:57 +02:00
adam jones
dcb5a6f696
Add 5s timeout to DNS auth lookup to prevent resource exhaustion (#825)
The DNS auth endpoint was vulnerable to resource exhaustion if an
attacker controlled a domain with a slow/non-responding authoritative
DNS server. Without a timeout, handler goroutines would block on DNS
resolution indefinitely, allowing an attacker to pile up goroutines
until OOM.

Production is protected by NGINX ingress's default 60s proxy timeout,
but this adds defense-in-depth with a tighter 5s bound.
2025-12-10 16:22:27 +00:00
Joel Verhagen
80cb03ff7a
Fix NuGet.org base URL default, improve API integration (#813)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

This resolves
https://github.com/modelcontextprotocol/registry/issues/738. There are
two issues with the NuGet.org integration today:

1. Change the `registryBaseUrl` for NuGet to
`https://api.nuget.org/v3/index.json`. `https://api.nuget.org` is not
the entry point for our service and is not specific enough.
`https://api.nuget.org/v3/index.json` is the value that users put into
their tooling to connect to our public registry (it is installed by
default in .NET client tooling).
2. Use the service index API to get the URL pattern for package READMEs.
- This URL is not guaranteed to be stable. The service index URL
(`registryBaseUrl`) is, however.

Additionally, I polished up the error messages so various error modes
are clearer. When packages are published to NuGet.org, they take 2-5
minutes to become available due to validations we run.

Some possible states that are now handled and described more helpfully
are:
- Version exists, but the package has no README
- The ID exists, but the version does not
- The ID does not exist at all

There are only 3 authors (me, another MSFT person, and a community
member) explicitly setting the old value. The DB migration will fix the
issue. A future publish will fail with the validation error but the
error message is clear.

`registry type and base URL do not match: 'https://api.nuget.org' is not
valid for registry type 'nuget'. Expected:
https://api.nuget.org/v3/index.json`

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

- Unit tests, new and existing
- Manual test locally
- Set old data in the DB, `docker compose up`, confirm the migration
updates the specific package

## Breaking Changes

Yes, but minimal impact expected, unless there is a dependency on the
specific `https://api.nuget.org` value downstream. I am not aware of any
such dependency, and the new value is more proper and specific.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

For more information about the NuGet API see:
https://learn.microsoft.com/en-us/nuget/api/overview#service-index (I
wrote most of the docs).
2025-12-09 07:19:38 -08:00
Tadas Antanavicius
8f8fc89801
Fix missing $schema entries - data migration (#806)
Drafted by Claude Code, reviewed by me.

## Summary

This PR adds a data migration to fix 17 server entries that were
published with empty `$schema` fields.

**This is a pure data migration** - no code changes are needed because:

- Validation for the `$schema` field was already added in commit
[6d2001b](6d2001b)
on **2025-10-07**
- That validation was deployed to production on **2025-10-14** with
v1.3.0 ([PR
#668](https://github.com/modelcontextprotocol/registry/pull/668))
- All 17 affected entries were published before deployment (see dates
below)

## Affected Entries (17 total)

| Server | Version | Published |
|--------|---------|-----------|
| io.github.ruvnet/claude-flow | 2.0.0-alpha.104 | 2025-09-10 |
| io.github.ruvnet/claude-flow | 2.0.0-alpha.105 | 2025-09-10 |
| io.github.ruvnet/ruv-swarm | 1.0.18 | 2025-09-10 |
| io.github.ruvnet/ruv-swarm | 1.0.19 | 2025-09-10 |
| travel.kismet/mcp-server | 0.0.0 | 2025-09-10 |
| io.github.jkakar/cookwith-mcp | 1.0.0 | 2025-09-11 |
| io.github.jkakar/cookwith-mcp | 1.0.1 | 2025-09-12 |
| io.github.jkakar/cookwith-mcp | 1.0.2 | 2025-09-12 |
| io.github.OtherVibes/mcp-as-a-judge | 0.3.12 | 2025-09-18 |
| io.github.OtherVibes/mcp-as-a-judge | 0.3.13 | 2025-09-18 |
| io.github.OtherVibes/mcp-as-a-judge | 0.3.14 | 2025-09-18 |
| io.github.OtherVibes/mcp-as-a-judge | 0.3.20 | 2025-09-20 |
| io.github.Skills03/scrimba-teaching | 1.0.1 | 2025-09-21 |
| io.github.Skills03/scrimba-teaching | 1.1.0 | 2025-09-21 |
| io.github.Skills03/scrimba-teaching | 1.2.0 | 2025-09-21 |
| io.github.toby/mirror-mcp | 0.0.4 | 2025-10-01 |
| io.github.antuelle78/weather-mcp | 1.0.0 | 2025-10-09 ⚠️ |

**Note on outlier:** `io.github.antuelle78/weather-mcp` was published on
2025-10-09, which is after the validation code was committed
(2025-10-07) but before it was deployed (2025-10-14). This confirms the
entries slipped through due to the gap between merge and deployment.

## Dry Run SQL

To verify affected rows before running the migration:

```sql
SELECT server_name, version, value->>'$schema' as schema_value
FROM servers
WHERE (server_name, version) IN (
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.12'),
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.13'),
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.14'),
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.20'),
    ('io.github.Skills03/scrimba-teaching', '1.0.1'),
    ('io.github.Skills03/scrimba-teaching', '1.1.0'),
    ('io.github.Skills03/scrimba-teaching', '1.2.0'),
    ('io.github.antuelle78/weather-mcp', '1.0.0'),
    ('io.github.jkakar/cookwith-mcp', '1.0.0'),
    ('io.github.jkakar/cookwith-mcp', '1.0.1'),
    ('io.github.jkakar/cookwith-mcp', '1.0.2'),
    ('io.github.ruvnet/claude-flow', '2.0.0-alpha.104'),
    ('io.github.ruvnet/claude-flow', '2.0.0-alpha.105'),
    ('io.github.ruvnet/ruv-swarm', '1.0.18'),
    ('io.github.ruvnet/ruv-swarm', '1.0.19'),
    ('io.github.toby/mirror-mcp', '0.0.4'),
    ('travel.kismet/mcp-server', '0.0.0')
)
AND (value->>'$schema' = '' OR value->>'$schema' IS NULL);
```

Result against prod:

```sql
app=> SELECT server_name, version, value->>'$schema' as schema_value
FROM servers
WHERE (server_name, version) IN (
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.12'),
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.13'),
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.14'),
    ('io.github.OtherVibes/mcp-as-a-judge', '0.3.20'),
    ('io.github.Skills03/scrimba-teaching', '1.0.1'),
    ('io.github.Skills03/scrimba-teaching', '1.1.0'),
    ('io.github.Skills03/scrimba-teaching', '1.2.0'),
    ('io.github.antuelle78/weather-mcp', '1.0.0'),
    ('io.github.jkakar/cookwith-mcp', '1.0.0'),
    ('io.github.jkakar/cookwith-mcp', '1.0.1'),
    ('io.github.jkakar/cookwith-mcp', '1.0.2'),
    ('io.github.ruvnet/claude-flow', '2.0.0-alpha.104'),
    ('io.github.ruvnet/claude-flow', '2.0.0-alpha.105'),
    ('io.github.ruvnet/ruv-swarm', '1.0.18'),
    ('io.github.ruvnet/ruv-swarm', '1.0.19'),
    ('io.github.toby/mirror-mcp', '0.0.4'),
    ('travel.kismet/mcp-server', '0.0.0')
)
AND (value->>'$schema' = '' OR value->>'$schema' IS NULL);
             server_name             |     version     | schema_value 
-------------------------------------+-----------------+--------------
 io.github.toby/mirror-mcp           | 0.0.4           | 
 io.github.jkakar/cookwith-mcp       | 1.0.0           | 
 io.github.ruvnet/ruv-swarm          | 1.0.19          | 
 io.github.jkakar/cookwith-mcp       | 1.0.1           | 
 io.github.ruvnet/ruv-swarm          | 1.0.18          | 
 io.github.antuelle78/weather-mcp    | 1.0.0           | 
 io.github.jkakar/cookwith-mcp       | 1.0.2           | 
 io.github.OtherVibes/mcp-as-a-judge | 0.3.12          | 
 travel.kismet/mcp-server            | 0.0.0           | 
 io.github.Skills03/scrimba-teaching | 1.0.1           | 
 io.github.OtherVibes/mcp-as-a-judge | 0.3.20          | 
 io.github.Skills03/scrimba-teaching | 1.2.0           | 
 io.github.ruvnet/claude-flow        | 2.0.0-alpha.105 | 
 io.github.ruvnet/claude-flow        | 2.0.0-alpha.104 | 
 io.github.Skills03/scrimba-teaching | 1.1.0           | 
 io.github.OtherVibes/mcp-as-a-judge | 0.3.14          | 
 io.github.OtherVibes/mcp-as-a-judge | 0.3.13          | 
```

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-30 08:41:02 -08:00
Joel Verhagen
2ca4ae9977
Add support for ACR (Azure Container Registry) and MCR (Microsoft Container Registry) (#754)
<!-- Provide a brief summary of your changes -->

## Motivation and Context

Microsoft uses `mcr.microsoft.com` to host our public containers. These
are syndicated to Docker Hub but image references use
`mcr.microsoft.com` which is not currently supported by the Official MCP
Registry.

See https://github.com/modelcontextprotocol/registry/issues/753.

Microsoft teams want to use MCR-based images for Microsoft MCP servers.
I believe we should be adding ACR support as well, so that our customers
using Azure Container Registry can use the MCP Registry as well.

In short, MCR is used for Microsoft managed containers. ACR is a
solution for Azure customers to host their own containers.

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

Unit tests.

Manual test: local push of a server.json with
`mcr.microsoft.com/azure-sdk/azure-mcp:2.0.0-beta.1` and
`azurearcjumpstart.azurecr.io/hello-arc:latest` in it. The error is now
about missing annotation not unsupported OCI registry.

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

No.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->
2025-11-18 09:58:10 +02:00
adam jones
e7de7541c7
improve: Add hint about making GitHub org membership public in error messages (#772)
When users try to publish to a GitHub organization but don't have
permission, the error message now includes a helpful hint suggesting
they may need to make their organization membership public in GitHub
settings.

This addresses the issue where users couldn't understand why they were
unable to publish servers from organization repositories, even when they
were members of the organization.

Fixes #398

Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: adam jones <domdomegg@users.noreply.github.com>
2025-11-17 13:59:19 +00:00