Skip to content

A2A Protocol Integration: Comprehensive Developer Guide

This guide documents the Agent-to-Agent (A2A) protocol implementation in the MCP Gateway Registry. Rather than a specification, this is a practical guide to understanding how the system works today, how agents register themselves, how discovery works, and how access control is enforced across the entire stack.

Table of Contents

  1. What We Built
  2. The Big Picture: Request Flow
  3. How Requests Get Authenticated
  4. The Agent Card: Machine-Readable Profile
  5. CRUD Operations: Agents Registering Themselves
  6. Discovery: How Agents Find Other Agents
  7. Access Control: Three-Tier Permission System
  8. The Code: Where Everything Lives

What We Built

The MCP Gateway Registry now supports Agent-to-Agent (A2A) communication through a registry-only design. This means:

  • Agents can register their capabilities and metadata with the registry
  • Agents can discover other agents they have permission to access
  • Agents communicate directly with each other using URLs returned by the registry
  • The registry itself is NOT involved in agent-to-agent communication

This is fundamentally different from how the MCP Gateway works. The gateway proxies MCP server requests, but for A2A agents, it simply acts as a discovery and validation service. Once agents find each other through the registry, they communicate peer-to-peer with no registry intermediation.

Why This Matters

Building an autonomous agent ecosystem requires that agents be able to find each other without a central orchestrator. This architecture enables:

  • Decentralized coordination: Agents discover and contact each other directly
  • Scalability: No bottleneck at the registry for agent-to-agent communication
  • Security: Each agent maintains its own authentication and authorization
  • Autonomy: Agents can operate independently after discovery

The Big Picture: Request Flow

When an agent wants to register or discover other agents, here's the complete journey of a request:

Agent (AI Code)
M2M Token (from Keycloak Service Account)
[Port 80 - Nginx Reverse Proxy]
[Auth Validation]
Nginx calls auth-server:/validate
Returns groups and scopes
[FastAPI Routes]
/api/agents/register
/api/agents
/api/agents/{path}
/api/agents/discover/semantic
etc.
[Authorization Enforcement]
Check if user has permission for requested action
Filter results based on access control
[Business Logic]
Registry Services (agent_service.py)
File-based persistence (agent_state.json)
FAISS semantic search
[Response]
Agent cards, discovery results, or error

The Key Difference from MCP

MCP Request Flow:
Agent → Nginx → Auth → FastAPI → Gateway Proxy → MCP Server → Agent

A2A Request Flow:
Agent → Nginx → Auth → FastAPI → Registry Service
         ↓ Returns: Agent Card + Direct URL
Agent ← [Agents now communicate directly, registry is done] → Other Agent

How Requests Get Authenticated

Every request to the A2A agent API must include a valid JWT token from Keycloak. Here's the authentication journey:

1. Token Generation (M2M Service Account)

The mcp-gateway-m2m Keycloak service account generates tokens that are used for all A2A operations:

# Service Account Details
Client ID: mcp-gateway-m2m
Service User: service-account-mcp-gateway-m2m
Token File: .oauth-tokens/ingress.json (generated by credentials-provider/generate_creds.sh)
TTL: 5 minutes (expiration is critical)

When a token is generated, it contains:

{
  "exp": 1761942660,
  "iat": 1761942360,
  "iss": "http://localhost:8080/realms/mcp-gateway",
  "sub": "user-id-uuid",
  "typ": "Bearer",
  "azp": "mcp-gateway-m2m",
  "client_id": "mcp-gateway-m2m",
  "preferred_username": "service-account-mcp-gateway-m2m",
  "groups": [
    "mcp-servers-unrestricted",
    "a2a-agent-admin"
  ],
  "scope": "profile email mcp-servers-unrestricted/read mcp-servers-unrestricted/execute a2a-agent-admin"
}

The critical fields are:

  • groups: List of Keycloak groups the account belongs to (controls what agents it can access)
  • exp: Expiration timestamp (checked for token validity)

2. Nginx Reverse Proxy Intercepts Request

Nginx runs on port 80 and intercepts all requests to /api/ paths. It extracts the JWT and calls the auth-server to validate it:

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost/api/agents/register

Nginx intercepts → Calls auth-server:/validate

The auth-server validates the JWT and maps the groups in the token to internal scope names.

3. Auth-Server Validates and Maps Groups

The auth-server decodes the JWT, extracts the groups, and looks them up in auth_server/scopes.yml:

# Example from scopes.yml
mcp-registry-admin:
- mcp-registry-admin
- mcp-servers-unrestricted/read
- mcp-servers-unrestricted/execute

a2a-agent-admin:
- a2a-agent-admin  # Implicit (service accounts have special mapping)

The auth-server returns:

{
  "groups": ["mcp-servers-unrestricted", "a2a-agent-admin"],
  "scopes": ["mcp-servers-unrestricted/read", "mcp-servers-unrestricted/execute", "a2a-agent-admin"],
  "username": "service-account-mcp-gateway-m2m"
}

4. Nginx Forwards to FastAPI with Scopes

Nginx adds a header with the scopes and forwards the request:

X-Scopes: a2a-agent-admin, mcp-servers-unrestricted/read, mcp-servers-unrestricted/execute
Authorization: Bearer $TOKEN

5. FastAPI Endpoint Checks Permissions

The FastAPI endpoint reads the scopes and enforces permissions:

@router.post("/agents/register")
async def register_agent(
    request: Request,
    agent_card: AgentCard,
    user_context: dict = Depends(enhanced_auth)
):
    # Check if user has a2a-agent-admin scope
    if "a2a-agent-admin" not in user_context.get("scopes", []):
        raise HTTPException(status_code=403, detail="Not authorized")

    # Proceed with registration

6. Agent State Persisted

The registered agent is saved to registry/agents/agent_state.json in the format:

{
  "agents": {
    "/code-reviewer": {
      "name": "Code Reviewer Agent",
      "path": "/code-reviewer",
      "url": "https://agent.example.com/code-reviewer",
      "protocol_version": "1.0",
      "is_enabled": true,
      "registered_at": "2025-11-09T10:30:00Z",
      "registered_by": "service-account-mcp-gateway-m2m",
      "visibility": "public"
    }
  }
}

Token Validation in CLI

The CLI (cli/agent_mgmt.py) validates tokens before making requests. It checks:

  1. Token exists: .oauth-tokens/ingress.json file is present
  2. Token is not expired: Decodes JWT payload, checks exp claim against current timestamp
  3. Token has correct groups: Verifies groups claim includes required groups

This ensures requests fail fast with clear messages if credentials are stale (tokens expire in 5 minutes).


The Agent Card: Machine-Readable Profile

An agent card is a JSON document that describes what an agent does, how to reach it, and what capabilities it offers. The registry stores these cards and returns them during discovery.

Complete Agent Card Structure

{
  "protocol_version": "1.0",
  "name": "Code Reviewer Agent",
  "description": "Analyzes Python and JavaScript code for bugs, style issues, and security vulnerabilities",
  "url": "https://agents.example.com/code-reviewer",
  "version": "2.1.0",
  "provider": "Acme Corp",

  "skills": [
    {
      "id": "review-python-code",
      "name": "Review Python Code",
      "description": "Performs static analysis on Python source code",
      "parameters": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Python source code to review"
          },
          "strict_mode": {
            "type": "boolean",
            "default": false,
            "description": "Enable strict analysis rules"
          }
        },
        "required": ["code"]
      },
      "tags": ["python", "code-review", "security"]
    },
    {
      "id": "review-javascript-code",
      "name": "Review JavaScript Code",
      "description": "Performs static analysis on JavaScript source code",
      "parameters": {
        "type": "object",
        "properties": {
          "code": { "type": "string" },
          "strict_mode": { "type": "boolean", "default": false }
        },
        "required": ["code"]
      },
      "tags": ["javascript", "code-review", "security"]
    }
  ],

  "security_schemes": {
    "bearer": {
      "type": "http",
      "scheme": "bearer",
      "bearer_format": "JWT"
    }
  },
  "security": [{"bearer": []}],

  "streaming": false,
  "path": "/code-reviewer",
  "tags": [
    "code-review",
    "security",
    "static-analysis"
  ],
  "is_enabled": true,
  "num_stars": 0,
  "license": "MIT",
  "visibility": "public",
  "allowed_groups": [],
  "trust_level": "community",
  "registered_at": "2025-11-09T10:30:00Z",
  "registered_by": "service-account-mcp-gateway-m2m",
  "updated_at": "2025-11-09T10:35:00Z"
}

Field Descriptions

Core A2A Fields (required):

  • protocol_version: A2A protocol version (currently "1.0")
  • name: Human-readable agent name
  • description: What the agent does
  • url: Direct URL to reach the agent (used by other agents after discovery)

Capabilities:

  • skills: List of capabilities the agent offers. Each skill has:
  • id: Unique identifier within the agent
  • name: Human-readable name
  • description: What the skill does
  • parameters: JSON Schema defining input parameters
  • tags: Categorization for discovery

Security:

  • security_schemes: How to authenticate with the agent (bearer, OAuth2, etc.)
  • security: Which schemes are required
  • trust_level: Verification status (unverified, community, verified, trusted)

Registry Metadata:

  • path: Registry path (like /code-reviewer)
  • visibility: Who can see it (public, private, group-restricted)
  • is_enabled: Whether it's active in the registry
  • registered_at: When it was registered
  • registered_by: Which service account registered it

Why the Agent Card Matters

The agent card is the contract between agents. When Agent B discovers Agent A, it gets the agent card which tells it:

  • How to reach Agent A (url)
  • What Agent A can do (skills)
  • How to authenticate with Agent A (security_schemes)
  • Whether it should trust Agent A (trust_level)

CRUD Operations: Agents Registering Themselves

All CRUD (Create, Read, Update, Delete) operations happen through REST API endpoints. Every operation requires authentication and goes through the permission check.

Creating: POST /api/agents/register

An agent registers itself by POSTing a card to the registry:

curl -X POST http://localhost/api/agents/register \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @agent-card.json

What Happens:

  1. Nginx receives request, validates JWT, calls auth-server
  2. Auth-server maps groups to scopes, returns a2a-agent-admin scope
  3. Nginx forwards request with X-Scopes: a2a-agent-admin header
  4. FastAPI endpoint checks if scopes include a2a-agent-admin
  5. agent_routes.py validates the agent card using agent_validator.py:
  6. Schema validation (Pydantic)
  7. Unique path check
  8. Skills have unique IDs
  9. Security schemes are properly configured
  10. agent_service.py saves to agent_state.json
  11. FAISS service indexes the agent for semantic search
  12. Response: 201 Created with registered agent info

Success Response:

{
  "message": "Agent registered successfully",
  "agent": {
    "name": "Code Reviewer Agent",
    "path": "/code-reviewer",
    "url": "https://agents.example.com/code-reviewer",
    "num_skills": 2,
    "registered_at": "2025-11-09T10:30:00Z",
    "is_enabled": false
  }
}

Error Responses:

  • 400 Bad Request: Invalid agent card format
  • 409 Conflict: Agent path already exists
  • 403 Forbidden: User lacks a2a-agent-admin scope
  • 422 Unprocessable Entity: Validation failed

Reading: GET /api/agents/{path} and GET /api/agents

Get Single Agent:

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost/api/agents/code-reviewer

Returns the complete agent card (if user has permission).

List All Agents:

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost/api/agents

What Happens:

  1. Auth checks what groups the user belongs to
  2. Loads all agents from agent_state.json
  3. Filters by access control: Only returns agents the user is authorized to see
  4. Returns list with summaries

Example: If user is in registry-users-lob1 group, they only see agents in their scope (/code-reviewer, /test-automation).

Updating: PUT /api/agents/{path}

An agent can update its own card:

curl -X PUT http://localhost/api/agents/code-reviewer \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @updated-card.json

What Happens:

  1. Check permissions: User must have modify_agent scope for this path
  2. Validate new card: Same validation as registration
  3. Update in storage: Modify agent_state.json
  4. Re-index in FAISS: Update semantic search index
  5. Update timestamp: Set updated_at to current time
  6. Return: Updated agent card

Deleting: DELETE /api/agents/{path}

Remove an agent from the registry:

curl -X DELETE http://localhost/api/agents/code-reviewer \
  -H "Authorization: Bearer $TOKEN"

What Happens:

  1. Check permissions: User must have delete_agent scope
  2. Remove from storage: Delete from agent_state.json
  3. Remove from FAISS: Delete from semantic search index
  4. Return: 204 No Content or success message

Toggling: POST /api/agents/{path}/toggle

Enable or disable an agent without deleting it:

curl -X POST http://localhost/api/agents/code-reviewer/toggle?enabled=true \
  -H "Authorization: Bearer $TOKEN"

What Happens:

  1. Check permissions: User must have modify permissions
  2. Toggle state: Set is_enabled to true or false
  3. Update FAISS: Enabled status affects search results
  4. Return: Updated agent info

Discovery: How Agents Find Other Agents

Once agents are registered, other agents can discover them through two mechanisms: semantic search and direct queries.

Semantic Search: POST /api/agents/discover/semantic

An agent asks a natural language question to find other agents:

curl -X POST http://localhost/api/agents/discover/semantic \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "I need an agent that can review Python code for security vulnerabilities",
    "max_results": 5,
    "entity_types": ["a2a_agent"]
  }'

How It Works:

  1. Query Embedding: The natural language query is converted to a vector using an embedding model
  2. FAISS Search: The vector is compared against all agent cards stored in the FAISS index
  3. Ranking: Results ranked by similarity score
  4. Filtering: Only agents visible to this user (based on groups and visibility)
  5. Return: Agents with relevance_score

Response:

{
  "entities": [
    {
      "entity_type": "a2a_agent",
      "name": "Code Reviewer Agent",
      "path": "/code-reviewer",
      "description": "Analyzes Python and JavaScript code...",
      "url": "https://agents.example.com/code-reviewer",
      "relevance_score": 0.92,
      "skills": ["review-python-code", "review-javascript-code"],
      "trust_level": "community"
    }
  ],
  "query": "I need an agent that can review Python code..."
}

How FAISS Indexing Works

When an agent is registered, the registry creates an embedding for it:

# From agent_service.py
def _get_agent_text_for_embedding(agent_card):
    """Prepare agent card for semantic search"""
    name = agent_card["name"]
    description = agent_card["description"]

    # Extract skill information
    skills_text = "\n".join([
        f"{s['name']}: {s['description']}"
        for s in agent_card.get("skills", [])
    ])

    # Combine all searchable text
    text = f"""
    Name: {name}
    Description: {description}
    Skills: {skills_text}
    Tags: {', '.join(agent_card.get('tags', []))}
    """

    return text.strip()

# This text is embedded and stored in FAISS
# When someone searches, their query is embedded and compared

The FAISS index maintains metadata about each entity:

{
  "id": 42,
  "entity_type": "a2a_agent",
  "path": "/code-reviewer",
  "text_for_embedding": "Name: Code Reviewer...",
  "full_entity_info": { /* complete agent card */ },
  "is_enabled": true
}

Direct API Queries

For more precise queries, agents can also search using filters:

curl -X POST http://localhost/api/agents/discover \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "skills": ["review-python-code"],
    "tags": ["security", "python"],
    "max_results": 10
  }'

Access Control: Three-Tier Permission System

Access control is enforced at three levels: UI scopes, group mappings, and individual agent permissions. All three work together to determine what an authenticated user can do.

Tier 1: UI-Scopes (High-Level Actions)

The UI-Scopes section in auth_server/scopes.yml defines what high-level actions each group can perform:

UI-Scopes:
  mcp-registry-admin:
    list_agents: [all]
    get_agent: [all]
    publish_agent: [all]
    modify_agent: [all]
    delete_agent: [all]

  registry-users-lob1:
    list_agents:
    - /code-reviewer
    - /test-automation
    get_agent:
    - /code-reviewer
    - /test-automation
    publish_agent:
    - /code-reviewer
    - /test-automation
    modify_agent:
    - /code-reviewer
    - /test-automation
    delete_agent:
    - /code-reviewer
    - /test-automation

This says:

  • mcp-registry-admin can list ALL agents
  • registry-users-lob1 can ONLY list /code-reviewer and /test-automation

Tier 2: Group Mappings (Keycloak to Internal Scopes)

The group_mappings section maps Keycloak groups to internal scope names:

group_mappings:
  mcp-registry-admin:
  - mcp-registry-admin
  - mcp-servers-unrestricted/read
  - mcp-servers-unrestricted/execute

  registry-users-lob1:
  - registry-users-lob1

When a user authenticates with Keycloak, their JWT includes groups. The auth-server uses this mapping to determine which internal scopes apply.

Tier 3: Individual Group Scopes (Detailed Permissions)

The bottom of scopes.yml defines detailed permissions for each group:

registry-users-lob1:
- server: currenttime
  methods:
  - initialize
  - tools/list
  - tools/call
- agents:
    actions:
    - action: list_agents
      resources:
      - /code-reviewer
      - /test-automation
    - action: get_agent
      resources:
      - /code-reviewer
      - /test-automation
    - action: publish_agent
      resources:
      - /code-reviewer
      - /test-automation
    - action: modify_agent
      resources:
      - /code-reviewer
      - /test-automation
    - action: delete_agent
      resources:
      - /code-reviewer
      - /test-automation

This defines:

  • Which MCP servers the group can access (currenttime, mcpgw)
  • Which agent actions are allowed (list, get, publish, modify, delete)
  • Which agent paths apply (only /code-reviewer and /test-automation)

How Permission Checking Works in Code

When a request comes in to list agents:

@router.get("/agents")
async def list_agents(
    request: Request,
    user_context: dict = Depends(enhanced_auth)
):
    # user_context contains:
    # {
    #   "username": "service-account-mcp-gateway-m2m",
    #   "groups": ["mcp-registry-admin"],
    #   "scopes": ["mcp-registry-admin", "mcp-servers-unrestricted/read", ...]
    # }

    # Load all agents
    agents = agent_service.load_agents()

    # Filter based on scopes
    accessible_agents = _filter_agents_by_access(
        agents,
        user_context
    )

    return {"agents": accessible_agents}

def _filter_agents_by_access(agents, user_context):
    """Filter agents based on user's access permissions"""

    groups = user_context.get("groups", [])
    scopes = user_context.get("scopes", [])

    # Admin can see all
    if "mcp-registry-admin" in groups:
        return agents

    # LOB1 can only see LOB1 agents
    if "registry-users-lob1" in groups:
        return [a for a in agents if a["path"] in [
            "/code-reviewer",
            "/test-automation"
        ]]

    # LOB2 can only see LOB2 agents
    if "registry-users-lob2" in groups:
        return [a for a in agents if a["path"] in [
            "/data-analysis",
            "/security-analyzer"
        ]]

    # Unknown group sees nothing
    return []

Agent Visibility Levels

Beyond group-based access, agents also have visibility settings:

  • public: Visible to all authenticated users
  • private: Only visible to owner and admins
  • group-restricted: Only visible to specific groups

The filtering considers both the user's groups and the agent's visibility setting.


The Code: Where Everything Lives

This section maps the implementation to actual files and shows how the pieces fit together.

API Routes (registry/api/agent_routes.py)

This file defines 8 REST API endpoints:

@router.post("/agents/register")
async def register_agent(request: Request, agent_card: AgentCard):
    """Register a new agent (requires a2a-agent-admin scope)"""
    # 1. Check user has a2a-agent-admin scope
    # 2. Validate agent card using agent_validator
    # 3. Check path is unique
    # 4. Save to agent_state.json
    # 5. Index in FAISS
    # 6. Return 201 Created

@router.get("/agents")
async def list_agents(request: Request):
    """List agents (filtered by user permissions)"""
    # 1. Load all agents
    # 2. Filter by user's groups and visibility
    # 3. Return summary list

@router.get("/agents/{path}")
async def get_agent(request: Request, path: str):
    """Get complete agent card by path"""
    # 1. Check user has permission for this agent
    # 2. Load from agent_state.json
    # 3. Return full agent card

@router.put("/agents/{path}")
async def update_agent(request: Request, path: str, agent_card: AgentCard):
    """Update agent card (requires modify_agent scope for this path)"""
    # 1. Check user has modify_agent scope
    # 2. Validate new card
    # 3. Update agent_state.json
    # 4. Re-index in FAISS
    # 5. Return updated card

@router.delete("/agents/{path}")
async def delete_agent(request: Request, path: str):
    """Delete agent (requires delete_agent scope for this path)"""
    # 1. Check user has delete_agent scope
    # 2. Remove from agent_state.json
    # 3. Remove from FAISS index
    # 4. Return 204 No Content

@router.post("/agents/{path}/toggle")
async def toggle_agent(request: Request, path: str, enabled: bool):
    """Enable or disable agent"""
    # 1. Check user has modify_agent scope
    # 2. Update is_enabled flag
    # 3. Return updated agent info

@router.post("/agents/discover/semantic")
async def discover_agents_semantic(request: Request, query: DiscoveryQuery):
    """Semantic search for agents"""
    # 1. Embed the natural language query
    # 2. Search FAISS index
    # 3. Filter results by user permissions
    # 4. Return ranked results

Business Logic (registry/services/agent_service.py)

This file handles all agent operations:

class AgentService:
    """CRUD operations for agents"""

    def load_agents_and_state(self) -> dict:
        """Load all agents from agent_state.json"""
        # Returns: {"agents": {"/code-reviewer": {...}, ...}}

    def register_agent(self, agent_card: AgentCard) -> AgentCard:
        """Register a new agent"""
        # 1. Validate path is unique
        # 2. Generate registered_at timestamp
        # 3. Add to agent_state.json
        # 4. Return registered card

    def get_agent(self, path: str) -> AgentCard:
        """Get agent by path"""
        # Returns agent card or raises HTTPException(404)

    def update_agent(self, path: str, card: AgentCard) -> AgentCard:
        """Update existing agent"""
        # 1. Load current agent
        # 2. Merge updates
        # 3. Update agent_state.json
        # 4. Return updated card

    def delete_agent(self, path: str) -> None:
        """Delete agent by path"""
        # Remove from agent_state.json

    def toggle_agent(self, path: str, enabled: bool) -> AgentCard:
        """Enable or disable agent"""
        # Update is_enabled flag in agent_state.json

    def list_agents(self) -> List[AgentInfo]:
        """Get all agents as summaries"""
        # Returns list of simplified agent info

Data Models (registry/schemas/agent_models.py)

Pydantic models for validation:

class SecurityScheme(BaseModel):
    """How to authenticate with an agent"""
    type: str  # "apiKey", "http", "oauth2", "openIdConnect"
    scheme: Optional[str] = None
    in_: Optional[str] = None
    name: Optional[str] = None

class Skill(BaseModel):
    """A capability an agent offers"""
    id: str
    name: str
    description: str
    parameters: Optional[Dict[str, Any]] = None
    tags: List[str] = []

class AgentCard(BaseModel):
    """Complete agent profile"""
    protocol_version: str
    name: str
    description: str
    url: str  # Direct URL for peer-to-peer communication

    skills: List[Skill] = []
    security_schemes: Dict[str, SecurityScheme] = {}
    security: Optional[List[Dict[str, List[str]]]] = None

    path: str  # Registry path: /agents/code-reviewer
    visibility: str = "public"
    is_enabled: bool = False
    trust_level: str = "unverified"

    registered_at: Optional[datetime] = None
    registered_by: Optional[str] = None
    updated_at: Optional[datetime] = None

Validation (registry/utils/agent_validator.py)

Ensures agent cards are valid:

class AgentValidator:
    """Validate agent cards"""

    async def validate_agent_card(
        self,
        card: AgentCard,
        verify_endpoint: bool = True
    ) -> ValidationResult:
        """
        Validate agent card:
        - Schema validation (Pydantic)
        - Unique skill IDs
        - Valid security schemes
        - Endpoint reachability (optional)
        """

Storage (registry/agents/agent_state.json)

Central file tracking all registered agents:

{
  "agents": {
    "/code-reviewer": {
      "name": "Code Reviewer Agent",
      "path": "/code-reviewer",
      "url": "https://agents.example.com/code-reviewer",
      "protocol_version": "1.0",
      "is_enabled": true,
      "registered_at": "2025-11-09T10:30:00Z",
      "registered_by": "service-account-mcp-gateway-m2m"
    },
    "/test-automation": {
      "name": "Test Automation Agent",
      "path": "/test-automation",
      "url": "https://agents.example.com/test-automation",
      "protocol_version": "1.0",
      "is_enabled": true,
      "registered_at": "2025-11-09T10:31:00Z",
      "registered_by": "service-account-mcp-gateway-m2m"
    }
  }
}

FAISS Search Integration (registry/search/service.py)

The FAISS service indexes both MCP servers and agents:

class FaissService:
    """Semantic search for MCP servers and A2A agents"""

    async def add_or_update_entity(
        self,
        entity_path: str,
        entity_info: Dict[str, Any],
        entity_type: str  # "mcp_server" or "a2a_agent"
    ):
        """Add or update entity in FAISS index"""

        # Generate text for embedding
        if entity_type == "a2a_agent":
            text = self._get_agent_text_for_embedding(entity_info)
        else:
            text = self._get_server_text_for_embedding(entity_info)

        # Create embedding and add to index
        embedding = self.embedding_model.embed(text)
        self.faiss_index.add(embedding)

        # Store metadata
        metadata = {
            "entity_type": entity_type,
            "path": entity_path,
            "text_for_embedding": text,
            "full_entity_info": entity_info
        }

Authentication (Keycloak + Auth Server)

The M2M service account mcp-gateway-m2m has:

# Auto-assigned Keycloak groups (from keycloak/setup/init-keycloak.sh):
- mcp-servers-unrestricted  # Full MCP server access
- a2a-agent-admin           # Full agent management

# Mapped scopes (from auth_server/scopes.yml):
- mcp-servers-unrestricted/read
- mcp-servers-unrestricted/execute
- a2a-agent-admin

The token is generated every 5 minutes and stored in .oauth-tokens/ingress.json.


Putting It All Together: Complete Request Example

Here's what happens when an agent registers itself:

1. Agent Prepares Card

{
  "name": "Code Reviewer Agent",
  "description": "Reviews Python code",
  "url": "https://agents.example.com/code-reviewer",
  "path": "/code-reviewer",
  "protocol_version": "1.0",
  "skills": [
    {
      "id": "review-python",
      "name": "Review Python Code",
      "description": "Analyzes Python source code",
      "parameters": {"type": "object", "properties": {"code": {"type": "string"}}},
      "tags": ["python", "review"]
    }
  ],
  "security_schemes": {
    "bearer": {"type": "http", "scheme": "bearer", "bearer_format": "JWT"}
  },
  "security": [{"bearer": []}],
  "tags": "code-review,security"
}

2. Agent Gets JWT Token

$ ./credentials-provider/generate_creds.sh
# Generates .oauth-tokens/ingress.json with 5-minute TTL

3. Agent POSTs to Registry

curl -X POST http://localhost/api/agents/register \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @agent-card.json

4. Nginx Intercepts

  • Extracts JWT from Authorization header
  • Calls auth-server:/validate with the token
  • Auth-server decodes JWT and returns scopes

5. Auth-Server Returns

{
  "username": "service-account-mcp-gateway-m2m",
  "groups": ["mcp-servers-unrestricted", "a2a-agent-admin"],
  "scopes": ["mcp-servers-unrestricted/read", "mcp-servers-unrestricted/execute", "a2a-agent-admin"]
}

6. Nginx Forwards to FastAPI

POST /api/agents/register HTTP/1.1
Authorization: Bearer $TOKEN
X-Scopes: mcp-servers-unrestricted/read,mcp-servers-unrestricted/execute,a2a-agent-admin

7. FastAPI Endpoint Executes

  • Checks a2a-agent-admin in scopes ✓
  • Validates agent card with Pydantic ✓
  • Checks path /code-reviewer doesn't exist ✓
  • Calls agent_service.register_agent()

8. Agent Service Saves

  • Loads current agent_state.json
  • Adds /code-reviewer entry
  • Saves back to disk
  • Returns registered agent

9. FAISS Indexing

  • Generates embedding text from agent card
  • Converts to vector using embedding model
  • Adds to FAISS index with metadata

10. Response to Agent

{
  "message": "Agent registered successfully",
  "agent": {
    "name": "Code Reviewer Agent",
    "path": "/code-reviewer",
    "url": "https://agents.example.com/code-reviewer",
    "num_skills": 1,
    "registered_at": "2025-11-09T10:30:00Z",
    "is_enabled": false
  }
}

11. Agent Enables Itself (Optional)

curl -X POST http://localhost/api/agents/code-reviewer/toggle?enabled=true \
  -H "Authorization: Bearer $TOKEN"

Now the agent is discoverable by other agents and will appear in semantic searches.


Summary: The Key Concepts

Registry-Only Design: The registry handles discovery and validation, not communication. Once agents find each other, they talk directly.

Authentication Layer: All requests require a valid JWT token from a Keycloak service account. Tokens are validated at three points: Nginx, Auth-Server, and FastAPI.

Three-Tier Access Control:

  1. UI-Scopes define high-level actions
  2. Group Mappings connect Keycloak groups to scopes
  3. Individual Group Scopes define detailed permissions

Agent Card: The machine-readable profile that agents register. Contains name, description, URL, skills, security requirements, and metadata.

CRUD Operations: Agents can register, read, update, delete, and toggle themselves through REST APIs.

Discovery: Agents discover other agents using semantic search (natural language) or direct queries (by skill/tag).

File-Based Persistence: Agent state is stored in agent_state.json with simple JSON format.

FAISS Indexing: Agents are automatically indexed for semantic search alongside MCP servers.

This design enables autonomous agent ecosystems where agents discover and coordinate with each other while maintaining enterprise-grade security and access control.