CASE · 082025 — ONGOINGSOLOREGISTRY · A2A ↔ MCP BRIDGE
◦ AGORAHUB · agorahub.dev

Every agent a tool. Every protocol bridged.

Open registry where AI agents register via agent.json, get discovered, and are bridged. Agents speak A2A (Google) and are exposed at runtime as MCP tools (Anthropic). Next.js 16, Prisma 6, Postgres, self-hosted on Coolify.

§ 01Problem · motivation

Why this exists.

Agents from different ecosystems can't talk to each other. Every vendor builds their own walled garden.

Agents from different ecosystems — Claude/MCP, Google/A2A, self-hosted — currently can't talk to each other. Every vendor builds their own walled garden. Developers building or consuming agents need a neutral discovery layer that existing marketplaces don't provide.

GPT Store, Claude MCP Directory — vendor lock-in, schema-thin, no cross-protocol execution bridge. AgoraHub is the attempt to build one registry where an A2A agent automatically shows up as an MCP tool in Claude Desktop, without the agent builder having to write anything twice.

§ 02Constraints · operating box

The box it had to fit in.

Solo dev, self-hosted, security-first — because user-submitted URLs get executed directly.
C/01 · OPS
Solo dev, side project. Every design decision maintainable without a team.
C/02 · HOSTING
Self-hosted on Coolify. No Vercel lock-in, infra costs in the low two-digit €/month range.
C/03 · STACK
Next.js 16 App Router + Prisma 6 + Postgres. One deployment, no microservices.
C/04 · SECURITY
Agents ingest untrusted JSON from external endpoints. XSS sanitization, SSRF quarantine, HMAC webhooks, API keys as SHA-256 hashes.
C/05 · NEUTRAL
Pure A2A + MCP. No proprietary extensions — agents stay portable.
C/06 · BURSTS
MCP tool calls arrive in bursts (LLMs probe in parallel). Rate limiting per IP + per key.
C/07 · TRUST
Anyone can publish. 4 trust tiers + 5 badges — computed automatically from uptime, completion rate, reviews.
C/08 · NO GATE
No manual moderation by me. The trust system has to scale on its own.
§ 03Architecture · A2A ↔ MCP bridge

How it runs.

A2A agents left → security chain (SSRF · schema check · trust gate) → bridge runtime → MCP clients right. The registry is the hub — every other route runs through it.
registry.live·agents 84·trusted 61·mcp-tools 47·ssrf-blocked 12
bridge ms·rl ok
A2A · AGENT 01
translator.v3
agent.json · skills: [translate]
trustS★
A2A · AGENT 02
web-scrape.io
skills: [fetch, parse]
trustG★★
A2A · AGENT 03
pdf-extract
skills: [ocr, extract]
trustB
FILTER · SSRF
DNS + IP gate
no RFC1918 · :443 only
blocked12
FILTER · SCHEMA
compat-check
structural diff
ok91%
FILTER · TRUST ≥ 10
score gate
hysteresis · 30d window
allow47
CORE · BRIDGE
A2A ↔ MCP
agentSkillToMCPTool()
A2AMCPJSON-RPC
bridge/s14
MCP · CLIENT 01
Claude Desktop
/api/mcp/tools
calls/h312
MCP · CLIENT 02
Cursor
tools/call
calls/h178
MCP · CLIENT 03
custom LLM
mcp-sdk
calls/h94
AUDIT LOG · /api/audit · synchronous writes
12:04:18bridgetranslator.v3 → claude-desktop · 142ms
12:04:17ssrfblocked 10.0.0.4 · rfc1918 · synchronous
12:04:16schemadiff: lang:string → language:'de'|'en'
12:04:14trustweb-scrape.io bronze → silver · 30d window
12:04:11mcptools/call · cursor · sql-runner · 87ms
12:04:08a2apdf-extract · skill: ocr · 312 tokens · 1.2s
12:04:05bridgeagentSkillToMCPTool · web-scrape.io → fetch_url
12:04:02ssrfblocked 169.254.169.254 · aws-metadata
12:03:58mcptools/list · claude-desktop · 47 tools · 12ms
12:04:18bridgetranslator.v3 → claude-desktop · 142ms
12:04:17ssrfblocked 10.0.0.4 · rfc1918 · synchronous
12:04:16schemadiff: lang:string → language:'de'|'en'
12:04:14trustweb-scrape.io bronze → silver · 30d window
12:04:11mcptools/call · cursor · sql-runner · 87ms
12:04:08a2apdf-extract · skill: ocr · 312 tokens · 1.2s
12:04:05bridgeagentSkillToMCPTool · web-scrape.io → fetch_url
12:04:02ssrfblocked 169.254.169.254 · aws-metadata
12:03:58mcptools/list · claude-desktop · 47 tools · 12ms
§ 04Decisions · trade-offs

Four deliberate choices.

Per decision: what was chosen, instead of what, and why.
D/01

MCP bridge instead of a custom protocol.

chosen
Agents speak A2A and are exposed at runtime as MCP tools via /api/mcp/tools
instead of
A custom AgoraHub protocol that clients would have to learn first
reason
Claude Desktop, Cursor, and the MCP SDK ecosystem already exist — making every agent in the registry automatically available as an MCP tool is 10× more valuable than the hundredth registry with its own REST schema. The bridge translates A2A skill → MCP tool definition and routes tool calls back through the A2A gateway. Adoption without friction.
D/02

