eclipse-agentready 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyan Eclipse Technologies LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: eclipse-agentready
3
+ Version: 0.1.0
4
+ Summary: AgentReady (T6 CLI edition) — audit a public storefront for AI shopping-agent readiness from CI: UCP profile, live MCP endpoint, llms.txt, robots AI policy, Product structured data, sitemap, platform, HTTPS/canonical.
5
+ Author: Cyan Eclipse Technologies LLC
6
+ License: MIT
7
+ Project-URL: Homepage, https://cetech.ai
8
+ Project-URL: Repository, https://github.com/CYAN-ECLIPSE-TECHNOLOGIES-LLC/agent-ready-mcp
9
+ Project-URL: Vendor, https://cetech.ai
10
+ Keywords: agent-readiness,storefront,e-commerce,ci,audit,mcp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Internet :: WWW/HTTP
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # eclipse-agentready
23
+
24
+ **What this is:** `eclipse-agentready` audits **storefronts** — public
25
+ e-commerce sites — for AI shopping-agent readiness: can an agent
26
+ (ChatGPT, Claude, Perplexity, Gemini…) discover the store, read its
27
+ catalog in machine-readable form, and get correct price/stock data?
28
+
29
+ **What this is not:** it does **not** audit repositories, and it does
30
+ **not** grade MCP servers. (The unrelated PyPI project named
31
+ `agentready` scores *code repositories* for AI-assisted development —
32
+ a different tool for a different job. This one is the CLI edition of
33
+ Cyan Eclipse Technologies' **AgentReady** storefront auditor, also
34
+ available as an MCP server.)
35
+
36
+ ## Use — a CI step for merchants and storefront DevOps
37
+
38
+ ```console
39
+ $ pip install eclipse-agentready
40
+ $ eclipse-agentready audit https://shop.example --fail-under 60
41
+ $ eclipse-agentready audit https://shop.example --format json --out report.json
42
+ $ eclipse-agentready list-checks
43
+ ```
44
+
45
+ Exit codes: `0` = audit completed and (if `--fail-under N` given) the
46
+ score is at least N · `1` = score below N · `2` = the audit itself
47
+ could not run.
48
+
49
+ ## The 8 checks (weights sum to 100)
50
+
51
+ | Check | Weight |
52
+ |---|---|
53
+ | Live MCP endpoint answers JSON-RPC | 20 |
54
+ | Product structured data (JSON-LD with price + availability) | 20 |
55
+ | UCP profile published (/.well-known/ucp) | 15 |
56
+ | HTTPS + canonical sanity | 12 |
57
+ | robots.txt allows AI crawlers | 10 |
58
+ | Sitemap present with product URLs | 10 |
59
+ | llms.txt present | 8 |
60
+ | Platform identified | 5 |
61
+
62
+ Scoring: pass = full weight, warn = half, fail = 0, n/a = excluded.
63
+ Grade: A ≥ 90 · B ≥ 75 · C ≥ 60 · D ≥ 40 · F < 40. Every check reports
64
+ observed evidence only, plus a concrete fix hint when it does not fully
65
+ pass. The CLI and the MCP server run the *identical* check catalog and
66
+ scoring path — same evidence in, same grade out.
67
+
68
+ ## Honest scope
69
+
70
+ - **Public-surface audit only.** All checks read public endpoints with
71
+ no credentials, a declared user agent, 10s timeouts, and redirect
72
+ following.
73
+ - **No authenticated checkout execution** — this tool never logs in,
74
+ never buys, and cannot verify a full agent purchase. A store can
75
+ score well here and still fail at the checkout step (and vice versa).
76
+ - **No private data** is collected or stored; audit output is the
77
+ caller's. Evidence is a point-in-time observation; storefronts change.
78
+ - A grade is not an endorsement and not a Bazaar listing claim.
79
+
80
+ The merchant's path to agent-commerce readiness starts with a grade
81
+ you can watch: run it in CI on every deploy. A hosted monitoring tier
82
+ (scheduled re-audits, score-change alerts) is on the product roadmap
83
+ and is **not yet launched**.
84
+
85
+ Powered by Cyan Eclipse Technologies LLC (cetech.ai) — a Veteran-Owned
86
+ Small Business. MIT licensed. Not affiliated with Shopify, WooCommerce,
87
+ Automattic, or any audited store; platform names describe detection
88
+ results only.
@@ -0,0 +1,67 @@
1
+ # eclipse-agentready
2
+
3
+ **What this is:** `eclipse-agentready` audits **storefronts** — public
4
+ e-commerce sites — for AI shopping-agent readiness: can an agent
5
+ (ChatGPT, Claude, Perplexity, Gemini…) discover the store, read its
6
+ catalog in machine-readable form, and get correct price/stock data?
7
+
8
+ **What this is not:** it does **not** audit repositories, and it does
9
+ **not** grade MCP servers. (The unrelated PyPI project named
10
+ `agentready` scores *code repositories* for AI-assisted development —
11
+ a different tool for a different job. This one is the CLI edition of
12
+ Cyan Eclipse Technologies' **AgentReady** storefront auditor, also
13
+ available as an MCP server.)
14
+
15
+ ## Use — a CI step for merchants and storefront DevOps
16
+
17
+ ```console
18
+ $ pip install eclipse-agentready
19
+ $ eclipse-agentready audit https://shop.example --fail-under 60
20
+ $ eclipse-agentready audit https://shop.example --format json --out report.json
21
+ $ eclipse-agentready list-checks
22
+ ```
23
+
24
+ Exit codes: `0` = audit completed and (if `--fail-under N` given) the
25
+ score is at least N · `1` = score below N · `2` = the audit itself
26
+ could not run.
27
+
28
+ ## The 8 checks (weights sum to 100)
29
+
30
+ | Check | Weight |
31
+ |---|---|
32
+ | Live MCP endpoint answers JSON-RPC | 20 |
33
+ | Product structured data (JSON-LD with price + availability) | 20 |
34
+ | UCP profile published (/.well-known/ucp) | 15 |
35
+ | HTTPS + canonical sanity | 12 |
36
+ | robots.txt allows AI crawlers | 10 |
37
+ | Sitemap present with product URLs | 10 |
38
+ | llms.txt present | 8 |
39
+ | Platform identified | 5 |
40
+
41
+ Scoring: pass = full weight, warn = half, fail = 0, n/a = excluded.
42
+ Grade: A ≥ 90 · B ≥ 75 · C ≥ 60 · D ≥ 40 · F < 40. Every check reports
43
+ observed evidence only, plus a concrete fix hint when it does not fully
44
+ pass. The CLI and the MCP server run the *identical* check catalog and
45
+ scoring path — same evidence in, same grade out.
46
+
47
+ ## Honest scope
48
+
49
+ - **Public-surface audit only.** All checks read public endpoints with
50
+ no credentials, a declared user agent, 10s timeouts, and redirect
51
+ following.
52
+ - **No authenticated checkout execution** — this tool never logs in,
53
+ never buys, and cannot verify a full agent purchase. A store can
54
+ score well here and still fail at the checkout step (and vice versa).
55
+ - **No private data** is collected or stored; audit output is the
56
+ caller's. Evidence is a point-in-time observation; storefronts change.
57
+ - A grade is not an endorsement and not a Bazaar listing claim.
58
+
59
+ The merchant's path to agent-commerce readiness starts with a grade
60
+ you can watch: run it in CI on every deploy. A hosted monitoring tier
61
+ (scheduled re-audits, score-change alerts) is on the product roadmap
62
+ and is **not yet launched**.
63
+
64
+ Powered by Cyan Eclipse Technologies LLC (cetech.ai) — a Veteran-Owned
65
+ Small Business. MIT licensed. Not affiliated with Shopify, WooCommerce,
66
+ Automattic, or any audited store; platform names describe detection
67
+ results only.
@@ -0,0 +1,103 @@
1
+ # AgentReady MCP — v0.1.0
2
+
3
+ **Powered by Cyan Eclipse Technologies** — https://cetech.ai
4
+
5
+ An MCP server that audits any public storefront for **AI-agent readiness**:
6
+ can a shopping agent (ChatGPT, Claude, Perplexity, Gemini…) discover the
7
+ store, read its catalog in machine-readable form, and get correct price and
8
+ stock data?
9
+
10
+ Why it exists (from CETech's Oct 2026 e-commerce MCP research,
11
+ `~/workspace/research_notes/ecommerce-mcp-opportunities-20m-20261025-0106/report.md`):
12
+
13
+ - Only ~36% of US storefronts can complete an agent purchase end-to-end
14
+ (InMobi, 3M merchants, Oct 2026).
15
+ - ~95% of merchants already see agent traffic, but only ~20% have
16
+ machine-readable catalogs (PayPal-cited data).
17
+ - 77% of retailers are not prepared for AI shopping; <1% are fully
18
+ optimized (Channable, Oct 2026).
19
+ - Comparable readiness/MCP/analytics products charge $69–$999/mo
20
+ (SignalixIQ, verified pricing Oct 2026).
21
+
22
+ ## Install & run
23
+
24
+ Stdlib-only Python 3 — no dependencies, no build step.
25
+
26
+ ```bash
27
+ python3 server.py # MCP over stdio (newline-delimited JSON-RPC 2.0)
28
+ python3 smoke_test.py # protocol + live-target smoke test (writes results/)
29
+ ```
30
+
31
+ MCP client config (Claude Desktop / Muse / any stdio client):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "agent-ready": {
37
+ "command": "python3",
38
+ "args": ["/path/to/agent-ready-mcp/server.py"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Tools
45
+
46
+ | Tool | Purpose |
47
+ |---|---|
48
+ | `audit_storefront(url)` | Full readiness audit of a storefront URL: 0–100 score, letter grade, per-check evidence, top-5 fixes. |
49
+ | `list_checks()` | The check catalog with weights and scoring rules. |
50
+
51
+ ## The 8 checks (weights sum to 100)
52
+
53
+ | Check | Weight | What passes |
54
+ |---|---|---|
55
+ | Live MCP endpoint | 20 | `/api/ucp/mcp`, `/api/mcp`, or `/mcp` answers a JSON-RPC initialize ping (WooCommerce: `/wp-json/mcp` also probed) |
56
+ | Product structured data | 20 | A product page carries JSON-LD `Product` with `offers.price` **and** `offers.availability` |
57
+ | UCP profile | 15 | Valid JSON profile at `/.well-known/ucp` |
58
+ | HTTPS + canonical | 12 | HTTPS final URL + same-host `rel=canonical` |
59
+ | robots.txt AI policy | 10 | GPTBot / ClaudeBot / PerplexityBot / ChatGPT-User / Google-Extended not blocked |
60
+ | Sitemap | 10 | `sitemap.xml` served (index files sampled) with product URLs |
61
+ | llms.txt | 8 | Non-empty `/llms.txt` |
62
+ | Platform detection | 5 | Shopify / WooCommerce / Wix / BigCommerce / Squarespace / Adobe Commerce fingerprint from strong markers |
63
+
64
+ Scoring: pass = full weight, warn = half, fail = 0, n/a = excluded.
65
+ Grade: A ≥ 90 · B ≥ 75 · C ≥ 60 · D ≥ 40 · F < 40.
66
+ Every check reports **observed evidence only** (HTTP status, response
67
+ content) plus a concrete `fix_hint` when it doesn't fully pass.
68
+
69
+ ## Honest scope
70
+
71
+ - **Public-surface audit only.** All checks read public endpoints with no
72
+ credentials, a declared user agent, 10s timeouts, and redirect following.
73
+ - **No authenticated checkout execution** — this version never logs in,
74
+ never buys, and cannot verify a full agent purchase. A store can score
75
+ well here and still fail at the checkout step (and vice versa).
76
+ - **No private data** is collected or stored; audit output is the caller's.
77
+ - Evidence is a point-in-time observation; storefronts change.
78
+
79
+ ## Smoke-test results (2026-10-24, live)
80
+
81
+ | Target | Platform | Score | Grade |
82
+ |---|---|---|---|
83
+ | gymshark.com | Shopify | 52 | D |
84
+ | woocommerce.com | WooCommerce | 55 | D |
85
+ | thewhitereport.com (non-commerce control) | custom | 32 | F |
86
+
87
+ Full per-check JSON in `results/`. Even flagship stores fail the UCP and
88
+ MCP-endpoint checks today — that gap is the product opportunity.
89
+
90
+ ## Status / roadmap
91
+
92
+ v0.1.0 — in development / dogfood. Local-audit v0.1 closure (EB-11,
93
+ 2026-10-26): normative check catalog in `docs/check-catalog.md`,
94
+ evidence report format in `docs/evidence-report-format.md`,
95
+ sample-storefront test corpus in `corpus/sample-storefronts.json`;
96
+ `python3 validate_closure.py` re-verifies all three against the code
97
+ and recorded reports (31/31). Planned: hosted monitoring tier
98
+ (re-audit on a schedule, score-change alerts), fix-playbook deep links per
99
+ platform, authenticated checkout verification (with merchant credentials),
100
+ and a Bazaar listing via Eclipse Bazaar once the test bench certifies it.
101
+
102
+ *Not affiliated with Shopify, WooCommerce, Automattic, or the audited
103
+ stores. Platform names are used to describe detection results only.*
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "eclipse-agentready"
7
+ dynamic = ["version"]
8
+ description = "AgentReady (T6 CLI edition) — audit a public storefront for AI shopping-agent readiness from CI: UCP profile, live MCP endpoint, llms.txt, robots AI policy, Product structured data, sitemap, platform, HTTPS/canonical."
9
+ readme = "PYPI_README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Cyan Eclipse Technologies LLC" },
14
+ ]
15
+ keywords = ["agent-readiness", "storefront", "e-commerce", "ci", "audit", "mcp"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Internet :: WWW/HTTP",
22
+ "Topic :: Software Development :: Quality Assurance",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://cetech.ai"
27
+ Repository = "https://github.com/CYAN-ECLIPSE-TECHNOLOGIES-LLC/agent-ready-mcp"
28
+ Vendor = "https://cetech.ai"
29
+
30
+ [project.scripts]
31
+ eclipse-agentready = "eclipse_agentready.cli:main"
32
+
33
+ # Version source of truth: VERSION, kept in lockstep with the engine's
34
+ # __version__ by tests/test_cli.py (drift = failing test).
35
+ [tool.setuptools.dynamic]
36
+ version = { file = "VERSION" }
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ # AgentReady · © 2026 Cyan Eclipse Technologies LLC · cetech.ai
2
+ """eclipse-agentready — the T6 CLI edition of AgentReady: the same
3
+ certified storefront-audit engine as the AgentReady MCP server, in
4
+ command-line form for CI pipelines. Audits *storefronts* for
5
+ shopping-agent readiness — not repositories, not MCP servers."""
6
+ from .engine import CHECKS, __version__, grade_for, run_audit, score_results
7
+
8
+ __all__ = ["CHECKS", "__version__", "grade_for", "run_audit", "score_results"]
@@ -0,0 +1,110 @@
1
+ # AgentReady · © 2026 Cyan Eclipse Technologies LLC · cetech.ai
2
+ """`eclipse-agentready` — AgentReady as a CI artifact (slate T6).
3
+
4
+ eclipse-agentready audit <url> [--fail-under N] [--format json|text]
5
+ [--out PATH]
6
+ eclipse-agentready list-checks [--format json|text]
7
+ eclipse-agentready version
8
+
9
+ Exit codes: audit — 0 when the audit completed and (no --fail-under
10
+ given, or score >= N); 1 when score < --fail-under; 2 when the audit
11
+ itself could not run (unreachable / unparseable target). The grade is
12
+ a point-in-time public-surface observation, printed with its limits.
13
+ """
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from .engine import CHECKS, ToolError, __version__, run_audit
20
+
21
+ SCOPE_FOOTER = (
22
+ "Scope: Public-surface audit only (no credentials, no checkout "
23
+ "execution). No authenticated checkout execution — this tool never "
24
+ "logs in, never buys, and cannot verify a full agent purchase. "
25
+ "Evidence is a point-in-time observation; storefronts change. "
26
+ "A grade is not an endorsement and not a Bazaar listing claim."
27
+ )
28
+
29
+
30
+ def _render_report(rep):
31
+ lines = [f"{rep['url']} — score {rep['score']}/100, grade {rep['grade']}"
32
+ f" (platform: {rep['platform']})"]
33
+ for c in rep["checks"]:
34
+ pts = "n/a" if c["points"] is None else f"{c['points']:g}/{c['weight']}"
35
+ lines.append(f" [{c['status']:<4}] {c['id']:<26} {pts:<7} {c['evidence']}")
36
+ if rep["top_fixes"]:
37
+ lines.append("Top fixes:")
38
+ for f in rep["top_fixes"]:
39
+ lines.append(f" - {f['check']}: {f['fix']}")
40
+ lines.append(SCOPE_FOOTER)
41
+ return "\n".join(lines)
42
+
43
+
44
+ def _checks_payload():
45
+ return {"checks": [{"id": c["id"], "name": c["name"],
46
+ "weight": c["weight"],
47
+ "description": c["description"]} for c in CHECKS],
48
+ "total_weight": sum(c["weight"] for c in CHECKS),
49
+ "scoring": "pass = full weight, warn = half, fail = 0, "
50
+ "na = excluded. Grade: A>=90 B>=75 C>=60 D>=40 F<40."}
51
+
52
+
53
+ def main(argv=None) -> int:
54
+ ap = argparse.ArgumentParser(
55
+ prog="eclipse-agentready",
56
+ description="Audit a public storefront for AI shopping-agent "
57
+ "readiness (UCP profile, live MCP endpoint, llms.txt, "
58
+ "robots.txt AI policy, Product structured data, "
59
+ "sitemap, platform, HTTPS/canonical). Storefronts "
60
+ "only — this is not a repository or MCP-server grader.")
61
+ sub = ap.add_subparsers(dest="cmd", required=True)
62
+
63
+ p = sub.add_parser("audit", help="audit one storefront URL")
64
+ p.add_argument("url")
65
+ p.add_argument("--fail-under", type=int, default=None, metavar="N",
66
+ help="exit 1 when the score is below N (CI gate)")
67
+ p.add_argument("--format", choices=("json", "text"), default="text")
68
+ p.add_argument("--out", default=None, metavar="PATH",
69
+ help="also write the JSON report to this path")
70
+
71
+ p = sub.add_parser("list-checks", help="the check catalog + weights")
72
+ p.add_argument("--format", choices=("json", "text"), default="text")
73
+
74
+ sub.add_parser("version", help="print the package version")
75
+
76
+ args = ap.parse_args(argv)
77
+
78
+ if args.cmd == "version":
79
+ print(__version__)
80
+ return 0
81
+ if args.cmd == "list-checks":
82
+ payload = _checks_payload()
83
+ if args.format == "json":
84
+ print(json.dumps(payload, indent=1))
85
+ else:
86
+ for c in payload["checks"]:
87
+ print(f" {c['id']:<26} {c['weight']:>3} {c['name']}")
88
+ print(f"Total weight: {payload['total_weight']}. "
89
+ f"{payload['scoring']}")
90
+ return 0
91
+ if args.cmd == "audit":
92
+ try:
93
+ rep = run_audit(args.url)
94
+ except ToolError as e:
95
+ print(f"audit failed: {e}", file=sys.stderr)
96
+ return 2
97
+ if args.out:
98
+ Path(args.out).write_text(json.dumps(rep, indent=1) + "\n")
99
+ if args.format == "json":
100
+ print(json.dumps(rep, indent=1))
101
+ else:
102
+ print(_render_report(rep))
103
+ if args.fail_under is not None and rep["score"] < args.fail_under:
104
+ return 1
105
+ return 0
106
+ return 2
107
+
108
+
109
+ if __name__ == "__main__":
110
+ raise SystemExit(main())
@@ -0,0 +1,526 @@
1
+ #!/usr/bin/env python3
2
+ # AgentReady MCP
3
+ # Copyright © 2026 Cyan Eclipse Technologies LLC · cetech.ai
4
+
5
+ """AgentReady MCP — v0.1.0 reference implementation.
6
+
7
+ Powered by Cyan Eclipse Technologies (cetech.ai).
8
+
9
+ What it does: audits a public storefront for AI-agent readiness — can a
10
+ shopping agent (ChatGPT, Claude, Perplexity, Gemini…) discover this store,
11
+ read its catalog in machine-readable form, and get correct price/stock
12
+ data? Research context: only ~36% of US storefronts can complete an agent
13
+ purchase end-to-end (InMobi, Oct 2026); 95% of merchants already see agent
14
+ traffic but only ~20% have machine-readable catalogs (PayPal-cited data).
15
+
16
+ SCOPE (honest): public-surface audit only. Every check reads public
17
+ endpoints with no credentials — UCP profile, MCP endpoints, llms.txt,
18
+ robots.txt, structured data, sitemap. It does NOT execute an authenticated
19
+ checkout, does NOT log in anywhere, and collects no private data.
20
+
21
+ Transport: MCP over stdio (newline-delimited JSON-RPC 2.0), stdlib only.
22
+ The audit engine is also importable (audit_storefront()) for batch use.
23
+ """
24
+ import json
25
+ import re
26
+ import sys
27
+ import time
28
+ import urllib.error
29
+ import urllib.parse
30
+ import urllib.request
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ UA = f"AgentReadyMCP/{__version__} (+https://cetech.ai) public readiness audit"
35
+ TIMEOUT = 10
36
+ MAX_BODY = 1_500_000
37
+
38
+ AI_BOTS = ["GPTBot", "ClaudeBot", "PerplexityBot", "ChatGPT-User", "Google-Extended"]
39
+
40
+
41
+ class ToolError(Exception):
42
+ pass
43
+
44
+
45
+ # --------------------------------------------------------------------- http
46
+ def fetch(url, method="GET", data=None, headers=None, timeout=TIMEOUT):
47
+ """Returns dict(status, final_url, headers, body_text, error).
48
+ Never raises for HTTP errors; network errors land in `error`."""
49
+ req = urllib.request.Request(url, data=data, method=method)
50
+ req.add_header("User-Agent", UA)
51
+ req.add_header("Accept", "*/*")
52
+ for k, v in (headers or {}).items():
53
+ req.add_header(k, v)
54
+ try:
55
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
56
+ chunks, total = [], 0
57
+ try:
58
+ while total < MAX_BODY: # slow pages: keep partial body, don't fail
59
+ chunk = resp.read(65536)
60
+ if not chunk:
61
+ break
62
+ chunks.append(chunk)
63
+ total += len(chunk)
64
+ except Exception:
65
+ pass
66
+ return {"status": resp.status, "final_url": resp.geturl(),
67
+ "headers": dict(resp.headers.items()),
68
+ "body": b"".join(chunks).decode("utf-8", errors="replace"), "error": None}
69
+ except urllib.error.HTTPError as e:
70
+ try:
71
+ raw = e.read(MAX_BODY)
72
+ body = raw.decode("utf-8", errors="replace")
73
+ except Exception:
74
+ body = ""
75
+ return {"status": e.code, "final_url": e.geturl() if hasattr(e, "geturl") else url,
76
+ "headers": dict(e.headers.items()) if e.headers else {},
77
+ "body": body, "error": None}
78
+ except Exception as e:
79
+ return {"status": None, "final_url": url, "headers": {}, "body": "",
80
+ "error": f"{type(e).__name__}: {e}"}
81
+
82
+
83
+ def norm_url(raw):
84
+ u = str(raw or "").strip()
85
+ if not u:
86
+ raise ToolError("url is required")
87
+ if not re.match(r"^https?://", u, re.I):
88
+ u = "https://" + u
89
+ p = urllib.parse.urlparse(u)
90
+ if not p.netloc:
91
+ raise ToolError(f"could not parse host from url: {raw}")
92
+ return urllib.parse.urlunparse((p.scheme, p.netloc, p.path or "/", "", p.query, ""))
93
+
94
+
95
+ def origin_of(url):
96
+ p = urllib.parse.urlparse(url)
97
+ return f"{p.scheme}://{p.netloc}"
98
+
99
+
100
+ # ------------------------------------------------------------------- checks
101
+ # Each check returns (status, evidence, fix_hint); status in pass/warn/fail/na.
102
+ CHECKS = [
103
+ {"id": "ucp_profile", "weight": 15,
104
+ "name": "UCP profile published (/.well-known/ucp)",
105
+ "description": "A Universal Commerce Protocol profile tells agents which commerce services (catalog, checkout) the store exposes and where. Pass = valid JSON profile served at /.well-known/ucp."},
106
+ {"id": "mcp_endpoint", "weight": 20,
107
+ "name": "Live MCP endpoint answers JSON-RPC",
108
+ "description": "An MCP server endpoint (Shopify /api/mcp or /api/ucp/mcp, or generic /mcp) that answers an initialize ping. Pass = JSON-RPC result with server info; warn = endpoint exists but is gated/errors; fail = no MCP endpoint found."},
109
+ {"id": "llms_txt", "weight": 8,
110
+ "name": "llms.txt present",
111
+ "description": "An llms.txt file gives LLM crawlers a curated map of the store. Pass = HTTP 200 with non-empty content at /llms.txt."},
112
+ {"id": "robots_ai_policy", "weight": 10,
113
+ "name": "robots.txt allows AI crawlers",
114
+ "description": "If GPTBot / ClaudeBot / PerplexityBot are disallowed in robots.txt, those agents cannot legally crawl the catalog. Pass = major AI bots allowed; warn = partially blocked or no robots.txt; fail = all major AI bots blocked."},
115
+ {"id": "product_structured_data", "weight": 20,
116
+ "name": "Product structured data (JSON-LD with price + availability)",
117
+ "description": "Agents read JSON-LD Product markup to extract price and stock without guessing from page text. Pass = a product page carries Product JSON-LD with offers.price and offers.availability; warn = Product markup present but incomplete; fail = none found."},
118
+ {"id": "sitemap", "weight": 10,
119
+ "name": "Sitemap present with product URLs",
120
+ "description": "A sitemap.xml lets agent crawlers enumerate the catalog. Pass = sitemap served and product URLs present; warn = sitemap served but no recognizable product URLs; fail = no sitemap."},
121
+ {"id": "platform_detection", "weight": 5,
122
+ "name": "Platform identified",
123
+ "description": "Platform fingerprint (Shopify / WooCommerce / Wix / BigCommerce / Squarespace / Adobe Commerce / custom) from response headers and HTML markers. Informs which native agent features the store can switch on. Pass = identified."},
124
+ {"id": "https_canonical", "weight": 12,
125
+ "name": "HTTPS + canonical sanity",
126
+ "description": "Agents and crawlers need one canonical HTTPS URL per page. Pass = site served over HTTPS and homepage declares a rel=canonical on the same host; warn = HTTPS ok but canonical missing/mismatched; fail = not served over HTTPS."},
127
+ ]
128
+ CHECK_BY_ID = {c["id"]: c for c in CHECKS}
129
+
130
+
131
+ def check_ucp(origin):
132
+ r = fetch(origin + "/.well-known/ucp")
133
+ if r["status"] == 200:
134
+ try:
135
+ doc = json.loads(r["body"])
136
+ keys = sorted(doc.keys())[:8]
137
+ has_ucp = "ucp" in doc or "services" in doc or "capabilities" in doc
138
+ if has_ucp:
139
+ return "pass", f"GET /.well-known/ucp -> 200, JSON profile keys: {keys}", None
140
+ return "warn", f"GET /.well-known/ucp -> 200 JSON but no ucp/services/capabilities keys (keys: {keys})", \
141
+ "Publish a UCP profile describing your catalog/checkout services at /.well-known/ucp."
142
+ except Exception:
143
+ return "warn", "GET /.well-known/ucp -> 200 but body is not valid JSON", \
144
+ "Serve a valid JSON UCP profile at /.well-known/ucp."
145
+ return "fail", f"GET /.well-known/ucp -> {r['status'] or r['error']}", \
146
+ "Publish a UCP profile at /.well-known/ucp (Shopify exposes one via its UCP catalog support; other platforms can hand-serve the JSON document)."
147
+
148
+
149
+ def _mcp_ping(url):
150
+ payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize",
151
+ "params": {"protocolVersion": "2025-03-26", "capabilities": {},
152
+ "clientInfo": {"name": "agent-ready-mcp", "version": "0.1.0"}}}).encode()
153
+ r = fetch(url, method="POST", data=payload,
154
+ headers={"Content-Type": "application/json", "Accept": "application/json, text/event-stream"})
155
+ body = r["body"] or ""
156
+ doc = None
157
+ try:
158
+ doc = json.loads(body)
159
+ except Exception:
160
+ for line in body.splitlines(): # SSE framing
161
+ if line.startswith("data:"):
162
+ try:
163
+ doc = json.loads(line[5:].strip())
164
+ break
165
+ except Exception:
166
+ pass
167
+ return r, doc
168
+
169
+
170
+ def check_mcp(origin, platform):
171
+ candidates = ["/api/ucp/mcp", "/api/mcp", "/mcp"]
172
+ if platform == "woocommerce":
173
+ candidates = ["/wp-json/mcp", "/mcp"] + candidates
174
+ seen = []
175
+ for path in candidates:
176
+ r, doc = _mcp_ping(origin + path)
177
+ st = r["status"]
178
+ if doc and isinstance(doc, dict) and "result" in doc:
179
+ info = (doc["result"] or {}).get("serverInfo", {})
180
+ seen.append(f"{path} -> JSON-RPC result (server: {info.get('name', 'unknown')} {info.get('version', '')})".strip())
181
+ return "pass", "; ".join(seen), None
182
+ if doc and isinstance(doc, dict) and "error" in doc:
183
+ seen.append(f"{path} -> HTTP {st}, JSON-RPC error response (endpoint exists)")
184
+ return "warn", "; ".join(seen), \
185
+ "An MCP endpoint exists but returned a protocol error to a standard initialize ping — test it with a real agent client and fix the handshake."
186
+ if st in (401, 403, 405, 406):
187
+ seen.append(f"{path} -> HTTP {st} (endpoint likely exists, gated)")
188
+ return "warn", "; ".join(seen), \
189
+ "An endpoint answers at an MCP path but refuses anonymous pings — confirm public catalog tools are reachable without auth, or document the auth flow for agents."
190
+ seen.append(f"{path} -> {st or r['error']}")
191
+ return "fail", "; ".join(seen), \
192
+ "Expose a public MCP endpoint for catalog search/product lookup (Shopify: enable the Storefront/UCP Catalog MCP; WooCommerce: enable the MCP abilities adapter; other stacks: a hosted catalog MCP)."
193
+
194
+
195
+ def check_llms(origin):
196
+ r = fetch(origin + "/llms.txt")
197
+ if r["status"] == 200 and len(r["body"].strip()) > 20:
198
+ first = r["body"].strip().splitlines()[0][:80]
199
+ return "pass", f"GET /llms.txt -> 200, {len(r['body'])} bytes, starts: {first!r}", None
200
+ return "fail", f"GET /llms.txt -> {r['status'] or r['error']}", \
201
+ "Publish /llms.txt: a short markdown map of your store, top categories and policies for LLM crawlers."
202
+
203
+
204
+ def _robots_policy(body):
205
+ """Parse robots.txt -> {bot: 'blocked'|'allowed'} for AI_BOTS (crude but factual)."""
206
+ groups, current = [], []
207
+ for line in body.splitlines():
208
+ line = line.strip()
209
+ if not line or line.startswith("#"):
210
+ continue
211
+ if line.lower().startswith("user-agent:"):
212
+ if current and any(k.lower().startswith("disallow") for k, _ in current[1:]):
213
+ groups.append(current)
214
+ current = [(line.split(":", 1)[0].lower(), line.split(":", 1)[1].strip())]
215
+ elif current:
216
+ k, _, v = line.partition(":")
217
+ current.append((k.strip().lower(), v.strip()))
218
+ if current:
219
+ groups.append(current)
220
+ policy = {}
221
+ for bot in AI_BOTS:
222
+ blocked = None
223
+ for g in groups:
224
+ agents = [v.lower() for k, v in g if k == "user-agent"]
225
+ rules = [(k, v) for k, v in g if k in ("allow", "disallow")]
226
+ if bot.lower() in agents or "*" in agents:
227
+ for k, v in rules:
228
+ if k == "disallow" and v == "/":
229
+ blocked = True
230
+ if k == "allow" and v == "/":
231
+ blocked = False
232
+ policy[bot] = "blocked" if blocked else "allowed"
233
+ return policy
234
+
235
+
236
+ def check_robots(origin):
237
+ r = fetch(origin + "/robots.txt")
238
+ if r["status"] != 200 or not r["body"].strip():
239
+ return "warn", f"GET /robots.txt -> {r['status'] or r['error']} (no robots policy published)", \
240
+ "Publish a robots.txt that explicitly allows GPTBot, ClaudeBot and PerplexityBot on catalog paths."
241
+ policy = _robots_policy(r["body"])
242
+ blocked = [b for b, v in policy.items() if v == "blocked"]
243
+ ev = ", ".join(f"{b}: {v}" for b, v in policy.items())
244
+ if not blocked:
245
+ return "pass", f"robots.txt policy — {ev}", None
246
+ if len(blocked) >= 4:
247
+ return "fail", f"robots.txt policy — {ev}", \
248
+ "Your robots.txt blocks the major AI crawlers outright — those agents cannot see your catalog. Allow GPTBot, ClaudeBot and PerplexityBot at least on product/category paths."
249
+ return "warn", f"robots.txt policy — {ev}", \
250
+ f"Some AI crawlers are blocked ({', '.join(blocked)}). If that is not deliberate, allow them on catalog paths."
251
+
252
+
253
+ def _jsonld_docs(html_text):
254
+ docs = []
255
+ for m in re.finditer(r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
256
+ html_text, re.I | re.S):
257
+ try:
258
+ docs.append(json.loads(m.group(1).strip()))
259
+ except Exception:
260
+ pass
261
+ return docs
262
+
263
+
264
+ def _products_in(docs):
265
+ found = []
266
+
267
+ def walk(node):
268
+ if isinstance(node, dict):
269
+ t = node.get("@type")
270
+ if t == "Product" or (isinstance(t, list) and "Product" in t):
271
+ found.append(node)
272
+ for v in node.values():
273
+ walk(v)
274
+ elif isinstance(node, list):
275
+ for v in node:
276
+ walk(v)
277
+
278
+ for d in docs:
279
+ walk(d)
280
+ return found
281
+
282
+
283
+ def _product_page_url(home_html, origin, sitemap_urls):
284
+ for m in re.finditer(r'href=["\']([^"\']+)["\']', home_html, re.I):
285
+ href = m.group(1)
286
+ if re.search(r"/products?/", href):
287
+ return urllib.parse.urljoin(origin + "/", href)
288
+ for u in sitemap_urls:
289
+ if re.search(r"/products?/", u):
290
+ return u
291
+ return None
292
+
293
+
294
+ def check_structured_data(home, origin, sitemap_urls):
295
+ home_products = _products_in(_jsonld_docs(home["body"]))
296
+ purl = _product_page_url(home["body"], origin, sitemap_urls)
297
+ target, label = home, "homepage"
298
+ if purl:
299
+ pr = fetch(purl)
300
+ if pr["status"] == 200:
301
+ target, label = pr, f"product page {purl}"
302
+ prods = _products_in(_jsonld_docs(target["body"]))
303
+ if not prods and target is not home:
304
+ prods = home_products
305
+ label = "homepage (product page had none)"
306
+ if not prods:
307
+ return "fail", f"No JSON-LD Product found on homepage" + (f" or {purl}" if purl else " (no product link discovered)"), \
308
+ "Add JSON-LD Product markup (name, offers.price, offers.priceCurrency, offers.availability) to every product page — most platforms do this via their SEO/schema settings or a schema app."
309
+ p = prods[0]
310
+ offers = p.get("offers") or {}
311
+ if isinstance(offers, list):
312
+ offers = offers[0] if offers else {}
313
+ has_price = bool(offers.get("price") or offers.get("lowPrice"))
314
+ has_avail = bool(offers.get("availability"))
315
+ ev = (f"{label}: Product JSON-LD for {p.get('name', 'unnamed')!r}; "
316
+ f"price={'yes' if has_price else 'no'}, availability={'yes' if has_avail else 'no'}")
317
+ if has_price and has_avail:
318
+ return "pass", ev, None
319
+ return "warn", ev, \
320
+ "Your Product JSON-LD is missing price and/or availability in offers — agents then guess from page text and get it wrong. Emit both fields."
321
+
322
+
323
+ def check_sitemap(origin):
324
+ r = fetch(origin + "/sitemap.xml")
325
+ if r["status"] != 200 or "<loc>" not in r["body"]:
326
+ return "fail", f"GET /sitemap.xml -> {r['status'] or r['error']}, no <loc> entries", \
327
+ "Publish a sitemap.xml (your platform generates one) and reference it in robots.txt.", []
328
+ urls = re.findall(r"<loc>([^<]+)</loc>", r["body"])
329
+ followed = ""
330
+ if "<sitemapindex" in r["body"]: # index file: sample children for product URLs
331
+ children = urls[:]
332
+ urls = []
333
+ for child in children[:3]:
334
+ cr = fetch(child)
335
+ if cr["status"] == 200:
336
+ urls += re.findall(r"<loc>([^<]+)</loc>", cr["body"])
337
+ followed = f" (index of {len(children)} children, sampled {min(3, len(children))})"
338
+ prod = [u for u in urls if re.search(r"/products?/", u)]
339
+ ev = f"sitemap.xml -> 200{followed}, {len(urls)} URLs sampled, {len(prod)} product-pattern URLs"
340
+ if prod:
341
+ return "pass", ev, None, urls
342
+ return "warn", ev, \
343
+ "Your sitemap sample has no recognizable product URLs — make sure product pages are included so agent crawlers can enumerate the catalog.", urls
344
+
345
+
346
+ def detect_platform(home):
347
+ """Strong markers only — a comparison page merely *mentioning* a rival
348
+ platform must not fingerprint the store (learned on woocommerce.com)."""
349
+ html_l = home["body"][:400_000].lower()
350
+ hdr_names = [k.lower() for k in home["headers"].keys()]
351
+ hdr_blob = " ".join(f"{k.lower()}:{v.lower()}" for k, v in home["headers"].items())
352
+ if "cdn.shopify.com" in html_l or "myshopify.com" in html_l or \
353
+ any(h.startswith("x-shopify") for h in hdr_names):
354
+ return "shopify", "cdn.shopify.com / x-shopify header marker"
355
+ if "plugins/woocommerce" in html_l or "/woocommerce/assets" in html_l or "wc-blocks" in html_l:
356
+ return "woocommerce", "WooCommerce plugin asset marker in HTML"
357
+ if "wixstatic.com" in html_l or any(h.startswith("x-wix") for h in hdr_names):
358
+ return "wix", "wixstatic.com / x-wix header marker"
359
+ if "mybigcommerce.com" in html_l or "cdn11.bigcommerce.com" in html_l:
360
+ return "bigcommerce", "BigCommerce CDN marker"
361
+ if "static1.squarespace.com" in html_l or "universal-js.squarespace.com" in html_l:
362
+ return "squarespace", "Squarespace CDN marker"
363
+ if "magento" in hdr_blob or "/mage/" in html_l:
364
+ return "adobe-commerce", "Magento marker in headers/assets"
365
+ if "wp-content" in html_l:
366
+ return "wordpress", "wp-content marker (WordPress, WooCommerce not confirmed)"
367
+ return "unknown/custom", "no known platform markers in homepage HTML/headers"
368
+
369
+
370
+ def grade_for(score):
371
+ return "A" if score >= 90 else "B" if score >= 75 else "C" if score >= 60 else "D" if score >= 40 else "F"
372
+
373
+
374
+ # --------------------------------------------------------------------- audit
375
+ def run_audit(raw_url):
376
+ url = norm_url(raw_url)
377
+ started = time.time()
378
+ home = fetch(url)
379
+ if home["status"] is None:
380
+ raise ToolError(f"could not fetch {url}: {home['error']}")
381
+ origin = origin_of(home["final_url"])
382
+ platform, plat_ev = detect_platform(home)
383
+
384
+ results = {}
385
+ sm = check_sitemap(origin)
386
+ results["sitemap"] = (sm[0], sm[1], sm[2])
387
+ sm_urls = sm[3]
388
+
389
+ results["ucp_profile"] = check_ucp(origin)
390
+ results["mcp_endpoint"] = check_mcp(origin, platform)
391
+ results["llms_txt"] = check_llms(origin)
392
+ results["robots_ai_policy"] = check_robots(origin)
393
+ results["product_structured_data"] = check_structured_data(home, origin, sm_urls)
394
+ results["platform_detection"] = ("pass" if platform != "unknown/custom" else "warn",
395
+ f"detected: {platform} ({plat_ev})",
396
+ None if platform != "unknown/custom" else
397
+ "No platform fingerprint found — if you run a custom stack, make sure the UCP/MCP/structured-data checks above are covered by hand.")
398
+ # https + canonical
399
+ final = home["final_url"]
400
+ canon = re.search(r'<link[^>]+rel=["\']canonical["\'][^>]*href=["\']([^"\']+)', home["body"], re.I) or \
401
+ re.search(r'<link[^>]+href=["\']([^"\']+)["\'][^>]*rel=["\']canonical["\']', home["body"], re.I)
402
+ canon_url = canon.group(1) if canon else None
403
+ if not final.startswith("https://"):
404
+ results["https_canonical"] = ("fail", f"final URL is {final} (not HTTPS); canonical={canon_url}", \
405
+ "Serve the store over HTTPS and redirect all HTTP traffic to it.")
406
+ elif canon_url and urllib.parse.urlparse(canon_url).netloc == urllib.parse.urlparse(final).netloc:
407
+ results["https_canonical"] = ("pass", f"final URL {final}; canonical {canon_url} (same host)", None)
408
+ elif canon_url:
409
+ results["https_canonical"] = ("warn", f"final URL {final}; canonical {canon_url} (different host/scheme)", \
410
+ "Point rel=canonical at the same HTTPS host agents land on — cross-host canonicals confuse catalog indexing.")
411
+ else:
412
+ results["https_canonical"] = ("warn", f"final URL {final}; no rel=canonical on homepage", \
413
+ "Add rel=canonical tags so agents see one canonical URL per product instead of duplicates.")
414
+
415
+ checks_out, score, grade, top_fixes = score_results(results)
416
+ return {
417
+ "url": url, "final_url": home["final_url"], "platform": platform,
418
+ "score": score, "grade": grade,
419
+ "checks": checks_out,
420
+ "top_fixes": top_fixes,
421
+ "audited_in_s": round(time.time() - started, 1),
422
+ "scope": "Public-surface audit only (no credentials, no checkout execution).",
423
+ "auditor": f"AgentReady MCP {__version__} — Cyan Eclipse Technologies",
424
+ }
425
+
426
+
427
+ def score_results(results):
428
+ """{check_id: (status, evidence, fix_hint)} -> (checks_out, score,
429
+ grade, top_fixes). The single scoring path: the MCP tool, the
430
+ eclipse-agentready CLI, and the recorded-evidence replays in the
431
+ test suite all score through here, so the surfaces can never
432
+ disagree on the same evidence (gate B2)."""
433
+ checks_out, earned, possible = [], 0.0, 0.0
434
+ for c in CHECKS:
435
+ st, ev, fx = results[c["id"]]
436
+ w = c["weight"]
437
+ if st == "na":
438
+ pts = None
439
+ else:
440
+ possible += w
441
+ pts = w if st == "pass" else (w * 0.5 if st == "warn" else 0)
442
+ earned += pts
443
+ checks_out.append({"id": c["id"], "name": c["name"], "status": st, "weight": w,
444
+ "points": pts, "evidence": ev, "fix_hint": fx})
445
+ score = round(100 * earned / possible) if possible else 0
446
+ fixes = [c for c in checks_out if c["status"] in ("fail", "warn") and c["fix_hint"]]
447
+ fixes.sort(key=lambda c: -c["weight"])
448
+ return checks_out, score, grade_for(score), \
449
+ [{"check": f["id"], "fix": f["fix_hint"]} for f in fixes[:5]]
450
+
451
+
452
+ # --------------------------------------------------------------------- tools
453
+ def t_audit_storefront(args):
454
+ return run_audit(args.get("url", ""))
455
+
456
+
457
+ def t_list_checks(args):
458
+ return {"checks": [{"id": c["id"], "name": c["name"], "weight": c["weight"],
459
+ "description": c["description"]} for c in CHECKS],
460
+ "total_weight": sum(c["weight"] for c in CHECKS),
461
+ "scoring": "pass = full weight, warn = half, fail = 0, na = excluded. Grade: A>=90 B>=75 C>=60 D>=40 F<40."}
462
+
463
+
464
+ TOOLS = {
465
+ "audit_storefront": (t_audit_storefront,
466
+ "Audit a public storefront for AI-agent readiness: UCP profile, live MCP endpoint, llms.txt, robots.txt AI-crawler policy, Product structured data, sitemap, platform, HTTPS/canonical. Returns a 0-100 score, letter grade, per-check evidence and top fixes. Public endpoints only — no credentials.",
467
+ {"url": {"type": "string"}}, ["url"]),
468
+ "list_checks": (t_list_checks,
469
+ "List the readiness check catalog with weights and scoring rules.",
470
+ {}, []),
471
+ }
472
+
473
+
474
+ def tool_list():
475
+ return [{"name": name,
476
+ "description": desc,
477
+ "inputSchema": {"type": "object", "properties": props, "required": req}}
478
+ for name, (_, desc, props, req) in TOOLS.items()]
479
+
480
+
481
+ def handle(req: dict):
482
+ method, rid = req.get("method", ""), req.get("id")
483
+ if method == "initialize":
484
+ return {"jsonrpc": "2.0", "id": rid, "result": {
485
+ "protocolVersion": req.get("params", {}).get("protocolVersion", "2025-03-26"),
486
+ "capabilities": {"tools": {}},
487
+ "serverInfo": {"name": "agent-ready-mcp", "version": "0.1.0"}}}
488
+ if method == "ping":
489
+ return {"jsonrpc": "2.0", "id": rid, "result": {}}
490
+ if method == "tools/list":
491
+ return {"jsonrpc": "2.0", "id": rid, "result": {"tools": tool_list()}}
492
+ if method == "tools/call":
493
+ name = req["params"]["name"]
494
+ args = req["params"].get("arguments", {})
495
+ if name not in TOOLS:
496
+ return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32602, "message": f"unknown tool {name}"}}
497
+ try:
498
+ out = TOOLS[name][0](args)
499
+ return {"jsonrpc": "2.0", "id": rid, "result": {
500
+ "content": [{"type": "text", "text": json.dumps(out, indent=1)}]}}
501
+ except ToolError as e:
502
+ return {"jsonrpc": "2.0", "id": rid, "result": {
503
+ "content": [{"type": "text", "text": json.dumps({"error": str(e)})}], "isError": True}}
504
+ if rid is not None:
505
+ return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": f"method not found: {method}"}}
506
+ return None # notification
507
+
508
+
509
+ def main():
510
+ for line in sys.stdin:
511
+ line = line.strip()
512
+ if not line:
513
+ continue
514
+ try:
515
+ resp = handle(json.loads(line))
516
+ except Exception as e: # never crash the transport
517
+ resp = {"jsonrpc": "2.0", "id": None, "error": {"code": -32603, "message": str(e)}}
518
+ if resp is not None:
519
+ sys.stdout.write(json.dumps(resp) + "\n")
520
+ sys.stdout.flush()
521
+
522
+
523
+ if __name__ == "__main__":
524
+ main()
525
+
526
+ # AgentReady MCP · © 2026 Cyan Eclipse Technologies LLC · cetech.ai
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: eclipse-agentready
3
+ Version: 0.1.0
4
+ Summary: AgentReady (T6 CLI edition) — audit a public storefront for AI shopping-agent readiness from CI: UCP profile, live MCP endpoint, llms.txt, robots AI policy, Product structured data, sitemap, platform, HTTPS/canonical.
5
+ Author: Cyan Eclipse Technologies LLC
6
+ License: MIT
7
+ Project-URL: Homepage, https://cetech.ai
8
+ Project-URL: Repository, https://github.com/CYAN-ECLIPSE-TECHNOLOGIES-LLC/agent-ready-mcp
9
+ Project-URL: Vendor, https://cetech.ai
10
+ Keywords: agent-readiness,storefront,e-commerce,ci,audit,mcp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Internet :: WWW/HTTP
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # eclipse-agentready
23
+
24
+ **What this is:** `eclipse-agentready` audits **storefronts** — public
25
+ e-commerce sites — for AI shopping-agent readiness: can an agent
26
+ (ChatGPT, Claude, Perplexity, Gemini…) discover the store, read its
27
+ catalog in machine-readable form, and get correct price/stock data?
28
+
29
+ **What this is not:** it does **not** audit repositories, and it does
30
+ **not** grade MCP servers. (The unrelated PyPI project named
31
+ `agentready` scores *code repositories* for AI-assisted development —
32
+ a different tool for a different job. This one is the CLI edition of
33
+ Cyan Eclipse Technologies' **AgentReady** storefront auditor, also
34
+ available as an MCP server.)
35
+
36
+ ## Use — a CI step for merchants and storefront DevOps
37
+
38
+ ```console
39
+ $ pip install eclipse-agentready
40
+ $ eclipse-agentready audit https://shop.example --fail-under 60
41
+ $ eclipse-agentready audit https://shop.example --format json --out report.json
42
+ $ eclipse-agentready list-checks
43
+ ```
44
+
45
+ Exit codes: `0` = audit completed and (if `--fail-under N` given) the
46
+ score is at least N · `1` = score below N · `2` = the audit itself
47
+ could not run.
48
+
49
+ ## The 8 checks (weights sum to 100)
50
+
51
+ | Check | Weight |
52
+ |---|---|
53
+ | Live MCP endpoint answers JSON-RPC | 20 |
54
+ | Product structured data (JSON-LD with price + availability) | 20 |
55
+ | UCP profile published (/.well-known/ucp) | 15 |
56
+ | HTTPS + canonical sanity | 12 |
57
+ | robots.txt allows AI crawlers | 10 |
58
+ | Sitemap present with product URLs | 10 |
59
+ | llms.txt present | 8 |
60
+ | Platform identified | 5 |
61
+
62
+ Scoring: pass = full weight, warn = half, fail = 0, n/a = excluded.
63
+ Grade: A ≥ 90 · B ≥ 75 · C ≥ 60 · D ≥ 40 · F < 40. Every check reports
64
+ observed evidence only, plus a concrete fix hint when it does not fully
65
+ pass. The CLI and the MCP server run the *identical* check catalog and
66
+ scoring path — same evidence in, same grade out.
67
+
68
+ ## Honest scope
69
+
70
+ - **Public-surface audit only.** All checks read public endpoints with
71
+ no credentials, a declared user agent, 10s timeouts, and redirect
72
+ following.
73
+ - **No authenticated checkout execution** — this tool never logs in,
74
+ never buys, and cannot verify a full agent purchase. A store can
75
+ score well here and still fail at the checkout step (and vice versa).
76
+ - **No private data** is collected or stored; audit output is the
77
+ caller's. Evidence is a point-in-time observation; storefronts change.
78
+ - A grade is not an endorsement and not a Bazaar listing claim.
79
+
80
+ The merchant's path to agent-commerce readiness starts with a grade
81
+ you can watch: run it in CI on every deploy. A hosted monitoring tier
82
+ (scheduled re-audits, score-change alerts) is on the product roadmap
83
+ and is **not yet launched**.
84
+
85
+ Powered by Cyan Eclipse Technologies LLC (cetech.ai) — a Veteran-Owned
86
+ Small Business. MIT licensed. Not affiliated with Shopify, WooCommerce,
87
+ Automattic, or any audited store; platform names describe detection
88
+ results only.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ PYPI_README.md
3
+ README.md
4
+ VERSION
5
+ pyproject.toml
6
+ src/eclipse_agentready/__init__.py
7
+ src/eclipse_agentready/cli.py
8
+ src/eclipse_agentready/engine.py
9
+ src/eclipse_agentready.egg-info/PKG-INFO
10
+ src/eclipse_agentready.egg-info/SOURCES.txt
11
+ src/eclipse_agentready.egg-info/dependency_links.txt
12
+ src/eclipse_agentready.egg-info/entry_points.txt
13
+ src/eclipse_agentready.egg-info/top_level.txt
14
+ tests/test_cli.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ eclipse-agentready = eclipse_agentready.cli:main
@@ -0,0 +1 @@
1
+ eclipse_agentready
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ # AgentReady — T6 CLI edition tests (gate B1–B4, B-M1 known-answer).
3
+ # Copyright © 2026 Cyan Eclipse Technologies LLC · cetech.ai
4
+ """The CLI is the same engine in CI dress:
5
+ * list-checks serves the identical catalog as the MCP list_checks;
6
+ * recorded check-level evidence replays through the CLI's scoring
7
+ path to the identical score/grade/top_fixes — for the three
8
+ recorded storefront reports AND the cetech.ai known-answer fixture
9
+ (live-audited 2026-10-26; its known band is F — if the CLI ever
10
+ returns a pass band for cetech.ai's evidence, the CLI is broken,
11
+ not the site healed);
12
+ * a deterministic bare-storefront fixture over local HTTP exercises
13
+ the full audit path incl. --fail-under exit codes;
14
+ * name/scope law: VERSION == engine __version__; the PyPI readme
15
+ carries the B1 disambiguation; B4's forbidden framing appears in
16
+ no shipped surface.
17
+ """
18
+ import json
19
+ import sys
20
+ import threading
21
+ import unittest
22
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
23
+ from pathlib import Path
24
+
25
+ ROOT = Path(__file__).resolve().parents[1]
26
+ sys.path.insert(0, str(ROOT / "src"))
27
+
28
+ from eclipse_agentready import CHECKS, __version__, score_results # noqa: E402
29
+ from eclipse_agentready import cli # noqa: E402
30
+
31
+
32
+ def replay(rep):
33
+ results = {c["id"]: (c["status"], c["evidence"], c["fix_hint"])
34
+ for c in rep["checks"]}
35
+ checks_out, score, grade, top_fixes = score_results(results)
36
+ assert score == rep["score"], (score, rep["score"])
37
+ assert grade == rep["grade"], (grade, rep["grade"])
38
+ assert top_fixes == rep["top_fixes"]
39
+ assert checks_out == rep["checks"]
40
+
41
+
42
+ class KnownAnswer(unittest.TestCase):
43
+ def test_recorded_reports_replay_exactly(self):
44
+ for name in ("gymshark", "woocommerce", "thewhitereport"):
45
+ rep = json.loads((ROOT / "results" / f"{name}.json").read_text())
46
+ replay(rep)
47
+
48
+ def test_cetech_known_answer_band(self):
49
+ rep = json.loads((ROOT / "tests/fixtures"
50
+ / "cetech-ai-live-2026-10-26.json").read_text())
51
+ replay(rep)
52
+ self.assertEqual(rep["grade"], "F") # the known band
53
+ self.assertLess(rep["score"], 40)
54
+
55
+
56
+ class _BareHandler(BaseHTTPRequestHandler):
57
+ def _reply(self):
58
+ if self.path == "/":
59
+ body = b"<html><head><title>bare</title></head><body>hi</body></html>"
60
+ self.send_response(200)
61
+ self.send_header("Content-Type", "text/html")
62
+ self.send_header("Content-Length", str(len(body)))
63
+ self.end_headers()
64
+ self.wfile.write(body)
65
+ else:
66
+ self.send_response(404)
67
+ self.send_header("Content-Length", "0")
68
+ self.end_headers()
69
+
70
+ do_GET = _reply
71
+ do_POST = _reply
72
+
73
+ def log_message(self, *a):
74
+ pass
75
+
76
+
77
+ class BareFixtureAudit(unittest.TestCase):
78
+ @classmethod
79
+ def setUpClass(cls):
80
+ cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), _BareHandler)
81
+ cls.port = cls.httpd.server_address[1]
82
+ threading.Thread(target=cls.httpd.serve_forever, daemon=True).start()
83
+
84
+ @classmethod
85
+ def tearDownClass(cls):
86
+ cls.httpd.shutdown()
87
+
88
+ def test_bare_storefront_scores_f(self):
89
+ import io
90
+ from contextlib import redirect_stdout
91
+ buf = io.StringIO()
92
+ with redirect_stdout(buf):
93
+ rc = cli.main(["audit", f"http://127.0.0.1:{self.port}",
94
+ "--format", "json"])
95
+ self.assertEqual(rc, 0) # no --fail-under: completion is success
96
+ rep = json.loads(buf.getvalue())
97
+ self.assertEqual(rep["grade"], "F")
98
+ self.assertEqual(rep["score"], 8) # robots warn 5 + platform warn 2.5
99
+ self.assertEqual([c["id"] for c in rep["checks"]],
100
+ [c["id"] for c in CHECKS])
101
+
102
+ def test_fail_under_exit_code(self):
103
+ import io
104
+ from contextlib import redirect_stdout
105
+ with redirect_stdout(io.StringIO()):
106
+ rc = cli.main(["audit", f"http://127.0.0.1:{self.port}",
107
+ "--fail-under", "60"])
108
+ self.assertEqual(rc, 1)
109
+
110
+ def test_unreachable_target_exit_2(self):
111
+ rc = cli.main(["audit", "http://127.0.0.1:9/nowhere"])
112
+ self.assertEqual(rc, 2)
113
+
114
+
115
+ class CatalogAndLaw(unittest.TestCase):
116
+ def test_list_checks_matches_engine(self):
117
+ payload = cli._checks_payload()
118
+ self.assertEqual([(c["id"], c["weight"]) for c in payload["checks"]],
119
+ [(c["id"], c["weight"]) for c in CHECKS])
120
+ self.assertEqual(payload["total_weight"], 100)
121
+
122
+ def test_version_lockstep(self):
123
+ self.assertEqual((ROOT / "VERSION").read_text().strip(), __version__)
124
+ text = (ROOT / "pyproject.toml").read_text()
125
+ self.assertIn('name = "eclipse-agentready"', text)
126
+ self.assertIn('version = { file = "VERSION" }', text)
127
+
128
+ def test_disambiguation_and_no_forbidden_framing(self):
129
+ readme = (ROOT / "PYPI_README.md").read_text()
130
+ flat = " ".join(readme.split()) # unwrap hard-wrapped lines
131
+ self.assertIn("does **not** grade MCP servers", flat)
132
+ self.assertIn("not** audit repositories", flat)
133
+ for surface in (readme, Path(cli.__file__).read_text()):
134
+ self.assertNotIn("registry submission", surface.lower())
135
+ self.assertNotIn("submit your server", surface.lower())
136
+
137
+ def test_scope_footer_travels(self):
138
+ self.assertIn("Public-surface audit only", cli.SCOPE_FOOTER)
139
+ self.assertIn("never logs in, never buys", cli.SCOPE_FOOTER)
140
+ self.assertIn("not an endorsement", cli.SCOPE_FOOTER)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ unittest.main()