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¶
- What We Built
- The Big Picture: Request Flow
- How Requests Get Authenticated
- The Agent Card: Machine-Readable Profile
- CRUD Operations: Agents Registering Themselves
- Discovery: How Agents Find Other Agents
- Access Control: Three-Tier Permission System
- 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:
- Token exists:
.oauth-tokens/ingress.jsonfile is present - Token is not expired: Decodes JWT payload, checks
expclaim against current timestamp - Token has correct groups: Verifies
groupsclaim 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 namedescription: What the agent doesurl: 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 agentname: Human-readable namedescription: What the skill doesparameters: JSON Schema defining input parameterstags: Categorization for discovery
Security:
security_schemes: How to authenticate with the agent (bearer, OAuth2, etc.)security: Which schemes are requiredtrust_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 registryregistered_at: When it was registeredregistered_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:
- Nginx receives request, validates JWT, calls auth-server
- Auth-server maps groups to scopes, returns
a2a-agent-adminscope - Nginx forwards request with
X-Scopes: a2a-agent-adminheader - FastAPI endpoint checks if scopes include
a2a-agent-admin - 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
- agent_service.py saves to
agent_state.json - FAISS service indexes the agent for semantic search
- 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-adminscope - 422 Unprocessable Entity: Validation failed
Reading: GET /api/agents/{path} and GET /api/agents¶
Get Single Agent:
Returns the complete agent card (if user has permission).
List All Agents:
What Happens:
- Auth checks what groups the user belongs to
- Loads all agents from
agent_state.json - Filters by access control: Only returns agents the user is authorized to see
- 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:
- Check permissions: User must have
modify_agentscope for this path - Validate new card: Same validation as registration
- Update in storage: Modify
agent_state.json - Re-index in FAISS: Update semantic search index
- Update timestamp: Set
updated_atto current time - Return: Updated agent card
Deleting: DELETE /api/agents/{path}¶
Remove an agent from the registry:
What Happens:
- Check permissions: User must have
delete_agentscope - Remove from storage: Delete from
agent_state.json - Remove from FAISS: Delete from semantic search index
- 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:
- Check permissions: User must have modify permissions
- Toggle state: Set
is_enabledto true or false - Update FAISS: Enabled status affects search results
- 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:
- Query Embedding: The natural language query is converted to a vector using an embedding model
- FAISS Search: The vector is compared against all agent cards stored in the FAISS index
- Ranking: Results ranked by similarity score
- Filtering: Only agents visible to this user (based on groups and visibility)
- 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-admincan list ALL agentsregistry-users-lob1can ONLY list/code-reviewerand/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
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-adminin scopes ✓ - Validates agent card with Pydantic ✓
- Checks path
/code-reviewerdoesn't exist ✓ - Calls agent_service.register_agent()
8. Agent Service Saves
- Loads current agent_state.json
- Adds
/code-reviewerentry - 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:
- UI-Scopes define high-level actions
- Group Mappings connect Keycloak groups to scopes
- 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.