Skip to content

Conversation

cbbayburt
Copy link
Contributor

@cbbayburt cbbayburt commented Aug 19, 2025

Allows using an external idP for OAuth 2 authentication with protected resource metadata.

Fixes: https://github.com/SUSE/spacewalk/issues/27633

How to test with Keycloak (insecure!)

  1. Start a Keycloak instance
docker run -d --name keycloak-http -p 8080:8080 \
    -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
    -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
    quay.io/keycloak/keycloak:latest start-dev

Setting up the client

  1. Login to the web console with the admin account
  2. Create a new realm (e.g. uyuni)
  3. Create a new client (e.g. mcp-server-uyuni)
  4. In the "Capabilities" section of the new client, set up the following:
    • Client authentication: Off
    • Standard flow: Checked
  5. Add "*" as "Web Origins" (To enable CORS from any URL)
  6. Add your MCP client's expected callback URI to the "Valid redirect URIs"
    • Depends on the client. For example, if on the same host, gemini-cli's URL would be http://localhost:7777/oauth/callback
  7. After creating the client, go to the "Client Scopes" tab and click on "mcp-server-uyuni-dedicated"
  8. Under the "Scope" tab, turn "Full scope allowed" off
  9. Under the "Mappers" tab, click on "Configure a new mapper" and select "Audience"
  10. Pick a name (e.g. audience-mapper) and select mcp-server-uyuni in the Included Client Audience tab.
  11. Leave the other fields as is and click Save.
  12. Go to "Client scopes" on the left menu bar
  13. Create two client scopes with the following values:
  - Name: `mcp:read`
  - Description: `Execute read-only tools`
  - Type: Default
  - Protocol: OpenID Connect
  - Display on consent screen: On 
  - Consent screen text: `Execute read-only tools`
  - Include in token scope: On

  - Name: `mcp:write`
  - Description: `Execute write-enabled tools`
  - Type: Optional
  - Protocol: OpenID Connect
  - Display on consent screen: On 
  - Consent screen text: `Execute read-only tools`
  - Include in token scope: On
  1. Go back to uyuni-mcp-server client details, to "Client scopes" tab and add mcp:read (Default) and mcp:write (Optional) client scopes.

Setting up a test user

  1. Go to create user page and fill out username and email
  2. Set "Email verified" to On
  3. Remove any entries in the "Required user actions" input
  4. Create the user
  5. Go to "Credentials" tab and create a password for the user (make sure the "Temporary" is off)

Test config for gemini-cli

Gemini doesn't seem to be able to consume protected resource metadata files properly, so we need to add the auth endpoints manually.

.gemini/settings.json:

  "mcpServers": {
    "mcp-server-uyuni": {
      "httpUrl": "http://localhost:8000",
      "description": "Tools to interact with the Uyuni/Multi-Linux Manager/MLM server.",
      "oauth": {
        "enabled": true,
        "clientId": "mcp-server-uyuni",
        "authorizationUrl": "http://<keycloak_url>/realms/<your_realm>/protocol/openid-connect/auth",
        "tokenUrl": "http://<keycloak_url>/realms/<your_realm>/protocol/openid-connect/token"
      }
    }

Testing with MCP inspector

  1. Open MCP inspector web UI
    • Transport Type: Streamable HTTP
    • URL: http://localhost:8000
  2. In "Authentication" dropdown, under "OAuth 2.0 Flow":
    • Client ID mcp-server-uyuni (or whatever is set in Keycloak)
    • Copy the "Redirect URL" and add it as a "Valid redirect URI" in Keycloak
    • Click "Connect"

Testing with VS Code

Add the following to the mcp.json file:

	"servers": {
		"mcp-server-uyuni": {
			"url": "http://localhost:8000",
			"type": "http"
		}
	},

Click "Connect" and follow the instructions for the OAuth flow. When instructed, add the VSCode callback URLs to the Keycloak client "Valid Callback URLs".

AUTH_SERVER = os.environ.get("UYUNI_AUTH_SERVER")

auth_provider = AuthProvider(AUTH_SERVER) if AUTH_SERVER else None
mcp = FastMCP("mcp-server-uyuni", auth=auth_provider)
Copy link
Contributor

Choose a reason for hiding this comment

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

using an auth makes the transport protocol be http?

Copy link
Contributor Author

@cbbayburt cbbayburt Aug 19, 2025

Choose a reason for hiding this comment

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

I think auth parameter does nothing if the transport is set to stdio.

In the final code we should validate these kind of things in the config values and write warnings in the logs when there's conflicting stuff like if there's an IDP specified and also stdio is selected at the same time, etc.

Another example is that it should be either Uyuni user/pass values or external idP.

I'm not planning to mark this PR ready to merge any time soon, so it's only skeleton code.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see it depends on having the UYUNI_MCP_TRANSPORT set to Transport.HTTP.value

mcp.run(transport="streamable-http")
.

I think it would be safer if we remove that environment variable and instead we make the transport be HTTP if and only if there is an AUTH_SERVER. This way, you only use HTTP if you have authentication.

This way, we do not have an MCP server running with HTTP without auth that anyone can use to access the uyuni server.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As long as we make it clear with docs, log entries and comments on the example config, I think we can let it in case people wanna test/develop the server with simple user/pass credentials.

super().__init__(
token_verifier=verifier,
authorization_servers=[AnyHttpUrl(auth_server)],
resource_server_url="http://localhost:8000/mcp", #TODO: Get URL dynamically?
Copy link
Contributor

Choose a reason for hiding this comment

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

you can use host and port properties of the FastMCP Server implementation

https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L162
https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L163

you will have to pass them to the class constructor, or pass the FastMCP object itself.

@jordimassaguerpla
Copy link
Contributor

In case we need to setup different known urls, because of different clients expect differently, we could use a custom route: https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L429 . We might not need that if clients are fixed upstream.

@cbbayburt cbbayburt changed the base branch from fastmcp-v2-update to main September 17, 2025 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants