resk-mcp · Documentation

Installation

From an empty directory to a protected MCP endpoint answering authenticated calls. About five minutes.

  • Version 0.1.4
  • Python 3.11+
  • Linux, macOS, WSL

Requirements

Install the package

mkdir my-secure-mcp && cd my-secure-mcp
python3 -m venv .venv
source .venv/bin/activate

pip install mcp-resk

With uv, the same thing:

uv venv
source .venv/bin/activate
uv pip install mcp-resk

Verify that the import name resolves (the distribution is mcp-resk, the module is resk_mcp):

python -c "import resk_mcp; print(resk_mcp.__version__)"
# 0.1.0

Create the JWT secret

The library refuses to load without a signing key: resk_mcp.config raises ValueError("JWT_SECRET is required...") at import time when no secret is configured. Generate a high-entropy value and keep it out of version control:

python -c 'import secrets; print(secrets.token_urlsafe(48))'

Write it to .env next to your server entry point:

# .env
JWT_SECRET="paste-the-generated-value-here"
RATE_LIMIT="100/minute"
MAX_TOKEN_PER_REQUEST="4000"
LOG_LEVEL="INFO"

python-dotenv is loaded by resk_mcp.config, so the .env file is picked up automatically from the working directory. Add .env to .gitignore before your first commit.

Security

A leaked JWT_SECRET lets an attacker mint tokens for any user_id. Rotate it immediately if it is exposed — every previously issued token becomes invalid, which is the desired behaviour.

Write the server

Create main.py. Tools, resources and prompts are registered with the usual decorators; resk-mcp only adds tracking around them.

from dotenv import load_dotenv

load_dotenv()  # before importing resk_mcp, so JWT_SECRET is set

from resk_mcp import SecureMCPServer, create_jwt_token

server = SecureMCPServer(name="my-secure-mcp", title="My Secure MCP")


@server.tool(name="calculator/add")
async def add(a: int, b: int) -> int:
    """Adds two numbers."""
    return a + b


@server.resource(uri="info/version")
async def version() -> dict:
    """Returns the application version."""
    return {"version": "1.0"}


if __name__ == "__main__":
    token = create_jwt_token(user_id="[email protected]")
    print(f"Test token: Bearer {token}")
    server.run_server()          # host/port from settings, default 0.0.0.0:8001

Start it:

python main.py

You should see uvicorn bind on 0.0.0.0:8001 and the tool registration log line. run_server() takes optional overrides (host, port, ssl_keyfile, ssl_certfile) and forwards any remaining keyword arguments to uvicorn.run().

Make a first authenticated call

Export the token printed at startup, then send a JSON-RPC body. Note the method format: tool/<registered-name>, with the tool arguments directly in params.

export TOKEN="paste-the-token"

curl -s http://localhost:8001/mcp_secure \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1,"method":"tool/calculator/add","params":{"a":2,"b":3}}'

Successful response (the id is echoed, the payload is under result):

{"id": 1, "result": 5}

Now remove the header and repeat the call. The server answers with an authentication error rather than executing the tool:

{"id": 1, "error": {"code": -33000, "message": "Invalid or expired token"}}

Note

The secured endpoint expects {id, method, params}, not the full {"jsonrpc":"2.0", ...} envelope: RawMCPRequest declares exactly three fields and rejects methods that do not start with tool/, resource/ or prompt/. Extra fields are ignored, so a standard JSON-RPC body with a correct method still works as long as it also carries id and params.

Enable the dashboard

The dashboard fails closed: /dashboard returns 404 while authentication is disabled. Enable it with a real password:

# .env
DASHBOARD_AUTH_ENABLED="true"
DASHBOARD_USERNAME="admin"
DASHBOARD_PASSWORD="a-long-random-value"   # "admin" is rejected at startup

Restart the server and open http://localhost:8001/dashboard. See the dashboard guide for the endpoints and production caveats.

TLS

For local testing, a self-signed certificate is enough:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -sha256 -days 365 -nodes -subj "/CN=localhost"

Then point the server at it, either through the environment or the call:

# .env
SSL_KEYFILE="./key.pem"
SSL_CERTFILE="./cert.pem"
server.run_server(port=8443)

When both paths are set and the files exist, uvicorn serves HTTPS. In production, prefer terminating TLS at a reverse proxy (nginx, Caddy, Traefik) and keep the Python process on a private interface.

Install from source

Use an editable install when you want to modify the library or run its test suite:

git clone https://github.com/Resk-Security/resk-mcp.git
cd resk-mcp

uv venv
source .venv/bin/activate
uv sync                          # installs the project plus its dev dependency group
uv run pytest

With plain pip, use pip install -e ., then pip install pytest pytest-asyncio pytest-mock flake8 mypy for the checks. Test layout and per-module commands are described in README.dev.md.

The repository ships a config.yaml with every option commented, plus examples/example_server.py — a fuller walkthrough that combines SecureMCPServer, ToolSchemaValidator and MCPSecurityManager.

If you use a custom configuration file, point the library at it before importing:

export RESK_MCP_CONFIG_FILE="/etc/resk-mcp/config.yaml"

Deployment notes

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py config.yaml ./
RUN useradd -r -u 10001 app
USER app
EXPOSE 8001
CMD ["python", "main.py"]

Upgrade and uninstall

pip install --upgrade mcp-resk
pip uninstall mcp-resk

Before upgrading across minor versions, re-read the configuration reference: defaults are stable, but new security checks can reject payloads that previously passed.