uho-wq.dev

AIエージェント・プロトコル実装ガイド ― MCP / A2A / AG-UI / x402 / DID をGoで書く

AIエージェント・プロトコル実装ガイド ― MCP / A2A / AG-UI / x402 / DID をGoで書く

結論を先に言う。 エージェントプロトコル群は、概念図で眺めるとレイヤが綺麗に分かれていることが分かるが、実装に降ろした瞬間に「結局なにを書けばいいのか」が見えなくなる。本稿は、2026年6月末時点で実用性のある主要プロトコル ―― MCP / A2A / AG-UI / x402 / DID ―― を、Goでの具体的なサーバ/クライアント実装まで踏み込んで解説する。SSEのクライアントだけはiOS/Swiftで補う。

全体マップ ― 8つのレイヤ

最初に俯瞰しておく。乱立しているように見えるエージェントプロトコルは、「誰と何を交換するか」という軸でおおむね8レイヤに収まる。

レイヤ役割代表的なプロトコル本稿でGo実装
1. Agent ↔ Tool / Dataツール・データへの接続MCP, WebMCP
2. Agent ↔ Agentエージェント間の水平協調A2A, ACP, ANP, AConP, SLIM
3. Agent ↔ UI / Userフロントエンド連携AG-UI, A2UI◯ + Swift
4. Discovery / Identity発見・識別・命名OASF, ADS, Agent Card, DID
5. Payment / Commerce決済・商取引x402, AP2, ACP(商取引), UCP, MPP, Trusted Agent Protocol
6. 通信プロトコル(研究系)適応的・分散的な対話Agora, AITP, Coral, LOKA, PXP
7. インターフェース宣言 / 基盤API/エージェント宣言agents.json, Agent Protocol (AIEF), LMOS
8. AGNTCY スタック統合スタックのコンポーネントOASF / SLIM / AConP / ADS / Observability

ガバナンスでは MCP (Anthropic→Linux Foundation, 2025年12月寄贈)、A2A (Google Cloud, 2025年4月発表→2025年6月Linux Foundation移管)、AGNTCY (Cisco主導, Linux Foundation配下) と、いずれもLinux Foundation配下に集まる流れになっている。実装で投資する対象としても安全性が高い。

それでは順に手を動かしていく。

1. MCP ― Go SDKでツールサーバを書く

MCP(Model Context Protocol)は、エージェントがツールやデータソースを呼ぶための標準プロトコルだ。トランスポートは stdio と Streamable HTTP の2系統、メッセージは JSON-RPC 2.0 で、tools/list tools/call resources/read prompts/get などのメソッドが定義されている。

Goでは公式SDK github.com/modelcontextprotocol/go-sdk が提供されている。「指定された都市の天気を返す」だけのツールサーバを書いてみる。

// cmd/mcp-weather/main.go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type GetWeatherInput struct {
	City string `json:"city" jsonschema:"description=City name in English"`
}

type GetWeatherOutput struct {
	City        string  `json:"city"`
	TemperatureC float64 `json:"temperature_c"`
	Condition   string  `json:"condition"`
}

func getWeather(ctx context.Context, req *mcp.CallToolRequest, in GetWeatherInput) (
	*mcp.CallToolResult, GetWeatherOutput, error,
) {
	// 実運用では外部APIを叩く。ここではダミー応答。
	out := GetWeatherOutput{
		City:        in.City,
		TemperatureC: 22.5,
		Condition:   "Sunny",
	}
	text := fmt.Sprintf("%s is %.1f°C and %s.", out.City, out.TemperatureC, out.Condition)
	return &mcp.CallToolResult{
		Content: []mcp.Content{&mcp.TextContent{Text: text}},
	}, out, nil
}

func main() {
	srv := mcp.NewServer(
		&mcp.Implementation{Name: "weather", Version: "0.1.0"},
		nil,
	)

	mcp.AddTool(srv, &mcp.Tool{
		Name:        "get_weather",
		Description: "Get current weather for a city.",
	}, getWeather)

	// stdio トランスポート(Claude Desktop / Codex CLI などが想定)
	if err := srv.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
		log.Fatal(err)
	}
}

ポイントは AddTool がジェネリクスで型を取り、入力スキーマを jsonschema タグから自動生成する点だ。クライアント(Claude Desktop など)が tools/list を投げると、SDKがこの構造体定義からJSON Schemaを組み立てて返す。手書きでスキーマを書く必要がない。

クライアント側で同じSDKを使い、上のサーバを呼ぶコードはこうなる。

// cmd/mcp-call/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os/exec"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
	ctx := context.Background()
	client := mcp.NewClient(&mcp.Implementation{Name: "demo", Version: "0.1.0"}, nil)

	// 子プロセスとしてサーバを起動し、stdio で接続する
	cmd := exec.Command("./mcp-weather")
	session, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	res, err := session.CallTool(ctx, &mcp.CallToolParams{
		Name:      "get_weather",
		Arguments: map[string]any{"city": "Tokyo"},
	})
	if err != nil {
		log.Fatal(err)
	}
	for _, c := range res.Content {
		if t, ok := c.(*mcp.TextContent); ok {
			fmt.Println(t.Text) // → "Tokyo is 22.5°C and Sunny."
		}
	}
}

実運用では Streamable HTTP トランスポート(mcp.NewStreamableHTTPHandler)でリモートサーバとして公開し、認証は OAuth 2.1 + PKCE に乗せるのが定石だ。仕様としても2025年3月版から認可ベースラインがOAuth 2.1に統一されている。

派生として、ブラウザ/Web側からツールを露出する WebMCP がある。位置づけはW3CのDraft Community Group Reportで、2026年6月時点ではまだ標準化前。Webサイト自身が、訪れたエージェントに対して機能をMCPツールとして提供する、というクライアントサイドのMCPだ。

2. A2A ― Agent CardをGoで配信し、タスクを委譲する

A2A(Agent2Agent)はGoogle Cloud発(2025年4月)→ Linux Foundation配下(2025年6月)のプロトコルで、エージェント間のタスク委譲を担う。MCPがツール接続なら、A2Aはエージェントへの委譲、と切り分けるのが分かりやすい。

A2Aの心臓は Agent Card ―― エージェントが自身の能力を機械可読JSONで宣言するカードだ。クライアントエージェントは /.well-known/agent-card.json を取得し、相手の能力を把握してからタスクを投げる。

Agent Card を Go で配信する

// cmd/a2a-agent/main.go
package main

import (
	"encoding/json"
	"log"
	"net/http"
)

type Skill struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Tags        []string `json:"tags"`
	InputModes  []string `json:"inputModes"`
	OutputModes []string `json:"outputModes"`
}

type AgentCard struct {
	ProtocolVersion string   `json:"protocolVersion"`
	Name            string   `json:"name"`
	Description     string   `json:"description"`
	URL             string   `json:"url"`
	Version         string   `json:"version"`
	Capabilities    map[string]any `json:"capabilities"`
	DefaultInputModes  []string `json:"defaultInputModes"`
	DefaultOutputModes []string `json:"defaultOutputModes"`
	Skills          []Skill  `json:"skills"`
}

var card = AgentCard{
	ProtocolVersion: "0.3.0",
	Name:            "translation-agent",
	Description:     "Translates plain text between Japanese and English.",
	URL:             "https://example.com/a2a",
	Version:         "1.0.0",
	Capabilities: map[string]any{
		"streaming":         true,
		"pushNotifications": false,
	},
	DefaultInputModes:  []string{"text/plain"},
	DefaultOutputModes: []string{"text/plain"},
	Skills: []Skill{{
		ID:          "translate",
		Name:        "Translate JA<->EN",
		Description: "Translate text between Japanese and English.",
		Tags:        []string{"translation", "nlp"},
		InputModes:  []string{"text/plain"},
		OutputModes: []string{"text/plain"},
	}},
}

