Overview
The reference app-sdk provides a reference integration layer used by CoffeeAGNTCY to demonstrate how multi-agent applications can connect AGNTCY components and selected open source project as needed. It includes factory interfaces, abstractions, and examples that help developers understand interoperability patterns across AGNTCY transports, protocols, and directories.
It is intended as an integration and learning surface primarily used in coffeeAgntcy, not as a canonical or prescriptive SDK for AGNTCY subsystems. Standards and reference guidance remain the responsibility of the working groups. Please see the respective components for their latest SDK or interfaces.
| This is | This is not |
|---|---|
| A reference integration SDK used by CoffeeAGNTCY | The canonical SDK for every AGNTCY subsystem |
| A learning surface for application developers | A replacement for component-specific SDKs |
| A place to demonstrate cross-component application patterns | A standards authority |
| A bridge across AGNTCY components in reference app contexts | Ownership of the underlying components |
Features
|
🔌 Semantic Layer
|
🚀 Transport Layer
|
📂 Directory
|
🔐 Identity
|
|
🔍 Observability • Built-in Agntcy Observe SDK integration |
|||
📦 Installation
# Install via pip
pip install agntcy-app-sdk
# Or for uv based projects
uv add agntcy-app-sdk
# Install from source
git clone https://github.com/agntcy/app-sdk.git
pip install -e app-sdk
Quick Start
The Application SDK provides a single factory interface that abstracts over multiple semantic protocols and transports, so you can switch between them without rewriting your agent logic. Full usage guides: A2A · MCP / FastMCP
| Protocol | Transport | Client Type | Patterns | |
|---|---|---|---|---|
| Section 1 | A2A | Card-driven (multi-transport) | — | All (via add_a2a_card()) |
| Section 2 | A2A | SlimRPC | a2a.client.Client |
Point-to-point |
| Section 3 | A2A | SLIM patterns / NATS patterns | A2AExperimentalClient |
Point-to-point, broadcast |
| Section 4 | A2A | SLIM patterns | A2AExperimentalClient |
Group chat |
| Section 5 | MCP | SLIM / NATS | MCPClientSession |
Point-to-point |
| FastMCP | SLIM / NATS | FastMCPClient |
Point-to-point |
SlimRPC is the native A2A-over-SLIM RPC transport — simplest setup for 1:1 request/response. SLIM patterns and NATS patterns are experimental transports that unlock pub/sub fan-out and moderated group chat via the same AgntcyFactory interface. The factory negotiates the best transport automatically from the agent card.
Running examples: Save any snippet as a
.pyfile and run withuv run python my_script.py. The SDK is async — wrap top-levelawaitcalls inasyncio.run(main()).
1. Serve an A2A Agent
Declare your agent's transports in the card and serve over all of them with a single call:
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from agntcy_app_sdk.factory import AgntcyFactory
from agntcy_app_sdk.semantic.a2a.server.card_bootstrap import InterfaceTransport
# Declare transports in the card — this is the single source of truth.
# The order of `supported_interfaces` is the server's preference order:
# the first entry is the preferred transport.
agent_card = AgentCard(
name="My Agent", version="1.0.0",
default_input_modes=["text"], default_output_modes=["text"],
capabilities=AgentCapabilities(streaming=True), skills=[...],
supported_interfaces=[
AgentInterface(protocol_binding=InterfaceTransport.SLIM_PATTERNS,
url="slim://localhost:46357/default/default/My_Agent_slimpatterns"),
AgentInterface(protocol_binding=InterfaceTransport.SLIM_RPC,
url="slim://localhost:46357/default/default/My_Agent_rpc"),
AgentInterface(protocol_binding=InterfaceTransport.NATS_PATTERNS,
url="nats://localhost:4222/default/default/My_Agent_natspatterns"),
],
description="My agent description",
)
# One call does it all — add_a2a_card() reads the card's interfaces.
# a2a-sdk 1.x requires the card at handler construction.
factory = AgntcyFactory()
session = factory.create_app_session(max_sessions=10)
await (
session.add_a2a_card(agent_card, DefaultRequestHandler(
agent_executor=MyAgentExecutor(),
task_store=InMemoryTaskStore(),
agent_card=agent_card))
.start(keep_alive=True)
)
Alternative: Serve a single transport manually (SlimRPC)
from agntcy_app_sdk.factory import AgntcyFactory
from agntcy_app_sdk.semantic.a2a.server.srpc import A2ASlimRpcServerConfig, SlimRpcConnectionConfig
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
config = A2ASlimRpcServerConfig(
agent_card=agent_card,
request_handler=DefaultRequestHandler(
agent_executor=MyAgentExecutor(),
task_store=InMemoryTaskStore(),
agent_card=agent_card,
),
connection=SlimRpcConnectionConfig(
identity="default/default/my_agent",
# Hardcoded on purpose: a demo value, not a real secret.
shared_secret="my-shared-secret-at-least-32-characters-long",
endpoint="http://localhost:46357",
),
)
factory = AgntcyFactory()
session = factory.create_app_session(max_sessions=1)
session.add(config).with_session_id("default").build()
await session.start_all_sessions(keep_alive=True)
2. Connect to an Agent
Configure all transports once, then let the factory negotiate the best one from the agent card:
from a2a.helpers import get_stream_response_text, new_text_message
from a2a.types import Role, SendMessageRequest
from agntcy_app_sdk.semantic.a2a.client.factory import A2AClientFactory
from agntcy_app_sdk.semantic.a2a.client.config import (
ClientConfig, SlimRpcConfig, SlimTransportConfig, NatsTransportConfig,
)
# Multi-transport config — factory picks the right one from the agent card
config = ClientConfig(
slimrpc_config=SlimRpcConfig(
namespace="default",
group="default",
name="my_client",
slim_url="http://localhost:46357",
# Hardcoded on purpose: a demo value, not a real secret.
secret="my-shared-secret-at-least-32-characters-long",
),
slim_config=SlimTransportConfig(
endpoint="http://localhost:46357",
name="default/default/my_client",
# Hardcoded on purpose: a demo value, not a real secret.
shared_secret_identity="my-shared-secret-at-least-32-characters-long",
),
nats_config=NatsTransportConfig(
endpoint="nats://localhost:4222",
),
)
# Create the factory — supports slimrpc, slimpatterns, natspatterns, and JSONRPC
a2a = A2AClientFactory(config)
# Create a client — factory negotiates over the card's supported_interfaces
# (server order wins unless ClientConfig(use_client_preference=True))
client = await a2a.create(agent_card)
# Send a message and stream the response. Events are `StreamResponse`
# objects carrying a message, task, or status/artifact update.
request = SendMessageRequest(
message=new_text_message("Hello, agent!", role=Role.ROLE_USER)
)
async for event in client.send_message(request):
text = get_stream_response_text(event)
if text:
print(text)
Alternative: Connect with a single transport (SlimRPC only)
from a2a.client import minimal_agent_card
from a2a.helpers import get_stream_response_text, new_text_message
from a2a.types import Role, SendMessageRequest
from slima2a import setup_slim_client
from slima2a.client_transport import slimrpc_channel_factory
from agntcy_app_sdk.factory import AgntcyFactory
from agntcy_app_sdk.semantic.a2a.client.config import ClientConfig
# Set up the SLIM connection
service, slim_app, local_name, conn_id = await setup_slim_client(
namespace="default", group="default", name="my_client",
slim_url="http://localhost:46357",
# Hardcoded on purpose: a demo value, not a real secret. Must match the
# server's shared_secret.
secret="my-shared-secret-at-least-32-characters-long",
)
# Enable slimrpc on the SDK's ClientConfig and let the factory negotiate
config = ClientConfig(
slimrpc_channel_factory=slimrpc_channel_factory(slim_app, conn_id),
)
# With IDENTITY_AUTH_ENABLED, every slimrpc call carries this agent's
# identity token, whatever the card declares: only call peers you trust.
card = minimal_agent_card("default/default/my_agent", ["slimrpc"])
client = await AgntcyFactory().a2a(config).create(card)
request = SendMessageRequest(
message=new_text_message("Hello, agent!", role=Role.ROLE_USER)
)
async for event in client.send_message(request):
print(get_stream_response_text(event))
3. Broadcast to Many Agents
Fan out a single request to N agents simultaneously. Broadcast uses the SLIM patterns or NATS patterns transport (not SlimRPC) — the factory returns an A2AExperimentalClient with broadcast_message() when transport negotiation selects slimpatterns or natspatterns.
Broadcast example (SLIM patterns)
from a2a.helpers import new_text_message
from a2a.types import (
AgentCapabilities, AgentCard, AgentInterface, Role, SendMessageRequest,
)
from agntcy_app_sdk.factory import AgntcyFactory
from agntcy_app_sdk.semantic.a2a import A2AExperimentalClient
from agntcy_app_sdk.semantic.a2a.client.config import ClientConfig, SlimTransportConfig
from agntcy_app_sdk.semantic.a2a.server.card_bootstrap import InterfaceTransport
factory = AgntcyFactory()
# SLIM patterns transport — creates an A2AExperimentalClient
# (swap SlimTransportConfig for NatsTransportConfig + nats:// scheme for NATS)
config = ClientConfig(
slim_config=SlimTransportConfig(
endpoint="http://localhost:46357",
name="default/default/broadcaster",
),
)
card = AgentCard(
name="default/default/agent1",
version="1.0.0",
default_input_modes=["text"],
default_output_modes=["text"],
capabilities=AgentCapabilities(),
skills=[],
supported_interfaces=[
AgentInterface(
protocol_binding=InterfaceTransport.SLIM_PATTERNS,
url="slim://default/default/agent1",
),
],
description="Agent 1",
)
client = await factory.a2a(config).create(card)
# create() is typed as Client; patterns negotiation yields the experimental client
assert isinstance(client, A2AExperimentalClient)
request = SendMessageRequest(
message=new_text_message("Status check", role=Role.ROLE_USER)
)
# Fan-out to 3 agents at once
responses = await client.broadcast_message(
request,
broadcast_topic="status_channel",
recipients=[
"default/default/agent1",
"default/default/agent2",
"default/default/agent3",
],
)
# `responses` is a list of SendMessageResponse (message or task per agent)
for resp in responses:
print(f"Response: {resp}")
# A broadcast only returns normally when *every* recipient replied
# successfully. Otherwise it raises A2ABroadcastError, which keeps the
# partial result:
from agntcy_app_sdk.semantic.a2a import A2ABroadcastError
try:
responses = await client.broadcast_message(request, recipients=[...])
except A2ABroadcastError as err:
responses = err.responses # the successful replies
for failure in err.failures: # error replies, with their sender
print(f"{failure.sender} failed: {failure.error}")
print(f"missing: {err.missing_recipients or err.missing}")
# Streaming variant — yields a StreamResponse for each event as it arrives
async for resp in client.broadcast_message_streaming(
request,
broadcast_topic="status_channel",
recipients=[
"default/default/agent1",
"default/default/agent2",
"default/default/agent3",
],
message_limit=3,
):
print(f"Streaming response: {resp}")
# The streaming variant yields every successful event first, then raises
# A2ABroadcastError at the end if a final reply failed or never arrived
# (err.responses is empty there, err.succeeded names who answered).
How a missing reply surfaces depends on the transport. SLIM raises at the timeout, so you get A2ABroadcastTimeoutError (a subclass of both A2ABroadcastError and TimeoutError). NATS ends the stream at the timeout without raising, so you get a plain A2ABroadcastError with err.missing > 0. Replies are matched to recipients through the x-a2a-sender header that SDK servers set to their topic, so a recipient that replies twice is counted once. missing_recipients is only filled when every reply names one of the recipients. A recipient that rejects the caller's identity (status 401 / 403, or a "forbidden" / "unauthorized" error) shows up in err.failures, not as a reply. Transport failures, such as not being connected or passing no recipients, raise the transport's own exception. If the transport fails after at least one final reply has arrived (for example, the SLIM session closes), you get an A2ABroadcastError that keeps those replies, with the transport's exception in err.transport_error.
4. Run a Group Chat
Start a moderated multi-party conversation between agents. Each participant processes the message and forwards it to the next. Group chat uses the SLIM patterns transport — the same A2AExperimentalClient from above.
Note: Group chat currently requires SLIM transport.
Group chat example (SLIM patterns)
from a2a.helpers import new_text_message
from a2a.types import (
AgentCapabilities, AgentCard, AgentInterface, Role, SendMessageRequest,
)
from agntcy_app_sdk.factory import AgntcyFactory
from agntcy_app_sdk.semantic.a2a import A2AExperimentalClient
from agntcy_app_sdk.semantic.a2a.client.config import ClientConfig, SlimTransportConfig
from agntcy_app_sdk.semantic.a2a.server.card_bootstrap import InterfaceTransport
factory = AgntcyFactory()
# SLIM patterns transport — creates an A2AExperimentalClient
config = ClientConfig(
slim_config=SlimTransportConfig(
endpoint="http://localhost:46357",
name="default/default/moderator",
),
)
card = AgentCard(
name="default/default/agent_a",
version="1.0.0",
default_input_modes=["text"],
default_output_modes=["text"],
capabilities=AgentCapabilities(),
skills=[],
supported_interfaces=[
AgentInterface(
protocol_binding=InterfaceTransport.SLIM_PATTERNS,
url="slim://default/default/agent_a",
),
],
description="Agent A",
)
client = await factory.a2a(config).create(card)
assert isinstance(client, A2AExperimentalClient)
request = SendMessageRequest(
message=new_text_message("Plan a team lunch", role=Role.ROLE_USER)
)
# Non-streaming group chat — collects all messages then returns
responses = await client.start_groupchat(
init_message=request,
group_channel="lunch_planning",
participants=["default/default/agent_a", "default/default/agent_b"],
end_message="DELIVERED",
timeout=60,
)
for resp in responses:
print(f"Group message: {resp}")
# Streaming group chat — yields each message as it arrives
async for message in client.start_streaming_groupchat(
init_message=request,
group_channel="lunch_planning",
participants=["default/default/agent_a", "default/default/agent_b"],
end_message="DELIVERED",
timeout=60,
):
print(f"Live: {message}")
How group chat errors surface. Group chat runs over SLIM only; on NATS both methods raise NotImplementedError. A transport failure, such as not being connected, passing no participants, the session closing, or reaching the timeout, raises the transport's own exception from both methods. start_groupchat then returns nothing, so the messages received before the failure are lost; start_streaming_groupchat has already yielded them. A single message that can't be read (not JSON, a JSON-RPC error from a participant, or an after interceptor that raises) is handled differently: start_groupchat logs it and leaves it out of the list, while start_streaming_groupchat raises its exception and ends the stream. A participant that rejects the caller's identity (status 401 / 403, or a "forbidden" / "unauthorized" error) shows up in both as a synthetic agent message saying access was forbidden.
Relayed replies must carry a message. A participant can receive another agent's reply as its input. A Task reply is relayed through its status message, or else the last agent message in its history. A Task with only artifacts, which is what a2a-sdk's TaskUpdater.complete() produces without a message, is rejected with InvalidRequest, so participants should complete with a message, for example await updater.complete(message=...).
5. Use MCP Tools over a Transport
Connect to a remote MCP server and call its tools — same transport layer as A2A, different protocol:
import asyncio
from agntcy_app_sdk.factory import AgntcyFactory
factory = AgntcyFactory()
# Create a transport (swap "SLIM" for "NATS" + endpoint to switch)
transport = factory.create_transport(
"SLIM", endpoint="http://localhost:46357", name="default/default/mcp_client"
)
async def main():
mcp_client = await factory.mcp().create_client(
topic="my_weather_agent.mcp",
transport=transport,
)
async with mcp_client as client:
tools = await client.list_tools()
print("Available tools:", tools)
result = await client.call_tool(
name="get_forecast",
arguments={"location": "Colombia"},
)
print(f"Forecast: {result}")
await transport.close()
asyncio.run(main())
📖 Complete MCP & FastMCP guide →
Transport & Protocol Support
The SDK negotiates the best transport automatically by intersecting the server's AgentCard transports with your ClientConfig capabilities. When the winning transport is slimpatterns or natspatterns, the factory returns an A2AExperimentalClient that extends the standard Client with broadcast and group-chat methods.
| Pattern | SLIM | NATS | Description |
|---|---|---|---|
| Point-to-Point | ✅ | ✅ | Standard 1:1 send_message() over transport |
| Broadcast | ✅ | ✅ | 1:N fan-out via broadcast_message() |
| Broadcast Streaming | ✅ | ✅ | Streaming variant via broadcast_message_streaming() |
| Group Chat | ✅ | — | Multi-party moderated conversation via start_groupchat() |
| Group Chat Streaming | ✅ | — | Streaming variant via start_streaming_groupchat() |
Architecture
Layered Architecture
┌─────────────────────────────────────────────────────────────┐
│ AgntcyFactory │
│ factory = AgntcyFactory() │
│ │
│ factory.a2a(config) → A2AClientFactory │
│ factory.mcp() → MCPClientFactory │
│ factory.fast_mcp() → FastMCPClientFactory │
│ factory.create_transport("SLIM"|"NATS", ...) │
│ factory.create_app_session(max_sessions=10) │
└──────┬──────────────────┬──────────────────┬────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌───────────────────┐ ┌─────────────────────┐
│ Transport │ │ Protocol Client │ │ AppSession │
│ Layer │ │ Layer │ │ (server-side) │
│ │ │ │ │ │
│ BaseTransport│ │ a2a.client.Client │ │ .add(target) │
│ ├ SLIM │ │ (SlimRPC p2p) │ │ .with_transport() │
│ ├ NATS │ │ │ │ .with_topic() │
│ └ HTTP │ │ A2AExperimental- │ │ .with_session_id() │
│ │ │ Client │ │ .build() │
│ SlimRPC │ │ (SLIM/NATS │ │ │
│ (native A2A │ │ patterns) │ │ Targets: │
│ transport) │ │ │ │ ├ A2AServerConfig │
│ │ │ MCPClientSession │ │ ├ A2ASlimRpcServer │
│ 3 Mixins: │ │ FastMCPClient │ │ ├ MCP Server │
│ ├ P2P │ └───────────────────┘ │ └ FastMCP │
│ ├ FanOut │ └─────────────────────┘
│ └ GroupChat │
└──────────────┘
Card-Driven Bootstrap (Recommended)
add_a2a_card() reads the agent card's supported_interfaces and creates all transports automatically — no manual builder chains required:
from agntcy_app_sdk.semantic.a2a.server.card_bootstrap import InterfaceTransport
agent_card = AgentCard(
...,
supported_interfaces=[ # order = server preference (first is preferred)
AgentInterface(protocol_binding=InterfaceTransport.SLIM_PATTERNS, url="slim://host:46357/topic"),
AgentInterface(protocol_binding=InterfaceTransport.NATS_PATTERNS, url="nats://host:4222/topic"),
AgentInterface(protocol_binding=InterfaceTransport.JSONRPC, url="http://0.0.0.0:9000"),
],
)
session = factory.create_app_session(max_sessions=10)
await (
session.add_a2a_card(agent_card, request_handler)
.with_factory(factory)
.skip("jsonrpc") # optionally skip a transport
.start(keep_alive=True)
)
| Method | Description |
|---|---|
.with_factory(factory) |
Reuse an existing AgntcyFactory (auto-created if omitted) |
.with_shared_secret(secret) |
Provide SLIM shared secret directly (falls back to SLIM_SHARED_SECRET) |
.allow_placeholder_secret() |
Local demos only: start SLIM interfaces with the public placeholder |
.skip(transport_type) |
Exclude a specific transport from being started |
.override(transport_type, obj) |
Supply a pre-built transport or config for an interface |
.with_public_url(url) |
Advertise a reachable URL for the JSONRPC interface (see below) |
.dry_run() |
Return a ServeCardPlan describing what would start |
.start(keep_alive=False) |
Build all containers and start sessions |
The JSONRPC URL in the card (http://0.0.0.0:9000 above) is the address the server binds to, and it is also what the served card advertises. Clients usually cannot reach 0.0.0.0 or an in-container address, so call .with_public_url("https://agent.example.com") to bind to the card's URL while advertising the public one. Your own card object is not modified.
SLIM shared secret
Every SLIM peer authenticates with a shared secret of at least 32 characters, and server and client must use the same value. The SDK picks it in this order:
- The value you pass:
shared_secret_identityonSLIMTransport/SlimTransportConfig,secretonSlimRpcConfig,shared_secretonSlimRpcConnectionConfig, orCardBuilder.with_shared_secret(). - The
SLIM_SHARED_SECRETenvironment variable. - A public placeholder, with a warning in the log. It only suits demos and local tests: any peer with the default can join your sessions.
CardBuilder stops before step 3: if neither a secret nor SLIM_SHARED_SECRET is set, or the value is the placeholder, start() and dry_run() raise ValueError. Call .allow_placeholder_secret() to run a local demo with the placeholder.
All SLIMTransports in one process share one SLIM connection, so they must use the same credentials; a transport asking for a different secret or JWT than the open connection raises ValueError. In production, set SLIM_SHARED_SECRET from a secret manager to a random value, for example python -c "import secrets; print(secrets.token_urlsafe(48))", and never commit it.
Manual Session Builder
The AppSession builder chains configuration into a single readable expression for fine-grained single-transport control. The SDK auto-detects the handler type from the target you pass to add():
| Target Type | Handler Created | Transport Required? |
|---|---|---|
A2AServerConfig |
A2AExperimentalServerHandler |
Yes |
A2AServerConfig |
A2AJsonRpcServerHandler |
No (HTTP) |
A2ASlimRpcServerConfig |
A2ASRPCServerHandler |
No (internal) |
mcp.server.lowlevel.Server |
MCPServerHandler |
Yes |
mcp.server.fastmcp.FastMCP |
FastMCPServerHandler |
Yes |
factory = AgntcyFactory()
session = factory.create_app_session(max_sessions=3)
# A2A over SLIM patterns
session.add(a2a_server) \
.with_transport(slim_transport) \
.with_topic("my_agent") \
.with_session_id("a2a") \
.build()
# A2A over SlimRPC (transport managed internally)
session.add(a2a_srpc_config) \
.with_session_id("slimrpc") \
.build()
# A2A over HTTP (no transport needed)
session.add(a2a_server) \
.with_host("0.0.0.0") \
.with_port(9000) \
.with_public_url("https://agent.example.com") \
.with_session_id("http") \
.build()
# Start everything
await session.start_all_sessions(keep_alive=True)
.with_public_url() is the address clients should use (it is added to the card as a JSONRPC interface if the card does not already declare one). The bind address from .with_host() / .with_port() is never written to the card, since it is often not reachable by clients.
Observability
Enable distributed tracing across all A2A and SLIM operations with a single flag:
factory = AgntcyFactory(enable_tracing=True)
This initialises the Agntcy Observe SDK (OpenTelemetry-based) and auto-instruments SLIM transports and A2A client calls. Traces are exported to the configured OTLP_HTTP_ENDPOINT (default: http://localhost:4318).
enable_tracing=True records the request in this process only. It does not set TRACING_ENABLED, so child processes do not inherit it. Create the factory before transports and A2A servers when you can. Components created earlier are remembered and installed when Observe.init() returns, including when you call Observe.init() yourself.
📁 Project Structure
📁 src/
└── 📦 agntcy_app_sdk/
├── 🏭 factory.py # Main factory interface
├── 🔄 app_sessions.py # Session management & fluent builder
├── 📂 directory/ # Agent directory services
├── 🔐 identity/ # Authentication & identity
├── 🧠 semantic/ # Protocol layer
│ ├── a2a/ # A2A protocol + experimental patterns
│ ├── mcp/ # MCP protocol bridge
│ └── fast_mcp/ # FastMCP protocol bridge
├── 🌐 transport/ # Transport implementations
│ ├── slim/ # SLIM (Secure Low-Latency Interactive Messaging)
│ ├── nats/ # NATS messaging
│ └── streamable_http/ # HTTP-based transport
└── 🛠️ common/ # Shared utilities & logging
Reference Application
For a fully functional distributed multi-agent sample app, check out our coffeeAgntcy!
Agntcy Component Usage
| Component | Version | Description | Repo |
|---|---|---|---|
| SLIM | 2.2.0 |
Secure Low-Latency Interactive Messaging (SLIM) facilitates communication between AI agents using request-reply and moderated group-chat patterns. | Repo |
| Observe SDK | 1.0.34 |
Enables multi-agent observability by setting enable_tracing=True when initializing the AgntcyFactory. This automatically configures tracing and auto-instrumentation for SLIM and A2A. |
Repo |
| Directory | 1.0.0 |
Agent discovery and registration via the Agntcy Directory service. Push, pull, and search agent records with automatic OASF ↔ AgentCard conversion. Integrated into AppSession lifecycle. |
Repo |
| Identity | 0.0.7 |
Agent identity and authentication via the Agntcy Identity Service SDK. Supports shared-secret and JWT identity providers, access token management, and auth middleware for A2A and FastMCP. | Repo |
Development
Prerequisites
- Python 3.12+
- uv — Package manager
- Task — Build orchestration (used locally and in CI)
- Docker — Required for E2E tests
Setup
# Install dependencies (including dev)
uv sync
# Run linting and type checking
task lint
# Auto-fix lint issues
task fix-lint
# Run unit tests
task test
# Run E2E tests (requires Docker services)
task test-e2e
Code Quality
The project uses Ruff for linting/formatting, mypy for type checking, and Codespell for spell checking. Pre-commit hooks enforce all checks automatically:
pre-commit run --all-files
Testing
The /tests directory contains both unit and end-to-end (E2E) tests for Agntcy components and workflows.
Prerequisites
Before running E2E tests, start the required message bus services:
docker-compose -f services/docker/docker-compose.yaml up
Running Tests
Unit tests (no Docker required):
task test
All E2E tests (requires Docker services):
task test-e2e
You can also pass additional arguments to pytest via the CLI_ARGS variable or run specific tests directly:
A2A Client Tests
Run all transports
uv run pytest tests/e2e/test_a2a_starlette.py::test_client -s
Run a single transport (e.g. SLIM):
uv run pytest tests/e2e/test_a2a_starlette.py::test_client -s -k "SLIM"
SlimRPC A2A
uv run pytest tests/e2e/test_a2a_slimrpc.py::test_client -s
Broadcast messaging
uv run pytest tests/e2e/test_a2a_starlette.py::test_broadcast -s
Group chat (SLIM only):
uv run pytest tests/e2e/test_a2a_starlette.py::test_groupchat -s -k "SLIM"
FastMCP Client Tests
Single transport (e.g. SLIM):
uv run pytest tests/e2e/test_fast_mcp.py::test_client -s -k "SLIM"
CI/CD
Continuous Integration
Every push to main and every pull request triggers the build-and-test workflow, which runs the following jobs in parallel:
| Job | Description |
|---|---|
| pylint | Ruff linting + mypy type checking + format check |
| pytest | Unit test suite |
| pytest-e2e | E2E tests with Docker services (SLIM + NATS) |
| pypackage | Build wheel and sdist artifacts |
Release Flow
Releases are automated via release-please:
- Merge pull requests with Conventional Commits (e.g.,
feat: add X,fix: resolve Y). - release-please automatically creates a release PR that bumps the version in
pyproject.tomland updates the changelog. - Merge the release PR to create a GitHub release and push a
v*tag. - The release workflow builds multi-platform wheels (Linux x86/ARM, macOS ARM, Windows) and publishes to PyPI.
PR merged → release-please creates release PR → merge → tag v* → build wheels → publish to PyPI
Contributing
Contributions are welcome! Please see the contribution guide for details on how to contribute to the Agntcy Application SDK.
Metadata
Release files for agntcy-app-sdk 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| agntcy_app_sdk-0.6.0.tar.gz | 3.2 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agntcy_app_sdk-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.4 MB
Release files / agntcy_app_sdk-0.6.0.tar.gz
| Download URL | agntcy_app_sdk-0.6.0.tar.gz |
|---|---|
| Size | 3.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
96d5c46e9496646c802d60e0daa682a86f6f34014872c43b66aec954ee18e1cf
|
|
BLAKE2b-256 checksum How to use checksums |
83d4d3d089036f52ce743973c59c7151f086dc5f98e335668b55b23641fb4ebb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / agntcy_app_sdk-0.6.0-py3-none-any.whl
| Download URL | agntcy_app_sdk-0.6.0-py3-none-any.whl |
|---|---|
| Size | 147.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
75fee7e3ca386a31a68fb9a7fad789e6bc49917cd02aa1b23dbaecb51f2f9529
|
|
BLAKE2b-256 checksum How to use checksums |
b5377c5af7c89ac644d045045475c9674a8bdacbd2fc809f3c0c45cd6b8527be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for agntcy-app-sdk 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.