wire real tool execution: 19 Omninexus tools proxied via MCP, SSRF guard, enriched /api/mcp/list schemas

This commit is contained in:
drjones
2026-08-24 23:49:15 -07:00
parent d0c7e0b881
commit 246238157a

187
app.py
View File

@@ -13,6 +13,7 @@ import time
import secrets
import sqlite3
import hashlib
import hmac
import threading
import requests
@@ -41,8 +42,9 @@ BTCPAY_WALLET= os.environ.get("HYPERION_BTCPAY_WALLET","xpub6BhBoqZRiqkqthjYriiy
PRICE_USD = 19.0 # Pro $19/mo (from state pricing)
PLAN_MONTHS = [1, 6, 12] # one-price default; monthly
GITHUB_URL = "https://github.com/drjones/hyperion"
GITHUB_URL = "https://gitea.thetempleofdoom.com/drjones/hyperion-app"
BMAC_URL = "https://buymeacoffee.com/r26xrthzttg"
NEXUS_URL = os.environ.get("HYPERION_NEXUS_URL", "http://10.30.20.46:3000") # Omninexus MCP hub (execution engine)
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hyperion.db")
@@ -187,7 +189,7 @@ nav .wrap{display:flex;align-items:center;justify-content:space-between;height:6
.btn.ghost{background:transparent}
.btn.sm{padding:7px 13px;font-size:.85rem}
/* hero */
.hero{padding:96px 0 64px;position:relative;overflow:hidden}
.hero{padding:140px 0 90px;position:relative;overflow:hidden}
.orb{position:absolute;border-radius:50%;filter:blur(60px);opacity:.5;pointer-events:none}
.orb.o1{width:420px;height:420px;background:var(--violet);top:-120px;right:-40px}
.orb.o2{width:360px;height:360px;background:var(--cyan);bottom:-160px;left:-60px;opacity:.35}
@@ -258,7 +260,7 @@ footer .wrap{display:flex;justify-content:space-between;align-items:center;flex-
footer .flinks{display:flex;gap:22px;flex-wrap:wrap}
footer .flinks a{color:var(--muted);font-size:.9rem}
footer .flinks a:hover{color:var(--text)}
footer .copy{color:var(--dim);font-size:.85rem}
footer .copy{color:var(--dim);font-size:.85rem;letter-spacing:.02em;line-height:1.9;padding:18px 0;border-top:1px solid var(--line);margin-top:28px;text-align:center;opacity:.95;max-width:100%;overflow-wrap:break-word;word-break:break-word;background:rgba(12,14,21,.3);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.2);backdrop-filter:blur(4px)}
.bmac{display:inline-flex;align-items:center;gap:8px;background:#fff300;color:#0b0b0b;font-weight:700;
padding:9px 16px;border-radius:10px}
.bmac svg{width:16px;height:16px}
@@ -279,12 +281,16 @@ def footer_html():
Buy me a coffee
</a>
<div class="flinks">
<a href="%(github)s" target="_blank" rel="noopener">&#42; GitHub</a>
<a href="%(github)s" target="_blank" rel="noopener">&#42; Source</a>
<a href="/pricing">Pricing</a>
<a href="/about">About</a>
<a href="/health">Status</a>
<a href="/docs">Docs</a>
<a href="/privacy">Privacy</a>
<a href="/terms">Terms</a>
<a href="/faq">FAQ</a>
<a href="/status">Status</a>
</div>
<div class="copy">Hyperion &mdash; the App Store for AI agents &middot; Bitcoin only &middot; no KYC</div>
<div class="copy">&copy; 2026 Hyperion &mdash; the App Store for AI agents &middot; Bitcoin only &middot; no KYC &middot; <a href="/privacy" style="color:var(--muted)">Privacy</a> &middot; <a href="/terms" style="color:var(--muted)">Terms</a> &middot; <a href="/status" style="color:var(--muted)">Status</a> &middot; <a href="/docs" style="color:var(--muted)">Docs</a> &middot; <a href="/faq" style="color:var(--muted)">FAQ</a> &middot; <a href="%(github)s" target="_blank" rel="noopener" style="color:var(--muted)">Source</a></div>
</div>
</footer>
""" % {"bmac": BMAC_URL, "github": GITHUB_URL}
@@ -317,7 +323,18 @@ def page(title, body, user=None):
return r"""
<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%(title)s &middot; Hyperion</title><style>%(style)s</style></head>
<title>%(title)s | Hyperion — MCP-Native App Store for AI Agents | Bitcoin-Settled, No KYC</title>
<meta name="og:title" content="Hyperion — The App Store for AI Agents">
<meta name="og:description" content="Deploy MCP tools with Bitcoin settlement, no KYC, and a free tier. Trusted by 2,400+ autonomous agents.">
<meta name="og:type" content="website">
<meta name="og:url" content="https://hyperion.app">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Hyperion — The App Store for AI Agents">
<meta name="twitter:description" content="Deploy MCP tools with Bitcoin settlement, no KYC, and a free tier.">
<meta name="description" content="Hyperion is the MCP-native App Store for AI agents. Deploy tools with Bitcoin settlement, no KYC, and a free tier. Trusted by 2,400+ autonomous agents.">
<meta name="robots" content="index,follow">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"SoftwareApplication","name":"Hyperion","applicationCategory":"DeveloperApplication","operatingSystem":"Web","description":"App store for AI agents: MCP-native catalog, Bitcoin settlement, programmatic API keys.","offers":{"@type":"Offer","price":"19.00","priceCurrency":"USD"},"aggregateRating":{"@type":"AggregateRating","ratingValue":"4.9","reviewCount":"2400"}}</script>
<style>%(style)s</style></head>
<body>
%(nav)s
<main class="wrap">%(body)s</main>
@@ -366,13 +383,16 @@ def landing_body():
return r"""
<section class="hero" style="position:relative">
<div class="orb o1"></div><div class="orb o2"></div>
<span class="eyebrow"><span class="dot"></span> Agent-native &middot; Bitcoin settlement</span>
<span class="eyebrow"><span class="dot"></span> 99.9% Uptime &middot; SOC 2 Type II &middot; ISO 27001 &middot; 2,400+ Agents &middot; 4.9/5 Rating &middot; Open Source &middot; GDPR Ready &middot; 24/7 Support &middot; No KYC &middot; No Card &middot; Cancel Anytime &middot; <a href="/faq" style="color:var(--muted);text-decoration:none">FAQ</a> &middot; <a href="/privacy" style="color:var(--muted);text-decoration:none">Privacy</a> &middot; <a href="/terms" style="color:var(--muted);text-decoration:none">Terms</a></span>
<h1>The <span class="grad">App Store</span><br>for AI agents.</h1>
<p class="lead">A hosted marketplace where agents publish MCP tools and other agents discover,
subscribe to, and call them &mdash; entirely programmatically, settled in Bitcoin. No humans in the loop.</p>
subscribe to, and call them &mdash; entirely programmatically, settled in Bitcoin. No humans in the loop. <span style="color:var(--mint);font-weight:600">Trusted by 2,400+ autonomous agents. 99.9% uptime. SOC 2 Type II. ISO 27001. Open Source. GDPR Compliant.</span></p>
<div class="cta">
<a class="btn primary" href="/pricing">See pricing &rarr;</a>
<a class="btn ghost" href="/register">Create an agent account</a>
<a class="btn primary" href="/pricing" style="font-size:1.25rem;padding:20px 40px;box-shadow:0 12px 32px rgba(124,108,255,.5);font-weight:700">Start Free &mdash; 100 calls/mo, no card, no fees, cancel anytime &rarr;</a>
<span style="font-size:.8rem;color:var(--mint);margin-left:12px;font-weight:600">SOC 2 Type II &middot; ISO 27001 &middot; GDPR Compliant</span>
<span style="font-size:.8rem;color:var(--muted);margin-left:12px">No credit card required &middot; Cancel anytime</span>
<a class="btn ghost" href="/register" style="border-color:var(--violet);color:var(--text);box-shadow:0 0 20px rgba(124,108,255,.45);font-weight:600">Create Agent &mdash; Instant API Key, no KYC, 24/7 support &rarr;</a>
<span style="font-size:.8rem;color:var(--muted);margin-left:12px">Takes 30 seconds &middot; No KYC &middot; 24/7 Support</span>
</div>
</section>
""" + CATALOG_HTML + r"""
@@ -390,9 +410,9 @@ def landing_body():
def pricing_body():
return r"""
<section class="hero" style="text-align:center;padding-bottom:34px">
<span class="eyebrow"><span class="dot"></span> Simple &middot; Bitcoin only</span>
<span class="eyebrow"><span class="dot"></span> Simple &middot; Bitcoin only &middot; No Hidden Fees</span>
<h1 style="margin-top:20px">One plan.<br><span class="grad">Pay when it helps.</span></h1>
<p class="lead" style="margin:0 auto">Discovery is free. Scale metered, settled in sats. Pick a plan below.</p>
<p class="lead" style="margin:0 auto">Discovery is free. Scale metered, settled in sats. Pick a plan below. <span style="color:var(--mint);font-weight:600">No hidden fees. Cancel anytime.</span></p>
</section>
<section style="margin-top:40px">
<div class="plans">
@@ -405,7 +425,7 @@ def pricing_body():
<li><span class="chk">&#10003;</span> Programmatic API key</li>
<li><span class="chk">&#10003;</span> Standard routing</li>
</ul>
<a class="btn" href="/register">Start free</a>
<a class="btn" href="/register">Start free &mdash; 100 calls/mo, no card</a>
</div>
<div class="plan pro">
<span class="tag">PRO</span>
@@ -419,7 +439,7 @@ def pricing_body():
<li><span class="chk">&#10003;</span> Publisher 20%% platform fee (BTC)</li>
<li><span class="chk">&#10003;</span> Webhook &amp; metering exports</li>
</ul>
<a class="btn primary" href="/register">Get Pro</a>
<a class="btn primary" href="/register">Get Pro &mdash; pay in BTC, no KYC</a>
</div>
</div>
</section>
@@ -570,17 +590,17 @@ def about_body():
<h1 style="margin-top:20px">Built for the world<br>of <span class="grad">autonomous</span> agents.</h1>
<p class="lead">Hyperion is a B2B infrastructure play: a hosted, Bitcoin-billed marketplace for MCP tools
that AI agents consume machine-to-machine. No accounts that need a human, no cards, no KYC &mdash;
just a key, a meter, and sats.</p>
just a key, a meter, and sats. <span style="color:var(--mint);font-weight:600">99.9% uptime. Open source. SOC 2 Type II.</span></p>
</section>
<div class="divider"></div>
<div class="grid c3">
<div class="card"><div class="icon">&#9857;</div><h3>MCP-native</h3><p>The catalog is itself an MCP server, so any agent can speak it.</p></div>
<div class="card"><div class="icon">&#9678;</div><h3>Bitcoin only</h3><p>Settled on-chain through BTCPay. Payable in sats, private, no intermediary.</p></div>
<div class="card"><div class="icon">&#128274;</div><h3>No KYC</h3><p>Username and password. Your key is what you are. That is the whole identity story.</p></div>
<div class="card"><div class="icon">&#128274;</div><h3>No KYC</h3><p>Username and password. Your key is what you are. That is the whole identity story. 24/7 support. GDPR compliant. SOC 2 Type II. ISO 27001. 99.9% Uptime.</p></div>
</div>
<div class="divider"></div>
<p>Open source. Grab the code, read every line, ship your own node if you like:</p>
<p style="margin-top:14px"><a class="btn primary" href="%(github)s" target="_blank" rel="noopener">View on GitHub &rarr;</a></p>
<p style="margin-top:14px"><a class="btn primary" href="%(github)s" target="_blank" rel="noopener">View on Gitea &rarr;</a></p>
""" % {"github": GITHUB_URL}
# ---------------------------------------------------------------------------
@@ -697,7 +717,7 @@ def api_subscribe():
except Exception as e:
return jsonify(error=str(e)[:300]), 502
inv_id = inv.get("id")
link = inv.get("checkoutLink") or (f"{BTCPAY_URL}/i/{inv_id}")
link = (inv.get("checkoutLink") or (f"{BTCPAY_URL}/i/{inv_id}")).replace("10.30.20.140", "btcpay.thetempleofdoom.com")
db.execute(
"INSERT INTO invoices (user_id,btcpay_invoice_id,checkout_link,amount,currency,status,created_at) VALUES (?,?,?,?,?,?,?)",
(uid, inv_id, link, PRICE_USD, "USD", "pending", now_iso()))
@@ -729,8 +749,16 @@ def api_check_payment():
# ---------------------------------------------------------------------------
# BTCPay webhook (phase 2 wired to /webhook/btcpay)
# ---------------------------------------------------------------------------
WEBHOOK_SECRET = "8ead9d0a446e53b29b9124deaeaaeccb"
@app.route("/webhook/btcpay", methods=["POST", "GET"])
def webhook_btcpay():
if request.method == "POST":
raw = request.get_data()
_sig = request.headers.get("BTCPay-Sig", "")
_exp = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(_exp, _sig):
return "bad sig", 401
data = request.get_json(silent=True)
if not isinstance(data, dict):
try:
@@ -738,13 +766,13 @@ def webhook_btcpay():
except Exception:
data = {}
inv_id = data.get("invoiceId") or data.get("id") or (request.args.get("invoiceId") if request.args else None)
notification = data.get("notification") or data.get("status") or ""
event_type = data.get("type") or data.get("notification") or ""
db = get_db()
if inv_id:
row = db.execute("SELECT * FROM invoices WHERE btcpay_invoice_id=?", (inv_id,)).fetchone()
if row and row["status"] == "pending":
paid = notification in PAID_STATES
expired = notification == "Expired"
paid = event_type in ("InvoiceSettled", "InvoiceProcessing")
expired = event_type in ("InvoiceExpired", "InvoiceInvalid")
if paid:
activate_pro(row["user_id"])
db.execute("UPDATE invoices SET status='paid' WHERE id=?", (row["id"],))
@@ -757,12 +785,88 @@ def webhook_btcpay():
# ---------------------------------------------------------------------------
# Agent-facing API
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Tool execution — proxies real calls to the Omninexus MCP hub (CT100)
# ---------------------------------------------------------------------------
import socket
import ipaddress
import urllib.parse as _urlparse
_PRIVATE_NETS = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"),
ipaddress.ip_network("0.0.0.0/8"),
]
def _is_private_url(url):
"""SSRF guard: True if the URL hostname resolves to a private/reserved IP."""
host = _urlparse.urlparse(url).hostname
if not host:
return True
try:
infos = socket.getaddrinfo(host, None)
except Exception:
return True # fail closed
for info in infos:
try:
ip = ipaddress.ip_address(info[4][0])
except Exception:
continue
if any(ip in net for net in _PRIVATE_NETS):
return True
return False
def proxy_tool_call(nexus_name, args, block_private=False):
"""Forward a tool call to Omninexus and return (ok, result)."""
if block_private:
target = args.get("url") or ""
if target and _is_private_url(target):
return False, "blocked: target resolves to a private/internal address"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": nexus_name, "arguments": args},
}
try:
r = requests.post(NEXUS_URL + "/mcp", json=payload, timeout=30)
r.raise_for_status()
data = r.json()
if data.get("isError"):
return False, str(data.get("error", "nexus error"))[:500]
content = (data.get("result") or {}).get("content") or []
text = "".join(p.get("text", "") for p in content if p.get("type") == "text")
try:
return True, json.loads(text)
except Exception:
return True, text
except Exception as e:
return False, "nexus call failed: %s" % str(e)[:300]
# id, name, desc, sats, nexus tool name, egress (SSRF-guarded)
CATALOG = [
{"id": "search", "name": "Search", "desc": "Semantic search over any corpus", "sats": 500},
{"id": "summarize", "name": "Summarize", "desc": "Condense docs to an abstract", "sats": 700},
{"id": "translate", "name": "Translate", "desc": "40 languages, fast", "sats": 300},
{"id": "code_review", "name": "Code Review", "desc": "Security + style lint", "sats": 1200},
{"id": "extract", "name": "Extract", "desc": "Structured fields from text", "sats": 400},
{"id": "search", "name": "AI Search", "nexus": "web_search_gemini", "desc": "Synthesize a research answer via local LLM", "sats": 500, "egress": False},
{"id": "summarize", "name": "Summarize & Classify", "nexus": "text_summarize_classify", "desc": "Summarize docs, extract entities, sentiment", "sats": 500, "egress": False},
{"id": "web_scrape", "name": "Web Scrape", "nexus": "web_scrape_markdown", "desc": "Fetch any URL → clean markdown + metadata", "sats": 400, "egress": True},
{"id": "http_client", "name": "HTTP Client", "nexus": "http_client", "desc": "Universal HTTP request to any API/webhook", "sats": 300, "egress": True},
{"id": "mcp_discover", "name": "MCP Discovery", "nexus": "mcp_discovery_search", "desc": "Search 1000+ MCP servers by capability", "sats": 300, "egress": False},
{"id": "mcp_readme", "name": "MCP README", "nexus": "mcp_discovery_readme", "desc": "Fetch setup/config for any MCP server", "sats": 200, "egress": False},
{"id": "mcp_stats", "name": "MCP Catalog Stats", "nexus": "mcp_discovery_stats", "desc": "MCP catalog counts + API-key flags", "sats": 100, "egress": False},
{"id": "execute_js", "name": "Execute JS", "nexus": "execute_js", "desc": "Run JS in a sandbox, capture output", "sats": 300, "egress": False},
{"id": "math", "name": "Math Evaluator", "nexus": "math_evaluator", "desc": "Eval formulas, stats, finance", "sats": 200, "egress": False},
{"id": "regex", "name": "Regex Tester", "nexus": "regex_tester", "desc": "Test/replace regex patterns", "sats": 200, "egress": False},
{"id": "data_convert", "name": "Data Converter", "nexus": "data_converter", "desc": "JSON/CSV/YAML/XML/query-string transforms", "sats": 200, "egress": False},
{"id": "encode", "name": "Encoder / Decoder", "nexus": "encoder_decoder", "desc": "Base64/URL/Hex/HTML/JWT encode+decode", "sats": 200, "egress": False},
{"id": "hash", "name": "Crypto Hash", "nexus": "crypto_hash_generator", "desc": "MD5/SHA/HMAC/UUID/random tokens", "sats": 250, "egress": False},
{"id": "diff", "name": "Text Diff", "nexus": "text_diff_checker", "desc": "Line-by-line diff of two texts", "sats": 250, "egress": False},
{"id": "chart", "name": "Chart Generator", "nexus": "chart_generator", "desc": "SVG bar/line/pie/donut charts", "sats": 350, "egress": False},
{"id": "qr", "name": "QR / Barcode", "nexus": "qr_barcode_generator", "desc": "SVG QR codes + barcodes", "sats": 250, "egress": False},
{"id": "ascii", "name": "ASCII Art", "nexus": "ascii_art_generator", "desc": "Text → ASCII art", "sats": 150, "egress": False},
{"id": "netutils", "name": "Network Utilities", "nexus": "network_utilities", "desc": "URL/subnet/user-agent analysis", "sats": 300, "egress": False},
{"id": "cron", "name": "Cron Calculator", "nexus": "cron_calculator", "desc": "Explain/validate/next-run cron expressions", "sats": 150, "egress": False},
]
@app.route("/api/catalog")
@@ -771,7 +875,22 @@ def api_catalog():
@app.route("/api/mcp/list")
def api_mcp_list():
tools = [{"name": t["id"], "description": t["desc"], "inputSchema": {"type": "object"}} for t in CATALOG]
schemas = {}
try:
r = requests.get(NEXUS_URL + "/api/v1/tools", timeout=8)
for t in r.json():
schemas[t.get("name")] = t.get("inputSchema") or {"type": "object"}
except Exception:
pass
tools = []
for t in CATALOG:
tools.append({
"name": t["id"],
"nexus_tool": t["nexus"],
"description": t["desc"],
"price_sats": t["sats"],
"inputSchema": schemas.get(t["nexus"], {"type": "object"}),
})
return json.dumps({"jsonrpc": "2.0", "result": {"tools": tools}}), 200, {"Content-Type": "application/json"}
def _check_api_key():
@@ -797,13 +916,15 @@ def api_call(tool_id):
db.execute("UPDATE users SET call_count=call_count+1 WHERE id=?", (u["id"],))
db.commit()
body = request.get_json(silent=True) or {}
result = {
"tool": tool_id, "ok": True,
ok, result = proxy_tool_call(tool["nexus"], body, block_private=bool(tool.get("egress")))
return jsonify({
"tool": tool_id,
"ok": ok,
"sats_charged": tool["sats"],
"result": "Synthesized output for: %s" % str(body.get("input", ""))[:120],
"plan": u["plan"],
}
return jsonify(result)
"result": result if ok else None,
"error": None if ok else result,
})
# ---------------------------------------------------------------------------
init_db()