はじめに
AIエージェント同士をつなぐオープン標準 A2A(Agent2Agent)プロトコル を、SDKを使わない生実装と実測ログで深掘りするシリーズです。前作のMCP深掘りシリーズ 1・2・3と同じ流儀で、今度はA2Aを調べていきます。
- 第1回(本記事): A2AでAIエージェント同士はどう会話するのか? — Agent Card と最小エージェントを依存ゼロで作り、curlで話しかける
- 第2回: A2Aエージェントは時間のかかる依頼をどう処理するのか? — Taskライフサイクル・中断と再開・SSE・成果物(Artifact)
- 第3回: A2Aエージェント同士をつなぐと何が起きるのか? — 2体のエージェントをつなぎ、role・ID・中断の伝搬を実測する
- 第4回: A2Aのプッシュ通知はどう動くのか? — webhookで結果を受け取る
- 第5回: 自作のA2Aエージェントは公式SDK製と会話できるのか? — 挙動差分と0.3系/1.0系の非互換
本記事のゴールは、次の一文を実感を持って言えるようになることです。
A2Aエージェントとは、自己紹介のJSON(Agent Card)を配り、
SendMessageというJSON-RPCメソッドを受けるHTTPサーバである。
検証環境: macOS / Node.js v25.6.1 / A2Aプロトコル v1.0
A2Aとは何か
A2A(Agent2Agent)は、AIエージェント同士が対等な相手(ピア)として通信・協調するためのオープン標準です。公式サイトの一文が、MCPとの住み分けをそのまま表しています。
An open standard enabling users to connect agents to tools and data through MCP, and to other agents through A2A.
- MCP: エージェント → ツール・データ。道具箱への接続
- A2A: エージェント → エージェント。同僚への依頼
A2Aが標準化されてきた経緯も確認しておきます。
| 時期 | 出来事 |
|---|---|
| 2025-04 | Googleが50社超のパートナーとA2Aを発表 |
| 2025-06 | GoogleがLinux Foundationへ寄贈。ベンダー中立の運営に |
| 2026-03 | v1.0.0リリース。破壊的変更を含む大規模な仕様刷新 |
| 2026-05 | v1.0.1リリース(本記事の対象バージョン) |
仕様の一次情報は a2a-protocol.org と GitHubリポジトリ にあります。
登場人物は3者です。
- User: リクエストを開始する人間または自動化されたサービス
- A2A Client: ユーザーに代わって通信を開始する側
- A2A Server(リモートエージェント): HTTPエンドポイントを実装し、依頼を処理するAIエージェント
基本の流れは [A2A Client → A2A Server] のHTTPリクエストで、サーバの中身がどのLLM・どのフレームワークかは相手から見えなくてよい(opaque)、というのが設計思想です。
最初に覚える概念は2つだけ
本記事の範囲では、次の2つを押さえれば十分です。
Agent Card — エージェントの自己紹介JSONです。名前・説明・できること(skills)・接続先URL・対応能力(capabilities)を記述し、決まったURLで配布します。
https://{エージェントのドメイン}/.well-known/agent-card.json
/.well-known/ はRFC 8615で定められた「メタデータの置き場所」の慣習で、クライアントはこのURLを見るだけで、接続前に相手が何者かを知ることができます。この「発見(discovery)」がA2Aの入口です。
Message — クライアントとエージェントの間でやりとりされる1ターンの発言です。本文は parts という配列に入り、テキスト・ファイル・構造化データを混載できます。発言者は role で表し、依頼する側が ROLE_USER、エージェント側が ROLE_AGENT です。
このほかにTask(状態を持つ作業単位)とArtifact(成果物)という重要な概念がありますが、これらは長時間処理の話なので第2回で扱います。今回のエージェントは「聞かれたら即答する」最小形です。
注意: v1.0で仕様が大きく変わっています
2026-03のv1.0.0はほぼ作り直しに近い刷新で、0.x系の解説記事やサンプルコードとは互換性がありません。これから学ぶ方は、読んでいる資料がどちらの系統か最初に確認することをおすすめします。見分け方は簡単です。
| 見る場所 | 0.x系 | v1.0系 |
|---|---|---|
| メソッド名 |
message/send、tasks/get
|
SendMessage、GetTask(PascalCase) |
| roleの値 |
"user" / "agent"
|
"ROLE_USER" / "ROLE_AGENT"
|
| 状態の値 |
"submitted" など小文字 |
"TASK_STATE_SUBMITTED" など |
| Cardの接続先 |
url + preferredTransport
|
supportedInterfaces 配列 |
一見バラバラの変更に見えますが、実は1つの決定の帰結です。v1.0ではデータモデルの正式な定義がProtocol Buffersのスキーマ(specification/a2a.proto)になり、JSONはその公式変換規則(ProtoJSON)で機械的に導出される表現になりました。enumが SCREAMING_SNAKE_CASE の文字列になったのも、フィールド名がcamelCaseなのも、全部ProtoJSONの規則です。
依存ゼロ・170行でA2Aエージェントを書く
npm install は一切不要です。題材は前作MCPシリーズと同じ「2つの数値の足し算」で、MCP版のaddツールをA2Aエージェントに移植します。同じ計算を両プロトコルで実装すると、差分がそのままプロトコルの個性として見えるからです。
calc-server.mjs として保存してください。
// A2A v1.0 の最小エージェント(依存ゼロNode, JSON-RPCバインディング)
// MCP学習の「addツール」をA2Aに移植したもの
import http from 'node:http';
import crypto from 'node:crypto';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
const PORT = 4101;
const BASE = `http://localhost:${PORT}`;
const WIRE_LOG = fileURLToPath(new URL('./calc-wire.log', import.meta.url));
// ---- Agent Card: このエージェントの自己紹介 ----
// MCP版の TOOLS 定義に相当するが、inputSchema のような機械可読な型強制は無い。
// examples は人間(と相手エージェントのLLM)向けのヒント。
const AGENT_CARD = {
name: 'Calc Agent',
description: '2つの数値の足し算を請け負うエージェント(MCP addツールのA2A移植版)',
version: '0.1.0',
supportedInterfaces: [
{ url: `${BASE}/a2a/v1`, protocolBinding: 'JSONRPC', protocolVersion: '1.0' },
],
capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
defaultInputModes: ['application/json', 'text/plain'],
defaultOutputModes: ['text/plain'],
skills: [
{
id: 'add',
name: '足し算',
description: '2つの数値を足し算する。JSON({"a":19,"b":23})でも文章("19 + 23")でも依頼できる',
tags: ['calculator', 'add'],
examples: ['{"a": 19, "b": 23}', '19 + 23 を計算して'],
inputModes: ['application/json', 'text/plain'],
outputModes: ['text/plain'],
},
],
};
const CARD_ETAG = `"${AGENT_CARD.version}"`; // ETagはversionフィールド由来でよい(§8.6)
// ---- JSON-RPC 2.0 のエラーコード ----
const PARSE_ERROR = -32700;
const INVALID_REQUEST = -32600;
const METHOD_NOT_FOUND = -32601;
const VERSION_NOT_SUPPORTED = -32009; // A2A固有(§5.4)
// ---- add の計算本体(MCP版からそのまま流用) ----
function add(a, b) {
return { text: `${a} + ${b} = ${a + b}` };
}
// メッセージ本文から「a+bの依頼」を読み取る。
// MCPでは inputSchema が保証してくれた構造を、A2Aではエージェント自身が解釈する。
function parseAddRequest(text) {
try {
const obj = JSON.parse(text); // JSON形式: {"a":19,"b":23}
if (typeof obj.a === 'number' && typeof obj.b === 'number') return [obj.a, obj.b];
} catch { /* JSONでなければ文章として解釈を試みる */ }
const m = text.match(/(-?\d+(?:\.\d+)?)\s*\+\s*(-?\d+(?:\.\d+)?)/); // 文章形式: "19 + 23"
return m ? [Number(m[1]), Number(m[2])] : null;
}
// ---- JSON-RPC メソッドごとのハンドラ ----
const handlers = {
SendMessage: (params) => {
const text = (params?.message?.parts ?? []).map((p) => p.text ?? '').join('');
const parsed = parseAddRequest(text);
const replyText = parsed
? add(...parsed).text
: 'すみません、足し算の依頼として理解できませんでした。例: {"a": 19, "b": 23} または「19 + 23」';
// MCP版は失敗を isError:true で表現した。A2Aの即答パターンでは
// 「できない」もエージェントの返答(Message)として返す
return {
message: {
messageId: crypto.randomUUID(),
role: 'ROLE_AGENT',
parts: [{ text: replyText }],
},
};
},
};
// ---- 通信ログ(wire log): 全トラフィックをファイルに記録する ----
function wire(direction, text) {
fs.appendFileSync(WIRE_LOG, `[${new Date().toISOString()}] [${direction}]\n${text}\n\n`);
}
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
const headerDump = ['host', 'content-type', 'a2a-version', 'if-none-match']
.filter((h) => req.headers[h] !== undefined)
.map((h) => `${h}: ${req.headers[h]}`)
.join('\n');
wire('クライアント → サーバ', `${req.method} ${req.url}\n${headerDump}${body ? '\n\n' + body : ''}`);
const reply = (status, headers, payload) => {
// ログを書いてから送信する。逆順にすると、クライアントが応答を受け取って
// 先に進み、サーバがログを書く前に停止させられる競合が起きる(実測で確認済み)
wire('サーバ → クライアント', `HTTP ${status}\n${Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join('\n')}${payload ? '\n\n' + payload : ''}`);
res.writeHead(status, headers);
res.end(payload);
};
// Agent Cardの配信(§8.6: キャッシュヘッダ付き。If-None-Match一致なら304で済む)
if (req.method === 'GET' && req.url === '/.well-known/agent-card.json') {
if (req.headers['if-none-match'] === CARD_ETAG) {
return reply(304, { ETag: CARD_ETAG, 'Cache-Control': 'max-age=3600' }, '');
}
return reply(
200,
{ 'Content-Type': 'application/json', ETag: CARD_ETAG, 'Cache-Control': 'max-age=3600' },
JSON.stringify(AGENT_CARD, null, 2),
);
}
if (req.method === 'POST' && req.url === '/a2a/v1') {
const respond = (obj) => reply(200, { 'Content-Type': 'application/json' }, JSON.stringify(obj, null, 2));
// エラー詳細は error.data 配列に @type 付きオブジェクトで載せる(§5.4)
const err = (id, code, message, data) => respond({ jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) } });
let rpc;
try {
rpc = JSON.parse(body);
} catch {
return err(null, PARSE_ERROR, 'Invalid JSON payload');
}
if (rpc.jsonrpc !== '2.0' || typeof rpc.method !== 'string') {
return err(rpc.id ?? null, INVALID_REQUEST, 'Request payload validation error');
}
// §3.6: A2A-Version ヘッダが空なら 0.3 とみなす。本エージェントは 1.0 のみ対応
const version = req.headers['a2a-version'] ?? '0.3';
if (version !== '1.0') {
return err(rpc.id, VERSION_NOT_SUPPORTED, 'Version not supported', [
{
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
reason: 'VERSION_NOT_SUPPORTED',
domain: 'a2a-protocol.org',
metadata: { requestedVersion: version, supportedVersions: '1.0' },
},
]);
}
const handler = handlers[rpc.method];
if (!handler) return err(rpc.id, METHOD_NOT_FOUND, 'Method not found');
return respond({ jsonrpc: '2.0', id: rpc.id, result: handler(rpc.params) });
}
reply(404, { 'Content-Type': 'application/json' }, JSON.stringify({ error: 'not found' }));
});
});
server.listen(PORT, () => {
console.log(`Calc Agent (A2A v1.0, JSON-RPC binding) : ${BASE}`);
console.log(` Agent Card : ${BASE}/.well-known/agent-card.json`);
console.log(` JSON-RPC : POST ${BASE}/a2a/v1`);
console.log(` wire log : ${WIRE_LOG}`);
});
中身のプロトコルは、前作のMCP深掘りシリーズ 1で学んだ JSON-RPC 2.0そのままです。エラーコードの体系も表引きハンドラの構造も、MCP版のコードから流用できました。差分は「HTTPで受ける」「Agent Cardを配る」「入力を自分で解釈する」の3点に集約されます。
全トラフィックは通信ログ(wire log)として calc-wire.log に記録されるので、あとから「実際に何が流れたか」を確認できます。
curlで話しかける
起動します。
node calc-server.mjs
別のターミナルから観察していきます。手順は次の7ステップです(すべて [クライアント → エージェント] のリクエストです)。
ステップ1: Agent Cardを取得する(発見)
curl -s http://localhost:4101/.well-known/agent-card.json
{
"name": "Calc Agent",
"description": "2つの数値の足し算を請け負うエージェント(MCP addツールのA2A移植版)",
"version": "0.1.0",
"supportedInterfaces": [
{
"url": "http://localhost:4101/a2a/v1",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"capabilities": {
"streaming": false,
"pushNotifications": false,
"extendedAgentCard": false
},
"skills": [
{
"id": "add",
"name": "足し算",
"description": "2つの数値を足し算する。JSON({\"a\":19,\"b\":23})でも文章(\"19 + 23\")でも依頼できる",
"examples": ["{\"a\": 19, \"b\": 23}", "19 + 23 を計算して"]
}
]
}
(紙面の都合で一部フィールドを省略しています)
いま取得したこのJSONが Agent Card です。MCPの initialize + tools/list に相当する情報が入っていますが、決定的な違いが1つ。まだ何も接続していません。Agent Cardという静的なJSONをGETしただけで、相手の名前・できること・接続先・対応能力が全部わかりました。
ステップ2: JSON形式で足し算を依頼する
supportedInterfaces[0].url で宣言されたエンドポイントに、SendMessage を送ります。
curl -s -X POST http://localhost:4101/a2a/v1 \
-H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"msg-001","role":"ROLE_USER","parts":[{"text":"{\"a\": 19, \"b\": 23}"}]}}}'
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"message": {
"messageId": "67646c41-dc15-4767-acb2-dce74bd67efb",
"role": "ROLE_AGENT",
"parts": [
{ "text": "19 + 23 = 42" }
]
}
}
}
答えが返ってきました。応答は result 直下ではなく result.message に包まれている点に注目してください。エージェントの応答は「即答のMessage」か「預かりのTask(時間のかかる依頼を預かるときの応答。第2回で扱います)」のどちらか一方で、どちらなのかをキー名で示す構造です。いま返ってきたのはキー名が message、つまり即答のMessageの実例です。
ステップ3: 文章で依頼する
curl -s -X POST http://localhost:4101/a2a/v1 \
-H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
-d '{"jsonrpc":"2.0","id":2,"method":"SendMessage","params":{"message":{"messageId":"msg-002","role":"ROLE_USER","parts":[{"text":"19 + 23 を計算して"}]}}}'
ステップ2と同じ応答(「19 + 23 = 42」のMessage)が返ります。MCPの tools/call は arguments: {a, b} という型付き引数でしたが、今回送ったのは "parts": [{"text": "19 + 23 を計算して"}] という自然文です。どう解釈するかはエージェントの自由(と責任)で、この実装では正規表現が解釈役を務めています。
ステップ4: 理解できない依頼をする
足し算と関係のない本文 "parts": [{"text": "こんにちは"}] を送って、エージェントが解釈できない依頼への振る舞いを見ます。
curl -s -X POST http://localhost:4101/a2a/v1 \
-H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
-d '{"jsonrpc":"2.0","id":3,"method":"SendMessage","params":{"message":{"messageId":"msg-003","role":"ROLE_USER","parts":[{"text":"こんにちは"}]}}}'
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"message": {
"messageId": "a4de47b9-370a-4a7c-b2b5-e45bef4baed0",
"role": "ROLE_AGENT",
"parts": [
{ "text": "すみません、足し算の依頼として理解できませんでした。例: {\"a\": 19, \"b\": 23} または「19 + 23」" }
]
}
}
}
エラーではなく、普通の返答Messageが返ります。「できない」もエージェントの応答のうち、という扱いです。MCP版が isError: true で表現していたのと同じく、アプリケーションレベルの失敗をプロトコルエラーにしない、という2層構造は共通です。
ステップ5: A2A-Versionヘッダを付け忘れる
curl -s -X POST http://localhost:4101/a2a/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"SendMessage","params":{"message":{"messageId":"msg-004","role":"ROLE_USER","parts":[{"text":"{\"a\": 1, \"b\": 2}"}]}}}'
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32009,
"message": "Version not supported",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "VERSION_NOT_SUPPORTED",
"domain": "a2a-protocol.org",
"metadata": { "requestedVersion": "0.3", "supportedVersions": "1.0" }
}
]
}
}
今度はプロトコルエラーです。仕様では「ヘッダが無いリクエストは0.3の書式とみなす」と決まっています(0.3時代のクライアントとの後方互換のため)。つまり、リクエストの書式が1.0系なら、A2A-Version: 1.0 ヘッダの指定も必須です。本エージェントは1.0しか話せないため、0.3とみなしたこのリクエストを -32009 VersionNotSupported で断りました。エラー詳細を error.data に @type 付きで載せる形式も仕様どおりです。
ステップ6: 0.x系の旧メソッド名を送ってみる
curl -s -X POST http://localhost:4101/a2a/v1 \
-H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
-d '{"jsonrpc":"2.0","id":5,"method":"message/send","params":{}}'
{
"jsonrpc": "2.0",
"id": 5,
"error": { "code": -32601, "message": "Method not found" }
}
0.x系の message/send は、v1.0の世界では未知のメソッドです。冒頭の「0.x系と互換性がない」は、この1往復に集約されています。
ステップ7: Agent Cardのキャッシュの動きを見る
ETagはHTTP標準のキャッシュ検証の仕組みで、サーバはコンテンツの版を示す識別子(指紋)を ETag ヘッダで返します。まず、Agent Card応答のヘッダを確認してみます(-sD - はヘッダだけを表示するオプションの組み合わせです):
curl -sD - -o /dev/null http://localhost:4101/.well-known/agent-card.json
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "0.1.0"
Cache-Control: max-age=3600
(Date等の一般的なヘッダは省略しています)
ステップ1では見えていませんでしたが、Agent Cardの応答には毎回このように ETag: "0.1.0"(Cardのversionフィールド由来)が付いています。クライアントは次回の取得時に If-None-Match ヘッダで「手元にはこの版がある」と伝えます。版が変わっていなければ、サーバは本文なしの 304 Not Modified で「手元のものをそのまま使ってよい」と応じます:
curl -sD - -o /dev/null \
-H 'If-None-Match: "0.1.0"' \
http://localhost:4101/.well-known/agent-card.json
HTTP/1.1 304 Not Modified
ETag: "0.1.0"
Cache-Control: max-age=3600
本文なしの304で済みました。Agent Cardは「接続前に毎回確認したいが、めったに変わらない」情報なので、仕様はHTTP標準のキャッシュ(ETag / If-None-Match)で配ることを推奨しています。エージェントの能力が変わったらCardの version を上げ、ETagが変わってキャッシュが自然に無効化される、という連鎖がWebの仕組みだけで回ります。
実測して初めて分かったこと
1. ハンドシェイクが存在しない
MCPは最初のツール実行までに initialize → initialized 通知 → tools/list → tools/call の4メッセージが必要でした。A2Aは Card取得 → SendMessage の2つで、しかもCard取得は304で済み、相手を知っていれば省略もできます。
この違いは、2つのプロトコルの通信の流儀の違いから来ています。
- セッションの流儀(MCP): 通信の最初に挨拶(ハンドシェイク)を交わして「お互いに何ができるか・どのバージョンで話すか」の合意を作り、その接続を保持したまま、以降のやりとりを「合意の続き」として行う。電話のイメージで、切れたら挨拶からやり直しになります
-
Webサービスの流儀(A2A): 接続に合意や状態を持たせず、1つ1つのリクエストが自己完結する。相手の能力は事前に取得したAgent Cardで、バージョンの合意は毎リクエストの
A2A-Versionヘッダで、認証はHTTP標準の仕組みで、それぞれリクエスト単位で成立させる。通常のWeb APIと同じイメージです
A2Aでは各リクエストが自己完結しているので、ハンドシェイクを置く場所がそもそもありません。
2. スキルは「宛先」ではなく「広告」
Agent Cardには skills: [{id: 'add', ...}] とスキルが列挙されていました。ではステップ2のリクエストを改めて確認してみましょう。
{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-001",
"role": "ROLE_USER",
"parts": [{ "text": "{\"a\": 19, \"b\": 23}" }]
}
}
}
スキルID(add)はどこにも指定していません。実はv1.0の SendMessage のパラメータには、スキルIDを入れる場所自体がありません。つまり、クライアントはどのスキルを使うかを指定しないし、そもそも指定できないのです。依頼は常にMessage本文で行われ、どのスキル(内部のどの処理)で応えるかは、受信側のエージェントが本文を解釈して自分で選びます。
MCPの tools/call が name: "add" という指定で対象ツールを選択させる、つまり「何を使うか」の選択をプロトコルが運ぶのと対照的です。Cardのskillsは「こういう依頼を受け付けます」という案内・広告であって、呼び出しのAPIではありません。
3. 入力の型検証は、プロトコルの仕事からエージェントの仕事になった
MCPではツール定義に inputSchema(引数の名前・型・必須項目をJSON Schemaで宣言したもの)が含まれ、定義に合わない引数は業務ロジックに届く前に機械的にエラーにできました。つまり入力の構造はプロトコル層が保証してくれていました。A2Aにはこの仕組みがなく、届いた本文の構造は受け手が自力で復元します。本記事の実装では parseAddRequest の「JSON.parse → 正規表現」という2段構えがその役です。
本記事のサンプルは正規表現で済ませましたが、LLMを組み込んだ実運用のエージェント(世の中で提供されているエージェント製品)では、この解釈をLLMが担います。本文をLLMが解釈し、内部の型付き関数の呼び出しに変換する — つまり構造はこうなります。
[クライアント → エージェント] 自然文(型なし・A2A)
[エージェント内部] LLMが型付き引数に変換
[エージェント → 内部の道具] 構造化された関数呼び出し(型あり)
A2Aは「相手は自然文を理解できる知能を持つ」という前提を置いたことで、プロトコル境界の型を緩められた、と整理できます。型の検証がなくなったのではなく、検証の責任者がプロトコルからエージェントに変わった、というのが正確な理解です。
4. エンドポイントのパスは自由(「v1」は本実装が付けた飾り)
本記事のエンドポイント /a2a/v1 の「v1」は本実装が勝手に付けた命名で、バージョンの意味はありません。A2A仕様はパスの形式を規定せず、クライアントはCardの supportedInterfaces[].url の文字列をそのまま使うだけです。バージョンが効くのは、Cardの protocolVersion フィールドと A2A-Version ヘッダの2箇所だけです。
5. 実行までの往復が少ない代わりに、状態管理は別の場所で必要になる
「接続もハンドシェイクも無いなら、MCPよりずっと簡単では?」というのが正直な第一印象だと思います。ただしそれは、今回が即答できる依頼だからです。接続が状態を持たない設計の代わりに、A2Aでは時間のかかる作業の状態管理をTaskという明示的なオブジェクトが引き受けます。ここが次回の主題です。
MCP addツールとの対応表
同じ足し算を両プロトコルで実装した差分のまとめです。比較対象の「MCP版」は、本記事の移植元にした前作第1回のstdio版addツールです(MCPにはリモート用のStreamable HTTPトランスポートもあります。前作第2回参照)。
| 観点 | MCP版 | A2A版(本記事) |
|---|---|---|
| トランスポート | stdio(パイプ)。ほかにリモート用のStreamable HTTPがあり、ローカル/リモートの2形態を持つ | HTTPのみ。A2Aの標準バインディングは3つともHTTP(S)上で、stdioのようなローカル形態は無い(相手は自分が起動する子プロセスではなく、独立して稼働する対等なサービスという前提のため) |
| 能力の公開 | 接続後に initialize + tools/list
|
接続前に Agent Card。ハンドシェイク自体が無い |
| 呼び出し |
tools/call {name:"add", arguments:{a,b}}
|
SendMessage。メッセージ本文で依頼し、スキル指定は無い |
| 入力の型 |
inputSchema で構造を機械的に強制 |
強制なし。本文の解釈はエージェントの責任 |
| 結果 | result.content[] |
result.message.parts[] |
| 実行の失敗 |
result に isError: true
|
「できません」という返答Message(プロトコルエラーにしない点は共通) |
| MCP版の実装からそのまま流用できた部分 | —(移植元) | JSON-RPC 2.0の骨格(エラーコードの体系・idの対応付け・表引きハンドラ)と、addの計算本体 |
まとめ
- A2Aエージェントの正体は「Agent Cardを配り、
SendMessageを受けるHTTPサーバ」。依存ゼロの170行で書ける - 発見はwell-known URIの静的JSONで完結し、ハンドシェイクは存在しない。能力交換・バージョン合意・認証はすべてWeb標準の仕組みに乗る
- スキルは広告であって宛先ではない。依頼は常に本文で行われ、解釈は受け手の責任
- v1.0はprotoファーストへの作り直しで、0.x系とはメソッド名からrole値まで非互換。資料を読むときは系統の確認を
- リクエストの書式が1.0系なら、
A2A-Version: 1.0ヘッダの指定も必須(ヘッダが無いリクエストは0.3の書式とみなされるため)
次回: 足し算に3秒かかる「遅いエージェント」を作り、Taskライフサイクルを深掘りします。応答がMessageからTaskに変わり、SUBMITTED → WORKING → COMPLETED の状態遷移、途中で質問される INPUT_REQUIRED と再開、成果物を運ぶArtifact、そしてSSEによるストリーミング配信までを、すべて実測ログで確認します。