Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 03-Azure/01-04-AI/03_Inventory_Planning_Agentic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ src/
workflow.py Stretch: the loop as one sequential workflow
live.py Stretch: change-feed watcher — the loop reacts to Cosmos inserts
mcp_server.py Stretch: MCP server exposing inject_signal (external trigger)
mcp_client.py Stretch: MCP client that launches the server and calls inject_signal
ui/ Minimal planner console (FastAPI + static HTML) — the only UI
challenges/ Challenge instructions
walkthrough/ Reference solutions (one per challenge)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,35 @@ The write can come from anywhere — including an **MCP** client (GitHub Copilot
another agent). [`src/mcp_server.py`](../src/mcp_server.py) exposes one tool,
`inject_signal`, that writes to the same `signals` container.

1. From `src/`, run the MCP server. It uses **stdio transport** — it talks over standard
input/output, **not a network port**, so there's nothing to `curl`; an MCP client
launches and speaks to the process directly.
The server uses **stdio transport**: it talks JSON-RPC over standard input/output, **not a
network port**. You don't run it on its own — an **MCP client** starts it as a child process
and speaks to it. [`src/mcp_client.py`](../src/mcp_client.py) is a minimal client that does
exactly that.

1. Keep the planner console running in its terminal. Open a **second terminal** (**+** in
the terminal panel) and go to `src/`:
```bash
cd src
```
2. Call the tool through MCP — the client launches `mcp_server.py`, lists its tools and calls
**`inject_signal`**:
```bash
uv run python mcp_server.py
uv run python mcp_client.py "Competitor out of stock on chainsaws in the West" --region West
```
You should see:
```text
Connected to MCP server 'zava-signals' - tools: inject_signal
Injected signal SIG-… — the planner loop will react automatically.
```
2. Point an MCP client at that command (or use an MCP client session to call the tool).
Invoke **`inject_signal`** with a headline, e.g. *"Competitor out of stock on
chainsaws in the West"*.
3. Switch to the console — it **reacts on its own** to the MCP-driven insert, runs the
loop, and waits at the gate. An external system just drove your inventory loop.

> [!IMPORTANT]
> **Don't run `uv run python mcp_server.py` by itself.** It starts and waits silently for
> JSON-RPC messages from a client; pressing **Enter** sends an empty line and prints
> `Invalid JSON: EOF while parsing a value … input_value='\n'`. That's expected — use
> `mcp_client.py` (or another MCP client) instead. Stop a stray server with **Ctrl+C**.

### Part D — (Go further) any producer, same reaction

The trigger is **decoupled** from the loop: the watcher reacts to the *data*, not to
Expand All @@ -117,7 +134,8 @@ Explorer, a `az cosmosdb`/SDK script, or a scheduled job — and watch the conso
| Steps error out mid-run | The auto-run uses the same agents as Challenges 2–4 — make sure they work manually first, and that `az login` is valid. |
| SSE doesn't update the UI | Some corporate proxies buffer `text/event-stream`; open the forwarded port directly, or check the browser console for the `/api/events` connection. |
| Watcher dot goes gray mid-session | The SSE link dropped; it **reconnects automatically** and the dot re-greens on the next event. If it stays gray, reload the page. |
| MCP client can't call the tool | Run `uv run python mcp_server.py` from `src/`, and make sure `.env` + `az login` are in that shell (the tool writes to Cosmos). |
| `Invalid JSON: EOF while parsing a value … input_value='\n'` | You ran `mcp_server.py` directly and pressed Enter. It's a stdio server that waits for a client — press **Ctrl+C** and use `uv run python mcp_client.py "<headline>"` instead (Part C). |
| `mcp_client.py` fails calling the tool | Run it from `src/` (where `uv run` finds the project), check `COSMOS_ENDPOINT` in `src/.env`, and make sure `az login` is valid (the tool writes to Cosmos). |

## 🚀 Go further

Expand All @@ -127,6 +145,9 @@ Explorer, a `az cosmosdb`/SDK script, or a scheduled job — and watch the conso
— same pattern, detached from the console. Note the added Function App infra.
- Give the MCP server a second tool, `list_exposure`, so an external agent can *ask*
before it *injects*.
- Connect a full MCP host instead of the script: register the server in **GitHub Copilot
Chat** (agent mode) as a **stdio** server with command `uv` and arguments
`run python mcp_server.py` (working directory `src/`), then ask Copilot to inject a signal.

