chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# Run sample
|
||||
|
||||
This sample starts an MCP Server with a middleware that checks for a valid Authorization header.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
pip install "mcp[cli]"
|
||||
```
|
||||
|
||||
## Start server
|
||||
|
||||
```bash
|
||||
python server.py
|
||||
```
|
||||
|
||||
start the client in another terminal
|
||||
|
||||
```bash
|
||||
python client.py
|
||||
```
|
||||
|
||||
You should see a result similar to:
|
||||
|
||||
```text
|
||||
2025-09-30 13:25:54 - mcp_client - INFO - Tool result: meta=None content=[TextContent(type='text', text='{\n "current_time": "2025-09-30T13:25:54.311900",\n "timezone": "UTC",\n "timestamp": 1759238754.3119,\n "formatted": "2025-09-30 13:25:54"\n}', annotations=None, meta=None)] structuredContent={'current_time': '2025-09-30T13:25:54.311900', 'timezone': 'UTC', 'timestamp': 1759238754.3119, 'formatted': '2025-09-30 13:25:54'} isError=False
|
||||
```
|
||||
|
||||
this means the credential being sent through is being allowed.
|
||||
|
||||
Try changing the credential in `client.py` to "secret-token2", then you should see this text as part of the response:
|
||||
|
||||
```text
|
||||
2025-09-30 13:27:44 - httpx - INFO - HTTP Request: POST http://localhost:8000/mcp "HTTP/1.1 403 Forbidden"
|
||||
```
|
||||
|
||||
this means you were authenticated (you had a credential), but it was invalid.
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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(
|
||||
url = f"http://localhost:{port}/mcp",
|
||||
headers = {"Authorization": "Bearer secret-token2"}
|
||||
) 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("get_time", {})
|
||||
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
|
||||
|
||||
logger.info("Running MCP client...")
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
# Don't run both by default, let the user choose the mode
|
||||
@@ -0,0 +1,102 @@
|
||||
from pydantic import AnyHttpUrl
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
from mcp.server.fastmcp.server import FastMCP
|
||||
from typing import Any, Literal
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import Response
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
settings = {
|
||||
"host": "localhost",
|
||||
"port": 8000,
|
||||
"auth_server_url": AnyHttpUrl("http://localhost:8001"),
|
||||
"mcp_scope": "mcp:read",
|
||||
"server_url": AnyHttpUrl("http://localhost:8000"),
|
||||
}
|
||||
|
||||
def valid_token(token: str) -> bool:
|
||||
# remove the "Bearer " prefix
|
||||
if token.startswith("Bearer "):
|
||||
token = token[7:]
|
||||
return token == "secret-token"
|
||||
return False
|
||||
|
||||
class CustomHeaderMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
|
||||
has_header = request.headers.get("Authorization")
|
||||
if not has_header:
|
||||
print("-> Missing Authorization header!")
|
||||
return Response(status_code=401, content="Unauthorized")
|
||||
|
||||
if not valid_token(has_header):
|
||||
print("-> Invalid token!")
|
||||
return Response(status_code=403, content="Forbidden")
|
||||
|
||||
print("Valid token, proceeding...")
|
||||
print(f"-> Received {request.method} {request.url}")
|
||||
response = await call_next(request)
|
||||
response.headers['Custom'] = 'Example'
|
||||
return response
|
||||
|
||||
app = FastMCP(
|
||||
name="MCP Resource Server",
|
||||
instructions="Resource Server that validates tokens via Authorization Server introspection",
|
||||
host=settings["host"],
|
||||
port=settings["port"],
|
||||
debug=True
|
||||
)
|
||||
|
||||
@app.tool()
|
||||
async def get_time() -> dict[str, Any]:
|
||||
"""
|
||||
Get the current server time.
|
||||
|
||||
This tool demonstrates that system information can be protected
|
||||
by OAuth authentication. User must be authenticated to access it.
|
||||
"""
|
||||
|
||||
now = datetime.datetime.now()
|
||||
|
||||
return {
|
||||
"current_time": now.isoformat(),
|
||||
"timezone": "UTC", # Simplified for demo
|
||||
"timestamp": now.timestamp(),
|
||||
"formatted": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
async def setup(app) -> None:
|
||||
"""Run the server using StreamableHTTP transport."""
|
||||
|
||||
starlette_app = app.streamable_http_app()
|
||||
return starlette_app
|
||||
|
||||
async def run(starlette_app):
|
||||
import uvicorn
|
||||
config = uvicorn.Config(
|
||||
starlette_app,
|
||||
host=app.settings.host,
|
||||
port=app.settings.port,
|
||||
log_level=app.settings.log_level.lower(),
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
||||
|
||||
async def main():
|
||||
print("Running MCP Resource Server...")
|
||||
starlette_app = await setup(app)
|
||||
print("Adding custom middleware...")
|
||||
starlette_app.add_middleware(CustomHeaderMiddleware)
|
||||
|
||||
await run(starlette_app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user