# 1759 A2a Protocol Integration 798c7701

> A2A Protocol Integration: Comprehensive Developer Guide

- Skill: `tools-only/1759-a2a-protocol-integration-798c7701` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/1759-a2a-protocol-integration-798c7701`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/1759-a2a-protocol-integration-798c7701/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tools-only/1759-a2a-protocol-integration-798c7701

---

# 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](#what-we-built)
2. [The Big Picture: Request Flow](#the-big-picture-request-flow)
3. [How Requests Get Authenticated](#how-requests-get-authenticated)
4. [The Agent Card: Machine-Readable Profile](#the-agent-card-machine-readable-profile)
5. [CRUD Operations: Agents Registering Themselves](#crud-operations-agents-registering-themselves)
6. [Discovery: How Agents Find Other Agents](#discovery-how-agents-find-other-agents)
7. [Access Control: Three-Tier Permission System](#access-control-three-tier-permission-system)
8. [The Code: Where Everything Lives](#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:

```bash
# 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:

```json
{
  "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`:

```yaml
# 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:
```json
{
  "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:

```python
@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:

```json
{
  "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

```json
{
  "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:

```bash
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`:
   - Schema validation (Pydantic)
   - Unique path check
   - Skills have unique IDs
   - Security schemes are properly configured
6. **agent_service.py** saves to `agent_state.json`
7. **FAISS service** indexes the agent for semantic search
8. **Response**: 201 Created with registered agent info

**Success Response**:
```json
{
  "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**:
```bash
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost/api/agents/code-reviewer
```

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

**List All Agents**:
```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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**:
```json
{
  "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:

```python
# 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:

```json
{
  "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:

```bash
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:

```yaml
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:

```yaml
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:

```yaml
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:

```python
@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 - 838 lines)

This file defines 8 REST API endpoints:

```python
@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 - 695 lines)

This file handles all agent operations:

```python
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 - 603 lines)

Pydantic models for validation:

```python
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 - 343 lines)

Ensures agent cards are valid:

```python
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:

```json
{
  "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:

```python
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:

```yaml
# 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**
```json
{
  "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**
```bash
$ ./credentials-provider/generate_creds.sh
# Generates .oauth-tokens/ingress.json with 5-minute TTL
```

**3. Agent POSTs to Registry**
```bash
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**
```json
{
  "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**
```json
{
  "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)**
```bash
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.

