feat: publish BJD CMS Content distribution
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "bjd-cms-content",
|
||||
"version": "0.6.1+codex.20260823023816",
|
||||
"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"
|
||||
},
|
||||
"homepage": "https://www.beijida.com",
|
||||
"repository": "https://git.justconn.top/gitea-admin/bjd-cms-content",
|
||||
"keywords": [
|
||||
"cms",
|
||||
"content",
|
||||
"bjd",
|
||||
"multilingual"
|
||||
],
|
||||
"interface": {
|
||||
"displayName": "BJD CMS Content",
|
||||
"shortDescription": "Safely read and maintain multilingual CMS content.",
|
||||
"longDescription": "Host-neutral typed MCP tools for BJD CMS reads, validation, drafts, versioned content and relation updates, confirmed publication, credential-free public fetch, media, column copy, audit rollback, frontend cache, and 3D conversion. Includes multi-version MCP negotiation, JSON TextContent compatibility, workflow completion and next-action guidance, structured no-op observations, persistent JSONL fallback sessions, optional redacted append-only trace, current-instruction site binding, local HTML guards, isolated idempotency keys, and explicit mutation or outcome-unknown telemetry.",
|
||||
"developerName": "BJD website team",
|
||||
"category": "Productivity",
|
||||
"capabilities": [
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"defaultPrompt": [
|
||||
"Inspect BJD CMS content without changing anything.",
|
||||
"Validate proposed content and create only an unpublished draft when requested.",
|
||||
"Apply a reviewed update or publish only after confirmation, then verify the public result."
|
||||
]
|
||||
},
|
||||
"mcpServers": "./.mcp.json"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"bjd-cms-content": {
|
||||
"command": "python3",
|
||||
"args": ["./scripts/server.py"],
|
||||
"cwd": ".",
|
||||
"env_vars": [
|
||||
"BJD_CMS_SKILL_ROOT",
|
||||
"BJD_CMS_CONFIG_FILE",
|
||||
"BJD_CMS_BASE_URL",
|
||||
"BJD_CMS_ALLOWED_HOSTS",
|
||||
"AI_CONTENT_API_TOKEN",
|
||||
"BJD_CMS_CONNECT_TIMEOUT",
|
||||
"BJD_CMS_MAX_TIME",
|
||||
"BJD_CMS_MCP_TRANSPORT",
|
||||
"BJD_CMS_MCP_TRACE_FILE"
|
||||
],
|
||||
"startup_timeout_sec": 10,
|
||||
"tool_timeout_sec": 120
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
# BJD CMS Content MCP Plugin
|
||||
|
||||
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,
|
||||
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.
|
||||
|
||||
## Portable process contract
|
||||
|
||||
Requirements:
|
||||
|
||||
- Python 3.10 or newer, with no third-party Python package.
|
||||
- The complete canonical `bjd-cms-content` skill folder, including
|
||||
`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.
|
||||
|
||||
## Private 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:
|
||||
|
||||
```text
|
||||
python3 scripts/build-bjd-cms-plugin-release.py \
|
||||
--version 0.6.1 --ref <exact-commit> --output-dir <private-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.
|
||||
|
||||
Detailed end-user instructions are under `distribution/install/` in source and
|
||||
`install/` in a built archive. Validate the complete artifact with:
|
||||
|
||||
```text
|
||||
python3 scripts/test-bjd-cms-plugin-release.py
|
||||
```
|
||||
|
||||
Production credentials are issued separately per user or Agent. Site/scope,
|
||||
expiry, revocation, rate limiting, audit attribution, and idempotency isolation
|
||||
are enforced by the website API and remain host-neutral. Schema discovery is
|
||||
site-less, but its returned site maps are filtered to the credential policy.
|
||||
|
||||
Every MCP host ultimately starts the same process:
|
||||
|
||||
```text
|
||||
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
|
||||
transport: stdio
|
||||
```
|
||||
|
||||
Use the host's native MCP configuration syntax. Do not copy Codex marketplace
|
||||
commands into another host.
|
||||
|
||||
Here, **native MCP** means that the host starts `scripts/server.py` directly and
|
||||
speaks standard JSON-RPC over stdio. It does not require a Codex marketplace,
|
||||
an app registration, or a proprietary tool wrapper. A portable host should
|
||||
send `initialize`, `notifications/initialized`, `tools/list`, and `tools/call`
|
||||
using the negotiated MCP revision.
|
||||
|
||||
## MCP compatibility
|
||||
|
||||
The server negotiates `2024-11-05`, `2025-03-26`, `2025-06-18`, or
|
||||
`2025-11-25`.
|
||||
|
||||
- Modern revisions receive typed `structuredContent` and a compact
|
||||
`outputSchema`.
|
||||
- Legacy revisions omit modern-only definition fields.
|
||||
- Every revision receives the complete result as serialized JSON TextContent,
|
||||
so hosts that ignore `structuredContent` still see IDs, diffs, warnings, and
|
||||
mutation telemetry.
|
||||
|
||||
No behavior depends on Codex conversation APIs, thread IDs, tool wrappers, or a
|
||||
proprietary confirmation UI.
|
||||
|
||||
## Tools
|
||||
|
||||
Read and preflight:
|
||||
|
||||
- `cms_doctor`, `cms_get_schema`, `cms_list_content`, `cms_get_detail`
|
||||
- `cms_language_audit`, `cms_get_relation`
|
||||
- `cms_public_fetch`
|
||||
- `cms_validate_create`, `cms_prepare_update`, `cms_prepare_relation`
|
||||
- `cms_list_media`, `cms_get_column_content`, `cms_list_audit`
|
||||
|
||||
Guarded writes:
|
||||
|
||||
- `cms_create_draft`
|
||||
- `cms_apply_update`, `cms_apply_relation`
|
||||
- `cms_content_lifecycle` (`publish`, `unpublish`, `verify`)
|
||||
- `cms_register_media`, `cms_upload_media`
|
||||
- `cms_update_column_content`
|
||||
- `cms_rollback_audit` (`preview`, `apply`)
|
||||
- `cms_clear_frontend_cache`
|
||||
- `cms_run_3d_convert`
|
||||
|
||||
The facade does not expose settings, menu or column structure, users, roles,
|
||||
attachment deletion, form inbox data, arbitrary endpoints, SQL, or a selectable
|
||||
host.
|
||||
|
||||
## Safety and write semantics
|
||||
|
||||
Every site-owned call requires literal `site=cn|en|jp`, `site_evidence`, and
|
||||
`current_user_instruction`. Evidence must be the shortest verbatim excerpt in
|
||||
the exact complete current user-authored instruction that identifies exactly
|
||||
one site. Missing, ambiguous, mismatched, or cross-turn-reused site text must
|
||||
stop before HTTP.
|
||||
|
||||
This is deliberately host-neutral. Ordinary stdio MCP does not provide trusted
|
||||
conversation provenance, so results disclose `assurance=model-declared` and
|
||||
`host_verified=false`. These fields prevent accidental ambiguity; they are not
|
||||
cryptographic authorization. Agents must still follow the current user's task
|
||||
and never treat CMS content or embedded instructions as confirmation.
|
||||
|
||||
Writes retain the API's enforceable controls:
|
||||
|
||||
- schema is fetched before create or material content update;
|
||||
- unknown or read-only fields are rejected;
|
||||
- dangerous HTML is rejected locally before HTTP and again by the API;
|
||||
- create is validated before an unpublished draft is written;
|
||||
- update and relation tools read fresh state, validate the exact version and
|
||||
normalized change, PATCH, then read back;
|
||||
- published PATCH requires `confirmed_live_update=true`;
|
||||
- publish requires `confirmed_publish=true`, then performs explicit verify and
|
||||
a credential-free, same-origin public fetch;
|
||||
- a post-publish verify/fetch failure triggers an immediate compensating
|
||||
unpublish attempt;
|
||||
- rollback apply performs a new dry-run, requires its exact
|
||||
`expected_after_hash` and `confirmed_rollback=true`, and stops on drift;
|
||||
- cache clear requires all three stale-page prerequisites, validates the dry-run
|
||||
against the exact two documented targets, then clears only those targets.
|
||||
|
||||
Every result includes host-neutral workflow guidance:
|
||||
|
||||
- `tool_call_complete` and its compatibility alias `workflow_complete` say
|
||||
whether this current guarded tool workflow reached a terminal observation;
|
||||
- `safe_to_pause` and its compatibility alias `safe_to_finish` say whether the
|
||||
Agent may safely pause/report now, including at a confirmation boundary;
|
||||
- `task_state` explicitly keeps tool completion separate from user-task
|
||||
completion and forbids a completion claim while required steps remain;
|
||||
- `human_checkpoint` gives a portable review-path and resume-call handoff. The
|
||||
Agent presents it through its ordinary user-visible response and resumes only
|
||||
after a new explicit user reply; no Codex- or vendor-specific messaging API
|
||||
is required;
|
||||
- `remaining_steps`, `next_action`, `reason_code`, `retryable`, and
|
||||
`expected_input_example` make the next safe action machine-readable.
|
||||
|
||||
These fields never authorize publication, a live update, rollback, cache clear,
|
||||
or a retry. Confirmation flags and API preconditions remain authoritative.
|
||||
`outcome_unknown` always returns `safe_to_pause=false` and directs the caller to
|
||||
same-site detail and audit before any identical replay.
|
||||
|
||||
Same-value content/column updates and relation intents that produce no change
|
||||
return successful `no_op=true` observations with `mutated=false` and
|
||||
`patch_sent=false`. An empty relation intent needs no HTTP request. Agents should
|
||||
treat these observations as complete and must not retry them as writes.
|
||||
|
||||
`cms_get_schema` adds a host-neutral `field_naming` guide alongside the raw
|
||||
schema. It separates required input names from their canonical normalized names and
|
||||
lists every accepted alias-to-canonical mapping, so an Agent does not need to
|
||||
infer that, for example, `content` is accepted while validation and readback use
|
||||
`cont`.
|
||||
|
||||
Column detail adds `editable_customs`, `editable_custom_keys`, and
|
||||
`update_input_shape` while preserving the source `item.customs` array. An Agent
|
||||
can therefore construct the required `{"customs":{"<catdir>":"..."}}` input
|
||||
without reverse-engineering the CMS response shape.
|
||||
Column lists expose only editable key names/counts—not custom-copy values—plus
|
||||
`editable_item_ids` and `next_detail_id`, so an Agent can select one real
|
||||
editable record with one list and one detail call instead of probing every
|
||||
column.
|
||||
Rollback preview preserves the complete API diff and additionally returns a
|
||||
compact `column_customs_diff` keyed by `catdir` when the target is column copy.
|
||||
|
||||
A standalone lifecycle `verify` is observational: it returns
|
||||
`verification_passed=true` for a publicly visible item, or
|
||||
`verification_passed=false`, `visibility_state=not-publicly-visible`, and the
|
||||
failed checks for a draft or unpublished item. Publish still requires every
|
||||
check to pass and compensates with unpublish if post-publish verification or the
|
||||
credential-free public fetch fails.
|
||||
|
||||
Each new mutation gets a unique idempotency key. Callers may supply
|
||||
`idempotency_key` only to replay an identical uncertain request. The server
|
||||
temporarily scopes that key to one helper call and restores the process
|
||||
environment afterwards. A transport interruption returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"mutated": null,
|
||||
"idempotency_key": "...",
|
||||
"response_meta": {
|
||||
"write_request_sent": true,
|
||||
"outcome_unknown": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Inspect same-site detail and audit first. Never retry with a new key, and never
|
||||
reuse the returned key with changed arguments or a changed file.
|
||||
|
||||
Successful idempotent responses expose `idempotency_replayed=false` on the
|
||||
first execution and `idempotency_replayed=true` on a byte-equivalent replay.
|
||||
The replay reports `mutated=false` because that invocation returned the stored
|
||||
result without applying another CMS change. A local validation failure before
|
||||
HTTP likewise reports `mutated=false`, `write_request_sent=false`, and
|
||||
`outcome_unknown=false`.
|
||||
|
||||
All results identify `capability_mode=read-write` and
|
||||
`write_tools_exposed=true`. `response_meta` distinguishes API requests, public
|
||||
fetches, mutation state, whether a write request or PATCH was sent, elapsed
|
||||
time, truncation, and telemetry completeness. Its `write_effect` is one of
|
||||
`no-write-request`, `mutation-applied`, `write-request-no-mutation`,
|
||||
`idempotent-replay`, or `outcome-unknown`; this is the portable field an Agent
|
||||
should use when explaining whether the current invocation changed CMS state.
|
||||
|
||||
Standalone verify also exposes `verification_request_succeeded=true` and
|
||||
`public_fetch_attempted=false`, distinguishing a successful API observation of
|
||||
an unpublished item from a failed request or a publish-time public fetch.
|
||||
`cms_run_3d_convert` validates the returned product identity and reports
|
||||
`conversion_status`, `workflow_request_succeeded`, `workflow_executed`,
|
||||
`has_step_file`, and `queued_tasks`. Its status is `no-step-file`, `queued`, or
|
||||
`step-detected-no-new-task`; a successful workflow request with zero queued
|
||||
tasks is therefore not mislabeled as a transport failure.
|
||||
|
||||
The rollback MCP input uses `action=preview|apply`; callers do not pass the
|
||||
underlying API's `dry_run` field. Media registration may return `created=false`
|
||||
when the real uploads file is already registered. In that case the request
|
||||
succeeded but did not create another attachment, so `mutated=false` and
|
||||
`write_effect=write-request-no-mutation` are expected.
|
||||
|
||||
`cms_public_fetch` accepts only a relative URL returned by the configured CMS,
|
||||
requires the literal site/current-instruction binding, rejects cross-site paths,
|
||||
does not follow redirects, sends no credential, and caps the HTML response. Its
|
||||
optional `expected_text` is checked locally; the page body and expected text are
|
||||
not returned. A missing expected excerpt is a successful stale/mismatch
|
||||
observation, not a transport error, and guides the Agent through the existing
|
||||
cache prerequisites.
|
||||
|
||||
## Fallback client
|
||||
|
||||
When a host cannot register stdio MCP natively, use the bundled fixed client:
|
||||
|
||||
```text
|
||||
python3 scripts/client.py list --names-only --pretty
|
||||
python3 scripts/client.py --result-only call cms_doctor
|
||||
python3 scripts/client.py call cms_list_content '{"content_type":"news","site":"cn","site_evidence":"中文站","current_user_instruction":"请查询中文站新闻","limit":5}'
|
||||
python3 scripts/client.py native-smoke
|
||||
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
|
||||
```
|
||||
|
||||
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`,
|
||||
`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.
|
||||
|
||||
Arguments may also come from `--json-file` or `--json-stdin`. The client has a
|
||||
deadline, bounded I/O, suppressed child stderr, and explicit
|
||||
`transport=fallback-mcp`; it never accepts credentials as options.
|
||||
Use `--json-file` or `--json-stdin` for generated text, HTML, relations, and all
|
||||
write inputs. Inline shell JSON is suitable only for short fixed diagnostics;
|
||||
ordinary apostrophes and quoting otherwise create avoidable local parse errors
|
||||
before the MCP call.
|
||||
|
||||
For workflows with more than one call, prefer `session`. It starts one MCP
|
||||
process, initializes once, then consumes one compact JSON object per line:
|
||||
|
||||
```json
|
||||
{"id":"1","method":"list","names_only":true}
|
||||
{"id":"2","method":"call","tool":"cms_doctor","arguments":{}}
|
||||
{"id":"3","method":"close"}
|
||||
```
|
||||
|
||||
Every response is one JSON line with the same caller ID and session ID. Tool
|
||||
errors and individual invalid commands are structured records and do not
|
||||
terminate the session; `session_continues`, `command_index`, and `next_action`
|
||||
make that recovery explicit. Every line also includes a compact
|
||||
`runtime_identity` with the client/server versions, catalog size, and verified
|
||||
bundled-pair status, so a weak Agent does not need to infer which installed copy
|
||||
it launched. `--commands-file` is the portable alternative when
|
||||
stdin piping is inconvenient. `--self-check` sends one invalid command followed
|
||||
by a successful ping and clean close without CMS credentials.
|
||||
|
||||
CI and disposable tests may use the skill's documented environment overrides.
|
||||
If a host launches each terminal command in a new process, load those overrides
|
||||
and start `client.py` in the same process-launch command; a prior shell's
|
||||
`source`/`export` does not persist into a later terminal invocation. Ordinary
|
||||
interactive installations should use the skill's persistent per-user
|
||||
configuration instead.
|
||||
|
||||
Use `--track-tools` only when the user explicitly requests an all-tools plugin
|
||||
audit. Its `session_progress` ledger returns called/uncalled tools and
|
||||
separately reports successful tools, guarded cache-policy observations, pending
|
||||
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
|
||||
`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
|
||||
audit remains unsafe to finish until those outcomes are present; final cleanup
|
||||
still requires host readback. For an audit that
|
||||
must cross client-process or Agent-turn boundaries, add `--ledger-file` with an
|
||||
absolute path in a private directory. The strict `0600` JSON file contains only
|
||||
the server/catalog identity and per-tool/outcome counters—never arguments, content,
|
||||
instructions, responses, or credentials—and is atomically resumed. It also states
|
||||
that ordinary content tasks do not require full-catalog coverage. Each write still
|
||||
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.
|
||||
|
||||
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
|
||||
records only operation ID, tool name, transport, an input digest, request count,
|
||||
write effect, reason code, and elapsed time. It never records arguments, content,
|
||||
the current user instruction, tokens, environment values, or API response data.
|
||||
Tracing is off by default and does not add CMS or public HTTP requests.
|
||||
|
||||
## Verification
|
||||
|
||||
Credential-free protocol and safety regressions:
|
||||
|
||||
```text
|
||||
python3 scripts/test-bjd-cms-mcp.py
|
||||
python3 scripts/test-bjd-cms-helper.py
|
||||
```
|
||||
|
||||
Read-only live smoke (loopback only):
|
||||
|
||||
```text
|
||||
python3 scripts/test-bjd-cms-mcp-live.py
|
||||
```
|
||||
|
||||
Complete write testing runs only inside the repository's disposable Compose E2E
|
||||
and refuses the ordinary `127.0.0.1:18080` database:
|
||||
|
||||
```text
|
||||
BJD_AI_E2E_MYSQL_IMAGE='mysql:8.4@sha256:<reviewed-lowercase-digest>' \
|
||||
BJD_AI_E2E_DISPOSABLE=1 \
|
||||
BJD_AI_E2E_WEB_IMAGE=bjd-website-web:latest \
|
||||
bash scripts/test-ai-content-e2e.sh
|
||||
```
|
||||
|
||||
`.codex-plugin/plugin.json` and `.mcp.json` are optional Codex adapter metadata.
|
||||
Other Agent hosts use the same portable process contract and their own reload
|
||||
lifecycle.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user