Back to blog
Jul 31, 2025
8 min read

How to Implement A2A Protocol on Your Blog: A Complete Guide

Learn how to implement Google's Agent-to-Agent (A2A) Protocol on your blog or website, enabling AI agents to discover and interact with your content programmatically

What is the A2A Protocol?

The Agent-to-Agent (A2A) Protocol is an open standard introduced by Google in April 2025 that enables AI agents to communicate and collaborate seamlessly. It’s designed to create an interoperable ecosystem where different AI systems can discover each other’s capabilities and work together effectively.

I recently implemented A2A Protocol on this blog to make my content accessible to AI agents. This guide will walk you through the entire process, from understanding the protocol to having a fully functional implementation that agents can discover and use.

Try It Before You Build It

Before diving into implementation, I encourage you to experience A2A in action. I’ve created a comprehensive Jupyter notebook that demonstrates A2A Protocol interaction.

The notebook allows you to:

  • Test against live A2A implementations (including this blog)
  • Understand the discovery process
  • See real request/response patterns
  • Benchmark performance
  • Validate your own implementation

You can run it locally or in Google Colab to experiment with A2A endpoints.

Core A2A Architecture

The A2A Protocol follows a client-server model with three main components:

  1. Discovery: Agents find your service via an agent card at /.well-known/agent.json
  2. Authentication: Secure access using API keys, OAuth 2.0, or OpenID Connect
  3. Communication: JSON-RPC 2.0 messages over HTTPS

Step 1: Create Your Agent Card

The agent card is the entry point for AI agents to discover your service. Create this file at /.well-known/agent.json:

{
  "name": "Colin McNamara's Blog",
  "description": "Tech blog focusing on AI, networking, and software development with A2A Protocol support",
  "version": "1.0.0",
  "protocolVersion": "0.1.0",
  "capabilities": {
    "jsonrpc": {
      "endpoint": "/api/a2a/service",
      "batchRequests": false,
      "extensions": []
    }
  },
  "serviceEndpoints": {
    "blog.list_posts": "/api/a2a/blog/list",
    "blog.get_post": "/api/a2a/blog/get",
    "blog.search_posts": "/api/a2a/blog/search",
    "blog.get_metadata": "/api/a2a/blog/metadata",
    "blog.get_author_info": "/api/a2a/blog/author"
  },
  "skills": [
    {
      "name": "blog",
      "description": "Access and search blog content",
      "methods": [
        {
          "name": "list_posts",
          "description": "List blog posts with pagination support",
          "parameters": {
            "limit": "Number of posts to return (default: 10)",
            "offset": "Number of posts to skip (default: 0)"
          }
        },
        {
          "name": "get_post",
          "description": "Get a specific blog post by ID",
          "parameters": {
            "id": "The post ID (slug)"
          }
        },
        {
          "name": "search_posts",
          "description": "Search blog posts by query",
          "parameters": {
            "query": "Search query string",
            "limit": "Maximum results to return"
          }
        }
      ]
    }
  ],
  "rateLimit": {
    "requestsPerMinute": 60,
    "requestsPerHour": 1000
  },
  "authenticationSchemes": []
}

Step 2: Implement the Service Endpoints

I recommend using a multi-endpoint architecture where each method has its own endpoint. This approach:

  • Avoids caching issues on edge networks
  • Provides better observability
  • Allows independent scaling
  • Simplifies debugging

List Posts Endpoint (/api/a2a/blog/list.ts)

import type { APIContext } from 'astro';
import { getCollection } from 'astro:content';

