Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
a2a

Check warning on line 1 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
A2A

Check warning on line 2 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
A2AFastAPI

Check warning on line 3 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
AAgent
Expand All @@ -20,6 +20,7 @@
aproject
ARequest
ARun
ARoute
AServer
AServers
AService
Expand All @@ -27,7 +28,7 @@
AUser
autouse
backticks
base64url

Check warning on line 31 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
buf
bufbuild
cla
Expand All @@ -42,7 +43,7 @@
drivername
DSNs
dunders
ES256

Check warning on line 46 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
euo
EUR
evt
Expand All @@ -57,8 +58,8 @@
gle
GVsb
hazmat
HS256

Check warning on line 61 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
HS384

Check warning on line 62 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
ietf
importlib
initdb
Expand Down Expand Up @@ -94,13 +95,15 @@
npx
oauthoidc
oidc
oneof
oneofs
Oneof
OpenAPI
openapiv
openapiv2

Check warning on line 103 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
opensource
otherurl
pb2

Check warning on line 106 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
podman
Podman
poolclass
Expand All @@ -112,6 +115,7 @@
protobuf
Protobuf
protoc
protojson
pydantic
pyi
pypistats
Expand All @@ -122,9 +126,10 @@
respx
resub
rmi
RS256

Check warning on line 129 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
RUF
SECP256R1
SFIXED
SLF
socio
sse
Expand Down
24 changes: 24 additions & 0 deletions docs/migrations/v1_0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,30 @@ app = FastAPI(routes=routes)
uvicorn.run(app, host=host, port=port)
```

`FastAPI(routes=routes)` mounts the A2A endpoints correctly, but FastAPI's OpenAPI generator only enumerates routes that are `fastapi.routing.APIRoute` instances, so the A2A endpoints will not appear in `/docs` or `/openapi.json`. To make them visible in the auto-generated OpenAPI schema — grouped into Agent Card, JSON-RPC, and REST sections — use the `add_a2a_routes_to_fastapi` helper:

```python
from fastapi import FastAPI
import uvicorn

from a2a.server.routes import (
add_a2a_routes_to_fastapi,
create_agent_card_routes,
create_jsonrpc_routes,
create_rest_routes,
)

app = FastAPI()
add_a2a_routes_to_fastapi(
app,
agent_card_routes=create_agent_card_routes(agent_card),
jsonrpc_routes=create_jsonrpc_routes(request_handler, rpc_url='/'),
rest_routes=create_rest_routes(request_handler),
)

uvicorn.run(app, host=host, port=port)
```

> **Example**: [`a2a-mcp-without-framework/server/__main__.py` in PR #509](https://github.com/a2aproject/a2a-samples/pull/509/files#diff-d15d39ae64c3d4e3a36cc6fb442302caf4e32a6dbd858792e7a4bed180a625ac)

---
Expand Down
2 changes: 2 additions & 0 deletions src/a2a/server/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
DefaultServerCallContextBuilder,
ServerCallContextBuilder,
)
from a2a.server.routes.helpers import add_a2a_routes_to_fastapi
from a2a.server.routes.jsonrpc_routes import create_jsonrpc_routes
from a2a.server.routes.rest_routes import create_rest_routes


__all__ = [
'DefaultServerCallContextBuilder',
'ServerCallContextBuilder',
'add_a2a_routes_to_fastapi',
'create_agent_card_routes',
'create_jsonrpc_routes',
'create_rest_routes',
Expand Down
1 change: 1 addition & 0 deletions src/a2a/server/routes/agent_card_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def create_agent_card_routes(
)

async def _get_agent_card(request: Request) -> Response:
"""Returns the public AgentCard describing this agent's capabilities, supported transports, and skills."""
card_to_serve = agent_card
if card_modifier:
card_to_serve = await card_modifier(card_to_serve)
Expand Down
6 changes: 6 additions & 0 deletions src/a2a/server/routes/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from a2a.server.routes.helpers.fastapi import add_a2a_routes_to_fastapi


__all__ = [
'add_a2a_routes_to_fastapi',
]
118 changes: 118 additions & 0 deletions src/a2a/server/routes/helpers/_proto_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Proto → JSON Schema helpers shared across transport helpers."""

from typing import Any

from google.protobuf.descriptor import Descriptor, FieldDescriptor
from google.protobuf.message import Message

from a2a.types.a2a_pb2 import SendMessageRequest, TaskPushNotificationConfig


REST_BODY_TYPES: dict[tuple[str, str], type[Message]] = {
('/message:send', 'POST'): SendMessageRequest,
('/message:stream', 'POST'): SendMessageRequest,
('/tasks/{id}/pushNotificationConfigs', 'POST'): TaskPushNotificationConfig,
}

# 64-bit integer types serialize as strings in protojson.
_PROTO_SCALAR_SCHEMAS: dict[int, dict[str, Any]] = {
FieldDescriptor.TYPE_DOUBLE: {'type': 'number'},
FieldDescriptor.TYPE_FLOAT: {'type': 'number'},
FieldDescriptor.TYPE_INT64: {'type': 'string', 'format': 'int64'},
FieldDescriptor.TYPE_UINT64: {'type': 'string', 'format': 'uint64'},
FieldDescriptor.TYPE_INT32: {'type': 'integer', 'format': 'int32'},
FieldDescriptor.TYPE_FIXED64: {'type': 'string', 'format': 'fixed64'},
FieldDescriptor.TYPE_FIXED32: {'type': 'integer', 'format': 'fixed32'},
FieldDescriptor.TYPE_BOOL: {'type': 'boolean'},
FieldDescriptor.TYPE_STRING: {'type': 'string'},
FieldDescriptor.TYPE_BYTES: {'type': 'string', 'format': 'byte'},
FieldDescriptor.TYPE_UINT32: {'type': 'integer', 'format': 'uint32'},
FieldDescriptor.TYPE_SFIXED32: {'type': 'integer'},
FieldDescriptor.TYPE_SFIXED64: {'type': 'string'},
FieldDescriptor.TYPE_SINT32: {'type': 'integer'},
FieldDescriptor.TYPE_SINT64: {'type': 'string'},
}

_WELL_KNOWN_SCHEMAS: dict[str, dict[str, Any]] = {
'google.protobuf.Timestamp': {'type': 'string', 'format': 'date-time'},
'google.protobuf.Duration': {'type': 'string'},
'google.protobuf.Struct': {'type': 'object'},
'google.protobuf.Value': {},
'google.protobuf.ListValue': {'type': 'array', 'items': {}},
'google.protobuf.Empty': {'type': 'object'},
'google.protobuf.Any': {'type': 'object'},
'google.protobuf.FieldMask': {'type': 'string'},
}


def field_schema(
field: FieldDescriptor, components: dict[str, Any]
) -> dict[str, Any]:
if field.message_type and field.message_type.GetOptions().map_entry:
value_field = field.message_type.fields_by_name['value']
return {
'type': 'object',
'additionalProperties': field_schema(value_field, components),
}

if field.type == FieldDescriptor.TYPE_MESSAGE:
item = message_schema(field.message_type, components)
elif field.type == FieldDescriptor.TYPE_ENUM:
item = {
'type': 'string',
'enum': [v.name for v in field.enum_type.values],
}
else:
item = dict(_PROTO_SCALAR_SCHEMAS.get(field.type, {'type': 'string'}))

if field.is_repeated:
return {'type': 'array', 'items': item}
return item


def message_schema(
descriptor: Descriptor | Any, components: dict[str, Any]
) -> dict[str, Any]:
"""Returns a $ref to descriptor's schema, registering it in components if needed."""
if descriptor.full_name in _WELL_KNOWN_SCHEMAS:
return dict(_WELL_KNOWN_SCHEMAS[descriptor.full_name])

name = descriptor.name
ref = {'$ref': f'#/components/schemas/{name}'}
if name in components:
return ref

# Reserve the slot before recursing so cyclic types terminate.
components[name] = {}

real_oneofs = [o for o in descriptor.oneofs if len(o.fields) > 1]
oneof_field_names = {f.name for o in real_oneofs for f in o.fields}
base_properties = {
f.name: field_schema(f, components)
for f in descriptor.fields
if f.name not in oneof_field_names
}

if not real_oneofs:
components[name] = {'type': 'object', 'properties': base_properties}
return ref

oneof_constraints = [
{
'oneOf': [
{
'type': 'object',
'properties': {f.name: field_schema(f, components)},
'required': [f.name],
}
for f in oneof.fields
]
}
for oneof in real_oneofs
]
parts: list[dict[str, Any]] = []
if base_properties:
parts.append({'type': 'object', 'properties': base_properties})
parts.extend(oneof_constraints)
components[name] = parts[0] if len(parts) == 1 else {'allOf': parts}
return ref
Loading
Loading