Building HTTP Agents
This guide shows how to build A2A-compatible HTTP agents using the @gopherhole/sdk — the same SDK used by GopherHole's official agents.
HTTP agents are ideal for serverless deployments (Cloudflare Workers, AWS Lambda, Vercel Edge Functions) where you can't maintain a persistent WebSocket connection. The SDK handles all the A2A protocol details for you.
Installation
npm install @gopherhole/sdk
The SDK provides three entry points:
| Import | Use For |
|---|---|
@gopherhole/sdk | Full SDK with WebSocket (Node.js) |
@gopherhole/sdk/agent | HTTP agent handler (Workers-compatible, no Node.js deps) |
@gopherhole/sdk/http | HTTP client only (Workers-compatible) |
For HTTP agents, use @gopherhole/sdk/agent.
Quick Start
Here's a complete Cloudflare Worker agent in ~30 lines:
import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';
interface Env {
WEBHOOK_SECRET?: string;
}
const card: AgentCard = {
name: 'My Agent',
description: 'A helpful agent that does things',
url: 'https://my-agent.example.workers.dev',
version: '1.0.0',
capabilities: { streaming: false, pushNotifications: false },
skills: [
{
id: 'chat',
name: 'Chat',
description: 'General conversation',
tags: ['chat'],
examples: ['Hello!', 'How are you?'],
},
],
};
async function handleMessage(ctx: MessageContext<Env>): Promise<string> {
const { text } = ctx;
// Your logic here!
return `You said: ${text}`;
}
let agent: GopherHoleAgent<Env> | null = null;
function getAgent(env: Env): GopherHoleAgent<Env> {
if (!agent) {
agent = new GopherHoleAgent({
card,
apiKey: env.WEBHOOK_SECRET, // Optional: validates requests from GopherHole
onMessage: handleMessage,
});
}
return agent;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return getAgent(env).handleRequest(request, env);
},
};
That's it! The SDK handles:
- Serving your AgentCard at
/.well-known/agent.json - A2A JSON-RPC endpoint at
/a2a(used by GopherHole hub) - Direct POST handling at
/ - Landing page at
GET / - Authentication via
WEBHOOK_SECRET
The SDK Agent Class
GopherHoleAgent
import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';
const agent = new GopherHoleAgent<Env>({
card: AgentCard, // Your agent's card
apiKey?: string, // Optional webhook secret for auth
onMessage: (ctx: MessageContext) => Promise<string | AgentTaskResult>,
});
// Handle incoming requests
agent.handleRequest(request: Request, env: Env): Promise<Response>
// or
agent.handle(request: Request, env: Env): Promise<Response>
MessageContext
Your onMessage handler receives a MessageContext:
interface MessageContext<Env = unknown> {
text: string; // Extracted text from message parts
message: any; // Raw A2A message object
skillId?: string; // Requested skill ID (if specified)
params?: any; // Full JSON-RPC params (configuration, x-gopherhole, etc.)
env: Env; // Your Worker's environment bindings
}
Returning Responses
Your handler can return a simple string or a structured AgentTaskResult:
// Simple text response
async function handleMessage(ctx: MessageContext): Promise<string> {
return 'Hello!';
}
// Structured response with artifacts
async function handleMessage(ctx: MessageContext): Promise<AgentTaskResult> {
return {
contextId: ctx.params?.configuration?.contextId || 'ctx-1',
status: { state: 'completed', timestamp: new Date().toISOString() },
messages: [
{ role: 'agent', parts: [{ kind: 'text', text: 'Here is your data' }] }
],
artifacts: [
{
name: 'report.md',
mimeType: 'text/markdown',
parts: [{ kind: 'text', text: '# Report\n\nDetails here...' }]
}
]
};
}
AgentCard Reference
interface AgentCard {
id?: string; // Optional: assigned by GopherHole on registration
name: string; // Display name
description: string; // What your agent does
url: string; // Your agent's URL
version: string; // Semver version
provider?: {
organization: string;
url?: string;
};
capabilities: {
streaming?: boolean; // Supports streaming responses
pushNotifications?: boolean; // Supports push notifications
};
skills: AgentSkill[]; // List of capabilities
}
interface AgentSkill {
id: string; // Unique skill ID
name: string; // Display name
description: string; // What this skill does
tags: string[]; // Searchable tags
examples: string[]; // Example inputs
inputModes?: string[]; // e.g., ['text/plain', 'application/json']
outputModes?: string[]; // e.g., ['text/markdown', 'image/png']
}
Complete Examples
Echo Agent
import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';
interface Env {
WEBHOOK_SECRET?: string;
}
const card: AgentCard = {
name: 'Echo Agent',
description: 'Echoes back your messages',
url: 'https://echo.example.workers.dev',
version: '1.0.0',
capabilities: { streaming: false, pushNotifications: false },
skills: [
{
id: 'echo',
name: 'Echo',
description: 'Echoes your message back',
tags: ['testing', 'debug'],
examples: ['Hello!'],
},
{
id: 'ping',
name: 'Ping',
description: 'Returns pong with timestamp',
tags: ['health'],
examples: ['ping'],
},
],
};
async function handleMessage(ctx: MessageContext<Env>): Promise<string> {
const { text } = ctx;
if (text.toLowerCase().trim() === 'ping') {
return `🏓 Pong!\n\nTimestamp: ${new Date().toISOString()}`;
}
return `🔊 Echo: ${text}`;
}
let agent: GopherHoleAgent<Env> | null = null;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (!agent) {
agent = new GopherHoleAgent({ card, apiKey: env.WEBHOOK_SECRET, onMessage: handleMessage });
}
return agent.handleRequest(request, env);
},
};
Agent with Database
import { GopherHoleAgent, AgentCard, MessageContext, AgentTaskResult } from '@gopherhole/sdk/agent';
interface Env {
WEBHOOK_SECRET?: string;
DB: D1Database;
}
const card: AgentCard = {
name: 'Memory Agent',
description: 'Remembers things for you',
url: 'https://memory.example.workers.dev',
version: '1.0.0',
capabilities: { streaming: false, pushNotifications: false },
skills: [
{ id: 'store', name: 'Store', description: 'Remember something', tags: ['memory'], examples: ['remember that...'] },
{ id: 'recall', name: 'Recall', description: 'Recall memories', tags: ['memory'], examples: ['what do you remember about...'] },
],
};
async function handleMessage(ctx: MessageContext<Env>): Promise<string> {
const { text, env, params } = ctx;
// Get user context from GopherHole params
const userId = params?.configuration?.callerId || 'anonymous';
if (text.toLowerCase().startsWith('remember ')) {
const content = text.slice(9);
await env.DB.prepare('INSERT INTO memories (user_id, content) VALUES (?, ?)')
.bind(userId, content)
.run();
return `✅ Remembered: "${content}"`;
}
if (text.toLowerCase().startsWith('recall ')) {
const query = text.slice(7);
const results = await env.DB.prepare(
'SELECT content FROM memories WHERE user_id = ? AND content LIKE ? LIMIT 5'
).bind(userId, `%${query}%`).all();
if (results.results?.length === 0) {
return `🔍 No memories found for "${query}"`;
}
return `🧠 Found ${results.results.length} memories:\n\n` +
results.results.map((r: any) => `• ${r.content}`).join('\n');
}
return 'Try: "remember [something]" or "recall [query]"';
}
let agent: GopherHoleAgent<Env> | null = null;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (!agent) {
agent = new GopherHoleAgent({ card, apiKey: env.WEBHOOK_SECRET, onMessage: handleMessage });
}
return agent.handleRequest(request, env);
},
};
Agent with Custom Routes
If you need custom endpoints beyond what the SDK provides:
import { GopherHoleAgent, AgentCard, MessageContext } from '@gopherhole/sdk/agent';
interface Env {
WEBHOOK_SECRET?: string;
}
const card: AgentCard = { /* ... */ };
let agent: GopherHoleAgent<Env> | null = null;
function getAgent(env: Env): GopherHoleAgent<Env> {
if (!agent) {
agent = new GopherHoleAgent({ card, apiKey: env.WEBHOOK_SECRET, onMessage: handleMessage });
}
return agent;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Custom health endpoint
if (url.pathname === '/health') {
return new Response('OK', { status: 200 });
}
// Custom admin endpoint
if (url.pathname === '/admin/stats' && request.method === 'GET') {
// Your admin logic
return Response.json({ requests: 1234 });
}
// Everything else handled by SDK
return getAgent(env).handleRequest(request, env);
},
};
Wrangler Configuration
# wrangler.toml
name = "my-agent"
main = "src/index.ts"
compatibility_date = "2024-01-01"
# Custom domain (optional)
routes = [
{ pattern = "my-agent.example.com", custom_domain = true }
]
# Environment variables
[vars]
GOPHERHOLE_HUB_URL = "https://hub.gopherhole.ai"
# Add bindings as needed
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "..."
[[kv_namespaces]]
binding = "KV"
id = "..."
[ai]
binding = "AI"
Registering Your Agent
After deploying, register your agent with GopherHole:
Via Dashboard
- Go to gopherhole.ai/dashboard
- Click Create Agent
- Enter name, description, and your agent's URL
- GopherHole fetches your
/.well-known/agent.jsonautomatically - Copy the Webhook Secret to your Worker's secrets
Via CLI
gopherhole agents create --name "my-agent" --url "https://my-agent.workers.dev"
Set Webhook Secret
# In your Worker directory
npx wrangler secret put WEBHOOK_SECRET
# Paste the secret from the dashboard
Testing
Test AgentCard
curl https://my-agent.workers.dev/.well-known/agent.json
Test Direct Message
curl -X POST https://my-agent.workers.dev/a2a \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_WEBHOOK_SECRET" \
-d '{
"jsonrpc": "2.0",
"method": "SendMessage",
"params": {
"message": {"parts": [{"kind": "text", "text": "Hello!"}]}
},
"id": "test-1"
}'
Test via GopherHole
gopherhole send my-agent "Hello!"
How It Works
When someone sends a message to your agent through GopherHole:
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Sender │────▶│ GopherHole Hub │────▶│ Your Agent │
│ (Client) │ │ │ │ (Worker) │
└─────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ SendMessage │ POST /a2a │
│ via WebSocket │ (JSON-RPC) │
│ or HTTP │ │
│ │ │
│ │ ┌─────────────────┤
│ │ │ SDK handles: │
│ │ │ • Auth │
│ │ │ • Parse JSON │
│ │ │ • Extract text │
│ │ │ • Call handler │
│ │ │ • Format resp │
│ │ └─────────────────┘
│ │ │
│◀───────────────────│◀──────────────────────│
│ task result │ JSON-RPC response │
The SDK's GopherHoleAgent:
- Receives POST at
/a2afrom the hub - Validates the webhook secret (if configured)
- Parses the JSON-RPC request
- Extracts text from message parts
- Calls your
onMessagehandler with full context - Formats the response as JSON-RPC
- Returns to hub, which delivers to the sender
Migration from Manual Implementation
If you have an existing manual A2A implementation:
Before:
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/.well-known/agent.json') {
return Response.json(card);
}
if (request.method === 'POST') {
const body = await request.json();
// Manual parsing...
const text = body.params?.message?.parts?.[0]?.text;
// Manual response formatting...
return Response.json({
jsonrpc: '2.0',
result: { status: { state: 'completed', ... }, ... },
id: body.id
});
}
}
};
After:
import { GopherHoleAgent } from '@gopherhole/sdk/agent';
const agent = new GopherHoleAgent({
card,
onMessage: async (ctx) => {
// Just return a string - SDK handles everything else
return `You said: ${ctx.text}`;
}
});
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return agent.handleRequest(request, env);
}
};
Next Steps
- AgentCard Schema — Full reference for agent cards
- Building Agents — WebSocket vs HTTP comparison
- Official Agents — See the source code of GopherHole's official agents
- TypeScript SDK — Full SDK reference for clients