Skip to main content

Publish MCP servers

This guide walks you through publishing MCP server entries to a Registry Server. The right path depends on how you want to manage your catalog:

ApproachBest for
Git or file sourceCatalogs that fit naturally in a repository with code review, CI, and version history.
Managed source (API)Programmatic or UI-driven publishing, per-version releases, and dynamic content.

This guide focuses on MCP servers. For skills, see Manage skills, which uses the same /v1/entries admin API with a skill payload instead of a server payload.

Prerequisites

  • A running Registry Server with at least one source configured (see Configuration)
  • curl or another HTTP client for the managed-source path
  • If authentication is enabled, a valid bearer token (see Authentication)

Registry file format

Git and file sources both read a single JSON file in the upstream MCP registry format. The file has top-level version and meta fields, plus a data object that holds servers and optionally skills arrays. A single file can carry servers, skills, or both:

registry.json
{
"$schema": "https://raw.githubusercontent.com/stacklok/toolhive-core/main/registry/types/data/upstream-registry.schema.json",
"version": "1.0.0",
"meta": {
"last_updated": "2026-04-21T00:00:00Z"
},
"data": {
"servers": [
{
"name": "io.github.acme/deploy-helper",
"description": "Deployment assistant for internal services",
"title": "deploy-helper",
"repository": {
"url": "https://github.com/acme/deploy-helper",
"source": "github"
},
"version": "1.0.0",
"packages": [
{
"registryType": "oci",
"identifier": "ghcr.io/acme/deploy-helper:1.0.0",
"transport": {
"type": "streamable-http",
"url": "http://localhost:8080"
}
}
],
"_meta": {
"io.modelcontextprotocol.registry/publisher-provided": {
"io.github.acme": {
"ghcr.io/acme/deploy-helper:1.0.0": {
"status": "Active",
"tier": "Official",
"tags": ["deployment", "internal"],
"tools": ["deploy", "rollback", "status"]
}
}
}
}
}
],
"skills": [
{
"namespace": "io.github.acme",
"name": "code-review",
"description": "Performs structured code reviews using best practices",
"version": "1.0.0",
"status": "active",
"packages": [
{
"registryType": "git",
"url": "https://github.com/acme/skills",
"ref": "v1.0.0",
"subfolder": "code-review"
}
]
}
]
}
}

Required fields

Each entry in the data.servers array needs the following keys, per the upstream schema:

  • name: reverse-DNS identifier (for example, io.github.acme/deploy-helper). This is the unique key for the entry.
  • description: short text displayed in listings.
  • version: the version this entry represents. Must follow the rules in the upstream schema.
  • Either packages (for servers distributed as container images or other package types) or remotes (for remote MCP servers accessed by URL).

Each entry in the data.skills array needs namespace, name, version, and at least one packages entry referencing the skill's Git repository or OCI artifact. See Manage skills for the full skill-specific field reference and the managed-source admin API for skills.

See the upstream registry schema for the full field catalog, including optional metadata, tool definitions, and the ToolHive extensions under _meta["io.modelcontextprotocol.registry/publisher-provided"].

Add the $schema property

Adding the $schema property at the top of the file enables inline validation and autocomplete in editors that support JSON Schema (for example, Visual Studio Code with the YAML/JSON extensions):

{
"$schema": "https://raw.githubusercontent.com/stacklok/toolhive-core/main/registry/types/data/upstream-registry.schema.json"
}

Publish via a Git source

Use a Git source when you want version control, code review, and CI for your registry content. The Registry Server clones the repository on startup and re-syncs on the configured interval.

Repository layout

The Registry Server reads a single JSON file from each Git source. The repository layout is up to you, but the path to that file is declared in the source's path field:

my-registry/
├── .github/
│ └── workflows/
│ └── validate.yml # CI to validate registry.json against the schema
├── README.md
└── registry.json # The file declared in the source's "path" field

You can keep multiple registry files in the same repository, served by different sources (for example, teams/platform/registry.json and teams/data/registry.json). Each source points at a different path.

If you omit path on the source, the server looks for registry.json at the repository root.

Point a source at the file

In the Registry Server's configuration, configure a Git source that references the repository, branch or tag, and path:

config.yaml
sources:
- name: internal
git:
repository: https://github.com/acme/my-registry.git
branch: main
path: registry.json
syncPolicy:
interval: '30m'

