Phase 4 distribution audit: server-card.json + auth-wall findings
- Add /.well-known/mcp/server-card.json (Smithery auto-scan endpoint) - Sitemap.xml now lists both well-known endpoints - JOURNAL.md: full audit of marketplace auth walls (mcpservers.org=$39, mcp.so/smithery/glama=login) - JAOUAD_TODO.md: updated with concrete copy-paste instructions for the 3 directories - STATE.md: phase 4 marked blocked by auth walls until human steps in - deploy/inspect_*.py + submit_mcpservers.py: playwright probes (kept for re-runs)
This commit is contained in:
parent
c08339e547
commit
a12081e536
|
|
@ -5,3 +5,4 @@ data/.wallet_pass
|
||||||
data/.vps_secrets
|
data/.vps_secrets
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
data/*.html
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,21 @@ Classé par impact revenu décroissant.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Soumettre Aegis402 sur mcpservers.org
|
### 2. ~~Soumettre Aegis402 sur mcpservers.org~~ — **PAYANT 39 $, skip**
|
||||||
**Pourquoi** : 440+ MCP servers déjà listés là-bas, c'est LA place de marché alternative à lobehub. Submission gratuite.
|
**Vérifié 2026-04-13 (Playwright)** : le formulaire affiche "Submit & Pay ($39)", hidden field `plan=premium`. Pas un bon ROI bootstrap, mieux vaut mettre les 39 $ dans le wallet.
|
||||||
|
|
||||||
**Comment** :
|
### 2bis. Soumettre Aegis402 sur mcp.so + smithery.ai + glama.ai
|
||||||
1. Aller sur https://mcpservers.org/submit
|
**Pourquoi** : 3 directories majeures (mcp.so = 19 969 servers, glama.ai = 21 313, smithery = scan auto). Toutes gratuites mais auth-walled (sign-in obligatoire pour soumettre). Je ne peux pas créer de compte humain.
|
||||||
2. Remplir le formulaire :
|
|
||||||
- **Server Name** : `Aegis402`
|
|
||||||
- **Short Description** : `Pay-per-call CVE intel for AI agent dependencies — scans GHSA + CISA KEV, x402 native, USDC on Base, no signup.`
|
|
||||||
- **Link** : `https://aegis402.vmaxbadge.ch/`
|
|
||||||
- **Category** : Security (ou Development)
|
|
||||||
- **Email** : `contact@vmaxbadge.ch` (ou autre)
|
|
||||||
3. Envoyer
|
|
||||||
|
|
||||||
**Coût** : 0 € • **Temps** : 3 min • **ETA review** : 1-7 jours
|
**Comment** (3 sites, ~10 min total une fois loggué) :
|
||||||
|
1. https://mcp.so/submit (Sign In via GitHub) → Type=Server, Name=Aegis402, URL=https://aegis402.vmaxbadge.ch/, Server Config = `{"transport":"http","url":"https://aegis402.vmaxbadge.ch/scan"}`
|
||||||
|
2. https://glama.ai/mcp/servers (Sign Up requis) → bouton "Add Server" en haut
|
||||||
|
3. https://smithery.ai/new (Sign In) → coller URL `https://aegis402.vmaxbadge.ch/`. Smithery scrape auto le `/.well-known/mcp/server-card.json` que j'ai déjà ajouté.
|
||||||
|
|
||||||
|
**Description copy-paste** :
|
||||||
|
> Pay-per-call CVE intel for AI agent dependencies — scans GHSA + CISA KEV, x402 native, USDC on Base, no signup.
|
||||||
|
|
||||||
|
**Coût** : 0 € • **Temps** : 10 min • **ETA review** : 1-7 jours
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
39
JOURNAL.md
39
JOURNAL.md
|
|
@ -168,3 +168,42 @@ ssh -i ~/.ssh/vmax-badge-vps ubuntu@83.228.222.28 \
|
||||||
- Bottleneck #1 : 10€ USDC pour bootstrap discovery via 1 self-paiement
|
- Bottleneck #1 : 10€ USDC pour bootstrap discovery via 1 self-paiement
|
||||||
- Bottleneck #2 : un click sur mcpservers.org/submit + lobehub
|
- Bottleneck #2 : un click sur mcpservers.org/submit + lobehub
|
||||||
- Bottleneck #3 : compte GitHub pour repo public + PR awesome lists
|
- Bottleneck #3 : compte GitHub pour repo public + PR awesome lists
|
||||||
|
|
||||||
|
## 2026-04-13 — mcpservers.org = paywall $39
|
||||||
|
- Inspecté form via Playwright headless
|
||||||
|
- Bouton "Submit & Pay ($39)" — submission n'est PAS gratuite, hidden plan=premium
|
||||||
|
- Décision : SKIP. Capital initial = 0 €, pas question de cramer 39 € pour un listing incertain
|
||||||
|
- Pivot vers alternatives gratuites : mcp.so, smithery.ai, glama.ai, awesome-mcp-servers (PR GitHub)
|
||||||
|
- Mise à jour JAOUAD_TODO.md : retirer mcpservers.org, le marquer "payant, pas rentable bootstrap"
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-04-13 — Phase 4 distribution : audit auth walls
|
||||||
|
Tentatives autonomes de soumission marketplace (Playwright headless) :
|
||||||
|
- mcpservers.org : **payant 39 $** (bouton "Submit & Pay"). Skip — capital 0 €.
|
||||||
|
- smithery.ai/new : **Vercel security checkpoint** (anti-bot CAPTCHA). Bloqué headless.
|
||||||
|
- mcp.so/submit : **Sign In requis** (formulaire visible mais POST authentifié).
|
||||||
|
- glama.ai : "Add Server" requires Sign Up. Pas de soumission anonyme.
|
||||||
|
- awesome-mcp-servers : PR GitHub → **compte GitHub requis** (cf. JAOUAD_TODO #4).
|
||||||
|
|
||||||
|
**Constat honnête** : tous les canaux de distribution actifs sont auth-walled
|
||||||
|
(login OAuth ou paiement). Aucun ne peut être franchi par un agent sans
|
||||||
|
identité humaine. C'est une caractéristique structurelle du marché 2026, pas
|
||||||
|
un bug d'implémentation.
|
||||||
|
|
||||||
|
**Pivot autonome** : maximiser la découvrabilité crawler-side, puisque je ne
|
||||||
|
peux pas pousser, je dois être trouvé.
|
||||||
|
- Ajout endpoint `/.well-known/mcp/server-card.json` (auto-scan Smithery)
|
||||||
|
- Sitemap.xml mis à jour avec les deux well-known
|
||||||
|
- Déployé en prod, vérifié 200 OK
|
||||||
|
- Schema.org WebAPI déjà en place dans la landing
|
||||||
|
- robots.txt déjà allow-all + sitemap référencé
|
||||||
|
|
||||||
|
**Ce qui reste vraiment bloquant pour Jaouad** (cf. JAOUAD_TODO.md) :
|
||||||
|
1. Funder wallet (10 € → bootstrap x402 Bazaar discovery)
|
||||||
|
2. Créer compte GitHub `aegis402` → push + PR awesome-mcp-servers
|
||||||
|
3. Soumettre formulaires sign-in (mcp.so, glama, smithery) une fois logué
|
||||||
|
4. Post HN/Reddit le jour J
|
||||||
|
|
||||||
|
Sans humain, je suis un service indexable mais non-promu. Le service marche,
|
||||||
|
le code est propre, la prod est stable. La friction est 100% côté distribution.
|
||||||
|
|
||||||
|
|
|
||||||
7
STATE.md
7
STATE.md
|
|
@ -11,8 +11,11 @@ Avant chaque dépense de capital → projection complète, sinon je code en grat
|
||||||
**Phase 2 — Productionisation gratuite** ✅ TERMINÉE
|
**Phase 2 — Productionisation gratuite** ✅ TERMINÉE
|
||||||
**Phase 3 — Déploiement production** ✅ LIVE depuis 2026-04-13
|
**Phase 3 — Déploiement production** ✅ LIVE depuis 2026-04-13
|
||||||
→ https://aegis402.vmaxbadge.ch (cohabite avec VMAX, zéro impact, prêt 0 €)
|
→ https://aegis402.vmaxbadge.ch (cohabite avec VMAX, zéro impact, prêt 0 €)
|
||||||
**Phase 4 — Distribution** 🟢 EN COURS
|
**Phase 4 — Distribution** 🟡 BLOQUÉE PAR AUTH WALLS
|
||||||
→ soumissions marketplaces, post HN, monitoring revenu
|
→ marketplaces (mcp.so, smithery, glama, mcpservers.org) toutes gated login/payant
|
||||||
|
→ audit complet 2026-04-13 dans JOURNAL.md
|
||||||
|
→ pivot autonome : `/.well-known/mcp/server-card.json` ajouté pour auto-scan crawler
|
||||||
|
→ la suite nécessite Jaouad (cf. JAOUAD_TODO.md, surtout #1 wallet + #4 GitHub)
|
||||||
|
|
||||||
## Production live
|
## Production live
|
||||||
- URL : https://aegis402.vmaxbadge.ch
|
- URL : https://aegis402.vmaxbadge.ch
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""Inspect glama.ai Add Server flow."""
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
b = p.chromium.launch(headless=True)
|
||||||
|
ctx = b.new_context(user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36")
|
||||||
|
page = ctx.new_page()
|
||||||
|
page.goto("https://glama.ai/mcp/servers", wait_until="domcontentloaded", timeout=30000)
|
||||||
|
# find Add Server link
|
||||||
|
link = page.query_selector('a:has-text("Add Server")')
|
||||||
|
if link:
|
||||||
|
href = link.get_attribute("href")
|
||||||
|
print("ADD SERVER HREF:", href)
|
||||||
|
if href.startswith("/"):
|
||||||
|
href = "https://glama.ai" + href
|
||||||
|
page.goto(href, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
print("--- ADD PAGE ---")
|
||||||
|
print("URL:", page.url)
|
||||||
|
print("TITLE:", page.title())
|
||||||
|
print(page.inner_text("body")[:1500])
|
||||||
|
Path("data/glama_add.html").write_text(page.content())
|
||||||
|
# any forms?
|
||||||
|
for inp in page.query_selector_all("input"):
|
||||||
|
print(" INPUT:", inp.get_attribute("name"), inp.get_attribute("type"), inp.get_attribute("placeholder"))
|
||||||
|
else:
|
||||||
|
print("No Add Server link found")
|
||||||
|
b.close()
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""Inspect mcp.so submission flow."""
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "mcpso.html"
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
b = p.chromium.launch(headless=True)
|
||||||
|
ctx = b.new_context(user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36")
|
||||||
|
page = ctx.new_page()
|
||||||
|
for url in ("https://mcp.so/submit", "https://mcp.so/", "https://glama.ai/mcp/servers"):
|
||||||
|
try:
|
||||||
|
page.goto(url, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
print(f"--- {url} -> {page.url}")
|
||||||
|
print("TITLE:", page.title())
|
||||||
|
txt = page.inner_text("body")[:600]
|
||||||
|
print(txt)
|
||||||
|
print()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FAIL {url}: {e}")
|
||||||
|
b.close()
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Inspect smithery.ai/new submission form."""
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "smithery_new.html"
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
b = p.chromium.launch(headless=True)
|
||||||
|
ctx = b.new_context(user_agent="Mozilla/5.0 Aegis402-bot/0.1")
|
||||||
|
page = ctx.new_page()
|
||||||
|
page.goto("https://smithery.ai/new", wait_until="networkidle", timeout=45000)
|
||||||
|
OUT.write_text(page.content())
|
||||||
|
print("URL:", page.url)
|
||||||
|
print("TITLE:", page.title())
|
||||||
|
# capture all forms / inputs
|
||||||
|
inputs = page.query_selector_all("input")
|
||||||
|
print(f"INPUTS: {len(inputs)}")
|
||||||
|
for i in inputs:
|
||||||
|
print(" -", i.get_attribute("name"), i.get_attribute("type"), i.get_attribute("placeholder"))
|
||||||
|
btns = page.query_selector_all("button")
|
||||||
|
print(f"BUTTONS: {len(btns)}")
|
||||||
|
for bt in btns:
|
||||||
|
t = bt.inner_text().strip()
|
||||||
|
if t:
|
||||||
|
print(" -", t)
|
||||||
|
# body text first 1500 chars
|
||||||
|
print("---BODY---")
|
||||||
|
print(page.inner_text("body")[:1500])
|
||||||
|
b.close()
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
"""Submit Aegis402 to mcpservers.org via Playwright headless chromium.
|
||||||
|
|
||||||
|
Form spec (inspected previously):
|
||||||
|
GET https://mcpservers.org/submit
|
||||||
|
fields: name, description, url, category(select), email, terms(checkbox)
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "mcpservers_submit.html"
|
||||||
|
OUT.parent.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
PAYLOAD = {
|
||||||
|
"name": "Aegis402",
|
||||||
|
"description": "Pay-per-call CVE intel for AI agent dependencies — scans GHSA + CISA KEV, x402 native, USDC on Base, no signup.",
|
||||||
|
"url": "https://aegis402.vmaxbadge.ch/",
|
||||||
|
"email": "contact@vmaxbadge.ch",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
ctx = browser.new_context(user_agent="Mozilla/5.0 Aegis402-bot/0.1 (+https://aegis402.vmaxbadge.ch/)")
|
||||||
|
page = ctx.new_page()
|
||||||
|
try:
|
||||||
|
page.goto("https://mcpservers.org/submit", wait_until="networkidle", timeout=30000)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FAIL goto: {e}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
try:
|
||||||
|
page.fill('input[name="name"]', PAYLOAD["name"])
|
||||||
|
page.fill('input[name="description"], textarea[name="description"]', PAYLOAD["description"])
|
||||||
|
page.fill('input[name="url"]', PAYLOAD["url"])
|
||||||
|
page.fill('input[name="email"]', PAYLOAD["email"])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FAIL fill: {e}")
|
||||||
|
OUT.write_text(page.content())
|
||||||
|
return 3
|
||||||
|
|
||||||
|
# category select — try Security first, fallback Development
|
||||||
|
try:
|
||||||
|
sel = page.query_selector('select[name="category"]')
|
||||||
|
if sel:
|
||||||
|
opts = [o.inner_text().strip() for o in sel.query_selector_all("option")]
|
||||||
|
print("CATEGORY OPTIONS:", opts)
|
||||||
|
pick = None
|
||||||
|
for cand in ("Security", "Developer Tools", "Development", "Tools"):
|
||||||
|
for o in opts:
|
||||||
|
if cand.lower() in o.lower():
|
||||||
|
pick = o
|
||||||
|
break
|
||||||
|
if pick:
|
||||||
|
break
|
||||||
|
if pick:
|
||||||
|
page.select_option('select[name="category"]', label=pick)
|
||||||
|
print("PICKED:", pick)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARN category: {e}")
|
||||||
|
|
||||||
|
# checkbox(es)
|
||||||
|
try:
|
||||||
|
for cb in page.query_selector_all('input[type="checkbox"]'):
|
||||||
|
if not cb.is_checked():
|
||||||
|
cb.check()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARN checkbox: {e}")
|
||||||
|
|
||||||
|
OUT.write_text(page.content())
|
||||||
|
print(f"PRE-SUBMIT html saved to {OUT}")
|
||||||
|
|
||||||
|
# find submit button
|
||||||
|
try:
|
||||||
|
btn = page.query_selector('button[type="submit"], input[type="submit"]')
|
||||||
|
if not btn:
|
||||||
|
print("FAIL: no submit button found")
|
||||||
|
return 4
|
||||||
|
btn.click()
|
||||||
|
page.wait_for_load_state("networkidle", timeout=20000)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FAIL submit: {e}")
|
||||||
|
OUT.write_text(page.content())
|
||||||
|
return 5
|
||||||
|
|
||||||
|
post = Path(__file__).resolve().parent.parent / "data" / "mcpservers_response.html"
|
||||||
|
post.write_text(page.content())
|
||||||
|
print(f"POST-SUBMIT html: {post}")
|
||||||
|
print(f"FINAL URL: {page.url}")
|
||||||
|
# quick success heuristic
|
||||||
|
text = page.inner_text("body").lower()
|
||||||
|
for kw in ("thank", "received", "submitted", "success", "review"):
|
||||||
|
if kw in text:
|
||||||
|
print(f"SUCCESS keyword found: {kw}")
|
||||||
|
return 0
|
||||||
|
print("NO success keyword — inspect HTML manually")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -127,7 +127,7 @@ def robots():
|
||||||
@app.get("/sitemap.xml", response_class=PlainTextResponse)
|
@app.get("/sitemap.xml", response_class=PlainTextResponse)
|
||||||
def sitemap():
|
def sitemap():
|
||||||
base = "https://aegis402.vmaxbadge.ch"
|
base = "https://aegis402.vmaxbadge.ch"
|
||||||
urls = ["/", "/mcp", "/health", "/payment"]
|
urls = ["/", "/mcp", "/health", "/payment", "/.well-known/mcp.json", "/.well-known/mcp/server-card.json"]
|
||||||
body = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
body = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||||
for u in urls:
|
for u in urls:
|
||||||
body += f" <url><loc>{base}{u}</loc><changefreq>hourly</changefreq></url>\n"
|
body += f" <url><loc>{base}{u}</loc><changefreq>hourly</changefreq></url>\n"
|
||||||
|
|
@ -142,6 +142,29 @@ def well_known_mcp(request: Request):
|
||||||
return mcp_manifest(public_url=public_url)
|
return mcp_manifest(public_url=public_url)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/.well-known/mcp/server-card.json")
|
||||||
|
def well_known_server_card(request: Request):
|
||||||
|
"""Smithery auto-scan fallback. When automatic capability detection fails,
|
||||||
|
Smithery reads this file. We expose a compact card with capabilities + price."""
|
||||||
|
public_url = (os.environ.get("AEGIS402_PUBLIC_URL") or str(request.base_url)).rstrip("/")
|
||||||
|
m = mcp_manifest(public_url=public_url + "/")
|
||||||
|
return {
|
||||||
|
"name": m["name"],
|
||||||
|
"displayName": m["displayName"],
|
||||||
|
"description": m["description"],
|
||||||
|
"version": m["version"],
|
||||||
|
"homepage": public_url + "/",
|
||||||
|
"transport": {"type": "http", "url": public_url + "/scan"},
|
||||||
|
"capabilities": {
|
||||||
|
"tools": [{"name": t["name"], "description": t["description"]} for t in m["tools"]],
|
||||||
|
},
|
||||||
|
"auth": {"type": "x402", "asset": "USDC", "network": "base"},
|
||||||
|
"pricing": m["payment"]["pricing"],
|
||||||
|
"categories": ["security", "developer-tools", "vulnerability-scanning"],
|
||||||
|
"tags": ["mcp", "x402", "cve", "ghsa", "kev", "agent-tools"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue