Skip to content

Implement OAuth endpoints#5

Closed
jhernand wants to merge 1 commit intoopenshift-assisted:masterfrom
jhernand:implement_oauth_endpoints
Closed

Implement OAuth endpoints#5
jhernand wants to merge 1 commit intoopenshift-assisted:masterfrom
jhernand:implement_oauth_endpoints

Conversation

@jhernand
Copy link
Copy Markdown
Collaborator

@jhernand jhernand commented Jun 25, 2025

This patch implements enough OAuth endpoints to make the server usable from MCP clients like VScode without having to manually provide a token.

When the MCP client receives a 401 code from the server it will assume that it uses OAuth, and it will try to fetch the OAuth metadata from /.well-known/oauth-authorization-server. As we don't want to filly implement that, we implement it calling the equivalent endpoint of sso.redhat.com. We then modify a couple of things in the content, and return it to the client.

One of the things that we modify is the dynamic client registration endpoint. The MCP client will try to use the one in the metadata, but sso.redhat.com doesn't allow that. So we replace it with our own, where we accept all requests and respond always with the same client identifier.

With this the MCP client is able to complete the OAuth process, and obtain an access token that will then use to call the MCP. The MCP will then in turn use it to call the assisted installer REST API.

Summary by CodeRabbit

  • New Features

    • Added support for OAuth authentication, allowing users to sign in via a browser and use access tokens instead of offline tokens.
    • Introduced new environment variables for configuring OAuth support.
    • Implemented OAuth middleware to handle authorization endpoints and enforce token-based access.
  • Documentation

    • Updated the README with instructions on enabling and configuring OAuth authentication.

@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented Jun 25, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: jhernand

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@jhernand jhernand marked this pull request as draft June 25, 2025 21:40
@openshift-ci openshift-ci Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. labels Jun 25, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 25, 2025

## Walkthrough

The changes introduce OAuth authentication support for the MCP server, including a new middleware to handle OAuth endpoints and enforce authentication. The server is now run as a Starlette application with middleware support, and documentation has been updated to describe the new OAuth configuration options.

## Changes

| File(s)            | Change Summary                                                                                                     |
|--------------------|--------------------------------------------------------------------------------------------------------------------|
| README.md          | Moved "Ask about your clusters" section earlier; added "Authorization Options" detailing four prioritized authentication methods including bearer token, OAuth flow, and offline tokens with environment variable configurations. |
| server.py          | Refactored imports to use `fastmcp` package; replaced direct request context with dependency injection for headers; added conditional OAuth middleware integration based on environment variables; switched server startup to Starlette app with Uvicorn on port 8000. |
| oauth.py           | Added new Starlette middleware class implementing OAuth endpoints (`/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server`, `/oauth/register`), enforcing authorization header presence for other requests and simulating dynamic client registration. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Server (Starlette+MCP)
    participant OAuth Server

    Client->>Server (/.well-known/oauth-authorization-server): Request metadata
    Server->>OAuth Server: Proxy metadata request
    OAuth Server-->>Server: Return metadata
    Server-->>Client: Return modified metadata

    Client->>Server (/oauth/register): Register client
    Server-->>Client: Return fixed client ID

    Client->>Server (other endpoints): API request with/without Authorization header
    alt Authorization header present
        Server->>Server: Allow request to proceed
    else No Authorization header
        Server-->>Client: Return 401 Unauthorized
    end

Possibly related PRs

  • MGMT-20908: Forward authorization #4: Adds OAuth token forwarding and centralizes access token retrieval in server.py, related to token handling but does not include middleware integration.

Suggested labels

lgtm, jira/valid-reference

Poem

🐇
In the warren, OAuth hops in,
Securing the tunnels with a clever spin.
Middleware guards each burrow and door,
Tokens in paw, we bounce and explore.
With Starlette and Uvicorn, the meadow’s anew—
Authentication’s breeze, fresh as dew!


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=openshift-assisted/assisted-service-mcp&utm_content=5):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@jhernand
Copy link
Copy Markdown
Collaborator Author

@eranco74 please take a look. Note that it still needs some work to avoid having the URLs as constants, and to make authentication optional.

@eranco74
Copy link
Copy Markdown
Collaborator

@jhernand this is great!
Please add a flag to enable/disable this flow and let's merge!

@jhernand jhernand force-pushed the implement_oauth_endpoints branch from 7789052 to bdec3d8 Compare June 26, 2025 17:17
@jhernand jhernand marked this pull request as ready for review June 26, 2025 17:17
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 26, 2025
@jhernand
Copy link
Copy Markdown
Collaborator Author

@eranco74 added a OAUTH_ENABLED environment variable set to false by default, as well as two OAUTH_URL and OAUTH_CLIENT environment variables in case it is necessary to change the authorization server.

@jhernand
Copy link
Copy Markdown
Collaborator Author

@eranco74 please don't merge yet, I need to make the registration URL configurable.

@jhernand jhernand marked this pull request as draft June 26, 2025 17:20
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 26, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
README.md (1)

72-87: Improve grammar and punctuation for better readability.

The documentation is helpful, but could benefit from minor grammatical improvements.

Apply this diff to improve readability:

-The server can implement a subset of the OAuth protocol that MCP clients like the one in VScode
-use to authenticate. To enable it set the environment variable `OAUTH_ENABLED` to true. Then when
-you try to connect the MCP client will open a browser where you can provide your credentials, and
+The server can implement a subset of the OAuth protocol that MCP clients like the one in VScode
+use to authenticate. To enable it, set the environment variable `OAUTH_ENABLED` to true. Then, when
+you try to connect, the MCP client will open a browser where you can provide your credentials, and
 the MCP client will request an access token that will then be used by the server to authenticate
 to the assisted installer REST API.

-If you use this mechanism then you don't need to provide the `OFFLINE_TOKEN`.
+If you use this mechanism, then you don't need to provide the `OFFLINE_TOKEN`.

-Additionaly you can configure the OAuth authorization server and client identifier to use with the
+Additionally, you can configure the OAuth authorization server and client identifier to use with the
 `OAUTH_URL` and `OAUTH_CLIENT` environment variables. The defaults are the following:
server.py (4)

311-311: Add blank line before class definition.

     return InventoryClient(get_access_token()).update_host(host_id, infraenv_id, host_role=role).to_str()

+
 class OAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):

423-428: Consider making CORS settings configurable for production security.

The current CORS configuration allows all origins with credentials, which could be a security risk in production environments.

Consider making CORS settings configurable via environment variables:

# Add configuration options
cors_origins = os.getenv("CORS_ORIGINS", "*").split(",")
cors_allow_credentials = os.getenv("CORS_ALLOW_CREDENTIALS", "true").lower() == "true"

# Apply configuration
app.add_middleware(
    starlette.middleware.cors.CORSMiddleware,
    allow_origins=cors_origins,
    allow_methods=["GET", "POST", "OPTIONS"],
    allow_credentials=cors_allow_credentials,
)

400-400: Add blank lines after class definition.

         )

+
 if __name__ == "__main__":

406-406: Consider using proper logging instead of print statement.

Debug print statements should be replaced with proper logging for production code.

+import logging
+
+# At module level
+logger = logging.getLogger(__name__)
+
 # In main block
-    print(f"oauth_enabled: {oauth_enabled}")
+    logger.info(f"OAuth enabled: {oauth_enabled}")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33314d5 and bdec3d8.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • server.py (4 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[uncategorized] ~75-~75: Possible missing comma found.
Context: ...n VScode use to authenticate. To enable it set the environment variable `OAUTH_ENA...

(AI_HYDRA_LEO_MISSING_COMMA)


[uncategorized] ~76-~76: Possible missing comma found.
Context: ..._ENABLED` to true. Then when you try to connect the MCP client will open a browser wher...

(AI_HYDRA_LEO_MISSING_COMMA)


[typographical] ~80-~80: Consider adding a comma.
Context: ...ler REST API. If you use this mechanism then you don't need to provide the `OFFLINE_...

(IF_THEN_COMMA)


[uncategorized] ~81-~81: Possible missing comma found.
Context: ...t need to provide the OFFLINE_TOKEN. Additionaly you can configure the OAuth authorizati...

(AI_HYDRA_LEO_MISSING_COMMA)

🪛 Flake8 (7.2.0)
server.py

[error] 311-311: expected 2 blank lines, found 1

(E302)


[error] 400-400: expected 2 blank lines after class or function definition, found 1

(E305)

🪛 Pylint (3.3.7)
server.py

[error] 364-364: Using variable 'oauth_url' before assignment

(E0601)

🔇 Additional comments (1)
server.py (1)

17-17: Good adaptation to the new FastMCP architecture.

The changes correctly utilize the FastMCP dependency injection pattern for header retrieval and remove the redundant host parameter.

Also applies to: 38-39, 59-64

Comment thread server.py Outdated
Comment thread server.py Outdated
@jhernand jhernand force-pushed the implement_oauth_endpoints branch from bdec3d8 to 023c53b Compare June 26, 2025 18:44
@jhernand jhernand marked this pull request as ready for review June 26, 2025 18:45
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 26, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
README.md (1)

72-94: Documentation looks comprehensive with minor grammar improvements needed.

The OAuth support documentation effectively explains the feature and configuration options. However, there are a few punctuation issues to address for better readability.

Apply these grammar improvements:

-The server can implement a subset of the OAuth protocol that MCP clients like the one in VScode
-use to authenticate. To enable it set the environment variable `OAUTH_ENABLED` to true. Then when
-you try to connect the MCP client will open a browser where you can provide your credentials, and
+The server can implement a subset of the OAuth protocol that MCP clients like the one in VSCode
+use to authenticate. To enable it, set the environment variable `OAUTH_ENABLED` to true. Then, when
+you try to connect, the MCP client will open a browser where you can provide your credentials, and
-If you use this mechanism then you don't need to provide the `OFFLINE_TOKEN`.
+If you use this mechanism, then you don't need to provide the `OFFLINE_TOKEN`.
-Additionaly you can configure the OAuth authorization server and client identifier to use with the
+Additionally, you can configure the OAuth authorization server and client identifier to use with the
server.py (3)

311-311: Fix formatting: Add required blank lines before class definition.

+

 class OAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):

396-402: Consider input validation for OAuth registration endpoint.

The OAuth registration endpoint accepts any JSON input without validation. While this may be intentional for a "fake" endpoint, consider adding basic validation to ensure the request contains expected fields.

     async def oauth_register(self, request: starlette.requests.Request) -> starlette.responses.Response:
         """
         This method implements the OAuth dynamic client registration endpoint. It responds to all requests
         with a fixed client identifier.
         """
-        body = await request.json()
+        try:
+            body = await request.json()
+            if "redirect_uris" not in body:
+                return starlette.responses.JSONResponse(
+                    status_code=400,
+                    content={"error": "redirect_uris is required"}
+                )
+        except Exception:
+            return starlette.responses.JSONResponse(
+                status_code=400,
+                content={"error": "Invalid JSON"}
+            )
         return starlette.responses.JSONResponse(
             content={
                 "client_id": self._oauth_client,
                 "redirect_uris": body["redirect_uris"],
             },
         )

404-404: Fix formatting: Add required blank lines after class definition.

         )

+
 if __name__ == "__main__":
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bdec3d8 and 023c53b.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • server.py (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
server.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🧬 Code Graph Analysis (1)
server.py (1)
service_client/assisted_service_api.py (1)
  • InventoryClient (11-129)
🪛 Flake8 (7.2.0)
server.py

[error] 311-311: expected 2 blank lines, found 1

(E302)


[error] 404-404: expected 2 blank lines after class or function definition, found 1

(E305)

🪛 LanguageTool
README.md

[uncategorized] ~75-~75: A comma might be missing here.
Context: ...n VScode use to authenticate. To enable it set the environment variable `OAUTH_ENA...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)


[uncategorized] ~76-~76: A comma might be missing here.
Context: ..._ENABLED` to true. Then when you try to connect the MCP client will open a browser wher...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)


[typographical] ~80-~80: Consider adding a comma.
Context: ...ler REST API. If you use this mechanism then you don't need to provide the `OFFLINE_...

(IF_THEN_COMMA)


[uncategorized] ~81-~81: Possible missing comma found.
Context: ...t need to provide the OFFLINE_TOKEN. Additionaly you can configure the OAuth authorizati...

(AI_HYDRA_LEO_MISSING_COMMA)

🔇 Additional comments (4)
server.py (4)

3-17: Import changes and FastMCP initialization look good.

The transition from mcp.server.fastmcp to fastmcp and the addition of Starlette/Uvicorn imports align well with the new middleware-based architecture.


38-64: Function refactoring correctly uses new dependency system.

