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.
Scanned 6/1/2026
Install via CLI
openskills install tools-only/X-SkillsAre you the author of 1759 A2a Protocol Integration 798c7701?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/tools-only-1759-a2a-protocol-integration-798c7701)More formats (shields.io, HTML) on the badges page.
# 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.
No comments yet. Be the first to comment!