a2a-handler 0.1.26__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/PKG-INFO +48 -6
  2. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/README.md +44 -2
  3. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/pyproject.toml +11 -5
  4. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/pyproject.toml.orig +20 -5
  5. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/auth.py +98 -11
  6. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/__init__.py +4 -2
  7. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/_helpers.py +52 -0
  8. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/card.py +12 -1
  9. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/mcp.py +12 -7
  10. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/message.py +193 -10
  11. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/server.py +103 -45
  12. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/task.py +313 -11
  13. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/input_validation.py +53 -0
  14. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/mcp/server.py +243 -3
  15. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/servers.py +94 -54
  16. a2a_handler-0.3.0/src/a2a_handler/service.py +1841 -0
  17. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/app.py +10 -2
  18. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/app.tcss +37 -0
  19. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/palette.py +5 -0
  20. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/auth.py +115 -0
  21. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/input.py +45 -2
  22. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/messages.py +129 -4
  23. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/save.py +18 -4
  24. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/tab.py +376 -283
  25. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/views.py +4 -4
  26. a2a_handler-0.3.0/src/a2a_handler/turn.py +335 -0
  27. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/validation.py +44 -77
  28. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/webhook.py +80 -19
  29. a2a_handler-0.1.26/src/a2a_handler/server/__init__.py +0 -79
  30. a2a_handler-0.1.26/src/a2a_handler/server/agent.py +0 -133
  31. a2a_handler-0.1.26/src/a2a_handler/server/app.py +0 -221
  32. a2a_handler-0.1.26/src/a2a_handler/server/card.py +0 -164
  33. a2a_handler-0.1.26/src/a2a_handler/server/ollama.py +0 -75
  34. a2a_handler-0.1.26/src/a2a_handler/server/tools/__init__.py +0 -23
  35. a2a_handler-0.1.26/src/a2a_handler/server/tools/a2a_docs.py +0 -140
  36. a2a_handler-0.1.26/src/a2a_handler/server/tools/handler_docs.py +0 -44
  37. a2a_handler-0.1.26/src/a2a_handler/server/tools/source.py +0 -109
  38. a2a_handler-0.1.26/src/a2a_handler/service.py +0 -745
  39. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/LICENSE +0 -0
  40. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/__init__.py +0 -0
  41. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/schema.py +0 -0
  42. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/cli/session.py +0 -0
  43. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/__init__.py +0 -0
  44. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/config.py +0 -0
  45. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/dotenv.py +0 -0
  46. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/logging.py +0 -0
  47. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/common/output.py +0 -0
  48. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/mcp/__init__.py +0 -0
  49. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/session.py +0 -0
  50. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/__init__.py +0 -0
  51. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/__init__.py +0 -0
  52. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/commands/screens.py +0 -0
  53. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/__init__.py +0 -0
  54. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/artifacts.py +0 -0
  55. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/card.py +0 -0
  56. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/headers.py +0 -0
  57. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/logs.py +0 -0
  58. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/components/tasks.py +0 -0
  59. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/__init__.py +0 -0
  60. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/session.py +0 -0
  61. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/tabs.py +0 -0
  62. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/types.py +0 -0
  63. {a2a_handler-0.1.26 → a2a_handler-0.3.0}/src/a2a_handler/tui/server/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a2a-handler
3
- Version: 0.1.26
3
+ Version: 0.3.0
4
4
  Summary: An A2A Protocol client TUI and CLI.
5
5
  Keywords: a2a,a2a-protocol,agent,agent-to-agent,ai,cli,tui
6
6
  Author: Al Duncanson
@@ -10,7 +10,6 @@ License-File: LICENSE
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Programming Language :: Python :: 3
16
15
  Classifier: Programming Language :: Python :: 3.11
@@ -20,23 +19,24 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
19
  Classifier: Typing :: Typed
21
20
  Requires-Dist: a2a-sdk[http-server]>=1.0.0,<2.0.0
22
21
  Requires-Dist: click>=8.0.0
23
- Requires-Dist: google-adk[mcp]>=2.5.0
24
22
  Requires-Dist: httpx>=0.27.0
25
- Requires-Dist: litellm>=1.0.0
26
23
  Requires-Dist: python-dotenv>=1.0.0
27
24
  Requires-Dist: rich>=13.0.0
25
+ Requires-Dist: starlette>=0.37.0
28
26
  Requires-Dist: textual>=0.47.0
29
27
  Requires-Dist: uvicorn>=0.30.0
30
28
  Requires-Dist: textual-serve>=1.1.0
31
29
  Requires-Dist: mcp[cli]>=1.27.1,<2
32
30
  Requires-Dist: truststore>=0.9.0
33
31
  Requires-Dist: platformdirs>=4.0.0
32
+ Requires-Dist: a2a-sdk[grpc]>=1.0.0,<2.0.0 ; extra == 'grpc'
34
33
  Requires-Python: >=3.11
35
34
  Project-URL: Homepage, https://github.com/alDuncanson/handler
36
35
  Project-URL: Repository, https://github.com/alDuncanson/handler
37
36
  Project-URL: Documentation, https://github.com/alDuncanson/handler/tree/main/docs
38
37
  Project-URL: Issues, https://github.com/alDuncanson/handler/issues
39
38
  Project-URL: Changelog, https://github.com/alDuncanson/handler/releases
39
+ Provides-Extra: grpc
40
40
  Description-Content-Type: text/markdown
41
41
 
42
42
  # Handler
@@ -53,9 +53,24 @@ client for software engineers building, testing, and operating agentic systems.
53
53
  It provides an interactive TUI, a scriptable CLI with structured output, and an
54
54
  MCP server that lets other agents integrate with A2A services directly. Handler
55
55
  also supports global and repo-scoped A2A server configuration with bearer,
56
- API key, mTLS, and OAuth2 client credentials auth.
56
+ API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
57
57
 
58
- ![Handler TUI connected to the built-in Handler Agent, showing the agent card and a completed assistant response](https://raw.githubusercontent.com/alDuncanson/Handler/73915875903b60dad6e4e404aa7ed91b6d94559f/assets/tui.png)
58
+ ![Handler TUI connected to an A2A agent, showing the agent card and a completed assistant response](https://raw.githubusercontent.com/alDuncanson/Handler/73915875903b60dad6e4e404aa7ed91b6d94559f/assets/tui.png)
59
+
60
+ ## Features
61
+
62
+ - Streams replies as they arrive, in the TUI and the CLI, and can stop a
63
+ running task or answer an agent that pauses for input
64
+ - Sends text, files (inline or by URL), and structured data, and shows the
65
+ file and data parts agents send back
66
+ - Lists, inspects, cancels, and resubscribes to tasks, and manages push
67
+ notification configs with a bundled local webhook receiver
68
+ - Speaks JSON-RPC and HTTP+JSON out of the box, gRPC with the `grpc` extra,
69
+ and lets the agent card pick the transport
70
+ - Requests A2A extensions and fetches the extended card an agent offers to
71
+ authenticated clients
72
+ - Keeps credentials on the origin you configured, never on a host an agent
73
+ card happens to name
59
74
 
60
75
  ## Install
61
76
 
@@ -97,6 +112,18 @@ Send a message from the CLI:
97
112
  handler message send --url URL --text "hello"
98
113
  ```
99
114
 
115
+ Stream the reply, with a file attached:
116
+
117
+ ```bash
118
+ handler message stream --url URL --text "Review this" --file ./report.pdf
119
+ ```
120
+
121
+ List the agent's tasks:
122
+
123
+ ```bash
124
+ handler task list --url URL
125
+ ```
126
+
100
127
  Open the full documentation:
101
128
 
102
129
  ```bash
@@ -120,3 +147,18 @@ pipx run a2a-handler
120
147
  ## Documentation
121
148
 
122
149
  Read the documentation at <https://handler.alduncanson.com>.
150
+
151
+ ### For coding agents
152
+
153
+ The docs ship as an agent skill, regenerated on every deploy. Install it into
154
+ Claude Code, Cursor, Windsurf, or any agent that reads the `SKILL.md` format:
155
+
156
+ ```bash
157
+ npx skills add https://handler.alduncanson.com
158
+ ```
159
+
160
+ Or fetch it by hand from <https://handler.alduncanson.com/skill.md>. For live
161
+ lookups, agents can also use the hosted documentation MCP server at
162
+ <https://handler.alduncanson.com/mcp> or the page index at
163
+ <https://handler.alduncanson.com/llms.txt>. See the
164
+ [agent skill guide](https://handler.alduncanson.com/guides/agent-skill).
@@ -12,9 +12,24 @@ client for software engineers building, testing, and operating agentic systems.
12
12
  It provides an interactive TUI, a scriptable CLI with structured output, and an
13
13
  MCP server that lets other agents integrate with A2A services directly. Handler
14
14
  also supports global and repo-scoped A2A server configuration with bearer,
15
- API key, mTLS, and OAuth2 client credentials auth.
15
+ API key, HTTP Basic, mTLS, OAuth2 client credentials, and OpenID Connect auth.
16
16
 
17
- ![Handler TUI connected to the built-in Handler Agent, showing the agent card and a completed assistant response](https://raw.githubusercontent.com/alDuncanson/Handler/73915875903b60dad6e4e404aa7ed91b6d94559f/assets/tui.png)
17
+ ![Handler TUI connected to an A2A agent, showing the agent card and a completed assistant response](https://raw.githubusercontent.com/alDuncanson/Handler/73915875903b60dad6e4e404aa7ed91b6d94559f/assets/tui.png)
18
+
19
+ ## Features
20
+
21
+ - Streams replies as they arrive, in the TUI and the CLI, and can stop a
22
+ running task or answer an agent that pauses for input
23
+ - Sends text, files (inline or by URL), and structured data, and shows the
24
+ file and data parts agents send back
25
+ - Lists, inspects, cancels, and resubscribes to tasks, and manages push
26
+ notification configs with a bundled local webhook receiver
27
+ - Speaks JSON-RPC and HTTP+JSON out of the box, gRPC with the `grpc` extra,
28
+ and lets the agent card pick the transport
29
+ - Requests A2A extensions and fetches the extended card an agent offers to
30
+ authenticated clients
31
+ - Keeps credentials on the origin you configured, never on a host an agent
32
+ card happens to name
18
33
 
19
34
  ## Install
20
35
 
@@ -56,6 +71,18 @@ Send a message from the CLI:
56
71
  handler message send --url URL --text "hello"
57
72
  ```
58
73
 
74
+ Stream the reply, with a file attached:
75
+
76
+ ```bash
77
+ handler message stream --url URL --text "Review this" --file ./report.pdf
78
+ ```
79
+
80
+ List the agent's tasks:
81
+
82
+ ```bash
83
+ handler task list --url URL
84
+ ```
85
+
59
86
  Open the full documentation:
60
87
 
61
88
  ```bash
@@ -79,3 +106,18 @@ pipx run a2a-handler
79
106
  ## Documentation
80
107
 
81
108
  Read the documentation at <https://handler.alduncanson.com>.
109
+
110
+ ### For coding agents
111
+
112
+ The docs ship as an agent skill, regenerated on every deploy. Install it into
113
+ Claude Code, Cursor, Windsurf, or any agent that reads the `SKILL.md` format:
114
+
115
+ ```bash
116
+ npx skills add https://handler.alduncanson.com
117
+ ```
118
+
119
+ Or fetch it by hand from <https://handler.alduncanson.com/skill.md>. For live
120
+ lookups, agents can also use the hosted documentation MCP server at
121
+ <https://handler.alduncanson.com/mcp> or the page index at
122
+ <https://handler.alduncanson.com/llms.txt>. See the
123
+ [agent skill guide](https://handler.alduncanson.com/guides/agent-skill).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "a2a-handler"
3
- version = "0.1.26"
3
+ version = "0.3.0"
4
4
  description = "An A2A Protocol client TUI and CLI."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -19,7 +19,6 @@ classifiers = [
19
19
  "Development Status :: 4 - Beta",
20
20
  "Environment :: Console",
21
21
  "Intended Audience :: Developers",
22
- "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
23
22
  "Operating System :: OS Independent",
24
23
  "Programming Language :: Python :: 3",
25
24
  "Programming Language :: Python :: 3.11",
@@ -31,11 +30,10 @@ classifiers = [
31
30
  dependencies = [
32
31
  "a2a-sdk[http-server]>=1.0.0,<2.0.0",
33
32
  "click>=8.0.0",
34
- "google-adk[mcp]>=2.5.0",
35
33
  "httpx>=0.27.0",
36
- "litellm>=1.0.0",
37
34
  "python-dotenv>=1.0.0",
38
35
  "rich>=13.0.0",
36
+ "starlette>=0.37.0",
39
37
  "textual>=0.47.0",
40
38
  "uvicorn>=0.30.0",
41
39
  "textual-serve>=1.1.0",
@@ -48,6 +46,9 @@ dependencies = [
48
46
  name = "Al Duncanson"
49
47
  email = "alDuncanson@proton.me"
50
48
 
49
+ [project.optional-dependencies]
50
+ grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
51
+
51
52
  [project.urls]
52
53
  Homepage = "https://github.com/alDuncanson/handler"
53
54
  Repository = "https://github.com/alDuncanson/handler"
@@ -70,12 +71,17 @@ dev = [
70
71
  ]
71
72
 
72
73
  [build-system]
73
- requires = ["uv_build>=0.9.15,<0.10.0"]
74
+ requires = ["uv_build>=0.9.15,<0.13.0"]
74
75
  build-backend = "uv_build"
75
76
 
76
77
  [tool.uv.build-backend]
77
78
  module-root = "src"
78
79
 
80
+ [[tool.uv.index]]
81
+ name = "pypi"
82
+ url = "https://pypi.org/simple"
83
+ default = true
84
+
79
85
  [tool.pytest.ini_options]
80
86
  asyncio_mode = "auto"
81
87
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "a2a-handler"
3
- version = "0.1.26"
3
+ version = "0.3.0"
4
4
  description = "An A2A Protocol client TUI and CLI."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -22,7 +22,6 @@ classifiers = [
22
22
  "Development Status :: 4 - Beta",
23
23
  "Environment :: Console",
24
24
  "Intended Audience :: Developers",
25
- "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
26
25
  "Operating System :: OS Independent",
27
26
  "Programming Language :: Python :: 3",
28
27
  "Programming Language :: Python :: 3.11",
@@ -34,11 +33,12 @@ classifiers = [
34
33
  dependencies = [
35
34
  "a2a-sdk[http-server]>=1.0.0,<2.0.0",
36
35
  "click>=8.0.0",
37
- "google-adk[mcp]>=2.5.0",
38
36
  "httpx>=0.27.0",
39
- "litellm>=1.0.0",
40
37
  "python-dotenv>=1.0.0",
41
38
  "rich>=13.0.0",
39
+ # Imported directly by the push notification webhook server. Declared rather
40
+ # than relied upon transitively via a2a-sdk[http-server].
41
+ "starlette>=0.37.0",
42
42
  "textual>=0.47.0",
43
43
  "uvicorn>=0.30.0",
44
44
  "textual-serve>=1.1.0",
@@ -47,6 +47,12 @@ dependencies = [
47
47
  "platformdirs>=4.0.0",
48
48
  ]
49
49
 
50
+ [project.optional-dependencies]
51
+ # Talking to gRPC-only agents needs the SDK's gRPC transport and its
52
+ # grpcio dependencies, which are heavy enough to keep out of the default
53
+ # install.
54
+ grpc = ["a2a-sdk[grpc]>=1.0.0,<2.0.0"]
55
+
50
56
  [dependency-groups]
51
57
  dev = [
52
58
  "ruff>=0.8.0",
@@ -69,12 +75,21 @@ Changelog = "https://github.com/alDuncanson/handler/releases"
69
75
  handler = "a2a_handler.cli:main"
70
76
 
71
77
  [build-system]
72
- requires = ["uv_build>=0.9.15,<0.10.0"]
78
+ requires = ["uv_build>=0.9.15,<0.13.0"]
73
79
  build-backend = "uv_build"
74
80
 
75
81
  [tool.uv.build-backend]
76
82
  module-root = "src"
77
83
 
84
+ # This is a public package published to PyPI, so its lockfile must resolve from
85
+ # pypi.org. Pinned explicitly because an ambient UV_INDEX_URL (e.g. a corporate
86
+ # mirror) would otherwise rewrite every source URL in uv.lock on the next
87
+ # `uv lock`, leaking internal infrastructure and breaking CI for contributors.
88
+ [[tool.uv.index]]
89
+ name = "pypi"
90
+ url = "https://pypi.org/simple"
91
+ default = true
92
+
78
93
  [tool.pytest.ini_options]
79
94
  asyncio_mode = "auto"
80
95
 
@@ -1,12 +1,13 @@
1
1
  """Authentication support for A2A protocol.
2
2
 
3
3
  Handles credential storage and HTTP authentication header generation.
4
- Supports API key, HTTP bearer, mTLS (mutual TLS), and OAuth2 client credentials
5
- authentication schemes.
4
+ Supports API key, HTTP bearer, HTTP basic, mTLS (mutual TLS), OAuth2 client
5
+ credentials, and OpenID Connect authentication schemes.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import base64
10
11
  import ssl
11
12
  import time
12
13
  from dataclasses import dataclass, field
@@ -30,9 +31,18 @@ class AuthType(str, Enum):
30
31
  """Supported authentication types."""
31
32
 
32
33
  API_KEY = "api_key"
34
+ BASIC = "basic"
33
35
  BEARER = "bearer"
34
36
  MTLS = "mtls"
35
37
  OAUTH2 = "oauth2"
38
+ OIDC = "oidc"
39
+
40
+
41
+ #: Auth types whose access token is fetched from a token endpoint and cached.
42
+ TOKEN_FETCHING_AUTH_TYPES = {AuthType.OAUTH2, AuthType.OIDC}
43
+
44
+ #: Path of the OpenID Connect discovery document below the issuer URL.
45
+ OIDC_DISCOVERY_PATH = "/.well-known/openid-configuration"
36
46
 
37
47
 
38
48
  @dataclass
@@ -45,13 +55,15 @@ class AuthCredentials:
45
55
  auth_type: AuthType
46
56
  value: str = ""
47
57
  header_name: str | None = None # For API key: custom header name
58
+ username: str | None = None # For HTTP basic: username (value is password)
48
59
  cert_path: str | None = None # For mTLS: client certificate path
49
60
  key_path: str | None = None # For mTLS: client private key path
50
61
  ca_cert_path: str | None = None # For mTLS: CA certificate path
51
- token_url: str | None = None # For OAuth2: token endpoint
52
- client_id: str | None = None # For OAuth2: client ID
53
- client_secret: str | None = None # For OAuth2: client secret
54
- scopes: list[str] | None = None # For OAuth2: optional scopes
62
+ token_url: str | None = None # For OAuth2/OIDC: token endpoint
63
+ issuer_url: str | None = None # For OIDC: issuer to discover endpoints from
64
+ client_id: str | None = None # For OAuth2/OIDC: client ID
65
+ client_secret: str | None = None # For OAuth2/OIDC: client secret
66
+ scopes: list[str] | None = None # For OAuth2/OIDC: optional scopes
55
67
  custom_headers: dict[str, str] | None = None # Additional headers for any auth type
56
68
  _token_expires_at: float | None = field(default=None, repr=False)
57
69
 
@@ -64,8 +76,9 @@ class AuthCredentials:
64
76
  "key_path": self.key_path,
65
77
  "ca_cert_path": self.ca_cert_path,
66
78
  "token_url": self.token_url,
79
+ "issuer_url": self.issuer_url,
67
80
  }
68
- for secret_field in ("value", "client_id", "client_secret"):
81
+ for secret_field in ("value", "username", "client_id", "client_secret"):
69
82
  val = getattr(self, secret_field)
70
83
  redacted[secret_field] = "***" if val else None
71
84
  if self.custom_headers:
@@ -83,8 +96,12 @@ class AuthCredentials:
83
96
  headers.update(self.custom_headers)
84
97
  if self.auth_type == AuthType.BEARER and self.value:
85
98
  headers["Authorization"] = f"Bearer {self.value}"
86
- elif self.auth_type == AuthType.OAUTH2 and self.value:
99
+ elif self.auth_type in TOKEN_FETCHING_AUTH_TYPES and self.value:
87
100
  headers["Authorization"] = f"Bearer {self.value}"
101
+ elif self.auth_type == AuthType.BASIC and self.username is not None:
102
+ userpass = f"{self.username}:{self.value}".encode()
103
+ encoded = base64.b64encode(userpass).decode("ascii")
104
+ headers["Authorization"] = f"Basic {encoded}"
88
105
  elif self.auth_type == AuthType.API_KEY:
89
106
  header = self.header_name or "X-API-Key"
90
107
  headers[header] = self.value
@@ -122,8 +139,39 @@ class AuthCredentials:
122
139
  self.value = ""
123
140
  self._token_expires_at = None
124
141
 
142
+ async def discover_oidc_token_endpoint(self) -> str:
143
+ """Resolve the token endpoint from the OIDC discovery document.
144
+
145
+ Fetches ``<issuer>/.well-known/openid-configuration`` and caches the
146
+ advertised ``token_endpoint`` on ``token_url``.
147
+ """
148
+ if self.auth_type != AuthType.OIDC:
149
+ raise ValueError("OIDC discovery is only valid for OIDC credentials")
150
+ if self.token_url:
151
+ return self.token_url
152
+ if not self.issuer_url:
153
+ raise ValueError("issuer_url is required for OIDC discovery")
154
+ validate_token_url(self.issuer_url)
155
+
156
+ discovery_url = self.issuer_url.rstrip("/") + OIDC_DISCOVERY_PATH
157
+ async with httpx.AsyncClient(trust_env=False) as discovery_client:
158
+ response = await discovery_client.get(discovery_url)
159
+ response.raise_for_status()
160
+ document = response.json()
161
+ token_endpoint = document.get("token_endpoint")
162
+ if not isinstance(token_endpoint, str) or not token_endpoint:
163
+ raise ValueError(
164
+ f"OIDC discovery document at {discovery_url} has no token_endpoint"
165
+ )
166
+ validate_token_url(token_endpoint)
167
+ self.token_url = token_endpoint
168
+ return token_endpoint
169
+
125
170
  async def fetch_oauth2_token(self) -> str:
126
- """Fetch an access token using OAuth2 client credentials grant.
171
+ """Fetch an access token using the client credentials grant.
172
+
173
+ Valid for OAuth2 and OIDC credentials; OIDC resolves its token
174
+ endpoint through the issuer's discovery document first.
127
175
 
128
176
  Uses a short-lived HTTP client so that agent auth headers and custom
129
177
  headers on the main client are never sent to the token endpoint.
@@ -131,8 +179,10 @@ class AuthCredentials:
131
179
  Updates self.value with the new token and returns it.
132
180
  Parses ``expires_in`` from the token response to track expiry.
133
181
  """
134
- if self.auth_type != AuthType.OAUTH2:
135
- raise ValueError("Token fetch is only valid for OAuth2 credentials")
182
+ if self.auth_type not in TOKEN_FETCHING_AUTH_TYPES:
183
+ raise ValueError("Token fetch is only valid for OAuth2 or OIDC credentials")
184
+ if self.auth_type == AuthType.OIDC:
185
+ await self.discover_oidc_token_endpoint()
136
186
  if not self.token_url or not self.client_id or not self.client_secret:
137
187
  raise ValueError("token_url, client_id, and client_secret are required")
138
188
  validate_token_url(self.token_url)
@@ -168,6 +218,10 @@ class AuthCredentials:
168
218
  "value": self.value,
169
219
  "header_name": self.header_name,
170
220
  }
221
+ if self.username is not None:
222
+ data["username"] = self.username
223
+ if self.issuer_url:
224
+ data["issuer_url"] = self.issuer_url
171
225
  if self.cert_path:
172
226
  data["cert_path"] = self.cert_path
173
227
  if self.key_path:
@@ -197,6 +251,8 @@ class AuthCredentials:
197
251
  auth_type=AuthType(data["auth_type"]),
198
252
  value=data.get("value") or "",
199
253
  header_name=data.get("header_name"),
254
+ username=data.get("username"),
255
+ issuer_url=data.get("issuer_url"),
200
256
  cert_path=data.get("cert_path"),
201
257
  key_path=data.get("key_path"),
202
258
  ca_cert_path=data.get("ca_cert_path"),
@@ -226,6 +282,15 @@ def create_bearer_auth(token: str) -> AuthCredentials:
226
282
  return AuthCredentials(auth_type=AuthType.BEARER, value=token)
227
283
 
228
284
 
285
+ def create_basic_auth(username: str, password: str) -> AuthCredentials:
286
+ """Create HTTP basic authentication."""
287
+ return AuthCredentials(
288
+ auth_type=AuthType.BASIC,
289
+ username=username,
290
+ value=password,
291
+ )
292
+
293
+
229
294
  def create_api_key_auth(
230
295
  key: str,
231
296
  header_name: str = "X-API-Key",
@@ -286,3 +351,25 @@ def create_oauth2_auth(
286
351
  client_secret=client_secret,
287
352
  scopes=scopes,
288
353
  )
354
+
355
+
356
+ def create_oidc_auth(
357
+ issuer_url: str,
358
+ client_id: str,
359
+ client_secret: str,
360
+ scopes: list[str] | None = None,
361
+ access_token: str = "",
362
+ ) -> AuthCredentials:
363
+ """Create OpenID Connect authentication.
364
+
365
+ The token endpoint is discovered from the issuer's
366
+ ``/.well-known/openid-configuration`` document on first token fetch.
367
+ """
368
+ return AuthCredentials(
369
+ auth_type=AuthType.OIDC,
370
+ value=access_token,
371
+ issuer_url=issuer_url,
372
+ client_id=client_id,
373
+ client_secret=client_secret,
374
+ scopes=scopes,
375
+ )
@@ -6,7 +6,7 @@ Provides commands for interacting with A2A agents:
6
6
  - task notification set: Configure push notifications
7
7
  - card get/validate: Agent card operations
8
8
  - server list/show/add/remove/validate: Manage configured servers
9
- - server run agent/push: Run local servers
9
+ - server run push: Run the local push notification webhook server
10
10
  - session list/show/clear: Manage saved conversation sessions
11
11
 
12
12
  Authentication is configured per-server via `handler server add` or
@@ -151,8 +151,10 @@ def cli(
151
151
  setup_logging(level="DEBUG")
152
152
  elif verbose:
153
153
  setup_logging(level="INFO")
154
- else:
154
+ elif quiet:
155
155
  setup_logging(level="ERROR")
156
+ else:
157
+ setup_logging(level="WARNING")
156
158
 
157
159
 
158
160
  cli.add_command(message)
@@ -11,6 +11,13 @@ from a2a.client.errors import (
11
11
  A2AClientTimeoutError,
12
12
  AgentCardResolutionError,
13
13
  )
14
+ from a2a.utils.errors import (
15
+ A2AError,
16
+ MethodNotFoundError,
17
+ TaskNotCancelableError,
18
+ TaskNotFoundError,
19
+ UnsupportedOperationError,
20
+ )
14
21
 
15
22
  from a2a_handler.auth import (
16
23
  AuthCredentials,
@@ -172,6 +179,14 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
172
179
  log.error("A2A client error: %s", e)
173
180
  message = str(e)
174
181
  error_code = "a2a_client_error"
182
+ elif isinstance(e, A2AError):
183
+ # The agent answered with a protocol error. That is a normal outcome
184
+ # with a clear meaning, not a crash, so no traceback.
185
+ log.error("Agent returned an error: %s", e)
186
+ message = str(e)
187
+ error_code = "a2a_error"
188
+ details = {"error": type(e).__name__, "agent_url": agent_url}
189
+ suggestion = _protocol_error_suggestion(e)
175
190
  elif isinstance(e, httpx.ConnectError):
176
191
  log.error("Connection refused to %s", agent_url)
177
192
  message = f"Connection refused: Is the server running at {agent_url}?"
@@ -206,6 +221,20 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
206
221
  print(f"Error [{error_code}]: {message}", file=sys.stderr)
207
222
 
208
223
 
224
+ def _protocol_error_suggestion(error: A2AError) -> str | None:
225
+ """A next step for the protocol errors a user can act on."""
226
+ if isinstance(error, TaskNotCancelableError):
227
+ return (
228
+ "The task has probably already finished; `handler task get` shows "
229
+ "its current state"
230
+ )
231
+ if isinstance(error, TaskNotFoundError):
232
+ return "Check the task ID; the agent may have discarded the task"
233
+ if isinstance(error, (UnsupportedOperationError, MethodNotFoundError)):
234
+ return "This agent does not implement that operation"
235
+ return None
236
+
237
+
209
238
  def _resolve_env_secret(env_var: str, label: str) -> str:
210
239
  """Read a secret from an environment variable."""
211
240
  value = os.environ.get(env_var)
@@ -292,6 +321,29 @@ def resolve_agent_target(
292
321
  return selection.agent_url, credentials
293
322
 
294
323
 
324
+ def resolve_task_id(positional: str | None, option: str | None) -> str | None:
325
+ """Resolve a task ID given either positionally or via --task.
326
+
327
+ Returns None when neither is supplied, because `--params` may still carry a
328
+ task_id; callers check for a missing ID after merging that payload.
329
+ """
330
+ if positional is not None and option is not None and positional != option:
331
+ raise click.UsageError(
332
+ "Conflicting task IDs: "
333
+ f"{positional!r} was given positionally and {option!r} with --task."
334
+ )
335
+ return positional if positional is not None else option
336
+
337
+
338
+ def require_task_id(task_id: str | None) -> str:
339
+ """Return the resolved task ID, or fail with usage guidance when absent."""
340
+ if not task_id:
341
+ raise click.UsageError(
342
+ "Missing task ID. Pass it positionally (e.g. `task-123`) or with --task."
343
+ )
344
+ return task_id
345
+
346
+
295
347
  def handle_validation_error(error: InputValidationError, output: Output) -> None:
296
348
  """Render input validation errors in the standard envelope."""
297
349
  output.error(
@@ -35,6 +35,11 @@ def card() -> None:
35
35
  @card.command("get")
36
36
  @click.option("--url", "agent_url", help="Agent URL")
37
37
  @click.option("--server", "-s", "server_name", help="Named server from servers.toml")
38
+ @click.option(
39
+ "--extended",
40
+ is_flag=True,
41
+ help="Fetch the extended card offered to authenticated clients",
42
+ )
38
43
  @click.option(
39
44
  "--bearer-env", "-b", help="Env var containing bearer token (overrides saved)"
40
45
  )
@@ -44,6 +49,7 @@ def card() -> None:
44
49
  def card_get(
45
50
  agent_url: Optional[str],
46
51
  server_name: Optional[str],
52
+ extended: bool,
47
53
  bearer_env: Optional[str],
48
54
  api_key_env: Optional[str],
49
55
  ) -> None:
@@ -54,6 +60,7 @@ def card_get(
54
60
  $ handler card get --server my_agent
55
61
  $ handler card get --url http://localhost:8000
56
62
  $ handler card get --url http://localhost:8000 --bearer-env MY_TOKEN
63
+ $ handler card get --url http://localhost:8000 --extended --bearer-env MY_TOKEN
57
64
  """
58
65
  output = Output()
59
66
 
@@ -74,7 +81,10 @@ def card_get(
74
81
  try:
75
82
  async with build_http_client(credentials=credentials) as http_client:
76
83
  service = A2AService(http_client, resolved_url, credentials=credentials)
77
- card_data = await service.get_card()
84
+ if extended:
85
+ card_data = await service.get_extended_card()
86
+ else:
87
+ card_data = await service.get_card()
78
88
  log.info("Retrieved card for agent: %s", card_data.name)
79
89
 
80
90
  _format_agent_card(card_data, output)
@@ -162,6 +172,7 @@ def card_validate(
162
172
  source=resolved_url,
163
173
  source_type=ValidationSource.URL,
164
174
  agent_card=agent_card,
175
+ raw_data=service.raw_card,
165
176
  )
166
177
 
167
178
  _format_validation_result(result, output)
@@ -38,7 +38,7 @@ def mcp(transport: TransportType) -> None:
38
38
  \b
39
39
  Card Tools:
40
40
  - validate_agent_card: Validate an agent card from URL or file
41
- - get_agent_card: Retrieve an agent's full card details
41
+ - get_agent_card: Retrieve an agent's public or extended card
42
42
 
43
43
  \b
44
44
  Message Tools:
@@ -47,9 +47,15 @@ def mcp(transport: TransportType) -> None:
47
47
  \b
48
48
  Task Tools:
49
49
  - get_task: Get task status and details
50
+ - list_tasks: List an agent's tasks, following pagination to the end
50
51
  - cancel_task: Cancel a running task
51
- - set_task_notification: Configure push notification webhooks
52
- - get_task_notification: Get push notification config
52
+
53
+ \b
54
+ Push Notification Tools:
55
+ - set_task_notification: Configure a push notification webhook
56
+ - get_task_notification: Get a task's push notification config
57
+ - list_task_notifications: List every push notification config for a task
58
+ - delete_task_notification: Delete a push notification config
53
59
 
54
60
  \b
55
61
  Session Tools:
@@ -57,10 +63,9 @@ def mcp(transport: TransportType) -> None:
57
63
  - get_session_info: Get session for a specific agent
58
64
  - clear_session_data: Clear saved session state
59
65
 
60
- \b
61
- Auth Tools:
62
- - set_agent_credentials: Save bearer token or API key
63
- - clear_agent_credentials: Remove saved credentials
66
+ Every tool that talks to an agent accepts bearer_token, api_key, mTLS
67
+ paths, and custom_headers; credentials are only sent to the origin of
68
+ the given agent URL.
64
69
 
65
70
  Example configuration for Claude Desktop (claude_desktop_config.json):
66
71