The updates to get_offline_token() and get_access_token() properly use fastmcp.server.dependencies.get_http_headers() instead of direct MCP request context access, which is the correct approach for the new architecture.


405-440: OAuth middleware integration implemented correctly.

The main execution block properly:

  • Creates a Starlette application from the MCP instance
  • Conditionally adds OAuth middleware based on environment variables (aligning with retrieved learnings about using environment variables for URL construction)
  • Configures CORS middleware appropriately
  • Uses Uvicorn for serving

The use of SELF_URL environment variable correctly addresses the learning about reverse proxy scenarios.


311-402: OAuth middleware implementation provides necessary functionality.

The OAuthMiddleware class effectively:

  • Proxies OAuth metadata from the real authorization server while modifying necessary fields
  • Implements a fake dynamic client registration endpoint
  • Enforces authorization header presence for protected endpoints

This implementation aligns well with the PR objectives of enabling OAuth flow for MCP clients without requiring manual token provision.

Comment thread server.py Outdated
@jhernand jhernand force-pushed the implement_oauth_endpoints branch from 023c53b to d682cad Compare June 26, 2025 21:16
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
README.md (1)

72-94: Fix grammatical issues in the OAuth documentation.

The documentation content is comprehensive and accurate, but there are several grammar and punctuation issues that should be addressed for better readability.

Apply these fixes to improve the grammar:

-The server can implement a subset of the OAuth protocol that MCP clients like the one in VScode
-use to authenticate. To enable it set the environment variable `OAUTH_ENABLED` to true. Then when
-you try to connect the MCP client will open a browser where you can provide your credentials, and
+The server can implement a subset of the OAuth protocol that MCP clients like the one in VSCode
+use to authenticate. To enable it, set the environment variable `OAUTH_ENABLED` to true. Then when
+you try to connect, the MCP client will open a browser where you can provide your credentials, and
 the MCP client will request an access token that will then be used by the server to authenticate
 to the assisted installer REST API.

-If you use this mechanism then you don't need to provide the `OFFLINE_TOKEN`.
+If you use this mechanism, then you don't need to provide the `OFFLINE_TOKEN`.

-Additionaly you can configure the OAuth authorization server and client identifier to use with the
+Additionally, you can configure the OAuth authorization server and client identifier to use with the
server.py (2)

365-369: Consider adding timeout and error handling for external requests.

The external HTTP request to the OAuth metadata endpoint should include timeout configuration and proper error handling for robustness.

 # Get the metadata from the real authorization service:
-response = requests.get(
-    url=f"{self._oauth_url}/.well-known/oauth-authorization-server",
-)
+try:
+    response = requests.get(
+        url=f"{self._oauth_url}/.well-known/oauth-authorization-server",
+        timeout=10,
+    )
+except requests.RequestException as e:
+    return starlette.responses.Response(
+        content=f"Failed to retrieve OAuth metadata: {e}",
+        status_code=502,
+    )

433-436: Review CORS configuration for production security.

The current CORS configuration allows all origins (allow_origins="*"), which may be too permissive for production environments. Consider making this configurable.

 # Add the CORS middleware:
+cors_origins = os.getenv("CORS_ORIGINS", "*").split(",")
 app.add_middleware(
     starlette.middleware.cors.CORSMiddleware,
-    allow_origins="*",
+    allow_origins=cors_origins,
     allow_methods="*",
     allow_credentials=True,
 )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 023c53b and d682cad.

📒 Files selected for processing (2)
  • README.md (1 hunks)
  • server.py (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
server.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🧬 Code Graph Analysis (1)
server.py (1)
service_client/assisted_service_api.py (1)
  • InventoryClient (11-129)
🪛 Flake8 (7.2.0)
server.py

[error] 311-311: expected 2 blank lines, found 1

(E302)


[error] 403-403: expected 2 blank lines after class or function definition, found 1

(E305)

🪛 LanguageTool
README.md

[uncategorized] ~75-~75: A comma might be missing here.
Context: ...n VScode use to authenticate. To enable it set the environment variable `OAUTH_ENA...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)


[uncategorized] ~76-~76: A comma might be missing here.
Context: ..._ENABLED` to true. Then when you try to connect the MCP client will open a browser wher...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)


[typographical] ~80-~80: Consider adding a comma.
Context: ...ler REST API. If you use this mechanism then you don't need to provide the `OFFLINE_...

(IF_THEN_COMMA)


[uncategorized] ~81-~81: Possible missing comma found.
Context: ...t need to provide the OFFLINE_TOKEN. Additionaly you can configure the OAuth authorizati...

(AI_HYDRA_LEO_MISSING_COMMA)

🔇 Additional comments (4)
server.py (4)

1-17: Import changes look good.

The transition from mcp.server.fastmcp to the standalone fastmcp package is properly implemented with all necessary Starlette and Uvicorn imports added for the new middleware functionality.


38-64: Approve header access method changes.

The functions correctly use fastmcp.server.dependencies.get_http_headers() instead of direct request object access, which is the proper way to access HTTP headers in the updated fastmcp framework.


311-401: OAuth middleware implementation is well-designed.

The middleware correctly implements the OAuth flow requirements:

  • Properly handles OAuth metadata endpoint by proxying to the real authorization server
  • Correctly replaces the registration endpoint URL using the SELF_URL environment variable (following the retrieved learning)
  • Appropriately limits supported scopes to prevent authorization server rejections
  • Implements fake dynamic client registration as intended
  • Enforces authorization header presence for protected endpoints

The security model is appropriate for the use case - requiring only the presence of an authorization header to trigger the OAuth flow rather than validating the token at the middleware level.


403-439: Main execution block implementation is excellent.

The restructuring to use Starlette application with middleware support is well-implemented:

  • Properly creates HTTP app for SSE transport
  • Correctly implements conditional OAuth middleware based on environment variable
  • Uses appropriate environment variable defaults that match the documentation
  • Properly configures CORS middleware for browser-based OAuth flows
  • Uses Uvicorn as the ASGI server appropriately

The debug print statement helps with troubleshooting OAuth configuration.

@jhernand jhernand force-pushed the implement_oauth_endpoints branch from d682cad to ea51a8b Compare June 27, 2025 07:32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
server.py (1)

365-370: Add timeout and error handling for external HTTP request.

The external request to the OAuth authorization server lacks timeout and proper error handling, which could cause the middleware to hang or fail ungracefully.

         # Get the metadata from the real authorization service:
-        response = requests.get(
-            url=f"{self._oauth_url}/.well-known/oauth-authorization-server",
-        )
-        response.raise_for_status()
+        try:
+            response = requests.get(
+                url=f"{self._oauth_url}/.well-known/oauth-authorization-server",
+                timeout=10,
+            )
+            response.raise_for_status()
+        except requests.RequestException as e:
+            return starlette.responses.JSONResponse(
+                content={"error": "Failed to fetch OAuth metadata"},
+                status_code=503,
+            )
🧹 Nitpick comments (3)
server.py (3)

310-311: Add missing blank lines for PEP 8 compliance.

Static analysis indicates missing blank lines before the class definition.

+
+
 class OAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):

402-403: Add missing blank lines for PEP 8 compliance.

Static analysis indicates missing blank lines after the class definition.

         )

+
 if __name__ == "__main__":

429-435: Consider restricting CORS configuration for production.

The current CORS configuration allows all origins with credentials, which may be too permissive for production environments.

Consider making CORS configuration environment-driven:

     # Add the CORS middleware:
+    cors_origins = os.getenv("CORS_ORIGINS", "*").split(",")
     app.add_middleware(
         starlette.middleware.cors.CORSMiddleware,
-        allow_origins="*",
+        allow_origins=cors_origins,
         allow_methods="*",
         allow_credentials=True,
     )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d682cad and ea51a8b.

📒 Files selected for processing (2)
  • README.md (2 hunks)
  • server.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
server.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🧬 Code Graph Analysis (1)
server.py (1)
service_client/assisted_service_api.py (1)
  • InventoryClient (11-129)
🪛 Flake8 (7.2.0)
server.py

[error] 311-311: expected 2 blank lines, found 1

(E302)


[error] 403-403: expected 2 blank lines after class or function definition, found 1

(E305)

🔇 Additional comments (3)
server.py (3)

4-13: Import changes look good.

The new imports properly support the OAuth middleware and Starlette-based architecture changes.


38-39: Header access implementation is correct.

The changes properly use the new fastmcp dependency injection system for accessing HTTP headers.

Also applies to: 59-65


404-438: OAuth configuration follows best practices.

The use of environment variables for OAuth configuration correctly follows the established pattern of using SELF_URL for endpoint construction, which properly handles reverse proxy scenarios.

