Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 阿里云百炼(OpenAI 兼容)配置
OPENAI_API_KEY=your_api_key_here
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
OPENAI_MODEL=qwen3.5-flash
OPENAI_MODEL=qwen-plus

# 选择当前演示使用的 MCP 协议:stdio / sse / streamable_http
MCP_TRANSPORT=stdio
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Deploy site to GitHub Pages

on:
push:
branches: [master]
paths:
- 'site/**'
- 'docs/*.puml'
- '.github/workflows/deploy-pages.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# Only one deployment at a time.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
with:
enablement: true # auto-enable Pages for Actions deployment if needed
- name: Static site lives in site/, copy it into the Pages artifact root
run: |
mkdir -p _pages
cp -r site/* _pages/
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: _pages
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
41 changes: 41 additions & 0 deletions .github/workflows/test-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Test site with Playwright

on:
push:
branches: [master]
paths:
- 'site/**'
- 'tests/**'
- '.github/workflows/test-pages.yml'
pull_request:
branches: [master]
paths:
- 'site/**'
- 'tests/**'

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install deps
working-directory: tests
run: npm install --no-audit --no-fund
- name: Install Playwright browsers
working-directory: tests
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
working-directory: tests
env:
CI: 'true'
run: npx playwright test
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: tests/playwright-report/
retention-days: 14
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ __pycache__/
.env.*
!.env.example
AGENTS.md
.zcode/
149 changes: 61 additions & 88 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
# MCP Minimal Interactive Demo

[English](./README.md) | [中文](./README_zh-CN.md)
[中文](./README_zh-CN.md) | English | [🏠 Live Tutorial Site](https://albert-pzy.github.io/mcp-tutorial/)

Start the program and chat directly in terminal.
A minimal MCP (Model Context Protocol) teaching demo: an LLM automatically picks and calls
FastMCP calculator tools (`add/subtract/multiply/divide`) through the standard MCP protocol,
and returns a natural-language answer.

Current structure (client/server split):
> This repo is deliberately small and readable. For a richer visual walkthrough
> (auto-rendered PlantUML diagrams + syntax-highlighted code) open the **Live Tutorial Site**.

## What you will learn

- How an MCP client and server talk via `initialize → tools/list → tools/call`.
- The three transport options: `stdio`, `sse`, `streamable_http`.
- How Function Calling (LLM decision) and MCP (tool transport) cooperate.

## What this demo is **not**

- Not a production agent. No streaming, no async error retry, no multi-turn memory.
- Not a tool catalog. It only ships 4 trivially-simple calculator tools on purpose.

## Project structure

```text
mcp-tutorial/
|-- main.py # Program entry (interactive client)
|-- config.py # Config schema and .env loading
|-- main.py # Entry: interactive chat loop (client)
|-- config.py # AppConfig schema + .env loading
|-- client/
| |-- runtime.py # MCP client transport routing (stdio/sse/streamable_http)
| `-- llm.py # OpenAI init + tool-calling loop
| `-- llm.py # OpenAI init + tool-discovery + LLM tool-calling loop
`-- server/
|-- app.py # MCP server and tool definitions
|-- app.py # FastMCP server + four @mcp.tool calculators
|-- runtime.py # MCP server transport routing
|-- stdio.py # stdio server entry
|-- sse.py # sse server entry
Expand All @@ -29,6 +45,8 @@ uv sync

## 2. Configure `.env`

Copy `.env.example` to `.env` and set your own key:

```env
OPENAI_API_KEY=your_api_key
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
Expand All @@ -42,33 +60,29 @@ MCP_STREAMABLE_PATH=/mcp
LLM_MAX_TOOL_ROUNDS=3
```

This demo uses Alibaba Cloud Bailian's OpenAI-compatible endpoint by default;
any OpenAI-compatible provider works.

## 3. Run

### 3.1 stdio (simplest)
### 3.1 stdio (simplest, one terminal)

```bash
uv run python main.py
```

Then type directly:
Then just chat:

```text
You: help me calculate 1+2
Assistant: 1 + 2 = 3
你:help me calculate 1 + 2
助手:1 + 2 = 3
```

Type `exit` to quit.

### 3.2 sse (two terminals)

Update `.env` first:

```env
MCP_TRANSPORT=sse
MCP_HOST=127.0.0.1
MCP_PORT=8000
MCP_SSE_PATH=/sse
```
In `.env` set `MCP_TRANSPORT=sse`.

Terminal A (server):

Expand All @@ -82,18 +96,9 @@ Terminal B (client):
uv run python main.py
```

Type `exit` in terminal B to quit client, and `Ctrl+C` in terminal A to stop server.

### 3.3 streamable_http (two terminals)

Update `.env` first:

```env
MCP_TRANSPORT=streamable_http
MCP_HOST=127.0.0.1
MCP_PORT=8000
MCP_STREAMABLE_PATH=/mcp
```
In `.env` set `MCP_TRANSPORT=streamable_http`.

Terminal A (server):

Expand All @@ -107,73 +112,41 @@ Terminal B (client):
uv run python main.py
```

Type `exit` in terminal B to quit client, and `Ctrl+C` in terminal A to stop server.

## 4. MCP Basics for Beginners

### 4.1 What is MCP?

MCP (Model Context Protocol) is a standard way for models to connect to external tools.

Without MCP, each tool integration often needs custom glue code.
With MCP, both sides follow the same protocol, so integration becomes consistent.

In one line: MCP is a standard interface for models to discover and use tools/resources/prompts.
## 4. Three transports at a glance

### 4.2 Three transport options in this demo
| Aspect | `stdio` | `sse` | `streamable_http` |
|---|---|---|---|
| Connection | Local process pipe | HTTP + Server-Sent Events | HTTP request/response |
| Startup | One command | server + client | server + client |
| Typical use | Local dev / teaching | LAN / remote service | Web-style deployment |
| Debug feel | Most direct | Need network reachability | Need network reachability |

1. `stdio`
Communication through standard input/output, usually local subprocess mode.
Quick pick:
- Fastest path → `stdio`.
- "Client talks to a remote service" → `sse` or `streamable_http`.
- Multi-user deployment → HTTP form (`sse` / `streamable_http`).

2. `sse`
HTTP + Server-Sent Events. Good for long-running remote server mode.
The protocol is the same MCP in all three cases; only how bytes get from
client to server changes.

3. `streamable_http`
HTTP-based MCP endpoint, suitable for web-style deployment.
## 5. Diagrams (PlantUML source)

The protocol is still MCP in all cases.
Only the transport changes.

### 4.3 MCP vs Function Calling

1. Function Calling
A model API feature where the model decides which function to call.

2. MCP
A protocol layer that standardizes model-to-tool communication across environments.

In this project, LLM decides tool calls, then actual tool execution happens via MCP client/server.

### 4.4 What is JSON-RPC 2.0 in MCP?

MCP messages are structured with JSON-RPC style request/response:

```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "calculator_add",
"arguments": {"a": 1, "b": 2}
}
}
```
Repo-kept PlantUML sources live in `docs/`:

### 4.5 Core MCP workflow in this demo
- [`docs/architecture.puml`](./docs/architecture.puml) — three-layer architecture
- [`docs/call_sequence.puml`](./docs/call_sequence.puml) — full call sequence (`1+2`)
- [`docs/process_flow.puml`](./docs/process_flow.puml) — client process flow
- [`docs/transport_comparison.puml`](./docs/transport_comparison.puml) — three transports side by side
- [`docs/mcp_vs_function_calling.puml`](./docs/mcp_vs_function_calling.puml) — MCP vs Function Calling

1. User enters a request in `main.py`.
2. Client calls `tools/list` to discover MCP tools.
3. Client sends tool schemas to LLM.
4. LLM returns `tool_calls`.
5. Client executes selected tool via `tools/call`.
6. Tool result is fed back to LLM.
7. LLM generates the final natural-language answer.
The Live Tutorial Site renders these in-browser; see `site/README.md` if you prefer local preview.

Workflow diagram:
## 6. Further reading

<img src="./docs/process_flow.png" alt="MCP process flow" width="780" />
- [MCP complete call flow (EN)](./docs/mcp_complete_call_flow.md) — step-by-step from discovery to final answer, with real captured JSON-RPC payloads.

## Extra docs
## 7. Constraints (see `AGENTS.md`)

- [MCP complete call flow (EN)](./docs/mcp_complete_call_flow.md)
- Python deps via `uv` only; no `pip install`.
- Encrypt secrets: `.env` is git-ignored; `.env.example` uses placeholders only.
- No real `OPENAI_API_KEY` / token / secret in any committed file.
Loading
Loading