-
Notifications
You must be signed in to change notification settings - Fork 1.5k
OCI Provider with Docs #2373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OCI Provider with Docs #2373
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,270 @@ | ||||||||||||||
| --- | ||||||||||||||
| title: OCI IAM OAuth 🤝 FastMCP | ||||||||||||||
| sidebarTitle: Oracle | ||||||||||||||
| description: Secure your FastMCP server with OCI IAM OAuth | ||||||||||||||
| icon: oracle | ||||||||||||||
| tag: NEW | ||||||||||||||
| --- | ||||||||||||||
|
|
||||||||||||||
| import { VersionBadge } from "/snippets/version-badge.mdx" | ||||||||||||||
|
|
||||||||||||||
| <VersionBadge version="2.13.0" /> | ||||||||||||||
|
|
||||||||||||||
| This guide shows you how to secure your FastMCP server using **OCI IAM OAuth**. Since OCI IAM doesn't support Dynamic Client Registration, this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge OCI's traditional OAuth with MCP's authentication requirements. | ||||||||||||||
|
|
||||||||||||||
| ## Configuration | ||||||||||||||
|
|
||||||||||||||
| ### Prerequisites | ||||||||||||||
|
|
||||||||||||||
| 1. An OCI cloud Account with access to create an Integrated Application in an Identity Domain. | ||||||||||||||
| 2. Your FastMCP server's URL (For dev environments, it is http://localhost:8000. For PROD environments, it could be https://mcp.${DOMAIN}.com) | ||||||||||||||
|
|
||||||||||||||
| ### Make sure client access is enabled for JWK's URL | ||||||||||||||
|
|
||||||||||||||
| 1. Login to OCI console (https://cloud.oracle.com for OCI commercial cloud). | ||||||||||||||
| 2. From "Identity & Security" menu, open Domains page. | ||||||||||||||
| 3. On the Domains list page, select the domain that you are using for MCP Authentication. | ||||||||||||||
| 4. Open Settings tab. | ||||||||||||||
| 5. Click on "Edit Domain Settings" button. | ||||||||||||||
|
|
||||||||||||||
|  | ||||||||||||||
|
|
||||||||||||||
| 6. Enable "Configure client access" checkbox as show in the screenshot. | ||||||||||||||
|
|
||||||||||||||
|  | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add descriptive alt text for accessibility. Apply this diff: -
+📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| ### Create OAuth client for MCP server authentication | ||||||||||||||
| Follow the steps as mentioned below to create an OAuth client. | ||||||||||||||
|
|
||||||||||||||
| 1. Login to OCI console (https://cloud.oracle.com for OCI commercial cloud). | ||||||||||||||
| 2. From "Identity & Security" menu, open Domains page. | ||||||||||||||
| 3. On the Domains list page, select the domain in which you want to create MCP server OAuth client. If you need help finding the list page for the domain, see [Listing Identity Domains.](https://docs.oracle.com/en-us/iaas/Content/Identity/domains/to-view-identity-domains.htm#view-identity-domains). | ||||||||||||||
| 4. On the details page, select Integrated applications. A list of applications in the domain is displayed. | ||||||||||||||
| 5. Select Add application. | ||||||||||||||
| 6. In the Add application window, select Confidential Application. | ||||||||||||||
| 7. Select Launch workflow. | ||||||||||||||
| 8. In the Add application details page, Enter name and description as shown below. | ||||||||||||||
|
|
||||||||||||||
|  | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add descriptive alt text for accessibility. Apply this diff: -
+📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| 9. Once the Integrated Application is created, Click on "OAuth configuration" tab. | ||||||||||||||
| 10. Click on "Edit OAuth configuration" button. | ||||||||||||||
| 11. Configure the application as OAuth client by selecting "Configure this application as a client now" radio button. | ||||||||||||||
| 12. Select "Authorization code" grant type. If you are planning to use the same OAuth client application for token exchange then select "Client credentials" grant type as well. In the sample, we will use the same client. | ||||||||||||||
| 13. For Authorization grant type, select redirect URL. This is, in most cases, will be MCP server URL followed by "/oauth/callback". | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent redirect path in documentation. Line 54 states the redirect URL is Apply this diff: -13. For Authorization grant type, select redirect URL. This is, in most cases, will be MCP server URL followed by "/oauth/callback".
+13. For Authorization grant type, select redirect URL. This is, in most cases, will be MCP server URL followed by "/auth/callback".📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
|  | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add descriptive alt text for accessibility. Apply this diff: -
+📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| 14. Click on "Submit" button to update OAuth configuration for the client application. | ||||||||||||||
| **Note: You don't need to do any special configuration to support PKCE for the OAuth client.** | ||||||||||||||
| 15. Make sure to Activate the client application. | ||||||||||||||
| 16. Note down client ID and client secret for the application. Update .env file and replace IAM_CLIENT_ID and IAM_CLIENT_SECRET values. | ||||||||||||||
| 17. IAM_DOMAIN in the env file is the Identity domain URL that you chose for the MCP server. | ||||||||||||||
|
|
||||||||||||||
| This is all you need to implement MCP server authentication against OCI IAM. However, if you want to use authenticated user token to invoke OCI control plane APIs and propagate identity to the OCI control plane instead of using a service user account, then you need to implement token exchange. | ||||||||||||||
|
|
||||||||||||||
| ### Token Exchange Setup | ||||||||||||||
| Token exchange helps you exchange logged in user's OCI IAM token for OCI control plane session token aka. UPST. To learn more about token exchange, refer to my [Workload Identity Federation Blog.](https://www.ateam-oracle.com/post/workload-identity-federation) | ||||||||||||||
|
|
||||||||||||||
| For token exchange, we need to configure Identity propagation trust. The blog above talks more about setting up the trust using REST APIs. However, you can use oci cli as well. Before you use the cli command below, make sure to create token exchange OAuth client. In most cases, you can use the same OAuth client that you created above. You will use client ID of the token exchange OAuth client in the cli command below and replace it for {IAM_TOKENEXCHANGE_CLIENT_ID}. | ||||||||||||||
|
|
||||||||||||||
| You will also need to update client secret for the token exchange OAuth client in .env file. It is IAM_TOKENEXCHANGE_CLIENT_SECRET parameter. Update IAM_GUID and IAM_TOKENEXCHANGE_CLIENT_ID as well for the token exchange OAuth client in the .env file. | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| oci identity-domains identity-propagation-trust create \ | ||||||||||||||
| --schemas '["urn:ietf:params:scim:schemas:oracle:idcs:IdentityPropagationTrust"]' \ | ||||||||||||||
| --name "For Token Exchange" --type "JWT" \ | ||||||||||||||
| --issuer "https://identity.oraclecloud.com/" \ | ||||||||||||||
| --endpoint "https://${IAM_GUID}}.identity.oraclecloud.com" \ | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix syntax error in CLI command. The command has an extra closing brace: Apply this diff: ---endpoint "https://${IAM_GUID}}.identity.oraclecloud.com" \
+--endpoint "https://${IAM_GUID}.identity.oraclecloud.com" \📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| --subject-claim-name "sub" --allow-impersonation false \ | ||||||||||||||
| --subject-mapping-attribute "username" \ | ||||||||||||||
| --subject-type "User" --client-claim-name "iss" \ | ||||||||||||||
| --client-claim-values '["https://identity.oraclecloud.com/"]' \ | ||||||||||||||
| --oauth-clients '["{IAM_TOKENEXCHANGE_CLIENT_ID}"]' | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| To exchange access token for OCI token and create a signer object, you need to add below code in MCP server. You can then use the signer object to create any OCI control plane client. | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| _global_token_cache = {} #In memory cache for OCI session token signer | ||||||||||||||
|
|
||||||||||||||
| def get_oci_signer() -> TokenExchangeSigner: | ||||||||||||||
|
|
||||||||||||||
| #Check if the signer exists for the token ID in memory cache | ||||||||||||||
| cached_signer = _global_token_cache.get(tokenID) | ||||||||||||||
| logger.debug(f"Global cached signer: {cached_signer}") | ||||||||||||||
| if cached_signer: | ||||||||||||||
| logger.debug(f"Using globally cached signer for token ID: {tokenID}") | ||||||||||||||
| return cached_signer | ||||||||||||||
|
|
||||||||||||||
| #If the signer is not yet created for the token then create new OCI signer object | ||||||||||||||
| logger.debug(f"Creating new signer for token ID: {tokenID}") | ||||||||||||||
| signer = TokenExchangeSigner( | ||||||||||||||
| jwt_or_func=token, | ||||||||||||||
| oci_domain_id=IAM_GUID, | ||||||||||||||
| client_id=IAM_TOKENEXCHANGE_CLIENT_ID, | ||||||||||||||
| client_secret=IAM_TOKENEXCHANGE_CLIENT_SECRET | ||||||||||||||
| ) | ||||||||||||||
| logger.debug(f"Signer {signer} created for token ID: {tokenID}") | ||||||||||||||
|
|
||||||||||||||
| #Cache the signer object in memory cache | ||||||||||||||
| _global_token_cache[tokenID] = signer | ||||||||||||||
| logger.debug(f"Signer cached for token ID: {tokenID}") | ||||||||||||||
|
|
||||||||||||||
| return signer | ||||||||||||||
| ``` | ||||||||||||||
|
Comment on lines
+88
to
+115
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete code example with undefined variables. The token exchange example references undefined variables ( Users cannot copy and execute this code as-is, violating the coding guideline: "Always include complete, runnable code examples that users can copy and execute." Add the missing context: +```python
+import os
+from oci.auth.signers import TokenExchangeSigner
+from fastmcp.server.dependencies import get_access_token
+
+# Configuration from environment
+IAM_GUID = os.environ["IAM_GUID"]
+IAM_TOKENEXCHANGE_CLIENT_ID = os.environ["IAM_TOKENEXCHANGE_CLIENT_ID"]
+IAM_TOKENEXCHANGE_CLIENT_SECRET = os.environ["IAM_TOKENEXCHANGE_CLIENT_SECRET"]
+
_global_token_cache = {} #In memory cache for OCI session token signer
def get_oci_signer() -> TokenExchangeSigner:
+ # Get the access token from the current request
+ access_token_obj = get_access_token()
+ token = access_token_obj.token
+ tokenID = access_token_obj.claims.get("jti") # JWT ID as cache key
#Check if the signer exists for the token ID in memory cache🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| ## Running MCP server | ||||||||||||||
|
|
||||||||||||||
| Once the setup is complete, to run the MCP server, run the below command. | ||||||||||||||
| ``` | ||||||||||||||
| fastmcp run server.py:mcp --transport http --port 8000 | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| To run MCP client, run the below command. | ||||||||||||||
| ``` | ||||||||||||||
| python3 client.py | ||||||||||||||
| ``` | ||||||||||||||
| MCP Client sample is as below. | ||||||||||||||
| ``` | ||||||||||||||
| from fastmcp import Client | ||||||||||||||
| import asyncio | ||||||||||||||
|
|
||||||||||||||
| async def main(): | ||||||||||||||
| # The client will automatically handle OCI OAuth flows | ||||||||||||||
| async with Client("http://localhost:8000/mcp/", auth="oauth") as client: | ||||||||||||||
| # First-time connection will open OCI login in your browser | ||||||||||||||
| print("✓ Authenticated with OCI IAM") | ||||||||||||||
|
|
||||||||||||||
| tools = await client.list_tools() | ||||||||||||||
| print(f"🔧 Available tools ({len(tools)}):") | ||||||||||||||
| for tool in tools: | ||||||||||||||
| print(f" - {tool.name}: {tool.description}") | ||||||||||||||
|
|
||||||||||||||
| if __name__ == "__main__": | ||||||||||||||
| asyncio.run(main()) | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| When you run the client for the first time: | ||||||||||||||
| 1. Your browser will open to OCI IAM's login page | ||||||||||||||
| 2. Sign in with your OCI account and grant the requested permissions | ||||||||||||||
| 3. After authorization, you'll be redirected back | ||||||||||||||
| 4. The client receives the token and can make authenticated requests | ||||||||||||||
|
|
||||||||||||||
| ## Production Configuration | ||||||||||||||
|
|
||||||||||||||
| <VersionBadge version="2.13.0" /> | ||||||||||||||
|
|
||||||||||||||
| For production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`: | ||||||||||||||
|
|
||||||||||||||
| ```python server.py | ||||||||||||||
|
|
||||||||||||||
| import os | ||||||||||||||
| from fastmcp import FastMCP | ||||||||||||||
| from fastmcp.server.auth.providers.oci import OCIProvider | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect import path. The import path Apply this diff: -from fastmcp.server.auth.providers.oci import OCIProvider
+from fastmcp.server.auth.providers.ociprovider import OCIProvider📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| from key_value.aio.stores.redis import RedisStore | ||||||||||||||
| from key_value.aio.wrappers.encryption import FernetEncryptionWrapper | ||||||||||||||
| from cryptography.fernet import Fernet | ||||||||||||||
|
|
||||||||||||||
| # Production setup with encrypted persistent token storage | ||||||||||||||
| auth_provider = OCIProvider( | ||||||||||||||
| config_url="https://{IDCS_GUID}.identity.oraclecloud.com/.well-known/openid-configuration", | ||||||||||||||
| client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB", | ||||||||||||||
| client_secret="idcscsvPYqbjemq...", | ||||||||||||||
|
Comment on lines
+173
to
+174
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove hardcoded credentials from example. The example includes what appear to be real client IDs and partial secrets. Even if these are fake, it sets a bad precedent. Based on coding guidelines: "Use realistic data instead of placeholder values" - but for credentials, use environment variables. Apply this diff: config_url="https://{IDCS_GUID}.identity.oraclecloud.com/.well-known/openid-configuration",
- client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB",
- client_secret="idcscsvPYqbjemq...",
+ client_id=os.environ["OCI_CLIENT_ID"],
+ client_secret=os.environ["OCI_CLIENT_SECRET"],
base_url="https://your-production-domain.com",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| base_url="https://your-production-domain.com", | ||||||||||||||
|
|
||||||||||||||
| # Production token management | ||||||||||||||
| jwt_signing_key=os.environ["JWT_SIGNING_KEY"], | ||||||||||||||
| client_storage=FernetEncryptionWrapper( | ||||||||||||||
| key_value=RedisStore( | ||||||||||||||
| host=os.environ["REDIS_HOST"], | ||||||||||||||
| port=int(os.environ["REDIS_PORT"]) | ||||||||||||||
| ), | ||||||||||||||
| fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"]) | ||||||||||||||
| ) | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| mcp = FastMCP(name="Production OCI App", auth=auth_provider) | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| <Note> | ||||||||||||||
| Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at Rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments. | ||||||||||||||
|
|
||||||||||||||
| For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters). | ||||||||||||||
| </Note> | ||||||||||||||
|
|
||||||||||||||
| <Info> | ||||||||||||||
| The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache. | ||||||||||||||
| </Info> | ||||||||||||||
|
|
||||||||||||||
| ## Environment Variables | ||||||||||||||
|
|
||||||||||||||
| For production deployments, use environment variables instead of hardcoding credentials. | ||||||||||||||
|
|
||||||||||||||
| ### Provider Selection | ||||||||||||||
|
|
||||||||||||||
| Setting this environment variable allows the OCI provider to be used automatically without explicitly instantiating it in code. | ||||||||||||||
|
|
||||||||||||||
| <Card> | ||||||||||||||
| <ParamField path="FASTMCP_SERVER_AUTH" default="Not set"> | ||||||||||||||
| Set to `fastmcp.server.auth.providers.oci.OCIProvider` to use OCI IAM authentication. | ||||||||||||||
| </ParamField> | ||||||||||||||
| </Card> | ||||||||||||||
|
|
||||||||||||||
| ### OCI-Specific Configuration | ||||||||||||||
|
|
||||||||||||||
| These environment variables provide default values for the OCI IAM provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`. | ||||||||||||||
|
|
||||||||||||||
| <Card> | ||||||||||||||
| <ParamField path="FASTMCP_SERVER_AUTH_OCI_CONFIG_URL" required> | ||||||||||||||
| Your OCI Application Configuration URL (e.g., `https://{IDCS_GUID}.identity.oraclecloud.com/.well-known/openid-configuration`) | ||||||||||||||
| </ParamField> | ||||||||||||||
|
|
||||||||||||||
| <ParamField path="FASTMCP_SERVER_AUTH_OCI_CLIENT_ID" required> | ||||||||||||||
| Your OCI Application Client ID (e.g., `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`) | ||||||||||||||
| </ParamField> | ||||||||||||||
|
|
||||||||||||||
| <ParamField path="FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET" required> | ||||||||||||||
| Your OCI Application Client Secret (e.g., `vPYqbjemq...`) | ||||||||||||||
| </ParamField> | ||||||||||||||
|
|
||||||||||||||
| <ParamField path="FASTMCP_SERVER_AUTH_OCI_BASE_URL" required> | ||||||||||||||
| Public URL where OAuth endpoints will be accessible (includes any mount path) | ||||||||||||||
| </ParamField> | ||||||||||||||
|
|
||||||||||||||
| <ParamField path="FASTMCP_SERVER_AUTH_OCI_REDIRECT_PATH" default="/auth/callback"> | ||||||||||||||
| Redirect path configured in your OCI IAM Integrated Application | ||||||||||||||
| </ParamField> | ||||||||||||||
|
|
||||||||||||||
| </Card> | ||||||||||||||
|
|
||||||||||||||
| Example `.env` file: | ||||||||||||||
| ```bash | ||||||||||||||
| # Use the OCI IAM provider | ||||||||||||||
| FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.oci.OCIProvider | ||||||||||||||
|
|
||||||||||||||
| # OCI IAM configuration and credentials | ||||||||||||||
| FASTMCP_SERVER_AUTH_OCI_CONFIG_URL=https://{IDCS_GUID}.identity.oraclecloud.com/.well-known/openid-configuration | ||||||||||||||
| FASTMCP_SERVER_AUTH_OCI_CLIENT_ID=tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove potentially real client ID from documentation. Line 249 contains what appears to be a real client ID ( Based on static analysis hints. Apply this diff: -FASTMCP_SERVER_AUTH_OCI_CLIENT_ID=tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB
+FASTMCP_SERVER_AUTH_OCI_CLIENT_ID=your_oci_client_id_here📝 Committable suggestion
Suggested change
🧰 Tools🪛 Gitleaks (8.28.0)[high] 249-249: Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (generic-api-key) 🤖 Prompt for AI Agents |
||||||||||||||
| FASTMCP_SERVER_AUTH_OCI_CLIENT_SECRET=idcscsvPYqbjemq... | ||||||||||||||
| FASTMCP_SERVER_AUTH_OCI_BASE_URL=https://your-server.com | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| With environment variables set, your server code simplifies to: | ||||||||||||||
|
|
||||||||||||||
| ```python server.py | ||||||||||||||
| from fastmcp import FastMCP | ||||||||||||||
|
|
||||||||||||||
| # Authentication is automatically configured from environment | ||||||||||||||
| mcp = FastMCP(name="OCI Secured App") | ||||||||||||||
|
|
||||||||||||||
| @mcp.tool | ||||||||||||||
| def whoami() -> str: | ||||||||||||||
| """The whoami function is to test MCP server without requiring token exchange. | ||||||||||||||
| This tool can be used to test successful authentication against OCI IAM. | ||||||||||||||
| It will return logged in user's subject (username from IAM domain).""" | ||||||||||||||
| token = get_access_token() | ||||||||||||||
| user = token.claims.get("sub") | ||||||||||||||
| return f"You are User: {user}" | ||||||||||||||
|
Comment on lines
+267
to
+269
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add missing import to make example complete. The Apply this diff: ```python server.py
from fastmcp import FastMCP
+from fastmcp.server.dependencies import get_access_token
# Authentication is automatically configured from environment🤖 Prompt for AI Agents |
||||||||||||||
| ``` | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add descriptive alt text for accessibility.
The image uses generic "alt text" placeholder instead of describing the image content, which fails accessibility requirements.
Based on coding guidelines: "Include descriptive alt text for all images and diagrams."
Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents