Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ build/
dist/
wheels/
*.egg-info
# tests generated files
.coverage
assisted-service-mcp.log

# Virtual environments
.venv
Expand Down
137 changes: 107 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,90 @@ MCP server for interacting with the OpenShift assisted installer API.

Diagnose cluster failures and find out how to fix them.

Try it out:
## Quick Start

1. Clone the repo:
```
git clone git@github.com:openshift-assisted/assisted-service-mcp.git
### Option 1: Simple Token Setup

1. **Get your OpenShift API token** from https://cloud.redhat.com/openshift/token

2. **Clone and run**:
```bash
git clone git@github.com:openshift-assisted/assisted-service-mcp.git
cd assisted-service-mcp
OFFLINE_TOKEN=<your token> uv run python -m assisted_service_mcp.src.main
```

3. **Configure your MCP client** (Cursor/Copilot):
```json
{
"assisted-service-mcp": {
"transport": "streamable-http",
"url": "http://127.0.0.1:8000/mcp"
}
}
```

### Option 2: OAuth Authentication (Advanced)

For automatic token management with Red Hat SSO:

1. **Clone the repo**:
```bash
git clone git@github.com:openshift-assisted/assisted-service-mcp.git
cd assisted-service-mcp
```

2. **Start the OAuth-enabled server**:
```bash
./start-oauth-server.sh
```

3. **Configure your MCP client** (Cursor/Copilot):
```json
{
"assisted-service-mcp": {
"transport": "streamable-http",
"url": "http://127.0.0.1:8000/mcp"
}
}
```

4. **Connect and authenticate**: When you connect from Cursor, a browser will open automatically for Red Hat SSO authentication.

**For detailed OAuth setup instructions, see [OAUTH_SETUP.md](doc/OAUTH_SETUP.md)**

### Option 3: OCM-Offline-Token Header
#### Note: this option is available only when OAuth is disabled

1. **Get your OpenShift API token** from https://cloud.redhat.com/openshift/token

2. **Clone and run**:
```bash
git clone git@github.com:openshift-assisted/assisted-service-mcp.git
cd assisted-service-mcp
uv run python -m assisted_service_mcp.src.main
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

```json
"mcpServers": {
"assisted": {
"transport": "streamable-http",
"url": "http://127.0.0.1:8000/mcp",
"headers": {
"OCM-Offline-Token": "<offline token>"
}
}
}
```

2. Get your OpenShift API token from https://cloud.redhat.com/openshift/token
## Advanced Transport Options

The recommended transport is streamable-http as shown in the examples above.
Other transport methods or detailed configuration:

3. The server is started and configured differently depending on what transport you want to use
**Configure the server** depending on your preferred transport:

For STDIO:
#### STDIO Transport

In VSCode for example:
```json
Expand All @@ -32,44 +104,49 @@ In VSCode for example:
"/path/to/assisted-service-mcp/assisted_service_mcp/src/main.py"
],
"env": {
"OFFLINE_TOKEN": <your token>
"OFFLINE_TOKEN": "<your token>"
}
}
}
}
```

For SSE (recommended):
#### Server-Sent Events (SSE) Transport (Alternative)
#### Note: SSE is supported for backward compatibility, Streamable HTTP is the recommended transport
Start the server with SSE transport:

Start the server in a terminal:
`OFFLINE_TOKEN=<your token> TRANSPORT=sse uv run python -m assisted_service_mcp.src.main`

`OFFLINE_TOKEN=<your token> uv run assisted_service_mcp.src.main`

Configure the server in the client:
Configure the client:

```json
"assisted-sse": {
"transport": "sse",
"url": "http://localhost:8000/sse"
}
{
"assisted-sse": {
"transport": "sse",
"url": "http://127.0.0.1:8000/sse"
}
}
```

### Providing the Offline Token via Request Header
## Authentication Methods

If you do not set the `OFFLINE_TOKEN` environment variable, you can provide the token as a request header.
When configuring your MCP client, add the `OCM-Offline-Token` header:
The server supports multiple authentication methods with automatic priority handling:

```json
"assisted-sse": {
"transport": "sse",
"url": "http://localhost:8000/sse",
"headers": {
"OCM-Offline-Token": "<your token>"
}
}
```
1. **Authorization Header** - `Bearer <token>` in request headers
2. **OAuth Flow** (when `OAUTH_ENABLED=true`) - Automatic browser-based authentication
3. **Environment Variable** - `OFFLINE_TOKEN` environment variable
4. **OCM-Offline-Token Header** - `OCM-Offline-Token: <token>` in request headers

### OAuth Benefits (Advanced Users)

**No Manual Token Management** - Tokens are obtained and cached automatically
**Secure PKCE Flow** - Enhanced OAuth security with Proof Key for Code Exchange
**Automatic Token Refresh** - Expired tokens are refreshed transparently using refresh tokens
**Multi-Client Support** - Different MCP clients can authenticate independently

## Usage

4. Ask about your clusters:
Ask about your clusters:
![Example prompt asking about a cluster](images/cluster-prompt-example.png)

