Skip to content

fix: correct A2A agent card URLs#1199

Merged
omeraplak merged 2 commits intomainfrom
fix/a2a-agent-card-url
Apr 11, 2026
Merged

fix: correct A2A agent card URLs#1199
omeraplak merged 2 commits intomainfrom
fix/a2a-agent-card-url

Conversation

@omeraplak
Copy link
Copy Markdown
Member

@omeraplak omeraplak commented Apr 11, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

A2A agent cards advertise the discovery document path (/.well-known/{serverId}/agent-card.json) in their url field instead of the JSON-RPC endpoint. The returned URL is also relative, which breaks third-party clients that expect an absolute URL.

What is the new behavior?

A2A agent cards now advertise /a2a/{serverId}. When the card is served through the Hono or Elysia integrations, VoltAgent resolves that endpoint to an absolute URL using the incoming request URL.

fixes #1198

Notes for reviewers

  • Added a regression test in @voltagent/a2a-server for relative and absolute card URLs.
  • Added route-level regression coverage for Hono and Elysia to ensure the request URL is forwarded when resolving the card.
  • Updated the A2A docs and the with-a2a-server example smoke test to assert the advertised endpoint.
  • Validation run:
    • pnpm --filter @voltagent/a2a-server exec vitest run src/server.spec.ts
    • pnpm --filter @voltagent/server-hono exec vitest run src/routes/a2a.routes.spec.ts
    • pnpm --filter @voltagent/server-elysia exec vitest run src/routes/a2a.routes.spec.ts
    • pnpm exec biome check packages/a2a-server/src/server.ts packages/a2a-server/src/server.spec.ts packages/a2a-server/src/types.ts packages/server-core/src/a2a/types.ts packages/server-hono/src/routes/a2a.routes.ts packages/server-hono/src/routes/a2a.routes.spec.ts packages/server-elysia/src/routes/a2a.routes.ts packages/server-elysia/src/routes/a2a.routes.spec.ts examples/with-a2a-server/scripts/smoke-test.mjs examples/with-a2a-server/README.md website/docs/agents/a2a/a2a-server.md
    • verified http://localhost:3141/.well-known/supportagent/agent-card.json returns "url": "http://localhost:3141/a2a/supportagent" in examples/with-a2a-server
  • Full examples/with-a2a-server smoke test was not run because OPENAI_API_KEY is not configured in this environment.

Summary by cubic

Fix A2A agent cards to advertise /a2a/{serverId} and return absolute URLs when served via @voltagent/server-hono or @voltagent/server-elysia. Also correct URL encoding for server IDs so reserved characters are encoded and spaces are preserved. Fixes #1198.

  • Bug Fixes
    • In @voltagent/a2a-server, the card url now points to /a2a/{serverId} and resolves to an absolute URL when a requestUrl is provided.
    • @voltagent/server-hono and @voltagent/server-elysia pass the incoming request URL to resolveAgentCard; added route tests to assert this.
    • In @voltagent/server-core, route helpers now percent‑encode server IDs for both /a2a/{serverId} and /.well-known/{serverId}/agent-card.json paths (spaces encoded as %20, nothing stripped).
    • Added regression tests and updated docs and the with-a2a-server example to validate absolute /a2a URLs.

Written for commit bc87f2c. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Agent cards now advertise the JSON-RPC endpoint path /a2a/{serverId} and return an absolute URL when served over HTTP.
    • Server IDs containing reserved characters are correctly percent-encoded in generated paths.
  • Documentation

    • Updated A2A docs and examples to reflect the new discovery and /a2a endpoint formats.
  • Tests

    • Added tests covering agent card URL construction and encoding behavior.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 11, 2026

🦋 Changeset detected

Latest commit: bc87f2c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@voltagent/a2a-server Patch
@voltagent/server-core Patch
@voltagent/server-hono Patch
@voltagent/server-elysia Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@joggrbot

This comment has been minimized.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

Agent-card URL generation was changed to advertise the JSON-RPC endpoint path /a2a/{serverId} (relative) and to resolve to an absolute URL when a request URL is available; request handlers in Hono and Elysia now pass the incoming request URL into the resolver. Tests and docs updated accordingly.

Changes

Cohort / File(s) Summary
Release Metadata
/.changeset/bright-badgers-confess.md
Patch release notes added for packages affected by the agent-card URL change.
A2A Server Logic & Tests
packages/a2a-server/src/server.ts, packages/a2a-server/src/server.spec.ts
Introduced A2A route prefix and URL/path helpers; getAgentCard now accepts context and returns a url pointing at /a2a/{serverId} (relative) or an absolute URL when requestUrl is provided. Tests added/updated to assert encoding and absolute resolution.
Types / Context
packages/a2a-server/src/types.ts, packages/server-core/src/a2a/types.ts
Extended A2ARequestContext with optional requestUrl?: string.
Server Core Routes & Tests
packages/server-core/src/a2a/routes.ts, packages/server-core/src/a2a/routes.spec.ts
Sanitization now trims only slashes and uses encodeURIComponent; added tests verifying percent-encoding in built paths and endpoints.
Hono Integration & Tests
packages/server-hono/src/routes/a2a.routes.ts, packages/server-hono/src/routes/a2a.routes.spec.ts
Agent-card route now passes c.req.url as requestUrl to resolveAgentCard. Comprehensive route tests added verifying requestUrl forwarding and response body.
Elysia Integration & Tests
packages/server-elysia/src/routes/a2a.routes.ts, packages/server-elysia/src/routes/a2a.routes.spec.ts
Route handler updated to pass Elysia request.url into resolveAgentCard; tests updated to expect the requestUrl argument.
Examples & Documentation
examples/with-a2a-server/README.md, examples/with-a2a-server/scripts/smoke-test.mjs, website/docs/agents/a2a/a2a-server.md
Example and docs updated to use /.well-known/{serverId}/agent-card.json and to show that agent-card url is /a2a/{serverId} (resolved to absolute URL when fetched); smoke test adds assertion that card url matches absolute /a2a/{serverId}.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant RouteHandler as Route Handler (Hono/Elysia)
participant Resolver as server-core.resolveAgentCard
participant A2AServer as A2A Server

Client->>RouteHandler: GET /.well-known/{serverId}/agent-card.json
RouteHandler->>Resolver: resolveAgentCard(registry, serverId, { requestUrl })
Resolver->>A2AServer: buildAgentCard(serverId, context.requestUrl)
A2AServer-->>Resolver: agent-card { url: "/a2a/{serverId}" or absolute URL }
Resolver-->>RouteHandler: JSON agent-card
RouteHandler-->>Client: 200 OK + agent-card JSON

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 A tiny rabbit hops with cheer,
I swapped the paths so cards are clear,
/a2a/{serverId} now leads the way,
Resolved by requests that come to play,
Hono, Elysia — endpoints all hooray! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: correct A2A agent card URLs' clearly and concisely summarizes the main change: correcting the URLs that A2A agent cards advertise, changing from the discovery document path to the JSON-RPC endpoint path.
Description check ✅ Passed The PR description comprehensively addresses all template requirements: it follows the commit convention guideline, explicitly checks all four checklist items (issue linked, tests added, docs updated, changesets added), clearly describes current and new behavior, provides validation commands and results, and includes detailed reviewer notes with regression test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/a2a-agent-card-url

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/server-hono/src/routes/a2a.routes.spec.ts (1)

31-49: Reduce any type casts in test mocks to maintain TypeScript type safety.

Lines 37, 44, 48, and 56 use as any, which bypasses compile-time checks in a critical test file. Replace with narrow test interfaces or Partial<T> to keep mocks properly typed. This aligns with the repo's TypeScript-first approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-hono/src/routes/a2a.routes.spec.ts` around lines 31 - 49, The
tests currently cast mockDeps and mockLogger to any (and cast app in
registerA2ARoutes) which removes TypeScript safety; replace these with narrow
mock types using Partial<T> or small test interfaces: type MockDeps =
Partial<Pick<YourA2ADepsType, "a2a">> (or Partial<typeof realDeps>) for
mockDeps, and type MockLogger = Partial<Logger> for mockLogger, then remove the
as any casts and pass app as the concrete OpenAPIHono instance to
registerA2ARoutes; update the beforeEach to use these typed mocks so vi.fn()
implementations satisfy the declared interfaces.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/with-a2a-server/README.md`:
- Around line 82-90: The README example uses inconsistent endpoint IDs: the
agent card path /.well-known/supportagent/agent-card.json and its advertised
JSON-RPC url "http://localhost:3141/a2a/supportagent" but a subsequent curl
example still targets "/a2a/support", causing a 404; update the later curl
request (and any references to the JSON-RPC endpoint) to use "/a2a/supportagent"
so the documented flow matches the advertised "url" from the agent card and the
paths (e.g., adjust the curl that currently calls "/a2a/support" to
"/a2a/supportagent").

In `@packages/a2a-server/src/server.ts`:
- Around line 36-42: The sanitizeSegment function currently strips internal
whitespace and characters which mangles IDs (e.g., "my agent" -> "myagent") and
fails to escape reserved URL characters; update sanitizeSegment (used by
buildA2AEndpointPath and DEFAULT_A2A_ROUTE_PREFIX) to only trim leading/trailing
slashes and then return an encoded segment via encodeURIComponent so internal
spaces and reserved chars are preserved/escaped rather than removed; ensure
buildA2AEndpointPath uses this new behavior to build safe, unambiguous route
paths.

In `@website/docs/agents/a2a/a2a-server.md`:
- Around line 74-75: Update the troubleshooting text to consistently use the
`{serverId}` terminology used by the endpoints (`GET
/.well-known/{serverId}/agent-card.json`, `POST /a2a/{serverId}`): replace
occurrences of “agent ID” with “serverId” and change any references to the
`agents` key to point to the correct A2A routing key (or configuration field)
that holds the serverId mapping (e.g., reference `serverId` or the servers
routing key used by A2A), and make the same fixes in the other affected block
(lines referenced around 81-84).

---

Nitpick comments:
In `@packages/server-hono/src/routes/a2a.routes.spec.ts`:
- Around line 31-49: The tests currently cast mockDeps and mockLogger to any
(and cast app in registerA2ARoutes) which removes TypeScript safety; replace
these with narrow mock types using Partial<T> or small test interfaces: type
MockDeps = Partial<Pick<YourA2ADepsType, "a2a">> (or Partial<typeof realDeps>)
for mockDeps, and type MockLogger = Partial<Logger> for mockLogger, then remove
the as any casts and pass app as the concrete OpenAPIHono instance to
registerA2ARoutes; update the beforeEach to use these typed mocks so vi.fn()
implementations satisfy the declared interfaces.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3801e0b8-4b96-4850-808c-9e89ef03768c

📥 Commits

Reviewing files that changed from the base of the PR and between 74b76aa and b539279.

📒 Files selected for processing (12)
  • .changeset/bright-badgers-confess.md
  • examples/with-a2a-server/README.md
  • examples/with-a2a-server/scripts/smoke-test.mjs
  • packages/a2a-server/src/server.spec.ts
  • packages/a2a-server/src/server.ts
  • packages/a2a-server/src/types.ts
  • packages/server-core/src/a2a/types.ts
  • packages/server-elysia/src/routes/a2a.routes.spec.ts
  • packages/server-elysia/src/routes/a2a.routes.ts
  • packages/server-hono/src/routes/a2a.routes.spec.ts
  • packages/server-hono/src/routes/a2a.routes.ts
  • website/docs/agents/a2a/a2a-server.md

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="examples/with-a2a-server/README.md">

<violation number="1" location="examples/with-a2a-server/README.md:89">
P2: The updated endpoint examples use `supportagent`, but the subsequent JSON-RPC curl command still targets `/a2a/support`, which makes the README flow inconsistent and likely to fail for users following it step-by-step.</violation>
</file>

<file name="packages/a2a-server/src/server.ts">

<violation number="1" location="packages/a2a-server/src/server.ts:37">
P2: The new segment sanitizer removes internal whitespace from agent IDs, which can advertise a different endpoint than the actual agent identifier.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 11, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: bc87f2c
Status:⚡️  Build in progress...

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/server-hono/src/routes/a2a.routes.spec.ts (1)

8-29: Type the mock factories to keep strict TypeScript guarantees.

The mock setup currently relies on untyped vi.importActual(...) and untyped callback params, which weakens compile-time checks in this TypeScript-first codebase.

♻️ Proposed typed mock refinement
 vi.mock("@voltagent/server-core", async () => {
-  const actual = await vi.importActual("@voltagent/server-core");
+  const actual = await vi.importActual<typeof import("@voltagent/server-core")>(
+    "@voltagent/server-core",
+  );
   return {
     ...actual,
     executeA2ARequest: vi.fn(),
     resolveAgentCard: vi.fn(),
     A2A_ROUTES: actual.A2A_ROUTES,
   };
 });

 vi.mock("@voltagent/a2a-server", async () => {
   return {
-    normalizeError: vi.fn().mockImplementation((id, error) => ({
+    normalizeError: vi
+      .fn()
+      .mockImplementation((id: string | number | null, error: { code?: number; message?: string }) => ({
       jsonrpc: "2.0",
       id,
       error: {
         code: error.code || -32603,
-        message: error.message,
+        message: error.message ?? "Internal error",
       },
     })),
   };
 });

As per coding guidelines, "Maintain type safety in TypeScript-first codebase".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-hono/src/routes/a2a.routes.spec.ts` around lines 8 - 29, The
mocks are untyped which weakens TS guarantees; update the mock factories to
return properly typed mocks by importing the original module types and
annotating the factory return types and mocked functions. For the first vi.mock
use const actual = await vi.importActual<typeof
import("@voltagent/server-core")>(...) and type the factory return as
Partial<typeof import("@voltagent/server-core")> (or vi.Mocked<typeof
import(...)) so executeA2ARequest and resolveAgentCard are typed (e.g. vi.fn()
as vi.MockedFunction<typeof actual.executeA2ARequest>), and for the second
vi.mock annotate the factory with typeof import("@voltagent/a2a-server") so
normalizeError is typed (e.g. vi.fn().mockImplementation(...) as
vi.MockedFunction<typeof import("@voltagent/a2a-server").normalizeError>). This
will preserve strict TypeScript checks while keeping the existing mock behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/server-hono/src/routes/a2a.routes.spec.ts`:
- Around line 8-29: The mocks are untyped which weakens TS guarantees; update
the mock factories to return properly typed mocks by importing the original
module types and annotating the factory return types and mocked functions. For
the first vi.mock use const actual = await vi.importActual<typeof
import("@voltagent/server-core")>(...) and type the factory return as
Partial<typeof import("@voltagent/server-core")> (or vi.Mocked<typeof
import(...)) so executeA2ARequest and resolveAgentCard are typed (e.g. vi.fn()
as vi.MockedFunction<typeof actual.executeA2ARequest>), and for the second
vi.mock annotate the factory with typeof import("@voltagent/a2a-server") so
normalizeError is typed (e.g. vi.fn().mockImplementation(...) as
vi.MockedFunction<typeof import("@voltagent/a2a-server").normalizeError>). This
will preserve strict TypeScript checks while keeping the existing mock behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3302e016-2b46-4a98-a2e4-dee617019756

📥 Commits

Reviewing files that changed from the base of the PR and between b539279 and bc87f2c.

📒 Files selected for processing (7)
  • examples/with-a2a-server/README.md
  • packages/a2a-server/src/server.spec.ts
  • packages/a2a-server/src/server.ts
  • packages/server-core/src/a2a/routes.spec.ts
  • packages/server-core/src/a2a/routes.ts
  • packages/server-hono/src/routes/a2a.routes.spec.ts
  • website/docs/agents/a2a/a2a-server.md
✅ Files skipped from review due to trivial changes (1)
  • packages/server-core/src/a2a/routes.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • examples/with-a2a-server/README.md
  • packages/a2a-server/src/server.ts
  • packages/a2a-server/src/server.spec.ts
  • website/docs/agents/a2a/a2a-server.md

@omeraplak omeraplak merged commit b6813e9 into main Apr 11, 2026
23 of 24 checks passed
@omeraplak omeraplak deleted the fix/a2a-agent-card-url branch April 11, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] A2A Server Agent Card displays internal path instead of  /a2a/{serverId}  endpoint and uses relative URL

1 participant