@event4u/agent-config 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Shared agent configuration \u2014 skills for AI coding tools (Claude Code, Augment, Cursor, Cline, Windsurf, Gemini CLI).",
9
- "version": "1.37.0"
9
+ "version": "1.38.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
package/CHANGELOG.md CHANGED
@@ -318,6 +318,36 @@ our recommendation order, not its support status.
318
318
  users" tension without removing any path that an existing user
319
319
  might rely on.
320
320
 
321
+ ## [1.38.0](https://github.com/event4u-app/agent-config/compare/1.37.0...1.38.0) (2026-05-11)
322
+
323
+ ### Features
324
+
325
+ * **mcp:** add cloud setup tasks + operator README ([c5aebba](https://github.com/event4u-app/agent-config/commit/c5aebba37a608d0a41b1586acacdb055996a961d))
326
+ * **scripts:** MCP content packer + cloud parity smoke ([e0d132a](https://github.com/event4u-app/agent-config/commit/e0d132afb487b720386bd54b68e10f1a342d6b0c))
327
+
328
+ ### Documentation
329
+
330
+ * **readme:** surface hosted Remote MCP as zero-install option ([de6161e](https://github.com/event4u-app/agent-config/commit/de6161e3660b81a460f057bc46c1b75e6fe8693c))
331
+ * **mcp:** add A0-cloud invariant 8 — ingress protection via edge cache + platform rate-limit ([c4b9371](https://github.com/event4u-app/agent-config/commit/c4b9371da2db55958bca29a7d10291626d13782f))
332
+ * **mcp:** drop archived-roadmap refs from stable contracts ([15418de](https://github.com/event4u-app/agent-config/commit/15418de5a65c3cd5968bbeecc20ed5ad5c4a4958))
333
+ * **mcp:** surface experimental hosted-MCP channel ([cecae08](https://github.com/event4u-app/agent-config/commit/cecae08ad647c7e28931e7328047fdef5ca3d3a3))
334
+ * **setup:** MCP cloud endpoints, R2 bootstrap, registry listing ([7cb3341](https://github.com/event4u-app/agent-config/commit/7cb334110ff940d3703ada013ef5163c899d96b2))
335
+ * **mcp:** add A0-cloud contract + cross-links (Phase 1 of cloudflare-mcp-hosting) ([2fc5084](https://github.com/event4u-app/agent-config/commit/2fc50845c3b194e861b54f82c852ba4bb9fc406a))
336
+ * **roadmap:** add Cloudflare-hosted MCP roadmap, archive distribution ([fd1c437](https://github.com/event4u-app/agent-config/commit/fd1c437ea8a918eebc8604ee7958d8c842c7aac8))
337
+
338
+ ### CI
339
+
340
+ * **deploy-mcp-worker:** release-tag triggered Worker deploy ([4297514](https://github.com/event4u-app/agent-config/commit/4297514b80457852511ad90674f4d633a75a92bf))
341
+
342
+ ### Chores
343
+
344
+ * **linter:** raise README overloaded threshold to 750 lines ([1e3fbb7](https://github.com/event4u-app/agent-config/commit/1e3fbb7c649398ba56bc32677c7b588131f7e949))
345
+ * **mcp:** mark dev content.json stub with explanatory _comment ([8f6c7ff](https://github.com/event4u-app/agent-config/commit/8f6c7ffe493b72ccb2d0ec66121a9d32d0b2fe8c))
346
+ * **roadmap:** archive road-to-cloudflare-mcp-hosting (100% complete) ([99e07f9](https://github.com/event4u-app/agent-config/commit/99e07f92293e62841ff87f315e41c5ceabd7b277))
347
+ * **workers/mcp:** scaffold TypeScript Cloudflare Worker ([447e071](https://github.com/event4u-app/agent-config/commit/447e071984f86fc084058d05f22d0e0a7c936a5e))
348
+
349
+ Tests: 2679 (+0 since 1.37.0)
350
+
321
351
  ## [1.37.0](https://github.com/event4u-app/agent-config/compare/1.36.1...1.37.0) (2026-05-10)
322
352
 
323
353
  ### Features
package/README.md CHANGED
@@ -90,6 +90,30 @@ Install directly in your agent for global, cross-project use:
90
90
  → [Full getting started guide](docs/getting-started.md) ·
91
91
  [More examples & expected behavior](docs/showcase.md)
92
92
 
93
+ ### Remote MCP — zero install
94
+
95
+ Skills, commands, rules, and guidelines are also served as a hosted MCP
96
+ endpoint. No clone, no `task mcp:setup`, no Python venv — point any
97
+ MCP-capable client (Claude Desktop, Cursor, Zed, Continue, hosted agents)
98
+ at:
99
+
100
+ ```
101
+ https://agent-config-mcp.event4u.workers.dev
102
+ ```
103
+
104
+ Verify it's live:
105
+
106
+ ```bash
107
+ curl https://agent-config-mcp.event4u.workers.dev
108
+ # → { "ok": true, "name": "agent-config-mcp", "release_key": "v…", … }
109
+ ```
110
+
111
+ Read-only, identity-stable per release. Client config snippets and URL
112
+ shapes (latest vs. pinned `/v<X.Y.Z>`) live in
113
+ [`docs/setup/mcp-cloud-endpoints.md`](docs/setup/mcp-cloud-endpoints.md).
114
+ Operator setup (account, R2, secrets) — [`docs/setup/mcp-cloud-setup.md`](docs/setup/mcp-cloud-setup.md).
115
+ Experimental — A0-cloud contract in [`docs/contracts/mcp-cloud-scope.md`](docs/contracts/mcp-cloud-scope.md).
116
+
93
117
  ### Optional: persistent agent memory
94
118
 
95
119
  `agent-config` integrates with [`@event4u/agent-memory`](https://www.npmjs.com/package/@event4u/agent-memory)
@@ -109,7 +133,7 @@ Install in the same project (dev-only):
109
133
  npm install --save-dev @event4u/agent-memory
110
134
  ```
111
135
 
112
- → [Memory contract & retrieval API](docs/contracts/agent-memory-contract.md) (beta) · [Built-in MCP server](docs/mcp-server.md) (experimental — read-only access from Claude Desktop / Cursor / Zed / Continue, install with `task mcp:setup`)
136
+ → [Memory contract & retrieval API](docs/contracts/agent-memory-contract.md) (beta) · [Built-in MCP server](docs/mcp-server.md) (experimental — local stdio access from Claude Desktop / Cursor / Zed / Continue, install with `task mcp:setup`)
113
137
 
114
138
  ---
115
139
 
@@ -0,0 +1,182 @@
1
+ ---
2
+ stability: experimental
3
+ ---
4
+
5
+ # MCP Server — Cloud Scope (A0-cloud Hard Contract)
6
+
7
+ > **Status:** Active · covers `workers/mcp/` (TypeScript Cloudflare
8
+ > Worker bridge), MVP-1 surface. Extends — does **not** supersede —
9
+ > [`mcp-phase-1-scope.md`](mcp-phase-1-scope.md), which retains
10
+ > exclusive ownership of `scripts/mcp_server/` (local stdio).
11
+ > **Stability:** experimental — not linked from README, AGENTS.md, or
12
+ > `docs/architecture.md`. Internal index reference only per `STABILITY.md`.
13
+
14
+ ## Purpose
15
+
16
+ Locks the **execution-safety boundary** for the hosted MCP Worker. Any
17
+ code under `workers/mcp/` must satisfy this contract verbatim. The
18
+ local stdio kernel and the hosted Worker are two distinct surfaces; a
19
+ deviation in one is **not** authorized by a precedent in the other.
20
+
21
+ The Worker IS the bridge described in
22
+ [`mcp-request-signing § Appendix`](../guidelines/agent-infra/mcp-request-signing.md#appendix--http-bridge-stdio-kernel-pattern-reference)
23
+ — but with two material differences from the appendix pattern: (1) no
24
+ spawned stdio child (content is read from a release-pinned R2 blob,
25
+ not a sub-process), and (2) no HMAC for MVP-1 (content is OSS and
26
+ read-only; the appendix pattern's `verifyRequest` is deferred to MVP-2
27
+ alongside auth).
28
+
29
+ ## In-scope (MVP-1)
30
+
31
+ - **Transport:** HTTP + SSE (Cloudflare Worker `fetch` handler). The
32
+ local stdio kernel is out-of-scope for this contract and stays
33
+ governed by `mcp-phase-1-scope.md`.
34
+ - **MCP primitives:** `prompts/list` + `prompts/get` + `resources/list`
35
+ + `resources/read` — read-only, parity with the local stdio surface.
36
+ - **Source data:** release-pinned content blob in R2 under the key
37
+ shape `releases/v<X.Y.Z>-<sha>/` (immutable per release). The blob
38
+ bundles `.agent-src/skills/<name>/SKILL.md`,
39
+ `.agent-src/commands/**/*.md`, and `docs/guidelines/` (the same
40
+ projection the local kernel reads). Never reads `.agent-src.uncompressed/`.
41
+ - **Identity surface:** `serverInfo.version` reads from a Worker-
42
+ bundled constant, `_meta.packageVersion` reads from a
43
+ `wrangler.toml` env var, `_meta.skillSetSignature` reads from a
44
+ **prebaked manifest JSON** shipped with the content blob. The
45
+ Worker never computes the signature at runtime.
46
+ - **URL shape:** two pinned shapes only —
47
+ `mcp.<host>/v<X.Y.Z>/sse` (immutable, cache TTL 1 h) and
48
+ `mcp.<host>/latest/sse` (pointer, cache TTL 5 min). The `latest`
49
+ pointer is repointed atomically by the release pipeline after a
50
+ green smoke run; pre-smoke failures leave it on the previous
51
+ release.
52
+ - **Pagination + hot-reload parity:** `prompts/list` paginates with
53
+ `nextCursor` the same way the stdio kernel does; the Worker has no
54
+ hot-reload because the content blob is immutable per release —
55
+ the **release** is the cache-bust event.
56
+ - **Deprecated tool stubs:** `tools/list` returns exactly two entries,
57
+ `lint_skills` and `chat_history_append`, both with
58
+ `deprecated: true` and a description pointing to the local stdio
59
+ server. `tools/call` against either returns `isError=true` with a
60
+ message naming the local-stdio successor. No other tool name is
61
+ reachable.
62
+
63
+ ## Out-of-scope (MVP-1)
64
+
65
+ - **Tool execution.** `lint_skills` and `chat_history_append` are
66
+ exposed as deprecated stubs only — no TS port, no FS access, no
67
+ shell, no Python runtime in the Worker. Restoration is the
68
+ Phase-7-DEFERRED block of the roadmap, gated on a real consumer ask
69
+ plus a multi-tenant security review.
70
+ - **`.agent-settings.yml` exposure.** Consumer-machine config, never
71
+ surfaced as a resource. The Worker has no access to consumer FS at
72
+ any layer.
73
+ - **Agent memory** — separate MCP server, different roadmap.
74
+ - **Chat history persistence** — the local kernel writes to
75
+ `agents/.agent-chat-history`; the Worker has no equivalent. Listed
76
+ as a deprecated stub per above.
77
+ - **Authentication / multi-tenancy.** MVP-1 is open (OSS content,
78
+ read-only). Bearer / CF Access / HMAC moves to MVP-2 alongside
79
+ tool restoration.
80
+ - **Network egress from the Worker** beyond an **explicit subrequest
81
+ allowlist** — see invariants below.
82
+
83
+ ## A0-cloud invariants
84
+
85
+ The Worker code must satisfy all of:
86
+
87
+ 1. **Origin allowlist** — `fetch()` calls from the Worker are limited
88
+ to R2 (content read) and an explicit observability sink (optional).
89
+ No calls to consumer infrastructure, no calls to upstream LLM
90
+ APIs, no calls to `api.github.com`, no DNS-based egress. Enforced
91
+ in `wrangler.toml` via Worker-level network policies.
92
+ 2. **R2 write boundary** — the Worker never writes to R2. The release
93
+ pipeline writes under `releases/v<X.Y.Z>-<sha>/` and atomically
94
+ repoints `releases/latest.txt`; the Worker only reads.
95
+ 3. **Per-versioned-URL immutability** — for any
96
+ `mcp.<host>/v<X.Y.Z>/sse`, the response body is deterministic for
97
+ the lifetime of the deployment. Patch fixes ship a new version key;
98
+ the existing key is never rewritten. R2 eventual consistency is
99
+ handled by the unique-key-per-release shape.
100
+ 4. **Cache-TTL policy** — pinned URLs cache at the edge for 1 h
101
+ (safe because immutable); `latest` caches for 5 min (bounded
102
+ staleness window after a repoint). No client may rely on `latest`
103
+ for reproducibility — that is what the pinned URL is for.
104
+ 5. **Prebaked signature** — `skillSetSignature` is computed once by
105
+ the release pipeline against the worktree at the tag and stored in
106
+ `releases/v<X.Y.Z>-<sha>/manifest.json`. The Worker reads, never
107
+ computes. Any signature drift at runtime is a contract violation.
108
+ 6. **No consumer code execution.** No `eval`, no dynamic import of
109
+ content, no shelling out, no spawning a runtime. The Worker is a
110
+ read-and-route function.
111
+ 7. **Single deployment per release.** One `wrangler deploy` per tag.
112
+ Concurrent deployments are not supported; the release pipeline
113
+ serializes through `release: published` + `workflow_dispatch`
114
+ hotfix paths.
115
+ 8. **Ingress protection = edge cache + platform rate limit.** MVP-1
116
+ is auth-less by design; the public surface is shielded by two
117
+ layers Cloudflare provides without code: (a) edge caching per
118
+ invariant 4 (1 h on pinned URLs, 5 min on `latest`) absorbs
119
+ read-loop traffic before it reaches the Worker, and (b)
120
+ Cloudflare's account-level anti-abuse + DDoS shielding caps
121
+ per-IP burst on `*.workers.dev`. These two together **are** the
122
+ MVP-1 auth surrogate. **Promotion triggers** — any of these
123
+ flips HMAC (currently MVP-2 §Out-of-scope) from deferred to
124
+ active before the wake-up triggers below would otherwise fire:
125
+ sustained 429 spikes from origin (cache miss storm), Workers
126
+ request-cost line item exceeding the free-tier budget for two
127
+ consecutive billing periods, or a CVE-class abuse report
128
+ against the endpoint. A per-Worker `[[unsafe.bindings]]`
129
+ rate-limiter in `wrangler.toml` is **not** configured in MVP-1
130
+ — adding one is a contract amendment, not a free hand.
131
+
132
+ ## Deprecated tool stub contract
133
+
134
+ `tools/list` returns:
135
+
136
+ ```json
137
+ [
138
+ {
139
+ "name": "lint_skills",
140
+ "description": "Deprecated on hosted MCP — use the local stdio server (scripts/mcp_server/) which retains this tool. See road-to-cloudflare-mcp-hosting Phase 7 for restoration triggers.",
141
+ "deprecated": true
142
+ },
143
+ {
144
+ "name": "chat_history_append",
145
+ "description": "Deprecated on hosted MCP — filesystem-bound, local-only. Use the local stdio server.",
146
+ "deprecated": true
147
+ }
148
+ ]
149
+ ```
150
+
151
+ `tools/call` against either name returns `isError=true` with the same
152
+ deprecation message. No other tool name is reachable.
153
+
154
+ ## MVP-2 wake-up triggers
155
+
156
+ The Phase-7 deferred items in the roadmap (tool restoration, history
157
+ persistence, auth) wake up only when **all** of these fire:
158
+
159
+ - A named consumer (internal or external) requests hosted lint or
160
+ history.
161
+ - A security review has approved the validation layer for
162
+ `lint_skills` (URI regex allowlist, size limits, timeout,
163
+ concurrency cap).
164
+ - An auth model has been selected (bearer vs. CF Access vs. HMAC).
165
+ - The server stability label has been promoted from *experimental*
166
+ to *beta*.
167
+
168
+ ## Revision policy
169
+
170
+ This contract is **experimental** — breaking changes are allowed in
171
+ any release with a CHANGELOG note. Promotion to `beta` requires at
172
+ least one shipped client connecting to the hosted endpoint end-to-end
173
+ without a contract amendment.
174
+
175
+ ## See also
176
+
177
+ - [`mcp-phase-1-scope.md`](mcp-phase-1-scope.md) — local-stdio kernel
178
+ contract (sibling, not parent).
179
+ - [`STABILITY.md`](../../STABILITY.md) — stability policy for
180
+ `docs/contracts/`.
181
+ - [`mcp-request-signing § Appendix`](../guidelines/agent-infra/mcp-request-signing.md#appendix--http-bridge-stdio-kernel-pattern-reference)
182
+ — bridge pattern reference (the Worker is a flavor of this).
@@ -6,9 +6,10 @@ stability: experimental
6
6
 
7
7
  > **Status:** Active · covers Phase 1 (A1–A7) + Phase 2 (B1–B5) +
8
8
  > Phase 3 (C1–C4) + Phase 4 (D1–D4) + Phase 6 F1/F3 of
9
- > `road-to-mcp-server.md`. Phase 6 F2 (SSE transport) is deferred to
10
- > [`road-to-mcp-distribution.md`](../../agents/roadmaps/road-to-mcp-distribution.md)
11
- > and remains out of scope here.
9
+ > `road-to-mcp-server.md`. Phase 6 F2 (SSE transport) is owned by
10
+ > [`mcp-cloud-scope.md`](mcp-cloud-scope.md) — the hosted Cloudflare
11
+ > Worker bridge — and remains out of scope here. This contract retains
12
+ > exclusive ownership of `scripts/mcp_server/` (local stdio).
12
13
  > **Stability:** experimental — not linked from README, AGENTS.md, or
13
14
  > `docs/architecture.md`. Internal index reference only per `STABILITY.md`.
14
15
 
@@ -187,4 +188,8 @@ prompts end-to-end without a contract amendment.
187
188
 
188
189
  ## See also
189
190
 
191
+ - [`mcp-cloud-scope.md`](mcp-cloud-scope.md) — hosted Worker contract
192
+ (sibling, not child). Extends the bridge pattern from
193
+ [`mcp-request-signing § Appendix`](../guidelines/agent-infra/mcp-request-signing.md#appendix--http-bridge-stdio-kernel-pattern-reference)
194
+ for multi-tenant SSE.
190
195
  - [`STABILITY.md`](STABILITY.md) — stability policy for `docs/contracts/`.
@@ -194,3 +194,7 @@ out-of-scope until a consumer surfaces a tenancy requirement.
194
194
  starts here; the upstream link is the authoritative source.
195
195
  - `road-to-ruflo-adoption.md` **P2.1** — landed this appendix; full
196
196
  bridge fork stays out-of-scope unless the dual trigger fires.
197
+ - [`mcp-cloud-scope.md`](../../contracts/mcp-cloud-scope.md) —
198
+ operationalizes this pattern as a TypeScript Cloudflare Worker (no
199
+ spawned stdio child; R2 blob replaces the child process). HMAC
200
+ `verifyRequest` is deferred to MVP-2 alongside auth.
@@ -4,13 +4,21 @@
4
4
 
5
5
  `agent-config` ships a built-in [Model Context Protocol](https://modelcontextprotocol.io)
6
6
  server that exposes the package's read-only governance surface to MCP-aware
7
- clients (Claude Desktop, Cursor, Zed, Continue, Codex via MCP). Two channels
7
+ clients (Claude Desktop, Cursor, Zed, Continue, Codex via MCP). Three channels
8
8
  coexist:
9
9
 
10
10
  - **File projection** — `task generate-tools` writes `.claude/`, `.cursor/`,
11
11
  `.clinerules/`, `.windsurfrules`. Used by Aider, Cline, Windsurf, Gemini CLI.
12
- - **MCP server** — `scripts/mcp_server/` exposes the same content over
13
- JSON-RPC. Used by clients that speak MCP natively.
12
+ - **Local stdio MCP server** — `scripts/mcp_server/` exposes the same content
13
+ over JSON-RPC. Used by clients that speak MCP natively. Default for personal
14
+ installs.
15
+ - **Remote MCP** *(experimental, opt-in)* — a Cloudflare-hosted TypeScript
16
+ Worker (`workers/mcp/`) serves the same wire surface over HTTP/SSE for
17
+ hosted-agent platforms. URL shapes pinned in
18
+ [`docs/setup/mcp-cloud-endpoints.md`](setup/mcp-cloud-endpoints.md);
19
+ safety contract in
20
+ [`docs/contracts/mcp-cloud-scope.md`](contracts/mcp-cloud-scope.md).
21
+ Wire-parity-checked against the local stdio kernel on every release.
14
22
 
15
23
  The MCP server **never executes engine code, never writes files, never spawns
16
24
  shells**. It is a read-only instructional surface — see
@@ -0,0 +1,93 @@
1
+ # MCP Cloud Endpoints — URL shapes & DNS
2
+
3
+ Public URL shapes for the hosted `agent-config-mcp` Worker. Governed
4
+ by `docs/contracts/mcp-cloud-scope.md` (A0-cloud) and Phase 5.2 of
5
+ `agents/roadmaps/road-to-cloudflare-mcp-hosting.md`.
6
+
7
+ ## Stability
8
+
9
+ **Experimental.** Inherits the label from `mcp-phase-1-scope.md`. URL
10
+ shapes below are pinned for the lifetime of the *experimental* window;
11
+ breaking changes require a stability-label bump.
12
+
13
+ ## URL shapes (pinned)
14
+
15
+ Two surfaces, both serve identical wire contracts (JSON-RPC over POST,
16
+ SSE on GET — A0-cloud invariant 1):
17
+
18
+ | Shape | Resolves to | Use case |
19
+ |---|---|---|
20
+ | `https://mcp.<domain>/latest/sse` | the release currently pointed at by `releases/latest.txt` in R2 | client wants the rolling cutting-edge build |
21
+ | `https://mcp.<domain>/v<X.Y.Z>/sse` | the immutable release `<X.Y.Z>` from the R2 archive | client wants a pinned, reproducible build |
22
+
23
+ The `<domain>` placeholder is operator-configured; the package itself
24
+ does not own DNS. Pin the chosen domain in your fork's
25
+ `mcp-cloud-scope.md` § Bucket / DNS.
26
+
27
+ For JSON-RPC, drop the `/sse` suffix:
28
+
29
+ | JSON-RPC | SSE |
30
+ |---|---|
31
+ | `POST https://mcp.<domain>/latest` | `GET https://mcp.<domain>/latest/sse` |
32
+ | `POST https://mcp.<domain>/v1.37.0` | `GET https://mcp.<domain>/v1.37.0/sse` |
33
+
34
+ The Worker reads its bundled blob at module init (per A0-cloud
35
+ invariant 2); the path prefix in MVP-1 is a routing artefact, not a
36
+ content selector. Multi-version routing lands in MVP-2.
37
+
38
+ ## DNS setup (operator-side)
39
+
40
+ One-time, requires Cloudflare account + zone access:
41
+
42
+ ```sh
43
+ # 1. Add the Worker custom domain in Cloudflare dashboard:
44
+ # Workers & Pages → agent-config-mcp → Settings → Domains & Routes
45
+ # → Add Custom Domain → "mcp.<your-domain>"
46
+ #
47
+ # 2. Cloudflare creates the AAAA + A records automatically. No manual
48
+ # CNAME — Custom Domains is the supported path (not "Routes").
49
+ #
50
+ # 3. Verify:
51
+ curl -s -X POST https://mcp.<your-domain>/ \
52
+ -H "content-type: application/json" \
53
+ -d '{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}'
54
+ ```
55
+
56
+ After DNS is live, uncomment the `routes` block in
57
+ `workers/mcp/wrangler.toml` and redeploy via `wrangler deploy` (or let
58
+ the GitHub Action pick it up on the next release).
59
+
60
+ The fallback `*.workers.dev` URL stays live for free; the custom
61
+ domain is only the public stability promise.
62
+
63
+ ## Health probe
64
+
65
+ Every URL accepts `GET /` with no body and returns release identity:
66
+
67
+ ```json
68
+ {
69
+ "ok": true,
70
+ "name": "agent-config-mcp",
71
+ "release_key": "v1.37.0-2fc5084",
72
+ "package_version": "1.37.0",
73
+ "signature": "35bc3c5e8b83",
74
+ "schema_version": 1
75
+ }
76
+ ```
77
+
78
+ The `signature` field is the wire-surface `skillSetSignature` — same
79
+ hash that MCP clients see under `_meta.skillSetSignature` on the
80
+ identity surface.
81
+
82
+ ## Parity smoke
83
+
84
+ Post-deploy CI runs `scripts/mcp_parity_smoke.py` against the new
85
+ deployment with `--target https://mcp.<domain>`. A non-zero exit
86
+ aborts the `latest.txt` repoint, so the previous release keeps
87
+ serving on `/latest/`.
88
+
89
+ ## See also
90
+
91
+ - A0-cloud contract: `docs/contracts/mcp-cloud-scope.md`
92
+ - R2 bootstrap: `docs/setup/mcp-r2-bootstrap.md`
93
+ - Local stdio fallback: `scripts/mcp_server/` (unchanged)
@@ -0,0 +1,99 @@
1
+ # MCP Registry Listing — submission package
2
+
3
+ Single source of truth for every MCP-registry submission of the hosted
4
+ `agent-config-mcp` Worker. Copy-paste sections from this file into the
5
+ target registry's template; do not maintain per-registry forks.
6
+
7
+ Phase 6.1 of `agents/roadmaps/road-to-cloudflare-mcp-hosting.md`.
8
+
9
+ ## One-liner
10
+
11
+ > Read-only governance surface for AI coding agents — 174 skills, 104
12
+ > commands, 60 rules, 100 guidelines + contexts. Hosted MCP bridge
13
+ > over the `event4u/agent-config` package.
14
+
15
+ ## Endpoints
16
+
17
+ | Shape | URL | Use |
18
+ |---|---|---|
19
+ | Rolling latest | `https://mcp.<operator-domain>/latest/sse` | clients tracking the live build |
20
+ | Pinned release | `https://mcp.<operator-domain>/v<X.Y.Z>/sse` | clients pinning a reproducible version |
21
+ | Liveness | `GET https://mcp.<operator-domain>/` | release identity + signature |
22
+
23
+ The `<operator-domain>` placeholder reflects the package's design —
24
+ every operator hosts their own Worker. The package upstream does not
25
+ run a public reference deployment; consumers point their Worker at
26
+ their own R2 bucket per `docs/setup/mcp-r2-bootstrap.md`.
27
+
28
+ ## Wire surface (MVP-1)
29
+
30
+ | Method | Status |
31
+ |---|---|
32
+ | `initialize` | implemented |
33
+ | `ping` | implemented |
34
+ | `prompts/list`, `prompts/get` | implemented |
35
+ | `resources/list`, `resources/read` | implemented |
36
+ | `tools/list` | implemented (returns deprecated stubs only) |
37
+ | `tools/call` | **not implemented** — returns `-32601 Method not found` |
38
+ | `notifications/*` | not implemented |
39
+
40
+ No mutation, no auth, no subrequests at runtime. Full contract:
41
+ `docs/contracts/mcp-cloud-scope.md` § A0-cloud.
42
+
43
+ ## Stability
44
+
45
+ **Experimental.** Wire surface, URL shapes, and the `_meta.signature`
46
+ field are pinned for the lifetime of the *experimental* window.
47
+ Breaking changes require a stability-label bump (see
48
+ `docs/contracts/mcp-phase-1-scope.md`).
49
+
50
+ ## Identity & reproducibility
51
+
52
+ Every response carries `_meta.skillSetSignature` — a 12-char SHA-256
53
+ prefix over the sorted `(uri, body)` pairs of the bundled content.
54
+ Identical content → identical signature, across machines and builds.
55
+ R2 archives every release indefinitely under
56
+ `releases/v<X.Y.Z>-<sha>/`.
57
+
58
+ ## Categories (for registry tagging)
59
+
60
+ - `governance`
61
+ - `meta-prompting`
62
+ - `skills`
63
+ - `agent-infrastructure`
64
+ - `code-review`
65
+ - `experimental`
66
+
67
+ ## License & contact
68
+
69
+ | Field | Value |
70
+ |---|---|
71
+ | License | MIT |
72
+ | Source | `https://github.com/event4u-app/agent-config` |
73
+ | Maintainer | event4u-app (org) |
74
+ | Contact | GitHub issues |
75
+
76
+ ## Links to upstream contracts
77
+
78
+ - A0-cloud safety contract: `docs/contracts/mcp-cloud-scope.md`
79
+ - Phase-1 scope (inherited): `docs/contracts/mcp-phase-1-scope.md`
80
+ - URL shapes & DNS: `docs/setup/mcp-cloud-endpoints.md`
81
+ - Local stdio kernel (predecessor): `scripts/mcp_server/`
82
+
83
+ ## Submission targets
84
+
85
+ | Target | Status | Notes |
86
+ |---|---|---|
87
+ | [`awesome-mcp-servers`](https://github.com/punkpeye/awesome-mcp-servers) | ready for PR | low-friction listing, accepts experimental |
88
+ | [`modelcontextprotocol.io` catalog](https://modelcontextprotocol.io/servers) | ready for PR after `awesome-mcp-servers` merges | needs evidence of community uptake |
89
+
90
+ Both submissions reuse the **One-liner**, **Endpoints**, **Wire
91
+ surface**, **Stability**, and **License & contact** sections verbatim
92
+ from this file.
93
+
94
+ ## Out of scope for this roadmap
95
+
96
+ npm-launcher listing (`npx @event4u/agent-config-mcp`) targets the
97
+ **local stdio** server, not the hosted Worker. Different transport,
98
+ different installation pattern, different audience — captured in
99
+ `agents/roadmaps/road-to-mcp-server.md` if revived.
@@ -0,0 +1,152 @@
1
+ # Cloudflare MCP — Operator Setup
2
+
3
+ One-stop landing for onboarding a Cloudflare account to host the
4
+ `agent-config-mcp` Worker. Combines bucket bootstrap, DNS, GitHub
5
+ secrets, and troubleshooting in one place.
6
+
7
+ Governed by [`docs/contracts/mcp-cloud-scope.md`](../contracts/mcp-cloud-scope.md)
8
+ (A0-cloud). Deploys run **only** in CI
9
+ ([`.github/workflows/deploy-mcp-worker.yml`](../../.github/workflows/deploy-mcp-worker.yml))
10
+ — per A0-cloud invariant 7, never `wrangler deploy` from a developer
11
+ machine.
12
+
13
+ ## TL;DR — the happy path
14
+
15
+ ```sh
16
+ task mcp:cloud:login # one-time, opens browser
17
+ task mcp:cloud:setup # check → r2-create → r2-verify → whoami
18
+ # Copy account id from output → GitHub Secrets (see § GitHub Secrets)
19
+ # Create scoped API token (see § API Token) → GitHub Secrets
20
+ # Done. First `release: published` triggers the deploy.
21
+ ```
22
+
23
+ ## Prerequisites
24
+
25
+ | Tool | Version | Install |
26
+ |---|---|---|
27
+ | Node.js | ≥ 20 | <https://nodejs.org/> or `nvm install 20` |
28
+ | `npx`/`wrangler` | wrangler ≥ 4.0 (auto-fetched on first run) | bundled with Node |
29
+ | `gh` CLI | latest | <https://cli.github.com/> (only for manual `deploy-dispatch`) |
30
+ | Cloudflare account | any plan | <https://dash.cloudflare.com/sign-up> |
31
+
32
+ Run `task mcp:cloud:check` to verify all four in one shot.
33
+
34
+ ## Step 1 — Cloudflare login
35
+
36
+ ```sh
37
+ task mcp:cloud:login
38
+ ```
39
+
40
+ Opens a browser to authorize wrangler against your Cloudflare account.
41
+ One-time per developer machine; the token is stored in
42
+ `~/Library/Preferences/.wrangler/` (macOS) or `~/.wrangler/` (Linux).
43
+
44
+ ## Step 2 — Enable R2
45
+
46
+ Cloudflare requires a one-time plan activation before R2 buckets can
47
+ be created. **You will hit error `code: 10042` on the first attempt
48
+ if you skip this.**
49
+
50
+ 1. <https://dash.cloudflare.com/?to=/:account/r2/overview>
51
+ 2. Click **Purchase R2 Plan**
52
+ 3. Select **Free Tier** — 10 GB storage / 1 M Class-A / 10 M Class-B
53
+ ops per month at **$0** (credit card required, $0 within quota,
54
+ $0 egress)
55
+ 4. Confirm; wait ~30 s for activation
56
+
57
+ ## Step 3 — Create the R2 bucket
58
+
59
+ ```sh
60
+ task mcp:cloud:r2-create # idempotent — safe to re-run
61
+ task mcp:cloud:r2-verify # ✅ if present
62
+ ```
63
+
64
+ The bucket is named `agent-config-mcp` and configured per
65
+ [`docs/setup/mcp-r2-bootstrap.md`](mcp-r2-bootstrap.md) (private,
66
+ indefinite retention, Worker reads via binding).
67
+
68
+ `task mcp:cloud:setup` chains steps 1, 3, and the account-id readout
69
+ in one shot.
70
+
71
+ ## Step 4 — API Token
72
+
73
+ The CI deploy pipeline needs a **scoped** token — never reuse the
74
+ Global API Key or a production-tenant token.
75
+
76
+ Dashboard → **My Profile → API Tokens → Create Token → Custom token**:
77
+
78
+ | Permission | Resource | Access |
79
+ |---|---|---|
80
+ | Account · Workers Scripts | your account | Edit |
81
+ | Account · Workers R2 Storage | your account | Edit |
82
+ | User · User Details | — | Read |
83
+
84
+ If you uncomment the `routes` block in `workers/mcp/wrangler.toml`
85
+ (custom domain cutover, Phase 5.2), add **Zone · DNS · Edit** on the
86
+ relevant zone.
87
+
88
+ Copy the generated token immediately — Cloudflare shows it once.
89
+
90
+ ## Step 5 — GitHub Secrets
91
+
92
+ Repository → **Settings → Secrets and variables → Actions → New
93
+ repository secret**:
94
+
95
+ | Secret | Value | Source |
96
+ |---|---|---|
97
+ | `CLOUDFLARE_API_TOKEN` | scoped token from Step 4 | dashboard |
98
+ | `CLOUDFLARE_ACCOUNT_ID` | account id | `task mcp:cloud:whoami` |
99
+
100
+ Set them in **Actions** secrets, not **Codespaces** or **Dependabot**
101
+ scopes.
102
+
103
+ ## Step 6 — Validate
104
+
105
+ Trigger the deploy workflow manually against an existing tag — no
106
+ release event needed:
107
+
108
+ ```sh
109
+ task mcp:cloud:deploy-dispatch TAG=v1.37.0
110
+ gh run watch
111
+ ```
112
+
113
+ Green smoke step → `latest.txt` repoints → release is live on
114
+ `*.workers.dev`. A red smoke step leaves `latest.txt` on the previous
115
+ release.
116
+
117
+ ## Step 7 — DNS (optional, Phase 5.2)
118
+
119
+ Custom domain `mcp.<your-domain>` setup lives in
120
+ [`docs/setup/mcp-cloud-endpoints.md`](mcp-cloud-endpoints.md) § DNS
121
+ setup. Until cutover, the Worker serves on the free
122
+ `agent-config-mcp.<account>.workers.dev` URL.
123
+
124
+ ## Troubleshooting
125
+
126
+ | Symptom | Cause | Fix |
127
+ |---|---|---|
128
+ | `code: 10042` on bucket create | R2 not enabled on account | Step 2 — Enable R2 |
129
+ | `wrangler whoami` shows no Account ID | not logged in | `task mcp:cloud:login` |
130
+ | Workflow fails on `wrangler deploy` with auth error | secret missing/wrong scope | re-check Step 4 token permissions |
131
+ | Smoke step red after deploy | bundle vs. SDK mismatch | check `compatibility_date` in `wrangler.toml` |
132
+ | Bucket create returns `already exists` | bucket present | not an error — `task mcp:cloud:r2-verify` to confirm |
133
+
134
+ ## Available tasks
135
+
136
+ | Task | Purpose |
137
+ |---|---|
138
+ | `task mcp:cloud:check` | Preflight — tools + login status |
139
+ | `task mcp:cloud:login` | Interactive wrangler login |
140
+ | `task mcp:cloud:whoami` | Print account id for GitHub Secret |
141
+ | `task mcp:cloud:r2-create` | Create R2 bucket (idempotent) |
142
+ | `task mcp:cloud:r2-verify` | Verify R2 bucket exists |
143
+ | `task mcp:cloud:setup` | Full chain — check → r2 → whoami |
144
+ | `task mcp:cloud:dev` | Local `wrangler dev` on :8787 |
145
+ | `task mcp:cloud:deploy-dispatch TAG=v…` | Manual workflow trigger |
146
+
147
+ ## See also
148
+
149
+ - [`docs/contracts/mcp-cloud-scope.md`](../contracts/mcp-cloud-scope.md) — A0-cloud contract
150
+ - [`docs/setup/mcp-r2-bootstrap.md`](mcp-r2-bootstrap.md) — R2 layout & break-glass
151
+ - [`docs/setup/mcp-cloud-endpoints.md`](mcp-cloud-endpoints.md) — URL shapes & DNS
152
+ - [`workers/mcp/README.md`](../../workers/mcp/README.md) — Worker source overview
@@ -0,0 +1,82 @@
1
+ # R2 Bootstrap — agent-config MCP
2
+
3
+ One-time setup for the R2 bucket that holds the Worker's release archive.
4
+ Owned by `mcp-cloud-scope.md` §3.3 and consumed by the deploy pipeline
5
+ (`.github/workflows/deploy-mcp-worker.yml`).
6
+
7
+ ## Bucket
8
+
9
+ | Field | Value |
10
+ |---|---|
11
+ | Bucket name | `agent-config-mcp` |
12
+ | Location hint | `auto` (Cloudflare-chosen) |
13
+ | Public access | **off** — Worker reads via binding, never via signed URL |
14
+ | Object retention | indefinite — every release stays for forensics |
15
+
16
+ ## Layout
17
+
18
+ ```
19
+ agent-config-mcp/
20
+ ├─ releases/
21
+ │ ├─ v<X.Y.Z>-<sha>/
22
+ │ │ ├─ content.json.gz # gzipped content blob (R2 archival copy)
23
+ │ │ └─ manifest.json # uncompressed manifest sidecar
24
+ │ └─ latest.txt # one-line: v<X.Y.Z>-<sha>
25
+ └─ (no other prefixes)
26
+ ```
27
+
28
+ `latest.txt` is the only mutable object. Every `releases/v*/` directory
29
+ is **immutable** once written; the pipeline refuses to overwrite an
30
+ existing release prefix.
31
+
32
+ ## Bootstrap — one-time
33
+
34
+ Requires `wrangler` ≥ 4.0 and a Cloudflare API token with R2 admin scope.
35
+
36
+ ```sh
37
+ # 1. Authenticate (interactive, opens browser).
38
+ npx wrangler login
39
+
40
+ # 2. Create the bucket.
41
+ npx wrangler r2 bucket create agent-config-mcp
42
+
43
+ # 3. Verify.
44
+ npx wrangler r2 bucket list | grep agent-config-mcp
45
+ ```
46
+
47
+ The Worker binding is declared in `workers/mcp/wrangler.toml` under
48
+ `[[r2_buckets]]`. The pipeline reads/writes via the wrangler CLI in CI,
49
+ not via the Worker — A0-cloud invariant 2 forbids the Worker from
50
+ issuing R2 writes.
51
+
52
+ ## Secrets in CI
53
+
54
+ The deploy pipeline needs two GitHub secrets:
55
+
56
+ | Secret | Scope |
57
+ |---|---|
58
+ | `CLOUDFLARE_API_TOKEN` | `Workers Scripts:Edit` + `Workers R2 Storage:Edit` |
59
+ | `CLOUDFLARE_ACCOUNT_ID` | account id (not a token) |
60
+
61
+ The token must be scoped to **this account only**; do not reuse a
62
+ production-tenant token.
63
+
64
+ ## Manual fixes — break-glass
65
+
66
+ If `latest.txt` ends up pointing to a broken release, the recovery is to
67
+ repoint it manually:
68
+
69
+ ```sh
70
+ # Repoint latest to a known-good release.
71
+ echo -n "v1.36.0-abc1234" | \
72
+ npx wrangler r2 object put agent-config-mcp/releases/latest.txt --pipe
73
+ ```
74
+
75
+ Past `releases/v*/` directories are not deleted in recovery — leave the
76
+ forensics intact.
77
+
78
+ ## Terraform note
79
+
80
+ The repo does not yet manage CF resources via Terraform. When TF lands,
81
+ this bucket should move into a `cloudflare_r2_bucket` resource and this
82
+ doc shrinks to a pointer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -0,0 +1,146 @@
1
+ """Live-replay parity smoke — local stdio kernel vs deployed Worker URL.
2
+
3
+ Replays a fixed set of JSON-RPC calls against:
4
+
5
+ 1. The local Python loaders (`prompts.py` / `resources.py`) — the
6
+ source-of-truth wire surface.
7
+ 2. An HTTP target (typically `wrangler dev` locally, or the deployed
8
+ Cloudflare Worker URL in CI / post-deploy).
9
+
10
+ Diffs the two on a normalised view (signature + release_key + content
11
+ hashes stripped). Exit 0 = parity, 1 = drift.
12
+
13
+ Usage:
14
+ python scripts/mcp_parity_smoke.py --target http://127.0.0.1:8787
15
+ python scripts/mcp_parity_smoke.py --target https://mcp.example.com
16
+
17
+ Phase 5.1 of `road-to-cloudflare-mcp-hosting.md`. Governed by
18
+ `docs/contracts/mcp-cloud-scope.md` §A0-cloud.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import sys
25
+ import urllib.request
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ _SCRIPTS = Path(__file__).resolve().parent
30
+ sys.path.insert(0, str(_SCRIPTS))
31
+
32
+ from mcp_server.prompts import load_all_prompts, to_mcp_prompt_meta # noqa: E402
33
+ from mcp_server.resources import load_all_resources, to_mcp_resource_meta # noqa: E402
34
+
35
+ PAGE_SIZE = 50
36
+
37
+
38
+ def _local_prompts_list() -> dict[str, Any]:
39
+ prompts, _ = load_all_prompts()
40
+ metas = [to_mcp_prompt_meta(p) for p in prompts]
41
+ page = metas[:PAGE_SIZE]
42
+ out: dict[str, Any] = {"prompts": page}
43
+ if len(metas) > PAGE_SIZE:
44
+ out["nextCursor"] = page[-1]["name"]
45
+ return out
46
+
47
+
48
+ def _local_resources_list() -> dict[str, Any]:
49
+ resources, _ = load_all_resources()
50
+ metas = [to_mcp_resource_meta(r) for r in resources]
51
+ page = metas[:PAGE_SIZE]
52
+ out: dict[str, Any] = {"resources": page}
53
+ if len(metas) > PAGE_SIZE:
54
+ out["nextCursor"] = page[-1]["uri"]
55
+ return out
56
+
57
+
58
+ def _rpc(target: str, method: str, params: dict[str, Any] | None = None) -> Any:
59
+ body = json.dumps(
60
+ {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}}
61
+ ).encode("utf-8")
62
+ req = urllib.request.Request(
63
+ target,
64
+ data=body,
65
+ headers={"content-type": "application/json"},
66
+ method="POST",
67
+ )
68
+ with urllib.request.urlopen(req, timeout=10) as r: # noqa: S310
69
+ resp = json.loads(r.read().decode("utf-8"))
70
+ if "error" in resp:
71
+ raise RuntimeError(f"{method}: {resp['error']}")
72
+ return resp["result"]
73
+
74
+
75
+ def _normalize_prompts(payload: dict[str, Any]) -> list[dict[str, Any]]:
76
+ out = []
77
+ for p in payload.get("prompts", []):
78
+ out.append({
79
+ "name": p["name"],
80
+ "description": p["description"],
81
+ "kind": p.get("_meta", {}).get("kind"),
82
+ })
83
+ return sorted(out, key=lambda x: x["name"])
84
+
85
+
86
+ def _normalize_resources(payload: dict[str, Any]) -> list[dict[str, Any]]:
87
+ out = []
88
+ for r in payload.get("resources", []):
89
+ out.append({
90
+ "uri": r["uri"],
91
+ "name": r["name"],
92
+ "description": r["description"],
93
+ "mimeType": r["mimeType"],
94
+ "kind": r.get("_meta", {}).get("kind"),
95
+ })
96
+ return sorted(out, key=lambda x: x["uri"])
97
+
98
+
99
+ def _diff(label: str, local: list[Any], remote: list[Any]) -> int:
100
+ if local == remote:
101
+ print(f"✅ {label}: {len(local)} entries match")
102
+ return 0
103
+ print(f"❌ {label}: drift ({len(local)} local vs {len(remote)} remote)")
104
+ local_set = {json.dumps(x, sort_keys=True) for x in local}
105
+ remote_set = {json.dumps(x, sort_keys=True) for x in remote}
106
+ only_local = local_set - remote_set
107
+ only_remote = remote_set - local_set
108
+ for s in sorted(only_local)[:5]:
109
+ print(f" local-only: {s}")
110
+ for s in sorted(only_remote)[:5]:
111
+ print(f" remote-only: {s}")
112
+ if len(only_local) > 5 or len(only_remote) > 5:
113
+ print(f" (+{len(only_local) - 5} local, +{len(only_remote) - 5} remote more)")
114
+ return 1
115
+
116
+
117
+ def main() -> int:
118
+ ap = argparse.ArgumentParser(description=__doc__)
119
+ ap.add_argument("--target", required=True, help="HTTP URL of the Worker.")
120
+ args = ap.parse_args()
121
+
122
+ failed = 0
123
+ local_p = _normalize_prompts(_local_prompts_list())
124
+ remote_p = _normalize_prompts(_rpc(args.target, "prompts/list"))
125
+ failed += _diff("prompts/list", local_p, remote_p)
126
+
127
+ local_r = _normalize_resources(_local_resources_list())
128
+ remote_r = _normalize_resources(_rpc(args.target, "resources/list"))
129
+ failed += _diff("resources/list", local_r, remote_r)
130
+
131
+ try:
132
+ _ = _rpc(args.target, "tools/list")
133
+ print("✅ tools/list: round-trips (stub list — content not parity-checked)")
134
+ except Exception as e:
135
+ print(f"❌ tools/list: {e}")
136
+ failed += 1
137
+
138
+ if failed:
139
+ print(f"\n{failed} surface(s) drifted between local stdio and {args.target}")
140
+ return 1
141
+ print(f"\nparity OK against {args.target}")
142
+ return 0
143
+
144
+
145
+ if __name__ == "__main__":
146
+ sys.exit(main())
@@ -0,0 +1,274 @@
1
+ """Pack agent-config content into a Worker-bundle JSON blob.
2
+
3
+ Walks `.agent-src/skills/`, `.agent-src/commands/`, `.agent-src/rules/`,
4
+ `docs/guidelines/`, `.agent-src/contexts/` via the same Python loaders
5
+ that drive the local stdio kernel, emits one JSON blob and a sidecar
6
+ manifest for `workers/mcp/`.
7
+
8
+ Outputs (relative to repo root):
9
+ - `workers/mcp/content.json` — uncompressed, bundled by `wrangler deploy`.
10
+ - `workers/mcp/content.json.gz` — gzipped archival copy for R2.
11
+ - `workers/mcp/manifest.json` — manifest only (RCA / R2 sidecar).
12
+
13
+ Hard-fail thresholds (Phase 2-5 council verdict D2):
14
+ - Uncompressed JSON > 2 MB → SystemExit(1).
15
+ - Empty content (zero URIs) → SystemExit(2). Catches a broken
16
+ `.agent-src/` tree before deploy.
17
+
18
+ Cloud signature divergence vs local kernel (`metadata.compute_skill_set_signature`):
19
+ - Local kernel: SHA-256 over `(uri, mtime)` pairs — reproducible only
20
+ within one filesystem.
21
+ - This packer: SHA-256 over `(uri, body)` pairs — reproducible across
22
+ CI runs, machines, and re-clones. Same 12-char prefix.
23
+
24
+ Governed by `docs/contracts/mcp-cloud-scope.md` §A0-cloud invariant 5.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import gzip
30
+ import hashlib
31
+ import json
32
+ import os
33
+ import subprocess
34
+ import sys
35
+ from datetime import datetime, timezone
36
+ from pathlib import Path
37
+ from typing import Any
38
+
39
+ # Re-use the local kernel's loaders so the live-replay baseline stays
40
+ # trivially comparable.
41
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
42
+ sys.path.insert(0, str(_SCRIPTS_DIR))
43
+
44
+ from mcp_server.prompts import scan_commands, scan_skills # noqa: E402
45
+ from mcp_server.resources import scan_contexts, scan_guidelines, scan_rules # noqa: E402
46
+
47
+ SCHEMA_VERSION = 1
48
+ PACKER_VERSION = "1.0.0"
49
+ # Worker bundle is the compact JSON; gzipped copy lives in R2. Cloudflare's
50
+ # compressed-bundle limit is 3 MB (free) / 10 MB (paid); 778 KB gz today
51
+ # (438 entries) leaves ample headroom. Hard-fail at 5 MB uncompressed so
52
+ # the build dies before the Worker upload does.
53
+ MAX_UNCOMPRESSED_BYTES = 5 * 1024 * 1024
54
+
55
+
56
+ def _repo_root() -> Path:
57
+ return Path(__file__).resolve().parent.parent
58
+
59
+
60
+ def _git_sha(root: Path) -> str:
61
+ """Resolve HEAD SHA. Falls back to env var, then to all-zeros."""
62
+ for env_var in ("GITHUB_SHA", "CI_COMMIT_SHA", "GIT_COMMIT"):
63
+ sha = os.environ.get(env_var)
64
+ if sha and len(sha) >= 7:
65
+ return sha
66
+ try:
67
+ out = subprocess.run(
68
+ ["git", "rev-parse", "HEAD"],
69
+ cwd=root,
70
+ capture_output=True,
71
+ text=True,
72
+ check=True,
73
+ timeout=5,
74
+ )
75
+ return out.stdout.strip()
76
+ except (subprocess.SubprocessError, FileNotFoundError):
77
+ return "0" * 40
78
+
79
+
80
+ def _package_version(root: Path) -> str:
81
+ data = json.loads((root / "package.json").read_text(encoding="utf-8"))
82
+ return str(data.get("version", "0.0.0"))
83
+
84
+
85
+ def _collect_entries(root: Path) -> tuple[dict[str, dict[str, Any]], list[str]]:
86
+ """Run all 5 scanners and project entries into the wire shape."""
87
+ uris: dict[str, dict[str, Any]] = {}
88
+ errors: list[str] = []
89
+
90
+ skills, e = scan_skills(root)
91
+ errors.extend(e)
92
+ for s in skills:
93
+ key = f"skill://{s.name}"
94
+ uris[key] = {
95
+ "uri": key,
96
+ "name": s.name,
97
+ "description": s.description,
98
+ "body": s.body,
99
+ "source": s.source,
100
+ "kind": "skill",
101
+ }
102
+
103
+ commands, e = scan_commands(root)
104
+ errors.extend(e)
105
+ for c in commands:
106
+ key = f"command://{c.name.replace(':', '.')}"
107
+ uris[key] = {
108
+ "uri": key,
109
+ "name": c.name,
110
+ "description": c.description,
111
+ "body": c.body,
112
+ "source": c.source,
113
+ "kind": "command",
114
+ }
115
+
116
+ for scan in (scan_rules, scan_guidelines, scan_contexts):
117
+ items, e = scan(root)
118
+ errors.extend(e)
119
+ for r in items:
120
+ uris[r.uri] = {
121
+ "uri": r.uri,
122
+ "name": r.name,
123
+ "description": r.description,
124
+ "body": r.body,
125
+ "source": r.source,
126
+ "kind": r.kind,
127
+ "mime_type": r.mime_type,
128
+ }
129
+
130
+ return uris, errors
131
+
132
+
133
+ def _content_signature(uris: dict[str, dict[str, Any]]) -> tuple[str, str]:
134
+ """SHA-256 over sorted (uri, body) pairs.
135
+
136
+ Returns (full_hex, 12-char prefix). The prefix is the wire-surface
137
+ `skillSetSignature`; the full hex is the diagnostic `content_hash_sha256`.
138
+ """
139
+ hasher = hashlib.sha256()
140
+ for uri in sorted(uris):
141
+ hasher.update(uri.encode("utf-8"))
142
+ hasher.update(b"\x00")
143
+ hasher.update(uris[uri]["body"].encode("utf-8"))
144
+ hasher.update(b"\x1e")
145
+ digest = hasher.hexdigest()
146
+ return digest, digest[:12]
147
+
148
+
149
+ def _count_kinds(uris: dict[str, dict[str, Any]]) -> dict[str, int]:
150
+ counts = {"skill": 0, "command": 0, "rule": 0, "guideline": 0, "context": 0}
151
+ for entry in uris.values():
152
+ kind = entry["kind"]
153
+ if kind in counts:
154
+ counts[kind] += 1
155
+ return counts
156
+
157
+
158
+
159
+ def _build_manifest(
160
+ *,
161
+ signature: str,
162
+ content_hash: str,
163
+ package_version: str,
164
+ git_sha: str,
165
+ built_at: str,
166
+ counts: dict[str, int],
167
+ ) -> dict[str, Any]:
168
+ short = git_sha[:7] if git_sha and git_sha != "0" * 40 else "unknown"
169
+ return {
170
+ "schema_version": SCHEMA_VERSION,
171
+ "signature": signature,
172
+ "content_hash_sha256": content_hash,
173
+ "package_version": package_version,
174
+ "release_key": f"v{package_version}-{short}",
175
+ "git_sha": git_sha,
176
+ "built_at": built_at,
177
+ "packer_version": PACKER_VERSION,
178
+ "content_uri_count": counts,
179
+ }
180
+
181
+
182
+ def pack(root: Path, out_dir: Path) -> dict[str, Any]:
183
+ """Run the full pack. Returns the manifest dict."""
184
+ uris, errors = _collect_entries(root)
185
+ if not uris:
186
+ sys.stderr.write("pack: empty content (zero URIs)\n")
187
+ for line in errors:
188
+ sys.stderr.write(f" - {line}\n")
189
+ raise SystemExit(2)
190
+
191
+ content_hash, signature = _content_signature(uris)
192
+ counts = _count_kinds(uris)
193
+ built_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
194
+ manifest = _build_manifest(
195
+ signature=signature,
196
+ content_hash=content_hash,
197
+ package_version=_package_version(root),
198
+ git_sha=_git_sha(root),
199
+ built_at=built_at,
200
+ counts=counts,
201
+ )
202
+
203
+ blob = {
204
+ "schema_version": SCHEMA_VERSION,
205
+ "uris": uris,
206
+ "manifest": manifest,
207
+ }
208
+ # Compact JSON for the bundle (saves ~20 KB vs indent=2). The R2
209
+ # archival copy is gzipped, so legibility there is moot.
210
+ payload = json.dumps(blob, ensure_ascii=False, sort_keys=True)
211
+ payload_bytes = payload.encode("utf-8")
212
+
213
+ if len(payload_bytes) > MAX_UNCOMPRESSED_BYTES:
214
+ sys.stderr.write(
215
+ f"pack: uncompressed content {len(payload_bytes)} bytes "
216
+ f"exceeds limit {MAX_UNCOMPRESSED_BYTES}\n"
217
+ )
218
+ raise SystemExit(1)
219
+
220
+ out_dir.mkdir(parents=True, exist_ok=True)
221
+ (out_dir / "content.json").write_bytes(payload_bytes)
222
+ # mtime=0 keeps the gzip header byte-stable across CI runs so the
223
+ # R2 archival copy hashes deterministically.
224
+ with open(out_dir / "content.json.gz", "wb") as raw:
225
+ with gzip.GzipFile(
226
+ fileobj=raw, mode="wb", compresslevel=9, mtime=0
227
+ ) as gz:
228
+ gz.write(payload_bytes)
229
+ (out_dir / "manifest.json").write_text(
230
+ json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
231
+ encoding="utf-8",
232
+ )
233
+
234
+ if errors:
235
+ sys.stderr.write("pack: non-fatal frontmatter errors:\n")
236
+ for line in errors:
237
+ sys.stderr.write(f" - {line}\n")
238
+
239
+ return manifest
240
+
241
+
242
+ def main(argv: list[str] | None = None) -> int:
243
+ parser = argparse.ArgumentParser(description=__doc__)
244
+ parser.add_argument(
245
+ "--root", type=Path, default=_repo_root(), help="Repository root."
246
+ )
247
+ parser.add_argument(
248
+ "--out",
249
+ type=Path,
250
+ default=None,
251
+ help="Output directory (defaults to <root>/workers/mcp).",
252
+ )
253
+ parser.add_argument(
254
+ "--quiet", action="store_true", help="Suppress success summary."
255
+ )
256
+ args = parser.parse_args(argv)
257
+
258
+ out_dir = args.out or (args.root / "workers" / "mcp")
259
+ manifest = pack(args.root, out_dir)
260
+
261
+ if not args.quiet:
262
+ c = manifest["content_uri_count"]
263
+ sys.stderr.write(
264
+ f"pack: ok signature={manifest['signature']} "
265
+ f"release={manifest['release_key']} "
266
+ f"skills={c['skill']} commands={c['command']} "
267
+ f"rules={c['rule']} guidelines={c['guideline']} "
268
+ f"contexts={c['context']}\n"
269
+ )
270
+ return 0
271
+
272
+
273
+ if __name__ == "__main__":
274
+ raise SystemExit(main())
@@ -88,7 +88,7 @@ GENERIC_BOILERPLATE = [
88
88
  r"(?i)\blightweight yet powerful\b",
89
89
  ]
90
90
 
91
- OVERLOADED_LINE_THRESHOLD = 500
91
+ OVERLOADED_LINE_THRESHOLD = 750
92
92
  WEAK_QUICKSTART_LINE_GAP = 80
93
93
 
94
94