'UCP'

AI Agent 핵심 프로토콜 완전 정복: MCP부터 AG-UI까지

0. 들어가며 ✍️ Reference: https://developers.googleblog.com/developers-guide-to-ai-agent-protocols/ 최근 AI Agent 개발 분야는 MCP, A2A, UCP, AP2, A2UI, AG-UI 등 수많은 약어로 인해 혼란이 가중되고 있습니다. 바로 어제, Google for Dev...

2026년 3월 20일13min read

0. 들어가며 ✍️

Reference: https://developers.googleblog.com/developers-guide-to-ai-agent-protocols/

최근 AI Agent 개발 분야는 MCP, A2A, UCP, AP2, A2UI, AG-UI 등 수많은 약어로 인해 혼란이 가중되고 있습니다.

바로 어제, Google for Developers 블로그에 이 6가지 프로토콜을 하나의 레스토랑 공급망 시나리오로 엮어, 각 프로토콜이 해결하는 문제를 실전 코드와 함께 단계별로 설명하는 가이드가 게시되었습니다.

이 글은 해당 가이드를 바탕으로, Google의 ADK(Agent Development Kit)를 활용해 재고 확인 → 견적 → 주문 → 결제 → 대시보드 렌더링까지 이어지는 전체 구조를 소개합니다. 각 프로토콜이 등장하는 이유, 해결하는 문제, 이전 프로토콜과의 관계를 중심으로 설명하겠습니다.

6개 프로토콜의 역할을 한 줄로 요약하면 이렇습니다.

- MCP: 에이전트를 데이터·도구에 연결

- A2A: 에이전트끼리 연결

- UCP: 에이전트가 실제 거래를 수행

- AP2: 그 거래에 승인과 감사 추적을 추가

- A2UI: 에이전트가 동적 UI를 생성

- AG-UI: 에이전트 응답을 프런트엔드에 실시간으로 스트리밍


1. MCP: Model Context Protocol ✍️

문제: 에이전트를 기존 시스템에 연결하는 일이 너무 번거롭다 ✅

주방 관리 에이전트가 재고를 확인하고 공급업체에 이메일을 보내려면, 지금까지는 각 기능에 맞춰 API 연동 코드를 일일이 작성해야 했습니다. 연결하려는 서비스의 기능이 10개라면, 그 서비스 하나를 위해 10개의 커스텀 도구를 만들고 직접 유지보수해야 했죠.

해결: 표준 연결 규격으로 수백 개의 서비스를 단번에 ✅

MCP는 소위 '노가다'를 없애줍니다. MCP 서버는 "나에게는 이런 도구들이 있다"고 스스로 알려주고, 에이전트는 이를 자동으로 탐색해 사용합니다. 더 중요한 점은, MCP 서버를 해당 시스템을 만든 팀이 직접 관리하기 때문에 개발자가 연동 코드를 새로 짜거나 업데이트할 필요가 없다는 것입니다.

MCP 구조 다이어그램

Google의 ADK는 McpToolset을 통해 이 표준 연결을 구현합니다. 아래 코드처럼 PostgreSQL 데이터베이스 조회(Toolbox), 레시피 확인(Notion MCP), 이메일 발송(Mailgun MCP)을 각각 독립된 MCP 서버로 연결하고, 에이전트에 한꺼번에 넘기면 됩니다.

python
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from google.adk.tools.toolbox_toolset import ToolboxToolset
from mcp import StdioServerParameters

# [Database MCP] 창고 재고(소고기, 연어 등)를 조회하는 도구
inventory_tools = ToolboxToolset(server_url=TOOLBOX_URL)

# [Notion MCP] 레시피 및 거래처 연락처를 조회하는 도구
notion_tools = McpToolset(
    connection_params=StdioConnectionParams(
        server_params=StdioServerParameters(
            command="npx",
            args=["-y", "@notionhq/notion-mcp-server"],
            env={"NOTION_TOKEN": NOTION_TOKEN}
        ),
        timeout=30
    )
)

# [Mailgun MCP] 부족한 재고를 채우기 위해 공급업체에 이메일을 보내는 도구
mailgun_tools = McpToolset(
    connection_params=StdioConnectionParams(
        server_params=StdioServerParameters(
            command="npx",
            args=["-y", "@mailgun/mcp-server"],
            env={"MAILGUN_API_KEY": MAILGUN_API_KEY}
        ),
        timeout=30
    )
)

# [에이전트 생성] 이름, 모델, 지시 사항, 사용할 도구를 조합해 에이전트를 만든다
kitchen_agent = Agent(
    model="gemini-3-flash-preview",
    name="kitchen_manager",
    instruction="당신은 식당 주방을 관리합니다. 재고를 확인하고, 레시피를 찾아보며, 공급업체에 이메일을 보내세요.",
    tools=[inventory_tools, notion_tools, mailgun_tools],
)

2. A2A: Agent to Agent Protocol ✍️

문제: 에이전트가 다른 팀의 에이전트와 통신하려면 매번 커스텀 코드가 필요하다 ✅

MCP가 데이터 접근 문제를 해결했다면, 그다음 과제는 전문 지식의 획득입니다. 주방 관리 에이전트는 재고 현황은 파악할 수 있지만, 오늘의 도매가, 공급업체 품질 등급, 배송 가능 시간은 알지 못합니다. 이 정보는 각기 다른 팀이 서로 다른 프레임워크로 개발하고 개별 서버에서 운영하는 '원격 에이전트'들이 나누어 가지고 있기 때문입니다.

표준 프로토콜이 없다면, 원격 에이전트가 변경될 때마다 커스텀 통합 코드를 다시 작성하고, 테스트하고, 재배포해야 합니다.

해결: Agent Card로 에이전트를 자기소개하게 만들다 ✅

A2A 프로토콜은 에이전트들이 서로를 발견하고 소통하는 방식을 표준화합니다. 모든 A2A 에이전트는 잘 알려진 경로(/.well-known/agent-card.json)에 자신의 이름, 기능, 엔드포인트를 기술한 Agent Card를 공개합니다.

A2A 구조 다이어그램

주방 관리 에이전트는 이 카드를 가져와 상대 에이전트가 무엇을 할 수 있는지 파악한 뒤, 실행 시점에 적절한 에이전트로 요청을 라우팅합니다. 새로운 원격 에이전트를 추가할 때는 URL 하나만 등록하면 됩니다. 수동 코드 수정이나 재배포가 필요 없습니다.

아래 코드는 MCP(도구 연결)와 A2A(에이전트 간 통신)를 결합한 형태입니다. to_a2a()로 내 에이전트를 외부에 A2A 서비스로 노출하고, ClientFactory로 원격 에이전트에 연결해 질의를 전송합니다.

python
import asyncio
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from google.adk.tools.toolbox_toolset import ToolboxToolset
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from a2a.client.client_factory import ClientFactory
from a2a.client.helpers import create_text_message_object
from mcp import StdioServerParameters

# ── 1단계: 에이전트가 사용할 MCP 도구 설정 ────────────────────────────────

inventory_tools = ToolboxToolset(server_url=TOOLBOX_URL)

notion_tools = McpToolset(
    connection_params=StdioConnectionParams(
        server_params=StdioServerParameters(
            command="npx",
            args=["-y", "@notionhq/notion-mcp-server"],
            env={"NOTION_TOKEN": NOTION_TOKEN}
        ),
        timeout=30
    )
)

mailgun_tools = McpToolset(
    connection_params=StdioConnectionParams(
        server_params=StdioServerParameters(
            command="npx",
            args=["-y", "@mailgun/mcp-server"],
            env={"MAILGUN_API_KEY": MAILGUN_API_KEY}
        ),
        timeout=30
    )
)

# ── 2단계: 에이전트 생성 및 A2A 서비스로 노출 ─────────────────────────────

kitchen_agent = Agent(
    model="gemini-3-flash-preview",
    name="kitchen_manager",
    instruction="당신은 식당 주방을 관리합니다. 재고를 확인하고, 레시피를 찾아보며, 공급업체에 이메일을 보내세요.",
    tools=[inventory_tools, notion_tools, mailgun_tools]
)

# 8080 포트로 에이전트를 A2A 서비스로 노출한다.
# 외부 에이전트는 http://localhost:8080/.well-known/agent-card.json 에서 내 정보를 확인한다.
app = to_a2a(kitchen_agent, port=8080)

# ── 3단계: 원격 에이전트(A2A)와 통신 ─────────────────────────────────────

async def check_wholesale_prices():
    # 가격 조회 전문 에이전트에 연결
    client = await ClientFactory.connect("http://pricing-agent:8001")

    # 연결된 에이전트의 Agent Card 정보를 확인
    card = await client.get_card()
    print(f"연결 성공: {card.name}")

    # 도매가 정보를 요청하고 응답을 스트리밍으로 수신
    message = create_text_message_object(content="오늘 연어 도매가 알려줘")
    async for response in client.send_message(message):
        pass

asyncio.run(check_wholesale_prices())

3. UCP: Universal Commerce Protocol ✍️

문제: 공급업체마다 주문 방식이 제각각이다 ✅

주문 에이전트는 공급업체를 찾아내고 견적을 받을 수 있습니다. 하지만 실제로 주문을 넣으려 하면 문제가 생깁니다. 업체마다 API가 다르기 때문입니다. 5군데 도매상에서 재료를 가져와야 한다면, 5개의 서로 다른 결제 시스템을 일일이 연동해야 합니다.

해결: 어떤 통신 방식이든 주문서 양식은 하나로 ✅

UCP는 상거래의 전 과정을 하나의 데이터 형식으로 통일합니다. REST, MCP, A2A, 브라우저(EP) 등 어떤 통신 방식을 쓰더라도 주문서 구조는 동일하게 적용됩니다. 에이전트는 하나의 통일된 패턴으로 모든 공급업체와 거래할 수 있습니다.

UCP 구조 다이어그램

A2A와 동일한 방식(/.well-known/ucp)으로 공급업체의 상거래 규격을 탐색하고, 표준 주문서 객체를 만들어 체크아웃을 완료합니다. UCP는 표준 REST API를 지원하기 때문에 별도의 전용 SDK 없이 기존 HTTP 클라이언트(httpx, axios 등)를 그대로 사용할 수 있습니다.

python
import asyncio
import httpx
import uuid
from ucp_sdk.models.discovery.profile_schema import UcpDiscoveryProfile
from ucp_sdk.models.schemas.shopping.checkout_create_req import CheckoutCreateRequest
from ucp_sdk.models.schemas.shopping.types.line_item_create_req import LineItemCreateRequest
from ucp_sdk.models.schemas.shopping.types.item_create_req import ItemCreateRequest
from ucp_sdk.models.schemas.shopping.payment_create_req import PaymentCreateRequest

# 1. 공급업체의 UCP 상거래 규격 조회
async def get_ucp_profile(base_url: str) -> UcpDiscoveryProfile:
    async with httpx.AsyncClient() as c:
        res = await c.get(f"{base_url}/.well-known/ucp")
        return UcpDiscoveryProfile.model_validate(res.json())

# 2. 아이템 딕셔너리를 UCP 표준 장바구니 객체로 변환
def create_ucp_checkout_request(items: dict, currency: str = "USD") -> CheckoutCreateRequest:
    line_items = [
        LineItemCreateRequest(quantity=qty, item=ItemCreateRequest(id=item_id))
        for item_id, qty in items.items()
    ]
    return CheckoutCreateRequest(
        currency=currency,
        line_items=line_items,
        payment=PaymentCreateRequest()
    )

# 3. 에이전트 신원 및 중복 결제 방지용 헤더 생성
def create_ucp_headers() -> dict:
    return {
        "UCP-Agent": 'profile="https://kitchen.example/agent"',
        "Idempotency-Key": str(uuid.uuid4()),
        "Request-Id": str(uuid.uuid4())
    }

# 4. 체크아웃 세션 생성
async def create_checkout_session(base_url: str, checkout_req: CheckoutCreateRequest, headers: dict) -> dict:
    async with httpx.AsyncClient() as c:
        res = await c.post(
            f"{base_url}/checkout-sessions",
            json=checkout_req.model_dump(mode="json", by_alias=True, exclude_none=True),
            headers=headers
        )
        return res.json()

# 5. 주문 최종 완료
async def complete_ucp_order(base_url: str, checkout_id: str, headers: dict):
    headers["Idempotency-Key"] = str(uuid.uuid4())  # 완료 요청은 새 키로
    async with httpx.AsyncClient() as c:
        return await c.post(
            f"{base_url}/checkout-sessions/{checkout_id}/complete",
            headers=headers
        )

# ── 통합 실행 예시 ──────────────────────────────────────────────────────────

async def run_ucp_order():
    supplier_url = "http://example-wholesale:8182"
    order_items = {"salmon": 10, "olive_oil": 3}

    await get_ucp_profile(supplier_url)
    checkout_req = create_ucp_checkout_request(order_items)
    headers = create_ucp_headers()
    session = await create_checkout_session(supplier_url, checkout_req, headers)
    result = await complete_ucp_order(supplier_url, session["id"], headers)
    print(f"주문 완료: {result.status_code}")

asyncio.run(run_ucp_order())

4. AP2: Agent Payments Protocol ✍️

문제: 에이전트가 대신 결제했는데 "누가 승인했는가?"를 증명할 수 없다 ✅

앞선 단계에서 주방 관리 에이전트는 공급업체에 주문을 넣는 능력을 갖췄습니다. 하지만 "이 비용 지출을 누가 승인했는가?"라는 문제가 남습니다. 어떤 한도가 설정되었는지, 승인된 업체는 어디인지, 이 권한이 언제 만료되는지에 대한 기록이 전혀 없습니다.

해결: 승인 증명서와 감사 추적을 거래에 내장하다 ✅

AP2는 Typed Mandates를 통해 사용자의 의도를 부인할 수 없는 형태로 기록하고, 모든 거래에 설정 가능한 Guardrails를 강제합니다.

- UCP가 '무엇을, 누구에게' 주문할지를 다룬다면 - AP2는 '누가 이 구매를 승인했는지' 를 관리하며 감사 추적(Audit Trail)을 제공합니다

AP2는 UCP의 확장 기능으로 결합되어, 체크아웃 흐름에 암호화된 승인 증명서를 추가합니다.

AP2 구조 다이어그램

흐름은 세 단계로 구성됩니다.

1. IntentMandate: 사용자가 허용 업체와 자동 승인 한도를 미리 정의 2. PaymentMandate: 에이전트가 특정 장바구니와 금액에 종속된 결제 승인서를 생성. 한도 초과 시 관리자 명시 승인 전까지 서명 보류 3. PaymentReceipt: 결제 완료 후 모든 감사 기록을 영수증으로 마무리

코드가 길어지므로, 지금까지의 내용을 세 파일로 정리합니다.

config.py

python
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from google.adk.tools.toolbox_toolset import ToolboxToolset
from mcp import StdioServerParameters

INVENTORY_SERVER_URL = "TOOLBOX_URL"
NOTION_API_TOKEN = "YOUR_TOKEN"
MAILGUN_API_KEY = "YOUR_KEY"

# [Database MCP] 창고 재고를 조회하는 도구
inventory_tools = ToolboxToolset(server_url=INVENTORY_SERVER_URL)

# [Notion MCP] 레시피 및 거래처 연락처를 조회하는 도구
notion_tools = McpToolset(
    connection_params=StdioConnectionParams(
        server_params=StdioServerParameters(
            command="npx",
            args=["-y", "@notionhq/notion-mcp-server"],
            env={"NOTION_TOKEN": NOTION_API_TOKEN}
        ),
        timeout=30
    )
)

# [Mailgun MCP] 공급업체에 이메일을 보내는 도구
mailgun_tools = McpToolset(
    connection_params=StdioConnectionParams(
        server_params=StdioServerParameters(
            command="npx",
            args=["-y", "@mailgun/mcp-server"],
            env={"MAILGUN_API_KEY": MAILGUN_API_KEY}
        ),
        timeout=30
    )
)

# [에이전트 본체] 페르소나와 도구를 결합
kitchen_agent = Agent(
    model="gemini-3-flash-preview",
    name="kitchen_manager",
    instruction="당신은 식당 주방을 관리합니다. 재고를 확인하고 주문을 진행하세요.",
    tools=[inventory_tools, notion_tools, mailgun_tools],
)

protocols.py

python
import httpx
import uuid
from a2a.client.client_factory import ClientFactory
from ucp_sdk.models.discovery.profile_schema import UcpDiscoveryProfile
from ucp_sdk.models.schemas.shopping.checkout_create_req import CheckoutCreateRequest
from ucp_sdk.models.schemas.shopping.types.line_item_create_req import LineItemCreateRequest
from ucp_sdk.models.schemas.shopping.types.item_create_req import ItemCreateRequest
from ucp_sdk.models.schemas.shopping.payment_create_req import PaymentCreateRequest
from ap2.types.mandate import IntentMandate, PaymentMandate, PaymentMandateContents
from ap2.types.payment_request import PaymentCurrencyAmount, PaymentItem, PaymentResponse
from ap2.types.payment_receipt import PaymentReceipt, Success

# ── A2A ────────────────────────────────────────────────────────────────────

# 상대 에이전트의 주소로 통신 채널을 연결
async def discover_supplier_agent(url: str):
    return await ClientFactory.connect(url)

# ── UCP ────────────────────────────────────────────────────────────────────

# 공급업체의 상거래 규격 조회
async def get_ucp_profile(base_url: str) -> UcpDiscoveryProfile:
    async with httpx.AsyncClient() as c:
        res = await c.get(f"{base_url}/.well-known/ucp")
        return UcpDiscoveryProfile.model_validate(res.json())

# 아이템 딕셔너리를 UCP 표준 장바구니 객체로 변환
def create_ucp_checkout_request(items: dict, currency: str = "USD") -> CheckoutCreateRequest:
    line_items = [
        LineItemCreateRequest(quantity=qty, item=ItemCreateRequest(id=i))
        for i, qty in items.items()
    ]
    return CheckoutCreateRequest(
        currency=currency,
        line_items=line_items,
        payment=PaymentCreateRequest()
    )

# 에이전트 신원 및 중복 결제 방지용 헤더 생성
def create_ucp_headers() -> dict:
    return {
        "UCP-Agent": 'profile="https://kitchen.example/agent"',
        "Idempotency-Key": str(uuid.uuid4()),
        "Request-Id": str(uuid.uuid4())
    }

# ── AP2 ────────────────────────────────────────────────────────────────────

# 에이전트가 사용할 지출 한도와 허용 업체를 사전 정의
def setup_payment_guardrails() -> IntentMandate:
    return IntentMandate(
        natural_language_description="Order salmon & olive oil",
        merchants=["Example Wholesale"],
        intent_expiry="2026-03-21T20:00:00Z"
    )

# 특정 주문에 대해 사용자 권한을 위임한 결제 승인서 생성
def create_payment_mandate(order_id: str, total_value: float, merchant_name: str) -> PaymentMandate:
    contents = PaymentMandateContents(
        payment_mandate_id=str(uuid.uuid4())[:8],
        payment_details_id=order_id,
        payment_details_total=PaymentItem(
            label="Kitchen Order",
            amount=PaymentCurrencyAmount(currency="USD", value=total_value)
        ),
        payment_response=PaymentResponse(request_id=order_id, method_name="CARD"),
        merchant_agent=merchant_name
    )
    mandate = PaymentMandate(payment_mandate_contents=contents)
    mandate.user_authorization = f"signed_{contents.payment_mandate_id}"
    return mandate

# 결제 완료 후 감사용 영수증 발행
def create_payment_receipt(mandate_id: str, total_value: float, confirmation_id: str) -> PaymentReceipt:
    return PaymentReceipt(
        payment_mandate_id=mandate_id,
        payment_id=f"PAY-{str(uuid.uuid4())[:8]}",
        amount=PaymentCurrencyAmount(currency="USD", value=total_value),
        payment_status=Success(merchant_confirmation_id=confirmation_id),
    )

main.py

python
import asyncio
import httpx
import uuid
from config import kitchen_agent
from protocols import (
    discover_supplier_agent,
    get_ucp_profile,
    create_ucp_checkout_request,
    create_ucp_headers,
    setup_payment_guardrails,
    create_payment_mandate,
    create_payment_receipt,
)

async def run_kitchen_workflow():
    order_items = {"salmon": 10, "olive_oil": 3}
    supplier_url = "http://example-wholesale:8182"
    total_price = 294.00

    # 1. [AP2] 지출 한도와 허용 업체를 먼저 설정
    intent = setup_payment_guardrails()

    # 2. [A2A] 가격 조회 전문 에이전트에 연결
    await discover_supplier_agent("http://pricing-agent:8001")

    # 3. [UCP] 공급업체의 상거래 규격 확인
    await get_ucp_profile(supplier_url)

    # 4. [UCP] 표준 장바구니 객체 및 헤더 생성
    checkout_req = create_ucp_checkout_request(order_items)
    headers = create_ucp_headers()

    # 5. [UCP] 체크아웃 세션 생성
    async with httpx.AsyncClient() as c:
        res = await c.post(
            f"{supplier_url}/checkout-sessions",
            json=checkout_req.model_dump(mode="json", by_alias=True, exclude_none=True),
            headers=headers
        )
        session = res.json()

    # 6. [AP2] 결제 승인서 생성 및 헤더에 첨부
    mandate = create_payment_mandate(session["id"], total_price, intent.merchants[0])
    headers["Idempotency-Key"] = str(uuid.uuid4())
    headers["Authorization-Mandate"] = mandate.user_authorization

    # 7. [UCP + AP2] 승인서가 포함된 최종 주문 완료
    async with httpx.AsyncClient() as c:
        final_res = await c.post(
            f"{supplier_url}/checkout-sessions/{session['id']}/complete",
            headers=headers
        )

        if final_res.status_code == 200:
            # 8. [AP2] 감사용 영수증 발행
            receipt = create_payment_receipt(
                mandate.payment_mandate_contents.payment_mandate_id,
                total_price,
                final_res.json().get("confirmation_id", "CONF-001")
            )
            print(f"결제 완료. 영수증 ID: {receipt.payment_id}")
        else:
            print(f"결제 실패: {final_res.status_code}")

if __name__ == "__main__":
    asyncio.run(run_kitchen_workflow())

5. A2UI: Agent to User Interface Protocol ✍️

문제: 에이전트가 보여줄 화면이 늘어날 때마다 프런트엔드 코드도 늘어난다 ✅

이제 주방 관리 에이전트는 재고 확인부터 주문, 결제 승인까지 모두 처리할 수 있습니다. 하지만 모든 결과가 단순 텍스트로만 전달됩니다. 에이전트가 재고 현황 대시보드, 주문서 양식, 업체별 가격 비교표를 보여줘야 한다면, 화면마다 별도의 프런트엔드 컴포넌트를 일일이 만들어야 합니다. 새로운 UI가 필요할 때마다 유지보수해야 할 코드가 계속 쌓입니다.

해결: 18개의 검증된 컴포넌트로 UI를 동적 조립 ✅

A2UI는 에이전트가 미리 검증된 컴포넌트 카탈로그 안에서 새로운 레이아웃을 동적으로 조립할 수 있게 합니다. 행(Row), 열(Column), 텍스트 필드(TextField) 같이 안전함이 검증된 단 18개의 primitives로 구성된 선언적 JSON 형식을 사용합니다.

A2UI 구조 다이어그램

핵심은 UI 구조와 데이터를 철저히 분리한다는 점입니다.

- 에이전트는 먼저 ID로 서로를 참조하는 평면적인 컴포넌트 리스트를 전송합니다. - 이어서 별도의 데이터 페이로드를 전송합니다. - 클라이언트 측 렌더러(Lit, Flutter, Angular 등)가 JSON을 받아 네이티브 UI로 변환합니다.

이 분리 덕분에 데이터만 바뀌었을 때 컴포넌트 트리를 다시 보낼 필요가 없습니다.

python
# 에이전트가 전송하는 A2UI 데이터 구조
# 클라이언트 측 렌더러가 이를 받아 네이티브 UI로 변환한다

a2ui_messages = [
    # 1. 렌더링을 시작할 화면(Surface) 생성
    {"beginRendering": {"surfaceId": "default", "root": "card"}},

    # 2. 컴포넌트 트리 전송: 중첩 구조가 아닌 ID 참조 방식의 평면 리스트
    {"surfaceUpdate": {"surfaceId": "default", "components": [
        # 'card'는 최상위 카드 컨테이너, 자식으로 'col'을 가짐
        {"id": "card",      "component": {"Card": {"child": "col"}}},

        # 'col'은 세로 정렬 컨테이너, 제목·가격·버튼을 순서대로 배치
        {"id": "col",       "component": {"Column": {"children": {"explicitList": ["title", "price", "buy"]}}}},

        # 'title'은 H3 스타일 텍스트, 내용은 데이터 경로 "name"에서 바인딩
        {"id": "title",     "component": {"Text": {"usageHint": "h3", "text": {"path": "name"}}}},

        # 'price'는 일반 텍스트, 내용은 데이터 경로 "price"에서 바인딩
        {"id": "price",     "component": {"Text": {"text": {"path": "price"}}}},

        # 'buy'는 클릭 시 "purchase" 액션을 실행하는 버튼
        {"id": "buy",       "component": {"Button": {"child": "btn-label",
            "action": {"name": "purchase", "context": [{"key": "item", "value": {"path": "name"}}]}}}},

        # 'btn-label'은 버튼 내부의 고정 텍스트
        {"id": "btn-label", "component": {"Text": {"text": {"literalString": "Buy Now"}}}},
    ]}},

    # 3. 실제 데이터 전송: 구조 재전송 없이 데이터만 업데이트 가능
    {"dataModelUpdate": {"surfaceId": "default", "contents": [
        {"key": "name",  "valueString": "Fresh Atlantic Salmon"},
        {"key": "price", "valueString": "$24.00/lb"},
    ]}},
]

동일한 18개의 컴포넌트만으로 에이전트는 요청에 따라 완전히 다른 인터페이스를 조립할 수 있습니다. 재고 체크리스트, 주문서 양식, 공급업체 비교표 모두 CheckBox, TextField, DateTimeInput, Card 등의 조합으로 만들어지며, 이를 위한 추가 프런트엔드 코드는 필요하지 않습니다.

A2UI 결과물 예시

개발 중에는 ADK Web이 A2UI 컴포넌트를 네이티브로 렌더링해 줍니다. 커스텀 렌더러를 직접 만들지 않고도 에이전트의 UI 출력을 즉시 확인하고 테스트할 수 있습니다.


6. AG-UI: Agent-User Interaction Protocol ✍️

문제: 에이전트의 응답을 프런트엔드에 연결하는 코드가 너무 복잡하다 ✅

전통적인 REST API는 요청에 응답을 한 번 주고 끝납니다. 에이전트는 다릅니다. 텍스트를 스트리밍으로 보내고, 응답 중간에 도구를 호출하며, 때로는 사용자 입력을 기다리며 멈춥니다. 이 때문에 에이전트를 프런트엔드에 연결하는 작업은 일반 API 호출보다 훨씬 복잡합니다.

물론 직접 구현할 수도 있습니다. ADK는 /run_sse 엔드포인트를 통해 이벤트를 직접 스트리밍하며, 수십 줄의 파싱 코드를 짜면 도구 호출을 화면에 그릴 수 있습니다. 하지만 이 파싱 코드는 일종의 보일러플레이트에 불과하며, 이벤트 형식이 바뀔 때마다 코드가 깨집니다.

해결: 프레임워크 원시 이벤트를 표준 SSE 스트림으로 변환 ✅

AG-UI는 이 보일러플레이트를 제거합니다. 프레임워크의 원시 이벤트를 표준화된 SSE 스트림으로 변환해 주는 미들웨어 역할을 합니다. 덕분에 프런트엔드는 어떤 에이전트 프레임워크가 응답을 생성했는지 신경 쓸 필요 없이, TEXT_MESSAGE_CONTENTTOOL_CALL_START 같이 타입이 지정된 이벤트만 리스닝하면 됩니다.

AG-UI 구조 다이어그램

주방 관리 에이전트를 AG-UI 스트리밍 엔드포인트로 변환하는 방법은 간단합니다. ag_ui_adk 패키지로 에이전트를 감싼 뒤 FastAPI 앱에 마운트하면 끝입니다.

python
from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint
from fastapi import FastAPI

# 에이전트를 AG-UI 규격으로 감싸고, FastAPI 앱에 스트리밍 엔드포인트를 추가
ag_ui_agent = ADKAgent(adk_agent=kitchen_agent, app_name="kitchen", user_id="chef")
app = FastAPI()
add_adk_fastapi_endpoint(app, ag_ui_agent, path="/")

# 실행: uvicorn main:app

# ── 클라이언트가 수신하는 표준 SSE 이벤트 스트림 ──────────────────────────
#
# RUN_STARTED           에이전트가 요청을 받아 추론을 시작
# TOOL_CALL_START       도구 호출 시작  (예: toolCallName="check_inventory")
# TOOL_CALL_RESULT      도구 실행 결과  (예: content="재고 3파운드, 재주문 필요")
# TOOL_CALL_END         도구 호출 프로세스 종료
# TEXT_MESSAGE_CONTENT  답변 생성 중    (델타: "현재 재고 상황을 바탕으로...")
# TEXT_MESSAGE_CONTENT  답변 생성 중    (델타: "연어 10파운드 주문을 권장합니다.")
# RUN_FINISHED          모든 응답 프로세스 완료

7. 마치며 ✍️

Visa, Stripe, Circle, Mastercard가 일제히 에이전트 결제 인프라를 들고 나왔습니다.

Visa CLI가 공개되었습니다. AI 에이전트가 터미널에서 바로 카드 결제를 실행할 수 있는 CLI 툴입니다. API 키 설정도 없고, 사전 충전도 없습니다. 이미지 생성 API, 음악 생성 API, 유료 데이터 피드 등 에이전트가 필요한 것을 그 자리에서 사서 쓸 수 있습니다. Stripe가 지원하는 블록체인 Tempo가 메인넷을 론칭하며 MPP(Machine Payments Protocol)를 발표했습니다. MPP는 에이전트가 지출 한도를 사전에 설정하고 스트리밍 방식으로 연속 결제를 실행하는 오픈 스탠다드입니다. Circle이 x402 스탠다드 기반 Nanopayments를 테스트넷에 올렸습니다. 0.01센트 이하, 가스비 없는 USDC 마이크로결제. 에이전트가 계정도 없이 API를 호출하고 즉시 결제합니다. Mastercard는 Google과 함께 Verifiable Intent 프레임워크를 발표했습니다. AI 에이전트가 대신 결제할 때 "누가 무엇을 승인했는지"를 암호학적으로 기록하는 신뢰 체계입니다.

인터넷이 문서를 연결했고, 스마트폰이 사람을 연결했다면, 지금 만들어지고 있는 것은 에이전트를 연결하는 인프라입니다.

MCP는 에이전트가 세상의 데이터에 손을 뻗을 수 있게 했고, A2A는 에이전트끼리 대화하게 했으며, UCP와 AP2는 그 대화가 실제 거래로 이어지도록 만들었습니다. A2UI와 AG-UI는 그 과정 전체를 사람이 보고 개입할 수 있는 형태로 감쌌습니다. 주요 금융 인프라 기업들이 일제히 에이전트 결제 표준을 내놓고 있다는 사실은, 해당 프로토콜들이 기술 실험을 넘어 실제 경제 활동의 기반이 될 것임을 시사합니다. 에이전트가 자율적으로 구매하고, 승인받고, 결제하는 흐름이 표준화되어, 머지않아 사람의 직접적인 개입 없이도 비즈니스 프로세스 전체가 돌아가는 환경이 구축될 것 입니다.