## 📚 Learning resources

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ again.
| `workflow.py` | Stretch: the whole loop as one callable (used by `/api/run-loop`) |
| `live.py` | Stretch (Challenge 6): Cosmos change-feed watcher — auto-runs the loop when a signal is written, streamed to the UI over SSE, stopping at the human gate |
| `mcp_server.py` | Stretch (Challenge 6): MCP server exposing `inject_signal` so an external client can trigger the loop |
| `mcp_client.py` | Stretch (Challenge 6): minimal MCP client that launches `mcp_server.py` over stdio and calls `inject_signal` |
| `ui/` | Minimal FastAPI + static-HTML planner console — the only way in |

> The store is provisioned by `labautomation` (per-attendee Cosmos DB, keyless). The
Expand Down
51 changes: 51 additions & 0 deletions 03-Azure/01-04-AI/03_Inventory_Planning_Agentic/src/mcp_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Minimal MCP client for Challenge 6: launch ``mcp_server.py`` and call ``inject_signal``.

``mcp_server.py`` uses the stdio transport, so it can't be run on its own and typed
into: an MCP client starts it as a child process and exchanges JSON-RPC over its
stdin/stdout. This script is that client.

Run it from ``src/`` in a second terminal while the planner console is running:

uv run python mcp_client.py "Competitor out of stock on chainsaws in the West"
uv run python mcp_client.py "Heatwave intensifies" --category outdoor_power_tools --region "Pacific Northwest"
"""

from __future__ import annotations

import argparse
import asyncio
import sys
from pathlib import Path

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

SERVER = Path(__file__).with_name("mcp_server.py")


async def inject(headline: str, category: str, region: str) -> int:
params = StdioServerParameters(command=sys.executable, args=[str(SERVER)], cwd=str(SERVER.parent))
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = [tool.name for tool in (await session.list_tools()).tools]
print(f"Connected to MCP server 'zava-signals' - tools: {', '.join(tools)}")
result = await session.call_tool(
"inject_signal", {"headline": headline, "category": category, "region": region}
)
for item in result.content:
print(getattr(item, "text", item))
return 1 if result.isError else 0


def main() -> None:
parser = argparse.ArgumentParser(description="Call the inject_signal MCP tool.")
parser.add_argument("headline", help='The market signal, e.g. "Competitor out of stock on chainsaws in the West"')
parser.add_argument("--category", default="outdoor_power_tools")
parser.add_argument("--region", default="Pacific Northwest")
args = parser.parse_args()
sys.exit(asyncio.run(inject(args.headline, args.category, args.region)))


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
sense → plan → propose and stops at the human-approval gate. No agent is called here —
the tool only writes the signal; the reactive loop does the rest.

Run it (stdio transport, from the ``src/`` folder):
Run it (stdio transport) through an MCP client — not on its own. From the ``src/`` folder:

uv run python mcp_server.py
uv run python mcp_client.py "Competitor out of stock on chainsaws in the West"

Then point an MCP client at that command. The app must be running and Cosmos configured
``mcp_client.py`` starts this server as a child process and calls ``inject_signal``. Running
``python mcp_server.py`` directly just waits for JSON-RPC on stdin (pressing Enter gives an
"Invalid JSON" error). The app must be running and Cosmos configured
(``COSMOS_ENDPOINT`` in ``.env`` + ``az login``) for the write — and the reaction — to work.
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"python-dotenv>=1.0.1",
# Model Context Protocol server — exposes `inject_signal` so an external MCP
# client can drop a market signal into Cosmos and trigger the loop (Challenge 5).
"mcp>=1.2.0",
"mcp>=1.2.0,<2",
]

# This is an application, not an installable library — uv manages the environment
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,20 @@ def inject_signal(headline, category="outdoor_power_tools", region="Pacific Nort
return f"Injected signal {doc['signalId']} — the planner loop will react automatically."
```

Run it with `uv run python mcp_server.py` (stdio) and point any MCP client at it. Calling
`inject_signal` writes to Cosmos → the console's watcher reacts → the loop runs → it stops
It's a **stdio** server, so an MCP client must launch it. [`src/mcp_client.py`](../../src/mcp_client.py)
is that client — from `src/`, in a second terminal while the console runs:

```bash
uv run python mcp_client.py "Competitor out of stock on chainsaws in the West" --region West
```

Calling `inject_signal` writes to Cosmos → the console's watcher reacts → the loop runs → it stops
at the gate. An external system, speaking a standard protocol, drove your inventory loop.

> **Coach note:** if a team runs `uv run python mcp_server.py` on its own and presses Enter,
> they get `Invalid JSON: EOF while parsing a value … input_value='\n'`. That's not a bug —
> the server is waiting for a client. Have them **Ctrl+C** and use `mcp_client.py`.

## Interactive planning & the informed gate

The reactive loop produces a **draft**; the planner refines it in natural language before
Expand Down
Loading