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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ for agent in results:
agent_summary = sdk.getAgent("11155111:123")
```

### 4b. Create MCP/A2A Clients Directly from URL

```python
# MCP: URL is treated as the direct MCP endpoint
mcp = sdk.createMCPClient("https://mcp.example.com/mcp")
tools = mcp.listTools()

# A2A: URL can be an agent-card URL or base URL (discovery is applied)
a2a = sdk.createA2AClient("https://a2a.example.com")
reply = a2a.messageA2A("hello")
```

### 5. Give and Retrieve Feedback

```python
Expand Down
30 changes: 30 additions & 0 deletions agent0_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@
AgentCardAuth,
)
from .core.a2a_summary_client import A2AClientFromSummary
from .core.a2a_summary_client import A2AClientFromUrl
from .core.mcp_client import MCPClient, create_mcp_handle
from .core.mcp_summary_client import MCPClientFromSummary
from .core.mcp_types import (
MCPAuthOptions,
MCPClientInfo,
MCPClientOptions,
MCPInitializeResult,
MCPTool,
MCPPrompt,
MCPPromptGetResult,
MCPResource,
MCPResourceTemplate,
MCPPromptMessage,
)
_sdk_available = True
except ImportError:
SDK = None
Expand All @@ -63,6 +78,7 @@
LoadTaskOptions = None
AgentCardAuth = None
A2AClientFromSummary = None
A2AClientFromUrl = None
_sdk_available = False

__version__ = "1.7.1"
Expand Down Expand Up @@ -103,4 +119,18 @@
"LoadTaskOptions",
"AgentCardAuth",
"A2AClientFromSummary",
"A2AClientFromUrl",
"MCPClient",
"create_mcp_handle",
"MCPClientFromSummary",
"MCPAuthOptions",
"MCPClientInfo",
"MCPClientOptions",
"MCPInitializeResult",
"MCPTool",
"MCPPrompt",
"MCPPromptGetResult",
"MCPResource",
"MCPResourceTemplate",
"MCPPromptMessage",
]
152 changes: 152 additions & 0 deletions agent0_sdk/core/a2a_summary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,155 @@ def pay_first_wrapper() -> AgentTask:
resolved.get("tenant"),
None,
)


class A2AClientFromUrl:
"""
A2A client backed directly by a URL (agent-card URL or base URL).
Resolves the A2A interface once on first use.
"""

def __init__(self, sdk: SDKLike, url: str) -> None:
self._sdk = sdk
self._url = url
self._resolved: Optional[Dict[str, Any]] = None

def _ensure_resolved(self) -> Dict[str, Any]:
if self._resolved is not None:
return self._resolved
if not self._url or not (str(self._url).startswith("http://") or str(self._url).startswith("https://")):
raise RuntimeError("A2A URL must be http or https")
self._resolved = resolve_a2a_from_endpoint_url(str(self._url))
return self._resolved

def messageA2A(
self,
content: Union[str, Dict[str, Any]],
options: Optional[MessageA2AOptions] = None,
) -> Union[MessageResponse, TaskResponse, A2APaymentRequired]:
resolved = self._ensure_resolved()
x402_deps = self._sdk.getX402RequestDeps() if hasattr(self._sdk, "getX402RequestDeps") else None
return send_message(
resolved["baseUrl"],
resolved["a2aVersion"],
content,
options=options,
auth=resolved.get("auth"),
tenant=resolved.get("tenant"),
binding=resolved.get("binding"),
x402_deps=x402_deps,
)

def listTasks(
self,
options: Optional[ListTasksOptions] = None,
) -> Union[List[TaskSummary], A2APaymentRequired]:
resolved = self._ensure_resolved()
x402_deps = self._sdk.getX402RequestDeps() if hasattr(self._sdk, "getX402RequestDeps") else None
auth_dict: Optional[Dict[str, Any]] = None
if resolved.get("auth"):
auth_dict = apply_credential((options.credential or "") if options else "", resolved["auth"])
else:
auth_dict = {"headers": {}, "queryParams": {}}
return list_tasks(
resolved["baseUrl"],
resolved["a2aVersion"],
options=options,
auth=auth_dict,
tenant=resolved.get("tenant"),
x402_deps=x402_deps,
)

def loadTask(
self,
task_id: str,
options: Optional[LoadTaskOptions] = None,
) -> Union[AgentTask, A2APaymentRequired]:
resolved = self._ensure_resolved()
x402_deps = self._sdk.getX402RequestDeps() if hasattr(self._sdk, "getX402RequestDeps") else None
resolved_auth: Optional[Dict[str, Any]] = None
if resolved.get("auth"):
resolved_auth = apply_credential((options.credential or "") if options else "", resolved["auth"])
else:
resolved_auth = {"headers": {}, "queryParams": {}}

result = get_task(
resolved["baseUrl"],
resolved["a2aVersion"],
task_id,
auth=resolved_auth,
x402_deps=x402_deps,
payment=options.payment if options else None,
tenant=resolved.get("tenant"),
)