## Available Tools
Expand Down
141 changes: 141 additions & 0 deletions assisted_service_mcp/src/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
with appropriate transport protocols.
"""

from typing import Awaitable, Callable

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response

from assisted_service_mcp.src.mcp import AssistedServiceMCPServer
from assisted_service_mcp.src.settings import settings
from assisted_service_mcp.src.logger import log, configure_logging
Expand All @@ -21,3 +27,138 @@
else:
app = server.mcp.sse_app()
log.info("Using SSE transport (stateful)")

# Add OAuth endpoints and middleware if OAuth is enabled
if settings.OAUTH_ENABLED:
from assisted_service_mcp.src.oauth import (
oauth_register_handler,
oauth_callback_handler,
oauth_token_handler,
mcp_oauth_middleware,
)

# Add OAuth middleware to handle authentication during MCP connection
class OAuthMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
return await mcp_oauth_middleware.handle_mcp_request(request, call_next)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

app.add_middleware(OAuthMiddleware)

# OAuth discovery endpoints for better MCP client compatibility

async def oauth_well_known_openid_handler(_request: Request) -> JSONResponse:
"""OAuth discovery endpoint."""
return JSONResponse(
{
"issuer": settings.SELF_URL,
"authorization_endpoint": f"{settings.OAUTH_URL}/protocol/openid-connect/auth",
"token_endpoint": f"{settings.OAUTH_URL}/protocol/openid-connect/token",
"registration_endpoint": f"{settings.SELF_URL}/oauth/register",
"userinfo_endpoint": f"{settings.OAUTH_URL}/protocol/openid-connect/userinfo",
"jwks_uri": f"{settings.OAUTH_URL}/protocol/openid-connect/certs",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid", "profile", "email"],
Copy link
Copy Markdown
Collaborator

@omertuc omertuc Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have registration_endpoint here pointing at our /register endpoint?

Image

Copy link
Copy Markdown
Collaborator

@omertuc omertuc Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do implement metadata discovery, so we should let the MCP client know where our registration endpoint is

This also means we don't have to keep it at /register, we can have it anywhere

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
)

async def mcp_register_handler(_request: Request) -> JSONResponse:
"""MCP registration endpoint."""
return JSONResponse(
{
"name": "AssistedService",
"version": "1.0.0",
"description": "Assisted Service MCP Server with OAuth",
"oauth": {
"authorization_endpoint": f"{settings.OAUTH_URL}/protocol/openid-connect/auth",
"token_endpoint": f"{settings.OAUTH_URL}/protocol/openid-connect/token",
"client_id": settings.OAUTH_CLIENT,
"redirect_uri": f"{settings.SELF_URL}/oauth/callback",
"scopes": ["openid", "profile", "email"],
},
}
)

# Wrapper functions to convert dict responses to JSONResponse for Starlette compatibility
async def wrapped_oauth_register_handler(request: Request) -> JSONResponse:
result = await oauth_register_handler(request)
return JSONResponse(result)

async def wrapped_oauth_token_handler(request: Request) -> JSONResponse:
result = await oauth_token_handler(request)
return JSONResponse(result)

# Use Starlette's add_route method instead of FastAPI's add_api_route
app.add_route("/oauth/register", wrapped_oauth_register_handler, methods=["GET"])
app.add_route(
"/oauth/callback", oauth_callback_handler, methods=["GET"]
) # This one returns Response already
app.add_route("/oauth/token", wrapped_oauth_token_handler, methods=["POST"])

# OAuth discovery endpoints - only the standard routes per MCP spec
app.add_route(
"/.well-known/openid-configuration/mcp",
oauth_well_known_openid_handler,
methods=["GET"],
)
app.add_route(
"/.well-known/openid-configuration",
oauth_well_known_openid_handler,
methods=["GET"],
)

# OAuth status endpoint for polling
async def oauth_status_handler(request: Request) -> JSONResponse:
"""Check OAuth authentication status for a client."""
middleware_instance = mcp_oauth_middleware

client_id = request.query_params.get("client_id")
if not client_id:
return JSONResponse(
{"error": "client_id parameter required"}, status_code=400
)

# Check if client has completed authentication
from assisted_service_mcp.src.oauth import oauth_manager

if oauth_manager.token_store.get_token_by_client(client_id):
return JSONResponse(
{
"status": "authenticated",
"message": "OAuth authentication completed successfully",
}
)

# Check if authentication is in progress
for (
session_id,
session_info,
) in middleware_instance.pending_auth_sessions.items():
if session_info.get("client_id") == client_id:
return JSONResponse(
{
"status": "pending",
"message": "OAuth authentication in progress",
"session_id": session_id,
}
)

return JSONResponse(
{
"status": "not_authenticated",
"message": "No authentication found for this client",
}
)

# MCP registration endpoint
app.add_route("/register", mcp_register_handler, methods=["POST", "GET"])
app.add_route("/oauth/status", oauth_status_handler, methods=["GET"])

log.info(
"OAuth endpoints and discovery registered: /oauth/*, /.well-known/*, /register, /oauth/status"
)
else:
log.info("OAuth is disabled - no OAuth endpoints registered")
Loading