feat: full-stack MCP surface — client template, npm CI, 13 repos
- build.py: vendor templates/client into mcp/templates/client with client.json manifest (16 kinds); register drone-npm as a third CI shape (types: pypi/npm/docker); root *.py stale doc numbers fixed - registry.py: registry://client resource + list_client_components / get_client_component / get_client_template tools; build prompt gains the client scaffold step (docs://modules/ui_admin); compliance prompt covers npm packages; drone docs describe all three shapes - repos.json (+mcp): doc-weaver, react-auth, react-openapi (npm Type B, package.json version source), vet (app Type C, VERSION) - docs: 90_drone_pipelines gains Type B npm section; 91_versioning covers package.json as a version source; 94_repository_inventory gains the npm package table and services/apps Type C; lib index updated Verified: bundle regen (24 modules, client 16, drone 3, repos 13); registry client readers exercised against the vendored bundle.
This commit is contained in:
171
registry.py
171
registry.py
@@ -9,7 +9,8 @@ Resources:
|
||||
docs://index, docs://nav, docs://modules/{module} (from the base server)
|
||||
docs://spec (the pinned auth-server OpenAPI contract)
|
||||
registry://libraries (the structured catalog)
|
||||
registry://components (the template component index)
|
||||
registry://components (the app template component index)
|
||||
registry://client (the client template component index)
|
||||
registry://drone (the CI template index)
|
||||
registry://release (the release template index)
|
||||
registry://repos (the repository inventory)
|
||||
@@ -20,7 +21,10 @@ Tools:
|
||||
list_app_components (the template component kinds)
|
||||
get_app_component (kind -> exact template file, seed tokens renamed)
|
||||
get_app_template (the whole app skeleton as a path -> content map)
|
||||
list_drone_templates (the two CI shapes: PyPI package / Docker deployment)
|
||||
list_client_components (the client template component kinds)
|
||||
get_client_component (kind -> exact client template file)
|
||||
get_client_template (the whole client skeleton as a path -> content map)
|
||||
list_drone_templates (the three CI shapes: PyPI package / npm package / Docker deployment)
|
||||
get_drone_template (kind -> exact .drone.yml template)
|
||||
list_release_templates (the changelog + version-file artefacts)
|
||||
get_release_template (kind -> exact release template file)
|
||||
@@ -91,10 +95,14 @@ def _build_text(app_name: str, resources: str, pipelines: str, auth: str) -> str
|
||||
"4. Scaffold with `get_app_template()` (whole tree) or pull single "
|
||||
"files with `list_app_components()` and "
|
||||
"`get_app_component(kind, resource=..., family=...)`.\n"
|
||||
"5. If auth is needed, read `docs://spec` (the auth-server contract) and "
|
||||
"5. If a UI is needed, scaffold the client with `get_client_template()` "
|
||||
"(or pull files via `list_client_components()` / "
|
||||
"`get_client_component(kind)`) — the admin renders from your spec's x- "
|
||||
"extensions; see `docs://modules/ui_admin`.\n"
|
||||
"6. If auth is needed, read `docs://spec` (the auth-server contract) and "
|
||||
"`docs://modules/auth_integration`; declare `securitySchemes.bearerAuth` "
|
||||
"and `security` in your spec — do not write auth code.\n"
|
||||
"6. Author only Tier C; keep Tier B as shipped.\n"
|
||||
"7. Author only Tier C; keep Tier B as shipped.\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -103,8 +111,12 @@ _COMPLIANCE_TEXT = (
|
||||
"Read `docs://modules/drone_pipelines` and `docs://modules/versioning_and_changelog`,\n"
|
||||
"then enumerate the working set with `registry://repos` (or `list_repos()`).\n\n"
|
||||
"For each repository, bring it to the standard for its `type`:\n\n"
|
||||
"- **package** — `version` in `pyproject.toml`; root `CHANGELOG.md`; Drone file\n"
|
||||
" from `get_drone_template(\"drone-pypi\")` (`python-lib.yaml`, tag-triggered).\n"
|
||||
"- **package** (Python) — `version` in `pyproject.toml`; root `CHANGELOG.md`;\n"
|
||||
" Drone file from `get_drone_template(\"drone-pypi\")` (`python-lib.yaml`,\n"
|
||||
" tag-triggered).\n"
|
||||
"- **package** (npm) — `version` in `package.json`; root `CHANGELOG.md`;\n"
|
||||
" Drone file from `get_drone_template(\"drone-npm\")` (`npm-lib.yaml`,\n"
|
||||
" tag-triggered, publishes to the private npm registry).\n"
|
||||
"- **service** — root `VERSION`; root `CHANGELOG.md`; Drone file from\n"
|
||||
" `get_drone_template(\"drone-service\")` (`service-docker.yaml`, tag-triggered\n"
|
||||
" with a `tag == VERSION` guard). The service must read its version from\n"
|
||||
@@ -291,6 +303,58 @@ class StandardsMCPServer(MCPServer):
|
||||
"content": text,
|
||||
}
|
||||
|
||||
def _client(self) -> dict[str, Any]:
|
||||
"""Load the client template component manifest."""
|
||||
path = Path(self.mcp_root) / "client.json"
|
||||
if not path.exists():
|
||||
return {"error": "not_found", "path": str(path), "components": []}
|
||||
return json.loads(path.read_bytes())
|
||||
|
||||
def _find_client_component(self, kind: str) -> dict[str, Any] | None:
|
||||
for entry in self._client().get("components", []):
|
||||
if entry.get("kind") == kind:
|
||||
return entry
|
||||
return None
|
||||
|
||||
def _read_client_component(self, entry: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Read a vendored client template file."""
|
||||
rel = entry["path"]
|
||||
path = Path(self.mcp_root) / "templates" / "client" / rel
|
||||
if not path.exists():
|
||||
return {"error": "not_found", "path": str(path)}
|
||||
return {
|
||||
"kind": entry["kind"],
|
||||
"path": rel,
|
||||
"placeholder": entry.get("placeholder"),
|
||||
"content": path.read_text(encoding="utf-8"),
|
||||
}
|
||||
|
||||
def _client_files(self, prefix: str | None = None) -> dict[str, str]:
|
||||
"""Read the vendored client template tree as a path -> content map.
|
||||
|
||||
Args:
|
||||
prefix (str | None): Optional path prefix filter with POSIX
|
||||
separators, for example ``"src/shared-theme/"``.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Relative POSIX path -> UTF-8 file content,
|
||||
excluding caches and build output.
|
||||
"""
|
||||
root = Path(self.mcp_root) / "templates" / "client"
|
||||
files: dict[str, str] = {}
|
||||
if not root.is_dir():
|
||||
return files
|
||||
for path in sorted(root.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if "__pycache__" in path.parts or path.suffix == ".pyc":
|
||||
continue
|
||||
rel = path.relative_to(root).as_posix()
|
||||
if prefix and not rel.startswith(prefix):
|
||||
continue
|
||||
files[rel] = path.read_text(encoding="utf-8")
|
||||
return files
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Standards surface
|
||||
# ------------------------------------------------------------------
|
||||
@@ -304,6 +368,16 @@ class StandardsMCPServer(MCPServer):
|
||||
def components() -> dict[str, Any]:
|
||||
return self._components()
|
||||
|
||||
@self.app.resource("registry://client")
|
||||
def client() -> dict[str, Any]:
|
||||
"""Return the client template component manifest.
|
||||
|
||||
The React client skeleton (Vite + MUI) that pairs with the app
|
||||
template; the admin UI renders from the app's openapi.yaml x-
|
||||
extensions via @aetoskia/react-openapi.
|
||||
"""
|
||||
return self._client()
|
||||
|
||||
@self.app.resource("registry://drone")
|
||||
def drone() -> dict[str, Any]:
|
||||
return self._drone()
|
||||
@@ -434,13 +508,77 @@ class StandardsMCPServer(MCPServer):
|
||||
"files": files,
|
||||
}
|
||||
|
||||
@self.app.tool()
|
||||
def list_client_components() -> list[dict[str, Any]]:
|
||||
"""List the reusable component kinds shipped in the client template.
|
||||
|
||||
The React client (Vite + MUI) that pairs with the app template:
|
||||
the admin shell rendering the app's resources from its
|
||||
``openapi.yaml``. See :func:`get_client_template` for the whole
|
||||
tree and :func:`list_drone_templates` ("drone-npm") for its CI.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]:
|
||||
Entries with ``kind``, ``path``, ``placeholder`` and
|
||||
``description``. Fetch one with :func:`get_client_component`.
|
||||
"""
|
||||
return self._client().get("components", [])
|
||||
|
||||
@self.app.tool()
|
||||
def get_client_component(kind: str) -> dict[str, Any]:
|
||||
"""Return the exact client template file for a component kind.
|
||||
|
||||
Use this to copy current, app-neutral client code instead of
|
||||
inventing it. See :func:`list_client_components` for valid kinds
|
||||
(e.g. "client-home", "client-header", "client-openapi-config").
|
||||
|
||||
Args:
|
||||
kind (str): Component kind, e.g. "client-openapi-config".
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: ``{kind, path, placeholder, content}`` or an
|
||||
error entry when the kind is unknown.
|
||||
"""
|
||||
entry = self._find_client_component(kind)
|
||||
if entry is None:
|
||||
return {
|
||||
"error": "unknown_kind",
|
||||
"kind": kind,
|
||||
"available": [c["kind"] for c in self._client().get("components", [])],
|
||||
}
|
||||
return self._read_client_component(entry)
|
||||
|
||||
@self.app.tool()
|
||||
def get_client_template(prefix: str = "") -> dict[str, Any]:
|
||||
"""Return the whole client skeleton as a path -> content map.
|
||||
|
||||
Use this to scaffold the UI in one shot instead of pulling
|
||||
components one by one; combine with :func:`list_client_components`
|
||||
for the kind and description metadata.
|
||||
|
||||
Args:
|
||||
prefix (str): Optional path prefix filter with POSIX
|
||||
separators, for example ``"src/shared-theme/"``. Empty
|
||||
returns every file.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: ``{root, file_count, files: {relpath: content}}``.
|
||||
"""
|
||||
files = self._client_files(prefix or None)
|
||||
return {
|
||||
"root": "templates/client",
|
||||
"file_count": len(files),
|
||||
"files": files,
|
||||
}
|
||||
|
||||
@self.app.tool()
|
||||
def list_drone_templates() -> list[dict[str, Any]]:
|
||||
"""List the CI (Drone) template shapes.
|
||||
|
||||
There are exactly two types of ``.drone.yml`` in the org: a Python
|
||||
package published to the private PyPI index, and a Docker
|
||||
deployment that builds, pushes, and runs a container.
|
||||
There are exactly three types of ``.drone.yml`` in the org: a
|
||||
Python package published to the private PyPI index, an npm package
|
||||
published to the private npm registry, and a Docker deployment
|
||||
that builds, pushes, and runs a container.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]:
|
||||
@@ -455,10 +593,10 @@ class StandardsMCPServer(MCPServer):
|
||||
|
||||
Use this to copy the current ``.drone.yml`` shape instead of
|
||||
inventing one. See :func:`list_drone_templates` for valid kinds
|
||||
("drone-pypi", "drone-service").
|
||||
("drone-pypi", "drone-npm", "drone-service").
|
||||
|
||||
Args:
|
||||
kind (str): Component kind, e.g. "drone-pypi" or
|
||||
kind (str): Component kind, e.g. "drone-pypi", "drone-npm" or
|
||||
"drone-service".
|
||||
|
||||
Returns:
|
||||
@@ -480,7 +618,7 @@ class StandardsMCPServer(MCPServer):
|
||||
|
||||
These are the files every repo carries for releases: a Keep a
|
||||
Changelog ``CHANGELOG.md`` and, for services, a root ``VERSION``
|
||||
file. See standards "11_versioning_and_changelog".
|
||||
file. See standards "91_versioning_and_changelog".
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]:
|
||||
@@ -517,13 +655,14 @@ class StandardsMCPServer(MCPServer):
|
||||
def list_repos(type: str | None = None) -> list[dict[str, Any]]:
|
||||
"""List the repositories covered by the CI and versioning standards.
|
||||
|
||||
Each entry has: ``name``, ``type`` ("package" | "service"),
|
||||
Each entry has: ``name``, ``type`` ("package" | "service" | "app"),
|
||||
``path`` (relative to the aetos/ workspace root), ``version_source``
|
||||
("pyproject.toml" | "VERSION"), ``drone`` (the template file),
|
||||
``trigger`` ("tag"), ``docs`` kinds, and ``mcp``.
|
||||
("pyproject.toml" | "package.json" | "VERSION"), ``drone`` (the
|
||||
template file), ``trigger`` ("tag"), ``docs`` kinds, and ``mcp``.
|
||||
|
||||
Args:
|
||||
type (str | None): Optional filter, e.g. "package" or "service".
|
||||
type (str | None): Optional filter, e.g. "package", "service"
|
||||
or "app".
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: Matching repository entries.
|
||||
|
||||
Reference in New Issue
Block a user