feat: publish BJD CMS Content 0.7.0
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user