Publish an MCP Server¶
For the supported platform workflow, start with .mcp metadata: initialize it
with mcp-runtime server init, then build, push, deploy, and verify the server.
Use a hand-written MCPServer manifest only for an admin/operator or GitOps
workflow.
Publishing an MCP server takes five steps:
- generate
.mcpmetadata withserver initand validate it - build the server image
- push the image to the platform registry
- deploy the server into the platform
- verify that the server is reachable and governed
Use an existing platform account, or self-host with Getting Started.
Install the CLI with the Quickstart, and log in before building
or publishing tenant images. Commands below use mcp-runtime on PATH; source
builds can add their checkout’s bin directory to PATH.
Choose an authoring format¶
You can describe a server in two ways:
.mcpmetadata Use this for user and team deployments. The CLI creates the metadata and handles build, push, and deploy.MCPServermanifest Use this for an admin/operator or GitOps workflow that directly manages the Kubernetes resource.
Either way, the operator reconciles a server deployment, service, route, and optional governed request path.
Recommended workflow: initialize or edit .mcp metadata¶
The metadata-driven server flow uses YAML files under .mcp. server deploy
publishes directly from that metadata, while server generate renders
MCPServer manifests when you need YAML for review or GitOps.
Start with server init when you do not already have metadata:
mcp-runtime server init payments --scope org --tool list_invoices --tool refund_invoice
mcp-runtime server init payments \
--scope org \
--tool list_invoices \
--tool-spec refund_invoice:high:destructive
This creates .mcp/servers.yaml with defaults. Re-run with --force to
replace the same server entry, or edit the generated file when tools need
different trust levels or side-effect values. --tool is shorthand for a
read-only, low-trust tool. Use --tool-spec name:low|medium|high:read|write|destructive
for per-tool metadata.
For grant manifests, access grant init --tool is shorthand for
name:allow:<--trust>. Use repeated --tool-rule name:allow|deny:low|medium|high
when a grant needs mixed per-tool decisions or trust levels. For explicit
session manifests, access session init supports --trust,
--expires-in, --expires-at, --revoked, and upstream-token secret flags.
For platform-backed access, use an active managed agent ID owned by the subject
team. The cross-team example uses a format sample; replace it with an ID from
mcp-runtime agent list globex --status active.
mcp-runtime auth login --api-url https://platform.example.com
mcp-runtime access grant init payments-globex-cursor \
--namespace mcp-team-acme \
--server payments \
--team-id <globex-team-id> \
--agent-id agt_01arz3ndektsv4rrffq69g5fav \
--tool list_invoices \
--tool-rule refund_invoice:allow:high \
--side-effect read \
--side-effect destructive \
--output grant.yaml
mcp-runtime access session init cursor-session \
--namespace mcp-team-acme \
--server payments \
--human-id <user-id> \
--team-id <globex-team-id> \
--agent-id agt_01arz3ndektsv4rrffq69g5fav \
--trust medium \
--expires-in 1h \
--output session.yaml
mcp-runtime server validate --metadata-dir .mcp \
--grant-file grant.yaml --session-file session.yaml
mcp-runtime access grant apply --file grant.yaml
# Platform API session apply is admin-only. Use the adapter for agents:
# mcp-runtime access session apply --file session.yaml
server validate cross-checks the grant and session manifests against the
server metadata before they reach the cluster. Both --grant-file and
--session-file are repeatable, and --metadata-file <path> replaces
--metadata-dir when the metadata lives outside .mcp.
Adapter-driven agents should skip manual session apply; after the grant exists,
use mcp-runtime adapter proxy --server payments --namespace mcp-team-acme
--agent agt_01arz3ndektsv4rrffq69g5fav --auto-refresh, replacing the sample
ID with the active agent's ID. See Agent Adapters.
Example metadata:
version: v1
servers:
- name: payments
description: Payments MCP server for invoice lookup and refund workflows.
scope: org
image: registry.example.com/org/payments
imageTag: v1.0.0
route: /payments
port: 8088
replicas: 1
gateway:
enabled: true
tools:
- name: list_invoices
description: List invoices for a customer account.
requiredTrust: low
sideEffect: read
riskLevel: low
- name: refund_invoice
description: Issue a refund for an invoice.
requiredTrust: high
sideEffect: destructive
riskLevel: high
Metadata fields¶
nameThe server name.descriptionA short platform-facing summary shown in the server catalog.imageThe image repository.imageTagThe image tag.scopeOptional publish destination:tenant,org, orpublic.server initwritesscope: tenantby default so new metadata starts in the private team publish path.orgresolves to the org catalog namespace, andpublicresolves to the public catalog namespace.tenantselects one of the authenticated user's team namespaces when you publish through the platform API; when generating Kubernetes YAML directly, setnamespaceexplicitly for team deployments.routeThe public path prefix that will become the server ingress path.portThe container port.replicasThe desired replica count.namespaceThe target namespace.toolsTool inventory for the platform catalog and policy authoring. Include each tool's description when the MCP server SDK exposes one throughtools/list, and setsideEffecttoread,write, ordestructive. Tool side effects are required when a tool is listed. OptionalriskLevel(low,medium,high) is informational for catalog and audit views; it does not change gateway authorization.auth,policy,session, andgatewayGoverned request-path settings.server initwritesgateway.enabled: true, allow-list/deny policy, andsession.required: trueso public tool calls go through the adapter/session path by default. Use--policy-mode,--default-decision, or--session-required=falseto change those scaffolded values. Init omits platform-managed gateway wiring and auth/session header details unless you override them intentionally.
Metadata defaults¶
If fields are omitted, the loader applies defaults:
- image defaults toward the platform registry path
server initwritesscope: tenantunless you pass--scope orgor--scope publicscope: org/scope: publicprefix default image repositories withorg/orpublic/and default the generated namespace tomcp-servers-orgormcp-servers-public- tag defaults to
latest - route is normalized with a leading
/ - port defaults to
8088 - replicas default to
1 - namespace defaults to
mcp-servers server initomits platform-managed gateway wiring and auth/session header details; add those fields only when you intentionally need to override the platform/operator defaults
For multi-team deployments, set scope: tenant and deploy through
server deploy --scope tenant with platform credentials. The platform API
resolves the target team namespace and defaults team ownership metadata. The
namespace is the write boundary for the MCPServer, grants, sessions, and
secrets. server deploy creates by default; if a server with the same name
already exists, pass --update to redeploy it intentionally.
For private registry images, the platform deployment path also creates or
refreshes mcp-runtime-registry-pull in that namespace and attaches it to the
workload service account. Raw kubectl apply does not run this provisioning;
create the pull Secret in the manifest's namespace before applying raw YAML.
Deploy from metadata:
mcp-runtime server deploy payments --scope org --metadata-dir .mcp
# Redeploy an existing server after changing metadata or image tag.
mcp-runtime server deploy payments --scope org --metadata-dir .mcp --update
# Optional: render YAML for review/GitOps.
mcp-runtime server generate --metadata-dir .mcp --output manifests/
Build and push the server image¶
Use one image flow per server so tags stay consistent.
server push is the user-facing command for publishing MCP server images
through the authenticated platform API. Registry commands are reserved for
registry administration; use admin registry push only for direct Kubernetes
operator debugging.
Flow A: metadata-driven build with the CLI¶
mcp-runtime server build image payments --tag v1.0.0 --platform linux/amd64
server build image builds the image, resolves the target registry host, tags the local image with that resolved reference, and rewrites matching .mcp metadata (image and imageTag). The command defaults Docker builds to linux/amd64, matching common amd64 Kubernetes nodes; set --platform or MCP_DOCKER_PLATFORM when your target nodes use another architecture. Without --registry, the registry host comes from the active auth login profile (the registry host saved at login, or registry.<domain> derived from a platform.<domain> API URL), then MCP_REGISTRY_INGRESS_HOST, MCP_REGISTRY_HOST, then MCP_PLATFORM_DOMAIN, the same order server push uses. The command prints the built image ref and the server push command to run next. MCP_REGISTRY_ENDPOINT is reserved for internal pulls and transfers; it does not configure an Ingress hostname, image name, or credential host. During manifest generation, unqualified and platform-registry image refs are rewritten to the kubelet's internal pull host; external registry refs such as ghcr.io/owner/image remain unchanged. When metadata sets scope: tenant, the build command uses platform credentials to resolve the same team repository prefix that server push --scope tenant uses, so log in first or set MCP_PLATFORM_API_TOKEN with a saved or explicit MCP_PLATFORM_API_URL.
After this command, push the exact image reference produced by the build output (or read it from the rewritten metadata):
mcp-runtime auth login --api-url https://platform.example.com
mcp-runtime server push --scope org --image <exact-image-ref-from-build>
server push requires platform credentials from mcp-runtime auth login or
MCP_PLATFORM_API_TOKEN with a saved or explicit MCP_PLATFORM_API_URL;
unauthenticated pushes are
rejected before Docker or the in-cluster helper starts. <exact-image-ref-from-build>
may be a resolved public registry host such as registry.example.com/org/payments:v1.0.0,
or a registry Service address when no public registry Ingress is configured.
Use --scope public for public catalog images. Use --scope tenant for team
images; if the image name has no repository prefix, the CLI prefixes it with
the authenticated user's sole team slug. For users in multiple teams, initialize
the metadata with a team-scoped repository, for example
mcp-runtime server init payments --scope tenant --image registry.example.com/acme/payments,
then run server build image, push
the printed image ref and deploy to that team. Explicit tenant repository
prefixes must match one of the user's teams; ambiguous unscoped tenant
repositories are rejected before Docker runs.
--scope org works in both platform modes; --scope public requires
PLATFORM_MODE=public. Public mode accepts all three scopes; server push and
server deploy reject a disabled scope the same way and list the enabled
scopes. --scope tenant publishes to an authorized team in either mode. Uploads may take up
to MCP_REGISTRY_PUSH_UPLOAD_TIMEOUT (default 20m) on mcp-runtime-api;
archives over 512 MiB are rejected with 413. The saved API URL must be
https:// when the front door redirects HTTP to HTTPS, or the upload POST
becomes a GET and the API returns 405 method_not_allowed. The proxy in
front of /api/v1/runtime/registry/push must allow a body large enough for
the archive. A 1 MiB default on /api/ rejects the push even when /v2/
is already unlimited.
Then deploy from metadata:
mcp-runtime server deploy payments --scope org --metadata-dir .mcp
Flow B: manual Docker build, push, and direct platform deploy¶
Use this when you manage image tags directly and want the platform API to write
the MCPServer for you:
docker build -t payments:v1.0.0 .
mcp-runtime auth login --api-url https://platform.example.com
mcp-runtime server push --scope public --image payments:v1.0.0
mcp-runtime server deploy payments --scope public --image payments --tag v1.0.0
Short names like payments:v1.0.0 are valid for server push when that
exact local image tag exists. For server deploy, the platform API accepts
short names such as payments and resolves them to the configured registry and
scope prefix, for example <registry>/public/payments in public mode or
<registry>/<team-slug>/payments with tenant scope in either mode.
server deploy --scope public resolves the platform public catalog namespace;
--scope org resolves the org catalog namespace; --scope tenant uses the
authenticated user's team namespace unless --team or --namespace selects one
explicitly. server deploy uses the default public route /<name>/mcp, and
the operator sets MCP_PATH to the path the server receives so the bundled Go,
Python, and Rust examples listen on the route the ingress exposes. The platform API and CLI
deploy flow also default spec.gateway.enabled: true, so published servers use
the governed gateway path unless you explicitly provide spec.gateway. When
you run server deploy from a directory with .mcp/*.yaml, the CLI copies the
matching server metadata into the request. If the metadata directory contains
exactly one server, it uses that server's inventory even when the deployed
runtime name is different; this keeps spec.tools side-effect metadata in sync
with governance policy.
Flow C: manual Docker build, push, and manifest apply (admin/GitOps)¶
Use this when you need full control of MCPServer fields and have
admin/operator Kubernetes access. For the normal tenant platform path, use
Flow A/B with server deploy.
docker build -t payments:v1.0.0 .
mcp-runtime auth login --api-url https://platform.example.com
mcp-runtime server push --scope tenant --image payments:v1.0.0
mcp-runtime server apply --file payments.yaml --use-kube
Retire a server¶
Use mcp-runtime server delete <name> --namespace <namespace> or the UI's
Retire action. Both use the same platform retirement operation: disable
grants, revoke sessions, remove all access records referencing that exact
server namespace/name (including records in other namespaces), remove its
exclusively managed analytics credential, and delete the server. Shared image
pull credentials, user-provided secrets, agents, teams, and historical audit
and analytics records are retained.
For servers applied directly through Kubernetes, retirement repairs a missing namespace-local API credential binding after checking ownership. It waits up to five seconds for Kubernetes authorization to observe that binding and never extends credential access to reserved infrastructure namespaces.
A cleanup error means retirement is incomplete. Retry the same command; the
server remains marked as retiring and refuses new access or redeployment while
cleanup completes. Administrators and team owners can still delete, disable,
or revoke existing access through its management endpoints; the retiring
server retains its ownership checks. New grants and manually applied sessions
return 409 Conflict, and adapter enrollment remains blocked.
If the server was already deleted, a platform admin must
run the same command to recover orphaned grants, sessions, and managed
credentials, because its previous ownership labels are no longer available.
Publishing the same name creates a fresh deployment and requires new access.
The explicit Kubernetes deletion path (--use-kube) deletes only the server
object and does not perform platform authorization cleanup.
What happens after deploy¶
After the server description reaches the platform, the operator:
- stores the
MCPServerresource in Kubernetes - resolves the final image reference
- creates or updates a
Deployment - creates or updates a
Service - creates or updates an
Ingress - renders gateway policy when governed access is enabled
- updates
MCPServer.statuswith readiness and progress
With the default path-based shape, the server becomes available at:
/{publicPathPrefix}/mcp
For the example above, that is:
/payments/mcp
Verify from the CLI¶
Check server state:
mcp-runtime auth login --api-url https://platform.example.com
mcp-runtime server status
mcp-runtime server get payments
mcp-runtime status
Tenant deploys land in the team namespace, mcp-team-<team-slug>, not
mcp-servers. Pass --namespace mcp-team-acme to server get,
server status, server policy inspect, and the access commands when the
server is not in your default namespace.
If the server uses governed access:
mcp-runtime server policy inspect payments
mcp-runtime ops status
If traffic is failing:
mcp-runtime server policy inspect payments
mcp-runtime status
# Admin/operator only
mcp-runtime ops logs gateway --follow
mcp-runtime server logs payments --follow --use-kube
Common failure points¶
Image built, but deploy still points at the wrong image¶
Check:
- the
spec.imageandspec.imageTagin your manifest - the metadata entry updated by
server build image - whether you pushed the exact same image reference (registry/repo/tag) that your metadata or manifest points to
Image pushed, but server never becomes ready¶
Check:
mcp-runtime server get <name>mcp-runtime server statusmcp-runtime status
Most often this is an image reference, image-pull, or routing mismatch.
Route exists, but governed calls fail¶
Check:
mcp-runtime server policy inspect <name>- your grant and session objects
mcp-runtime ops logs gateway --follow
Event count is 0¶
Check:
kubectl get mcpserver <name> -n <namespace> -o yamlGatewayReady=TrueandPolicyReady=Trueon the MCPServer status- the server pod is
2/2and includes themcp-gatewaysidecar kubectl logs -n <namespace> <pod> -c mcp-gatewaymcp-runtime ops statusmcp-runtime ops logs ingest --followmcp-runtime ops logs processor --follow
Request analytics only exist for traffic that flows through mcp-gateway.
The gateway is on by default; the adapter is optional for analytics and is
required only when you enforce grants/sessions via spec.policy. If you apply
raw YAML with kubectl apply or server apply --use-kube, nothing extra is
needed: the operator mounts a projected ServiceAccount token (audience
mcp-ingest) into the gateway sidecar and ingest validates it with a
Kubernetes TokenReview, so no ingest key has to be copied into your namespace.
Opt out of emission with:
analytics:
disabled: true
Admin and GitOps workflow: write an MCPServer manifest¶
Start with a minimal manifest:
apiVersion: mcpruntime.org/v1alpha1
kind: MCPServer
metadata:
name: payments
namespace: mcp-servers
spec:
description: Payments MCP server for invoice lookup and refund workflows.
image: registry.example.com/payments
imageTag: v1.0.0
port: 8088
publicPathPrefix: payments
gateway:
enabled: true
namespace: mcp-servers suits a single-team install. For a tenant deployment,
use the team namespace (mcp-team-<team-slug>) and set spec.teamID: the
platform API fills both when you publish with server deploy --scope tenant,
but hand-written YAML must state them explicitly.
What each field does¶
metadata.nameThe server name inside the platform. This is also the default public route prefix when you do not override it.metadata.namespaceUsuallymcp-serversfor a single-team setup. In a multi-team deployment, use the team's namespace, for examplemcp-team-acme; see Multi-team isolation.spec.teamIDStable platform team ID for the server owner. The platform API defaults this for team namespaces; hand-written YAML should set it explicitly.spec.descriptionA short platform-facing summary shown in the server catalog.spec.imageThe image repository to run.spec.imageTagThe image tag when the tag is not embedded directly inspec.image.spec.portThe port your MCP process listens on inside the container.spec.publicPathPrefixThe public route prefix.paymentsbecomes/payments/mcp.spec.gateway.enabledSends requests throughmcp-gatewayfor metrics, traces, and request analytics. The sidecar is on by default (includinggateway: {}). Setenabled: falseto opt out. Policy enforcement is separate: omitspec.policyfor observe-mode (tool calls allowed, decisions still recorded); setspec.policy(for example allow-list + deny) when you want grant/session enforcement with the adapter.spec.analyticsAnalytics emission is on by default whenever the gateway is on and an ingest URL is configured. The platform API fillsspec.analytics.ingestURLfromMCP_ANALYTICS_INGEST_URLwhen publishing a server; an explicit URL in the server metadata takes precedence. The platform API reads the oldMCP_SENTINEL_INGEST_URLkey as a fallback for installs awaiting setup migration. The operator also accepts its own ingest URL setting. Setspec.analytics.disabled: trueto opt out. The gateway sidecar authenticates to ingest with a projected ServiceAccount token the operator mounts automatically;apiKeySecretRefis an optional override.
mcp-runtime server deploy and the platform API (POST /api/v1/runtime/servers)
default the gateway on. Hand-written YAML can omit gateway or use
gateway: {}; only gateway.enabled: false disables the sidecar.
Common edits¶
- Add
spec.ingressHostfor host-based routing instead of path-based routing. - Add
spec.servicePortwhen you want a Service port other than80. - Add
spec.envVarsorspec.secretEnvVarsfor runtime configuration. - Add
spec.imagePullSecretsif your registry requires explicit pull credentials. - Add
spec.toolswith tool descriptions, trust levels, and side-effect classes so the platform catalog and policy engine mirror the tool summaries clients see fromtools/list. - Add
spec.auth,spec.policy,spec.session, orspec.rolloutwhen you want stricter governance or more delivery control.
Apply the manifest only from an admin/operator workstation. server apply
requires --use-kube, kubectl, and kubeconfig/RBAC access to the target
namespace. For normal platform workflows, use the platform-backed
server deploy flow in Option B.
mcp-runtime server apply --file payments.yaml --use-kube
mcp-runtime server status --use-kube # pod detail; omit --use-kube for platform API summary
Related docs¶
Next: Agent Adapters: connect your MCP client through the adapter proxy.