a2a-handler 0.1.21__tar.gz → 0.1.23__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.21 → a2a_handler-0.1.23}/PKG-INFO +42 -37
  2. a2a_handler-0.1.23/README.md +81 -0
  3. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/pyproject.toml +3 -3
  4. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/auth.py +4 -0
  5. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/__init__.py +139 -5
  6. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/_helpers.py +102 -3
  7. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/card.py +9 -2
  8. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/message.py +285 -14
  9. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/schema.py +3 -4
  10. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/server.py +2 -2
  11. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/session.py +8 -5
  12. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/task.py +4 -1
  13. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/logging.py +35 -13
  14. a2a_handler-0.1.23/src/a2a_handler/common/output.py +113 -0
  15. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/__init__.py +1 -1
  16. a2a_handler-0.1.23/src/a2a_handler/server/agent.py +133 -0
  17. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/card.py +58 -3
  18. a2a_handler-0.1.23/src/a2a_handler/server/tools/__init__.py +23 -0
  19. a2a_handler-0.1.23/src/a2a_handler/server/tools/a2a_docs.py +140 -0
  20. a2a_handler-0.1.23/src/a2a_handler/server/tools/handler_docs.py +44 -0
  21. a2a_handler-0.1.23/src/a2a_handler/server/tools/source.py +109 -0
  22. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/servers.py +35 -0
  23. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/service.py +3 -1
  24. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/app.py +48 -10
  25. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/app.tcss +154 -12
  26. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/artifacts.py +6 -19
  27. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/card.py +6 -4
  28. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/input.py +29 -1
  29. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/messages.py +188 -19
  30. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/tasks.py +99 -32
  31. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/tab.py +199 -0
  32. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/views.py +13 -4
  33. a2a_handler-0.1.21/README.md +0 -76
  34. a2a_handler-0.1.21/src/a2a_handler/common/output.py +0 -66
  35. a2a_handler-0.1.21/src/a2a_handler/server/agent.py +0 -76
  36. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/LICENSE +0 -0
  37. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/__init__.py +0 -0
  38. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/cli/mcp.py +0 -0
  39. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/__init__.py +0 -0
  40. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/config.py +0 -0
  41. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/dotenv.py +0 -0
  42. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/common/input_validation.py +0 -0
  43. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/mcp/__init__.py +0 -0
  44. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/mcp/server.py +0 -0
  45. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/app.py +0 -0
  46. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/server/ollama.py +0 -0
  47. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/session.py +0 -0
  48. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/__init__.py +0 -0
  49. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/commands/__init__.py +0 -0
  50. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/commands/palette.py +0 -0
  51. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/commands/screens.py +0 -0
  52. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/__init__.py +0 -0
  53. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/auth.py +0 -0
  54. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/headers.py +0 -0
  55. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/components/logs.py +0 -0
  56. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/__init__.py +0 -0
  57. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/save.py +0 -0
  58. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/session.py +0 -0
  59. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/tabs.py +0 -0
  60. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/types.py +0 -0
  61. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/tui/server/workspace.py +0 -0
  62. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/validation.py +0 -0
  63. {a2a_handler-0.1.21 → a2a_handler-0.1.23}/src/a2a_handler/webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a2a-handler
3
- Version: 0.1.21
3
+ Version: 0.1.23
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
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Dist: a2a-sdk>=0.3.24,<1.0.0
22
22
  Requires-Dist: click>=8.0.0
23
- Requires-Dist: google-adk>=1.26.0
23
+ Requires-Dist: google-adk>=2.0.0
24
24
  Requires-Dist: httpx>=0.27.0
25
25
  Requires-Dist: litellm>=1.0.0
26
26
  Requires-Dist: python-dotenv>=1.0.0
@@ -28,7 +28,7 @@ Requires-Dist: rich>=13.0.0
28
28
  Requires-Dist: textual>=0.47.0
29
29
  Requires-Dist: uvicorn>=0.30.0
