chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# Run sample
|
||||
|
||||
## Create environment
|
||||
|
||||
```sh
|
||||
python -m venv venv
|
||||
source ./venv/bin/activate
|
||||
```
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```sh
|
||||
pip install "mcp[cli]" dotenv PyJWT requeests
|
||||
```
|
||||
|
||||
## Generate token
|
||||
|
||||
You will need to generate a token that the client will use to talk to the server.
|
||||
|
||||
Call:
|
||||
|
||||
```sh
|
||||
python util.py
|
||||
```
|
||||
|
||||
## Run code
|
||||
|
||||
Run the code with:
|
||||
|
||||
```sh
|
||||
python server.py
|
||||
```
|
||||
|
||||
In a separate terminal, type:
|
||||
|
||||
```sh
|
||||
python client.py
|
||||
```
|
||||
|
||||
In the server terminal, you should see something like:
|
||||
|
||||
```text
|
||||
Valid token, proceeding...
|
||||
User exists, proceeding...
|
||||
User has required scope, proceeding...
|
||||
```
|
||||
|
||||
In the client window, you should text similar to:
|
||||
|
||||
```text
|
||||
Tool result: meta=None content=[TextContent(type='text', text='{\n "current_time": "2025-10-06T17:37:39.847457",\n "timezone": "UTC",\n "timestamp": 1759772259.847457,\n "formatted": "2025-10-06 17:37:39"\n}', annotations=None, meta=None)] structuredContent={'current_time': '2025-10-06T17:37:39.847457', 'timezone': 'UTC', 'timestamp': 1759772259.847457, 'formatted': '2025-10-06 17:37:39'} isError=False
|
||||
```
|
||||
|
||||
This means it's all working
|
||||
|
||||
### Change the info, to see it failing
|
||||
|
||||
Locate this code in *server.py*:
|
||||
|
||||
```python
|
||||
if not has_scope(has_header, "Admin.Write"):
|
||||
```
|
||||
|
||||
Change it to so it says "User.Write". Your current token doesn't have that permission level, so if you restart the server and try to run the client once more you should see an error similar to the following in the server terminal:
|
||||
|
||||
```text
|
||||
Valid token, proceeding...
|
||||
User exists, proceeding...
|
||||
-> Missing required scope!
|
||||
```
|
||||
|
||||
You can either change back your server code or generate a new token that contains this additional scope, up to you.
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
token = os.getenv("TOKEN")
|
||||
if not token:
|
||||
print("TOKEN not found in .env file, run util.py to generate one.")
|
||||
raise ValueError("TOKEN not found in .env file")
|
||||
|
||||
# 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": f"Bearer {token}"}
|
||||
) 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,134 @@
|
||||
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
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from util import validate_token
|
||||
load_dotenv()
|
||||
|
||||
settings = {
|
||||
"host": "localhost",
|
||||
"port": 8000,
|
||||
"mcp_scope": "mcp:read",
|
||||
"server_url": AnyHttpUrl("http://localhost:8000"),
|
||||
}
|
||||
|
||||
users = ["User Userson", "Admin Adminson"]
|
||||
|
||||
def is_user(token: str) -> bool:
|
||||
decodedToken = validate_token(token[7:])
|
||||
if not decodedToken:
|
||||
return False
|
||||
return decodedToken["name"] in users
|
||||
|
||||
def has_scope(token: str, scope: str) -> bool:
|
||||
token = token[7:]
|
||||
token = validate_token(token)
|
||||
|
||||
if not token:
|
||||
return False
|
||||
# very naive scope check, in real life parse the token and check scopes properly
|
||||
return scope in token["scopes"]
|
||||
|
||||
def validate_jwt(token: str) -> bool:
|
||||
token = token[7:]
|
||||
# print("Validating token:", token)
|
||||
return validate_token(token) != None
|
||||
|
||||
|
||||
class CustomHeaderMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
|
||||
has_header = request.headers.get("Authorization")
|
||||
# print("Authorization header:", has_header)
|
||||
if not has_header:
|
||||
print("-> Missing Authorization header!")
|
||||
return Response(status_code=401, content="Unauthorized")
|
||||
|
||||
if not validate_jwt(has_header):
|
||||
print("-> Invalid token!")
|
||||
return Response(status_code=403, content="Forbidden")
|
||||
|
||||
print("Valid token, proceeding...")
|
||||
|
||||
if not is_user(has_header):
|
||||
print("-> User does not exist!")
|
||||
return Response(status_code=403, content="Forbidden - user does not exist")
|
||||
print("User exists, proceeding...")
|
||||
|
||||
if not has_scope(has_header, "Admin.Write"):
|
||||
print("-> Missing required scope!")
|
||||
return Response(status_code=403, content="Forbidden - insufficient scopes")
|
||||
|
||||
print("User has required scope, 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,51 @@
|
||||
# pip install PyJWT
|
||||
|
||||
# create a token
|
||||
import jwt
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||
import datetime
|
||||
|
||||
# Secret key used to sign the JWT
|
||||
secret_key = 'your-secret-key'
|
||||
|
||||
def generate_token():
|
||||
header = {
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
# the user info andits claims and expiry time
|
||||
payload = {
|
||||
"sub": "1234567890", # Subject (user ID)
|
||||
"name": "User Userson", # Custom claim
|
||||
"admin": True, # Custom claim
|
||||
"iat": datetime.datetime.utcnow(),# Issued at
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1), # Expiry
|
||||
"scopes": ["Admin.Write", "User.Read"] # Custom claim for scopes/permissions
|
||||
}
|
||||
|
||||
# encode it
|
||||
encoded_jwt = jwt.encode(payload, secret_key, algorithm="HS256", headers=header)
|
||||
print("Encoded JWT:", encoded_jwt)
|
||||
return encoded_jwt
|
||||
|
||||
def validate_token(token: str) -> str | None:
|
||||
try:
|
||||
decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
|
||||
# print("✅ Token is valid.")
|
||||
# print("Decoded claims:")
|
||||
# for key, value in decoded.items():
|
||||
# print(f" {key}: {value}")
|
||||
return decoded
|
||||
except ExpiredSignatureError:
|
||||
print("❌ Token has expired.")
|
||||
except InvalidTokenError as e:
|
||||
print(f"❌ Invalid token: {e}")
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
token = generate_token()
|
||||
# write to .env file
|
||||
with open(".env", "w") as f:
|
||||
f.write(f"TOKEN={token}")
|
||||
print(token)
|
||||
# validate_token(token)
|
||||
Reference in New Issue
Block a user