@event4u/agent-config 1.36.1 → 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 +77 -0
- package/README.md +25 -1
- package/docs/contracts/mcp-cloud-scope.md +182 -0
- package/docs/contracts/mcp-phase-1-scope.md +195 -0
- package/docs/guidelines/agent-infra/mcp-request-signing.md +4 -0
- package/docs/mcp-server.md +164 -0
- 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/docs/setup/mcp-server-docker.md +97 -0
- package/package.json +1 -1
- package/scripts/agent-config +29 -0
- package/scripts/mcp_parity_smoke.py +146 -0
- package/scripts/mcp_server/__init__.py +13 -0
- package/scripts/mcp_server/__main__.py +12 -0
- package/scripts/mcp_server/metadata.py +75 -0
- package/scripts/mcp_server/prompts.py +305 -0
- package/scripts/mcp_server/requirements.txt +4 -0
- package/scripts/mcp_server/resources.py +201 -0
- package/scripts/mcp_server/server.py +269 -0
- package/scripts/mcp_server/tools.py +363 -0
- package/scripts/mcp_setup.sh +87 -0
- package/scripts/pack_mcp_content.py +274 -0
- package/scripts/readme_linter.py +1 -1
- package/scripts/skill_linter.py +7 -0
|
@@ -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())
|
package/scripts/readme_linter.py
CHANGED
package/scripts/skill_linter.py
CHANGED
|
@@ -2948,6 +2948,13 @@ def main() -> int:
|
|
|
2948
2948
|
|
|
2949
2949
|
paths = sorted(set(paths))
|
|
2950
2950
|
if not paths:
|
|
2951
|
+
# Emit a valid empty payload when a structured format was requested
|
|
2952
|
+
# so downstream parsers (e.g. PR-summary workflows) don't fail on an
|
|
2953
|
+
# empty stdout. stderr keeps the human-readable note.
|
|
2954
|
+
if args.report:
|
|
2955
|
+
print(format_report([]))
|
|
2956
|
+
elif args.format == "json":
|
|
2957
|
+
print(format_json([]))
|
|
2951
2958
|
print("No matching skill/rule files found.", file=sys.stderr)
|
|
2952
2959
|
return 0
|
|
2953
2960
|
|