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())
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Run sample
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Start server
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
this should start the server
|
||||
|
||||
### Start client
|
||||
|
||||
In a separate terminal, run:
|
||||
|
||||
```bash
|
||||
node run client
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"auth": "./build/server.js"
|
||||
},
|
||||
"scripts": {
|
||||
"client": "node build/client.js",
|
||||
"start": "node build/server.js",
|
||||
"build": "tsc",
|
||||
"inspector": "npx @modelcontextprotocol/inspector node build/index.js"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": ">=1.26.0",
|
||||
"type": "^2.7.3",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
// TODO add secret to header
|
||||
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StreamableHTTPClientTransport, StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
|
||||
|
||||
|
||||
let sessionId: string | undefined = undefined;
|
||||
|
||||
let options: StreamableHTTPClientTransportOptions = {
|
||||
sessionId: sessionId,
|
||||
requestInit: {
|
||||
headers: {
|
||||
"Authorization": "secret123"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const serverUrl = "http://localhost:8000/mcp";
|
||||
|
||||
async function main() {
|
||||
const transport = new StreamableHTTPClientTransport(
|
||||
new URL(serverUrl),
|
||||
options
|
||||
);
|
||||
|
||||
const client = new Client({
|
||||
name: "example-client",
|
||||
version: "1.0.0"
|
||||
});
|
||||
|
||||
await client.connect(transport);
|
||||
sessionId = transport.sessionId;
|
||||
console.log("Connected to MCP server with session ID:", sessionId);
|
||||
|
||||
let toolsResult = await client.listTools();
|
||||
|
||||
console.log("Available tools:", toolsResult);
|
||||
console.log("Client disconnected.");
|
||||
transport.close();
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log("Client is ready to use.");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error initializing client:", error);
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
|
||||
function isValid(secret) {
|
||||
return secret === "secret123";
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
console.log('Request received:', req.method, req.url, req.headers);
|
||||
console.log('Headers:', req.headers["authorization"]);
|
||||
if(!req.headers["authorization"]) {
|
||||
res.status(401).send('Unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
let token = req.headers["authorization"];
|
||||
|
||||
if(!isValid(token)) {
|
||||
res.status(403).send('Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Middleware executed');
|
||||
next();
|
||||
});
|
||||
|
||||
app.post('/mcp', (req, res) => {
|
||||
res.send('MCP endpoint reached');
|
||||
});
|
||||
|
||||
app.listen(8000, () => {
|
||||
console.log('Server is running on http://localhost:8000');
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user