Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ adheres to [Semantic Versioning](https://semver.org/).
- **OpenSSF Best Practices badge** added to the README badge row
(project [13958](https://www.bestpractices.dev/projects/13958),
`passing`).
- **Docker MCP Registry submission prep** (roadmap 21.1). Added
`packaging/docker-mcp-registry/` with a `server.yaml` draft and a
`generate_tools_json.py` script that derives the registry's `tools.json`
bypass file (`{name, description, arguments}` — confirmed against real
registry entries, not MCP-native `inputSchema`) from
`tests/contract/tool_surface.snapshot.json`, guarded by
`tests/contract/test_docker_mcp_registry_tools_json.py` so it can't
drift. The bypass is needed because MCPg requires a live
`MCPG_DATABASE_URL` to start, which their build sandbox can't supply.
Confirmed via a local build that `run.env: MCPG_TRANSPORT: stdio`
cleanly overrides the image's baked-in `streamable-http` default with a
real `initialize` + `tools/list` smoke test — no `Dockerfile` change
needed. Submission (fork + PR into `docker/mcp-registry`) not yet sent.

### Fixed

Expand Down
19 changes: 19 additions & 0 deletions docs/feature-shortlist.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,25 @@ API, redesigns per-request tenancy onto `mcp`'s new `ServerMiddleware` model
| 20.2 | ✅ **Shipped.** **Tenancy redesign onto `ServerMiddleware`.** Replaced the `mcp.server.lowlevel.server.request_ctx` ambient-contextvar hack (removed in mcp 2.0) with a `ServerMiddleware` that sets the existing `current_role` ContextVar per-request — reliable on every transport now that the SDK dispatches each request to its own asyncio task, not just stdio as before. | M | Medium-High | Security-sensitive seam; see `mcpg.tenancy.TenantRoleContextMiddleware`. |
| 20.3 | ✅ **Shipped.** **Elicitation confirmation for write-tier tools.** `ctx.elicit()`-based confirmation before any non-read-only tool call, gated by `MCPG_ELICIT_CONFIRM_WRITES` (opt-in) and the connected client's declared elicitation capability. Centralized in `AuditedMCPServer.call_tool`; no per-tool changes. | S-M | Medium | First concrete use of an mcp 2.0-only capability (elicitation didn't exist in 1.x `FastMCP`). |

## 21. Docker MCP Registry submission

MCPg ships to PyPI, GHCR, the official MCP registry, Smithery, and an HF
Spaces demo, but not [Docker's MCP Catalog](https://github.com/docker/mcp-registry)
— the registry backing Docker Desktop's MCP Toolkit and `docker mcp`
CLI. A 2026-08-14 eligibility check found no existing general-purpose
PostgreSQL entry there (only `prisma-postgres`, which is Prisma-specific),
and MCPg clears every eligibility bar in their `CONTRIBUTING.md`: MIT
license, a root `Dockerfile`, and a live-verified `run.env:
MCPG_TRANSPORT: stdio` override (confirmed against a local build — the
image's baked-in `streamable-http` default is cleanly overridden per-run,
no `Dockerfile` change needed).

| # | Item | Effort | Value | Notes |
|---|---|---|---|---|
| 21.1 | **In progress.** **`tools.json` bypass generator.** MCPg requires a live, reachable `MCPG_DATABASE_URL` to start (`load_settings` raises otherwise), so Docker's build sandbox can't run the container to auto-discover tools the way it does for DB-less servers. `packaging/docker-mcp-registry/generate_tools_json.py` derives the registry's bypass-file shape (`{name, description, arguments: [{name, type, desc}]}` — confirmed against real registry entries, NOT MCP-native `inputSchema`) from `tests/contract/tool_surface.snapshot.json`, guarded by `tests/contract/test_docker_mcp_registry_tools_json.py` so it can't drift as the tool surface grows. | S | Medium | Generated beats hand-maintained, per this file's own rule — 254 entries, no hand-authoring. |
| 21.2 | **Open.** **Submit the fork + PR.** `packaging/docker-mcp-registry/server.yaml` drafted (category `database`, `MCPG_DATABASE_URL` as the sole required secret, `MCPG_ACCESS_MODE` exposed read-only-default). Needs: `task validate` / `task build --tools mcpg` run against a real fork of `docker/mcp-registry` (requires the `Task` CLI, not yet installed anywhere this was drafted), then a PR per their `add_mcp_server.md` flow. | S-M | Medium-High | Outward-facing (third-party public repo) — needs explicit go-ahead before the fork/PR, not just the eligibility research. |
| 21.3 | **Open, known gap.** **254-tool surface size.** Unusually large for a single catalog entry relative to other registry servers; MCPg has no bucket/tool-filter env var today to ship a slimmer default surface (checked `config.py` — doesn't exist). Address proactively in the PR description rather than waiting for reviewer pushback; a follow-up `MCPG_ENABLED_BUCKETS`-style filter is a candidate if Docker's reviewers push back in practice. | — | — | Surfaced during 21.1; not blocking submission, just flagged. |

---

## Currently deferred (no commitments)
Expand Down
118 changes: 118 additions & 0 deletions packaging/docker-mcp-registry/generate_tools_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""Generate the Docker MCP Registry ``tools.json`` bypass file.

The Docker MCP Registry (``github.com/docker/mcp-registry``) normally
verifies a submitted server's tool list by running its container and
calling ``tools/list``. MCPg can't clear that bar in their build sandbox:
``MCPG_DATABASE_URL`` is a hard-required setting (see
``mcpg.config.load_settings``), so the process exits before it can answer
any MCP request unless a live, reachable Postgres is supplied. Their
CONTRIBUTING.md offers an explicit escape hatch for exactly this case: a
hand-authored (or, better, generated) ``tools.json`` listing every tool's
``name`` / ``description`` / ``arguments`` bypasses the live-container
check entirely.

This script derives that file from the one artifact in this repo that
already has the full, guarded tool surface: the contract snapshot at
``tests/contract/tool_surface.snapshot.json`` (regenerated via
``MCPG_REGENERATE_TOOL_SNAPSHOT=1``, per CLAUDE.md). Per this project's
"generated beats hand-maintained" rule, nobody should ever hand-edit the
output — rerun this script whenever the snapshot changes.

The registry's ``tools.json`` shape (confirmed 2026-08-14 against real
entries in docker/mcp-registry, e.g. ``servers/database-server``) is a
flattened argument list, NOT the MCP-native JSON-Schema ``inputSchema``:

[
{
"name": "<tool name>",
"description": "<tool description>",
"arguments": [
{"name": "<param>", "type": "<json type>", "desc": "<param description>"}
]
},
...
]

Usage:
python packaging/docker-mcp-registry/generate_tools_json.py
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

REPO_ROOT = Path(__file__).resolve().parent.parent.parent
SNAPSHOT_PATH = REPO_ROOT / "tests" / "contract" / "tool_surface.snapshot.json"
OUTPUT_PATH = Path(__file__).resolve().parent / "tools.json"

# Fallback when a property has no resolvable type (bare ``{"title": "..."}``
# schemas, seen for a couple of Any-typed passthrough params) — every
# observed real-world tools.json entry uses "string" as the type for
# untyped/loosely-typed arguments, so this matches established convention
# rather than inventing a new one.
_FALLBACK_TYPE = "string"


def _resolve_type(prop_schema: dict[str, Any]) -> str:
"""Best-effort JSON-Schema-property -> flat type-string mapping.

Handles the shapes seen across MCPg's 254-tool surface plus one not
currently emitted but legal under JSON Schema 2020-12: a plain ``type``
key (string or, per spec, a list like ``["string", "null"]`` — the
first non-null entry wins, same rule as ``anyOf`` below), an ``anyOf``
(Pydantic's ``Optional[X]`` rendering, e.g.
``[{"type": "string"}, {"type": "null"}]``), and schemas missing
``type`` entirely (Any-typed params).
"""
raw_type = prop_schema.get("type")
if isinstance(raw_type, list):
for candidate in raw_type:
if candidate and candidate != "null":
return str(candidate)
elif raw_type is not None:
return str(raw_type)
if "anyOf" in prop_schema:
for branch in prop_schema["anyOf"]:
branch_type = branch.get("type")
if branch_type and branch_type != "null":
return str(branch_type)
return _FALLBACK_TYPE


def _flatten_arguments(input_schema: dict[str, Any]) -> list[dict[str, str]]:
properties: dict[str, Any] = input_schema.get("properties", {})
arguments = []
for name, prop_schema in properties.items():
desc = prop_schema.get("description") or prop_schema.get("title") or ""
arguments.append(
{
"name": name,
"type": _resolve_type(prop_schema),
"desc": desc,
}
)
return arguments


def main() -> None:
snapshot = json.loads(SNAPSHOT_PATH.read_text(encoding="utf-8"))
tools = snapshot["tools"]

entries = [
{
"name": tool["name"],
"description": tool["description"],
"arguments": _flatten_arguments(tool["inputSchema"]),
}
for tool in tools
]

OUTPUT_PATH.write_text(json.dumps(entries, indent=2) + "\n", encoding="utf-8")
print(f"Wrote {len(entries)} tool entries to {OUTPUT_PATH}")


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions packaging/docker-mcp-registry/server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: mcpg
image: mcp/mcpg
type: server
meta:
category: database
tags:
- postgres
- postgresql
- database
- sql
- devops
about:
title: MCPg
description: >-
A production-grade PostgreSQL MCP server — 254 tools spanning schema
introspection, query execution and EXPLAIN analysis, index/vacuum/config
advisors, vector (pgvector) and full-text (pg_search) search, audited
DDL/DML, migrations, and multi-database support across PostgreSQL 14-19,
TimescaleDB, and WarehousePG. Ships with a read-only default and three
graduated access tiers (read-only / restricted / unrestricted).
icon: https://avatars.githubusercontent.com/u/5661555?v=4
source:
project: https://github.com/devopam/MCPg
commit: 3310154f5971d5c39606fb3f75925fa5748aded7
run:
env:
MCPG_TRANSPORT: stdio
config:
description: >-
Configure the connection to your PostgreSQL database and (optionally)
the access tier MCPg runs under.
secrets:
- name: mcpg.database_url
env: MCPG_DATABASE_URL
example: postgresql://user:password@host:5432/dbname
env:
- name: MCPG_ACCESS_MODE
example: read-only
value: '{{mcpg.access_mode}}'
parameters:
type: object
properties:
access_mode:
type: string
description: >-
read-only (default, safe for untrusted clients) | restricted
(adds DML writes) | unrestricted (adds DDL/shell/listen, each
gated by its own MCPG_ALLOW_* env var)
default: read-only
Loading
Loading