export async function POST({ request }: APIContext) {
  try {
    const { params = {}, id } = await request.json();
    const limit = params.limit || 10;
    const offset = params.offset || 0;
    
    // Get all posts from Astro content collection
    const allPosts = await getCollection('posts');
    
    // Sort by date (newest first) and paginate
    const sortedPosts = allPosts
      .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime())
      .slice(offset, offset + limit);
    
    // Format posts for A2A response
    const posts = sortedPosts.map(post => ({
      id: post.id,
      title: post.data.title,
      summary: post.data.description,
      date: post.data.date.toISOString(),
      url: `https://colinmcnamara.com/posts/${post.id}/`,
      author: {
        name: post.data.author,
        email: "[email protected]"
      },
      tags: post.data.tags || [],
      _a2a: {
        contentHash: "simplified-endpoint",
        lastModified: post.data.date.toISOString(),
        license: {
          type: "CC BY 4.0",
          url: "https://creativecommons.org/licenses/by/4.0/"
        }
      }
    }));
    
    return new Response(JSON.stringify({
      jsonrpc: "2.0",
      result: {
        posts,
        pagination: {
          total: allPosts.length,
          limit,
          offset,
          hasMore: offset + limit < allPosts.length
        }
      },
      id: id || 1
    }), {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'no-store',
        'X-A2A-Version': '0.1.0'
      }
    });
  } catch (error) {
    return new Response(JSON.stringify({
      jsonrpc: "2.0",
      error: {
        code: -32603,
        message: "Internal error",
        data: error instanceof Error ? error.message : "Unknown error"
      },
      id: id || 1
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

Search Posts Endpoint (/api/a2a/blog/search.ts)

export async function POST({ request }: APIContext) {
  try {
    const { params = {}, id } = await request.json();
    const query = params.query?.toLowerCase() || '';
    const limit = params.limit || 10;
    
    if (!query) {
      return new Response(JSON.stringify({
        jsonrpc: "2.0",
        error: {
          code: -32602,
          message: "Invalid params",
          data: "Missing required parameter: query"
        },
        id: id || 1
      }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    const allPosts = await getCollection('posts');
    
    // Search in title, description, and tags
    const matchingPosts = allPosts.filter(post => 
      post.data.title.toLowerCase().includes(query) ||
      post.data.description.toLowerCase().includes(query) ||
      post.data.tags?.some(tag => tag.toLowerCase().includes(query)) ||
      post.body.toLowerCase().includes(query)
    );
    
    // Sort by relevance (simple scoring based on title matches)
    const scoredPosts = matchingPosts.map(post => ({
      post,
      score: (
        (post.data.title.toLowerCase().includes(query) ? 3 : 0) +
        (post.data.description.toLowerCase().includes(query) ? 2 : 0) +
        (post.data.tags?.some(tag => tag.toLowerCase().includes(query)) ? 1 : 0)
      )
    }));
    
    const posts = scoredPosts
      .sort((a, b) => b.score - a.score || 
        new Date(b.post.data.date).getTime() - new Date(a.post.data.date).getTime())
      .slice(0, limit)
      .map(({ post }) => ({
        id: post.id,
        title: post.data.title,
        summary: post.data.description,
        date: post.data.date.toISOString(),
        url: `https://colinmcnamara.com/posts/${post.id}/`,
        tags: post.data.tags || [],
        _a2a: {
          relevanceScore: scoredPosts.find(s => s.post.id === post.id)?.score || 0
        }
      }));
    
    return new Response(JSON.stringify({
      jsonrpc: "2.0",
      result: { 
        posts,
        query,
        totalMatches: matchingPosts.length
      },
      id: id || 1
    }), {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'no-store'
      }
    });
  } catch (error) {
    return new Response(JSON.stringify({
      jsonrpc: "2.0",
      error: {
        code: -32603,
        message: "Internal error",
        data: error instanceof Error ? error.message : "Unknown error"
      },
      id: id || 1
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

Step 3: Add Discovery Headers

Help agents discover your A2A support by adding these headers to your homepage:

// middleware.ts
export async function onRequest(context, next) {
  const response = await next();
  
  // Add A2A discovery headers on homepage
  if (context.url.pathname === '/') {
    response.headers.set('X-A2A-Agent-Card', '/.well-known/agent.json');
    response.headers.set('Link', '</.well-known/agent.json>; rel="a2a-agent-card"');
  }
  
  return response;
}

Step 4: Test Your Implementation

Use the A2A Quick Start notebook to test your implementation:

import requests
import json

# Your domain
base_url = "https://yourdomain.com"

# 1. Test agent card discovery
agent_card_url = f"{base_url}/.well-known/agent.json"
response = requests.get(agent_card_url)
agent_card = response.json()
print(f"✅ Found A2A service: {agent_card['name']}")
print(f"   Protocol version: {agent_card['protocolVersion']}")

# 2. Test listing posts
list_endpoint = agent_card['serviceEndpoints']['blog.list_posts']
response = requests.post(
    f"{base_url}{list_endpoint}",
    json={
        "jsonrpc": "2.0",
        "method": "blog.list_posts",
        "params": {"limit": 5},
        "id": 1
    }
)
result = response.json()
print(f"\n✅ Listed {len(result['result']['posts'])} posts")

# 3. Test search
search_endpoint = agent_card['serviceEndpoints']['blog.search_posts']
response = requests.post(
    f"{base_url}{search_endpoint}",
    json={
        "jsonrpc": "2.0",
        "method": "blog.search_posts",
        "params": {"query": "AI", "limit": 3},
        "id": 2
    }
)
result = response.json()
print(f"\n✅ Found {len(result['result']['posts'])} posts matching 'AI'")

Implementation Best Practices

1. Error Handling

Always return proper JSON-RPC error responses:

// Standard error codes
const ERROR_CODES = {
  PARSE_ERROR: -32700,
  INVALID_REQUEST: -32600,
  METHOD_NOT_FOUND: -32601,
  INVALID_PARAMS: -32602,
  INTERNAL_ERROR: -32603
};

2. Input Validation

Use a schema validation library like Zod:

import { z } from 'zod';

const ListPostsSchema = z.object({
  limit: z.number().min(1).max(100).default(10),
  offset: z.number().min(0).default(0)
});

// In your endpoint
const params = ListPostsSchema.parse(request.params || {});

3. Rate Limiting

Implement rate limiting to prevent abuse:

const rateLimit = new Map();

export function checkRateLimit(clientId: string): boolean {
  const now = Date.now();
  const windowStart = now - 60000; // 1 minute window
  
  const requests = rateLimit.get(clientId) || [];
  const recentRequests = requests.filter(time => time > windowStart);
  
  if (recentRequests.length >= 60) {
    return false; // Rate limit exceeded
  }
  
  recentRequests.push(now);
  rateLimit.set(clientId, recentRequests);
  return true;
}

4. Caching Strategy

For static content, implement ETag-based caching:

import crypto from 'crypto';

function generateETag(content: any): string {
  return crypto
    .createHash('md5')
    .update(JSON.stringify(content))
    .digest('hex');
}

// In your response
const etag = generateETag(result);
response.headers.set('ETag', `"${etag}"`);

Testing with Live Endpoints

You can test your A2A client against this blog’s live endpoints:

Common Implementation Challenges

Edge Network Caching

If deploying to Vercel or similar platforms, be aware that edge caching can cause issues with POST requests. Solutions:

  1. Use multi-endpoint architecture (recommended)
  2. Disable caching with Cache-Control: no-store
  3. Add cache-busting parameters if needed

CORS Configuration

Enable CORS for browser-based agents:

response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type');

Content Freshness

Ensure agents get fresh content by:

  • Including lastModified timestamps
  • Implementing proper cache headers
  • Using ETags for conditional requests

Extending Your Implementation

Once basic functionality works, consider adding:

  1. Authentication: Implement API keys for premium content
  2. Webhooks: Notify agents of new content
  3. Batch Operations: Support multiple requests in one call
  4. Streaming: Use Server-Sent Events for real-time updates
  5. Custom Extensions: Add domain-specific methods

Join the A2A Ecosystem

By implementing A2A Protocol, you’re making your content accessible to a growing ecosystem of AI agents. This enables:

  • Automated content discovery
  • Cross-site content aggregation
  • AI-powered research tools
  • Agent-based workflows

Resources and References

Get Help

If you run into issues:

  1. Test against the live endpoints on this blog
  2. Use the notebook to debug your implementation
  3. Check the error responses for specific issues
  4. Validate your JSON-RPC formatting

Happy building! Welcome to the agent-enabled web. 🤖

Let's Build AI That Works

Ready to implement these ideas in your organization?