chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:31:35 +08:00
commit c275ba2868
13613 changed files with 2980806 additions and 0 deletions
@@ -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)