@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +25 -1
- package/docs/contracts/mcp-cloud-scope.md +182 -0
- package/docs/contracts/mcp-phase-1-scope.md +8 -3
- package/docs/guidelines/agent-infra/mcp-request-signing.md +4 -0
- package/docs/mcp-server.md +11 -3
- package/docs/setup/mcp-cloud-endpoints.md +93 -0
- package/docs/setup/mcp-cloud-registry-listing.md +99 -0
- package/docs/setup/mcp-cloud-setup.md +152 -0
- package/docs/setup/mcp-r2-bootstrap.md +82 -0
- package/package.json +1 -1
- package/scripts/mcp_parity_smoke.py +146 -0
- package/scripts/pack_mcp_content.py +274 -0
- package/scripts/readme_linter.py +1 -1
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 —
|
|
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
|
|
10
|
-
> [`
|
|
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.
|
package/docs/mcp-server.md
CHANGED
|
@@ -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).
|
|
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
|
|
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
|
@@ -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())
|