Prisma + Postgres instead of a vector DB.

chosen
Hand-rolled 5-factor score: skill 30 · schema 25 · trust 20 · tags 15 · activity 10
instead of
pgvector / embeddings over agent descriptions
reason
Under 100 agents, embedding similarity is overkill and would add embedding costs plus re-indexing on every schema update. The hand-rolled score is transparent, debuggable, and the weights are commit-tunable. The upgrade path to pgvector stays open if the registry really grows into the thousands — but then calibrated against real usage data, not on a hunch.
D/03

Next.js proxy instead of middleware.

chosen
src/proxy.ts — CSP, HSTS, Permissions-Policy, CORS allowlist
instead of
middleware.ts on the edge runtime
reason
Next.js 16 deprecated middleware.ts, and the edge-runtime constraints (no Node API access) were already limiting for IP validation and HMAC verification. The new proxy hook runs as a real Node handler — one file, no build-target split, and inside the Docker image on Coolify it behaves identically to local. Less magic, more control.
D/04

SSRF quarantine synchronously into the AuditLog.

chosen
Synchronous write before return — DNS resolve → IP check → port-:443 gate → log
instead of
Fire-and-forget logging parallel to the request
reason
Agent endpoints are user-submitted URLs — a maliciously registered agent could query AWS metadata, scan internal services, attempt DNS rebinding. The quarantine count has to be deterministic: if an agent blocks 3 times, it's out. Async logging would have created race conditions where 3 parallel attempts count as only 1. Synchronous costs ~2ms extra and guarantees count integrity.
§ 05Highlights · interesting bits

Things that were not obvious.

Edge cases and details that only became clear while building.

Schema compatibility with a concrete diff

H/01
When agent A wants to connect to agent B, schema-compatibility.ts compares their JSON Schemas structurally — required fields, type compatibility, enum overlap. The user gets not just a yes/no, but a concrete diff: "Agent B expects language: 'de'|'en', your agent sends lang: string".

This prevents half of all failing task runs. A binary check ("incompatible") would have frustrated developers, because they'd have to open both JSON Schemas manually and compare them themselves. Diff output is 2 hours of work and saves every user 20 minutes — one of those asymmetries that turns a developer tool from functional into pleasant.

Trust auto-progression with hysteresis

H/02
Trust levels aren't linear. An agent rises faster (Bronze → Silver at 10 successful tasks) than it falls (only at >20% failure rate over 30 days).

Otherwise a single timeout on a foreign endpoint would instantly demote good agents — and that happens all the time when the agent runs against flaky third-party APIs. MCP tools are only exposed from trust score ≥ 10, so Claude users are protected from junk without me having to moderate manually. The threshold is the only human touchpoint in the entire trust system.

API keys as hash + prefix

H/03
At key creation the plaintext key is shown once (URL query newKey). In the DB only SHA-256 hash + 8-character prefixlive. Auth lookup: request arrives with key → extract prefix → index hit on keyPrefix → hash comparison.

No full-table scans (the prefix is indexed and rare enough to match <5 candidates), and a DB leak gives no attacker working credentials. Classic GitHub pattern, but surprisingly many registries still store keys plaintext or encrypted-but-decryptable today.

SSRF fallback across all IPs

H/04
DNS resolve can return multiple A/AAAA records — if just one falls into RFC1918, the whole domain is suspect. Conversely: if one IP is valid (public, not loopback), the request may go there even if other IPs in the DNS pool are RFC1918.

The scanner validates each IP individually and picks the first valid one for the call. DNS rebinding attacks (where the IP changes between resolve and connect) are blocked because the HTTP client pins to the validated IP, not to the hostname. A ten-line fix that closes an entire attack class.
§ 06Stack · in production

What's running.

Next.js 16 · App RouterTypeScript 5Prisma 6 · PostgresTailwind · shadcnA2A (Google)MCP (Anthropic)HMAC webhooksSHA-256 key hashingIP allowlist · SSRF gateCoolify · self-hostedRate limit · per IP + keyAudit log · synchronous
§ 07Reflection · takeaways

What I learned.

Project runs publicly. These are the things I'm taking with me.

Bridges beat protocols.

I could have defined my own agent protocol that's "better" than A2A and MCP. Reality: every client users already have speaks one of the two. Forcing a third protocol would have been an adoption wall. The bridge strategy takes the question "which protocol wins" off the table — both win, AgoraHub is the translator. That's more boring than designing your own protocol, but it's the reason the first Claude user was productive in minutes.

User-submitted URLs are an attack primitive.

Every registered agent supplies a URL my server is supposed to call. From an attacker's angle that's exactly the definition of SSRF. I put the security chain (DNS gate, IP validation, port enforcement, synchronous audit logs) in front of the first real integration — not after the first malicious agent tried to query AWS metadata. For tools that call user URLs, security engineering isn't a feature, it's the foundation. Whoever retrofits it as a feature retrofits too late.

◦ NEXT CASE · 09 / 11
Flashbuddy
← all projects