func main() {
	http.HandleFunc("/.well-known/agent-card.json", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(card)
	})
	http.HandleFunc("POST /a2a", handleTaskRPC) // 次節で実装
	log.Fatal(http.ListenAndServe(":8080", nil))
}

タスク委譲を JSON-RPC で受ける

A2A の通信本体は JSON-RPC 2.0 over HTTPS で、メソッドは message/send(同期)と message/stream(SSE)が中心だ。同期版の最小実装はこうなる。

func handleTaskRPC(w http.ResponseWriter, r *http.Request) {
	var req struct {
		JSONRPC string          `json:"jsonrpc"`
		ID      json.RawMessage `json:"id"`
		Method  string          `json:"method"`
		Params  struct {
			Message struct {
				Role  string `json:"role"`
				Parts []struct {
					Kind string `json:"kind"`
					Text string `json:"text"`
				} `json:"parts"`
			} `json:"message"`
		} `json:"params"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	if req.Method != "message/send" {
		writeRPCError(w, req.ID, -32601, "method not found")
		return
	}

	// ここで実翻訳。サンプルはエコー。
	input := req.Params.Message.Parts[0].Text
	output := "[ja->en] " + input

	resp := map[string]any{
		"jsonrpc": "2.0",
		"id":      req.ID,
		"result": map[string]any{
			"kind": "message",
			"role": "agent",
			"parts": []map[string]any{
				{"kind": "text", "text": output},
			},
		},
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(resp)
}

func writeRPCError(w http.ResponseWriter, id json.RawMessage, code int, msg string) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"jsonrpc": "2.0",
		"id":      id,
		"error":   map[string]any{"code": code, "message": msg},
	})
}

呼び出し側のエージェントはまずカードを取り、/a2a に JSON-RPC を投げる。これだけで「翻訳エージェント」へのタスク委譲が成立する。

流派の整理 ― ACP / ANP / AConP / SLIM

A2A 以外の水平協調プロトコルも併せて押さえておく。

  • ACP(Agent Communication Protocol):IBM BeeAI 発の、SDK不要・REST中心の軽量メッセージング。後述する決済の「ACP(Agentic Commerce Protocol)」とは別物。同名衝突に注意。
  • ANP(Agent Network Protocol):HTTPS/DNS の上で P2P 直接通信する分散志向。三層構成(アイデンティティ/メタプロトコル交渉/アプリケーション)が特徴で、DID を採用する。
  • AConP / SLIM:AGNTCY スタックのコンポーネント。AConP が接続確立、SLIM (Secure Low-latency Interactive Messaging) がメッセージング層を担う。

実装観点では、A2A の Agent Card + JSON-RPC が今いちばん書ける選択肢だ。

3. AG-UI ― GoでSSEを配信し、iOS/Swiftで受ける

エージェントからユーザーへ向けた進捗・中間結果のストリーミングを標準化するのが AG-UI(CopilotKit主導)だ。ライフサイクル、テキスト、ツール呼び出し、状態同期、特殊イベントの5カテゴリ・約16イベントが定義されている。2026年3月には AWS Bedrock AgentCore Runtime がAG-UI対応を追加した。

ここではGoのSSEサーバと、それを受けるiOS/Swiftクライアントを書く。

Go側のSSE配信

// cmd/agui/main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"
)

type event struct {
	Type string
	Data map[string]any
}

func writeSSE(w http.ResponseWriter, ev event) error {
	b, err := json.Marshal(ev.Data)
	if err != nil {
		return err
	}
	if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", ev.Type, b); err != nil {
		return err
	}
	if f, ok := w.(http.Flusher); ok {
		f.Flush()
	}
	return nil
}

func runHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	threadID, runID := "t_1", "r_1"
	msgID := "m_1"

	events := []event{
		{"RUN_STARTED", map[string]any{"threadId": threadID, "runId": runID}},
		{"TEXT_MESSAGE_START", map[string]any{"messageId": msgID, "role": "assistant"}},
		{"TEXT_MESSAGE_CONTENT", map[string]any{"messageId": msgID, "delta": "今週の売上を集計します"}},
		{"TOOL_CALL_START", map[string]any{"toolCallId": "tc_1", "toolCallName": "query_sales"}},
		{"TOOL_CALL_END", map[string]any{"toolCallId": "tc_1"}},
		{"STATE_DELTA", map[string]any{
			"delta": []map[string]any{
				{"op": "add", "path": "/sales/mon", "value": 1_200_000},
			},
		}},
		{"TEXT_MESSAGE_END", map[string]any{"messageId": msgID}},
		{"RUN_FINISHED", map[string]any{"threadId": threadID, "runId": runID}},
	}

	for _, ev := range events {
		if err := writeSSE(w, ev); err != nil {
			log.Printf("write: %v", err)
			return
		}
		select {
		case <-time.After(150 * time.Millisecond):
		case <-r.Context().Done():
			return
		}
	}
}

func main() {
	http.HandleFunc("POST /agui/run", runHandler)
	log.Fatal(http.ListenAndServe(":8081", nil))
}

ポイントを2つ。

  • http.Flusher を必ず使う。Goの net/http はデフォルトでレスポンスをバッファするため、Flushしないとクライアントに届かない。
  • r.Context().Done() を監視して早期切断に対応する。SSEは長時間接続なのでクライアントが先に閉じることが多い。

iOS / Swiftクライアント(URLSessionでSSEを受ける)

ブラウザなら EventSource 一発だが、iOSにはネイティブSSEがない。URLSession のストリームを行単位で読み、自前で event: / data: をパースする。

// AGUIClient.swift
import Foundation

enum AGUIEvent {
    case runStarted(threadId: String, runId: String)
    case textContent(messageId: String, delta: String)
    case toolCallStart(toolCallId: String, name: String)
    case stateDelta(patches: [[String: Any]])
    case runFinished(threadId: String, runId: String)
    case other(name: String, payload: [String: Any])
}

actor AGUIClient {
    let endpoint: URL

    init(endpoint: URL) {
        self.endpoint = endpoint
    }

    func run() -> AsyncThrowingStream<AGUIEvent, Error> {
        AsyncThrowingStream { continuation in
            Task {
                do {
                    var req = URLRequest(url: endpoint)
                    req.httpMethod = "POST"
                    req.setValue("text/event-stream", forHTTPHeaderField: "Accept")

                    let (bytes, _) = try await URLSession.shared.bytes(for: req)
                    var eventName = ""
                    var dataBuffer = ""

                    for try await line in bytes.lines {
                        if line.isEmpty {
                            if let event = Self.parse(name: eventName, data: dataBuffer) {
                                continuation.yield(event)
                            }
                            eventName = ""
                            dataBuffer = ""
                            continue
                        }
                        if line.hasPrefix("event:") {
                            eventName = line.dropFirst("event:".count)
                                .trimmingCharacters(in: .whitespaces)
                        } else if line.hasPrefix("data:") {
                            dataBuffer += line.dropFirst("data:".count)
                                .trimmingCharacters(in: .whitespaces)
                        }
                    }
                    continuation.finish()
                } catch {
                    continuation.finish(throwing: error)
                }
            }
        }
    }

    private static func parse(name: String, data: String) -> AGUIEvent? {
        guard !name.isEmpty, !data.isEmpty,
              let raw = data.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: raw) as? [String: Any]
        else { return nil }

        switch name {
        case "RUN_STARTED":
            return .runStarted(
                threadId: json["threadId"] as? String ?? "",
                runId: json["runId"] as? String ?? ""
            )
        case "TEXT_MESSAGE_CONTENT":
            return .textContent(
                messageId: json["messageId"] as? String ?? "",
                delta: json["delta"] as? String ?? ""
            )
        case "TOOL_CALL_START":
            return .toolCallStart(
                toolCallId: json["toolCallId"] as? String ?? "",
                name: json["toolCallName"] as? String ?? ""
            )
        case "STATE_DELTA":
            return .stateDelta(patches: json["delta"] as? [[String: Any]] ?? [])
        case "RUN_FINISHED":
            return .runFinished(
                threadId: json["threadId"] as? String ?? "",
                runId: json["runId"] as? String ?? ""
            )
        default:
            return .other(name: name, payload: json)
        }
    }
}

SwiftUI側はこのストリームをイベントごとに @Observable なViewModelへ流し込み、TEXT_MESSAGE_CONTENT を逐次追記、STATE_DELTA をJSON Patch(RFC 6902)として適用すれば、ブラウザ実装と同等のUI更新ができる。

A2UI との関係

AG-UIがイベントストリーム寄りなのに対し、A2UIUIの宣言的記述そのものを扱う。両者は競合ではなく組み合わせるレイヤで、AG-UIのSTATE_DELTAの中身としてA2UIスニペットを流すといった統合が想定される。詳細は別記事「Generative UIとは何か ― AG-UIとA2UIで読み解くエージェント時代のUI」を参照。

4. Discovery / Identity ― Go で DID Web を解決する

エージェントの世界では、「相手は本物か」を検証する識別レイヤが必要になる。中心的な標準はW3Cの DID(Decentralized Identifiers) で、ANPはこれを採用している。

DIDのうち、Web上で運用しやすいのが did:web メソッドだ。did:web:example.com:agents:alicehttps://example.com/agents/alice/did.json を取って解決する、というシンプルなルールになっている。Goでは標準ライブラリだけで書ける。

// internal/didweb/resolver.go
package didweb

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"
)

type DIDDocument struct {
	Context            []string `json:"@context"`
	ID                 string   `json:"id"`
	VerificationMethod []struct {
		ID                 string         `json:"id"`
		Type               string         `json:"type"`
		Controller         string         `json:"controller"`
		PublicKeyJwk       map[string]any `json:"publicKeyJwk,omitempty"`
	} `json:"verificationMethod"`
	Service []struct {
		ID              string `json:"id"`
		Type            string `json:"type"`
		ServiceEndpoint string `json:"serviceEndpoint"`
	} `json:"service,omitempty"`
}

func Resolve(ctx context.Context, did string) (*DIDDocument, error) {
	if !strings.HasPrefix(did, "did:web:") {
		return nil, errors.New("not a did:web identifier")
	}
	parts := strings.Split(strings.TrimPrefix(did, "did:web:"), ":")
	if len(parts) == 0 {
		return nil, errors.New("empty did:web identifier")
	}

	// did:web:example.com           → https://example.com/.well-known/did.json
	// did:web:example.com:agents:a  → https://example.com/agents/a/did.json
	host, err := url.PathUnescape(parts[0])
	if err != nil {
		return nil, fmt.Errorf("decode host: %w", err)
	}
	u := &url.URL{Scheme: "https", Host: host}
	if len(parts) == 1 {
		u.Path = "/.well-known/did.json"
	} else {
		segs := make([]string, 0, len(parts)-1)
		for _, p := range parts[1:] {
			seg, err := url.PathUnescape(p)
			if err != nil {
				return nil, fmt.Errorf("decode path: %w", err)
			}
			segs = append(segs, seg)
		}
		u.Path = "/" + strings.Join(segs, "/") + "/did.json"
	}

	cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	req, _ := http.NewRequestWithContext(cctx, http.MethodGet, u.String(), nil)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("status %d for %s", resp.StatusCode, u.String())
	}

	var doc DIDDocument
	if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
		return nil, err
	}
	if doc.ID != did {
		return nil, fmt.Errorf("did mismatch: doc=%q req=%q", doc.ID, did)
	}
	return &doc, nil
}

注意点:

  • did:web:: はURLのパス区切り / に変換する。ただしポート番号を含むホストは localhost%3A8080 のようにパーセントエンコードされる仕様なので、ホスト部分も PathUnescape を通している。
  • 取得したドキュメントの id が要求した DID と一致するかを必ず検証する。これは仕様の MUST 要件で、外しがちなところだ。
  • HTTPSは必須。TLSの検証を切るオプションは絶対に提供しない。

エージェント側はこのドキュメント中の verificationMethod.publicKeyJwk を使い、相手が発行した JWS の署名を検証する流れになる。Agent Card と DID document を組み合わせれば「能力(card)」と「身元(did)」がプロトコルとして揃う

発見・識別レイヤの周辺には、他に OASF(AGNTCY内では「エージェントのDNS」と呼ばれる能力スキーマ)、ADS(AGNTCYのディレクトリ)、Agent Card(A2A)、ANS / AgentDNS / NANDA といった名前解決系がある。階層で見れば「DID=身分証」「Agent Card / OASF=能力記述」「ADS / ANS等=ディレクトリ」と守備範囲が分かれる。

5. x402 ― Goで HTTP 402 ミドルウェアを書く

x402 はCoinbase主導の、HTTP 402 Payment Required を使ったステーブルコインネイティブ決済レールだ。2026年5月時点で1.65億件超のエージェント取引を処理しており、実運用トラクションでは現時点トップ。フローはシンプル。

  1. クライアントが保護されたエンドポイントにリクエスト
  2. サーバが 402 Payment Required と支払い要件(receiver、amount、network 等)を返す
  3. クライアントが署名済みの payment payload を X-PAYMENT ヘッダに乗せて再送
  4. サーバ(または facilitator)が検証 → 決済処理 → 元のレスポンスを返す

Goで最小ミドルウェアを書くとこうなる。実際の検証・決済は facilitator に委ねる構成にしている。

// internal/x402/middleware.go
package x402

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
)

type Requirement struct {
	Scheme     string `json:"scheme"`      // "exact"
	Network    string `json:"network"`     // "base", "base-sepolia"
	MaxAmount  string `json:"maxAmountRequired"` // atomic units in string
	Asset      string `json:"asset"`       // token contract address
	PayTo      string `json:"payTo"`       // receiver address
	Resource   string `json:"resource"`    // URL of the protected resource
	MimeType   string `json:"mimeType"`
	MaxTimeout int    `json:"maxTimeoutSeconds"`
}

type PaymentRequiredBody struct {
	X402Version int           `json:"x402Version"`
	Accepts     []Requirement `json:"accepts"`
	Error       string        `json:"error,omitempty"`
}

type Facilitator interface {
	Verify(payload []byte, req Requirement) error
	Settle(payload []byte, req Requirement) error
}

func Middleware(req Requirement, f Facilitator, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		header := r.Header.Get("X-PAYMENT")
		if header == "" {
			writeRequired(w, req, "missing X-PAYMENT")
			return
		}
		payload, err := base64.StdEncoding.DecodeString(header)
		if err != nil {
			writeRequired(w, req, "X-PAYMENT must be base64")
			return
		}
		if err := f.Verify(payload, req); err != nil {
			writeRequired(w, req, "verify failed: "+err.Error())
			return
		}
		// 検証OK → 本来のレスポンスを返してから決済(または先に決済→応答)
		next.ServeHTTP(w, r)
		_ = f.Settle(payload, req)
	})
}

func writeRequired(w http.ResponseWriter, req Requirement, msg string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusPaymentRequired)
	_ = json.NewEncoder(w).Encode(PaymentRequiredBody{
		X402Version: 1,
		Accepts:     []Requirement{req},
		Error:       msg,
	})
}

実装上の注意。

  • 検証(Verify)と決済(Settle)を分ける。x402は「先に検証してから応答→後で決済」と「先に決済してから応答」の両モードがある。レスポンス本体が高価な計算なら前者、軽い読み出しなら後者を選ぶ。
  • 同じ X-PAYMENT を二度受け取らないよう、nonce 管理は facilitator 側の責務にすることが多い。

決済レイヤの全体観だけ整理しておく。

  • 運ぶ層:x402(Coinbase)、MPP (Stripe×Tempo、2026年3月18日メインネット開始、セッション上限承認+マイクロペイメントストリーム)
  • 認可する層AP2(Google、ユーザーが「いくらまで」を署名する限定的デジタル契約)
  • 商取引フロー層ACP(Agentic Commerce Protocol)(OpenAI + Stripe、2026年2月 ChatGPT Instant Checkout で開始)、UCP(Google + Shopify、2026年1月発表)
  • 認証する層Trusted Agent Protocol(Visa、attestation)

x402 と AP2 は直交軸として組み合わさり得る ―― x402で「決済を運び」、AP2で「決済の権限を限定する」、というのが現実的なスタックだ。

6〜8. 研究系・基盤系 ― 実装の選択肢としての位置づけ

実装の主戦場は1〜5レイヤなので残りは要点だけ触れる。

研究/コミュニティ系の通信プロトコル

  • Agora:固定スキーマでなく、エージェント同士が通信規約そのものを交渉・進化させる適応的アプローチ。ANPの「メタプロトコル交渉」と思想的に近い。
  • AITP:NEAR発。経済的トランザクション軸。
  • Coral Protocol:分散的な相互運用性、共有オントロジー、セマンティックグラウンディング。
  • LOKA:アイデンティティ・信頼に加え倫理をプロトコル層に埋め込む。
  • PXP:タスク指向対話における相互理解可能性にフォーカスした研究プロトタイプ。

インターフェース宣言/プラットフォーム

  • agents.jsonrobots.txt 的にWebサイトがAI連携機能を宣言する機械可読フォーマット。
  • Agent Protocol(AI Engineer Foundation):OpenAPI v3上にエージェントのライフサイクル操作(起動・停止・監視)を定義。コントロールコンソール↔エージェントの標準。
  • LMOS(Language Model Operating System):Eclipseのオープンソースプロジェクト。マルチエージェントシステムのフルスタック基盤。

AGNTCY スタック:Cisco主導・Linux Foundation配下で、OASF / SLIM / AConP / ADS / Observability といった複数レイヤのコンポーネントをまとめて提供する。AGNTCY を「単一プロトコル」だと誤解すると話が噛み合わない ―― 実態は複数レイヤのオープンスタックだ。

採用指針 ― 「何を書くか」

最後に、Goでエージェント基盤を作るときの現実的な指針を整理する。

やりたいこと採用すべきプロトコルGoでの主な道具
ツールサーバを作るMCPmodelcontextprotocol/go-sdk
別エージェントへ仕事を委譲A2Anet/http + JSON-RPC + Agent Card
進捗をUIに出すAG-UI (+ Swift クライアント)net/http SSE + http.Flusher
エージェントの身元保証DID(W3C)+ Agent Cardnet/httpdid:web 解決
自律決済を許すx402 (+ AP2)HTTP 402 ミドルウェア + facilitator

ポイントは、1つのGoサービスがMCPサーバ・A2Aエンドポイント・AG-UI配信・x402ゲートを兼ねられることだ。各プロトコルは独立した別ポート/別パスで共存できるよう設計されている。Linux Foundation 配下に集まった主要3本(MCP / A2A / AGNTCY)の中立性も担保された今、Goで足場を組む投資対効果は十分高い。

あとは 「同じACP」が2種類あるような小さな罠を避けながら、レイヤごとに実装を積み上げていけばいい。


参照(一次ソース/公式サイト)

本稿のサンプルコードは2026年6月30日時点で確認できる仕様をもとにしています。SDKやプロトコル仕様は急速に更新されているため、実装にあたっては各公式ドキュメントの最新版を参照してください。