chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# Running this sample
|
||||
|
||||
Here's how to run the classic HTTP streaming server and client, as well as the MCP streaming server and client using Python.
|
||||
|
||||
### Overview
|
||||
|
||||
- You will set up an MCP server that streams progress notifications to the client as it processes items.
|
||||
- The client will display each notification in real time.
|
||||
- This guide covers prerequisites, setup, running, and troubleshooting.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.9 or newer
|
||||
- The `mcp` Python package (install with `pip install mcp`)
|
||||
|
||||
### Installation & Setup
|
||||
|
||||
1. Clone the repository or download the solution files.
|
||||
|
||||
```pwsh
|
||||
git clone https://github.com/microsoft/mcp-for-beginners
|
||||
```
|
||||
|
||||
1. **Create and activate a virtual environment (recommended):**
|
||||
|
||||
```pwsh
|
||||
python -m venv venv
|
||||
.\venv\Scripts\Activate.ps1 # On Windows
|
||||
# or
|
||||
source venv/bin/activate # On Linux/macOS
|
||||
```
|
||||
|
||||
1. **Install required dependencies:**
|
||||
|
||||
```pwsh
|
||||
pip install "mcp[cli]" fastapi requests
|
||||
```
|
||||
|
||||
### Files
|
||||
|
||||
- **Server:** [server.py](./server.py)
|
||||
- **Client:** [client.py](./client.py)
|
||||
|
||||
### Running the Classic HTTP Streaming Server
|
||||
|
||||
1. Navigate to the solution directory:
|
||||
|
||||
```pwsh
|
||||
cd 03-GettingStarted/06-http-streaming/solution
|
||||
```
|
||||
|
||||
2. Start the classic HTTP streaming server:
|
||||
|
||||
```pwsh
|
||||
python server.py
|
||||
```
|
||||
|
||||
3. The server will start and display:
|
||||
|
||||
```
|
||||
Starting FastAPI server for classic HTTP streaming...
|
||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
### Running the Classic HTTP Streaming Client
|
||||
|
||||
1. Open a new terminal (activate the same virtual environment and directory):
|
||||
|
||||
```pwsh
|
||||
cd 03-GettingStarted/06-http-streaming/solution
|
||||
python client.py
|
||||
```
|
||||
|
||||
2. You should see streamed messages printed sequentially:
|
||||
|
||||
```text
|
||||
Running classic HTTP streaming client...
|
||||
Connecting to http://localhost:8000/stream with message: hello
|
||||
--- Streaming Progress ---
|
||||
Processing file 1/3...
|
||||
Processing file 2/3...
|
||||
Processing file 3/3...
|
||||
Here's the file content: hello
|
||||
--- Stream Ended ---
|
||||
```
|
||||
|
||||
### Running the MCP Streaming Server
|
||||
|
||||
1. Navigate to the solution directory:
|
||||
```pwsh
|
||||
cd 03-GettingStarted/06-http-streaming/solution
|
||||
```
|
||||
2. Start the MCP server with the streamable-http transport:
|
||||
```pwsh
|
||||
python server.py mcp
|
||||
```
|
||||
3. The server will start and display:
|
||||
```
|
||||
Starting MCP server with streamable-http transport...
|
||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
### Running the MCP Streaming Client
|
||||
|
||||
1. Open a new terminal (activate the same virtual environment and directory):
|
||||
```pwsh
|
||||
cd 03-GettingStarted/06-http-streaming/solution
|
||||
python client.py mcp
|
||||
```
|
||||
2. You should see notifications printed in real time as the server processes each item:
|
||||
```
|
||||
Running MCP client...
|
||||
Starting client...
|
||||
Session ID before init: None
|
||||
Session ID after init: a30ab7fca9c84f5fa8f5c54fe56c9612
|
||||
Session initialized, ready to call tools.
|
||||
Received message: root=LoggingMessageNotification(...)
|
||||
NOTIFICATION: root=LoggingMessageNotification(...)
|
||||
...
|
||||
Tool result: meta=None content=[TextContent(type='text', text='Processed files: file_1.txt, file_2.txt, file_3.txt | Message: hello from client')]
|
||||
```
|
||||
|
||||
### Key Implementation Steps
|
||||
|
||||
1. **Create the MCP server using FastMCP.**
|
||||
2. **Define a tool that processes a list and sends notifications using `ctx.info()` or `ctx.log()`.**
|
||||
3. **Run the server with `transport="streamable-http"`.**
|
||||
4. **Implement a client with a message handler to display notifications as they arrive.**
|
||||
|
||||
### Code Walkthrough
|
||||
- The server uses async functions and the MCP context to send progress updates.
|
||||
- The client implements an async message handler to print notifications and the final result.
|
||||
|
||||
### Tips & Troubleshooting
|
||||
|
||||
- Use `async/await` for non-blocking operations.
|
||||
- Always handle exceptions in both server and client for robustness.
|
||||
- Test with multiple clients to observe real-time updates.
|
||||
- If you encounter errors, check your Python version and ensure all dependencies are installed.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# client.py
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp import ClientSession
|
||||
import asyncio
|
||||
import mcp.types as types
|
||||
from mcp.shared.session import RequestResponder
|
||||
import requests
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger('mcp_client')
|
||||
|
||||
class LoggingCollector:
|
||||
def __init__(self):
|
||||
self.log_messages: list[types.LoggingMessageNotificationParams] = []
|
||||
async def __call__(self, params: types.LoggingMessageNotificationParams) -> None:
|
||||
self.log_messages.append(params)
|
||||
logger.info("MCP Log: %s - %s", params.level, params.data)
|
||||
|
||||
logging_collector = LoggingCollector()
|
||||
port = 8000
|
||||
|
||||
async def message_handler(
|
||||
message: RequestResponder[types.ServerRequest, types.ClientResult]
|
||||
| types.ServerNotification
|
||||
| Exception,
|
||||
) -> None:
|
||||
logger.info("Received message: %s", message)
|
||||
if isinstance(message, Exception):
|
||||
logger.error("Exception received!")
|
||||
raise message
|
||||
elif isinstance(message, types.ServerNotification):
|
||||
logger.info("NOTIFICATION: %s", message)
|
||||
elif isinstance(message, RequestResponder):
|
||||
logger.info("REQUEST_RESPONDER: %s", message)
|
||||
else:
|
||||
logger.info("SERVER_MESSAGE: %s", message)
|
||||
|
||||
async def main():
|
||||
logger.info("Starting client...")
|
||||
async with streamablehttp_client(f"http://localhost:{port}/mcp") as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
session_callback,
|
||||
):
|
||||
async with ClientSession(
|
||||
read_stream,
|
||||
write_stream,
|
||||
logging_callback=logging_collector,
|
||||
message_handler=message_handler,
|
||||
) as session:
|
||||
id_before = session_callback()
|
||||
logger.info("Session ID before init: %s", id_before)
|
||||
await session.initialize()
|
||||
id_after = session_callback()
|
||||
logger.info("Session ID after init: %s", id_after)
|
||||
logger.info("Session initialized, ready to call tools.")
|
||||
tool_result = await session.call_tool("process_files", {"message": "hello from client"})
|
||||
logger.info("Tool result: %s", tool_result)
|
||||
if logging_collector.log_messages:
|
||||
logger.info("Collected log messages:")
|
||||
for log in logging_collector.log_messages:
|
||||
logger.info("Log: %s", log)
|
||||
|
||||
def stream_progress(message="hello", url="http://localhost:8000/stream"):
|
||||
params = {"message": message}
|
||||
logger.info("Connecting to %s with message: %s", url, message)
|
||||
try:
|
||||
with requests.get(url, params=params, stream=True, timeout=10) as r:
|
||||
r.raise_for_status()
|
||||
logger.info("--- Streaming Progress ---")
|
||||
for line in r.iter_lines():
|
||||
if line:
|
||||
# Still print the streamed content to stdout for visibility
|
||||
decoded_line = line.decode().strip()
|
||||
print(decoded_line)
|
||||
logger.debug("Stream content: %s", decoded_line)
|
||||
logger.info("--- Stream Ended ---")
|
||||
except requests.RequestException as e:
|
||||
logger.error("Error during streaming: %s", e)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "mcp":
|
||||
# MCP client mode
|
||||
logger.info("Running MCP client...")
|
||||
asyncio.run(main())
|
||||
else:
|
||||
# Classic HTTP streaming client mode
|
||||
logger.info("Running classic HTTP streaming client...")
|
||||
stream_progress()
|
||||
|
||||
# Don't run both by default, let the user choose the mode
|
||||
@@ -0,0 +1,53 @@
|
||||
# server.py
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse, HTMLResponse
|
||||
from mcp.server.fastmcp import FastMCP, Context
|
||||
from mcp.types import (
|
||||
TextContent
|
||||
)
|
||||
import asyncio
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
# Create an MCP server
|
||||
mcp = FastMCP("Streamable DEMO")
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
html_path = os.path.join(os.path.dirname(__file__), "welcome.html")
|
||||
with open(html_path, "r", encoding="utf-8") as f:
|
||||
html_content = f.read()
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
async def event_stream(message: str):
|
||||
for i in range(1, 4):
|
||||
yield f"Processing file {i}/3...\n"
|
||||
await asyncio.sleep(1)
|
||||
yield f"Here's the file content: {message}\n"
|
||||
|
||||
@app.get("/stream")
|
||||
async def stream(message: str = "hello"):
|
||||
return StreamingResponse(event_stream(message), media_type="text/plain")
|
||||
|
||||
@mcp.tool(description="A tool that simulates file processing and sends progress notifications")
|
||||
async def process_files(message: str, ctx: Context) -> TextContent:
|
||||
files = [f"file_{i}.txt" for i in range(1, 4)]
|
||||
for idx, file in enumerate(files, 1):
|
||||
await ctx.info(f"Processing {file} ({idx}/{len(files)})...")
|
||||
await asyncio.sleep(1)
|
||||
await ctx.info("All files processed!")
|
||||
return TextContent(type="text", text=f"Processed files: {', '.join(files)} | Message: {message}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if "mcp" in sys.argv:
|
||||
# Configure MCP server with streamable-http transport
|
||||
print("Starting MCP server with streamable-http transport...")
|
||||
# MCP server will create its own FastAPI app with the /mcp endpoint
|
||||
mcp.run(transport="streamable-http")
|
||||
else:
|
||||
# Start FastAPI app for classic HTTP streaming
|
||||
print("Starting FastAPI server for classic HTTP streaming...")
|
||||
uvicorn.run("server:app", host="127.0.0.1", port=8000, reload=True)
|
||||
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>HTTP Streaming Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.description {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #3498db;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.endpoints {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.endpoint-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
width: calc(50% - 30px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
.endpoint-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.endpoint-card h2 {
|
||||
margin-top: 0;
|
||||
color: #3498db;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
.code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
margin: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.endpoint-card {
|
||||
width: 100%;
|
||||
}
|
||||
.endpoints {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>HTTP Streaming Demo</h1>
|
||||
|
||||
<div class="description">
|
||||
<p>This demo showcases two different approaches to HTTP streaming:</p>
|
||||
<ul>
|
||||
<li>Classic HTTP Streaming: Using chunked transfer encoding</li>
|
||||
<li>MCP Streaming: Using structured notifications with JSON-RPC</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="endpoints">
|
||||
<div class="endpoint-card">
|
||||
<h2>Classic HTTP Streaming</h2>
|
||||
<p>Simple chunked transfer encoding to send data in chunks with plain text format.</p>
|
||||
<p>Perfect for simple streaming needs like progress updates.</p>
|
||||
<div class="code">GET /stream</div>
|
||||
<a href="/stream" class="btn">Try it now</a>
|
||||
</div>
|
||||
|
||||
<div class="endpoint-card">
|
||||
<h2>MCP Streaming</h2>
|
||||
<p>Structured notification system with rich metadata and JSON-RPC protocol.</p>
|
||||
<p>Ideal for complex applications requiring detailed progress information.</p>
|
||||
<div class="code">Connect to /mcp with an MCP client</div>
|
||||
<div class="code">python client.py mcp</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Model Context Protocol (MCP) for Beginners © 2025</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user