30
30
  Requires-Dist: textual-serve>=1.1.0
31
- Requires-Dist: mcp[cli]>=1.2.0
31
+ Requires-Dist: mcp[cli]>=1.27.1
32
32
  Requires-Dist: truststore>=0.9.0
33
33
  Requires-Dist: platformdirs>=4.0.0
34
34
  Requires-Python: >=3.11
@@ -45,73 +45,78 @@ Description-Content-Type: text/markdown
45
45
  [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v0.3.0-blue)](https://a2a-protocol.org/latest/)
46
46
  [![PyPI version](https://img.shields.io/pypi/v/a2a-handler)](https://pypi.org/project/a2a-handler/)
47
47
  [![PyPI - Status](https://img.shields.io/pypi/status/a2a-handler)](https://pypi.org/project/a2a-handler/)
48
- [![PyPI monthly downloads](https://img.shields.io/pypi/dm/a2a-handler)](https://pypi.org/project/a2a-handler/)
49
48
  [![Pepy total downloads](https://img.shields.io/pepy/dt/a2a-handler?label=total%20downloads)](https://pepy.tech/projects/a2a-handler)
50
49
  [![GitHub stars](https://img.shields.io/github/stars/alDuncanson/handler)](https://github.com/alDuncanson/handler/stargazers)
51
50
 
52
- ![Handler TUI](https://github.com/alDuncanson/Handler/blob/main/assets/handler-tui.png?raw=true)
51
+ Handler is an open-source [A2A protocol](https://github.com/a2aproject/A2A)
52
+ client for software engineers building, testing, and operating agentic systems.
53
+ It provides an interactive TUI, a scriptable CLI with structured output, and an
54
+ MCP server that lets other agents integrate with A2A services directly. Handler
55
+ also supports global and repo-scoped A2A server configuration with bearer,
56
+ API key, mTLS, and OAuth2 client credentials auth.
53
57
 
54
- Handler is an open-source [A2A protocol](https://github.com/a2aproject/A2A) client for your terminal.
55
-
56
- Use the TUI or CLI to send messages, manage tasks, and inspect agent cards.
57
- An included MCP server lets your AI assistant talk to A2A agents too.
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
59
 
59
60
  ## Install
60
61
 
62
+ Install Handler from the [PyPI package](https://pypi.org/project/a2a-handler/) as a `uv` tool:
63
+
61
64
  ```bash
62
65
  uv tool install a2a-handler
63
66
  ```
64
67
 
65
- Or with [pipx](https://pipx.pypa.io/) / pip:
68
+ Or with [pipx](https://pipx.pypa.io/):
66
69
 
67
70
  ```bash
68
- pipx install a2a-handler # or: pip install a2a-handler
71
+ pipx install a2a-handler
69
72
  ```
70
73
 
71
- ## Quick Start
74
+ Or with pip:
72
75
 
73
76
  ```bash
74
- handler tui # launch the interactive TUI
75
- handler message send URL "hello" # send a message from the CLI
76
- handler card get URL # fetch an agent card
77
- handler mcp # start the MCP server
78
- handler server run agent # run the bundled reference agent
77
+ pip install a2a-handler
79
78
  ```
80
79
 
81
- ## Run Without Installing
80
+ ## Quick Start
81
+
82
+ Open the interactive terminal UI:
82
83
 
83
84
  ```bash
84
- uvx --from a2a-handler handler # or: pipx run a2a-handler
85
+ handler tui
85
86
  ```
86
87
 
87
- ## Servers
88
+ Inspect an A2A server's agent card:
89
+
90
+ ```bash
91
+ handler card get --url http://localhost:8000
92
+ ```
88
93
 
89
- Handler supports named servers in `$XDG_CONFIG_HOME/handler/servers.toml`
90
- (global) and `.handler/servers.toml` (repo-local).
94
+ Send a message from the CLI:
91
95
 
92
- ```toml
93
- version = 1
96
+ ```bash
97
+ handler message send --url URL --text "hello"
98
+ ```
94
99
 
95
- [servers.local]
96
- url = "http://localhost:8000"
100
+ Open the full documentation:
97
101
 
98
- [servers.local.auth]
99
- type = "bearer"
100
- env = "HANDLER_LOCAL_TOKEN"
102
+ ```bash
103
+ handler docs
101
104
  ```
102
105
 
103
- See [docs/](docs) for the full auth reference covering bearer, API key, mTLS,
104
- and OAuth2.
106
+ ## Run Without Installing
107
+
108
+ Run Handler with `uvx`:
105
109
 
106
- ## Development
110
+ ```bash
111
+ uvx --from a2a-handler handler
112
+ ```
107
113
 
108
- A [hermetic](https://zero-to-nix.com/concepts/hermeticity/) dev environment
109
- is available via [Nix](https://zero-to-nix.com/concepts/nix/):
114
+ Run Handler with `pipx`:
110
115
 
111
116
  ```bash
112
- nix develop
117
+ pipx run a2a-handler
113
118
  ```
114
119
 
115
- The Mintlify docs source lives in `docs/`.
120
+ ## Documentation
116
121
 
117
- See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
122
+ Read the documentation at <https://handler.alduncanson.com>.
@@ -0,0 +1,81 @@
1
+ # Handler
2
+
3
+ [![CI](https://github.com/alDuncanson/handler/actions/workflows/ci.yml/badge.svg)](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
4
+ [![A2A Protocol](https://img.shields.io/badge/A2A_Protocol-v0.3.0-blue)](https://a2a-protocol.org/latest/)
5
+ [![PyPI version](https://img.shields.io/pypi/v/a2a-handler)](https://pypi.org/project/a2a-handler/)
6
+ [![PyPI - Status](https://img.shields.io/pypi/status/a2a-handler)](https://pypi.org/project/a2a-handler/)
7
+ [![Pepy total downloads](https://img.shields.io/pepy/dt/a2a-handler?label=total%20downloads)](https://pepy.tech/projects/a2a-handler)
8
+ [![GitHub stars](https://img.shields.io/github/stars/alDuncanson/handler)](https://github.com/alDuncanson/handler/stargazers)
9
+
10
+ Handler is an open-source [A2A protocol](https://github.com/a2aproject/A2A)
11
+ client for software engineers building, testing, and operating agentic systems.
12
+ It provides an interactive TUI, a scriptable CLI with structured output, and an
13
+ MCP server that lets other agents integrate with A2A services directly. Handler
14
+ also supports global and repo-scoped A2A server configuration with bearer,
15
+ API key, mTLS, and OAuth2 client credentials auth.
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)
18
+
19
+ ## Install
20
+
21
+ Install Handler from the [PyPI package](https://pypi.org/project/a2a-handler/) as a `uv` tool:
22
+
23
+ ```bash
24
+ uv tool install a2a-handler
25
+ ```
26
+
27
+ Or with [pipx](https://pipx.pypa.io/):
28
+
29
+ ```bash
30
+ pipx install a2a-handler
31
+ ```
32
+
33
+ Or with pip:
34
+
35
+ ```bash
36
+ pip install a2a-handler
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ Open the interactive terminal UI:
42
+
43
+ ```bash
44
+ handler tui
45
+ ```
46
+
47
+ Inspect an A2A server's agent card:
48
+
49
+ ```bash
50
+ handler card get --url http://localhost:8000
51
+ ```
52
+
53
+ Send a message from the CLI:
54
+
55
+ ```bash
56
+ handler message send --url URL --text "hello"
57
+ ```
58
+
59
+ Open the full documentation:
60
+
61
+ ```bash
62
+ handler docs
63
+ ```
64
+
65
+ ## Run Without Installing
66
+
67
+ Run Handler with `uvx`:
68
+
69
+ ```bash
70
+ uvx --from a2a-handler handler
71
+ ```
72
+
73
+ Run Handler with `pipx`:
74
+
75
+ ```bash
76
+ pipx run a2a-handler
77
+ ```
78
+
79
+ ## Documentation
80
+
81
+ Read the documentation at <https://handler.alduncanson.com>.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "a2a-handler"
3
- version = "0.1.21"
3
+ version = "0.1.23"
4
4
  description = "An A2A Protocol client TUI and CLI."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -34,7 +34,7 @@ classifiers = [
34
34
  dependencies = [
35
35
  "a2a-sdk>=0.3.24,<1.0.0",
36
36
  "click>=8.0.0",
37
- "google-adk>=1.26.0",
37
+ "google-adk>=2.0.0",
38
38
  "httpx>=0.27.0",
39
39
  "litellm>=1.0.0",
40
40
  "python-dotenv>=1.0.0",
@@ -42,7 +42,7 @@ dependencies = [
42
42
  "textual>=0.47.0",
43
43
  "uvicorn>=0.30.0",
44
44
  "textual-serve>=1.1.0",
45
- "mcp[cli]>=1.2.0",
45
+ "mcp[cli]>=1.27.1",
46
46
  "truststore>=0.9.0",
47
47
  "platformdirs>=4.0.0",
48
48
  ]
@@ -250,6 +250,10 @@ def create_mtls_auth(
250
250
  ca_cert_path: str | None = None,
251
251
  ) -> AuthCredentials:
252
252
  """Create mTLS (mutual TLS) client certificate authentication."""
253
+ cert_path = str(Path(cert_path).expanduser())
254
+ key_path = str(Path(key_path).expanduser())
255
+ ca_cert_path = str(Path(ca_cert_path).expanduser()) if ca_cert_path else None
256
+
253
257
  if not Path(cert_path).is_file():
254
258
  raise FileNotFoundError(f"Client certificate not found: {cert_path}")
255
259
  if not Path(key_path).is_file():
@@ -19,6 +19,11 @@ truststore.inject_into_ssl()
19
19
 
20
20
  import logging
21
21
  import os
22
+ import shutil
23
+ import subprocess
24
+ import webbrowser
25
+ from collections.abc import Sequence
26
+ from typing import Any
22
27
 
23
28
  logging.getLogger().setLevel(logging.WARNING)
24
29
 
@@ -30,6 +35,7 @@ from a2a_handler.common.dotenv import load_runtime_dotenv
30
35
  from a2a_handler.common.output import OutputFormat
31
36
  from a2a_handler.tui import HandlerTUI
32
37
 
38
+ from ._helpers import configure_http_timeouts
33
39
  from .card import card
34
40
  from .mcp import mcp
35
41
  from .message import message
@@ -40,24 +46,81 @@ from .task import task
40
46
 
41
47
  log = get_logger(__name__)
42
48
 
49
+ DOCS_URL = "https://handler.alduncanson.com/"
50
+ PACKAGE_NAME = "a2a-handler"
43
51
 
44
- @click.group()
52
+
53
+ class RuntimeDotenvGroup(click.Group):
54
+ """Click group that loads workspace .env before option envvars resolve."""
55
+
56
+ def main(
57
+ self,
58
+ args: Sequence[str] | None = None,
59
+ prog_name: str | None = None,
60
+ complete_var: str | None = None,
61
+ standalone_mode: bool = True,
62
+ windows_expand_args: bool = True,
63
+ **extra: Any,
64
+ ) -> Any:
65
+ """Load runtime dotenv before Click parses envvar-backed options."""
66
+ load_runtime_dotenv()
67
+ return super().main(
68
+ args=args,
69
+ prog_name=prog_name,
70
+ complete_var=complete_var,
71
+ standalone_mode=standalone_mode,
72
+ windows_expand_args=windows_expand_args,
73
+ **extra,
74
+ )
75
+
76
+
77
+ @click.group(cls=RuntimeDotenvGroup)
45
78
  @click.version_option(version=__version__, prog_name="handler")
46
79
  @click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
47
80
  @click.option("--debug", "-d", is_flag=True, help="Enable debug logging")
48
81
  @click.option(
49
82
  "--output",
50
83
  "output_format",
51
- type=click.Choice(["json", "ndjson"]),
52
- default="json",
84
+ type=click.Choice(["text", "json", "ndjson"]),
85
+ default="text",
53
86
  show_default=True,
54
- help="Output format",
87
+ help="Output format (use json/ndjson for structured output)",
55
88
  )
56
89
  @click.option(
57
90
  "--quiet",
58
91
  is_flag=True,
59
92
  help="Suppress non-error output",
60
93
  )
94
+ @click.option(
95
+ "--connect-timeout",
96
+ envvar="HANDLER_CONNECT_TIMEOUT",
97
+ metavar="SECONDS|none",
98
+ help="HTTP connect timeout in seconds (env: HANDLER_CONNECT_TIMEOUT)",
99
+ )
100
+ @click.option(
101
+ "--read-timeout",
102
+ envvar="HANDLER_READ_TIMEOUT",
103
+ metavar="SECONDS|none",
104
+ help="HTTP read timeout for non-streaming calls (env: HANDLER_READ_TIMEOUT)",
105
+ )
106
+ @click.option(
107
+ "--write-timeout",
108
+ envvar="HANDLER_WRITE_TIMEOUT",
109
+ metavar="SECONDS|none",
110
+ help="HTTP write timeout in seconds (env: HANDLER_WRITE_TIMEOUT)",
111
+ )
112
+ @click.option(
113
+ "--pool-timeout",
114
+ envvar="HANDLER_POOL_TIMEOUT",
115
+ metavar="SECONDS|none",
116
+ help="HTTP connection pool timeout in seconds (env: HANDLER_POOL_TIMEOUT)",
117
+ )
118
+ @click.option(
119
+ "--stream-read-timeout",
120
+ envvar="HANDLER_STREAM_READ_TIMEOUT",
121
+ metavar="SECONDS|none",
122
+ help="HTTP read timeout for streaming calls; default is none (env: HANDLER_STREAM_READ_TIMEOUT)",
123
+ )
61
124
  @click.pass_context
62
125
  def cli(
63
126
  ctx: click.Context,
@@ -65,13 +128,24 @@ def cli(
65
128
  debug: bool,
66
129
  output_format: OutputFormat,
67
130
  quiet: bool,
131
+ connect_timeout: str | None,
132
+ read_timeout: str | None,
133
+ write_timeout: str | None,
134
+ pool_timeout: str | None,
135
+ stream_read_timeout: str | None,
68
136
  ) -> None:
69
137
  """Handler - A2A protocol client CLI."""
70
- load_runtime_dotenv()
71
138
  ctx.ensure_object(dict)
72
139
  ctx.obj["output_format"] = output_format
73
140
  ctx.obj["quiet"] = quiet
74
141
  configure_output(output_format=output_format, quiet=quiet)
142
+ configure_http_timeouts(
143
+ connect_timeout=connect_timeout,
144
+ read_timeout=read_timeout,
145
+ write_timeout=write_timeout,
146
+ pool_timeout=pool_timeout,
147
+ stream_read_timeout=stream_read_timeout,
148
+ )
75
149
 
76
150
  if debug:
77
151
  setup_logging(level="DEBUG")
@@ -103,6 +177,66 @@ def version() -> None:
103
177
  output.json({"version": __version__})
104
178
 
105
179
 
180
+ @cli.command()
181
+ def docs() -> None:
182
+ """Open the Handler documentation in your browser.
183
+
184
+ \b
185
+ Examples:
186
+ $ handler docs
187
+ """
188
+ opened = webbrowser.open(DOCS_URL)
189
+ output = Output()
190
+ output.json({"url": DOCS_URL, "opened": opened})
191
+
192
+
193
+ def _upgrade_command() -> list[str] | None:
194
+ """Return the preferred package manager command for upgrading Handler."""
195
+ if shutil.which("uv"):
196
+ return ["uv", "tool", "upgrade", PACKAGE_NAME]
197
+ if shutil.which("pipx"):
198
+ return ["pipx", "upgrade", PACKAGE_NAME]
199
+ return None
200
+
201
+
202
+ @cli.command()
203
+ def update() -> None:
204
+ """Update Handler to the latest published version.
205
+
206
+ \b
207
+ Examples:
208
+ $ handler update
209
+ $ handler upgrade
210
+ """
211
+ output = Output()
212
+ command = _upgrade_command()
213
+ if command is None:
214
+ output.error(
215
+ code="installer_not_found",
216
+ message="Could not find uv or pipx to update Handler.",
217
+ suggestion=(
218
+ "Install uv or pipx, then run `uv tool upgrade a2a-handler` "
219
+ "or `pipx upgrade a2a-handler`."
220
+ ),
221
+ )
222
+ raise SystemExit(1)
223
+
224
+ result = subprocess.run(command, check=False)
225
+ if result.returncode != 0:
226
+ output.error(
227
+ code="update_failed",
228
+ message="Handler update command failed.",
229
+ details={
230
+ "command": command,
231
+ "returncode": result.returncode,
232
+ },
233
+ )
234
+ raise SystemExit(result.returncode)
235
+
236
+
237
+ cli.add_command(update, name="upgrade")
238
+
239
+
106
240
  @cli.command()
107
241
  @click.option(
108
242
  "--bearer-env", "-b", help="Env var containing bearer token for agent auth"
@@ -27,6 +27,13 @@ from a2a_handler.servers import (
27
27
  )
28
28
 
29
29
  TIMEOUT = 120
30
+ _TIMEOUT_NONE_VALUES = {"none", "null", "off", "disabled"}
31
+
32
+ _connect_timeout: float | None = float(TIMEOUT)
33
+ _read_timeout: float | None = float(TIMEOUT)
34
+ _write_timeout: float | None = float(TIMEOUT)
35
+ _pool_timeout: float | None = float(TIMEOUT)
36
+ _stream_read_timeout: float | None = None
30
37
  log = get_logger(__name__)
31
38
 
32
39
 
@@ -39,17 +46,109 @@ class AgentSelection:
39
46
 
40
47
 
41
48
  def build_http_client(
42
- timeout: int = TIMEOUT,
49
+ timeout: int | float | httpx.Timeout | None = None,
43
50
  credentials: AuthCredentials | None = None,
44
51
  ) -> httpx.AsyncClient:
45
52
  """Build an HTTP client with the specified timeout."""
53
+ effective_timeout = timeout if timeout is not None else _standard_timeout()
46
54
  if credentials and credentials.auth_type == AuthType.MTLS:
47
55
  return httpx.AsyncClient(
48
- timeout=timeout,
56
+ timeout=effective_timeout,
49
57
  verify=credentials.build_ssl_context(),
50
58
  trust_env=False,
51
59
  )
52
- return httpx.AsyncClient(timeout=timeout, trust_env=False)
60
+ return httpx.AsyncClient(timeout=effective_timeout, trust_env=False)
61
+
62
+
63
+ def build_streaming_http_client(
64
+ credentials: AuthCredentials | None = None,
65
+ ) -> httpx.AsyncClient:
66
+ """Build an HTTP client for long-lived streaming responses.
67
+
68
+ Streaming keeps the usual finite connect/write/pool timeouts but disables
69
+ the read timeout so an active long-running A2A task is not aborted just
70
+ because the server has no SSE event ready for a while.
71
+ """
72
+ return build_http_client(
73
+ timeout=httpx.Timeout(
74
+ connect=_connect_timeout,
75
+ read=_stream_read_timeout,
76
+ write=_write_timeout,
77
+ pool=_pool_timeout,
78
+ ),
79
+ credentials=credentials,
80
+ )
81
+
82
+
83
+ def configure_http_timeouts(
84
+ *,
85
+ connect_timeout: str | int | float | None = None,
86
+ read_timeout: str | int | float | None = None,
87
+ write_timeout: str | int | float | None = None,
88
+ pool_timeout: str | int | float | None = None,
89
+ stream_read_timeout: str | int | float | None = None,
90
+ ) -> None:
91
+ """Configure default CLI HTTP timeouts.
92
+
93
+ Passing no values resets the defaults. String values may be seconds or
94
+ ``none`` to disable that timeout.
95
+ """
96
+ global _connect_timeout, _read_timeout, _write_timeout, _pool_timeout
97
+ global _stream_read_timeout
98
+
99
+ _connect_timeout = _parse_timeout_option(
100
+ connect_timeout, "connect-timeout", float(TIMEOUT)
101
+ )
102
+ _read_timeout = _parse_timeout_option(read_timeout, "read-timeout", float(TIMEOUT))
103
+ _write_timeout = _parse_timeout_option(
104
+ write_timeout, "write-timeout", float(TIMEOUT)
105
+ )
106
+ _pool_timeout = _parse_timeout_option(pool_timeout, "pool-timeout", float(TIMEOUT))
107
+ _stream_read_timeout = _parse_timeout_option(
108
+ stream_read_timeout, "stream-read-timeout", None
109
+ )
110
+
111
+
112
+ def _standard_timeout() -> httpx.Timeout:
113
+ """Return the configured timeout for regular request/response calls."""
114
+ return httpx.Timeout(
115
+ connect=_connect_timeout,
116
+ read=_read_timeout,
117
+ write=_write_timeout,
118
+ pool=_pool_timeout,
119
+ )
120
+
121
+
122
+ def _parse_timeout_option(
123
+ value: str | int | float | None,
124
+ field_name: str,
125
+ default: float | None,
126
+ ) -> float | None:
127
+ """Parse a CLI/env timeout value."""
128
+ if value is None:
129
+ return default
130
+ if isinstance(value, str):
131
+ normalized = value.strip().lower()
132
+ if not normalized:
133
+ return default
134
+ if normalized in _TIMEOUT_NONE_VALUES:
135
+ return None
136
+ try:
137
+ parsed = float(normalized)
138
+ except ValueError as exc:
139
+ raise click.BadParameter(
140
+ "must be a number of seconds or 'none'",
141
+ param_hint=f"--{field_name}",
142
+ ) from exc
143
+ else:
144
+ parsed = float(value)
145
+
146
+ if parsed < 0:
147
+ raise click.BadParameter(
148
+ "must be greater than or equal to 0",
149
+ param_hint=f"--{field_name}",
150
+ )
151
+ return parsed
53
152
 
54
153
 
55
154
  def handle_client_error(e: Exception, agent_url: str, output: Output | None) -> None:
@@ -11,8 +11,8 @@ from a2a_handler.common.input_validation import InputValidationError, validate_a
11
11
  from a2a_handler.service import A2AService
12
12
  from a2a_handler.validation import (
13
13
  ValidationResult,
14
+ ValidationSource,
14
15
  validate_agent_card_from_file,
15
- validate_agent_card_from_url,
16
16
  )
17
17
 
18
18
  from ._helpers import (
@@ -155,7 +155,14 @@ def card_validate(
155
155
 
156
156
  async def do_validate() -> None:
157
157
  async with build_http_client(credentials=credentials) as http_client:
158
- result = await validate_agent_card_from_url(resolved_url, http_client)
158
+ service = A2AService(http_client, resolved_url, credentials=credentials)
159
+ agent_card = await service.get_card()
160
+ result = ValidationResult(
161
+ valid=True,
162
+ source=resolved_url,
163
+ source_type=ValidationSource.URL,
164
+ agent_card=agent_card,
165
+ )
159
166
 
160
167
  _format_validation_result(result, output)
161
168