Skill: wangjipeng977/a2a-agent-protocol (version 1.0.0)

--- name: a2a-protocol description: > Use when (1) building an AI agent that needs to communicate with other agents over the Agent2Agent (A2A) protocol, (2) publishing an AgentCard for discovery, (3) acting as an A2A client or server, or (4) integrating A2A capabilities into an existing agent framework. Solves agent isolation — enabling discovery, capability negotiation, task delegation, and structured data exchange across different frameworks and vendors. license: MIT metadata: version: "1.0" category: dev-tools author: wangjipeng sources: - https://a2a-protocol.org/latest/specification/ - https://github.com/a2aproject/A2A - https://github.com/a2aproject/a2a-python --- # A2A Protocol Skill An AI agent skill for building, connecting, and operating A2A-compliant agents. This skill is the definitive guide for agents participating in the Agent2Agent protocol ecosystem. ## Core Position This skill is **NOT** a general API wrapper — it is a protocol education and implementation guide. It teaches agents what A2A is, how to structure AgentCards for discovery, which operations to perform in which order, and how to handle the multi-turn task lifecycle. This skill is **NOT** framework-specific. It guides agents regardless of whether they are built with LangGraph, BeeAI, Google ADK, or custom code. The A2A protocol is language-agnostic. This skill covers the JSON-RPC 2.0 over HTTP binding (the most common) plus key concepts for gRPC and HTTP+REST bindings. ## What Problem This Solves AI agents today operate in silos. Even when they share the same infrastructure, they have no standard way to: - Discover what capabilities other agents expose - Send a task to a remote agent and track its lifecycle - Handle streaming updates or push notifications from a remote agent - Exchange files, structured data, or multi-part messages The A2A protocol solves all of the above. This skill makes an AI agent A2A-aware. ## Modes ### `/a2a-protocol` Default mode. Learn A2A concepts, data structures, and operation semantics. **Preferred opening:** *"The A2A protocol enables agents to discover each other and collaborate without exposing internal state. Here is the protocol overview:"* Use it when: - The user asks what A2A is, how it works, or why it matters - You need to explain the difference between A2A and MCP - You want to understand the AgentCard structure and discovery mechanism - You need to understand the full task lifecycle (submitted → working → completed) ### `/a2a-protocol build-card` Generate an AgentCard JSON for the agent. The AgentCard is the public identity document that other agents use to discover capabilities, auth requirements, and connection endpoints. **Preferred opening:** *"To publish this agent in the A2A ecosystem, I will generate an AgentCard at `/.well-known/agent.json` declaring: name, version, capabilities, and auth requirements."* Use it when: - You are building an A2A server agent and need to publish its capabilities - You need a starting-point AgentCard template to customize - The user wants to register an agent in the A2A ecosystem **Output:** Writes `agent-card.json` to the workspace with the complete AgentCard. ### `/a2a-protocol send-task` Send a task to a remote A2A agent via `tasks/send`. Includes building the request, interpreting the response (synchronous or task ID for async), and handling errors. **Preferred opening:** *"To delegate this task to the remote agent, I will: (1) fetch its AgentCard, (2) validate capability compatibility, (3) construct a SendMessageRequest and POST it."* Use it when: - You need to delegate a subtask to another agent - You are acting as an A2A client initiating work on a remote agent - The user says "send this task to the X agent" or "delegate to agent Y" **Output:** Resolves to `artifacts[].parts` from the completed Task. ### `/a2a-protocol stream-task` Set up streaming task delivery via `tasks/sendSubscribe` with SSE (Server-Sent Events), receive incremental `TaskStatusUpdateEvent` and `TaskArtifactUpdateEvent` blocks, and reassemble the final artifact. **Preferred opening:** *"This remote task will take time. I will subscribe via SSE to receive real-time status transitions and partial artifacts as they arrive."* Use it when: - A remote agent's task will take time and you want real-time progress updates - You need to stream partial results as they become available - The user says "track the task progress" or "watch for updates" **Output:** Reassembles the final `Artifact` from accumulated streaming events. ### `/a2a-protocol subscribe-task` Subscribe to a task's push notification channel via `tasks/subscribe`, using either SSE streaming or webhook delivery to receive final results. **Preferred opening:** *"I will register a push notification subscription so the remote agent delivers the result to our callback endpoint rather than requiring us to poll."* Use it when: - You want the remote agent to push results back to you rather than polling - You need asynchronous result delivery to a callback URL - The user says "notify me when done" or "push results to this endpoint" **Output:** Receives and validates the push payload; extracts `taskId` + `artifacts`. ### `/a2a-protocol mock-server` Start a local mock A2A server for experimentation. The mock server is a fully functional A2A agent that serves its own AgentCard, accepts tasks via all supported operations, and responds with realistic simulated results. **Preferred opening:** *"To experiment with A2A locally, I will start a mock A2A server on a free port. This gives us a real A2A endpoint to practice against."* Use it when: - You want to test A2A interactions without a real remote agent - You need a safe sandbox to practice `send-task`, `stream-task`, or `subscribe-task` - You want to verify that the agent correctly implements the A2A protocol before deploying - The user says "try it out", "test A2A", "run locally", or "experiment" **Output:** Starts an HTTP server at `http://localhost:<port>/a2a/` with a ready AgentCard. > This mode is a no-op for agents without Python execution capability. If the host cannot run Python scripts, skip this mode. ## Execution Steps ### Mode: `/a2a-protocol` (Learn) 1. **You** read the A2A spec overview from `references/a2a-overview.md` 2. **You** review the data model objects from `references/data-model.md` 3. **You** answer the user's question using the reference material 4. If the user asks for a specific operation, **you** switch to the appropriate mode ### Mode: `/a2a-protocol build-card` 1. **You** read `references/agent-card-template.md` for the full AgentCard schema 2. **You** determine which capabilities the agent exposes (text, forms, media, streaming) 3. **You** determine auth scheme (none, API key, OAuth2, mTLS) 4. **You** determine push notification support (SSE, webhook, both, neither) 5. **You** generate a completed AgentCard JSON and write it to `agent-card.json` 6. **You** confirm the agent card URL path (default: `/.well-known/agent.json`) ### Mode: `/a2a-protocol send-task` 1. **You** read `references/operations.md` — SendMessage operation details 2. **You** identify the target agent's URL (from its AgentCard or given address) 3. **You** check the target agent's auth requirements (from its AgentCard) 4. **You** construct a `SendMessageRequest` with `taskId`, `sessionId`, `message`, and `context` 5. **You** send POST JSON-RPC 2.0 request to the target agent's A2A endpoint 6. **You** handle the response: immediate result (synchronous) or `taskId` for async polling 7. If async: **you** use `tasks/get` to poll for completion or `tasks/sendSubscribe` for streaming 8. On completion: **you** extract `artifacts[].parts` from the task result and return them ### Mode: `/a2a-protocol stream-task` 1. **You** read `references/operations.md` — SendStreamingMessage operation + streaming events 2. **You** construct a `SendMessageRequest` and set `stream: true` in configuration 3. **You** send `tasks/sendSubscribe` POST to the target agent's A2A endpoint 4. **You** receive SSE stream of `TaskStatusUpdateEvent` (status transitions) and `TaskArtifactUpdateEvent` (partial artifact updates) 5. **You** accumulate events until `TaskStatusUpdateEvent` with `state: completed`/`failed`/`canceled` 6. **You** close the SSE stream and extract the final artifact from accumulated data ### Mode: `/a2a-protocol subscribe-task` 1. **You** read `references/operations.md` — SubscribeToTask operation + push notification setup 2. **You** determine delivery mode: SSE stream (server-push via `tasks/sendSubscribe`) or webhook push (agent registers a callback URL via `tasks/sendSubscribe` with `pushConfig`) 3. For webhook mode: **you** register `PushNotificationConfig` with the target's `tasks/sendSubscribe` including `pushConfig.url` and `pushConfig.authentication` 4. **You** wait for delivery; validate the incoming `tasks/push` payload using the `AuthenticationInfo` 5. **You** extract `taskId` and `artifacts` from the validated push payload and return them ### Mode: `/a2a-protocol mock-server` 1. **You** locate `scripts/mock_a2a_server.py` in the skill directory 2. **You** start the server: `python3 scripts/mock_a2a_server.py --port <PORT> --name <NAME> --skills <SKILLS>` 3. **You** fetch the AgentCard from `http://localhost:<PORT>/.well-known/agent.json` to confirm it started 4. **You** optionally run `scripts/validate_agent_card.py` against the fetched AgentCard 5. **You** use `send-task`, `stream-task`, or `subscribe-task` against the local endpoint to test interactions 6. When done: **you** stop the server (Ctrl+C or SIGTERM) ## Mandatory Rules ### Do not - **Do not** hardcode any AgentCard endpoint URLs — always use the supplied or discovered URL - **Do not** assume unauthenticated access — always check the target agent's `AgentCard.authentication` - **Do not** send a task without a `sessionId` if the operation requires multi-turn context - **Do not** discard `TaskStatusUpdateEvent` blocks — they signal working/progress states - **Do not** treat A2A as a replacement for MCP — A2A is for agent-to-agent collaboration, MCP is for agent-to-tool/resource connectivity; use both together - **Do not** expose the agent's internal memory, tool implementations, or proprietary state in the AgentCard or in task payloads — A2A preserves agent opacity - **Do not** return the raw `task` object — always extract and return `artifacts[].parts` - **Do not** close an SSE stream before receiving `final: true` on a `TaskStatusUpdateEvent` ### Do - Always validate capability compatibility before sending a task (check `targetAgent.capabilities.streaming` and `pushNotifications`) - Always handle streaming with proper event accumulation — never treat partial events as final - Always close SSE streams when the task reaches a terminal state - Always use idempotent task IDs when retrying a failed `tasks/send` - Always include `context.contextId` to thread multi-turn conversations across agents - Always return structured output (`artifacts[].parts`) not raw JSON-RPC responses ## Quality Bar A good A2A interaction **must** satisfy all of: - AgentCard fetched from `/.well-known/agent.json` (HTTP 200, valid JSON, has required fields) - JSON-RPC 2.0 request has `jsonrpc: "2.0"`, `method`, `params.taskId` (or server-generated), and `id` - Task state machine transitions are followed: no `completed` before a `working` signal received - Streaming: `TaskStatusUpdateEvent.final: true` received before closing stream - Artifact extraction: `parts[].text` or `parts[].data` returned, not raw `task` object - Auth: correct scheme used (Bearer token → `Authorization: Bearer`, API key → `X-API-Key`, etc.) - Error handling: JSON-RPC error codes `-32700`/`-32600`/`-32603` recognized and surfaced as errors A bad A2A interaction exhibits any of: - No capability check before sending (assumes all agents accept all tasks) - SSE stream closed before receiving `final: true` on the terminal status event - Raw `task` object returned instead of extracted `artifacts[].parts` - Wrong HTTP method used (GET instead of POST, or wrong path for the operation) - Auth credentials omitted when `AgentCard.authentication.credentials: "required"` ## Good vs. Bad Examples ### Good: Sending a task with full capability check ``` 1. Fetch AgentCard from https://target-agent/.well-known/agent.json 2. Verify capabilities.streaming or dataStreamingSupported matches our needs 3. Check authentication.schemes and prepare credentials if credentials: "required" 4. Construct SendMessageRequest: taskId=uuid(), sessionId, message with parts 5. POST to https://target-agent/a2a/tasks/send with Authorization header 6. Receive { task: { taskId: "abc123", status: { state: "submitted" } } } 7. Poll tasks/get until status.state === "completed" 8. Return artifacts[0].parts[0].text ← structured output ``` ### Bad: Sending a task without discovery ``` 1. POST to https://target-agent/a2a/tasks/send with hardcoded payload 2. Assume success without checking status.state 3. Return raw task object instead of artifacts[].parts ``` ### Good: AgentCard with complete capability declaration ```json { "name": "data-analysis-agent", "description": "Specialized in financial data extraction and reporting", "version": "1.0", "capabilities": { "streaming": "SUPPORTED", "pushNotifications": "SUPPORTED", "agentPrivilege": false, "signedAgentCard": false }, "skills": [ { "id": "financial-data", "name": "Financial Data Extraction", "description": "..." } ], "authentication": { "schemes": ["httpBearer"], "credentials": "required" } } ``` ### Bad: Generic AgentCard without capability detail ```json { "name": "agent", "description": "An AI agent" } ``` ## References - `references/a2a-overview.md` — A2A protocol overview, key goals, A2A vs MCP comparison - `references/data-model.md` — Core data model (Task, Message, Part, Artifact, AgentCard, streaming events) - `references/operations.md` — All A2A operations reference (send, get, subscribe, push, cancel, list) - `references/agent-card-template.md` — AgentCard JSON schema with full annotated example - `references/quickstart.md` — Full end-to-end example: AgentCard → discovery → send → streaming → result - `scripts/mock_a2a_server.py` — Start a local mock A2A agent for experimentation and testing

path: CONTRIBUTING.md
content: # Contributing to a2a-protocol

Thank you for your interest in contributing to the A2A Protocol skill.

## How to Contribute

1. **Fork the repository** on GitHub
2. **Clone your fork**:
   ```bash
   git clone https://github.com/<your-username>/a2a-protocol.git
   cd a2a-protocol
   ```
3. **Create a feature branch**:
   ```bash
   git checkout -b feat/your-feature-name
   ```
4. **Make your changes** — follow the SKILL.md structure standards
5. **Run the audit**:
   ```bash
   python ~/.openclaw/workspace/skills/skill-factory/scripts/audit_skill.py . --all
   ```
6. **Commit and push**:
   ```bash
   git commit -m "feat(<feature>): add ..."
   git push origin feat/your-feature-name
   ```
7. **Open a Pull Request** with a clear description of what and why

## Skill Structure Standards

- `SKILL.md` must have valid YAML frontmatter with `name`, `description`, `license`, `metadata`
- `description` must contain trigger conditions, not just capability statements
- All scripts must have shebangs and `requirements.txt`
- No hardcoded API keys or secrets

## Reporting Issues

- Use GitHub Issues for bug reports and feature requests
- Search existing issues before creating a new one
- Include A2A protocol version reference when applicable

## Code of Conduct

