mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
feat(mcp): publish canonical server.json (#1510)
## Description Headroom can launch its MCP server, but did not publish a canonical `server.json` that registries and MCP hosts can consume directly. This PR adds a shared descriptor builder, commits a root `server.json`, parity-tests that artifact against the builder and existing runtime spec, and updates docs so registry authors do not need to reconstruct `headroom mcp serve` from prose. Closes #929. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added a shared `server_json.py` descriptor builder for Headroom MCP publication metadata. - Published a canonical root `server.json` and parity-tested it against the builder. - Encoded the publishable uvx contract as `headroom-ai[mcp]` plus `headroom mcp serve`. - Updated README and MCP docs to point registry authors at the canonical descriptor. - Added the README ownership marker used by MCP Registry verification. - Kept existing registrars and `headroom mcp install` behavior unchanged. ## Testing - [x] Unit tests pass - [x] Linting passes - [x] Type checking passes - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text Focused registry/server-json tests and reviewer approval were completed on this PR before the governance body cleanup. The current body update is documentation-only metadata for PR governance. ``` ## Real Behavior Proof - Environment: Headroom development checkout with MCP test dependencies. - Exact command / steps: Inspected the generated `server.json` contract and parity coverage against the descriptor builder and runtime MCP spec. - Observed result: The committed descriptor matches the builder/runtime contract and advertises the intended `headroom-ai[mcp]` / `headroom mcp serve` launch path. - Not tested: live publication to third-party registries ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes This body was normalized by a maintainer after approval so the governance parser reflects the already-reviewed PR state.
This commit is contained in:
parent
d7283387ac
commit
e9e9cd55b7
6 changed files with 268 additions and 0 deletions
|
|
@ -20,6 +20,8 @@
|
|||
<a href="https://headroom-docs.vercel.app/docs"><img src="https://img.shields.io/badge/docs-online-blue.svg" alt="Docs"></a>
|
||||
</p>
|
||||
|
||||
<!-- mcp-name: io.github.headroomlabs-ai/headroom -->
|
||||
|
||||
<p align="center">
|
||||
<a href="https://headroom-docs.vercel.app/docs">Docs</a> ·
|
||||
<a href="#get-started-60-seconds">Install</a> ·
|
||||
|
|
@ -238,6 +240,7 @@ shows an **Output Tokens Saved** card next to input compression, labelled
|
|||
|
||||
Any OpenAI-compatible client works via `headroom proxy`. MCP-native: `headroom mcp install`.
|
||||
Undo durable wrapping with `headroom unwrap <tool>` (supports: `claude`, `copilot`, `codex`, `opencode`, `openclaw`).
|
||||
Registry authors can use the canonical [`server.json`](server.json) in the repo root instead of reconstructing the `headroom mcp serve` contract from prose.
|
||||
|
||||
### GitHub Copilot CLI subscription mode
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ headroom mcp serve --debug
|
|||
|
||||
For MCP hosts that let you configure a local stdio server, point them at `headroom mcp serve`. If you also run the proxy, pass the proxy URL explicitly so retrieval and stats come from the intended proxy instance.
|
||||
|
||||
If you are publishing or consuming Headroom through an MCP registry, use the canonical descriptor at `https://github.com/headroomlabs-ai/headroom/blob/main/server.json`. It captures the package form for `headroom-ai[mcp]` and the current `headroom mcp serve` launch contract in one file.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from .install import (
|
|||
install_everywhere,
|
||||
)
|
||||
from .opencode import OpencodeRegistrar
|
||||
from .server_json import build_server_json, render_server_json
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_PROXY_URL",
|
||||
|
|
@ -39,9 +40,11 @@ __all__ = [
|
|||
"any_succeeded",
|
||||
"build_headroom_spec",
|
||||
"build_serena_spec",
|
||||
"build_server_json",
|
||||
"build_tokensave_spec",
|
||||
"format_result",
|
||||
"format_results",
|
||||
"get_all_registrars",
|
||||
"install_everywhere",
|
||||
"render_server_json",
|
||||
]
|
||||
|
|
|
|||
128
headroom/mcp_registry/server_json.py
Normal file
128
headroom/mcp_registry/server_json.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""Canonical MCP publication metadata for the Headroom server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
import tomli as tomllib # type: ignore[no-redef]
|
||||
|
||||
from .install import build_headroom_spec
|
||||
|
||||
SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json"
|
||||
SERVER_NAME = "io.github.headroomlabs-ai/headroom"
|
||||
SERVER_TITLE = "Headroom"
|
||||
SERVER_DESCRIPTION = (
|
||||
"Headroom MCP server for compression, retrieval, and stats in MCP-compatible hosts."
|
||||
)
|
||||
PYPI_OWNERSHIP_MARKER = f"<!-- mcp-name: {SERVER_NAME} -->"
|
||||
WEBSITE_URL = "https://headroomlabs-ai.github.io/headroom/"
|
||||
REPOSITORY_URL = "https://github.com/headroomlabs-ai/headroom"
|
||||
REPOSITORY_ID = "1129940957"
|
||||
PYPI_REGISTRY_URL = "https://pypi.org"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProjectMetadata:
|
||||
"""Package metadata needed for the published MCP descriptor."""
|
||||
|
||||
package_name: str
|
||||
version: str
|
||||
|
||||
|
||||
def _project_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def load_project_metadata(pyproject_path: Path | None = None) -> ProjectMetadata:
|
||||
"""Load the publishable package metadata from ``pyproject.toml``."""
|
||||
|
||||
path = pyproject_path or (_project_root() / "pyproject.toml")
|
||||
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
project = data["project"]
|
||||
return ProjectMetadata(
|
||||
package_name=project["name"],
|
||||
version=project["version"],
|
||||
)
|
||||
|
||||
|
||||
def _build_runtime_contract() -> tuple[str, tuple[str, str]]:
|
||||
"""Return the canonical CLI entrypoint plus the MCP subcommand tail."""
|
||||
|
||||
spec = build_headroom_spec()
|
||||
if spec.name != "headroom":
|
||||
raise ValueError(f"unexpected MCP server name: {spec.name}")
|
||||
runtime_tail = spec.args[-2:]
|
||||
if runtime_tail != ("mcp", "serve"):
|
||||
raise ValueError(f"unexpected MCP launch tail: {spec.args}")
|
||||
return spec.name, (runtime_tail[0], runtime_tail[1])
|
||||
|
||||
|
||||
def _build_mcp_package_spec(metadata: ProjectMetadata) -> str:
|
||||
"""Return the PyPI requirement needed to launch ``headroom mcp serve``."""
|
||||
|
||||
return f"{metadata.package_name}[mcp]"
|
||||
|
||||
|
||||
def build_server_json(metadata: ProjectMetadata | None = None) -> dict[str, object]:
|
||||
"""Build the canonical ``server.json`` payload for Headroom."""
|
||||
|
||||
metadata = metadata or load_project_metadata()
|
||||
command_name, runtime_tail = _build_runtime_contract()
|
||||
package_spec = _build_mcp_package_spec(metadata)
|
||||
return {
|
||||
"$schema": SCHEMA_URL,
|
||||
"name": SERVER_NAME,
|
||||
"description": SERVER_DESCRIPTION,
|
||||
"title": SERVER_TITLE,
|
||||
"websiteUrl": WEBSITE_URL,
|
||||
"repository": {
|
||||
"url": REPOSITORY_URL,
|
||||
"source": "github",
|
||||
"id": REPOSITORY_ID,
|
||||
},
|
||||
"version": metadata.version,
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "pypi",
|
||||
"registryBaseUrl": PYPI_REGISTRY_URL,
|
||||
"identifier": metadata.package_name,
|
||||
"version": metadata.version,
|
||||
"runtimeHint": "uvx",
|
||||
# Current uvx needs --from when the package name and script name differ.
|
||||
"runtimeArguments": [
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--from",
|
||||
"value": package_spec,
|
||||
}
|
||||
],
|
||||
"transport": {
|
||||
"type": "stdio",
|
||||
},
|
||||
"packageArguments": [
|
||||
{
|
||||
"type": "positional",
|
||||
"value": command_name,
|
||||
},
|
||||
*(
|
||||
{
|
||||
"type": "positional",
|
||||
"value": value,
|
||||
}
|
||||
for value in runtime_tail
|
||||
),
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def render_server_json(metadata: ProjectMetadata | None = None) -> str:
|
||||
"""Render the canonical ``server.json`` payload with stable formatting."""
|
||||
|
||||
return json.dumps(build_server_json(metadata), indent=2) + "\n"
|
||||
46
server.json
Normal file
46
server.json
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||
"name": "io.github.headroomlabs-ai/headroom",
|
||||
"description": "Headroom MCP server for compression, retrieval, and stats in MCP-compatible hosts.",
|
||||
"title": "Headroom",
|
||||
"websiteUrl": "https://headroomlabs-ai.github.io/headroom/",
|
||||
"repository": {
|
||||
"url": "https://github.com/headroomlabs-ai/headroom",
|
||||
"source": "github",
|
||||
"id": "1129940957"
|
||||
},
|
||||
"version": "0.27.0",
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "pypi",
|
||||
"registryBaseUrl": "https://pypi.org",
|
||||
"identifier": "headroom-ai",
|
||||
"version": "0.27.0",
|
||||
"runtimeHint": "uvx",
|
||||
"runtimeArguments": [
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--from",
|
||||
"value": "headroom-ai[mcp]"
|
||||
}
|
||||
],
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
},
|
||||
"packageArguments": [
|
||||
{
|
||||
"type": "positional",
|
||||
"value": "headroom"
|
||||
},
|
||||
{
|
||||
"type": "positional",
|
||||
"value": "mcp"
|
||||
},
|
||||
{
|
||||
"type": "positional",
|
||||
"value": "serve"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
86
tests/test_mcp_registry/test_server_json.py
Normal file
86
tests/test_mcp_registry/test_server_json.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""Tests for the canonical MCP server.json descriptor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from headroom.mcp_registry import build_server_json, render_server_json
|
||||
from headroom.mcp_registry.install import build_headroom_spec
|
||||
from headroom.mcp_registry.server_json import (
|
||||
PYPI_OWNERSHIP_MARKER,
|
||||
REPOSITORY_ID,
|
||||
REPOSITORY_URL,
|
||||
SCHEMA_URL,
|
||||
SERVER_DESCRIPTION,
|
||||
SERVER_NAME,
|
||||
WEBSITE_URL,
|
||||
_build_mcp_package_spec,
|
||||
load_project_metadata,
|
||||
)
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_build_server_json_uses_project_metadata() -> None:
|
||||
metadata = load_project_metadata()
|
||||
descriptor = build_server_json(metadata)
|
||||
|
||||
assert descriptor["$schema"] == SCHEMA_URL
|
||||
assert descriptor["name"] == SERVER_NAME
|
||||
assert descriptor["description"] == SERVER_DESCRIPTION
|
||||
assert descriptor["version"] == metadata.version
|
||||
assert descriptor["websiteUrl"] == WEBSITE_URL
|
||||
assert descriptor["repository"] == {
|
||||
"url": REPOSITORY_URL,
|
||||
"source": "github",
|
||||
"id": REPOSITORY_ID,
|
||||
}
|
||||
|
||||
package = descriptor["packages"][0]
|
||||
assert package["registryType"] == "pypi"
|
||||
assert package["registryBaseUrl"] == "https://pypi.org"
|
||||
assert package["identifier"] == metadata.package_name
|
||||
assert package["version"] == metadata.version
|
||||
assert package["runtimeArguments"] == [
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--from",
|
||||
"value": _build_mcp_package_spec(metadata),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_build_server_json_matches_runtime_contract() -> None:
|
||||
descriptor = build_server_json()
|
||||
runtime = build_headroom_spec()
|
||||
package = descriptor["packages"][0]
|
||||
|
||||
assert package["runtimeHint"] == "uvx"
|
||||
assert package["runtimeArguments"] == [
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--from",
|
||||
"value": _build_mcp_package_spec(load_project_metadata()),
|
||||
}
|
||||
]
|
||||
assert [arg["value"] for arg in package["packageArguments"]] == [
|
||||
runtime.name,
|
||||
*runtime.args[-2:],
|
||||
]
|
||||
assert package["transport"] == {"type": "stdio"}
|
||||
|
||||
|
||||
def test_root_server_json_matches_builder() -> None:
|
||||
artifact = PROJECT_ROOT / "server.json"
|
||||
assert artifact.read_text(encoding="utf-8") == render_server_json()
|
||||
assert json.loads(artifact.read_text(encoding="utf-8")) == build_server_json()
|
||||
|
||||
|
||||
def test_docs_point_to_canonical_server_json() -> None:
|
||||
readme = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
mcp_docs = (PROJECT_ROOT / "docs/content/docs/mcp.mdx").read_text(encoding="utf-8")
|
||||
|
||||
assert PYPI_OWNERSHIP_MARKER in readme
|
||||
assert "`server.json`" in readme
|
||||
assert "https://github.com/headroomlabs-ai/headroom/blob/main/server.json" in mcp_docs
|
||||
Loading…
Add table
Add a link
Reference in a new issue