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:
2026-10-20 12:02:31 +05:30
parent 406affcfcf
commit d7c344c9aa
44 changed files with 3251 additions and 94 deletions

View File

@@ -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.