Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 28 additions & 28 deletions .secrets.baseline

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ services:
- HOST=${GATEWAY_HOST:-0.0.0.0}
# Transport: sse, streamablehttp, http, or all (default: all)
- TRANSPORT_TYPE=streamablehttp
- GATEWAY_TOOL_NAME_SEPARATOR=${GATEWAY_TOOL_NAME_SEPARATOR:--}
# High-level Rust MCP UX:
# Deprecated as of 2026-06-11; sunsets on 2026-07-07. Prefer RUST_MCP_MODE=off and the Python MCP transport.
# RUST_MCP_MODE=off -> Python MCP transport
Expand Down Expand Up @@ -1075,6 +1076,7 @@ services:
dockerfile: Containerfile
environment:
- DATABASE_URL=postgresql+psycopg://postgres:${POSTGRES_PASSWORD:-mysecretpassword}@postgres:5432/mcp
- GATEWAY_TOOL_NAME_SEPARATOR=${GATEWAY_TOOL_NAME_SEPARATOR:--}
- "JWT_SECRET_KEY=${JWT_SECRET_KEY:?JWT_SECRET_KEY is not set. Run: make setup (first time) or make init-secrets-patch-env (if .env exists)}"
# Basic auth is DISABLED by default for security (API_ALLOW_BASIC_AUTH unset here)
# Only set these if you explicitly enable Basic auth for this container
Expand Down
21 changes: 20 additions & 1 deletion docs/docs/manage/export-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,26 @@ The export/import system enables complete backup and restoration of your Context
- **Resources** (locally defined resources)
- **Roots** (filesystem and HTTP root paths)

> **Note**: Only locally configured entities are exported. Dynamic content from federated MCP servers is excluded to ensure exports contain only your gateway's configuration.
> **Note**: Exports describe gateway configuration and resource metadata; they do not back up upstream resource content.

### Federated Resource Names

Federated resource names use the gateway slug, `GATEWAY_TOOL_NAME_SEPARATOR` (default `-`), and the resource base
slug. Local resource names and resource URIs are unchanged. Names are limited to 255 characters and are not unique:
long prefixes can consume the resource portion, and gateway slugs can collide. A virtual-server-scoped read resolves
a shared URI only when that server has one matching resource; prefixing names does not change URI routing.

Exports preserve local names verbatim. Federated exports select the effective base, then the upstream name, then the
derived name, using the first candidate that validates after applying the configured length limit (at most 255).
If none validates, the derived name is exported unchanged with a warning. Full `original_name` and `custom_name_slug`
values accompany federated exports as provenance; import consumes only `name` and does not restore that provenance.
Empty, oversized, or configuration-invalid bases can therefore change on import. Import also applies the destination's
validation settings. Lowering a validation limit does not truncate local names during export.

Changing the separator does not rewrite stored resource bases. They retain their separator until a manual rename or
an applicable upstream-name change replaces them, so derived names can contain mixed separators after a configuration
change. Separator-only differences do not count as manual overrides. Downgrading the resource-namespacing migration
restores upstream names for federated resources and discards manual base overrides.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/alembic/versions/c7e91a2b4d60_add_resource_namespacing.py
Copyright contributors to the MCP-CONTEXT-FORGE project
SPDX-License-Identifier: Apache-2.0

Add persisted resource namespacing.

Revision ID: c7e91a2b4d60
Revises: 5e211ec89cad

Downgrade restores federated upstream names and discards manual base overrides.
It uses stored values only, independently of the current separator configuration.
"""

# Standard
from typing import Sequence, Union

# Third-Party
from alembic import op
import sqlalchemy as sa

# First-Party
from mcpgateway.config import settings
from mcpgateway.utils.create_slug import slugify

revision: str = "c7e91a2b4d60" # pragma: allowlist secret
down_revision: Union[str, Sequence[str], None] = "5e211ec89cad" # pragma: allowlist secret
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Initialize naming state once and prefix federated resources."""
bind = op.get_bind()
inspector = sa.inspect(bind)
if not inspector.has_table("resources"):
return
columns = {column["name"] for column in inspector.get_columns("resources")}
for name in ("original_name", "custom_name_slug"):
if name not in columns:
op.add_column("resources", sa.Column(name, sa.Text(), nullable=True))

rows = (
bind.execute(sa.text("SELECT r.id, r.name, r.gateway_id, g.name AS gateway_name " "FROM resources r LEFT JOIN gateways g ON r.gateway_id = g.id " "WHERE r.original_name IS NULL"))
.mappings()
.all()
)
for row in rows:
base = slugify(row["name"])
gateway_slug = slugify(row["gateway_name"]) if row["gateway_id"] and row["gateway_name"] else ""
name = row["name"]
if gateway_slug:
name = (f"{gateway_slug}{settings.gateway_tool_name_separator}{base}" if base else gateway_slug)[:255]
bind.execute(
sa.text("UPDATE resources SET original_name = :original, custom_name_slug = :base, name = :name WHERE id = :id"),
{"id": row["id"], "original": row["name"], "base": base, "name": name},
)


def downgrade() -> None:
"""Restore upstream names, leaving local names unchanged, then drop naming state."""
bind = op.get_bind()
inspector = sa.inspect(bind)
if not inspector.has_table("resources"):
return
columns = {column["name"] for column in inspector.get_columns("resources")}
if "original_name" in columns:
bind.execute(sa.text("UPDATE resources SET name = original_name WHERE gateway_id IS NOT NULL AND original_name IS NOT NULL"))
with op.batch_alter_table("resources") as batch:
for name in ("custom_name_slug", "original_name"):
if name in columns:
batch.drop_column(name)
Loading