if _is_x402_response(result):
x402_resp = result
x402_payment = getattr(x402_resp, "x402Payment", None) or x402_resp.get("x402Payment")
if not x402_payment:
raise RuntimeError("x402 response missing x402Payment")
orig_pay = getattr(x402_payment, "pay", None) or x402_payment.get("pay")
orig_pay_first = getattr(x402_payment, "pay_first", None) or x402_payment.get("pay_first")

def pay_wrapper(accept: Any = None) -> AgentTask:
summary_result = orig_pay(accept)
tid = getattr(summary_result, "taskId", None) or (summary_result.get("taskId") if isinstance(summary_result, dict) else None)
cid = getattr(summary_result, "contextId", None) or (summary_result.get("contextId") if isinstance(summary_result, dict) else "")
if not tid:
raise RuntimeError("x402 pay() did not return taskId")
return create_task_handle(
resolved["baseUrl"],
resolved["a2aVersion"],
str(tid),
str(cid or ""),
x402_deps,
resolved_auth,
resolved.get("tenant"),
None,
)

def pay_first_wrapper() -> AgentTask:
if not orig_pay_first:
raise ValueError("x402: no pay_first available")
summary_result = orig_pay_first()
tid = getattr(summary_result, "taskId", None) or (summary_result.get("taskId") if isinstance(summary_result, dict) else None)
cid = getattr(summary_result, "contextId", None) or (summary_result.get("contextId") if isinstance(summary_result, dict) else "")
if not tid:
raise RuntimeError("x402 pay_first() did not return taskId")
return create_task_handle(
resolved["baseUrl"],
resolved["a2aVersion"],
str(tid),
str(cid or ""),
x402_deps,
resolved_auth,
resolved.get("tenant"),
None,
)

wrapped_payment = X402Payment(
accepts=getattr(x402_payment, "accepts", []) or x402_payment.get("accepts", []),
pay=pay_wrapper,
x402Version=getattr(x402_payment, "x402Version", None) or x402_payment.get("x402Version"),
error=getattr(x402_payment, "error", None) or x402_payment.get("error"),
resource=getattr(x402_payment, "resource", None) or x402_payment.get("resource"),
price=getattr(x402_payment, "price", None) or x402_payment.get("price"),
token=getattr(x402_payment, "token", None) or x402_payment.get("token"),
network=getattr(x402_payment, "network", None) or x402_payment.get("network"),
pay_first=pay_first_wrapper if orig_pay_first else None,
)
return A2APaymentRequired(x402Required=True, x402Payment=wrapped_payment)

summary = result
tid = getattr(summary, "taskId", None) or (summary.get("taskId") if isinstance(summary, dict) else task_id)
cid = getattr(summary, "contextId", None) or (summary.get("contextId") if isinstance(summary, dict) else "")
return create_task_handle(
resolved["baseUrl"],
resolved["a2aVersion"],
str(tid),
str(cid or ""),
x402_deps,
resolved_auth,
resolved.get("tenant"),
None,
)
32 changes: 32 additions & 0 deletions agent0_sdk/core/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
logger = logging.getLogger(__name__)

from .transaction_handle import TransactionHandle
from .mcp_client import create_mcp_handle
from .a2a import (
MessageResponse,
TaskResponse,
Expand Down Expand Up @@ -60,6 +61,7 @@ def __init__(self, sdk: "SDK", registration_file: RegistrationFile):
self._endpoint_crawler = EndpointCrawler(timeout=5)
# Lazy A2A resolution cache (baseUrl, a2aVersion, binding, tenant, auth)
self._cached_a2a: Optional[Dict[str, Any]] = None
self._mcp_handle: Optional[Any] = None

# Read-only properties for direct access
@property
Expand Down Expand Up @@ -167,6 +169,34 @@ def mcpEndpoint(self) -> Optional[str]:
return endpoint.value
return None

@property
def mcp(self) -> Any:
"""Lazy MCP JSON-RPC handle (Streamable HTTP); uses SDK x402 deps when configured."""
if self._mcp_handle is not None:
return self._mcp_handle
endpoint = self.mcpEndpoint
if not endpoint or not (
str(endpoint).startswith("http://") or str(endpoint).startswith("https://")
):
raise RuntimeError("Agent has no MCP endpoint")
mcp_ep = next(
(
e
for e in self.registration_file.endpoints
if e.type == EndpointType.MCP
),
None,
)
version = "2025-06-18"
if mcp_ep and mcp_ep.meta.get("version") is not None:
version = str(mcp_ep.meta.get("version"))
self._mcp_handle = create_mcp_handle(
str(endpoint),
{"protocolVersion": version},
self.sdk.getX402RequestDeps(),
)
return self._mcp_handle

@property
def a2aEndpoint(self) -> Optional[str]:
"""Get A2A endpoint value (read-only)."""
Expand Down Expand Up @@ -515,6 +545,8 @@ def removeEndpoint(
]

self.registration_file.updatedAt = int(time.time())
if self.mcpEndpoint is None:
self._mcp_handle = None
return self

def removeEndpoints(self) -> 'Agent':
Expand Down
Loading