registries:
- name: default
sources: ['internal']

Use branch, tag, or commit to pin a specific version. When more than one is set, commit takes precedence over branch, which takes precedence over tag.

For private repositories, set auth.username and auth.passwordFile on the source (see Private repository authentication).

Validate before committing

Validate the file against the upstream schema locally before opening a PR. Any JSON Schema validator works. For a quick check with check-jsonschema:

check-jsonschema \
--schemafile https://raw.githubusercontent.com/stacklok/toolhive-core/main/registry/types/data/upstream-registry.schema.json \
registry.json

Running this check in CI on every pull request catches formatting errors before they reach the Registry Server.

How changes reach the server

Once your change is merged into the tracked branch, the Registry Server picks it up on the next scheduled sync. If you need to verify immediately, either wait for the interval configured in syncPolicy, or restart the Registry Server to force an immediate sync.

Serve entries from a file source

A file source is similar to a Git source but reads the JSON from a local path or URL instead of cloning a repository. Use it for local development, air-gapped environments, or when you already have a delivery mechanism that writes files onto the server (for example, a mounted ConfigMap or a persistent volume).

config.yaml
sources:
- name: local
file:
path: /data/registry/registry.json
syncPolicy:
interval: '15m'

registries:
- name: default
sources: ['local']

The file format is identical to the Git source format. See File source configuration for mounting options in Kubernetes, and the Quickstart for a complete ConfigMap-based example.

Publish via the managed source API

Managed sources accept entries directly through the /v1/entries admin API instead of syncing from an external file. Use this when you want programmatic publishing, per-version release workflows, or a UI that calls the Registry Server for you.

A server deployment can have at most one managed source. See Managed source configuration for the startup rules.

Publish a server

To publish a new server version, send a POST request to /v1/entries with the entry wrapped in a server object:

Publish a server
curl -X POST \
https://registry.example.com/v1/entries \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{
"server": {
"name": "io.github.acme/deploy-helper",
"description": "Deployment assistant for internal services",
"version": "1.0.0",
"packages": [
{
"registryType": "oci",
"identifier": "ghcr.io/acme/deploy-helper:1.0.0",
"transport": {
"type": "streamable-http",
"url": "http://localhost:8080"
}
}
]
}
}'

Required fields in the server object: name, description, version, and either packages or remotes. See the upstream registry schema for the full structure and the optional ToolHive extensions.

A successful response returns 201 Created with the published server. If the version already exists, the server returns 409 Conflict.

Claims on publish

When authentication is enabled, publish requests must include a top-level claims object alongside server. Populate packages with the same entries shown in the publish example above; the snippet below uses an empty array for brevity:

{
"server": {
"name": "io.github.acme/deploy-helper",
"description": "Deployment assistant for internal services",
"version": "1.0.0",
"packages": []
},
"claims": { "org": "acme", "team": "platform" }
}

The publish claims must be a subset of your JWT claims, and subsequent versions of the same server must carry the same claims as the first version. See Claims on published entries for the full rules.

Versioning behavior

When you publish a new version, the Registry Server compares it against the current latest version. If the new version is newer, the latest pointer updates automatically. Publishing an older version (for example, backfilling 0.9.0 after 1.0.0 exists) does not change the latest pointer.

Delete a server version

To delete a specific version, send a DELETE request to the version path. URL-encode the / in the server name as %2F:

curl -X DELETE \
https://registry.example.com/v1/entries/server/io.github.acme%2Fdeploy-helper/versions/1.0.0 \
-H "Authorization: Bearer <TOKEN>"

A successful request returns 204 No Content. Deleting a version that doesn't exist returns 404 Not Found. If you delete the version currently marked latest, the server promotes the next-highest remaining version to latest automatically.

warning

Deleting a server version is permanent. The entry is removed from the database immediately, and any clients with cached references will get a 404 on subsequent lookups.

Update claims on a published server

To change the authorization claims on an existing server, send a PUT request to the claims path:

curl -X PUT \
https://registry.example.com/v1/entries/server/io.github.acme%2Fdeploy-helper/claims \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{"claims": {"org": "acme", "team": "platform"}}'

The JWT presented on both the original publish and this update must satisfy the claims being set. See Claims on published entries for the authorization rules.

Next steps