diff --git a/.gitignore b/.gitignore index 85cf6e7..1caa95a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ mcp_serve.egg-info build dist +certs env .eggs __pycache__ diff --git a/README.md b/README.md index 13c8b02..9042453 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,43 @@ This example is for basic manifests to work in Kind (or Kubernetes/Openshift). N We will be making a Kubernetes Operator to create this set of stuff soon. +### SSL + +Generate keys + +```bash +mkdir -p ./certs +openssl req -x509 -newkey rsa:4096 -keyout ./certs/key.pem -out ./certs/cert.pem -sha256 -days 365 -nodes -subj '/CN=localhost' +``` + +And start the server, indicating you want to use them. + +```bash +mcpserver start --transport http --port 8089 --ssl-keyfile ./certs/key.pem --ssl-certfile ./certs/cert.pem +``` + +For the client, the way that it works is that httpx discovers the certs via [environment variables](https://github.com/modelcontextprotocol/python-sdk/issues/870#issuecomment-3449911720). E.g., try the test first without them: + +```bash +python3 examples/ssl/test_ssl_client.py +šŸ“” Connecting to https://localhost:8089/mcp... +āŒ Connection failed: Client failed to connect: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1000) +``` + +Now export the envars: + +```bash +export SSL_CERT_DIR=$(pwd)/certs +export SSL_CERT_FILE=$(pwd)/certs/cert.pem +``` +```console +šŸ“” Connecting to https://localhost:8089/mcp... + ⭐ Discovered tool: simple_echo + +āœ… Connection successful! +``` +And you'll see the server get hit. + ### Design Choices Here are a few design choices (subject to change, of course). I am starting with re-implementing our fractale agents with this framework. For that, instead of agents being tied to specific functions (as classes on their agent functions) we will have a flexible agent class that changes function based on a chosen prompt. It will use mcp functions, prompts, and resources. In addition: diff --git a/examples/ssl/test_ssl_client.py b/examples/ssl/test_ssl_client.py new file mode 100644 index 0000000..accfd72 --- /dev/null +++ b/examples/ssl/test_ssl_client.py @@ -0,0 +1,35 @@ +import asyncio +import sys +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + +port = 8089 +if len(sys.argv) > 1: + port = sys.argv[1] + +# Use https for SSL stuff +url = f"https://localhost:{port}/mcp" + +# Create the transport with the verification +transport = StreamableHttpTransport(url=url) +client = Client(transport) + +async def list_tools(): + """ + Connects to the Flux MCP server via SSL and lists discovered tools. + WITH SSL! Oohlala. + """ + print(f"šŸ“” Connecting to {url}...") + async with client: + tools = await client.list_tools() + if not tools: + print(" āš ļø No tools discovered.") + for tool in tools: + print(f" ⭐ Discovered tool: {tool.name}") + print("\nāœ… Connection successful!") + +if __name__ == "__main__": + try: + asyncio.run(list_tools()) + except Exception as e: + print(f"āŒ Connection failed: {e}") \ No newline at end of file diff --git a/mcpserver/cli/args.py b/mcpserver/cli/args.py index 2e89426..ab09204 100644 --- a/mcpserver/cli/args.py +++ b/mcpserver/cli/args.py @@ -41,6 +41,9 @@ def populate_start_args(start): start.add_argument("--path", help="Server path for mcp", default=default_path) start.add_argument("--config", help="Configuration file for server.") + # Args for ssl + start.add_argument("--ssl-keyfile", default=None, help="SSL key file (e.g. key.pem)") + start.add_argument("--ssl-certfile", default=None, help="SSL certificate file (e.g. cert.pem)") start.add_argument( "--mask-error_details", help="Mask error details (for higher security deployments)", diff --git a/mcpserver/cli/manager.py b/mcpserver/cli/manager.py index 025f28f..1b29345 100644 --- a/mcpserver/cli/manager.py +++ b/mcpserver/cli/manager.py @@ -23,6 +23,10 @@ def get_manager(mcp, cfg): for tool in manager.load_tools(mcp, cfg.include, cfg.exclude): print(f" āœ… Registered: {tool.name}") + # Visual to show user we have ssl + if cfg.server.ssl_keyfile is not None and cfg.server.ssl_certfile is not None: + print(f" šŸ” SSL Enabled") + def register(mcp, cfg: MCPConfig): """ diff --git a/mcpserver/cli/start.py b/mcpserver/cli/start.py index 161ade0..3e847a4 100644 --- a/mcpserver/cli/start.py +++ b/mcpserver/cli/start.py @@ -1,5 +1,15 @@ +import warnings + +import uvicorn from fastapi import FastAPI +# Ignore these for now +warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.legacy") +warnings.filterwarnings( + "ignore", category=DeprecationWarning, module="uvicorn.protocols.websockets" +) + + from mcpserver.app import init_mcp from mcpserver.cli.manager import get_manager from mcpserver.core.config import MCPConfig @@ -21,7 +31,7 @@ def main(args, extra, **kwargs): # Get the tool manager and register discovered tools mcp = init_mcp(cfg.exclude, cfg.include, args.mask_error_details) - manager = get_manager(mcp, cfg) + get_manager(mcp, cfg) # Create ASGI app from MCP server mcp_app = mcp.http_app(path=cfg.server.path) @@ -34,7 +44,16 @@ def main(args, extra, **kwargs): # http transports can accept a host and port if "http" in cfg.server.transport: - mcp.run(transport=cfg.server.transport, port=cfg.server.port, host=cfg.server.host) + # mcp.run(transport=cfg.server.transport, port=cfg.server.port, host=cfg.server.host) + uvicorn.run( + app, + host=cfg.server.host, + port=cfg.server.port, + ssl_keyfile=cfg.server.ssl_keyfile, + ssl_certfile=cfg.server.ssl_certfile, + timeout_graceful_shutdown=75, + timeout_keep_alive=60, + ) # stdio does not! else: diff --git a/mcpserver/core/config.py b/mcpserver/core/config.py index 02b9ebf..5bbc483 100644 --- a/mcpserver/core/config.py +++ b/mcpserver/core/config.py @@ -32,6 +32,8 @@ class ServerConfig: """ transport: str = defaults.transport + ssl_keyfile: str = None + ssl_certfile: str = None port: int = int(defaults.port) host: str = defaults.host path: str = defaults.path @@ -90,7 +92,12 @@ def from_args(cls, args): """ return cls( server=ServerConfig( - transport=args.transport, port=args.port, host=args.host, path=args.path + transport=args.transport, + port=args.port, + host=args.host, + path=args.path, + ssl_certfile=args.ssl_certfile, + ssl_keyfile=args.ssl_keyfile, ), include=args.include, exclude=args.exclude, diff --git a/mcpserver/version.py b/mcpserver/version.py index 983fa24..469f3f0 100644 --- a/mcpserver/version.py +++ b/mcpserver/version.py @@ -1,4 +1,4 @@ -__version__ = "0.0.12" +__version__ = "0.0.13" AUTHOR = "Vanessa Sochat" AUTHOR_EMAIL = "vsoch@users.noreply.github.com" NAME = "mcp-serve"