Comment thread server.py Outdated
Comment on lines +390 to +431
async def oauth_register(self, request: starlette.requests.Request) -> starlette.responses.Response:
"""
This method implements the OAuth dynamic client registration endpoint. It responds to all requests
with a fixed client identifier.
"""
body = await request.json()
return starlette.responses.JSONResponse(
content={
"client_id": self._oauth_client,
"redirect_uris": body["redirect_uris"],
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for OAuth registration request.

The method assumes the request body contains "redirect_uris" without validation, which could cause a KeyError if the field is missing.

     async def oauth_register(self, request: starlette.requests.Request) -> starlette.responses.Response:
         """
         This method implements the OAuth dynamic client registration endpoint. It responds to all requests
         with a fixed client identifier.
         """
-        body = await request.json()
+        try:
+            body = await request.json()
+            if "redirect_uris" not in body:
+                return starlette.responses.JSONResponse(
+                    content={"error": "redirect_uris is required"},
+                    status_code=400,
+                )
+        except Exception:
+            return starlette.responses.JSONResponse(
+                content={"error": "Invalid JSON in request body"},
+                status_code=400,
+            )
         return starlette.responses.JSONResponse(
             content={
                 "client_id": self._oauth_client,
                 "redirect_uris": body["redirect_uris"],
             },
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def oauth_register(self, request: starlette.requests.Request) -> starlette.responses.Response:
"""
This method implements the OAuth dynamic client registration endpoint. It responds to all requests
with a fixed client identifier.
"""
body = await request.json()
return starlette.responses.JSONResponse(
content={
"client_id": self._oauth_client,
"redirect_uris": body["redirect_uris"],
},
)
async def oauth_register(self, request: starlette.requests.Request) -> starlette.responses.Response:
"""
This method implements the OAuth dynamic client registration endpoint. It responds to all requests
with a fixed client identifier.
"""
try:
body = await request.json()
if "redirect_uris" not in body:
return starlette.responses.JSONResponse(
content={"error": "redirect_uris is required"},
status_code=400,
)
except Exception:
return starlette.responses.JSONResponse(
content={"error": "Invalid JSON in request body"},
status_code=400,
)
return starlette.responses.JSONResponse(
content={
"client_id": self._oauth_client,
"redirect_uris": body["redirect_uris"],
},
)
🤖 Prompt for AI Agents
In server.py around lines 390 to 401, the oauth_register method assumes the
request body contains "redirect_uris" without validation, risking a KeyError.
Add input validation to check if "redirect_uris" exists in the request JSON
before accessing it. If missing, return an appropriate error response indicating
the required field is absent. This will prevent runtime errors and improve
robustness.

@jhernand jhernand force-pushed the implement_oauth_endpoints branch from ea51a8b to b9281b9 Compare June 27, 2025 19:01
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
server.py (1)

395-398: Add timeout to external HTTP request.

The external request to the OAuth authorization server lacks a timeout, which could cause the middleware to hang indefinitely if the server is unresponsive.

         response = requests.get(
             url=f"{self._oauth_url}/.well-known/oauth-authorization-server",
+            timeout=10,
         )
🧹 Nitpick comments (4)
server.py (4)

14-14: Remove unused import.

The urllib.parse import is not used anywhere in the code.

-import urllib.parse

312-312: Fix formatting: Add required blank lines.

Class definitions should be preceded by two blank lines according to PEP 8.

+
+
 class OAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):

424-431: Consider adding error handling for malformed JSON.

While the code handles missing redirect_uris properly, it doesn't handle malformed JSON in the request body.

     async def oauth_register(self, request: starlette.requests.Request) -> starlette.responses.Response:
         """
         This method implements the OAuth dynamic client registration endpoint. It responds to all requests
         with a fixed client identifier.
         """
-        body = await request.json()
+        try:
+            body = await request.json()
+        except Exception:
+            return starlette.responses.JSONResponse(
+                content={"error": "Invalid JSON in request body"},
+                status_code=400,
+            )
         redirect_uris = body.get("redirect_uris", [])
         return starlette.responses.JSONResponse(
             content={
                 "client_id": self._oauth_client,
                 "redirect_uris": redirect_uris,
             },
         )

433-433: Fix formatting: Add blank line after class definition.

There should be two blank lines after class or function definitions according to PEP 8.

         )

+
 if __name__ == "__main__":
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea51a8b and b9281b9.

📒 Files selected for processing (2)
  • README.md (2 hunks)
  • server.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
server.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🪛 Ruff (0.11.9)
server.py

14-14: urllib.parse imported but unused

Remove unused import: urllib.parse

(F401)

🪛 Flake8 (7.2.0)
server.py

[error] 14-14: 'urllib.parse' imported but unused

(F401)


[error] 312-312: expected 2 blank lines, found 1

(E302)


[error] 433-433: expected 2 blank lines after class or function definition, found 1

(E305)

🔇 Additional comments (3)
server.py (3)

39-40: Good API migration.

The update to use fastmcp.server.dependencies.get_http_headers() correctly follows the new API pattern.


60-65: Improved authorization header parsing.

The enhanced logic for parsing the Authorization header with proper splitting and validation is well implemented.


434-468: Well-implemented OAuth integration with proper configuration.

The main execution block correctly:

  • Creates a Starlette application from the MCP instance
  • Conditionally enables OAuth middleware based on environment variables
  • Provides sensible defaults for OAuth configuration
  • Adds CORS middleware for cross-origin support
  • Uses appropriate uvicorn configuration

The environment variable approach aligns with the learning about using environment variables instead of request URLs for OAuth endpoints, which is important for reverse proxy scenarios.

@jhernand jhernand force-pushed the implement_oauth_endpoints branch from b9281b9 to 3029d8c Compare June 27, 2025 19:10
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
server.py (1)

434-469: Good implementation of configurable OAuth with proper middleware setup.

The main execution block correctly implements the requirements:

  • Conditional OAuth enablement via OAUTH_ENABLED environment variable addresses the PR objective
  • Sensible defaults for OAuth configuration parameters
  • Proper Starlette application creation for middleware support
  • CORS middleware addition for client compatibility

Consider tightening CORS configuration for production deployments, as the current settings (allow_origins="*" with allow_credentials=True) are very permissive and could pose security risks.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9281b9 and 3029d8c.

📒 Files selected for processing (2)
  • README.md (2 hunks)
  • server.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
server.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🔇 Additional comments (4)
server.py (4)

1-13: Import statements look good for OAuth implementation.

The migration to fastmcp package and addition of Starlette/Uvicorn imports properly support the new OAuth middleware functionality.


17-17: Good simplification of MCP instance creation.

Removing the host parameter is appropriate since host configuration is now handled by uvicorn.run() in the main execution block.


38-64: Excellent migration to dependency injection for header access.

The update to use fastmcp.server.dependencies.get_http_headers() improves testability and follows better architectural patterns while maintaining the same authentication logic.


312-431: Well-implemented OAuth middleware with proper architecture.

The OAuthMiddleware class correctly implements the required OAuth endpoints with good separation of concerns:

  • Proper routing of OAuth endpoints without authentication requirements
  • Correct 401 response with WWW-Authenticate header for protected endpoints
  • Environment variable usage for URLs follows the established learning about reverse proxy scenarios
  • Good error handling for external OAuth metadata requests
  • Safe handling of JSON request body in registration endpoint using .get() method

The implementation successfully enables MCP clients to complete OAuth flows as intended.

@jhernand
Copy link
Copy Markdown
Collaborator Author

@eranco74 I think this is ready to merge now.

Comment thread server.py Outdated
return InventoryClient(get_access_token()).update_host(host_id, infraenv_id, host_role=role).to_str()


class OAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do you think this might be better in a separate file? server.py is getting a bit busy.

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.

Yes, it can be in a separate file, will do that.

Comment thread server.py Outdated
async def oauth_register(self, request: starlette.requests.Request) -> starlette.responses.Response:
"""
This method implements the OAuth dynamic client registration endpoint. It responds to all requests
with a fixed client identifier.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What does this mean? Is there any problem with reusing a client identifier? (disclaimer, I'm not very familiar with oauth)

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.

The "client" is the application that is requesting access to the protected resources. If we were strict this should be something like "VScode", "Cursor", etc. But that would mean requesting the creation of such clients to the sso.redhat.com team. Instead we use "cloud-services" that is already used by plenty of applications in OCM. There is no problem in reusing it, as it isn't an confidential client.

Comment thread server.py Outdated
Comment on lines +460 to +466
# Add the CORS middleware:
app.add_middleware(
starlette.middleware.cors.CORSMiddleware,
allow_origins="*",
allow_methods="*",
allow_credentials=True,
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this only be added if oauth_enabled is true?

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.

I am not sure. CORS would be useful even if there was no OAuth authorization. For example, if access was completely anonymous CORS would still be needed for clients like the MCP inspector, which runs in a browser. Actually that is the reason I added this: to be able to use the MCP inspector. But the MCP inspector combined with OAuth doesn't work anyhow, because sso.redhat.com would also need to support it. So I think I will CORS support completely, we can add it later.

@jhernand jhernand force-pushed the implement_oauth_endpoints branch from 3029d8c to a343a0f Compare June 30, 2025 13:08
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
oauth.py (1)

124-136: Add input validation and error handling for JSON parsing.

The method lacks proper error handling for JSON parsing and input validation, which could cause runtime errors if the request body is malformed or missing expected fields.

Apply this diff to add proper error handling:

 async def _register(self, request: starlette.requests.Request) -> starlette.responses.Response:
     """
     This method implements the OAuth dynamic client registration endpoint. It responds to all requests
     with a fixed client identifier.
     """
-    body = await request.json()
-    redirect_uris = body.get("redirect_uris", [])
+    try:
+        body = await request.json()
+    except Exception:
+        return starlette.responses.JSONResponse(
+            content={"error": "Invalid JSON in request body"},
+            status_code=400,
+        )
+    
+    redirect_uris = body.get("redirect_uris", [])
     return starlette.responses.JSONResponse(
         content={
             "client_id": self._oauth_client,
             "redirect_uris": redirect_uris,
         },
     )
🧹 Nitpick comments (1)
oauth.py (1)

2-2: Fix typo in module docstring.

There's a typo in the module docstring: "implemnts" should be "implements".

-This module contains the Starlette middleware that implemnts OAuth authorization.
+This module contains the Starlette middleware that implements OAuth authorization.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3029d8c and a343a0f.

📒 Files selected for processing (3)
  • README.md (2 hunks)
  • oauth.py (1 hunks)
  • server.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
server.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
oauth.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🧬 Code Graph Analysis (1)
server.py (2)
service_client/assisted_service_api.py (1)
  • InventoryClient (11-129)
oauth.py (1)
  • Middleware (13-136)
🪛 Pylint (3.3.7)
oauth.py

[refactor] 13-13: Too few public methods (1/2)

(R0903)

🔇 Additional comments (10)
oauth.py (5)

5-10: LGTM!

The imports are well-organized and include all necessary Starlette components for middleware implementation.


19-38: LGTM!

The constructor properly initializes the middleware with required OAuth configuration parameters and follows proper inheritance patterns.


40-71: LGTM!

The dispatch method correctly routes OAuth endpoints without authentication and enforces the presence of an Authorization header for all other requests. The implementation properly follows the learning about using environment variables (self._self_url) instead of request URLs for constructing OAuth endpoints.


73-91: LGTM!

The protected resource metadata endpoint correctly returns the required OAuth 2.0 resource metadata format with appropriate scopes and authorization server information.


93-122: LGTM!

The metadata endpoint implementation correctly fetches from the real authorization server, modifies the registration endpoint to point to the local endpoint, and restricts scopes to prevent authorization failures. The timeout parameter and error handling are appropriate.

server.py (5)

3-9: LGTM!

The import changes properly transition from direct MCP imports to using fastmcp and add necessary dependencies for OAuth integration and uvicorn server.


13-13: LGTM!

The simplified FastMCP initialization removes unnecessary explicit host parameter, improving configurability.


34-36: LGTM!

The change to use dependency injection via fastmcp.server.dependencies.get_http_headers() instead of direct request context access improves the architecture and follows proper dependency injection patterns.


55-60: LGTM!

Consistent application of the new dependency injection pattern for HTTP header access, maintaining consistency with the previous change.


308-334: LGTM!

The OAuth middleware integration is well-implemented with proper conditional enabling based on environment variables. The configuration uses sensible defaults and follows the established learning about using environment variables for URL construction. The transition to using uvicorn directly provides better control over the server configuration.

Copy link
Copy Markdown
Collaborator

@eranco74 eranco74 left a comment

Choose a reason for hiding this comment

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

Looks good to me.
Just a nit
Can you update the readme with the auth options priority/exclusion.
What happens if I pass the offline token when OAUTH_ENABLED is enabled?
Will passing the access token (as we currently do from LSC) still work?

Comment thread server.py
from service_client import InventoryClient

mcp = FastMCP("AssistedService", host="0.0.0.0")
mcp = fastmcp.FastMCP("AssistedService")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why? I think the default is 127.0.0.1

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.

The default may be 127.0.0.1, but it isn't relevant anymore because we don't run the MCP directly. Instead we create a Starlette application and start it with this:

uvicorn.run(app, host="0.0.0.0", port=8000)

See the __main__ code for details.

@jhernand
Copy link
Copy Markdown
Collaborator Author

jhernand commented Jul 1, 2025

Can you update the readme with the auth options priority/exclusion.

Yes, I will.

What happens if I pass the offline token when OAUTH_ENABLED is enabled?

It depends on your client.

If your client knows how to use OAuth (VScode or Cursor, for example) then it will initiate the OAuth process and when that finishes it will get an access token and send it with every request. As a result the OFFLINE_TOKEN environment variable or the OCM-Offline-Token header will be ignored.

If your client doesn't know how to use OAuth then access will be rejected, and you need to either set the Authorization header upfront, or else disable OAuth.

Will passing the access token (as we currently do from LSC) still work?

Yes, if the access token is present in the Authorization header upfront, then the OAuth process will not be triggered.

@jhernand jhernand force-pushed the implement_oauth_endpoints branch from a343a0f to 2e50a7d Compare July 1, 2025 08:52
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
oauth.py (2)

2-2: Fix typo in module docstring.

There's a spelling error in the docstring.

-This module contains the Starlette middleware that implemnts OAuth authorization.
+This module contains the Starlette middleware that implements OAuth authorization.

124-136: Consider adding basic validation for dynamic client registration.

The current implementation accepts any request body without validation. Consider adding basic checks for required OAuth dynamic registration parameters.

 async def _register(self, request: starlette.requests.Request) -> starlette.responses.Response:
     """
     This method implements the OAuth dynamic client registration endpoint. It responds to all requests
     with a fixed client identifier.
     """
     try:
         body = await request.json()
     except Exception:
         return starlette.responses.JSONResponse(
             status_code=400,
             content={"error": "invalid_request", "error_description": "Invalid JSON in request body"},
         )
+    
+    # Basic validation for redirect_uris
+    redirect_uris = body.get("redirect_uris", [])
+    if redirect_uris and not isinstance(redirect_uris, list):
+        return starlette.responses.JSONResponse(
+            status_code=400,
+            content={"error": "invalid_request", "error_description": "redirect_uris must be an array"},
+        )
+    
-    redirect_uris = body.get("redirect_uris", [])
     return starlette.responses.JSONResponse(
         content={
             "client_id": self._oauth_client,
             "redirect_uris": redirect_uris,
         },
     )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a343a0f and 2e50a7d.

📒 Files selected for processing (3)
  • README.md (2 hunks)
  • oauth.py (1 hunks)
  • server.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • README.md
  • server.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
oauth.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🪛 Pylint (3.3.7)
oauth.py

[refactor] 13-13: Too few public methods (1/2)

(R0903)

🔇 Additional comments (2)
oauth.py (2)

63-63: Excellent use of environment variable for URL construction.

Good implementation following the retrieved learning about using environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place.


48-71: Authentication logic correctly implements OAuth flow initiation.

The middleware correctly handles the OAuth endpoints without authentication and returns proper 401 responses with WWW-Authenticate headers for protected resources, which will trigger OAuth flow in compliant clients.

Comment thread oauth.py Outdated
Comment thread oauth.py
This method implements the OAuth dynamic client registration endpoint. It responds to all requests
with a fixed client identifier.
"""
body = await request.json()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for JSON parsing.

The await request.json() call can raise exceptions if the request body is malformed JSON or missing. This should be handled gracefully.

-    body = await request.json()
+    try:
+        body = await request.json()
+    except Exception:
+        return starlette.responses.JSONResponse(
+            status_code=400,
+            content={"error": "invalid_request", "error_description": "Invalid JSON in request body"},
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
body = await request.json()
try:
body = await request.json()
except Exception:
return starlette.responses.JSONResponse(
status_code=400,
content={"error": "invalid_request", "error_description": "Invalid JSON in request body"},
)
🤖 Prompt for AI Agents
In oauth.py at line 129, the call to await request.json() can raise exceptions
if the JSON is malformed or missing. Wrap this call in a try-except block to
catch JSON parsing errors, and handle them gracefully by returning an
appropriate error response or message instead of letting the exception
propagate.

Comment thread oauth.py Outdated
@jhernand jhernand force-pushed the implement_oauth_endpoints branch from 2e50a7d to fa1e4fc Compare July 1, 2025 09:51
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
oauth.py (3)

99-104: Replace synchronous HTTP call with asynchronous version.

Using requests.get() in an async method blocks the event loop and degrades performance. This issue was previously identified and should be addressed.


98-104: Add error handling for external OAuth server failures.

The code only uses raise_for_status() for error handling but lacks handling for network issues. This was previously identified and should be addressed with proper exception handling and fallback behavior.


129-129: Add error handling for JSON parsing.

The await request.json() call can raise exceptions if the request body is malformed JSON. This was previously identified and should be handled gracefully.

🧹 Nitpick comments (2)
oauth.py (1)

2-2: Fix typo in module docstring.

"implemnts" should be "implements".

-This module contains the Starlette middleware that implemnts OAuth authorization.
+This module contains the Starlette middleware that implements OAuth authorization.
README.md (1)

167-168: Fix markdown list formatting for consistency.

The static analysis tool flags inconsistent list formatting. Consider using asterisks instead of dashes for unordered lists to maintain consistency with the rest of the document.

-- `OAUTH_URL`: `https://sso.redhat.com/auth/realms/redhat-external`
-- `OAUTH_CLIENT`: `cloud-services`
+* `OAUTH_URL`: `https://sso.redhat.com/auth/realms/redhat-external`
+* `OAUTH_CLIENT`: `cloud-services`
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e50a7d and fa1e4fc.

📒 Files selected for processing (3)
  • README.md (2 hunks)
  • oauth.py (1 hunks)
  • server.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
oauth.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🪛 Pylint (3.3.7)
oauth.py

[refactor] 13-13: Too few public methods (1/2)

(R0903)

🪛 LanguageTool
README.md

[uncategorized] ~167-~167: Loose punctuation mark.
Context: .... The default values are: - OAUTH_URL: `https://sso.redhat.com/auth/realms/red...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 markdownlint-cli2 (0.17.2)
README.md

167-167: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


168-168: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

🔇 Additional comments (5)
oauth.py (3)

63-63: Consistent use of environment variable for self URL construction.

Based on the retrieved learning, when constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place. The code correctly uses self._self_url here, which aligns with the best practice.


13-38: Well-structured middleware initialization.

The middleware constructor properly accepts all necessary configuration parameters and follows Starlette middleware patterns. The parameter documentation is clear and comprehensive.


40-71: Effective request routing and authentication enforcement.

The dispatch method properly routes OAuth endpoints and enforces authentication for protected endpoints. The approach of checking for the authorization header presence (without validation) to trigger OAuth flow is appropriate for this use case.

README.md (2)

57-58: Good restructuring of content flow.

Moving the "Ask about your clusters" section earlier improves the document's logical flow by placing usage examples closer to the setup instructions.


139-185: Comprehensive and accurate OAuth documentation.

The new "Authorization Options" section provides clear, well-structured documentation of the authentication methods. The priority order, configuration parameters, and detailed explanations align well with the implementation in oauth.py.

This patch implements enough OAuth endpoints to make the server usable
from MCP clients like VScode without having to manually provide a token.

When the MCP client receives a 401 code from the server it will assume
that it uses OAuth, and it will try to fetch the OAuth metadata from
`/.well-known/oauth-authorization-server`. As we don't want to filly
implement that, we implement it calling the equivalent endpoint of
_sso.redhat.com_. We then modify a couple of things in the content, and
return it to the client.

One of the things that we modify is the dynamic client registration
endpoint. The MCP client will try to use the one in the metadata, but
_sso.redhat.com_ doesn't allow that. So we replace it with our own,
where we accept all requests and respond always with the same client
identifier.

With this the MCP client is able to complete the OAuth process, and
obtain an access token that will then use to call the MCP. The MCP will
then in turn use it to call the assisted installer REST API.

Signed-off-by: Juan Hernandez <juan.hernandez@redhat.com>
@jhernand jhernand force-pushed the implement_oauth_endpoints branch from fa1e4fc to b38232d Compare July 1, 2025 10:21
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
oauth.py (1)

133-133: Add error handling for JSON parsing.

The await request.json() call can raise exceptions if the request body contains malformed JSON or is missing. This should be handled gracefully to prevent the middleware from crashing.

-        body = await request.json()
+        try:
+            body = await request.json()
+        except Exception:
+            return starlette.responses.JSONResponse(
+                status_code=400,
+                content={"error": "invalid_request", "error_description": "Invalid JSON in request body"},
+            )
🧹 Nitpick comments (3)
oauth.py (2)

2-2: Fix typo in module docstring.

There's a typo in "implemnts" - it should be "implements".

-This module contains the Starlette middleware that implemnts OAuth authorization.
+This module contains the Starlette middleware that implements OAuth authorization.

113-121: Consider adding validation for scope restrictions.

While the scope filtering is a good security practice, consider adding validation to ensure only expected scopes are included and log any unexpected scopes from the upstream server for monitoring purposes.

         # The MCP clients also try to request all the scopes listed in the metadata, but our authorization
         # server returns a lot of scopes, and most of them will be rejected for our client. So we replace
         # that large list with a much smaller list containing only the scopes that we need.
-        body["scopes_supported"] = [
-            "openid",
-            "api.ocm",
-        ]
+        expected_scopes = ["openid", "api.ocm"]
+        original_scopes = body.get("scopes_supported", [])
+        unexpected_scopes = [s for s in original_scopes if s not in expected_scopes]
+        if unexpected_scopes:
+            # Consider logging unexpected scopes for monitoring
+            pass
+        body["scopes_supported"] = expected_scopes
README.md (1)

167-168: Fix markdown list formatting inconsistency.

The static analysis correctly identified inconsistent list formatting. The document uses asterisks (*) for other lists but dashes (-) here. Maintain consistency throughout the document.

-        - `OAUTH_URL`: `https://sso.redhat.com/auth/realms/redhat-external`
-        - `OAUTH_CLIENT`: `cloud-services`
+        * `OAUTH_URL`: `https://sso.redhat.com/auth/realms/redhat-external`
+        * `OAUTH_CLIENT`: `cloud-services`
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa1e4fc and b38232d.

📒 Files selected for processing (3)
  • README.md (2 hunks)
  • oauth.py (1 hunks)
  • server.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
oauth.py (1)
Learnt from: jhernand
PR: openshift-assisted/assisted-service-mcp#5
File: server.py:0-0
Timestamp: 2025-06-26T18:46:49.998Z
Learning: When constructing URLs for OAuth endpoints in middleware, use environment variables (like SELF_URL) instead of request URLs when there may be reverse proxies in place, as the URL in the request may not be the URL that clients need to use.
🪛 LanguageTool
README.md

[uncategorized] ~167-~167: Loose punctuation mark.
Context: .... The default values are: - OAUTH_URL: `https://sso.redhat.com/auth/realms/red...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 markdownlint-cli2 (0.17.2)
README.md

167-167: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


168-168: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

🪛 Pylint (3.3.7)
oauth.py

[refactor] 13-13: Too few public methods (1/2)

(R0903)

🔇 Additional comments (4)
oauth.py (2)

99-108: Excellent async HTTP implementation with proper error handling.

The implementation correctly uses httpx.AsyncClient for non-blocking HTTP calls and includes proper exception handling for network and HTTP errors. This addresses the concerns from previous reviews about blocking the event loop and handling external server failures.


63-63: URL construction follows best practices from retrieved learnings.

The implementation correctly uses the self._self_url environment variable instead of extracting URLs from the request, which aligns with the retrieved learning about handling reverse proxies properly.

README.md (2)

139-185: Comprehensive and accurate OAuth documentation.

The authorization options section provides clear, well-structured documentation that accurately reflects the implementation. The priority ordering, configuration details, and examples are helpful for users. The explanation of URL construction for reverse proxy scenarios aligns well with the implementation approach.


154-162: Verify OAuth flow behavior with MCP clients.
The server-side code configures the OAuth endpoints, but the browser-launch behavior is implemented in the MCP client library. Please confirm that all supported MCP clients (e.g., the VS Code extension, the Cursor CLI, etc.) actually open a browser window when you initiate OAuth authentication. If any client differs, update the README to accurately describe each client’s user experience.

Comment thread server.py

token = mcp.get_context().request_context.request.headers.get("OCM-Offline-Token")
headers = fastmcp.server.dependencies.get_http_headers()
token = headers.get("ocm-offline-token")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why change this? Also I assume this header name is not case sensitive?

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.

As part of the change we are importing fastmcp instead of mcp.server.fastmcp. One of the differences is that there is no get_context() method. Instead of that there is the fastmcp.server.dependencies package that provides methods (like get_http_headers) to get the dependencies.

Changing the imported package was necessary because the mcp.server.fastmcp doesn't expose the functionality to create a Starlette applicaton from the FastMCP instance, and I needed that to be able to add the middleware.

The get_http_headers method returns a plain Python dictionary witht the headers converted to lower case:

https://github.com/jlowin/fastmcp/blob/edbf0ae81625156891a76a437304fa70ea2dd947/src/fastmcp/server/dependencies.py#L79-L89

For more information, this is what Gemini has to say about the differences between fastmcp and mcp.server.fastmcp:

The relationship between "fastmcp" and "mcp.server.fastmcp" in Python is a matter of packaging and evolution within the Model Context Protocol (MCP) ecosystem.

Here's the breakdown:

  • fastmcp (the standalone library):

    • This refers to the fastmcp Python package that you would install directly (e.g., pip install fastmcp).
    • It's a high-level, Pythonic framework designed to dramatically simplify the process of building MCP servers.
    • It abstracts away many of the complexities of the raw MCP protocol, offering a decorator-based API (@mcp.tool(), @mcp.resource(), @mcp.prompt()) that makes it easy to define functions that expose capabilities to Large Language Models (LLMs).
    • It aims to provide a fast and simple way for Python developers to create MCP servers.
  • mcp.server.fastmcp (part of the official MCP Python SDK):

    • This refers to fastmcp as it is integrated within the official modelcontextprotocol/python-sdk.
    • Crucially, FastMCP 1.0 was so successful that it was directly incorporated into the official MCP Python SDK. This means that for basic use cases, you can often use mcp.server.fastmcp directly if you have the SDK installed.
    • It represents the "upstream" version of FastMCP for those who are using the broader MCP Python SDK.

In essence:

  • fastmcp is the project/framework that originated as a simpler way to build MCP servers.
  • mcp.server.fastmcp is the module within the official modelcontextprotocol SDK that contains the FastMCP implementation.

Why the two ways of importing/using?

  • Historical Context: FastMCP was initially a separate project that proved to be very effective. The developers of the official MCP Python SDK recognized its value and integrated it.
  • Version Evolution: While mcp.server.fastmcp in the SDK might represent a stable version (often FastMCP 1.0), the standalone fastmcp package continues to evolve with newer versions (e.g., FastMCP 2.0 and beyond) that introduce advanced features like proxying, composing servers, and generating servers from OpenAPI specs.
  • Choice for Developers:
    • If you're starting a new project and want the latest features and a more direct experience with the FastMCP framework, you'd likely install fastmcp as a standalone package.
    • If you're already using the broader MCP Python SDK and only need basic MCP server functionalities, the mcp.server.fastmcp module within the SDK might suffice.

General Recommendation:

For new projects, it's generally recommended to install and use the standalone fastmcp package, as it is the actively maintained version and provides the most up-to-date features for building MCP servers.

@jhernand jhernand marked this pull request as draft July 3, 2025 08:28
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 3, 2025
@jhernand
Copy link
Copy Markdown
Collaborator Author

jhernand commented Jul 3, 2025

I am moving this to draft again because in my latest tests I wasn't able to connect with VS code: the authorization server is rejecting the redirect URI. This worked fine a few days ago, I wonder if something has changed in the configuration of the authorization server. I will check. Don't merge it yet.

@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 3, 2025
@openshift-merge-robot
Copy link
Copy Markdown

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants