feat: publish BJD CMS Content 0.7.0

This commit is contained in:
2026-08-24 07:04:34 +00:00
parent 3515f3f745
commit de550119dd
27 changed files with 1266 additions and 510 deletions
@@ -1,6 +1,6 @@
{
"name": "bjd-cms-content",
"version": "0.6.1+codex.20260823023816",
"version": "0.7.0+codex.20260823052431",
"description": "Portable guarded read/write MCP tools for multilingual BJD CMS content, with workflow guidance, no-op observations, public fetch, persistent fallback sessions, and trustworthy telemetry.",
"author": {
"name": "BJD website team"
+35 -22
View File
@@ -4,12 +4,12 @@ Portable, dependency-free Python stdio MCP server for guarded BJD CMS content
maintenance. It works with any MCP-capable Agent host; Codex is one optional
packaging target, not a runtime dependency.
Version 0.6.1 exposes 23 typed tools spanning diagnostics, reads, validation,
Version 0.7.0 exposes 23 typed tools spanning diagnostics, reads, validation,
drafts, updates, relations, lifecycle, media, column copy, audit rollback,
credential-free public fetch, frontend cache, and product 3D conversion. All CMS API traffic still goes
through the canonical `bjd-cms-content` helper. The MCP layer never accepts a
token as a tool argument and does not implement its own unrestricted HTTP or SQL
client. Its bundled fallback client is version 1.8.0.
client. Its bundled fallback client is version 1.9.0.
## Portable process contract
@@ -20,30 +20,38 @@ Requirements:
`scripts/cms_api.py` and `references/endpoints.json`.
- The token configuration documented by that skill.
Set `BJD_CMS_SKILL_ROOT` when the plugin and skill are installed separately. The
server also discovers common `skills/`, `.agents/skills/`, `.claude/skills/`,
`.codex/skills/`, and `.opencode/skills/` layouts.
The standard archive/installer layout discovers its sibling
`skill/bjd-cms-content` automatically. Set `BJD_CMS_SKILL_ROOT` only when the
plugin and skill are installed separately or an explicit override is desired.
The server also discovers common `skills/`, `.agents/skills/`,
`.claude/skills/`, `.codex/skills/`, and `.opencode/skills/` layouts.
## Private release distribution
## Public release distribution
Production distribution uses immutable release assets in the existing private
Gitea repository, not a clone of the floating `main` branch. Build a release
from an exact commit with:
Production distribution uses immutable release assets in the dedicated public
Gitea repository `gitea-admin/bjd-cms-content`, never a clone of floating
`main`. The website repository remains the authoritative development source;
the public repository is a generated distribution surface. Build from an exact
tested source commit with:
```text
python3 scripts/build-bjd-cms-plugin-release.py \
--version 0.6.1 --ref <exact-commit> --output-dir <private-output-directory>
--version 0.7.0 --ref <exact-commit> --output-dir <new-output-directory>
```
The builder packages the complete plugin, complete canonical skill,
host-specific adapter notes, POSIX/Windows versioned installers, internal file
hashes, `.tar.gz`, `.zip`, and external `SHA256SUMS`. It rejects symlinks,
credential/state-like files, populated token assignments, and private keys.
Users install a fixed Gitea tag and verify the checksum; no release archive or
MCP registration contains a CMS token.
Users install a fixed Gitea tag and verify the checksum; no public source,
release archive, prompt, or MCP registration contains a CMS token.
Detailed end-user instructions are under `distribution/install/` in source and
`install/` in a built archive. Validate the complete artifact with:
Detailed end-user instructions and the host-neutral Agent install-or-upgrade
prompt are under `distribution/install/` in source and `install/` in a built
archive. The same prompt handles first installation, side-by-side upgrade, and
already-current verification while preserving the previous rollback version
and private CMS configuration.
Validate the complete artifact with:
```text
python3 scripts/test-bjd-cms-plugin-release.py
@@ -60,7 +68,7 @@ Every MCP host ultimately starts the same process:
command: python3 (or python/py -3 on platforms that use that launcher)
args: /absolute/path/to/bjd-cms-content/scripts/server.py
cwd: /absolute/path/to/bjd-cms-content
env.BJD_CMS_SKILL_ROOT: /absolute/path/to/the/complete/bjd-cms-content-skill
env.BJD_CMS_SKILL_ROOT: optional override for a split/nonstandard install
transport: stdio
```
@@ -264,12 +272,13 @@ python3 scripts/client.py session < commands.jsonl
python3 scripts/client.py session --commands-file commands.jsonl
python3 scripts/client.py session --self-check
python3 scripts/client.py session --track-tools --ledger-file /absolute/private/coverage.json --commands-file acceptance.jsonl
python3 scripts/client.py session --track-tools --require-complete --ledger-file /absolute/private/coverage.json --commands-file acceptance.jsonl
```
Always invoke the `client.py` distributed beside the target `server.py`; do not
resolve a similarly named client from a global skill/plugin cache. After an
install or upgrade, run `native-smoke` before an acceptance test and require
`client_version=1.8.0`, `server_version=0.6.1`,
`client_version=1.9.0`, `server_version=0.7.0`,
`bundled_pair_verified=true`, and `tool_count=23`. A mismatch means the host is
loading a stale or mixed distribution; stop before any CMS call and update the
host registration or plugin copy.
@@ -315,12 +324,14 @@ human checkpoints, and tools without a functional observation.
`catalog_attempt_coverage_complete` means every name was merely attempted;
`catalog_coverage_complete` remains false for missing-argument/error probes and
becomes true only after every tool has a successful or explicitly accepted
guard-policy observation. While that explicit audit remains incomplete, the
outer `session_task_state` overrides the temptation to stop on one tool's local
completion: it marks ordinary success as one step, keeps
`safe_to_finish_catalog_audit=false`, identifies the next functionally unobserved
tool, and pauses only for a real human checkpoint or blocker. Tool-name coverage
does not by itself satisfy a comprehensive audit: `observed_outcome_counts` and
guard-policy observation. While tool-level catalog coverage remains incomplete,
the outer `session_task_state` marks ordinary success as one step, identifies
the next functionally unobserved tool, and pauses only for a real human
checkpoint or blocker. Once tool-level coverage is complete,
`safe_to_finish_catalog_audit=true` is consistent with that narrow state, but
`safe_to_finish_comprehensive_audit` and `safe_to_finish_acceptance_audit`
remain false until the deeper scenarios are complete. Tool-name coverage does
not by itself satisfy a comprehensive audit: `observed_outcome_counts` and
`comprehensive_scenario_gaps` separately require real/no-op update preflights,
no-op plus applied updates, relation change plus restoration, verify/publish/
unpublish, column no-op/change/restoration, and rollback preview/apply. The
@@ -334,6 +345,8 @@ that ordinary content tasks do not require full-catalog coverage. Each write sti
gets an isolated idempotency scope and every apply tool still performs fresh
version/preflight checks. `native-smoke` runs initialize, initialized
notification, ping, and tools/list without loading CMS credentials.
For unattended acceptance only, `--require-complete` rejects early close/EOF and
returns nonzero until catalog plus comprehensive scenario coverage are complete.
Hosts may optionally set `BJD_CMS_MCP_TRACE_FILE` to an absolute path in an
existing directory. The server creates/appends a private `0600` JSONL file and
+97 -21
View File
@@ -21,8 +21,8 @@ SERVER = Path(__file__).resolve().with_name("server.py")
MAX_ARGUMENT_BYTES = 1024 * 1024
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
SERVER_TIMEOUT_SECONDS = 120
CLIENT_VERSION = "1.8.0"
BUNDLED_SERVER_VERSION = "0.6.1"
CLIENT_VERSION = "1.9.0"
BUNDLED_SERVER_VERSION = "0.7.0"
PROTOCOL_VERSION = "2025-11-25"
FALLBACK_TRANSPORT = "fallback-mcp"
LEDGER_FORMAT_VERSION = 2
@@ -75,7 +75,7 @@ def usage(code: int = 0) -> NoReturn:
" python3 scripts/client.py [--pretty] list [--names-only] [--pretty]\n"
" python3 scripts/client.py [--pretty] [--result-only] call TOOL "
"[JSON_ARGS|--json-file FILE|--json-stdin]\n"
" python3 scripts/client.py session [--track-tools] [--ledger-file FILE] "
" python3 scripts/client.py session [--track-tools] [--require-complete] [--ledger-file FILE] "
"[--commands-file FILE]\n"
" python3 scripts/client.py session --self-check\n"
" python3 scripts/client.py native-smoke\n\n"
@@ -86,6 +86,8 @@ def usage(code: int = 0) -> NoReturn:
"Use --track-tools only for an explicit all-tools audit; it adds a catalog ledger and never "
"makes unused tools mandatory for ordinary content work. Add --ledger-file only when that "
"audit must resume across client processes; it stores tool names and counts, never content. "
"For unattended acceptance runs, --require-complete rejects an early close or EOF until "
"both catalog and required scenario coverage are complete. "
"--self-check safely proves that an "
"invalid JSONL command does not end the session. native-smoke checks initialize, "
"ping, and tools/list without loading CMS credentials. This fallback talks only to this "
@@ -690,6 +692,7 @@ def _run_jsonl_session(
*,
track_tools: bool = False,
ledger_file: str | None = None,
require_complete: bool = False,
) -> int:
input_stream = sys.stdin.buffer if source is None else source
with McpSession() as session:
@@ -717,6 +720,7 @@ def _run_jsonl_session(
outcome_counts = loaded_counts["outcome_counts"]
command_index = 0
ledger_error: str | None = None
closed = False
def persist_ledger() -> None:
nonlocal ledger_error
@@ -787,12 +791,13 @@ def _run_jsonl_session(
else False
)
correctable_error = latest_is_error and latest_retryable
must_continue = (
can_continue = (
continues
and not comprehensive_complete
and not checkpoint_required
and (not latest_is_error or correctable_error)
)
must_continue_catalog = can_continue and not coverage_complete
must_continue_comprehensive = can_continue and not comprehensive_complete
if comprehensive_complete:
session_next_action = (
"Catalog and comprehensive scenario coverage are complete. Verify cleanup and the "
@@ -866,22 +871,36 @@ def _run_jsonl_session(
comprehensive_gaps[0] if comprehensive_gaps else None
),
"ordinary_tasks_require_full_catalog": False,
"strict_completion_required": require_complete,
}
fields["session_task_state"] = {
"scope": "explicit-all-tools-audit",
"catalog_coverage_complete": coverage_complete,
"comprehensive_scenario_coverage_complete": comprehensive_complete,
"must_not_claim_catalog_audit_complete": not comprehensive_complete,
"must_not_claim_catalog_audit_complete": not coverage_complete,
"must_not_claim_comprehensive_audit_complete": not comprehensive_complete,
"tool_result_completion_is_only_one_step": True,
"must_continue_catalog_audit": must_continue,
"must_continue_catalog_audit": must_continue_catalog,
"must_continue_comprehensive_audit": must_continue_comprehensive,
"safe_to_pause_catalog_audit": (
coverage_complete
or checkpoint_required
or not continues
or (latest_is_error and not correctable_error)
),
"safe_to_pause_comprehensive_audit": (
comprehensive_complete
or checkpoint_required
or not continues
or (latest_is_error and not correctable_error)
),
"safe_to_finish_catalog_audit": comprehensive_complete,
"safe_to_finish_catalog_audit": coverage_complete,
"safe_to_finish_comprehensive_audit": comprehensive_complete,
"acceptance_audit_complete": comprehensive_complete,
"must_not_claim_acceptance_audit_complete": not comprehensive_complete,
"must_continue_acceptance_audit": must_continue_comprehensive,
"safe_to_finish_acceptance_audit": comprehensive_complete,
"strict_completion_required": require_complete,
"human_checkpoint_required": checkpoint_required,
"host_must_evaluate_cleanup_and_user_goal": True,
"next_functional_tool": (
@@ -916,6 +935,30 @@ def _run_jsonl_session(
unknown = sorted(set(command) - {"id", "method"})
if unknown:
raise SessionCommandFailure(f"Unknown close command field: {unknown[0]}")
close_fields = session_fields(continues=True)
close_state = close_fields.get("session_task_state", {})
if (
require_complete
and close_state.get("comprehensive_scenario_coverage_complete") is not True
):
_emit_jsonl(
{
"id": command_id,
"ok": False,
"session_id": session.session_id,
"closed": False,
"error": {
"code": "incomplete-audit",
"message": (
"Strict acceptance mode rejected close because catalog or "
"comprehensive scenario coverage is incomplete."
),
},
**close_fields,
}
)
continue
closed = True
_emit_jsonl(
{
"id": command_id,
@@ -973,20 +1016,21 @@ def _run_jsonl_session(
raise SessionCommandFailure("call.tool must be a non-empty string")
if not isinstance(arguments, dict):
raise SessionCommandFailure("call.arguments must be an object")
if tool not in catalog_names:
raise SessionCommandFailure(f"Unknown tool: {tool}")
structured, is_error = session.tool(tool, arguments)
if tool in catalog_names:
tool_counts[tool] = tool_counts.get(tool, 0) + 1
if not is_error:
successful_counts[tool] = successful_counts.get(tool, 0) + 1
elif structured.get("human_checkpoint", {}).get("required") is True:
pending_checkpoint_counts[tool] = pending_checkpoint_counts.get(tool, 0) + 1
elif structured.get("reason_code") == "cache-prerequisites-not-proven":
guarded_policy_counts[tool] = guarded_policy_counts.get(tool, 0) + 1
outcome = _coverage_outcome(tool, structured, is_error=is_error)
if outcome is not None:
tool_outcomes = outcome_counts.setdefault(tool, {})
tool_outcomes[outcome] = tool_outcomes.get(outcome, 0) + 1
persist_ledger()
tool_counts[tool] = tool_counts.get(tool, 0) + 1
if not is_error:
successful_counts[tool] = successful_counts.get(tool, 0) + 1
elif structured.get("human_checkpoint", {}).get("required") is True:
pending_checkpoint_counts[tool] = pending_checkpoint_counts.get(tool, 0) + 1
elif structured.get("reason_code") == "cache-prerequisites-not-proven":
guarded_policy_counts[tool] = guarded_policy_counts.get(tool, 0) + 1
outcome = _coverage_outcome(tool, structured, is_error=is_error)
if outcome is not None:
tool_outcomes = outcome_counts.setdefault(tool, {})
tool_outcomes[outcome] = tool_outcomes.get(outcome, 0) + 1
persist_ledger()
_emit_jsonl(
{
"id": command_id,
@@ -1016,6 +1060,28 @@ def _run_jsonl_session(
**session_fields(continues=True),
}
)
if require_complete and not closed:
final_fields = session_fields(continues=False)
final_state = final_fields.get("session_task_state", {})
if final_state.get("comprehensive_scenario_coverage_complete") is not True:
_emit_jsonl(
{
"id": None,
"ok": False,
"session_id": session.session_id,
"closed": False,
"session_ended": True,
"error": {
"code": "incomplete-audit",
"message": (
"Strict acceptance session reached EOF before catalog and "
"comprehensive scenario coverage were complete."
),
},
**final_fields,
}
)
return 3
return 0
@@ -1079,6 +1145,7 @@ def main(argv: list[str] | None = None) -> int:
commands = b'not-json\n{"id":"recovered","method":"ping"}\n{"id":"done","method":"close"}\n'
return _run_jsonl_session(io.BytesIO(commands))
track_tools = False
require_complete = False
commands_file: str | None = None
ledger_file: str | None = None
index = 0
@@ -1090,6 +1157,12 @@ def main(argv: list[str] | None = None) -> int:
track_tools = True
index += 1
continue
if option == "--require-complete":
if require_complete:
raise ClientFailure("--require-complete may be provided only once")
require_complete = True
index += 1
continue
if option == "--commands-file":
if commands_file is not None:
raise ClientFailure("--commands-file may be provided only once")
@@ -1109,11 +1182,14 @@ def main(argv: list[str] | None = None) -> int:
raise ClientFailure(f"Unknown session option: {option}")
if ledger_file is not None and not track_tools:
raise ClientFailure("--ledger-file requires --track-tools")
if require_complete and not track_tools:
raise ClientFailure("--require-complete requires --track-tools")
source = io.BytesIO(_read_file(commands_file)) if commands_file is not None else None
return _run_jsonl_session(
source,
track_tools=track_tools,
ledger_file=ledger_file,
require_complete=require_complete,
)
if command == "native-smoke":
if len(args) != 1 or result_only:
File diff suppressed because it is too large Load Diff