This project follows the [A2A Protocol Code of Conduct](https://github.com/a2aproject/A2A/blob/main/CODE_OF_CONDUCT.md).

path: README.md
content: ---
name: a2a-protocol
license: MIT
---

# a2a-protocol

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Version](https://img.shields.io/badge/version-1.0.0-blue)

> Use when building A2A-compliant agents, publishing an AgentCard for discovery, sending tasks
> to remote agents, or integrating the Agent2Agent protocol into an existing agent framework.

## What Problem This Solves

AI agents operate in silos. Even when they share infrastructure, they have no standard way to
discover peers, delegate tasks, track lifecycle, or exchange structured data across frameworks.

The A2A (Agent2Agent) protocol — an open standard maintained by Google and the a2aproject
community — solves agent interoperability. This skill makes an AI agent A2A-aware, guiding it
through discovery, task submission, streaming, push notifications, and multi-turn collaboration.

**When triggered:** When the user wants to connect agents, delegate tasks, build an A2A server,
publish an AgentCard, or understand the A2A/MCP relationship.

## Features

- **Protocol education** — full A2A overview, data model, and operation reference
- **AgentCard builder** — generate discovery-ready JSON identity documents
- **Task operations** — send, poll, stream, subscribe to remote agent tasks
- **Multi-turn support** — session and context threading across agent boundaries
- **Enterprise auth** — guidance on API key, Bearer, OAuth2, and mTLS schemes
- **Streaming events** — real-time TaskStatusUpdateEvent and TaskArtifactUpdateEvent handling

## Quick Start

### Installation

```bash
# Via ClawHub
clawhub install a2a-protocol

# Or manually
cp -r a2a-protocol/ ~/.openclaw/skills/
```

### Usage

```bash
# Learn A2A concepts
/a2a-protocol

# Build an AgentCard for discovery
/a2a-protocol build-card

# Send a task to a remote A2A agent
/a2a-protocol send-task

# Stream real-time task updates from a remote agent
/a2a-protocol stream-task

# Subscribe to push notifications from a remote agent
/a2a-protocol subscribe-task
```

### Example: Send a Task

```
/a2a-protocol send-task --agent https://target-agent/.well-known/agent.json --task "Analyze Q3 revenue"
```

## Directory Structure

```
a2a-protocol/
├── SKILL.md                    # Entry point (this skill)
├── LICENSE                    # MIT
├── README.md                  # This file
├── README_zh.md              # Chinese version
├── CONTRIBUTING.md           # Contribution guide
├── .gitignore
├── references/
│   ├── a2a-overview.md       # Protocol overview and key concepts
│   ├── data-model.md         # Core data model (Task, Message, Part, Artifact, AgentCard)
│   ├── operations.md         # All A2A operations reference
│   └── agent-card-template.md # AgentCard JSON schema with annotations
└── scripts/                  # (reserved for future helpers)
```

## References

- [A2A Protocol Specification](https://a2a-protocol.org/latest/specification/)
- [A2A on GitHub](https://github.com/a2aproject/A2A)
- [Python SDK (a2a-sdk)](https://github.com/a2aproject/a2a-python)

## License

This project is licensed under the MIT License — see [LICENSE](LICENSE) for details.

---

Powered by [MiniMax](https://minimax.io).

path: README_zh.md
content: ---
name: a2a-protocol
license: MIT
---

# a2a-protocol

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Version](https://img.shields.io/badge/version-1.0.0-blue)

> 当需要构建 A2A 兼容智能体、发布 AgentCard 发现文档、向远程智能体发送任务，
> 或将 Agent2Agent 协议集成到现有智能体框架时使用此技能。

## 解决什么问题

AI 智能体长期处于孤岛状态。即使共享基础设施，也没有标准方式实现：
跨框架发现能力、委托任务、跟踪生命周期或交换结构化数据。

A2A（Agent2Agent）协议是由 Google 与 a2aproject 社区共同维护的开放标准，
解决了智能体互操作性问题。本技能使 AI 智能体具备 A2A 能力，覆盖发现、
任务提交、 streaming 、推送通知和多轮协作场景。

**触发条件：** 当用户想要连接智能体、委托任务、构建 A2A 服务器、
发布 AgentCard，或理解 A2A/MCP 关系时使用。

## 功能特性

- **协议学习** — 完整 A2A 概述、数据模型和操作参考
- **AgentCard 生成** — 构建用于发现的 JSON 身份文档
- **任务操作** — 发送、轮询、stream、订阅远程智能体任务
- **多轮支持** — 跨智能体会话与上下文线程
- **企业级认证** — API Key、Bearer、OAuth2、mTLS 方案指导
- **流式事件** — 实时 TaskStatusUpdateEvent 和 TaskArtifactUpdateEvent 处理

## 快速开始

### 安装

```bash
# 通过 ClawHub
clawhub install a2a-protocol

# 或手动安装
cp -r a2a-protocol/ ~/.openclaw/skills/
```

### 使用方式

```
/a2a-protocol              # 学习 A2A 概念
/a2a-protocol build-card   # 为发现服务生成 AgentCard
/a2a-protocol send-task    # 向远程 A2A 智能体发送任务
/a2a-protocol stream-task  # 从远程智能体实时获取任务更新
/a2a-protocol subscribe-task # 订阅来自远程智能体的推送通知
```

## 目录结构

```
a2a-protocol/
├── SKILL.md                    # 入口点
├── LICENSE                     # MIT 协议
├── README.md                   # 英文说明
├── README_zh.md                # 本文件（中文）
├── CONTRIBUTING.md             # 贡献指南
├── .gitignore
├── references/
│   ├── a2a-overview.md         # 协议概述与核心概念
│   ├── data-model.md           # 核心数据模型
│   ├── operations.md           # 所有 A2A 操作参考
│   └── agent-card-template.md # AgentCard JSON 模式与注解
└── scripts/                   # （预留辅助脚本）
```

## 参考资料

- [A2A 协议规范](https://a2a-protocol.org/latest/specification/)
- [A2A GitHub 仓库](https://github.com/a2aproject/A2A)
- [Python SDK (a2a-sdk)](https://github.com/a2aproject/a2a-python)

## 协议

本项目采用 MIT 协议 — 详见 [LICENSE](LICENSE)。

---

Powered by [MiniMax](https://minimax.io)。

path: references/a2a-overview.md
content: # A2A Protocol Overview

> Reference: https://a2a-protocol.org/latest/specification/

## What is A2A?

The Agent2Agent (A2A) protocol is an **open specification** enabling AI agents built on
different frameworks by different vendors, running on separate servers, to communicate
and collaborate — as agents, not just as tools.

A2A provides a common language for agents, fostering a more interconnected AI ecosystem.

## Key Goals

| Goal | Description |
|------|-------------|
| **Break Down Silos** | Connect agents across different ecosystems and frameworks |
| **Enable Complex Collaboration** | Allow specialized agents to work together on tasks a single agent cannot handle alone |
| **Promote Open Standards** | Community-driven approach to agent communication |
| **Preserve Opacity** | Agents collaborate without exposing internal memory, proprietary logic, or tool implementations |

## Core Concepts

### Agent

An AI application that:
- Exposes capabilities via an **AgentCard**
- Communicates via A2A operations (SendMessage, GetTask, etc.)
- Can act as A2A **server** (receives tasks), **client** (sends tasks), or both

### AgentCard

A JSON document at `/.well-known/agent.json` that declares:
- Agent name, version, description
- Capabilities (streaming, push notifications, authentication)
- Skills (specific capabilities exposed)
- Endpoints and auth requirements

### Task

The central unit of work in A2A:
- Has a unique `taskId`
- Has a full lifecycle: `submitted` → `working` → `completed`/`failed`/`canceled`
- Carries a `Message` (the work to be done)
- Produces `Artifact[]` (results, as `Part[]`)

### Message

A `Message` contains a `role` (user/agent) and `parts[]` — the actual content blocks.

### Part

The content unit. Types:
- `TextPart` — plain text
- `DataPart` — structured JSON data
- `FilePart` — file with inline bytes or remote URL

### Artifact

A collection of related parts forming a deliverable output (e.g., a report document,
a generated image, a data table).

## A2A vs MCP

| Aspect | A2A | MCP |
|--------|-----|-----|
| **Purpose** | Agent-to-agent collaboration | Agent-to-tool/resource connectivity |
| **Focus** | Task delegation, status tracking, multi-agent workflows | Tool invocation, resource access |
| **State** | Long-running tasks with full lifecycle | Stateless tool calls |
| **Communication** | Rich messages, artifacts, streaming events | JSON-RPC request/response |
| **Discovery** | AgentCard at `/.well-known/agent.json` | MCP server registry |

**Use both together**: MCP connects an agent to tools and resources; A2A connects
agents to each other.

## Protocol Binding

A2A supports multiple wire protocols:
- **JSON-RPC 2.0 over HTTP** — most common, agents `POST tasks/send`
- **gRPC** — for high-performance agent interop
- **HTTP+REST/JSON** — for simple integrations

## Task Lifecycle

```
[Client]                    [Server/Remote Agent]
   |                              |
   |--- tasks/send -------------->| (submit task)
   |<-- taskId + status=submitted -|
   |                              |
   |<-- streaming events (SSE) ----| (working, partial artifacts)
   |                              |
   |<-- tasks/get -----------------| (poll for completion)
   |--> task with status=completed|
   |                              |
   |--> artifacts[].parts -------->| (result delivered)
```

## A2A and Enterprise

A2A is enterprise-ready with:
- Authentication schemes: API key, HTTP Bearer, OAuth2, mTLS, OpenID Connect
- Push notifications via web hooks
- AgentCard signing for trusted discovery
- Version negotiation between agents

path: references/agent-card-template.md
content: # AgentCard Template and Reference

> Reference: https://a2a-protocol.org/latest/specification/ Section 8

The AgentCard is a JSON document that represents an agent's public identity and capabilities.
It is served at `/.well-known/agent.json` on the agent's host.

## Minimal AgentCard

```json
{
  "name": "string (required)",
  "description": "string (required)",
  "version": "string (required)",
  "capabilities": {
    "streaming": "SUPPORTED | NOT_SUPPORTED",
    "pushNotifications": "SUPPORTED | NOT_SUPPORTED",
    "agentPrivilege": false,
    "signedAgentCard": false
  },
  "authentication": {
    "schemes": ["string"],
    "credentials": "required | optional"
  }
}
```

## Annotated AgentCard (Complete)

```json
{
  "name": "financial-analysis-agent",
  "description": "Specialized agent for financial data extraction, ratio analysis, and trend reporting",
  "version": "1.0",
  "provider": {
    "organization": "Acme Corp",
    "description": "Acme financial analytics division",
    "url": "https://acme.example.com"
  },
  "capabilities": {
    "streaming": "SUPPORTED",
    "pushNotifications": "SUPPORTED",
    "agentPrivilege": false,
    "signedAgentCard": false,
    "dataStreamingSupported": true,
    "supportsAuthenticatedExtendedAgentCard": true
  },
  "skills": [
    {
      "id": "financial-data-extraction",
      "name": "Financial Data Extraction",
      "description": "Extracts financial metrics from raw CSV, Excel, and JSON data sources",
      "tags": ["finance", "extraction", "csv", "excel"],
      "examples": ["Extract revenue and EBITDA from Q3 report.csv"]
    },
    {
      "id": "ratio-analysis",
      "name": "Ratio Analysis",
      "description": "Computes liquidity, profitability, and leverage ratios from financial statements",
      "tags": ["finance", "ratios", "analysis"],
      "examples": ["Calculate current ratio and ROIC for FY2025"]
    },
    {
      "id": "trend-reporting",
      "name": "Trend Reporting",
      "description": "Generates time-series trend visualizations and narrative reports",
      "tags": ["finance", "reporting", "trends", "charts"],
      "examples": ["Generate a 5-year revenue trend report"]
    }
  ],
  "authentication": {
    "schemes": ["httpBearer"],
    "credentials": "required"
  },
  "defaultInputModes": ["text", "json"],
  "defaultOutputModes": ["text", "json", "png"],
  "endpoint": "https://financial-agent.acme.example.com/a2a/",
  "url": "https://financial-agent.acme.example.com"
}
```

## Field Reference

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique agent name within the deployment |
| `description` | Yes | Human-readable description of what the agent does |
| `version` | Yes | Semantic version string for capability negotiation |
| `capabilities` | Yes | Declares streaming, push notification, and auth support |
| `capabilities.streaming` | Yes | `SUPPORTED`, `NOT_SUPPORTED`, or `STREAMABLE` |
| `capabilities.pushNotifications` | Yes | `SUPPORTED` or `NOT_SUPPORTED` |
| `capabilities.agentPrivilege` | No | If true, agent can act with elevated privileges |
| `capabilities.signedAgentCard` | No | If true, AgentCard is cryptographically signed |
| `capabilities.dataStreamingSupported` | No | If true, agent can stream data parts (not just status) |
| `capabilities.supportsAuthenticatedExtendedAgentCard` | No | If true, agent exposes extended card with auth |
| `authentication.schemes` | Yes | Auth methods: `apiKey`, `httpBearer`, `oauth2`, `mutualTLS`, `openIdConnect` |
| `authentication.credentials` | Yes | `required` or `optional` |
| `provider` | No | Provider organization info |
| `skills` | No | Array of `AgentSkill` objects describing exposed capabilities |
| `defaultInputModes` | No | MIME types the agent accepts (e.g., `text`, `json`, `image/png`) |
| `defaultOutputModes` | No | MIME types the agent produces (e.g., `text`, `json`, `image/png`) |
| `endpoint` | No | Explicit A2A endpoint URL (if not inferable from `url`) |
| `url` | No | Base URL for the agent service |

## skills[] — Capability Declaration

The `skills` array declares specific capabilities other agents can request:

```json
{
  "id": "skill-id (required, unique within agent)",
  "name": "Human-readable skill name",
  "description": "What this skill does and when to request it",
  "tags": ["optional", "category", "tags"],
  "examples": ["Example invocation phrases"]
}
```

## AgentCard Discovery Flow

```
1. Client fetches GET https://<target-agent>/.well-known/agent.json
2. Client reads capabilities, skills, auth requirements
3. Client verifies capability compatibility
4. Client constructs and sends task via appropriate A2A operation
```

## AgentCard Caching

- **Servers** should set `Cache-Control: max-age=3600` (or similar) on the AgentCard endpoint
- **Clients** should cache the AgentCard for the duration of a session; re-fetch on explicit refresh
- Extended AgentCard (authenticated) should NOT be cached

path: references/data-model.md
content: # A2A Data Model Reference

> Reference: https://a2a-protocol.org/latest/specification/ Section 4

## Core Objects

### Task

```json
{
  "taskId": "string (required)",
  "sessionId": "string (optional, for multi-turn threading)",
  "contextId": "string (optional, for conversation threading)",
  "status": "TaskStatus (required)",
  "metadata": "object (optional, arbitrary metadata)",
  "artifacts": [
    "Artifact[] (optional, produced during or after task)"
  ],
  "messages": [
    "Message[] (optional, history if retained)"
  ]
}
```

### TaskStatus

```json
{
  "state": "TaskState (required)",
  "message": "string (optional, human-readable status message)",
  "timestamp": "ISO-8601 datetime (required)"
}
```

### TaskState

| State | Meaning |
|-------|---------|
| `submitted` | Task received, not yet processing |
| `working` | Task is actively processing |
| `completed` | Task finished successfully |
| `failed` | Task encountered an error |
| `canceled` | Task was cancelled by client |

### Message

```json
{
  "role": "Role (required) — 'user' or 'agent'",
  "parts": [
    "Part[] (required, content blocks)"
  ],
  "messageId": "string (optional)"
}
```

### Role

| Value | Who |
|-------|-----|
| `user` | The initiating human or client agent |
| `agent` | The responding server agent |

### Part

A `Part` is a discriminated union with a `kind` field:

```json
// TextPart
{ "kind": "text", "text": "string" }

// DataPart
{ "kind": "data", "data": object }

// FilePart
{ "kind": "file", "file": { "name": "string", "mimeType": "string", "bytes": "base64", "url": "https://..." } }
```

### Artifact

```json
{
  "artifactId": "string",
  "name": "string (optional)",
  "description": "string (optional)",
  "parts": [
    "Part[] (required, content blocks)"
  ],
  "metadata": "object (optional)"
}
```

### AgentCard

> Full schema with all optional fields — see `references/agent-card-template.md` for a complete annotated example.

Minimal AgentCard:

```json
{
  "name": "string (required)",
  "description": "string (required)",
  "version": "string (required)",
  "capabilities": {
    "streaming": "SUPPORTED | NOT_SUPPORTED",
    "pushNotifications": "SUPPORTED | NOT_SUPPORTED",
    "agentPrivilege": "boolean",
    "signedAgentCard": "boolean"
  },
  "authentication": {
    "schemes": ["string"],
    "credentials": "required | optional"
  }
}
```

### AgentCapabilities

```json
{
  "streaming": "SUPPORTED | NOT_SUPPORTED | STREAMABLE",
  "pushNotifications": "SUPPORTED | NOT_SUPPORTED",
  "agentPrivilege": "boolean",
  "signedAgentCard": "boolean",
  "dataStreamingSupported": "boolean",
  "supportsAuthenticatedExtendedAgentCard": "boolean"
}
```

### AgentSkill

```json
{
  "id": "string (required)",
  "name": "string (required)",
  "description": "string (required)",
  "tags": ["string[] (optional)"],
  "examples": ["string[] (optional)"]
}
```

## Streaming Events

### TaskStatusUpdateEvent

```json
{
  "kind": "TaskStatusUpdateEvent",
  "taskId": "string",
  "status": "TaskStatus",
  "final": "boolean (true when task reaches terminal state)"
}
```

### TaskArtifactUpdateEvent

```json
{
  "kind": "TaskArtifactUpdateEvent",
  "taskId": "string",
  "artifact": "Artifact",
  "final": "boolean"
}
```

## Push Notification Objects

### PushNotificationConfig

```json
{
  "id": "string",
  "taskId": "string",
  "pushProviderName": "string",
  "pushProviderUrl": "string",
  "authentication": "AuthenticationInfo",
  "payload": {
    "secret": "string (optional)"
  }
}
```

### AuthenticationInfo

```json
{
  "kind": "APIKeyAuthenticationInfo | HTTPBearerAuthenticationInfo | OAuth2AuthenticationInfo",
  "credentials": "string or object"
}
```

## Security Objects

### SecurityScheme ( discriminated union)

| Kind | Auth Type |
|------|-----------|
| `apiKey` | X-API-Key header |
| `httpBearer` | Authorization: Bearer <token> |
| `oauth2` | OAuth 2.0 flow |
| `mutualTLS` | Mutual TLS certificate |
| `openIdConnect` | OIDC token |

path: references/operations.md
content: # A2A Protocol Operations Reference

> Reference: https://a2a-protocol.org/latest/specification/ Section 3

All operations are JSON-RPC 2.0 over HTTP POST to the agent's A2A endpoint.
Base URL pattern: `https://<agent-host>/a2a/`

---

## Core Operations

### tasks/send — Send Message

**Purpose:** Submit a new task to a remote agent.

**Request:**
```json
{
  "jsonrpc": "2.0",
  "method": "tasks/send",
  "params": {
    "taskId": "string (optional — server generates if omitted)",
    "sessionId": "string (optional)",
    "contextId": "string (optional)",
    "message": {
      "role": "user | agent",
      "parts": [{ "kind": "text", "text": "..." }]
    },
    "configuration": {
      "stream": false,
      "pushNotificationConfig": null
    }
  },
  "id": 1
}
```

**Response (sync):**
```json
{
  "jsonrpc": "2.0",
  "result": {
    "task": { ... Task object with status=completed ... },
    "artifacts": [ ... ]
  }
}
```

**Response (async — task requires time):**
```json
{
  "jsonrpc": "2.0",
  "result": {
    "task": { "taskId": "abc123", "status": { "state": "submitted" } }
  }
}
```

---

### tasks/sendSubscribe — Send Streaming Message

**Purpose:** Submit a task and receive real-time SSE stream of status/artifact updates.

**Request:** Same as `tasks/send` with `configuration.stream: true`

**Response:** SSE stream of events:
```
event: task_status_update
data: { "kind": "TaskStatusUpdateEvent", "taskId": "abc123", "status": { "state": "working" }, "final": false }

event: task_artifact_update
data: { "kind": "TaskArtifactUpdateEvent", "taskId": "abc123", "artifact": { ... }, "final": false }

event: task_status_update
data: { "kind": "TaskStatusUpdateEvent", "taskId": "abc123", "status": { "state": "completed" }, "final": true }
```

---

### tasks/get — Get Task

**Purpose:** Retrieve the current state of a task by ID.

**Request:**
```json
{
  "jsonrpc": "2.0",
  "method": "tasks/get",
  "params": { "taskId": "string (required)" },
  "id": 2
}
```

**Response:**
```json
{
  "jsonrpc": "2.0",
  "result": {
    "task": {
      "taskId": "abc123",
      "status": { "state": "completed", "timestamp": "..." },
      "artifacts": [ ... ],
      "messages": [ ... ]
    }
  }
}
```

---

### tasks/list — List Tasks

**Purpose:** List tasks, optionally filtered by session or context.

**Request:**
```json
{
  "jsonrpc": "2.0",
  "method": "tasks/list",
  "params": {
    "sessionId": "string (optional)",
    "contextId": "string (optional)",
    "pageSize": 10,
    "continuationToken": "string (optional)"
  },
  "id": 3
}
```

---

### tasks/cancel — Cancel Task

**Purpose:** Cancel a running task.

**Request:**
```json
{
  "jsonrpc": "2.0",
  "method": "tasks/cancel",
  "params": { "taskId": "string (required)" },
  "id": 4
}
```

---

### tasks/subscribe — Subscribe to Task Updates

**Purpose:** Subscribe to push notifications for a task (webhook or SSE delivery).

**Request:**
```json
{
  "jsonrpc": "2.0",
  "method": "tasks/subscribe",
  "params": {
    "taskId": "string (required)",
    "pushConfig": {
      "pushProviderName": "string",
      "pushProviderUrl": "https://...",
      "authentication": { "kind": "apiKey", "credentials": "..." },
      "payload": { "secret": "..." }
    }
  },
  "id": 5
}
```

---

### tasks/push — Receive Push Notification (webhook endpoint)

**Purpose:** Incoming push notification webhook (agent implements this endpoint).

```
POST /a2a/tasks/push
Content-Type: application/a2a+json

{
  "taskId": "abc123",
  "result": { ... },
  "pushNotificationConfigId": "string"
}
```

---

### pushNotificationConfig/create — Create Push Config

### pushNotificationConfig/get — Get Push Config

### pushNotificationConfig/list — List Push Configs

### pushNotificationConfig/delete — Delete Push Config

### agent/getExtendedAgentCard — Get Authenticated Extended Agent Card

---

## Operation Semantics

### Idempotency

- `tasks/send` is idempotent if `taskId` is supplied by the client (same `taskId` = same task)
- Without a client-supplied `taskId`, the server generates a unique one

### Capability Validation

Before sending, client should verify:
1. Target agent supports the required operation (check AgentCard)
2. Authentication requirements are met
3. Streaming preference is supported (if streamingRequested)

### Error Handling

Standard JSON-RPC 2.0 error codes:

| Code | Meaning |
|------|---------|
| `-32700` | Parse error — malformed JSON |
| `-32600` | Invalid request |
| `-32603` | Internal error |
| Agent-specific codes in `data` object |

### Multi-Turn Threads

| Field | Purpose |
|-------|---------|
| `sessionId` | Groups all messages in a single conversation session |
| `contextId` | Threads related sessions into a broader context |
| `contextId` + `sessionId` together enable multi-turn cross-agent conversations |

---

## URL Patterns (HTTP+JSON binding)

| Operation | Method | Path |
|-----------|--------|------|
| `tasks/send` | POST | `/a2a/tasks/send` |
| `tasks/sendSubscribe` | POST | `/a2a/tasks/sendSubscribe` |
| `tasks/get` | POST | `/a2a/tasks/get` |
| `tasks/list` | POST | `/a2a/tasks/list` |
| `tasks/cancel` | POST | `/a2a/tasks/cancel` |
| `tasks/subscribe` | POST | `/a2a/tasks/subscribe` |
| `tasks/push` | POST | `/a2a/tasks/push` |
| AgentCard | GET | `/.well-known/agent.json` |
| Extended AgentCard | GET | `/a2a/agent/extendedCard` |

path: references/quickstart.md
content: # A2A Quickstart — Full End-to-End Example

> This document walks through a complete A2A interaction from discovery to task completion.
> All JSON-RPC requests and responses are shown in full — copy and adapt directly.

---

## Scenario

- **Server Agent**: `data-agent` — receives a data extraction task, outputs structured JSON
- **Client Agent**: `orchestrator` — discovers `data-agent`, sends a task, retrieves results

Both agents are A2A-compliant. The client does not know the server's internal implementation.

---

## Step 1: Server Agent publishes its AgentCard

The server agent serves `/.well-known/agent.json`:

```bash
# Server: GET https://data-agent.example.com/.well-known/agent.json
```

```json
{
  "name": "data-agent",
  "description": "Specialized in extracting financial metrics from raw CSV and JSON data sources",
  "version": "1.0",
  "provider": {
    "organization": "Acme Corp",
    "url": "https://acme.example.com"
  },
  "capabilities": {
    "streaming": "SUPPORTED",
    "pushNotifications": "NOT_SUPPORTED",
    "agentPrivilege": false,
    "signedAgentCard": false,
    "dataStreamingSupported": true,
    "supportsAuthenticatedExtendedAgentCard": false
  },
  "skills": [
    {
      "id": "csv-extraction",
      "name": "CSV Financial Extraction",
      "description": "Extracts revenue, EBITDA, and net income from CSV files",
      "tags": ["finance", "csv", "extraction"],
      "examples": ["Extract revenue from Q3_2025.csv"]
    }
  ],
  "authentication": {
    "schemes": ["httpBearer"],
    "credentials": "required"
  },
  "defaultInputModes": ["text", "json", "csv"],
  "defaultOutputModes": ["json"],
  "endpoint": "https://data-agent.example.com/a2a/"
}
```

---

## Step 2: Client discovers the server's AgentCard

```bash
GET https://data-agent.example.com/.well-known/agent.json
```

Client reads the AgentCard and confirms:
- `capabilities.streaming = SUPPORTED` ✓ (we want streaming)
- `authentication.schemes = ["httpBearer"]` → need a Bearer token
- `skills[0].id = "csv-extraction"` matches the task

---

## Step 3: Client sends a task (streaming)

Client constructs and sends a streaming task:

```bash
POST https://data-agent.example.com/a2a/tasks/sendSubscribe
Content-Type: application/a2a+json
[REDACTED_SECRET]
```

```json
{
  "jsonrpc": "2.0",
  "method": "tasks/sendSubscribe",
  "params": {
    "taskId": "task-abc-123",
    "sessionId": "session-xyz-456",
    "message": {
      "role": "user",
      "parts": [
        {
          "kind": "data",
          "data": {
            "instruction": "Extract revenue and EBITDA from Q3_2025.csv",
            "fileUrl": "https://storage.acme.example.com/data/Q3_2025.csv"
          }
        }
      ]
    },
    "configuration": {
      "stream": true
    }
  },
  "id": 1
}
```

---

## Step 4: Server responds with streaming SSE events

```
event: task_status_update
data: {"kind":"TaskStatusUpdateEvent","taskId":"task-abc-123","status":{"state":"submitted","message":"Task received","timestamp":"2026-05-14T07:00:01Z"},"final":false}

event: task_status_update
data: {"kind":"TaskStatusUpdateEvent","taskId":"task-abc-123","status":{"state":"working","message":"Fetching file","timestamp":"2026-05-14T07:00:02Z"},"final":false}

event: task_artifact_update
data: {"kind":"TaskArtifactUpdateEvent","taskId":"task-abc-123","artifact":{"artifactId":"art-001","name":"partial-extract","parts":[{"kind":"data","data":{"status":"parsing","rows_processed":42}}]},"final":false}

event: task_status_update
data: {"kind":"TaskStatusUpdateEvent","taskId":"task-abc-123","status":{"state":"completed","message":"Extraction complete","timestamp":"2026-05-14T07:00:05Z"},"final":true}
```

Client accumulates events, then extracts the final result.

---

## Step 5: Client extracts the final artifact

From the completed task (via `tasks/get` if polling instead of streaming):

```json
{
  "task": {
    "taskId": "task-abc-123",
    "sessionId": "session-xyz-456",
    "status": {
      "state": "completed",
      "message": "Extraction complete",
      "timestamp": "2026-05-14T07:00:05Z"
    },
    "artifacts": [
      {
        "artifactId": "art-001",
        "name": "Q3-2025-financials",
        "description": "Extracted financial metrics from Q3_2025.csv",
        "parts": [
          {
            "kind": "data",
            "data": {
              "revenue": 4523000,
              "ebitda": 1234000,
              "net_income": 891000,
              "currency": "USD",
              "source_file": "Q3_2025.csv",
              "rows_processed": 847
            }
          }
        ]
      }
    ]
  }
}
```

Client returns `artifacts[0].parts[0].data` to the user — structured output, not the raw task object.

---

## Error Handling Example

### Version Negotiation Failure

```json
// Client sends (declares A2A version):
{
  "jsonrpc": "2.0",
  "method": "tasks/send",
  "params": {
    "message": { "role": "user", "parts": [{"kind": "text", "text": "do stuff"}] }
  },
  "id": 1,
  "a2aVersion": "0.9"   // ← old version
}
```

```json
// Server responds:
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Version negotiation failed",
    "data": {
      "supportedVersions": ["1.0"],
      "requestedVersion": "0.9"
    }
  }
}
```

Client retries with `a2aVersion: "1.0"`.

---

## MCP + A2A Combined Scenario

```
User ──→ Orchestrator Agent (A2A Client)
              │
              ├── MCP: calls search_tool → gets raw CSV URL
              │
              └── A2A: sends task to data-agent
                        │ (with CSV URL in message.data)
                        ▼
                   data-agent
                        │ (extracts, computes)
                        ▼
                   returns artifacts[].parts[].data
                        │
                        ▼
              Orchestrator ──→ MCP: calls report_tool → generates PDF
```

In this flow:
- **MCP** connects the orchestrator to tools and storage
- **A2A** connects the orchestrator to the specialized data-agent
- Both protocols coexist — they solve different problems

path: scripts/mock_a2a_server.py
content: #!/usr/bin/env python3
"""
Mock A2A Server — a local A2A agent for testing and experimentation.

This server implements a complete A2A agent endpoint. Once running, other A2A clients
(can be AI agents using the a2a-protocol skill) can discover and interact with it.

Usage:
    python3 mock_a2a_server.py [--port 8080] [--name string] [--skills id:name:desc ...]

The server automatically serves its AgentCard at /.well-known/agent.json.

Examples:
    # Basic server on port 8080
    python3 mock_a2a_server.py --port 8080

    # Custom named agent with specific skills
    python3 mock_a2a_server.py --port 8080 --name helper-agent --skills "code:Coding:Reviews and writes code" "doc:Documentation:Generates docs"

    # Start and test from another terminal:
    #   curl http://localhost:8080/.well-known/agent.json
    #   python3 scripts/validate_agent_card.py /tmp/card.json  (fetch it first)
"""

import argparse
import json
import re
import sys
import time
import uuid
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from datetime import datetime, timezone


# ─── Configuration ────────────────────────────────────────────────────────────

DEFAULT_PORT = 8080
DEFAULT_NAME = "mock-a2a-agent"
DEFAULT_DESCRIPTION = "A helpful mock A2A agent for testing. Echoes structured data back with simulated processing."
DEFAULT_VERSION = "1.0"
DEFAULT_SKILLS = [
    ("echo", "Echo", "Echoes back the input data as output (for testing connectivity)"),
    ("capitalize", "Capitalize Text", "Capitalizes all text in the input string"),
    ("word-count", "Word Count", "Returns a word-count summary of the input text"),
]

TASKS = {}  # taskId -> task object (in-memory store)


# ─── JSON-RPC Helpers ────────────────────────────────────────────────────────

def jsonrpc_response(result, req_id):
    return {"jsonrpc": "2.0", "result": result, "id": req_id}


def jsonrpc_error(code, message, req_id, data=None):
    err = {"jsonrpc": "2.0", "error": {"code": code, "message": message}, "id": req_id}
    if data:
        err["error"]["data"] = data
    return err


def now_iso():
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


# ─── Task Lifecycle ──────────────────────────────────────────────────────────

def make_task(task_id, session_id, context_id, message):
    return {
        "taskId": task_id,
        "sessionId": session_id,
        "contextId": context_id,
        "status": {"state": "submitted", "message": "Task received", "timestamp": now_iso()},
        "metadata": {"receivedAt": now_iso()},
        "artifacts": [],
        "messages": [message] if message else [],
    }


def process_task(task):
    """
    Simulate work and produce an artifact.
    Reads the input parts and produces a response based on message content.
    """
    msg = task["messages"][-1] if task["messages"] else {}
    parts = msg.get("parts", [])

    # Extract text from parts
    text_input = ""
    data_input = {}
    for part in parts:
        kind = part.get("kind")
        if kind == "text":
            text_input += part.get("text", "")
        elif kind == "data":
            data_input.update(part.get("data", {}))

    instruction = data_input.get("instruction", text_input.strip())
    skill_id = data_input.get("skill", "")

    # Infer skill from content if not explicitly specified
    if not skill_id and instruction:
        lowered = instruction.lower()
        if "capitalize" in lowered:
            skill_id = "capitalize"
        elif "word count" in lowered or "word-count" in lowered or "count words" in lowered:
            skill_id = "word-count"
        elif "echo" in lowered:
            skill_id = "echo"
        else:
            skill_id = "echo"  # default

    # ── Skill routing ─────────────────────────────────────────────────────
    if skill_id == "capitalize":
        output_data = {"result": instruction.upper()}
        artifact_name = "capitalized-text"
    elif skill_id == "word-count":
        words = instruction.split()
        output_data = {
            "word_count": len(words),
            "char_count": len(instruction),
            "char_count_no_spaces": len(instruction.replace(" ", "")),
            "preview": instruction[:50] + "..." if len(instruction) > 50 else instruction,
        }
        artifact_name = "word-count-result"
    else:  # echo
        output_data = {
            "echo": instruction,
            "skill_used": skill_id,
            "processing_time_ms": round((time.time() % 1) * 1000),
            "server": args.name,
        }
        artifact_name = "echo-response"

    task["artifacts"] = [{
        "artifactId": f"art-{uuid.uuid4().hex[:8]}",
        "name": artifact_name,
        "description": f"Result from {skill_id} skill",
        "parts": [{"kind": "data", "data": output_data}],
    }]
    task["status"] = {"state": "completed", "message": "Task completed successfully", "timestamp": now_iso()}


def streaming_events(task, stream=True):
    """
    Yield SSE-formatted lines for a streaming response.
    Simulates working state -> artifact updates -> completed.
    """
    task["status"] = {"state": "working", "message": "Processing task...", "timestamp": now_iso()}

    yield f"event: task_status_update\ndata: {json.dumps({
        'kind': 'TaskStatusUpdateEvent',
        'taskId': task['taskId'],
        'status': task['status'],
        'final': False
    })}\n\n"

    # Simulate partial artifact update
    yield f"event: task_artifact_update\ndata: {json.dumps({
        'kind': 'TaskArtifactUpdateEvent',
        'taskId': task['taskId'],
        'artifact': {
            'artifactId': f'art-{uuid.uuid4().hex[:8]}',
            'name': 'partial-result',
            'parts': [{'kind': 'data', 'data': {'status': 'processing'}}]
        },
        'final': False
    })}\n\n"

    time.sleep(0.05)

    # Process the task
    process_task(task)

    # Final artifact update
    if task["artifacts"]:
        yield f"event: task_artifact_update\ndata: {json.dumps({
            'kind': 'TaskArtifactUpdateEvent',
            'taskId': task['taskId'],
            'artifact': task["artifacts"][0],
            'final': True
        })}\n\n"

    # Terminal status
    yield f"event: task_status_update\ndata: {json.dumps({
        'kind': 'TaskStatusUpdateEvent',
        'taskId': task['taskId'],
        'status': task['status'],
        'final': True
    })}\n\n"


# ─── Request Parser ──────────────────────────────────────────────────────────

def parse_jsonrpc(body):
    try:
        req = json.loads(body)
        return req
    except json.JSONDecodeError:
        return None


# ─── AgentCard ───────────────────────────────────────────────────────────────

def build_agent_card():
    skills = []
    for sid, sname, sdesc in args.skills or DEFAULT_SKILLS:
        skills.append({
            "id": sid,
            "name": sname,
            "description": sdesc,
            "tags": sid.split("-"),
            "examples": [f"Use the {sname} skill"]
        })

    return {
        "name": args.name or DEFAULT_NAME,
        "description": args.description or DEFAULT_DESCRIPTION,
        "version": DEFAULT_VERSION,
        "provider": {
            "organization": "mock-a2a-server",
            "url": f"http://localhost:{args.port}"
        },
        "capabilities": {
            "streaming": "SUPPORTED",
            "pushNotifications": "SUPPORTED",
            "agentPrivilege": False,
            "signedAgentCard": False,
            "dataStreamingSupported": True,
            "supportsAuthenticatedExtendedAgentCard": False,
        },
        "skills": skills,
        "authentication": {
            "schemes": [],
            "credentials": "optional"
        },
        "defaultInputModes": ["text", "json"],
        "defaultOutputModes": ["json", "text"],
        "endpoint": f"http://localhost:{args.port}/a2a/",
        "url": f"http://localhost:{args.port}",
    }


# ─── HTTP Handler ────────────────────────────────────────────────────────────

class A2AHandler(BaseHTTPRequestHandler):

    def log_message(self, fmt, *args):
        sys.stdout.write(f"[{now_iso()}] {fmt % args}\n")
        sys.stdout.flush()

    # ── Utility ──────────────────────────────────────────────────────────

    def send_json(self, status, payload):
        body = json.dumps(payload, ensure_ascii=False)
        self.send_response(status)
        self.send_header("Content-Type", "application/a2a+json; charset=utf-8")
        self.send_header("Content-Length", str(len(body.encode("utf-8"))))
        self.end_headers()
        self.wfile.write(body.encode("utf-8"))

    def send_sse(self, event_generator):
        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Cache-Control", "no-cache")
        self.send_header("Connection", "keep-alive")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        try:
            for line in event_generator:
                self.wfile.write(line.encode("utf-8"))
                self.wfile.flush()
        except BrokenPipeError:
            pass

    def read_body(self):
        content_len = int(self.headers.get("Content-Length", 0))
        return self.rfile.read(content_len).decode("utf-8")

    # ── Routes ───────────────────────────────────────────────────────────

    def do_GET(self):
        parsed = urlparse(self.path)
        path = parsed.path.rstrip("/")

        if path == "/.well-known/agent.json":
            card = build_agent_card()
            self.send_json(200, card)
            return

        if path == "/a2a/agent/extendedCard":
            # No auth in mock server, just echo the card
            self.send_json(200, build_agent_card())
            return

        if path == "/a2a/tasks/list":
            self.send_json(200, jsonrpc_response({
                "tasks": list(TASKS.values()),
            }, req_id=1))
            return

        self.send_json(404, {"error": f"GET {path} not found"})

    def do_POST(self):
        parsed = urlparse(self.path)
        path = parsed.path.rstrip("/")
        body = self.read_body()
        req = parse_jsonrpc(body)

        if req is None:
            self.send_json(400, jsonrpc_error(-32700, "Parse error", req_id=None))
            return

        method = req.get("method", "")
        params = req.get("params", {})
        req_id = req.get("id")

        # ── tasks/send ─────────────────────────────────────────────────
        if method == "tasks/send":
            task_id = params.get("taskId") or f"task-{uuid.uuid4().hex[:8]}"
            session_id = params.get("sessionId", "")
            context_id = params.get("contextId", "")
            message = params.get("message", {})
            configuration = params.get("configuration", {})

            task = make_task(task_id, session_id, context_id, message)
            TASKS[task_id] = task

            # Sync execution: process immediately
            process_task(task)
            self.send_json(200, jsonrpc_response({
                "task": task,
                "artifacts": task["artifacts"],
            }, req_id=req_id))
            return

        # ── tasks/sendSubscribe ─────────────────────────────────────────
        if method == "tasks/sendSubscribe":
            task_id = params.get("taskId") or f"task-{uuid.uuid4().hex[:8]}"
            session_id = params.get("sessionId", "")
            context_id = params.get("contextId", "")
            message = params.get("message", {})
            configuration = params.get("configuration", {})

            task = make_task(task_id, session_id, context_id, message)
            TASKS[task_id] = task

            self.send_sse(streaming_events(task))
            return

        # ── tasks/get ───────────────────────────────────────────────────
        if method == "tasks/get":
            task_id = params.get("taskId", "")
            task = TASKS.get(task_id)
            if not task:
                self.send_json(404, jsonrpc_error(-32603, f"Task not found: {task_id}", req_id=req_id))
                return
            self.send_json(200, jsonrpc_response({"task": task}, req_id=req_id))
            return

        # ── tasks/list ──────────────────────────────────────────────────
        if method == "tasks/list":
            session_id = params.get("sessionId")
            tasks = list(TASKS.values())
            if session_id:
                tasks = [t for t in tasks if t.get("sessionId") == session_id]
            self.send_json(200, jsonrpc_response({"tasks": tasks}, req_id=req_id))
            return

        # ── tasks/cancel ────────────────────────────────────────────────
        if method == "tasks/cancel":
            task_id = params.get("taskId", "")
            task = TASKS.get(task_id)
            if not task:
                self.send_json(404, jsonrpc_error(-32603, f"Task not found: {task_id}", req_id=req_id))
                return
            task["status"] = {"state": "canceled", "message": "Canceled by client", "timestamp": now_iso()}
            self.send_json(200, jsonrpc_response({"task": task}, req_id=req_id))
            return

        # ── tasks/subscribe ────────────────────────────────────────────
        if method == "tasks/subscribe":
            task_id = params.get("taskId", "")
            push_config = params.get("pushConfig", {})
            task = TASKS.get(task_id)
            if not task:
                self.send_json(404, jsonrpc_error(-32603, f"Task not found: {task_id}", req_id=req_id))
                return
            # Mock: immediately push result to the push URL
            push_url = push_config.get("pushProviderUrl")
            if push_url:
                try:
                    import urllib.request
                    payload = json.dumps({
                        "taskId": task_id,
                        "result": {"task": task, "artifacts": task["artifacts"]},
                        "pushNotificationConfigId": push_config.get("id", "mock-config"),
                    })
                    req = urllib.request.Request(
                        push_url,
                        data=payload.encode("utf-8"),
                        headers={"Content-Type": "application/a2a+json"},
                        method="POST"
                    )
                    urllib.request.urlopen(req, timeout=5)
                except Exception as e:
                    sys.stdout.write(f"[mock server] push notification failed: {e}\n")
            self.send_json(200, jsonrpc_response({"taskId": task_id, "subscribed": True}, req_id=req_id))
            return

        # ── tasks/push (incoming webhook — stub) ───────────────────────
        if method == "tasks/push":
            # This is what the mock server calls when it pushes to a client
            self.send_json(200, jsonrpc_response({"received": True}, req_id=req_id))
            return

        # ── pushNotificationConfig/* ──────────────────────────────────
        if method.startswith("pushNotificationConfig/"):
            # Mock implementation — store config in memory (not persisted)
            action = method.split("/", 1)[1]
            if action == "create":
                cfg = params
                cfg["id"] = f"cfg-{uuid.uuid4().hex[:8]}"
                self.send_json(200, jsonrpc_response(cfg, req_id=req_id))
            elif action == "get":
                self.send_json(200, jsonrpc_response(params, req_id=req_id))
            elif action == "list":
                self.send_json(200, jsonrpc_response({"configs": []}, req_id=req_id))
            elif action == "delete":
                self.send_json(200, jsonrpc_response({"deleted": True}, req_id=req_id))
            return

        # ── agent/getExtendedAgentCard ────────────────────────────────
        if method == "agent/getExtendedAgentCard":
            self.send_json(200, jsonrpc_response(build_agent_card(), req_id=req_id))
            return

        self.send_json(404, jsonrpc_error(-32603, f"Unknown method: {method}", req_id=req_id))


# ─── CLI ────────────────────────────────────────────────────────────────────

def parse_skills(skill_strs):
    """Parse 'id:name:desc' strings into skill tuples."""
    result = []
    for s in skill_strs:
        parts = s.split(":", 2)
        if len(parts) == 3:
            result.append(tuple(parts))
        elif len(parts) == 2:
            result.append((parts[0], parts[1], parts[1]))
        else:
            sys.stderr.write(f"⚠️  Ignoring malformed skill: {s} (expected id:name:description)\n")
    return result


parser = argparse.ArgumentParser(description="Mock A2A Server")
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Port to listen on (default: {DEFAULT_PORT})")
parser.add_argument("--name", type=str, default=None, help="Agent name")
parser.add_argument("--description", type=str, default=None, help="Agent description")
parser.add_argument("--skills", nargs="+", default=None, help="Skills as 'id:name:description' triplets")
args = parser.parse_args()
args.skills = parse_skills(args.skills) if args.skills else None


def run():
    addr = ("0.0.0.0", args.port)
    server = HTTPServer(addr, A2AHandler)
    card = build_agent_card()

    print()
    print("=" * 56)
    print(f"  🤖  A2A Mock Server")
    print("=" * 56)
    print(f"  Name:     {card['name']}")
    print(f"  Version:  {card['version']}")
    print(f"  Skills:   {', '.join(s[0] for s in (args.skills or DEFAULT_SKILLS))}")
    print(f"  Endpoint: http://localhost:{args.port}/a2a/")
    print(f"  AgentCard: http://localhost:{args.port}/.well-known/agent.json")
    print("=" * 56)
    print()
    print(f"  Available skills:")
    for sid, sname, sdesc in (args.skills or DEFAULT_SKILLS):
        print(f"    [{sid}] {sname}")
        print(f"          {sdesc}")
    print()
    print(f"  Try this from another terminal:")
    print(f"    curl http://localhost:{args.port}/.well-known/agent.json")
    print()
    print(f"  Press Ctrl+C to stop.")
    print()
    sys.stdout.flush()

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n👋 Shutting down mock A2A server.")
        server.shutdown()


if __name__ == "__main__":
    run()

path: scripts/requirements.txt
content: #!/usr/bin/env python3
# Requirements for validate_agent_card.py
# (no external dependencies — uses stdlib only)

path: scripts/setup.sh
content: #!/bin/bash
# Setup script for a2a-protocol scripts/
# (validate_agent_card.py uses stdlib only — no install needed)

echo "✅ scripts/ ready — validate_agent_card.py is runnable directly"
echo "   Usage: python3 scripts/validate_agent_card.py <agent-card.json>"

path: scripts/validate_agent_card.py
content: #!/usr/bin/env python3
"""
Validate an AgentCard JSON file against A2A schema requirements.

Usage:
    python3 validate_agent_card.py <agent-card.json>

Exit codes:
    0 — valid
    1 — invalid (missing required fields)
    2 — file not found or not readable
"""

import json
import sys
from pathlib import Path


REQUIRED_TOP_LEVEL = ["name", "description", "version", "capabilities", "authentication"]
REQUIRED_CAPABILITIES = ["streaming", "pushNotifications"]
REQUIRED_AUTH = ["schemes", "credentials"]


def validate_file(path: str) -> bool:
    p = Path(path)
    if not p.exists():
        print(f"ERROR: file not found: {path}")
        return False

    try:
        with open(p) as f:
            card = json.load(f)
    except json.JSONDecodeError as e:
        print(f"ERROR: invalid JSON: {e}")
        return False

    # Top-level required fields
    for field in REQUIRED_TOP_LEVEL:
        if field not in card:
            print(f"ERROR: missing required top-level field: {field}")
            return False

    # capabilities.* required fields
    caps = card.get("capabilities", {})
    for field in REQUIRED_CAPABILITIES:
        if field not in caps:
            print(f"ERROR: missing capabilities.{field}")
            return False

    # Validate streaming value
    valid_streaming = {"SUPPORTED", "NOT_SUPPORTED", "STREAMABLE"}
    if caps.get("streaming") not in valid_streaming:
        print(f"ERROR: capabilities.streaming must be one of {valid_streaming}")
        return False

    # Validate pushNotifications value
    valid_push = {"SUPPORTED", "NOT_SUPPORTED"}
    if caps.get("pushNotifications") not in valid_push:
        print(f"ERROR: capabilities.pushNotifications must be one of {valid_push}")
        return False

    # authentication.* required fields
    auth = card.get("authentication", {})
    for field in REQUIRED_AUTH:
        if field not in auth:
            print(f"ERROR: missing authentication.{field}")
            return False

    # credentials must be "required" or "optional"
    valid_creds = {"required", "optional"}
    if auth.get("credentials") not in valid_creds:
        print(f"ERROR: authentication.credentials must be one of {valid_creds}")
        return False

    # skills[] — each skill needs id, name, description
    for i, skill in enumerate(card.get("skills", [])):
        for field in ["id", "name", "description"]:
            if field not in skill:
                print(f"ERROR: skills[{i}] missing required field: {field}")
                return False

    # skills[].id uniqueness
    ids = [s["id"] for s in card.get("skills", [])]
    if len(ids) != len(set(ids)):
        print("ERROR: skills[].id values must be unique")
        return False

    print(f"✅ AgentCard is valid — {p}")
    print(f"   name:         {card['name']}")
    print(f"   version:      {card['version']}")
    print(f"   streaming:    {caps['streaming']}")
    print(f"   pushNotif:    {caps['pushNotifications']}")
    print(f"   auth.schemes: {auth['schemes']}")
    print(f"   skills count: {len(card.get('skills', []))}")
    return True


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 validate_agent_card.py <agent-card.json>")
        sys.exit(2)
    sys.exit(0 if validate_file(sys.argv[1]) else 1)


if __name__ == "__main__":
    main()

path: tests/test_skill.py
content: #!/usr/bin/env python3
"""
Auto-generated test suite for skill: a2a-protocol
Run with: python tests/test_skill.py
"""

import sys, os, re, yaml

def _p(name, passed, msg=''):
    emoji = "✅" if passed else "❌"
    result = "PASS" if passed else "FAIL"
    print(f"  [{emoji}] {result} -- {name}{msg}")
    return passed

def _skill_md():
    return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'SKILL.md')

def _frontmatter():
    with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
        text = f.read()
    end = text.find('\n---', 3)
    if end == -1:
        return None
    try:
        return yaml.safe_load(text[3:end]) or {}
    except Exception:
        return None

def test_frontmatter_delimiters():
    with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
        text = f.read()
    return _p("frontmatter_delimiters", text.startswith("---"))

def test_frontmatter_name():
    fm = _frontmatter()
    if fm is None:
        return _p("frontmatter_name", False, " -- no frontmatter")
    name = fm.get('name', '').strip()
    skill_dir = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    return _p('frontmatter_name', name == skill_dir,
             f" (got '{name}', expected '{skill_dir}')")

def test_frontmatter_description():
    fm = _frontmatter()
    if fm is None:
        return _p("frontmatter_description", False)
    return _p('frontmatter_description', bool(fm.get('description', '').strip()))

def test_frontmatter_license():
    fm = _frontmatter()
    if fm is None:
        return _p("frontmatter_license", False)
    lic = fm.get('license', '').strip().upper()
    return _p('frontmatter_license', lic == 'MIT' or 'MIT' in lic)

def test_frontmatter_metadata():
    fm = _frontmatter()
    if fm is None:
        return _p("frontmatter_metadata", False)
    meta = fm.get('metadata', {}) or {}
    has_version = bool(str(meta.get('version', '')).strip())
    has_category = bool(str(meta.get('category', '')).strip())
    return _p('frontmatter_metadata', has_version and has_category,
             f" (version={has_version}, category={has_category})")

def test_has_modes():
    with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
        text = f.read()
    return _p("has_modes", "## Modes" in text or "## Mode" in text or "## Core Position" in text)

def test_has_do_not():
    with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
        text = f.read()
    return _p("has_do_not", "Do not" in text or "do-not" in text or "Must not" in text)

def test_no_secrets():
    SECRET_PATTERNS = [
        r"sk-[A-Za-z0-9]{20,}",
        r"sk-[A-Za-z0-9][A-Za-z0-9-]{19,}",
        r"AKIA[A-Z0-9]{16}",
        r"ghp_[A-Za-z0-9]{36}",
        r"(?i)api[_-]?key\s*[=:]\s*['\"]?[A-Za-z0-9_-]{20,}",
    ]
    skill_path = os.path.dirname(os.path.abspath(__file__))
    passed = True
    for root, dirs, files in os.walk(skill_path):
        dirs[:] = [d for d in dirs if not d.startswith(".") and "installed_skills" not in d]
        for fname in files:
            if fname.startswith("."):
                continue
            fpath = os.path.join(root, fname)
            try:
                with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read()
            except Exception:
                continue
            for pat in SECRET_PATTERNS:
                if re.search(pat, content):
                    rel = os.path.relpath(fpath, skill_path)
                    print(f"  \u274c FAIL -- secret found: {rel}")
                    passed = False
                    break
    return _p('no_secrets', passed)

def test_scripts_shebangs():
    scripts_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'scripts')
    if not os.path.isdir(scripts_dir):
        return _p("scripts_shebangs_skipped", True, " (no scripts/ dir)")
    passed = True
    for fname in os.listdir(scripts_dir):
        if fname.startswith("."):
            continue
        if fname.endswith((".py", ".sh")):
            fpath = os.path.join(scripts_dir, fname)
            try:
                with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                    first = f.read(10)
                if not first.startswith("#!"):
                    print(f"  \u274c FAIL -- missing shebang: scripts/{fname}")
                    passed = False
            except Exception:
                pass
    return _p('scripts_have_shebangs', passed)

def test_skill_md_size():
    with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
        content = f.read()
    line_count = content.count('\n') + 1
    if line_count <= 400:
        hint = "\u2705 good"
    elif line_count <= 800:
        hint = "\u26a0\ufe0f consider extracting to references/"
    else:
        hint = "\u274c too large"
    return _p('skill_md_size', line_count <= 800,
             f" ({line_count} lines -- {hint})")

def _main():
    print()
    print("=" * 60)
    print(f"  \U0001f9ea TEST SUITE -- {os.path.basename(os.path.dirname(os.path.abspath(__file__)))}")
    print("=" * 60)
    print()
    tests = [
        test_frontmatter_delimiters,
        test_frontmatter_name,
        test_frontmatter_description,
        test_frontmatter_license,
        test_frontmatter_metadata,
        test_has_modes,
        test_has_do_not,
        test_no_secrets,
        test_scripts_shebangs,
        test_skill_md_size,
    ]
    results = []
    for t in tests:
        try:
            results.append(t())
        except Exception as e:
            print(f"  \u274c FAIL -- {t.__name__} raised {e}")
            results.append(False)
    passed = sum(results)
    total = len(results)
    print()
    print("=" * 60)
    print(f"  \U0001f4ca RESULTS: {passed}/{total} passed")
    print("=" * 60)
    if passed == total:
        print("  \u2705 ALL TESTS PASSED")
    else:
        print(f'  \u274c {total - passed} test(s) FAILED -- fix before submission')
    print()
    return 0 if passed == total else 1

if __name__ == '__main__':
    sys.exit(_main())


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)