49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
162 lines
5.2 KiB
Python
162 lines
5.2 KiB
Python
"""MCP Resource Server with Token Introspection.
|
|
|
|
This server validates tokens via Authorization Server introspection and serves MCP resources.
|
|
Demonstrates RFC 9728 Protected Resource Metadata for AS/RS separation.
|
|
|
|
NOTE: this is a simplified example for demonstration purposes.
|
|
This is not a production-ready implementation.
|
|
"""
|
|
|
|
import datetime
|
|
import logging
|
|
from typing import Any, Literal
|
|
|
|
import click
|
|
from pydantic import AnyHttpUrl
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
from mcp.server.auth.settings import AuthSettings
|
|
from mcp.server.mcpserver.server import MCPServer
|
|
|
|
from .token_verifier import IntrospectionTokenVerifier
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ResourceServerSettings(BaseSettings):
|
|
"""Settings for the MCP Resource Server."""
|
|
|
|
model_config = SettingsConfigDict(env_prefix="MCP_RESOURCE_")
|
|
|
|
# Server settings
|
|
host: str = "localhost"
|
|
port: int = 8001
|
|
server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:8001/mcp")
|
|
|
|
# Authorization Server settings
|
|
auth_server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:9000")
|
|
auth_server_introspection_endpoint: str = "http://localhost:9000/introspect"
|
|
# No user endpoint needed - we get user data from token introspection
|
|
|
|
# MCP settings
|
|
mcp_scope: str = "user"
|
|
|
|
# RFC 8707 resource validation
|
|
oauth_strict: bool = False
|
|
|
|
|
|
def create_resource_server(settings: ResourceServerSettings) -> MCPServer:
|
|
"""Create MCP Resource Server with token introspection.
|
|
|
|
This server:
|
|
1. Provides protected resource metadata (RFC 9728)
|
|
2. Validates tokens via Authorization Server introspection
|
|
3. Serves MCP tools and resources
|
|
"""
|
|
# Create token verifier for introspection with RFC 8707 resource validation
|
|
token_verifier = IntrospectionTokenVerifier(
|
|
introspection_endpoint=settings.auth_server_introspection_endpoint,
|
|
server_url=str(settings.server_url),
|
|
validate_resource=settings.oauth_strict, # Only validate when --oauth-strict is set
|
|
)
|
|
|
|
# Create MCPServer server as a Resource Server
|
|
app = MCPServer(
|
|
name="MCP Resource Server",
|
|
instructions="Resource Server that validates tokens via Authorization Server introspection",
|
|
debug=True,
|
|
# Auth configuration for RS mode
|
|
token_verifier=token_verifier,
|
|
auth=AuthSettings(
|
|
issuer_url=settings.auth_server_url,
|
|
required_scopes=[settings.mcp_scope],
|
|
resource_server_url=settings.server_url,
|
|
),
|
|
)
|
|
# Store settings for later use in run()
|
|
app._resource_server_settings = settings # type: ignore[attr-defined]
|
|
|
|
@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"),
|
|
}
|
|
|
|
return app
|
|
|
|
|
|
@click.command()
|
|
@click.option("--port", default=8001, help="Port to listen on")
|
|
@click.option("--auth-server", default="http://localhost:9000", help="Authorization Server URL")
|
|
@click.option(
|
|
"--transport",
|
|
default="streamable-http",
|
|
type=click.Choice(["sse", "streamable-http"]),
|
|
help="Transport protocol to use ('sse' or 'streamable-http')",
|
|
)
|
|
@click.option(
|
|
"--oauth-strict",
|
|
is_flag=True,
|
|
help="Enable RFC 8707 resource validation",
|
|
)
|
|
def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http"], oauth_strict: bool) -> int:
|
|
"""Run the MCP Resource Server.
|
|
|
|
This server:
|
|
- Provides RFC 9728 Protected Resource Metadata
|
|
- Validates tokens via Authorization Server introspection
|
|
- Serves MCP tools requiring authentication
|
|
|
|
Must be used with a running Authorization Server.
|
|
"""
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
try:
|
|
# Parse auth server URL
|
|
auth_server_url = AnyHttpUrl(auth_server)
|
|
|
|
# Create settings
|
|
host = "localhost"
|
|
server_url = f"http://{host}:{port}/mcp"
|
|
settings = ResourceServerSettings(
|
|
host=host,
|
|
port=port,
|
|
server_url=AnyHttpUrl(server_url),
|
|
auth_server_url=auth_server_url,
|
|
auth_server_introspection_endpoint=f"{auth_server}/introspect",
|
|
oauth_strict=oauth_strict,
|
|
)
|
|
except ValueError as e:
|
|
logger.error(f"Configuration error: {e}")
|
|
logger.error("Make sure to provide a valid Authorization Server URL")
|
|
return 1
|
|
|
|
try:
|
|
mcp_server = create_resource_server(settings)
|
|
|
|
logger.info(f"🚀 MCP Resource Server running on {settings.server_url}")
|
|
logger.info(f"🔑 Using Authorization Server: {settings.auth_server_url}")
|
|
|
|
# Run the server - this should block and keep running
|
|
mcp_server.run(transport=transport, host=host, port=port)
|
|
logger.info("Server stopped")
|
|
return 0
|
|
except Exception:
|
|
logger.exception("Server error")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() # type: ignore[call-arg]
|