# Bridge an existing HTTP API to A2A agents: JSON-RPC, REST, and gRPC from one handler

Most teams that need an A2A endpoint already own the hard part: the business logic. They have an HTTP service that takes a request, does the work, and returns a result. What they lack is the agent-facing surface, the Agent Card and the JSON-RPC, REST, and gRPC bindings that other agents expect. Reaching for a full agent framework to provide that surface is overkill and usually means rewriting logic that already works. The lighter path is a stateless bridge: implement one authenticated handler, and let an adapter expose it over every A2A transport.

## The shape of the bridge

The adapter is deliberately small. It terminates the three A2A bindings, translates each into a single internal call, and forwards that call to your existing HTTP handler. It holds no conversation state and makes no decisions; your handler owns the goal.

``` text
A2A client
   |   JSON-RPC (/rpc)   |   REST (/rest)   |   gRPC
   +----------+----------+------------------+-------+
                      stateless A2A adapter
                               |
                    POST { message, contextId }
                    Authorization: Bearer <HANDLER_TOKEN>
                               v
                   your existing business handler
```

One handler backs all three transports, so request validation, authz, and the actual work live in exactly one place instead of being reimplemented per binding.

## The contract your handler implements

The adapter calls your handler with a JSON body containing the A2A message and a context id, and expects an A2A 1.0 Message back with non-empty parts. A minimal handler using Express looks like this:

``` js
import express from "express";

const app = express();
app.use(express.json({ limit: "2mb" }));

app.post("/a2a-work", (req, res) => {
  const { message, contextId } = req.body ?? {};

  // Authenticate the adapter-to-handler call yourself, or rely on HANDLER_TOKEN
  // enforced at your reverse proxy.
  const text = message?.parts?.find((p) => typeof p.text === "string")?.text;
  if (!text) {
    return res.status(400).json({ parts: [{ text: "Send a non-empty text message." }] });
  }

  // Your existing logic lives here. This is where an LLM, a workflow engine,
  // or a plain deterministic service turns the message into a result.
  const answer = `Handled in context ${contextId ?? "new"}: ${text}`;

  return res.status(200).json({
    role: "ROLE_AGENT",
    parts: [{ text: answer }],
  });
});

app.listen(8080, "127.0.0.1");
```

The only hard requirement is non-empty `parts`. Everything else, routing to skills, calling internal APIs, escalating to a human, is your decision in the handler. Build and deploy that endpoint first; the adapter cannot invent behavior you have not implemented.

## What the generated server ships

Exporting the adapter produces a small tar archive rather than a hidden binary. It contains the server, a canonicalization module used for card signing, a generated Agent Card, a package manifest, and a README:

-   `server.mjs`, the stateless adapter.
-   `canonicalize.mjs`, the canonical JSON used when the card is signed.
-   `agent-card.json`, the public card with a single `handle-message` skill by default.
-   `package.json`, pinned to Node 22+ with explicit dependency versions.
-   `README.md`, the deployment and security notes.

The dependencies are ordinary, auditable libraries rather than a private runtime: the official A2A SDK, `@grpc/grpc-js` and `@bufbuild/protobuf` for native gRPC, Express for HTTP, and `jose` for card signing. Run `npm install`, configure environment variables, and `npm start`; no secret is ever embedded in the export.

## One handler, three bindings

With the adapter running, the same handler is reachable three ways. JSON-RPC carries the standard envelope at `/rpc`:

``` bash
curl -s http://127.0.0.1:9999/rpc \
  -H "Authorization: Bearer $A2A_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "req-1",
    "method": "SendMessage",
    "params": {
      "message": {
        "messageId": "msg-1",
        "role": "ROLE_USER",
        "parts": [{ "text": "Reconcile today's failed renewals" }]
      }
    }
  }'
```

The REST binding at `/rest` takes the params object directly, with no JSON-RPC wrapper:

``` bash
curl -s http://127.0.0.1:9999/rest \
  -H "Authorization: Bearer $A2A_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "messageId": "msg-2",
      "role": "ROLE_USER",
      "parts": [{ "text": "Summarize open support tickets" }]
    }
  }'
```

Native gRPC uses the official service descriptor and ProtoJSON conversion rather than hand-encoded messages, including server-streaming methods. The Agent Card is served publicly at `/.well-known/agent-card.json`, and its `supportedInterfaces` array tells clients which of these URLs and bindings exist.

## Configuration, and what is required

| Variable                        | Required | Purpose                                                                 |
|---------------------------------|----------|-------------------------------------------------------------------------|
| `A2A_TOKEN`                     | Yes      | Random bearer secret clients present; use at least 32 random characters |
| `HANDLER_URL`                   | Yes      | The URL of your authenticated business handler                          |
| `HANDLER_TOKEN`                 | No       | Bearer token the adapter presents to your handler                       |
| `CORS_ORIGINS`                  | No       | Comma-separated browser origins allowed to call the agent               |
| `PUBLIC_URL`                    | No       | Public base URL advertised in the card                                  |
| `HOST`, `PORT`                  | No       | HTTP listener settings; local listeners default to loopback             |
| `GRPC_PORT`, `GRPC_PUBLIC_URL`  | No       | gRPC listener and its advertised address                                |
| `TLS_CERT_FILE`, `TLS_KEY_FILE` | No       | Certificate and key; required for non-loopback gRPC                     |
| `SIGNING_JWK_FILE`              | No       | JWK used to sign the Agent Card                                         |

## The honest capability boundary

This is the part most "instant agent" generators gloss over, and it matters operationally. A stateless message adapter is **not** an autonomous model and **not** a persistent task engine. Concretely:

-   Streaming calls emit the final message; they do not stream incremental model tokens.
-   Persistent tasks that survive a restart, remote cancellation of running work, and push notifications all require a custom executor plus a durable store. If you need the task to still exist after the process restarts, this adapter alone does not provide it.
-   The card must not advertise capabilities the deployment lacks. If you have not implemented durable tasks, do not claim them; clients route based on the card and will treat advertised capabilities as real.

When you outgrow the bridge, the upgrade path is clear: put an executor and store behind the same handler, advance tasks through their real states, and only then advertise the corresponding methods. The adapter is the correct starting point for "expose my service to agents", not the end state for a long-running autonomous worker.

## Deployment security

Local listeners bind to `127.0.0.1` by default. For HTTP in production, put an HTTPS reverse proxy in front with rate limits, and expose non-loopback gRPC only with a certificate and key. The shared bearer token represents a single client principal; before multi-user deployment, integrate your identity provider so each caller is distinguishable. If you sign the card with `SIGNING_JWK_FILE`, distribute the matching public JWKS through a trusted channel; never trust a key merely because the agent served it alongside the card. Finally, review dependency advisories and commit a lockfile so the audited versions are what actually run.

You can generate this adapter, inspect the full card, and exercise all three bindings from the Powerduck workspace; the [online demo](https://www.powerduck.com/app/?ref=powerduck.com) shows the spec-driven loop, and the trust model for the generated card is covered in [the Agent Card discovery and verification guide](https://www.powerduck.com/blog/a2a-agent-card-discovery-verification/).

