diff --git a/.gitignore b/.gitignore index 3a19ff5..7b0361a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ data/.wallet_pass data/.vps_secrets __pycache__/ *.pyc +data/*.html diff --git a/JAOUAD_TODO.md b/JAOUAD_TODO.md index 72515bc..d60265a 100644 --- a/JAOUAD_TODO.md +++ b/JAOUAD_TODO.md @@ -20,20 +20,21 @@ Classé par impact revenu décroissant. --- -### 2. Soumettre Aegis402 sur mcpservers.org -**Pourquoi** : 440+ MCP servers déjà listés là-bas, c'est LA place de marché alternative à lobehub. Submission gratuite. +### 2. ~~Soumettre Aegis402 sur mcpservers.org~~ — **PAYANT 39 $, skip** +**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** : -1. Aller sur https://mcpservers.org/submit -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 +### 2bis. Soumettre Aegis402 sur mcp.so + smithery.ai + glama.ai +**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. -**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 --- diff --git a/JOURNAL.md b/JOURNAL.md index 6defd51..a0cbbc4 100644 --- a/JOURNAL.md +++ b/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 #2 : un click sur mcpservers.org/submit + lobehub - 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. + diff --git a/STATE.md b/STATE.md index f609f92..280fdbc 100644 --- a/STATE.md +++ b/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 3 — Déploiement production** ✅ LIVE depuis 2026-04-13 → https://aegis402.vmaxbadge.ch (cohabite avec VMAX, zéro impact, prêt 0 €) -**Phase 4 — Distribution** 🟢 EN COURS - → soumissions marketplaces, post HN, monitoring revenu +**Phase 4 — Distribution** 🟡 BLOQUÉE PAR AUTH WALLS + → 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 - URL : https://aegis402.vmaxbadge.ch diff --git a/deploy/inspect_glama.py b/deploy/inspect_glama.py new file mode 100644 index 0000000..29865cc --- /dev/null +++ b/deploy/inspect_glama.py @@ -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() diff --git a/deploy/inspect_mcpso.py b/deploy/inspect_mcpso.py new file mode 100644 index 0000000..78f002f --- /dev/null +++ b/deploy/inspect_mcpso.py @@ -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() diff --git a/deploy/inspect_smithery.py b/deploy/inspect_smithery.py new file mode 100644 index 0000000..a711995 --- /dev/null +++ b/deploy/inspect_smithery.py @@ -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() diff --git a/deploy/submit_mcpservers.py b/deploy/submit_mcpservers.py new file mode 100644 index 0000000..26533b8 --- /dev/null +++ b/deploy/submit_mcpservers.py @@ -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()) diff --git a/src/server.py b/src/server.py index 8a08f73..c9354ca 100644 --- a/src/server.py +++ b/src/server.py @@ -127,7 +127,7 @@ def robots(): @app.get("/sitemap.xml", response_class=PlainTextResponse) def sitemap(): base = "https://aegis402.vmaxbadge.ch" - urls = ["/", "/mcp", "/health", "/payment"] + urls = ["/", "/mcp", "/health", "/payment", "/.well-known/mcp.json", "/.well-known/mcp/server-card.json"] body = '\n\n' for u in urls: body += f" {base}{u}hourly\n" @@ -142,6 +142,29 @@ def well_known_mcp(request: Request): 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") def health(): conn = get_conn()