Skip to main content
Glama
ndigrazia

mcp-keycloak

by ndigrazia
README.md
# MCP Keycloak Resource Server (`mcp-keycloak`)

This project implements a **Model Context Protocol (MCP)** protected Resource Server secured by an OAuth 2.0 Authorization Server (**Keycloak**) using **Token Introspection (RFC 7662)**. 

The server is built with the Python `mcp` SDK using the `FastMCP` framework, showcasing how to restrict access to MCP tools by requiring a valid Bearer token.

---

## Architecture Overview

```
+--------------------+        1. Request Token         +--------------------+
|                    | ------------------------------> |                    |
|     MCP Client     |                                 |      Keycloak      |
| (e.g., test_client)| <------------------------------ | (Auth Server, :8080)
|                    |         2. Access Token         +--------------------+
+--------------------+                                           ^
          |                                                      |
          | 3. Call Tool (with Bearer Token)                     |
          v                                                      | 4. Introspect
+--------------------+                                           |    Token
|     MCP Server     | ------------------------------------------+
| (Resource, :3000)  | <------------------------------------------
+--------------------+            5. Active: True/False
```

1. **Keycloak (Authorization Server)**: Serves on port `8080`. It handles client credential grants and token introspection. A pre-configured database is included in the project directory (`keycloak_data/`) to make spin-up seamless.
2. **MCP Resource Server (Resource Server)**: Built with `FastMCP`, running on port `3000`. When a client calls a protected tool, the server intercepts the request and validates the `Authorization: Bearer <token>` header against Keycloak's introspection endpoint.
3. **Token Verifier (`IntrospectionTokenVerifier`)**: Implements standard OAuth 2.0 token introspection (`RFC 7662`). It validates token activity, resource/audience restrictions (`aud`), scope limits, and expiration times.

---

## Prerequisites

