5.0 Why agents need protocols
Agentic AI on AWS, hands-on labs by Shivank Agrawal

How agents
talk

Section 5. Two open protocols do for agents what USB and HTTP did for computers. MCP plugs an agent into tools and data that live in other programs. A2A lets independent agents find each other and work together. You'll build both.

Folder section-05-mcp-and-a2a Labs mcp-basics mcp-with-strands-agent a2a-travel-agents Model: Claude Sonnet 4.6 on Amazon Bedrock
Swipe sideways to see the full diagram
Travel agentyours Flights agentanother team's A2A agent ↔ agent MCPMCP Weather Database Flight search Booking API MCP = agent ↔ tools each agent's own toolbox
Two layers of interoperability. A2A connects agents to agents; MCP connects each agent to its tools and data. You'll build each layer in its own lab.
5.0

Why agents need protocols

You've built agents, given them tools and memory, and organised them into teams. The next step is communication: how agents connect reliably to outside tools, data and other agents, in a structured, reusable way.

As agents get more capable, this communication layer becomes critical. Without a standard, every new tool, service or partner agent means more custom integration code, more maintenance and more complexity. Two protocols tackle this from different directions:

MModel Context Protocol (MCP)

Standardises how agents and models connect to tools, resources and prompt templates, so an agent can discover and use outside capabilities without hard-coding every integration.

AAgent-to-Agent protocol (A2A)

Standardises how agents talk to other agents, so specialised agents can collaborate across teams and systems while keeping their internals private.

✦Think of it like this

MCP is the universal charging port: any device that has it works with any charger that has it, and nobody needs a drawer full of special cables. A2A is more like the way two companies email each other: each keeps its own staff and processes private, but they agree on how a request looks, how to reply, and how to follow up.

What you'll learn here

Before you start

  1. 1

    A working Strands setup from earlier sections

  2. 2

    Basic Python functions and classes

    The MCP lab also uses async/await; you met it in Section 2.

  3. 3

    AWS credentials with Amazon Bedrock access

    Needed for the Strands + MCP lab and the A2A lab (Claude Sonnet 4.6, override with BEDROCK_MODEL_ID). The MCP basics lab needs no AWS and no LLM at all.

  4. 4

    An IDE and three terminal windows

    VS Code, Kiro or Claude Code. The A2A lab runs three programs side by side.

  5. 5

    REST and JSON (helpful, not required)

FolderWhat it showsOn this page
mcp-basics/An MCP server exposing every primitive (tools, resources, prompts, roots) and a client that uses them. No LLM involved.5.6
mcp-with-strands-agent/Strands agents that get their tools from MCP servers: our local server, and the AWS Documentation MCP server5.7
a2a-travel-agents/Two A2A agent servers (weather, flights) and an orchestrator that discovers and calls them5.11
terminal
# MCP basics (no AWS needed)
cd mcp-basics && pip install -r requirements.txt && python client.py

# Strands + MCP
cd ../mcp-with-strands-agent && pip install -r requirements.txt
python agent_with_local_mcp.py
python agent_with_aws_docs_mcp.py

# A2A — three terminals
cd ../a2a-travel-agents && pip install -r requirements.txt
python weather_agent_server.py      # terminal 1
python flights_agent_server.py      # terminal 2
python travel_orchestrator.py       # terminal 3
iLibrary versions matter here

The MCP labs pin mcp[cli]>=1.23.0,<2.0.0 because version 2 renamed FastMCP and changed client APIs; this code targets the 1.x line. The A2A lab pins a2a-sdk>=0.3.0,<0.4.0 and installs Strands with its [a2a] extra.

MCPA2A
PurposeConnect an LLM app to tools and dataConnect agents to other agents
Transportstdio, Streamable HTTPHTTP (JSON-RPC, SSE)
Discoverylist_tools(), list_resources(), list_prompts()Agent card at /.well-known/agent-card.json
Who has the LLMThe client sideBoth sides; each agent has its own
MessagesStructured calls (tool name + arguments)Natural-language messages and tasks
→In one line

MCP is LLM ↔ tool. A2A is agent ↔ agent.

5.1

MCP: why tools need a standard

As agents grow, managing dozens or hundreds of tools gets hard. What if an agent could discover tools at run time, including tools built by other teams, without hard-coding each one? That's what the is for.

Agents need to reach APIs, databases and developer platforms. Without a standard, every model needs a custom integration for every tool, which quickly gets complex, costly and hard to maintain. A standard protocol gives one common interface for tool use: less integration work, better interoperability, and easier growth as new tools and models arrive.

Swipe sideways to see the full diagram
Traditional (N × M) Model 1 Model 2 Model 3 DynamoDB AWS Glue GitHub 9 connections to build and maintain With MCP (N + M) MCP Model 1 Model 2 Model 3 DynamoDB AWS Glue GitHub 6 connections
N × M becomes N + M. Each model connects to MCP once and each tool connects to MCP once. Adding a fourth model or tool adds one connection, not three.
Try it

Count the integrations

Swipe sideways to see the full diagram
App 1App 2App 3Tool 1Tool 2Tool 3
3 × 3 = 9 custom integrations to build, test and maintain. Every new app has to be wired to every tool again, and one API change in a tool breaks 3 integrations. Try adding one more tool: that's 3 more connectors.

What problem does MCP solve?

Imagine an enterprise agent that must work with your CRM, inventory database, email, calendar, document storage and more. Traditionally you'd write custom integration code for each system, keep it working as each system changes, handle authentication and authorisation for every one, juggle different API formats and conventions, and update the agent whenever a new system is added. That siloed approach leads to fragmented app development: every integration built from scratch, with no shared standard.

Worse, each AI app builds its entire stack on its own:

Swipe sideways to see the full diagram
AI app 1 Custom implementation Custom prompt logic Custom tool calls Custom data access AI app 2 Custom implementation Custom prompt logic Custom tool calls Custom data access AI app 3 Custom implementation Custom prompt logic Custom tool calls Custom data access
Three apps, three of everything. If app 1 solves a data-access problem, apps 2 and 3 can't benefit; bugs are fixed three times; the same task may behave differently in each app.

This doesn't scale. It's slow, error-prone, and couples your agent tightly to every system it touches. MCP replaces it with one standard layer that every AI app shares, a common protocol for tool calls and data access. The benefits:

Dynamic tool discovery

Your agent automatically sees every tool on the servers it's connected to.

No integration code

Connect to new tool sources without writing custom adapters.

Automatic updates

When a server adds or changes tools, your agent sees the change.

Unified interface

Tools from AWS, GitHub or your internal systems all work the same way.

Flexible deployment

MCP servers can run locally on your machine or remotely in the cloud.

MCP is a universal adapter for agents, much like USB for devices. Through it, models reach databases (such as DynamoDB), CRMs, calendars, code repositories (GitHub), messaging (Slack) and document storage (Google Drive), all through one integration layer:

Swipe sideways to see the full diagram
Many-to-many integrations AI model 1 AI model 2 AI model 3 Database CRM Calendar GitHub Slack complex and hard to maintain Model Context Protocol AI models MCP layer Database CRM Calendar GitHub Slack Drive one central interface
A tangle vs a hub. Without MCP, agents maintain a separate integration for each tool. With MCP, one standard, reusable connection layer reaches all of them the same way.
5.2

How MCP works

MCP sets up a standard protocol between your agent and tool providers, much as USB set a universal standard for connecting devices to computers.

Swipe sideways to see the full diagram
Hostyour AI app or agent MCP clienttool consumer MCP servertool provider Data sources requests go right → · ← results come back
Four parts, one chain. The host never talks to a database directly; it goes through a client and a server that both speak MCP.
  1. 1

    Host

    The AI application or agent that needs to get things done: answer questions, retrieve information, use tools. It doesn't connect to outside systems directly; it talks to an MCP client.

  2. 2

    MCP client (tool consumer)

    Built into your agent. It connects to an MCP server, discovers the available tools and handles the protocol. When your agent needs a tool, the client calls the right server with the required parameters.

  3. 3

    MCP server (tool provider)

    A service that exposes a catalogue of tools with descriptions, parameters and capabilities. For example, AWS Labs publishes an MCP server for AWS documentation, GitHub publishes servers for repository operations, and you can build your own for internal systems.

  4. 4

    Data sources

    The real databases, APIs, files or enterprise systems behind the server. The server fetches what's needed and sends results back through the client to the host.

These parts work through one standardised communication layer, the core idea of MCP. Instead of every host and tool provider inventing its own integration, MCP defines one protocol for how clients and servers discover capabilities, exchange requests and return results. That shared protocol is what makes it scalable, reusable and easier to maintain.

The difference between a host and an MCP client

An MCP client never runs on its own. It's created and managed by a application, such as a code editor, an AI assistant or a browser extension. The host owns the user experience: the interface, session memory and prompt orchestration. It can run several clients at once, each connected to a different server.

Each , in contrast, is a protocol-level component with a one-to-one connection to a single . It discovers that server's tools, resources and prompts, gathers context for the model, and asks the user for missing information when needed. In short: the host is what users interact with; the client lets the host talk to one specific server.

Swipe sideways to see the full diagram
MCP host MCP client 1 MCP client 2 MCP client 3 UI, memory, workflows transport layertransport layertransport layer MCP server A MCP server B MCP server C web APIweb APIweb API Local file system Database Email 1 host : many clients1 client : 1 server1 server : 1 system
One host, many clients, one server each. Keeping every connection one-to-one keeps each simple and isolated, while the host as a whole reaches many services.
iRemember it this way

The host is the application layer (UI, workflows, user interaction). The client is the integration layer inside the host that speaks MCP. The host orchestrates; each client talks to exactly one server.

Transports, lifecycle and authorisation

A client and its server exchange messages over a . MCP messages use and must be UTF-8 encoded. The client and server define what is said; the transport defines how it travels. There are two standard transports plus a custom option, all for the same job in different deployments:

Swipe sideways to see the full diagram
stdiosame machine your laptop client server pipes: stdin / stdoutsimple, local, no ports Streamable HTTPremote or web client server HTTP, streaming via SSEresults arrive as they're ready Custombring your own client server gRPC… e.g. gRPC for low latencysame MCP messages inside
Same conversation, different wires. The JSON-RPC messages are identical; only the way they travel changes.
TransportBest forExample
(standard input/output)Local integrations where client and server run on the same machine. Simple, direct, lightweight, like command-line programs piping data to each other.An agent running on your laptop, as in this section's labs
Streamable HTTPWeb-based or remote servers. Uses HTTP and can stream results incrementally with . (It replaced the older stand-alone SSE transport.)A live assistant receiving updates as they happen
CustomWhen the built-in options don't fitAn enterprise team using for high performance

Managing the lifecycle

management covers how a client and server start, maintain and end a connection: agreeing on protocol versions, learning each other's capabilities, and managing session state. Three stages:

Swipe sideways to see the full diagram
MCP client MCP server Initiali-zation initialize {protocolVersion, capabilities, clientInfo} result {protocolVersion, capabilities, serverInfo} notifications/initialized Operation tools/list · resources/read · prompts/get · tools/call … results server → client requests too (roots/list, sampling…) Shutdown close the connection (end the transport)
Handshake, conversation, goodbye. Nothing else may happen until the handshake agrees a protocol version and capabilities.
  1. 1

    Initialisation

    Client and server agree on the protocol version and capabilities. Example: an agent connecting to a server for a company database performs the handshake, and the server confirms which features it offers, for example tools such as a run_query tool.

  2. 2

    Operation

    Normal protocol traffic. The agent asks for customer records; the server processes the requests and returns structured responses.

  3. 3

    Shutdown

    The client closes the connection gracefully, letting ongoing operations finish and releasing resources.

Watch the wire

A real MCP conversation, message by message

These are the real JSON-RPC messages that go over the pipes when you run this section's client.py against server.py (captured from a real run, lightly shortened).

not connected

Securing the connection

MCP defines an framework for HTTP-based transports, built on . Servers reached over HTTP should follow it. Servers using stdio should not; they take any credentials they need from the environment instead, such as environment variables set when the client starts them. Clients and servers may also agree their own custom authentication and authorisation.

5.3

MCP server features

An MCP server offers clients three kinds of capability: tools to do things, resources to read things, and prompt templates to ask things the same way every time.

Swipe sideways to see the full diagram
Toolsfunctions the agent can run Retrieve / search Send a message Update database records actions Resourcesdata the agent can read Files Database records API responses read-only context Prompt templatesreusable instructions Document Q&A Transcript summary Output as JSON consistent asks
Do, read, ask. Tools perform actions, resources provide context without side effects, and prompt templates package good instructions for reuse.

Tools

the agent can execute to act on outside systems: retrieving or searching data, sending messages, updating records.

Resources

Read-only, persistent data the server exposes: files, database records, API responses. They don't perform actions; they make information available to browse, search and reference, so a model can look up facts without triggering an operation.

Prompt templates

Predefined prompts the agent can use through the client to guide how it handles data or a task: document Q&A, transcript summarisation, structured JSON output.

Swipe sideways to see the full diagram
MCP client queries for resourcesinvokes toolsfills in prompts MCP MCP server exposes resourcesexposes toolsexposes prompt templates
The server exposes, the client consumes. All three features travel over the same MCP connection.

A client typically uses them together: discover what the server offers, retrieve resources to gather context, invoke tools to act or fetch more data, and use prompt templates to structure the model's work.

Tools in MCP

You already know tools from Section 2. In MCP, tools are actions a server exposes, like built-in functions or APIs the LLM can call mid-conversation. When a request needs action (fetching data, triggering an operation, a calculation), the model picks the right tool. MCP expects a human in the loop: users should be able to see and approve tool calls, so they stay in control. Two protocol methods cover tools:

MethodWhat it does
tools/listDiscover the server's tools. Returns tool definitions with their input schemas.
tools/callRun one tool with arguments. Returns the tool's result.

Resources

A server can expose collections and documents as , which clients fetch, filter or search on demand. Clients send resources/list to discover them (and resources/templates/list for templates, covered below) and resources/read to get their contents.

Every resource has a unique (for example file:///path/to/document.md) and can declare a , a standard label that tells the client what kind of content it is (Markdown, JSON, PDF…) and how to handle it. For example, a server could share a course directory like this:

a resource description
{
  "uri": "file:///data/student-directory.json",
  "name": "courseDirectory",
  "title": "Course Directory",
  "description": "List of courses and instructors in a program",
  "mimeType": "application/json"
}

Resource templates

expose parameterised resources through URI templates. The client fills in the variables to ask for specific data. For example, travel://activities/{city}/{category} covers many cities and categories, and travel://activities/barcelona/museums asks for one. In this section's lab, greeting://{name} works the same way.

Scenario 1: directly querying a resource

A student assistant helps students find course and instructor information. A student asks: "Who is the instructor for the Machine Learning course?" The model realises it needs course information, the client fetches the right resource from the server, and the assistant answers from it:

Swipe sideways to see the full diagram
"Who is the instructor for theMachine Learning course?" MCP clientinside the studentassistant 1 query resource 2 return result MCP server resource: course directory Course: Machine LearningInstructor: Dr. RaoSchedule: Mon, WedDepartment: Computing Answer:Dr. Rao
Read, don't act. The answer is in fairly static data, so the assistant just reads a resource; no tool needs to run.

Conceptually the request looks like findResource("courseDirectory", {"course": "Machine Learning"}). The resource holds structured information (course names, instructors, schedules, departments) that rarely changes, so reading it is enough.

Scenario 2: a resource and a tool together

Now the student asks: "Find the instructor for the Machine Learning course and send them a message asking about office hours." Reading isn't enough this time; something must happen.

Swipe sideways to see the full diagram
Studentassistant MCP server Resourcecourse directory→ Instructor: Dr. Rao ToolsendMessage(to, text)→ message sent 1 read 2 invoke Dr. Rao"What are your office hours?"
Look up, then act. The resource answers "who"; the tool performs the "send".
  1. 1

    Query the resource

    courseDirectory identifies the instructor.

  2. 2

    Invoke a tool

    Something like sendMessage sends the instructor the question. Tools perform actions such as sending messages or scheduling meetings.

iRule of thumb

Resources provide information (course directories, schedules). Use them for static or semi-static data so the AI gets context quickly, without running unnecessary operations. Use tools when something must happen.

Prompt templates

MCP are predefined instruction templates a server exposes. They standardise recurring interactions and save you rewriting the same instructions. Each can have variables that the client fills in at run time; the client fetches the prompt from the server and passes the completed text to the model.

Prompts can be versioned and updated centrally without changing client code. They're explicitly chosen by the user or client; they never run on their own. And they can be context-aware, referring to available resources and tools to build richer workflows. Here's a template for a student study assistant:

a prompt template description
{
  "name": "generateStudyPlan",
  "title": "Generate Study Plan",
  "description": "Create a personalized study plan for a student based on their course and exam date",
  "arguments": [
    { "name": "course", "description": "Course the student is preparing for", "required": true },
    { "name": "examDate", "description": "Date of the upcoming exam", "required": true }
  ]
}
Swipe sideways to see the full diagram
"Create a study plan for myMachine Learning exam on May 10." MCP client generateStudyPlan(course="Machine Learning",examDate="2026-05-10") MCP server Prompt template Generate study plan fora {course} exam on{examDate}, week by week,with revision and practice… to model Study plan ✓ Days 1–3: fundamentals✓ Days 4–5: practise neural networks✓ Days 6–7: deep learning✓ Days 8–9: mock exams and revision
Fill in the blanks, get consistent instructions. The server owns the wording of the prompt; the client only supplies the values.

The client fills in the arguments, generateStudyPlan({"course": "Machine Learning", "examDate": "2026-05-10"}), and the template guides the model to a structured schedule leading up to the exam. In the lab, the same idea appears as generate_study_plan(course, exam_date).

Try it

Tool, resource or prompt?

1. get_weather(city): fetch the current conditions when the model decides it needs them.

2. greeting://{name}: a short read-only text the app can load into context.

3. weather_report(city): a ready-made request for a detailed weather report that the user picks from a menu.

4. Send an email to the course coordinator.

5. The student directory as a JSON file, for the model to look things up in.

6. A study-plan instruction that takes a course name and an exam date.

0 of 6 answered

5.4

MCP client features

Clients aren't just consumers. They can offer servers three things of their own: borrowed model access, safe folder boundaries, and a way to ask the user a question.

Remember that a client lives inside a host, such as a code editor or browser extension. That host has the AI model, the user's permissions and the user interface, so the client can offer:

Sampling

Let MCP servers use your AI model, safely.

Roots

Define the filesystem boundaries a server may work in.

Elicitation

Let a server request input from the user at run time.

Sampling

lets a server temporarily use the AI model and permissions the client already has, instead of calling a model of its own. It reverses the usual direction: rather than the client asking the server for data, the server asks the client to run an AI task for it. That's ideal when the server has the domain data or business logic, but the client has the model, the user's permissions and the UI. The user stays in control of model usage, approvals and final outputs.

Swipe sideways to see the full diagram
Travel server Rome activities list120 items, no AI to rank "Pick 5 best for history,art & food" samplingrequest MCP client Approve AI task:rank activities? APPROVE running the model… user review User reviews 1. Colosseum & Forum2. Vatican Museums3. Trastevere food tour4. Borghese Gallery5. Campo de' Fiori confirm approved results sent back to the server
The server borrows the client's brain, with permission. The user approves the AI task and reviews the output before anything goes back.

A travel server retrieves many Rome activities but has no AI to rank them. It sends a sampling request asking the client's model to choose the top five for someone who loves history, art and food. The client asks the user for approval, runs the model, shows the results, and once the user approves, sends the final AI-generated list back to the server.

Roots

are the client's standard way to tell servers where in the filesystem they may operate: which directories and files they have access to. Servers can ask for the list of roots, and get notified when it changes.

Swipe sideways to see the full diagram
Root: 2025 Conference Workspacefile:///Users/planner/conference-2025 MCP clientthe planner's assistant roots sent MCP serverstays inside the roots ✓ changes allowed in the rootsaves approved attendee list ✗ No access Personal documents Other projects restricted filesand folders
A fence the client draws. The client tells the server which folder is the current workspace; inside it the server can work, outside it there's nothing for the server to touch.

Example: an event planner uses an MCP-powered assistant. The folder file:///Users/planner/conference-2025 is the workspace for the current event, so the client defines it as a root:

roots the client shares
[
  {
    "uri": "file:///Users/planner/conference-2025",
    "name": "2025 Conference Workspace"
  }
]
Try it

What may the server touch?

The planner's computer (root highlighted)

  • /Users/planner/
  • └ conference-2025/
  • └ attendees.csv
  • └ schedule.md
  • └ venues/
  • └ hotel-quotes.pdf
  • └ gala-2025/
  • └ budget.xlsx
  • └ personal/
  • └ tax-return.pdf

The server tries to…

Pick an action.

Inside the root, the server can save updated attendee lists to conference-2025. Outside it, personal documents and other projects, it has no access. That's deliberate. When the planner switches projects, the client updates the roots list and notifies the server, so access stays tightly scoped.

</>In the lab

The lab's client shares its own folder as a root ("Course workspace"), and the server's list_client_roots tool asks the client for it, so you can watch a server-to-client request happen.

Elicitation

lets a server ask for missing information during a session instead of guessing. The server notices that required context is missing or ambiguous and sends an elicitation request; the client asks the user, who can review, edit, reject or cancel; the answer goes back and the server continues. There are two modes:

Swipe sideways to see the full diagram
Form modecollect structured fields through the client bookingserver client Form passport no. date of birth submit the user can review, edit,reject or cancel URL modesend the user to a secure page instead payrollserver URL client secure HRportaltax, bank details the client never sees the data; only "done" comes back
Ask, don't assume. Form mode collects fields through the client; URL mode keeps sensitive data away from the client entirely. Either way the client stays in control.

MCP at a glance

MCP componentPurposeWhat it handles
Base protocolDefines the core communication structureJSON-RPC message types between clients and servers
TransportThe channel between client and serverstdio and Streamable HTTP
Lifecycle managementManages the connectionInitialisation, capability negotiation, session control
AuthorisationSecures communicationAuthentication and authorisation for HTTP-based transports
Server featuresCapabilities servers exposeResources, prompts and tools
Client featuresCapabilities clients provideSampling, roots and elicitation
5.5

Using and building MCP servers

Two ways to work with servers: use one that already exists, or build your own for your systems.

Using pre-built servers

Many ready-made servers exist, from Anthropic, the open-source community and AWS (through the official MCP servers for AWS from AWS Labs). You can connect, say, the GitHub MCP server or a Slack MCP server to an MCP host such as Claude Code, VS Code or Kiro, and every MCP-compatible agent can use their tools.

A concrete example is the AWS Documentation MCP server, which exposes tools for searching and reading official AWS documentation. Your code launches it as a local process with uvx, creates an MCP client that talks to it over stdio, lists its tools, and hands them to a Strands agent. The agent then answers AWS questions from the docs rather than only from its training. You'll run exactly this in 5.7. You write no AWS-documentation integration code yourself: the server packages that capability as reusable tools.

Building your own server

To expose your organisation's tools and data, you build a custom server: use an SDK to define tools (actions) and resources (data), then run it with a chosen transport. Official MCP SDKs exist for many languages, including Python, TypeScript, Java, C# and Go; this course uses Python.

FastMCP vs FastAPI

What it isBuilt specifically for MCP, with MCP-native ways to define tools, resources and promptsA general-purpose web framework
Choose it whenYou want an MCP server quickly, with little boilerplate, and the service mainly exposes MCP capabilitiesMCP is one part of a bigger service that also has REST APIs, custom auth flows, health checks or dashboards
Trade-offFocused on MCPMore flexibility, but you write more of the MCP plumbing yourself
✦Think of it like this

FastMCP is a ready-made food-truck kit: perfect when all you want to do is serve one menu, fast. FastAPI is a full restaurant kitchen: more to set up, but it can also run the bar, the bakery and the delivery counter.

Using MCP from agent frameworks

Frameworks such as Strands and LangChain act as MCP clients (or adapters): they let agents discover and call tools exposed by MCP servers. They don't define the server; they help agents consume MCP capabilities inside a bigger reasoning and orchestration flow.

Strands

Strands Agents ships with MCP support built on the official MCP Python SDK (which includes FastMCP). Use Strands for the agent, tool discovery and AWS connections, and FastMCP for the fast, focused tools the agent calls.

LangChain

The langchain-mcp-adapters library converts MCP tools into standard LangChain tools, so LangChain and LangGraph agents can use any MCP server.

5.6

Lab: an MCP server and client

Build a weather server with FastMCP that exposes every MCP primitive, and a client that discovers and uses each one. No LLM, no AWS: just the protocol.

Swipe sideways to see the full diagram
server.pyFastMCP("WeatherServer") Toolsget_weather(city)list_client_roots() Resourcesgreeting://{name}courses://directory (JSON) Promptsweather_report(city)generate_study_plan(...) runs on stdio JSON-RPCstdin / stdout pipes client.pyClientSession 1. Start the server as a subprocess 2. Handshake: initialize() 3. Discover tools, resources, prompts 4. Call tools, read resources 5. Get prompts 6. Answer the server's roots request Results: "Weather in London: 15°C, cloudy" · "Hello, Developer!…" · 3 courses · 2 filled prompts · "Course workspace: file:///…"
One server, one client, every primitive. The client starts the server itself and they talk through pipes: no ports, no HTTP.
PrimitiveNameWhat it is
Toolget_weather(city)A function the LLM can call
Toollist_client_roots()Asks the client which folders it shared (roots)
Resource (template)greeting://{name}Data the app can read, like a GET endpoint
Resource (JSON)courses://directoryA fixed resource with application/json content
Promptweather_report(city)A reusable prompt template
Promptgenerate_study_plan(course, exam_date)A template with two arguments

And the client exposes roots: the folders the server is allowed to work in.

server.py, step by step

server.py · imports and the server object
"""
MCP Server — exposes all the MCP primitives over stdio transport:

  * Tools      — get_weather, list_client_roots
  * Resources  — greeting://{name}  and  courses://directory (JSON)
  * Prompts    — weather_report, generate_study_plan

MCP (Model Context Protocol) lets LLM applications discover and call tools,
read resources, and use prompt templates through one standard protocol.

Run:  python client.py   (the client starts this server for you)
"""

import json

# FastMCP: high-level helper for building MCP servers. It handles JSON-RPC
# parsing, registration of tools/resources/prompts, and the transport.
from mcp.server.fastmcp import Context, FastMCP

# The name identifies this server to clients when they connect.
mcp = FastMCP("WeatherServer", log_level="WARNING")

FastMCP is the high-level API from the official MCP Python SDK. It handles the protocol details for you: JSON-RPC messages, capability negotiation, and generating schemas from your type hints. The name "WeatherServer" is how the server introduces itself during the handshake; log_level="WARNING" keeps its own logs quiet.

server.py · tool: get_weather
# --- Tool: get_weather ---------------------------------------------------
# Tools are functions an LLM can call. @mcp.tool() registers the function so
# it appears in list_tools() and can be invoked with call_tool(). The
# docstring becomes the description the LLM sees.

@mcp.tool()
def get_weather(city: str) -> str:
    """Get the current weather for a given city."""
    # Fake data for demo purposes — a real server would call a weather API
    fake_weather = {
        "london": "15°C, cloudy",
        "paris": "18°C, sunny",
        "tokyo": "22°C, humid",
        "new york": "12°C, windy",
        "bangalore": "26°C, pleasant with light showers",
    }
    result = fake_weather.get(city.lower())
    if result:
        return f"Weather in {city}: {result}"
    return f"Sorry, no weather data available for '{city}'."

@mcp.tool() registers the function so it appears in tools/list and can be run with tools/call. Just like Strands' @tool, the type hints become the input schema and the docstring becomes the description. The data is fake on purpose (including Bangalore, which the Strands lab asks about); a real server would call a weather API.

server.py · tool that uses roots
# --- Tool that uses a client capability: roots ---------------------------
# Roots are folders/URIs the *client* says the server may work within.
# A server can ask the client for its roots at any time.

@mcp.tool()
async def list_client_roots(ctx: Context) -> str:
    """List the workspace roots the connected client has shared with this server."""
    result = await ctx.session.list_roots()
    if not result.roots:
        return "The client did not share any roots."
    return "\n".join(f"{root.name}: {root.uri}" for root in result.roots)

This tool shows the protocol running backwards. FastMCP injects a Context object (it is hidden from the input schema, which is why list_client_roots shows no arguments in tools/list); ctx.session.list_roots() sends a roots/list request from the server to the client and waits for the answer. Because it waits for a round trip to the client over the pipes, the function is async.

server.py · resource template: greeting
# --- Resource: greeting ---------------------------------------------------
# Resources are data an LLM application can read (like GET endpoints).
# "greeting://{name}" is a URI template: read_resource("greeting://Developer")
# fills in {name}. Use resources for data, tools for actions.

@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Return a personalized greeting."""
    return f"Hello, {name}! Welcome to the WeatherServer."

Resources are read-only data endpoints, like GET requests in a REST API. Because the URI contains {name}, this is a template: reading greeting://Developer calls the function with name="Developer".

server.py · resource: course directory (JSON)
# --- Resource: course directory (structured JSON) -------------------------
# A fixed URI with a MIME type tells the client how to interpret the content.

COURSES = [
    {"course": "Machine Learning", "instructor": "Dr. Rao", "weeks": 12},
    {"course": "Agentic AI on AWS", "instructor": "Shivank Agrawal", "weeks": 8},
    {"course": "Data Engineering", "instructor": "Ms. Iyer", "weeks": 10},
]


@mcp.resource(
    "courses://directory",
    name="courseDirectory",
    title="Course Directory",
    description="List of courses and instructors in a program",
    mime_type="application/json",
)
def course_directory() -> str:
    return json.dumps(COURSES, indent=2)

A fixed URI with a name, title, description and mime_type="application/json", the same shape as the resource description in 5.3, with a custom courses:// URI instead of a file path. The function returns the course list as JSON text.

server.py · prompts
# --- Prompts ---------------------------------------------------------------
# Prompts are reusable templates. Clients call get_prompt(name, arguments)
# and get back ready-to-send messages.

@mcp.prompt()
def weather_report(city: str) -> str:
    """Generate a prompt asking for a weather report."""
    return f"Please provide a detailed weather report for {city}, including temperature, humidity, and forecast."


@mcp.prompt(title="Generate Study Plan")
def generate_study_plan(course: str, exam_date: str) -> str:
    """Create a personalized study plan for a student based on their course and exam date."""
    return (
        f"Create a week-by-week study plan for the course '{course}'. "
        f"The exam is on {exam_date}. Include revision weeks, practice tests, "
        "and one concrete goal per week."
    )

@mcp.prompt() registers reusable templates. A client calls get_prompt("weather_report", {"city": "Tokyo"}) and gets back ready-to-send messages. generate_study_plan is the lab's version of the study-plan template from 5.3, with a title.

server.py · start
# --- Start the server ------------------------------------------------------
# "stdio" = read JSON-RPC messages from stdin, write responses to stdout.
# The client spawns this script as a subprocess and talks to it through pipes.

if __name__ == "__main__":
    mcp.run(transport="stdio")

stdio means: read JSON-RPC messages from standard input and write responses to standard output. The server can connect to an MCP host (Claude Code, Kiro) or to your own client, which is next.

client.py, step by step

client.py · imports
"""
MCP Client — starts server.py over stdio and exercises every MCP primitive:
tools, resources, prompts, and roots.

Run:  python client.py
"""

import asyncio
import json
import sys
from pathlib import Path

import mcp.types as types

# ClientSession: the MCP protocol session (initialize, list, call, ...)
# StdioServerParameters: how to start the server subprocess
from mcp import ClientSession, StdioServerParameters

# stdio_client: starts the server process and returns read/write streams
from mcp.client.stdio import stdio_client

HERE = Path(__file__).resolve().parent
ImportIts job
ClientSessionThe high-level MCP client that speaks the protocol for you
StdioServerParametersSettings that tell the client how to start the server
stdio_clientThe transport: starts the subprocess and manages its stdin/stdout pipes
mcp.typesProtocol data types, used here to describe roots
client.py · roots callback
# --- Roots -----------------------------------------------------------------
# The client decides which folders the server may work within. When the
# server asks (list_roots), this callback answers.

async def list_roots(context) -> types.ListRootsResult:
    return types.ListRootsResult(
        roots=[
            types.Root(uri=HERE.as_uri(), name="Course workspace"),
        ]
    )

The client decides which folders the server may use. When the server asks (through list_client_roots), this callback answers with one root: the lab folder, named "Course workspace". HERE.as_uri() turns the folder path into a file:// URI.

client.py · connect, handshake, discover
async def main():
    # Start "python server.py" as a subprocess. sys.executable is the same
    # Python that runs this client, so the server sees the same packages.
    server_params = StdioServerParameters(
        command=sys.executable,
        args=[str(HERE / "server.py")],
    )

    async with stdio_client(server_params) as (read_stream, write_stream):
        async with ClientSession(read_stream, write_stream, list_roots_callback=list_roots) as session:
            # Required first step: exchange protocol version and capabilities
            await session.initialize()

            # --- 1. Discover tools ----------------------------------------
            tools = await session.list_tools()
            print("=== Available Tools ===")
            for tool in tools.tools:
                print(f"  - {tool.name}: {tool.description}")

            # --- 2. Discover resources ------------------------------------
            # Fixed resources and URI templates are listed separately.
            print("\n=== Available Resources ===")
            resources = await session.list_resources()
            for r in resources.resources:
                print(f"  - {r.uri} ({r.mimeType}): {r.description}")
            templates = await session.list_resource_templates()
            for rt in templates.resourceTemplates:
                print(f"  - {rt.uriTemplate}: {rt.description}")

            # --- 3. Discover prompts --------------------------------------
            print("\n=== Available Prompts ===")
            prompts = await session.list_prompts()
            for p in prompts.prompts:
                args = ", ".join(a.name for a in (p.arguments or []))
                print(f"  - {p.name}({args}): {p.description}")
  1. a

    StdioServerParameters(...)

    Doesn't start anything yet. It says: when you connect, run server.py with the same Python (sys.executable), so the server sees the same packages.

  2. b

    async with stdio_client(...)

    Starts the server subprocess and hands back raw read/write streams. The async with blocks guarantee clean-up: streams closed, process stopped, even if something fails. That's the shutdown stage of the lifecycle.

  3. c

    ClientSession(..., list_roots_callback=list_roots) and initialize()

    Wraps the streams in the MCP protocol and performs the handshake: versions and capabilities are exchanged. Passing the roots callback is what tells the server "this client supports roots".

  4. d

    Discovery

    list_tools(), list_resources(), list_resource_templates() and list_prompts() ask the server "what do you offer?". Fixed resources and URI templates are listed separately.

client.py · call tools, read resources
            # --- 4. Call a tool (known city) ------------------------------
            print("\n=== Calling get_weather('London') ===")
            result = await session.call_tool("get_weather", {"city": "London"})
            print(f"  Result: {result.content[0].text}")

            # --- 5. Call a tool (unknown city) ----------------------------
            print("\n=== Calling get_weather('Berlin') ===")
            result = await session.call_tool("get_weather", {"city": "Berlin"})
            print(f"  Result: {result.content[0].text}")

            # --- 6. Read a templated resource -----------------------------
            print("\n=== Reading greeting://Developer ===")
            resource = await session.read_resource("greeting://Developer")
            print(f"  Result: {resource.contents[0].text}")

            # --- 7. Read a JSON resource ----------------------------------
            print("\n=== Reading courses://directory ===")
            resource = await session.read_resource("courses://directory")
            courses = json.loads(resource.contents[0].text)
            for c in courses:
                print(f"  - {c['course']} — {c['instructor']} ({c['weeks']} weeks)")

call_tool sends tools/call with a name and arguments; result.content is a list of content blocks, so [0].text is the first block's text. Berlin isn't in the fake data, so you'll see the polite "no data" message: tools return errors as normal results. read_resource sends resources/read; the server matches greeting://Developer against the template and fills in the name. The course directory comes back as JSON text, which json.loads turns into Python data.

client.py · prompts, roots, entry point
            # --- 8. Get prompts -------------------------------------------
            print("\n=== Getting weather_report prompt ===")
            prompt = await session.get_prompt("weather_report", {"city": "Tokyo"})
            print(f"  Result: {prompt.messages[0].content.text}")

            print("\n=== Getting generate_study_plan prompt ===")
            prompt = await session.get_prompt(
                "generate_study_plan", {"course": "Machine Learning", "exam_date": "2026-05-10"}
            )
            print(f"  Result: {prompt.messages[0].content.text}")

            # --- 9. Roots: the server asks the client ---------------------
            print("\n=== Calling list_client_roots ===")
            result = await session.call_tool("list_client_roots", {})
            print(f"  Result: {result.content[0].text}")


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

get_prompt fetches a template with arguments; the server fills it in and returns a list of messages. In a real app you'd send those messages to an LLM. The final call triggers the roots round trip. asyncio.run(main()) starts an event loop (the scheduler that runs async code and switches between tasks while they wait), runs main(), and closes the loop when done.

Run it

terminal
pip install -r requirements.txt
python client.py        # starts server.py automatically
outputreal output; your folder path will differ
=== Available Tools ===
  - get_weather: Get the current weather for a given city.
  - list_client_roots: List the workspace roots the connected client has shared with this server.

=== Available Resources ===
  - courses://directory (application/json): List of courses and instructors in a program
  - greeting://{name}: Return a personalized greeting.

=== Available Prompts ===
  - weather_report(city): Generate a prompt asking for a weather report.
  - generate_study_plan(course, exam_date): Create a personalized study plan for a student based on their course and exam date.

=== Calling get_weather('London') ===
  Result: Weather in London: 15°C, cloudy

=== Calling get_weather('Berlin') ===
  Result: Sorry, no weather data available for 'Berlin'.

=== Reading greeting://Developer ===
  Result: Hello, Developer! Welcome to the WeatherServer.

=== Reading courses://directory ===
  - Machine Learning — Dr. Rao (12 weeks)
  - Agentic AI on AWS — Shivank Agrawal (8 weeks)
  - Data Engineering — Ms. Iyer (10 weeks)

=== Getting weather_report prompt ===
  Result: Please provide a detailed weather report for Tokyo, including temperature, humidity, and forecast.

=== Getting generate_study_plan prompt ===
  Result: Create a week-by-week study plan for the course 'Machine Learning'. The exam is on 2026-05-10. Include revision weeks, practice tests, and one concrete goal per week.

=== Calling list_client_roots ===
  Result: Course workspace: file:///…/mcp-basics

Notice that the client never imported the server's functions. Everything it knows (names, descriptions, argument lists) came over the protocol at run time. That's dynamic discovery.

</>Want a visual tool?

If you have Node.js installed, run npx @modelcontextprotocol/inspector python server.py in the mcp-basics folder. The MCP Inspector opens in your browser and lets you click through the server's tools, resources and prompts.

5.7

Lab: Strands agents with MCP tools

Now add the LLM. A Strands agent gets its tools from MCP servers, first from your own server, then from AWS's documentation server, without knowing anything about those tools in advance.

Swipe sideways to see the full diagram
Strands agenttools = list_tools_sync() Claude Sonnet 4.6 (Bedrock) MCPClientopens the transport stdio stdio Local: mcp-basics/server.pyget_weather, list_client_rootsstarted with sys.executable AWS Documentation MCP serversearch and read official AWS docsuvx awslabs.aws-documentation-mcp-server
Same pattern, any server. Swap the command that starts the server and the agent gets a completely different toolbox, with no other code changes.

Both scripts follow the same pattern:

the pattern
mcp_client = MCPClient(lambda: stdio_client(StdioServerParameters(command=..., args=[...])))

with mcp_client:
    tools = mcp_client.list_tools_sync()
    agent = Agent(tools=tools)
    agent("...")

Script 1: an agent using your own MCP server

agent_with_local_mcp.py
"""
A Strands agent that gets its tools from an MCP server.

It starts the server from ../mcp-basics/server.py over stdio, discovers its
tools, and lets the model decide when to call them. The agent code knows
nothing about the weather tool in advance — it all comes over MCP.

Run:  python agent_with_local_mcp.py
"""

import os
import sys
from pathlib import Path

from mcp import StdioServerParameters, stdio_client
from strands import Agent
from strands.models.bedrock import BedrockModel
from strands.tools.mcp import MCPClient

MODEL_ID = os.getenv("BEDROCK_MODEL_ID", "global.anthropic.claude-sonnet-4-6")
REGION = os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "us-east-1"))

SERVER = Path(__file__).resolve().parent.parent / "mcp-basics" / "server.py"

# MCPClient takes a function that opens the transport. Here: spawn the server.
mcp_client = MCPClient(
    lambda: stdio_client(StdioServerParameters(command=sys.executable, args=[str(SERVER)]))
)

with mcp_client:
    tools = mcp_client.list_tools_sync()
    print("Tools discovered over MCP:", [t.tool_name for t in tools])

    agent = Agent(model=BedrockModel(model_id=MODEL_ID, region_name=REGION), tools=tools)
    agent("I'm flying from Bangalore to London tomorrow. What's the weather like in both cities?")
    print()
  1. a

    SERVER = …/"mcp-basics"/"server.py"

    Reuses the server you just built. Path(__file__).resolve().parent.parent climbs from this script's folder to the section folder.

  2. b

    MCPClient(lambda: stdio_client(...))

    Strands' MCP client. It takes a function that opens the transport (that's what the lambda is), so Strands can open the connection when it's ready, on its own background thread.

  3. c

    with mcp_client:

    Starts the server and keeps the MCP session open for the whole block. The agent must be used inside it, because its tools are live connections to the server.

  4. d

    list_tools_sync()

    Discovers the server's tools and wraps each as a Strands tool. t.tool_name gives each one's name. Pass the list straight to Agent(tools=...).

outputillustrative wording
Tools discovered over MCP: ['get_weather', 'list_client_roots']
I'll check the weather in both cities for you.
Tool #1: get_weather
Tool #2: get_weather
Here's the weather for your trip:

Bangalore: 26°C, pleasant with light showers. Keep an umbrella handy on the way to the airport.
London: 15°C and cloudy. Pack a light jacket for when you land.

The agent's code never mentioned weather. It learned about get_weather over MCP, and Claude decided to call it twice, once per city.

Script 2: an agent using the AWS Documentation MCP server

agent_with_aws_docs_mcp.py
"""
A Strands agent connected to the AWS Documentation MCP server.

The AWS Labs documentation server (https://github.com/awslabs/mcp) lets the
agent search and read official AWS docs, so its answers come from current
documentation instead of only the model's training data.

Prerequisite: `uv` installed (it provides the `uvx` command that downloads and
runs the server on demand).

Run:  python agent_with_aws_docs_mcp.py
"""

import os
import shutil

from mcp import StdioServerParameters, stdio_client
from strands import Agent
from strands.models.bedrock import BedrockModel
from strands.tools.mcp import MCPClient

MODEL_ID = os.getenv("BEDROCK_MODEL_ID", "global.anthropic.claude-sonnet-4-6")
REGION = os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "us-east-1"))

if shutil.which("uvx") is None:
    raise SystemExit("`uvx` not found. Install uv first: pip install uv")

# Create MCP client
mcp_client = MCPClient(
    lambda: stdio_client(
        StdioServerParameters(
            command="uvx",
            args=["awslabs.aws-documentation-mcp-server@latest"],
            env={**os.environ, "FASTMCP_LOG_LEVEL": "ERROR"},
        )
    ),
    startup_timeout=120,  # first run downloads the server package
)

# Use with agent
with mcp_client:
    tools = mcp_client.list_tools_sync()
    print("Tools from the AWS Documentation MCP server:", [t.tool_name for t in tools])

    agent = Agent(
        model=BedrockModel(model_id=MODEL_ID, region_name=REGION),
        tools=tools,
        system_prompt="Answer AWS questions using the documentation tools. Cite the doc URLs you used.",
    )
    agent("What is AWS Lambda, and what is the maximum timeout for a Lambda function?")
    print()
LineWhy it's there
shutil.which("uvx")The server is started with , which comes with uv (in this lab's requirements.txt). The check fails early with a clear message if it's missing.
args=["awslabs.aws-documentation-mcp-server@latest"]Tells uvx to download (first time) and run the latest AWS Labs documentation server.
env={**os.environ, "FASTMCP_LOG_LEVEL": "ERROR"}Passes your environment through to the server, and quiets its logs.
startup_timeout=120The first run downloads the server package, which can take 30 seconds or more and exceed Strands' default 30-second startup timeout.
system_prompt=…"Cite the doc URLs you used."Asks the agent to answer from the documentation tools and show its sources.
outputillustrative; tool names depend on the server version
Tools from the AWS Documentation MCP server: ['read_documentation', 'search_documentation', 'recommend']
Tool #1: search_documentation
Tool #2: read_documentation
AWS Lambda is a serverless compute service that runs your code in response to events
and automatically manages the underlying compute resources.

The maximum timeout for a Lambda function is 900 seconds (15 minutes).

Sources:
- https://docs.aws.amazon.com/lambda/latest/dg/welcome.html
- https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html
iWhy this matters

Model knowledge has a cut-off date; documentation doesn't. By plugging in a documentation server, the agent's answers come from current docs, and you wrote zero integration code for it.

5.8

A2A: agents talking to agents

MCP connects an agent to tools. But what about connecting an agent to another agent, perhaps built by a different team, in a different framework, running somewhere else?

The is a standard bridge that lets agents built on different stacks (LangGraph, CrewAI, Google ADK, Strands and others) communicate, discover each other and manage shared workflows, even across organisations. Google introduced it in 2025; it has since merged with IBM's Agent Communication Protocol (ACP) and is now a vendor-neutral project governed by the Linux Foundation.

A2A is about peer-to-peer collaboration between agents: they can delegate complex tasks and solve open-ended problems while keeping their internals private. MCP, in contrast, connects agents to specific tools and functions.

MCP vs A2A: why not just wrap an agent as a tool?

MCP reduces the complexity of connecting agents to tools and data. Tools are usually and do one predefined thing (a calculator, a database query). So could you simply expose an agent as an MCP tool? Sometimes, and Section 4 did something similar inside one program. But as a general approach for independent agents it's limiting: tools execute actions; agents solve problems. Squeezing an agent into a tool call hides what it can really do: handle ambiguity, ask follow-up questions, run multi-step and long-running work, report progress.

A2A complements MCP. It lets agents collaborate in their natural mode, talking as agents (or as users) rather than as function calls, which enables multi-turn interactions where agents reason, plan and delegate.

Swipe sideways to see the full diagram
Diagnosis & schedulingagent ▸ Find doctor tool▸ Book appointment tool Insurance & pharmacyagent ▸ Check coverage tool▸ Order medication tool A2A organisational ortechnologyboundary MCPMCP EMR / EHR systems Insurance & pharmacy APIs secure A2A communication
A2A between agents, MCP inside each. The two agents collaborate as peers; each uses its own MCP-connected tools to do its part.
  1. 1

    The patient's request goes to the diagnosis agent

  2. 2

    It talks to the insurance agent over A2A

  3. 3

    Each agent acts with its own MCP-connected tools

    The diagnosis agent reaches the hospital's systems; the insurance agent reaches insurance and pharmacy APIs.

  4. 4

    Information flows back and forth over A2A until the task is complete

Why agents need a standard protocol

As agents become more capable they also become more specialised, and more likely to work inside a larger multi-agent system. But the ecosystem is fragmented across frameworks, vendors and styles. A2A gives them a common protocol to communicate and coordinate across organisational and technical boundaries, without sharing their internals.

Swipe sideways to see the full diagram
A2Aagent-to-agent 1 Framework fragmentationLangGraph, CrewAI, ADK, Strands…one shared protocol for all 2 Complex collaborationflights + hotels + activitiesneed specialists working together 3 A common languagewhat HTTP did for the web,A2A does for agents 4 Cross-organisation workteams and companies collaborate;agents stay opaque 5 Preserve agent capabilitiesmore than a single tool call:multi-step, open-ended goals 6 Vendor neutralityhosted by the Linux Foundation,community governed
Six reasons for a shared agent protocol. Different frameworks, harder tasks, and organisational boundaries all point to the same need: one common way for agents to talk.
ReasonIn detail
Solve framework fragmentationAgent frameworks (LangGraph, CrewAI, Microsoft's framework and others) aren't compatible out of the box. A2A lets agents talk whatever model or language they use.
Enable complex collaborationTasks like planning a whole trip (flights, hotels, activities) need several specialised agents; A2A gives the structure and methods to coordinate them.
A common language (the HTTP analogy)The web needed HTTP to move data between different systems; the agent world needs A2A as a universal bridge beyond siloed apps.
Cross-organisation workflowsAgents collaborate across teams and companies while staying opaque: no internal implementation or private code is exposed.
Preserve agent capabilitiesAgents handle ambiguity, multi-step workflows, coordination and open-ended goals. Reducing them to one tool call loses that; A2A supports the richer interaction.
Vendor neutralityHosted by the Linux Foundation and governed by a technical steering committee, so no single company controls how agents interact.

A2A by example: a product launch

A marketing manager asks an AI system: "Create and execute a launch plan for our new eco-friendly smartphone in the European market." That needs many specialities, so (just like the multi-agent patterns in Section 4) a lead orchestrator agent breaks it down and works with:

Swipe sideways to see the full diagram
"Create and execute a launch planfor our eco-friendly smartphone…" Lead orchestratorsplits, delegates, combines Market researchtrends, competitors Supply chaininventory, shipping Creative contentads, slogans LegalGDPR, EU rules without a standard: functional silos Proprietary data ERP system AI creative tool Securelegal model
One request, four experts, four silos. Each agent is built differently, uses different data formats and runs on different systems, so without a shared protocol they can't even discover each other.

Each specialist sits on its own system: market research on proprietary data, supply chain on the company's , creative content on an AI creative tool, legal on a secure legal model. Without A2A, each agent has its own bespoke development and deployment, so they can't collaborate, let alone discover what each other can do. A2A acts as the common communication layer: the orchestrator can discover agents by capability, agents exchange information in a shared format, and one agent can even verify or approve another's output. The orchestrator then combines everything into one launch playbook, turning weeks of human coordination into a fast, automated workflow.

iSwarm vs A2A

The swarm pattern from Section 4 organises agents inside one multi-agent system. A2A solves a different problem: standard communication between independently built agents, across frameworks, platforms and organisations. You could even run a swarm whose members talk A2A.

5.9

How A2A works

The protocol standardises only the bridge between agents. Agent A doesn't need Agent B's code, prompts or database; it only needs to format an A2A request and read an A2A response.

Swipe sideways to see the full diagram
Agent Aclient agent A2A client send requeststo its URI, over a supportedprotocol binding Agent Bremote / server agent Agent card • agent URI• protocol bindings• skills, auth… /.well-known/agent-card.json
Client agent, remote agent, business card. Agent A reads Agent B's card to learn where and how to talk to it, then sends a request.

Agent A: the client agent

Starts the conversation and asks for work. Usually the front-facing agent that talks to the end user. It discovers other agents by reading their to see what they can do.

Agent B: the remote (server) agent

Receives the request and does the work. It hosts its capabilities at a URL and publishes its agent card at /.well-known/agent-card.json. It decides how to fulfil a request: a simple one gets a message straight back; a complex one becomes a task it manages until it produces an artifact (the result).

Here's a simplified agent card for a weather agent (you'll see a complete one from the lab in 5.11):

an agent card
{
  "name": "Weather Agent",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "skills": [
    {
      "id": "get_weather",
      "name": "Get Weather",
      "description": "Returns current weather conditions for a given city."
    }
  ],
  "url": "https://api.example.com/weather-agent/"
}
?Capabilities

How this agent communicates: does it support streaming, push notifications…

?Skills

What work it can do: each skill has an id, name and description (and often tags and examples).

iOpaque by design

A remote agent never has to show how it works inside: which code, which model. It only has to produce the output the protocol defines.

Discovery and protocol bindings

Agents find each other through agent cards: JSON files at a well-known URL, a bit like an , acting as a digital business card. A card details the agent's name, capabilities, supported protocols, media types and authentication requirements, including its URI (endpoint) and its supported .

A binding tells the client agent exactly which technical format to use with that remote agent. A2A provides the language; the binding is the physical connection that carries it.

✦Think of it like this

You've written a letter in English (the common language, A2A). How do you send it? HTTP binding is ordinary post: the most common and easiest. gRPC binding is a high-speed courier: faster and more efficient for heavy data. JSON-RPC binding is a telegram: it follows a strict set of remote-control rules.

BindingIn practice
JSON-RPCJSON-RPC 2.0 over HTTP, with server-sent events for streaming. The binding this section's lab uses.
gRPCBinary, high-performance remote calls (); good for heavy traffic
HTTP + JSON (REST)Plain HTTP endpoints with JSON bodies
!Both sides must share a binding

If one agent only speaks gRPC and the other only HTTP+JSON (REST), they can't talk directly. They must agree on a binding, which is exactly what their agent cards declare.

The A2A data model: messages, tasks, artifacts

Swipe sideways to see the full diagram
Clientsends a request Messageone turn of interaction Parts• text• JSON data• file• other media Tasktracks work over time GetTaskstatus + results Artifactholds the outputs of the task Parts• text output• JSON output• file output• multimodal output
In: a message made of parts. Out: artifacts made of parts. In between, a task tracks the work and its status.
  1. 1

    Message

    The client sends a (one turn of interaction) made of one or more parts: text, JSON data, files.

  2. 2

    Task

    When the remote agent starts working, it may create a to track progress over time, with an ID and a status.

  3. 3

    Artifacts

    As the task progresses or completes it produces , output containers that also hold parts.

  4. 4

    GetTask

    The client can call GetTask at any time to retrieve the latest status and whatever artifacts exist so far.

Communication styles

Tasks differ: some finish in a second, some take hours. A2A supports four ways of exchanging them:

Swipe sideways to see the full diagram
Synchronouswait for animmediate response Asynchronouscarry on withoutblocking; poll later Streamingcontinuous updatesas they happen Push notificationsget called back whensomething important happens
Four execution modes. From "wait right here" to "call me when it's done".

Synchronous

For simple, immediate answers. Agent A sends a message with SendMessage; Agent B processes it and replies right away. The client waits for the result, so it suits quick, short-lived tasks.

Asynchronous (polling)

For complex or long-running jobs where the client shouldn't block. Agent B replies with a task object instead of the final result: a task ID plus a status. Agent A carries on and periodically calls GetTask with that ID. When the task reaches a completed state, the results come back as artifacts.

In the Python A2A SDK, a plain message/send waits for the answer by default. To poll instead, the client sets ClientConfig(polling=True); the request then asks the server not to block, and the client calls GetTask (tasks/get) in a loop until the task is finished.

Swipe sideways to see the full diagram
Agent Aclient agent Agent Bremote agent Agent cardstreaming: falsepushNotifications: false→ so the client polls SendMessage {role, parts} Task {id: 12345, status: submitted} GetTask(12345) status: working GetTask(12345) … later status: completed + artifacts Task states • submitted• working• input-required• completed• failed• canceled, rejected…
Take a token, check back later. Agent A isn't stuck waiting; it polls with the task ID until the status reaches a final state.

Streaming

Uses server-sent events to deliver real-time updates or text in chunks as it's generated, so you see live progress. It's only available if the server sets capabilities.streaming = true in its agent card. Agent A calls SendStreamingMessage; the server keeps an HTTP connection open (text/event-stream) and sends structured events (status changes, intermediate messages, partial results) until the task completes.

Swipe sideways to see the full diagram
Agent A Agent B Agent cardstreaming: true SendStreamingMessage open connection · text/event-stream event: status = working event: "Found 2 flights…" event: artifact update event: status = completed
Updates as they happen. One open connection carries a stream of events, so the client sees progress without asking.

Push notifications

For tasks that take a long time, or when the client can't stay connected. Instead of polling or streaming, the client gives a URL and the server calls it when something important happens, such as completion or "input required". A pharmacy agent might notify a healthcare system hours later when a prescription is ready; a flights agent might notify when a booking is confirmed. Ideal for long-running, event-driven work.

Swipe sideways to see the full diagram
Agent A Agent B Agent cardpushNotifications:true request + PushNotificationConfig {callback URL} works for hours… HTTP POST → callback URL: "task completed" GetTask → full details and artifacts
"Don't call us, we'll call you." No open connection, no polling: the server rings the client's doorbell when it matters.

Push notifications only work if the server declares capabilities.pushNotifications = true in its agent card. Agent A supplies a PushNotificationConfig (webhook URL plus optional authentication) with the first request or through a separate call. Agent B then POSTs to that webhook on significant updates, and Agent A usually follows up with GetTask to fetch the full task and results.

?Method names on the wire

The A2A spec names its operations SendMessage, SendStreamingMessage and GetTask. With the JSON-RPC binding and the a2a-sdk 0.3 used in this section, they travel as the methods message/send, message/stream and tasks/get.

Try it

Four ways to ask the flights agent to book a ticket

  • Agent ASendMessage “Book flight AE303 for Asha Rao”. The connection stays open while it waits.
  • Agent AWaiting… can't do anything else with this request until the reply arrives.
  • Agent BChecks the flight, reserves a seat, issues the booking.
  • Agent BReply on the same connection: task completed, artifact “Booking confirmed: AE303, Asha Rao”.
Synchronous: one request, one response. The simplest style, but Agent A is blocked until B finishes. Best for quick jobs.
5.10

Implementing A2A

The easiest way to understand the options is as three layers, each built on the one below.

Swipe sideways to see the full diagram
1 Core protocol layer: the A2A SDK (a2a-sdk)Message · TextPart · AgentSkill · AgentCapabilities · A2ACardResolver · ClientFactory · ClientConfig 2 Framework integrationsStrands A2A (strands-agents[a2a]): A2AServer, A2AAgent · Google ADK 3 Platform-specific implementationsLangSmith Agent Server: managed endpoints
Primitives, integrations, platforms. Higher layers implement the protocol for you; lower layers give you more control and more plumbing.
  1. 1

    Core protocol layer: the A2A SDK

    The a2a-sdk package is the low-level protocol library, the most direct way to build an A2A client or server. It provides data types such as Message, TextPart, AgentSkill and AgentCapabilities, and client utilities such as A2ACardResolver, ClientFactory and ClientConfig. Using only the SDK gives full control, but you wire everything yourself: how your server publishes capabilities, routes requests, creates and updates tasks, and returns responses.

  2. 2

    Framework integrations

    Libraries on top of the SDK that implement the protocol for you while keeping your framework's programming model, for example Google ADK and Strands A2A (strands-agents[a2a]). Strands provides two key pieces: A2AServer exposes a Strands agent as an A2A-compliant server, and A2AAgent lets you use a remote A2A agent as if it were a local Strands agent.

  3. 3

    Platform-specific implementations

    Hosted platforms that expose A2A endpoints for agents deployed on them. For example, LangSmith Agent Server is the main way LangChain offers A2A today: the platform handles JSON-RPC messaging and task management, and your agent still takes part in A2A.

LayerExampleGives you
Protocol primitivesA2A SDKEvery building block, full control, most wiring
Framework integrationStrands A2AAn agent becomes a server in a few lines
Managed platformLangSmith Agent ServerEndpoints for agents deployed on that platform

The lab uses both of the first two layers: Strands A2A for the two agent servers (the quickest path from an agent definition to a working A2A server), and the raw A2A SDK for the orchestrator, so you can see the protocol objects directly.

5.11

Lab: a travel assistant over A2A

Two independent Strands agents (weather and flights) run as A2A servers. A travel orchestrator discovers them through their and asks each for its part of a trip.

Swipe sideways to see the full diagram
Travel orchestratorA2A client · no LLM · travel_orchestrator.py 1. Discover agent cards2. Send A2A messages3. Extract responses4. Print trip summary HTTP (A2A) HTTP (A2A) Weather agent:9001 Strands Agent + A2AServer Tool:get_weather Flights agent:9002 Strands Agent + A2AServer Tools:search_flights, book_flight each agent usesClaude on Bedrock
Three processes, one protocol. The two agents could be written by different teams in different frameworks; the orchestrator only needs their URLs.
FileRoleDescription
weather_agent_server.pyA2A serverWeather lookup agent on port 9001
flights_agent_server.pyA2A serverFlight search and booking agent on port 9002
travel_orchestrator.pyA2A clientCalls both agents and prints a trip summary

The weather agent server

weather_agent_server.py · imports and settings
"""
Weather Agent — A2A Server on port 9001.

Exposes a weather lookup tool via the Strands A2A protocol.

Run:  python weather_agent_server.py
"""

import logging
import os

# Agent: the core Strands agent class that wraps an LLM with tools
# tool: decorator that registers a Python function as a tool the LLM can call
from strands import Agent, tool

# A2AServer: wraps a Strands Agent into an HTTP server that speaks the A2A protocol
from strands.multiagent.a2a import A2AServer

# AgentSkill: A2A SDK type that describes a capability in the agent card
from a2a.types import AgentSkill

# BedrockModel: the LLM behind this agent (Amazon Bedrock)
from strands.models.bedrock import BedrockModel

MODEL_ID = os.getenv("BEDROCK_MODEL_ID", "global.anthropic.claude-sonnet-4-6")
REGION = os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "us-east-1"))

# Set up logging so we can see what's happening during requests
logging.basicConfig(level=logging.INFO)

Note the new import: A2AServer from strands.multiagent.a2a, and AgentSkill from the A2A SDK (a2a.types). The model is set explicitly from BEDROCK_MODEL_ID, as everywhere in this course.

weather_agent_server.py · fake data and the tool
# --- Fake Weather Data ---------------------------------------------------
# In a real app this would call a weather API.
# Here we use a dictionary for demo purposes.

FAKE_WEATHER = {
    "london": "15°C, cloudy with occasional rain",
    "paris": "18°C, sunny with light breeze",
    "tokyo": "22°C, humid and partly cloudy",
    "new york": "12°C, windy and cool",
    "dubai": "38°C, hot and sunny",
    "sydney": "20°C, mild and clear",
    "cancun": "30°C, tropical and sunny",
    "rome": "24°C, warm and sunny",
    "bangkok": "33°C, hot and humid",
    "reykjavik": "5°C, cold and overcast",
}


# --- Weather Tool --------------------------------------------------------
# The @tool decorator registers this function so the LLM can call it.
# The docstring becomes the tool description the LLM sees when deciding
# which tool to use. Args/Returns are parsed for the tool's input schema.

@tool
def get_weather(city: str) -> str:
    """Get the current weather for a given city.

    Args:
        city: Name of the city to get weather for.

    Returns:
        A string describing the current weather conditions.
    """
    # Normalize input: strip whitespace, lowercase for dictionary lookup
    result = FAKE_WEATHER.get(city.strip().lower())
    if result:
        return f"Weather in {city}: {result}"
    # If city not found, tell the user what cities are available
    return f"No weather data available for '{city}'. Available cities: {', '.join(FAKE_WEATHER.keys())}"
weather_agent_server.py · skills for the agent card
# --- Agent Card Skills ---------------------------------------------------
# Skills are published in the agent card at /.well-known/agent-card.json.
# Any A2A client fetches this card first to discover what the agent can do
# BEFORE sending any messages. Think of it like an API spec for agents.

WEATHER_SKILLS = [
    AgentSkill(
        # Unique identifier for this skill
        id="get_weather",
        # Human-readable name shown in the agent card
        name="Get Weather",
        # Detailed description — helps clients understand what this skill does
        description=(
            "Returns current weather conditions (temperature, sky, wind) "
            "for a given city. Supports: London, Paris, Tokyo, New York, "
            "Dubai, Sydney, Cancun, Rome, Bangkok, Reykjavik."
        ),
        # Tags for categorization and discovery
        tags=["weather", "temperature", "forecast", "travel"],
        # Example prompts — hints to clients on how to use this skill
        examples=[
            "What is the weather in London?",
            "How hot is it in Dubai right now?",
            "Tell me the weather conditions in Tokyo",
        ],
    ),
]

AgentSkill describes each capability, or , with an id, name, description, tags and example prompts. Passed to the server as skills=WEATHER_SKILLS, they appear in the agent card, which clients fetch first to understand what the agent can do. Think of it as an API spec for agents.

weather_agent_server.py · the Strands agent
# --- Strands Agent -------------------------------------------------------
# This is the actual AI agent. It combines:
#   - An LLM (Amazon Bedrock, chosen by BEDROCK_MODEL_ID)
#   - A system prompt (instructions for the LLM)
#   - Tools (functions the LLM can call)
#
# When a message comes in, the agent sends it to the LLM. The LLM reads
# the system prompt, sees the available tools, and decides whether to call
# get_weather. If it does, the tool runs, and the LLM formats the final answer.

weather_agent = Agent(
    # The LLM that powers this agent
    model=BedrockModel(model_id=MODEL_ID, region_name=REGION),
    # Name and description — used in the A2A agent card
    name="Weather Agent",
    description="Provides current weather information for cities around the world.",
    # System prompt — instructions the LLM follows for every request
    system_prompt=(
        "You are a helpful weather assistant. Use the get_weather tool to look up "
        "weather conditions for any city the user asks about. Be concise and friendly."
    ),
    # Register the get_weather tool so the LLM can call it
    tools=[get_weather],
    # Disable streaming callback on the server side (responses are collected, not printed)
    callback_handler=None,
)

An ordinary Strands agent. Two details matter for A2A: name and description become the agent card's name and description (Strands refuses to build a card if either is empty), and callback_handler=None stops the server printing every response, since answers go back over HTTP instead.

weather_agent_server.py · serve it over A2A
# --- A2A Server ----------------------------------------------------------
# Only runs when you execute this file directly (python weather_agent_server.py).
# Importing this file from another module won't start the server.

if __name__ == "__main__":
    # A2AServer wraps the Strands Agent into an HTTP server that:
    #   1. Serves an agent card at GET /.well-known/agent-card.json
    #   2. Accepts A2A messages at POST / (JSON-RPC: message/send, message/sendStream)
    #   3. Routes messages through the Strands Agent (LLM + tools)
    #   4. Returns responses as A2A task objects with artifacts
    server = A2AServer(
        # The Strands Agent that handles all incoming messages
        agent=weather_agent,
        # Bind to localhost only (use "0.0.0.0" to expose to network)
        host="127.0.0.1",
        # HTTP port to listen on
        port=9001,
        # Skills published in the agent card for client discovery
        skills=WEATHER_SKILLS,
        # Version string in the agent card
        version="1.0.0",
        # Use A2A-spec-compliant SSE streaming with artifact updates
        # (vs legacy status-only streaming). The agent card will advertise
        # {"capabilities": {"streaming": true}} so clients know SSE is supported.
        enable_a2a_compliant_streaming=True,
    )
    print("Weather Agent A2A server starting on http://127.0.0.1:9001")
    # Start the uvicorn HTTP server. This is a blocking call —
    # the process stays alive serving requests until you Ctrl+C.
    server.serve()
ArgumentWhat it does
agent=weather_agentThe Strands agent that handles every incoming message
host="127.0.0.1"Listen on this machine only ("0.0.0.0" would expose it to the network)
port=9001The HTTP port
skills=WEATHER_SKILLSPublished in the agent card for discovery
version="1.0.0"The agent's version, shown in the card
enable_a2a_compliant_streaming=TrueWhen a client asks for streaming, reply with spec-compliant SSE events including artifact updates (instead of older status-only streaming). Strands' A2AServer always advertises "streaming": true in the card; this flag only chooses the event format.
iMethod names in the comments

The comment above mentions message/sendStream, and flights_agent_server.py has the same comment. In the A2A SDK version this lab installs (0.3), the streaming method on the wire is called message/stream; the non-streaming one is message/send. The client picks the right one for you, so nothing in the code needs changing.

server.serve() starts a web server () and blocks until you press Ctrl+C. Your agent is now a stand-alone HTTP microservice that speaks A2A. Its card, at http://127.0.0.1:9001/.well-known/agent-card.json, looks like this:

GET /.well-known/agent-card.jsonabridged
{
  "name": "Weather Agent",
  "description": "Provides current weather information for cities around the world.",
  "version": "1.0.0",
  "protocolVersion": "0.3.0",
  "preferredTransport": "JSONRPC",
  "capabilities": { "streaming": true },
  "skills": [
    {
      "id": "get_weather",
      "name": "Get Weather",
      "description": "Returns current weather conditions...",
      "tags": ["weather", "temperature", "forecast", "travel"],
      "examples": ["What is the weather in London?", "..."]
    }
  ],
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "url": "http://127.0.0.1:9001/"
}
iagent= vs agent_factory=

In recent Strands versions, passing a single agent= is deprecated (you may see a DeprecationWarning) in favour of A2AServer(agent_factory=...), a function that builds a fresh agent per conversation, for servers that many callers use at once. For this one-user demo, agent= still works; every caller simply shares the same agent.

The flights agent server

Same pattern, with two tools and two skills, on port 9002.

flights_agent_server.py · fake data
# --- Fake Flight Data ----------------------------------------------------
# In a real app this would query a flights API or database.
# Here we use a dictionary keyed by (origin, destination) tuples.

FAKE_FLIGHTS = {
    ("london", "paris"): [
        {"airline": "AirEurope", "flight": "AE101", "depart": "08:00", "arrive": "10:30", "price": "$120"},
        {"airline": "SkyWings", "flight": "SW202", "depart": "14:00", "arrive": "16:30", "price": "$95"},
    ],
    ("new york", "london"): [
        {"airline": "TransAtlantic", "flight": "TA501", "depart": "22:00", "arrive": "10:00+1", "price": "$450"},
        {"airline": "AirEurope", "flight": "AE303", "depart": "18:00", "arrive": "06:00+1", "price": "$380"},
    ],
    ("tokyo", "sydney"): [
        {"airline": "PacificAir", "flight": "PA701", "depart": "09:00", "arrive": "20:00", "price": "$520"},
    ],
    ("paris", "rome"): [
        {"airline": "AirEurope", "flight": "AE150", "depart": "07:30", "arrive": "09:30", "price": "$85"},
        {"airline": "MedFly", "flight": "MF44", "depart": "12:00", "arrive": "14:00", "price": "$70"},
    ],
    ("dubai", "bangkok"): [
        {"airline": "GulfAir", "flight": "GA880", "depart": "02:00", "arrive": "12:00", "price": "$340"},
    ],
    ("new york", "cancun"): [
        {"airline": "SunJet", "flight": "SJ610", "depart": "06:00", "arrive": "10:00", "price": "$250"},
        {"airline": "SkyWings", "flight": "SW415", "depart": "11:00", "arrive": "15:00", "price": "$210"},
    ],
}
flights_agent_server.py · tools
# --- Flight Tools --------------------------------------------------------
# Each @tool-decorated function becomes a tool the LLM can call.
# The docstring is what the LLM sees when deciding which tool to use.
# Args/Returns are parsed to build the tool's input schema automatically.


@tool
def search_flights(origin: str, destination: str) -> str:
    """Search for available flights between two cities.

    Args:
        origin: Departure city name.
        destination: Arrival city name.

    Returns:
        A formatted list of available flights or a message if none found.
    """
    # Normalize both cities to lowercase for dictionary lookup
    key = (origin.strip().lower(), destination.strip().lower())
    flights = FAKE_FLIGHTS.get(key)

    # If no flights found, show the user what routes are available
    if not flights:
        available = [f"{o.title()} -> {d.title()}" for o, d in FAKE_FLIGHTS.keys()]
        return (
            f"No flights found from {origin} to {destination}. "
            f"Available routes: {', '.join(available)}"
        )

    # Format each flight as a readable line
    lines = [f"Flights from {origin.title()} to {destination.title()}:"]
    for f in flights:
        lines.append(
            f"  {f['airline']} {f['flight']}: depart {f['depart']}, "
            f"arrive {f['arrive']}, price {f['price']}"
        )
    return "\n".join(lines)


@tool
def book_flight(flight_number: str, passenger_name: str) -> str:
    """Book a specific flight for a passenger.

    Args:
        flight_number: The flight number to book (e.g. AE101).
        passenger_name: Name of the passenger.

    Returns:
        Booking confirmation or error message.
    """
    # Search all routes for the matching flight number
    for flights in FAKE_FLIGHTS.values():
        for f in flights:
            if f["flight"].upper() == flight_number.strip().upper():
                # Generate a fake confirmation number from a hash
                return (
                    f"Booking confirmed! {passenger_name} is booked on "
                    f"{f['airline']} flight {f['flight']} "
                    f"(depart {f['depart']}, arrive {f['arrive']}). "
                    f"Total: {f['price']}. Confirmation #: TRV-{hash(flight_number + passenger_name) % 100000:05d}"
                )
    return f"Flight {flight_number} not found. Please search for available flights first."

Routes are keyed by (origin, destination) in lower case, so "New York" and "new york" both work. An unknown route returns the list of available routes, a friendly error the LLM can relay. book_flight searches every route for the flight number and returns a confirmation with a made-up number (built from a hash, so it changes between Python runs).

See the rest of flights_agent_server.py (skills, agent and server)
flights_agent_server.py · skills, agent, server
# --- Agent Card Skills ---------------------------------------------------
# Skills are published in the agent card at /.well-known/agent-card.json.
# Any A2A client fetches this card first to discover what the agent can do
# BEFORE sending any messages. Think of it like an API spec for agents.

FLIGHTS_SKILLS = [
    AgentSkill(
        # Unique identifier for this skill
        id="search_flights",
        # Human-readable name shown in the agent card
        name="Search Flights",
        # Detailed description — helps clients understand what this skill does
        description=(
            "Searches for available flights between two cities. "
            "Returns airline, flight number, departure/arrival times, and price. "
            "Available routes: London→Paris, New York→London, Tokyo→Sydney, "
            "Paris→Rome, Dubai→Bangkok, New York→Cancun."
        ),
        # Tags for categorization and discovery
        tags=["flights", "search", "travel", "airlines", "booking"],
        # Example prompts — hints to clients on how to use this skill
        examples=[
            "Find flights from New York to London",
            "Search for flights from Paris to Rome",
            "What flights go from Dubai to Bangkok?",
        ],
    ),
    AgentSkill(
        id="book_flight",
        name="Book Flight",
        description=(
            "Books a specific flight for a passenger given a flight number "
            "and passenger name. Returns a booking confirmation with a "
            "confirmation number."
        ),
        tags=["booking", "reservation", "flights", "travel"],
        examples=[
            "Book flight AE303 for John Smith",
            "Reserve flight TA501 for Jane Doe",
        ],
    ),
]

# --- Strands Agent -------------------------------------------------------
# This is the actual AI agent. It combines:
#   - An LLM (Amazon Bedrock, chosen by BEDROCK_MODEL_ID)
#   - A system prompt (instructions for the LLM)
#   - Tools (functions the LLM can call)
#
# When a message comes in, the agent sends it to the LLM. The LLM reads
# the system prompt, sees the available tools, and decides whether to call
# search_flights or book_flight. The tool runs, and the LLM formats the answer.

flights_agent = Agent(
    # The LLM that powers this agent
    model=BedrockModel(model_id=MODEL_ID, region_name=REGION),
    # Name and description — used in the A2A agent card
    name="Flights Agent",
    description="Searches for flights between cities and handles bookings.",
    # System prompt — instructions the LLM follows for every request
    system_prompt=(
        "You are a helpful flight booking assistant. Use search_flights to find "
        "available flights between cities, and book_flight to make reservations. "
        "Be concise and provide clear flight options."
    ),
    # Register both tools so the LLM can call them
    tools=[search_flights, book_flight],
    # Disable streaming callback on the server side (responses are collected, not printed)
    callback_handler=None,
)

# --- A2A Server ----------------------------------------------------------
# Only runs when you execute this file directly (python flights_agent_server.py).

if __name__ == "__main__":
    # A2AServer wraps the Strands Agent into an HTTP server that:
    #   1. Serves an agent card at GET /.well-known/agent-card.json
    #   2. Accepts A2A messages at POST / (JSON-RPC: message/send, message/sendStream)
    #   3. Routes messages through the Strands Agent (LLM + tools)
    #   4. Returns responses as A2A task objects with artifacts
    server = A2AServer(
        # The Strands Agent that handles all incoming messages
        agent=flights_agent,
        # Bind to localhost only (use "0.0.0.0" to expose to network)
        host="127.0.0.1",
        # HTTP port to listen on
        port=9002,
        # Skills published in the agent card for client discovery
        skills=FLIGHTS_SKILLS,
        # Version string in the agent card
        version="1.0.0",
        # Use A2A-spec-compliant SSE streaming with artifact updates
        enable_a2a_compliant_streaming=True,
    )
    print("Flights Agent A2A server starting on http://127.0.0.1:9002")
    # Start the uvicorn HTTP server — blocking call, runs until Ctrl+C
    server.serve()

The travel orchestrator (A2A client)

This program has no LLM. It just sends A2A messages to the two servers and prints what comes back, so you can see the protocol objects directly.

travel_orchestrator.py · imports and settings
"""
Travel Orchestrator — A2A client that talks to Weather + Flights agents.

This is the A2A CLIENT side. It connects to two remote A2A agent servers,
sends them messages using the A2A protocol, and prints the results.

No LLM here — just direct A2A request/response.

Prerequisites:
    1. Start weather agent:  python weather_agent_server.py
    2. Start flights agent:  python flights_agent_server.py
    3. Run this:             python travel_orchestrator.py

Usage:
    python travel_orchestrator.py
    python travel_orchestrator.py "London" "New York" "London"
"""

import sys
import asyncio
import logging
from uuid import uuid4

# httpx: async HTTP client used to make requests to the A2A servers
import httpx

# A2ACardResolver: fetches the agent card from /.well-known/agent-card.json
# ClientConfig: configures the A2A client (streaming vs sync, timeout, etc.)
# ClientFactory: creates an A2A client from an agent card
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory

# A2A message types used to construct the request
# Message: the envelope for a user or agent message
# Part/TextPart: wraps the actual text content
# Role: identifies the sender (user or agent)
from a2a.types import Message, Part, Role, TextPart

# Set up logging so we can see A2A protocol activity
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# URLs of the two A2A agent servers
WEATHER_URL = "http://127.0.0.1:9001"
FLIGHTS_URL = "http://127.0.0.1:9002"

# Timeout for HTTP requests (5 minutes — agents may take time with LLM calls)
TIMEOUT = 300
travel_orchestrator.py · step 1 build a message, step 3 extract text
# --- Helper: Build an A2A Message ----------------------------------------
# Creates a standard A2A Message object that the server expects.
# This is the equivalent of a user typing a question in a chat.

def make_message(text: str) -> Message:
    return Message(
        kind="message",          # Always "message" for chat messages
        role=Role.user,          # We're the user sending a question
        parts=[Part(TextPart(    # Wrap the text in a TextPart inside a Part
            kind="text",
            text=text,
        ))],
        message_id=uuid4().hex,  # Unique ID for this message
    )


# --- Helper: Extract Text from A2A Response ------------------------------
# A2A servers return a "task" object. The actual answer text lives inside
# task.artifacts[].parts[].text. This function digs it out.

def extract_text(result) -> str:
    """Pull text out of an A2A task response (from artifacts)."""
    if hasattr(result, "artifacts") and result.artifacts:
        texts = []
        for artifact in result.artifacts:
            for part in artifact.parts:
                # The a2a SDK wraps parts in a discriminated union (Part.root)
                if hasattr(part, "root") and hasattr(part.root, "text"):
                    texts.append(part.root.text)
                elif hasattr(part, "text"):
                    texts.append(part.text)
        if texts:
            return "".join(texts)
    # Fallback: return the raw string representation
    return str(result)
  1. 1

    make_message: build an A2A message

    A Message with role=Role.user, one text part (Part(TextPart(...))), and a unique message_id. It's the A2A equivalent of a user typing in a chat box.

  2. 3

    extract_text: dig the answer out

    A2A servers return a task; the answer lives in task.artifacts[].parts[]. The SDK wraps each part in a container that can hold any kind of part (text, file or data) and keeps the real part in part.root, so the code checks both shapes and falls back to the raw text if needed.

travel_orchestrator.py · step 2 discover and send
# --- Core: Send a Message to an A2A Agent --------------------------------
# This is the main A2A client logic. It:
#   1. Fetches the agent card to discover capabilities
#   2. Auto-selects streaming or sync mode based on the card
#   3. Sends the message using the A2A protocol
#   4. Extracts and returns the text response

async def send_a2a_message(base_url: str, text: str) -> str:
    """Send a message to an A2A agent, auto-selecting streaming or sync based on the agent card."""
    async with httpx.AsyncClient(timeout=TIMEOUT) as hc:

        # Step 1: Fetch the agent card from /.well-known/agent-card.json
        # This tells us the agent's name, skills, and whether it supports streaming
        resolver = A2ACardResolver(httpx_client=hc, base_url=base_url)
        card = await resolver.get_agent_card()

        # Step 2: Check the agent card capabilities to decide the mode
        # If the server advertises {"capabilities": {"streaming": true}},
        # we use SSE streaming; otherwise we use sync (single response)
        use_streaming = bool(card.capabilities and card.capabilities.streaming)
        mode = "streaming" if use_streaming else "sync"
        logger.info(f"Connected to: {card.name} — mode: {mode}")

        # Step 3: Create an A2A client configured for this agent
        # ClientConfig sets streaming mode; ClientFactory builds the client from the card
        config = ClientConfig(httpx_client=hc, streaming=use_streaming)
        client = ClientFactory(config).create(card)

        # Step 4: Send the message and process response events
        # In sync mode: yields one task with artifacts (the final answer)
        # In streaming mode: yields intermediate Messages + final task with artifacts
        msg = make_message(text)
        final_text = ""

        async for event in client.send_message(msg):
            # Task with artifacts — this is the final complete response
            if hasattr(event, "artifacts") and event.artifacts:
                final_text = extract_text(event)

            # (Task, UpdateEvent) tuple — check for artifacts on the task
            elif isinstance(event, tuple) and len(event) == 2:
                task, _ = event
                if hasattr(task, "artifacts") and task.artifacts:
                    final_text = extract_text(task)

            # Intermediate streaming Message — we skip these and wait for
            # the final artifacts which contain the complete answer
            elif isinstance(event, Message):
                pass

        return final_text if final_text else "No response received."
  1. a

    Fetch the agent card

    A2ACardResolver(...).get_agent_card() downloads /.well-known/agent-card.json: name, skills, capabilities.

  2. b

    Pick the mode from the card

    If the card says streaming: true, use streaming (SSE); otherwise a single synchronous response. That's capability negotiation in two lines.

  3. c

    Create a client for that agent

    ClientFactory(ClientConfig(...)).create(card) builds a client configured from the card: URL, binding, streaming.

  4. d

    Send and read the events

    client.send_message(msg) sends the JSON-RPC request and yields events. Both cards in this lab say streaming: true, so it is always message/stream here; against an agent without streaming, the same code would send message/send. A task with artifacts, or a (task, update) pair, carries the answer; intermediate streaming messages are skipped until the final artifacts arrive.

travel_orchestrator.py · step 4 the trip summary
# --- Main: Orchestrate Calls to Both Agents ------------------------------
# Sends one request to the Weather Agent and one to the Flights Agent,
# then prints a combined trip summary.

async def main(city: str, origin: str, destination: str):
    print("=" * 60)
    print("  Travel Orchestrator (A2A)")
    print("=" * 60)

    # 1. Ask Weather Agent for current conditions at the destination
    print(f"\n>>> Asking Weather Agent: weather in {city}")
    weather = await send_a2a_message(WEATHER_URL, f"What is the weather in {city}?")
    print(f"<<< Weather Agent response:\n{weather}")

    # 2. Ask Flights Agent for available flights on the route
    print(f"\n>>> Asking Flights Agent: flights from {origin} to {destination}")
    flights = await send_a2a_message(FLIGHTS_URL, f"Search flights from {origin} to {destination}")
    print(f"<<< Flights Agent response:\n{flights}")

    # 3. Print a combined trip summary
    print("\n" + "=" * 60)
    print("  Trip Summary")
    print("=" * 60)
    print(f"\nDestination: {city}")
    print(f"Weather: {weather.strip()}")
    print(f"\nFlights ({origin} → {destination}):")
    print(flights.strip())


# --- Entry Point ---------------------------------------------------------
# Supports two modes:
#   - Default: demo with London / New York → London
#   - Custom:  pass city, origin, destination as arguments

if __name__ == "__main__":
    if len(sys.argv) == 4:
        # python travel_orchestrator.py <city> <origin> <destination>
        asyncio.run(main(sys.argv[1], sys.argv[2], sys.argv[3]))
    else:
        # Default demo
        asyncio.run(main("London", "New York", "London"))

What happens on the wire

Swipe sideways to see the full diagram
Orchestrator Weather agent :9001 GET /.well-known/agent-card.json { name, description, skills, capabilities } POST / JSON-RPC message/stream · "What is the weather in London?" LLM calls get_weatherinside Agent B, invisible to A SSE events, ending in a task with artifacts: "15°C, cloudy…"
Card first, then message, then task. The orchestrator never sees the tool call; the weather agent stays opaque.
  1. 1

    The client fetches the agent card to discover what the agent can do

  2. 2

    It sends a JSON-RPC message request (message/stream here, because the card says streaming) with the user's question

  3. 3

    The server's Strands agent processes it, calls its tools, and streams back events that end in a task with artifacts holding the final text

Run it in three terminals

!Every terminal needs the same setup

Each new terminal starts fresh. In all three, cd into section-05-mcp-and-a2a/a2a-travel-agents, activate the course's virtual environment, and make sure your AWS credentials and AWS_REGION are set there too.

terminal
pip install -r requirements.txt

# Terminal 1
python weather_agent_server.py

# Terminal 2
python flights_agent_server.py

# Terminal 3 — default demo (New York → London)
python travel_orchestrator.py

# Or specify city, origin, destination
python travel_orchestrator.py "Rome" "Paris" "Rome"

You need AWS credentials with Bedrock access; each agent uses BEDROCK_MODEL_ID (default Claude Sonnet 4.6). Start terminals 1 and 2 first and wait until each shows Uvicorn running on http://127.0.0.1:9001 (or :9002). The results print in terminal 3; the grey INFO lines are the protocol at work: fetch the card, decide the mode, POST the message.

terminal 3 outputillustrative; the agents word their answers freely
============================================================
  Travel Orchestrator (A2A)
============================================================

>>> Asking Weather Agent: weather in London
INFO:httpx:HTTP Request: GET http://127.0.0.1:9001/.well-known/agent-card.json "HTTP/1.1 200 OK"
INFO:a2a.client.card_resolver:Successfully fetched agent card data from http://127.0.0.1:9001/.well-known/agent-card.json: {…}
INFO:__main__:Connected to: Weather Agent — mode: streaming
INFO:httpx:HTTP Request: POST http://127.0.0.1:9001/ "HTTP/1.1 200 OK"
<<< Weather Agent response:
The weather in London is currently 15°C, cloudy with occasional rain. You may want an umbrella!

>>> Asking Flights Agent: flights from New York to London
INFO:httpx:HTTP Request: GET http://127.0.0.1:9002/.well-known/agent-card.json "HTTP/1.1 200 OK"
INFO:a2a.client.card_resolver:Successfully fetched agent card data from …
INFO:__main__:Connected to: Flights Agent — mode: streaming
INFO:httpx:HTTP Request: POST http://127.0.0.1:9002/ "HTTP/1.1 200 OK"
<<< Flights Agent response:
Here are the available flights from New York to London:
1. TransAtlantic TA501: departs 22:00, arrives 10:00 (+1 day), $450
2. AirEurope AE303: departs 18:00, arrives 06:00 (+1 day), $380

============================================================
  Trip Summary
============================================================

Destination: London
Weather: The weather in London is currently 15°C, cloudy with occasional rain. …

Flights (New York → London):
Here are the available flights from New York to London: …
Available data
Weather citiesLondon, Paris, Tokyo, New York, Dubai, Sydney, Cancun, Rome, Bangkok, Reykjavik
Flight routesLondon→Paris, New York→London, Tokyo→Sydney, Paris→Rome, Dubai→Bangkok, New York→Cancun
Try it

Run the orchestrator

Terminal 3
python travel_orchestrator.py "London" "New York" "London"
Three arguments: the city for the weather, then the flight origin and destination.
→ Weather agent :9001 · GET /.well-known/agent-card.json
Step 1: discover the agent before talking to it.
← Agent card
{ "name": "Weather Agent", "capabilities": { "streaming": true },
  "skills": [ { "id": "get_weather", … } ] }
streaming: true, so the orchestrator logs mode: streaming and will use message/stream.
→ POST / · JSON-RPC message/stream
{ "role": "user", "parts": [ { "kind": "text",
  "text": "What is the weather in London?" } ] }
← Task completed · artifact
The weather in London is currently 15°C, cloudy with occasional rain.
Inside, the agent's LLM called get_weather("London") and got Weather in London: 15°C, cloudy with occasional rain. The orchestrator never sees that call. (The real wording varies; the facts come from the tool.)
→ Flights agent :9002 · GET /.well-known/agent-card.json
← Agent card
{ "name": "Flights Agent", "capabilities": { "streaming": true },
  "skills": [ { "id": "search_flights", … }, { "id": "book_flight", … } ] }
→ POST / · JSON-RPC message/stream
{ "role": "user", "parts": [ { "kind": "text",
  "text": "Search flights from New York to London" } ] }
← Task completed · artifact
Here are the available flights from New York to London:
1. TransAtlantic TA501: departs 22:00, arrives 10:00+1, $450
2. AirEurope AE303: departs 18:00, arrives 06:00+1, $380
Inside, the agent's LLM called search_flights("New York", "London").
Terminal 3 · Trip Summary
Destination: London
Weather: The weather in London is currently 15°C, cloudy with occasional rain.

Flights (New York → London):
Here are the available flights from New York to London:
1. TransAtlantic TA501: departs 22:00, arrives 10:00+1, $450
2. AirEurope AE303: departs 18:00, arrives 06:00+1, $380
Two independent agents, one combined answer. The orchestrator itself has no LLM: it just routes messages and prints.

Both protocols together: A2A outside, MCP inside

In this lab the weather agent keeps its data in a Python dictionary. In a real system it would more likely get weather from an MCP server, which is exactly what you built in Lab 5.6. The two protocols then sit at different layers: A2A between agents, MCP between each agent and its tools. The orchestrator doesn't notice the change at all, because the weather agent is opaque.

Swipe sideways to see the full diagram
Travelorchestrator A2A HTTP Weather agent :9001 opaque from outside Strands Agent + A2AServerthe MCP host MCPClient MCP stdio MCP servermcp-basics/server.pyget_weather
Different layers, no overlap. A2A lets agents collaborate; MCP lets each agent do its own work with tools and data.

To try it, change only the agent in weather_agent_server.py, reusing the MCPClient pattern from Lab 5.7. Recent Strands versions accept an MCPClient directly in tools=[...] and open and close the connection for you:

weather_agent_server.py · optional change
import sys
from pathlib import Path
from mcp import StdioServerParameters, stdio_client
from strands.tools.mcp import MCPClient

# The MCP server from Lab 5.6, started as a child process over stdio
SERVER = Path(__file__).resolve().parent.parent / "mcp-basics" / "server.py"
mcp_client = MCPClient(
    lambda: stdio_client(StdioServerParameters(command=sys.executable, args=[str(SERVER)]))
)

weather_agent = Agent(
    model=BedrockModel(model_id=MODEL_ID, region_name=REGION),
    name="Weather Agent",
    description="Provides current weather information for cities around the world.",
    system_prompt="You are a helpful weather assistant. Be concise and friendly.",
    tools=[mcp_client],      # tools now arrive over MCP instead of @tool
    callback_handler=None,
)
!The data changes too

The MCP server only knows London, Paris, Tokyo, New York and Bangalore, with shorter descriptions ("15°C, cloudy"). Ask the orchestrator for Bangalore and it now works; ask for Rome and the weather agent will say it has no data. This is an experiment for you to try, not part of the course lab.

</>Extend it yourself

Give the orchestrator an LLM: create a Strands agent whose tools wrap the two remote agents, for example one small @tool per agent that calls send_a2a_message with that agent's URL. Then it can decide on its own when to check the weather, when to search flights, and even when to call book_flight.

5.12

Recap and self-check

Your agents can now plug into any tool that speaks MCP, and talk to any agent that speaks A2A.

Standard protocols make agentic systems easier to build, extend and scale. You started with MCP, which solves the explosion of custom integrations between many AI apps and many tools: instead of N × M connectors, everything speaks one shared interface through hosts, clients and servers. You saw how messages travel (JSON-RPC over stdio or Streamable HTTP), how a connection is set up and closed (the lifecycle), how remote servers are protected (authorization), and what servers offer: tools to act, resources to read and prompt templates to ask consistently. Clients can offer things back: sampling, roots and elicitation.

Then you met A2A, which solves a different problem: letting agents, open-ended problem solvers rather than simple tools, collaborate without exposing their insides. Agents discover each other through agent cards, exchange messages, track work as tasks and return artifacts, in four interaction styles: synchronous, asynchronous polling, streaming and push notifications.

In the labs you built an MCP weather server with FastMCP, called it from a raw client and from Strands agents, and then ran a travel orchestrator that talks to weather and flights agents over A2A. The two protocols complement each other: A2A for collaboration between agents, MCP for each agent's own work with tools and data.

Swipe sideways to see the full diagram
MCPLLM app ↔ tools and data Host Client Server Transportsstdio · Streamable HTTP · custom Lifecycleinitialize → operate → shut down SecurityOAuth 2.1 for HTTP · env for stdio Server offers tools resources prompts Client offers sampling roots elicitation A2Aagent ↔ agent Client agent Remote agent Discoveryagent card at /.well-known/… BindingsJSON-RPC · gRPC · HTTP+JSON Insideopaque: no shared memory or tools Data model message task artifact Styles sync polling streaming push They complement each other A2A lets agents collaborate · MCP lets each agent do its own work with tools and data
Two protocols, two layers. Built in this section with FastMCP and the MCP client on one side, and Strands A2AServer plus the A2A SDK on the other.
Try it

MCP or A2A?

For each situation, pick the protocol that fits best.

1. Give a coding assistant access to your GitHub issues and pull requests.

2. A hospital's scheduling agent asks an insurance company's agent to verify a patient's coverage.

3. Let a chat app read your team's database schema as context.

4. Your travel orchestrator delegates weather questions to another team's weather agent, without knowing how it works inside.

5. Share a “summarise this meeting” instruction template that every AI app in your company can reuse.

6. A booking agent that takes hours to confirm must tell the requesting agent when it's done.

0 of 6 answered

You should now be able to explain

IdeaIn one line
Why MCPN apps × M tools custom integrations become N + M implementations of one standard
Host, client, serverThe app the user sees; one connector per server inside it; the program that offers capabilities
Transportsstdio for local child processes, Streamable HTTP for remote servers, custom (e.g. gRPC) when needed
Lifecycleinitialize → result → notifications/initialized → normal work → close the connection
Tools, resources, promptsDo things; read things without side effects; reuse good instructions
Sampling, roots, elicitationServer borrows the client's model; client limits where the server may work; server asks the user for missing details
FastMCPDecorators (@mcp.tool(), @mcp.resource(), @mcp.prompt()) turn Python functions into MCP features
Strands + MCPMCPClient starts or connects to a server; list_tools_sync() hands its tools to an Agent
Why A2AAgents are opaque, open-ended problem solvers; they need a standard way to discover and delegate to each other
Agent cardA JSON document at /.well-known/agent-card.json: name, skills, capabilities, endpoint
Message, task, artifactThe request; the unit of work with an ID and status; the output
Four stylesSynchronous, polling with GetTask, streaming over SSE, push to a webhook
A2AServerWraps a Strands agent as an A2A HTTP server that publishes its own card

When something goes wrong

What you seeWhat it usually means and how to fix it
Import errors or unexpected API errors from mcpA different major version of the MCP SDK is installed. Run pip install -r requirements.txt in the lab folder; it pins mcp below 2.0.
`uvx` not found. Install uv firstThe AWS documentation lab launches its server with uvx. Install the folder's requirements (they include uv) or run pip install uv.
The AWS docs agent is slow or times out the first timeuvx downloads the server package on first run. The script already allows 120 seconds; run it again once the download has finished.
AccessDeniedException or credential errors from BedrockYour AWS credentials can't call the model. Check AWS_REGION, your credentials, and that BEDROCK_MODEL_ID is available to your account.
address already in use when starting an A2A serverAn earlier copy is still running on port 9001 or 9002. Stop it with Ctrl+C in its terminal (or close that terminal), then start again.
The orchestrator fails with a connection errorIt can't reach an agent. Start both servers first (terminals 1 and 2) and wait until each shows Uvicorn running on … before running the orchestrator.
No response received.The server answered without any artifacts, usually because its agent hit an error. Look at that server's terminal for the real message (often a Bedrock error).
"No flights found" or "No weather data available"The city or route isn't in the lab's fake data. Use one from the Available data table; the agent lists what it has.
iNothing to clean up

Everything in this section runs on your own machine and only calls Bedrock (plus the public AWS documentation site in one lab). No AWS resources are created, so there's nothing to delete. Stop the A2A servers with Ctrl+C when you're done.

Self-check

Ten quick questions. Pick an answer to see why it's right or wrong.

1. What problem does MCP mainly solve?

2. How many MCP servers does a single MCP client talk to?

3. Your client starts the server as a child process on the same machine. Which transport is that?

4. Which message completes the MCP initialization handshake?

5. A server wants to share a read-only student directory that the model can look things up in, with no side effects. Which feature fits?

6. A server has the data but no AI model. It wants an AI to rank activities, with the user approving. Which client feature allows that?

7. Where does an A2A client look first to learn what a remote agent can do?

8. A remote agent returns a task with status input-required. What does that mean?

9. In the lab, how does travel_orchestrator.py decide whether to use streaming?

10. Which sentence best describes how MCP and A2A relate?

Score: 0 of 10 answered

→Up next: Section 6, shipping agents to production

Everything so far has run on your laptop. Next you'll deploy agents on AWS: Amazon Bedrock AgentCore Runtime, Amazon ECS on Fargate and AWS Lambda, plus AgentCore Gateway, which turns an existing REST API into MCP tools. The final lab puts this section's ideas in the cloud: an orchestrator calls a stock agent over A2A, and that agent gets its tools over MCP. Folder: section-06-production-deployment.

A–Z

Glossary

Every underlined word on this page, in one place.

A2A (Agent2Agent protocol)

An open standard that lets independent AI agents, built by different teams on different frameworks, discover each other and work together over HTTP.

Agent card

A JSON document an A2A agent publishes, usually at /.well-known/agent-card.json, describing its name, skills, capabilities, endpoint and how to authenticate. Clients read it before sending anything.

Agent skill

One entry in an agent card describing something the agent can do: an ID, a name, a description, tags and example requests. In Python it's an AgentSkill object.

Artifact

An output a remote agent produces for a task, such as a report, a booking confirmation or a file, made up of parts.

Authorization

Checking that a caller is allowed to use a server. Remote MCP servers over HTTP can use OAuth 2.1 access tokens; local stdio servers take credentials from their environment instead.

Elicitation

An MCP client feature that lets a server ask the user for missing information in the middle of a task, either through a structured form or by sending them to a URL.

EMR / EHR

Electronic medical record / electronic health record: the hospital systems that store patients' histories, diagnoses and treatments.

ERP system

Enterprise resource planning: a company's central software for inventory, orders, shipping, finance and other operations.

FastAPI

A popular Python framework for building web APIs for any client. Its design inspired FastMCP, but it speaks plain HTTP, not MCP.

FastMCP

The high-level part of the Python MCP SDK. Decorators like @mcp.tool(), @mcp.resource() and @mcp.prompt() turn ordinary Python functions into MCP features.

gRPC

A fast way for programs to call each other over the network, using compact binary messages instead of JSON text. Popular for high-traffic internal services.

JSON-RPC 2.0

A simple standard for remote calls written in JSON. A request names a method and its parameters and carries an ID; the response carries the same ID with a result or an error. Notifications have no ID and expect no reply.

Lifecycle

The fixed order of an MCP connection: initialize (agree on versions and capabilities), normal operation, then shutdown.

MCP (Model Context Protocol)

An open standard for connecting AI applications to tools, data and prompt templates. Build a connector once as an MCP server and any MCP-compatible app can use it.

MCP client

The connector inside a host that talks to exactly one MCP server: it opens the connection, discovers what the server offers and passes requests and results back and forth.

MCP host

The AI application the user actually works in, such as a chat app, an IDE or your own agent. It contains one MCP client for each server it connects to.

MCP server

A program that offers tools, resources and prompt templates to MCP clients. It can run on your machine (started as a child process) or remotely over HTTP.

Message (A2A)

One turn of communication between agents, with a role (user or agent) and one or more parts: text, files or structured data.

MIME type

A standard label for what kind of content something is, such as text/plain, application/json or application/pdf, so the receiver knows how to handle it.

OAuth 2.1

The standard way for an app to get a limited, expiring access token to act on a user's behalf, instead of handling their password. MCP uses it to protect remote servers.

Opaque agent

An agent that collaborates without revealing its internals: other agents see only its card, its messages and its results, never its memory, tools or prompts.

OpenAPI (Swagger)

A standard JSON or YAML document that describes a web API: its endpoints, inputs and outputs. Tools can read it to learn how to call the API.

Prompt template

A reusable, predefined prompt that an MCP server offers, with arguments to fill in, such as a weather report for a given city. Users usually pick them explicitly.

Protocol binding

The concrete format A2A messages travel in: JSON-RPC, gRPC or plain HTTP+JSON. The meaning is the same; only the wrapping differs.

Resource

Read-only data an MCP server exposes, such as a file, a database record or an API response. Reading a resource gives context without changing anything.

Resource template

A pattern for a family of resources with a placeholder, such as greeting://{name}. The client fills in the placeholder to read a specific one.

Roots

Folders or URIs the client tells a server it may work within. They scope what the server should touch, and the client can update them when the user switches projects.

Sampling

An MCP client feature that lets a server ask the client's model to generate text for it, with the user able to review and approve. The server needs no model of its own.

SSE (server-sent events)

A way for a server to keep an HTTP response open and push a stream of small events to the client as they happen, instead of one reply at the end.

Stateless

Keeps no memory between calls: the same input always gets handled the same way, with nothing remembered from earlier requests.

stdio (standard input/output)

The text pipes every program has for input and output. With the stdio transport, the client starts the server as a child process and they write JSON-RPC messages to each other through these pipes.

Task (A2A)

The unit of work a remote agent creates to track a request, with an ID and a status such as submitted, working, input-required or completed.

Tool (in MCP)

A function an MCP server exposes for the model to call, such as searching, sending a message or updating a record. Clients find them with tools/list and run them with tools/call.

Transport

How MCP messages physically travel between client and server: through standard input/output pipes (stdio), over HTTP, or a custom channel. The messages themselves are the same.

URI

A unique address for a piece of data, written like file:///docs/notes.md or greeting://Asha. MCP uses URIs to name resources.

uvicorn

A fast Python web server that runs async web apps. A2AServer.serve() uses it to listen for HTTP requests.

uvx

A command that comes with the uv package manager. It downloads a Python tool into a temporary environment and runs it in one step, which is handy for starting MCP servers.

Webhook

A URL you give to another system so it can call you when something happens. It's how A2A push notifications reach the client.

© 2026 Shivank Agrawal, Future with Shivank. Agentic AI on AWS, Section 5. Course code is provided under the MIT License.

AWS service names, tool names, protocol versions and library versions change often. When something here differs from what you see, the current documentation wins.