- **Python**: Version `3.12` or higher (compatible with [`uv`](https://github.com/astral-sh/uv))
- **Docker & Docker Compose**: To run Keycloak

---

## Pre-Configured Keycloak Clients

The embedded Keycloak database is pre-configured with three OAuth 2.0 clients within the `master` realm. The full, exact JSON configuration of these clients is exported and available in the [`keycloak_config.json`](./keycloak_config.json) file.

These clients use the Client Credentials Grant flow (via Service Accounts) and are configured with specific client secrets, scopes, and audience mappers:

### 1. `mcp-client` (Primary Testing Client)
*   **Client ID**: `mcp-client`
*   **Client Secret**: `64gq3p8y3siZKZoKH6X9Bq2oqIPfukBZYMnmyCVCJw1QgjfNzdympB0eaZ1aXtFl0yNUWzGr3S7Gov3gR4kLfe`
*   **Authentication Flow**: Client Credentials (Service Account enabled)
*   **Assigned Scopes**: `mcp:tools`
*   **Audience Mappers**:
    *   `mcp-server`
    *   `http://localhost:3000` (derived resource server URL)

### 2. `mcp-client-2` (Alternative Testing Client)
*   **Client ID**: `mcp-client-2`
*   **Client Secret**: `FebOB24OaG3F4J9dMZPoZ7qhzQvUKQ5HgJ6IbQ6yXQBd5tg88f8tPSEA0aUGK9AuEebxHRBwsPdLuDjQpiLFhR`
*   **Authentication Flow**: Client Credentials (Service Account enabled)
*   **Assigned Scopes**: `mcp:tools`
*   **Audience Mappers**:
    *   `mcp-server`
    *   `http://localhost:3000`

### 3. `mcp-server` (Resource Server Introspection)
*   **Client ID**: `mcp-server`
*   **Client Secret**: `5XflSzJJrvQD8iNz4t7RMI4i9rNvdnLQs5PSo8EMGkfUDKr02SjaPs4fKA9kWNO1G06nhDzTHMeHrnz9H4h5ut`
*   **Authentication Flow**: Client Credentials (Service Account enabled) — used by the Resource Server to authenticate introspection requests.
*   **Assigned Scopes**: `mcp:tools`
*   **Audience Mappers**:
    *   `mcp-server`
    *   `http://localhost:3000`

---

## Installation & Setup

### 1. Start Keycloak

Run the Keycloak container in development mode using the provided `docker-compose.yml`:

```bash
docker compose up -d
```

Keycloak starts on `http://127.0.0.1:8080`. Admin credentials are:
- **Username**: `admin`
- **Password**: `admin`

*Note: The container mounts `./keycloak_data`, preserving the pre-configured realm, clients (`mcp-server`, `mcp-client`, and `mcp-client-2`), and settings.*

### 2. Install Project Dependencies

If you are using `uv`:

```bash
uv sync
```

Alternatively, standard `pip` can be used:

```bash
pip install -e .
```

---

## Running the Server

Start the MCP Resource Server:

```bash
uv run main.py
```

By default, the server starts on `http://localhost:3000` using the `streamable-http` transport (supporting Streamable HTTP MCP communication).

### Server Configuration

Configuration options can be customized via environment variables defined in `config.py`:

| Environment Variable | Default Value | Description |
|----------------------|---------------|-------------|
| `HOST` | `localhost` | MCP Server hostname |
| `PORT` | `3000` | MCP Server port |
| `AUTH_HOST` | `localhost` | Keycloak server host |
| `AUTH_PORT` | `8080` | Keycloak server port |
| `AUTH_REALM` | `master` | Keycloak Realm to use |
| `OAUTH_CLIENT_ID` | `mcp-server` | Client ID the Resource Server uses for Introspection |
| `OAUTH_CLIENT_SECRET`| `5XflSzJJrv...` | Client Secret for Introspection client |
| `MCP_SCOPE` | `mcp:tools` | Scope required to invoke the MCP tools |
| `OAUTH_STRICT` | `false` | Enable strict OAuth validations |
| `TRANSPORT` | `streamable-http` | MCP Transport protocol (`streamable-http` or `sse`) |

---

## Secure MCP Tools Provided

The server registers two secure arithmetic tools:

1. **`add_numbers`**
   - **Arguments**: `a: float`, `b: float`
   - **Operation**: Adds two numbers together.
   - **Output**: Returns JSON containing parameters, sum result, and timestamp.

2. **`multiply_numbers`**
   - **Arguments**: `x: float`, `y: float`
   - **Operation**: Multiplies two numbers.
   - **Output**: Returns JSON containing parameters, product result, and timestamp.

---

## Testing with the Test Client

A CLI-based test client (`test_client.py`) is included to simulate MCP client interactions.

### 1. View Help Options

To see all available CLI flags:

```bash
uv run test_client.py --help
```

### 2. Automatically Fetch Token & List Secure Tools

To retrieve a client token via Keycloak's client credentials flow (using client credentials configured for `mcp-client`) and list the available tools on the MCP server:

```bash
uv run test_client.py --fetch-token
```

To use the alternative `mcp-client-2` client, specify the client credentials flags:

```bash
uv run test_client.py --fetch-token --client-id mcp-client-2 --client-secret FebOB24OaG3F4J9dMZPoZ7qhzQvUKQ5HgJ6IbQ6yXQBd5tg88f8tPSEA0aUGK9AuEebxHRBwsPdLuDjQpiLFhR
```

### 3. Call a Protected Tool

To execute a secure tool using an automatically fetched token:

#### Addition Example:
```bash
uv run test_client.py --fetch-token --call add_numbers --args '{"a": 15.5, "b": 24.5}'
```

#### Multiplication Example:
```bash
uv run test_client.py --fetch-token --call multiply_numbers --args '{"x": 6.0, "y": 7.0}'
```

---

## Technical Details

### Token Validation Rules (`token_verifier.py`)

When an incoming MCP request is received, `IntrospectionTokenVerifier.verify_token(token)` validates:
- **HTTP status**: The introspection request successfully responds with an HTTP `200` status.
- **Activity**: The response JSON includes `"active": true`.
- **Audience (`aud`)**: The audience claim matches the resource server URL (derived dynamically, e.g., `http://localhost:3000/`).
- **Scopes**: Validated to ensure the client has the required `mcp:tools` scope.