From 142b5a865b62a5410a6766252effb3879d60f67a Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Sat, 24 Jan 2026 22:04:14 +0800 Subject: [PATCH 1/7] docs: add provider guides and simplify tutorial --- docs/provider-guides/generic.mdx | 90 +++++++++++++++++ docs/provider-guides/logto.mdx | 98 +++++++++++++++++++ docs/tutorials/todo-manager/README.mdx | 130 +++---------------------- sidebars.ts | 10 ++ 4 files changed, 211 insertions(+), 117 deletions(-) create mode 100644 docs/provider-guides/generic.mdx create mode 100644 docs/provider-guides/logto.mdx diff --git a/docs/provider-guides/generic.mdx b/docs/provider-guides/generic.mdx new file mode 100644 index 0000000..7a96d3a --- /dev/null +++ b/docs/provider-guides/generic.mdx @@ -0,0 +1,90 @@ +--- +sidebar_position: 100 +sidebar_label: Generic OAuth 2.0 / OIDC +--- + +# Generic OAuth 2.0 / OIDC + +This guide covers general configuration steps for OAuth 2.0 and OpenID Connect providers. Since OIDC is built on top of OAuth 2.0, both follow similar steps. + +:::tip +Check our [Provider List](/provider-list) to see if your specific provider has been tested with MCP Auth. +::: + +## Get issuer URL {#get-issuer-url} + +The issuer URL (also called authorization server URL or base URL) is required for MCP Auth configuration. To find it: + +1. Check your provider's documentation for the authorization server URL +2. Some providers expose this at `https://{your-domain}/.well-known/oauth-authorization-server` +3. For OIDC providers, try `https://{your-domain}/.well-known/openid-configuration` +4. Look in your provider's admin console under OAuth/API settings + +## Configure scopes {#configure-scopes} + +You'll need to define scopes in your authorization server that represent the permissions your MCP server needs: + +1. **Define scopes** in your authorization server, e.g.: + - `create:todos` + - `read:todos` + - `delete:todos` + +2. **Assign scopes to users** using your provider's interface + - Some providers support role-based management + - Others use direct scope assignments + +Check your provider's documentation for specific instructions on scope management. + +## Token request parameters {#token-request-parameters} + +Different authorization servers use various approaches for specifying the target resource: + +### Resource indicator based + +Uses the `resource` parameter ([RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707)): + +```json +{ + "resource": "http://localhost:3001/", + "scope": "create:todos read:todos" +} +``` + +### Audience based + +Uses the `audience` parameter: + +```json +{ + "audience": "todo-api", + "scope": "create:todos read:todos" +} +``` + +### Pure scope based + +Relies solely on scopes (traditional OAuth 2.0): + +```json +{ + "scope": "todo-api:create todo-api:read openid profile" +} +``` + +Check your provider's documentation for supported parameters. + +## Register MCP client {#register-mcp-client} + +If your provider supports [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591) or [OAuth Client ID Metadata Document](https://www.ietf.org/archive/id/draft-parecki-oauth-client-id-metadata-document-00.html), you may skip manual registration. Otherwise: + +1. Sign in to your provider's console +2. Navigate to "Applications" or "Clients" section +3. Create a new application/client +4. Select "Native App" or "Public client" if required +5. Configure the redirect URIs. For VS Code: + ``` + http://127.0.0.1 + https://vscode.dev/redirect + ``` +6. Configure the required scopes/permissions +7. Copy the "Client ID" or "Application ID" for use in your MCP client diff --git a/docs/provider-guides/logto.mdx b/docs/provider-guides/logto.mdx new file mode 100644 index 0000000..c3a085f --- /dev/null +++ b/docs/provider-guides/logto.mdx @@ -0,0 +1,98 @@ +--- +sidebar_position: 1 +sidebar_label: Logto +--- + +# Logto + +[Logto](https://logto.io) is an open-source identity platform that provides OpenID Connect authentication with built-in RBAC support through API resources and roles. + +## Get issuer URL {#get-issuer-url} + +You can find the issuer URL on your application details page within Logto Console, under the "Endpoints & Credentials / Issuer endpoint" section. It should look like: + +``` +https://my-project.logto.app/oidc +``` + +## Create API resource and scopes {#create-api-resource-and-scopes} + +1. Sign in to [Logto Console](https://cloud.logto.io) (or your self-hosted Logto Console) +2. Go to "API Resources" +3. Create a new API resource: + - **Name**: e.g., "Todo Manager" + - **Resource indicator**: Your MCP server URL, e.g., `http://localhost:3001/` + - The resource indicator must match your MCP server's URL. +4. Add the scopes your MCP server needs, e.g.: + - `create:todos`: "Create new todo items" + - `read:todos`: "Read all todo items" + - `delete:todos`: "Delete any todo item" + +:::note[Trailing slash in resource indicator] +Always include a trailing slash (`/`) in the resource indicator. Due to a current bug in the MCP official SDK, clients using the SDK will automatically append a trailing slash to resource identifiers when initiating auth requests. +::: + +## Create roles {#create-roles} + +Roles make it easier to manage permissions for groups of users: + +1. Go to "Roles" +2. Create roles with appropriate scopes, e.g.: + - **Admin**: Assign all scopes + - **User**: Assign limited scopes +3. (Optional) Set a default role for new users in the role's "General" tab. + +## Assign permissions to users {#assign-permissions-to-users} + +1. Go to "User management" +2. Select a user +3. In the "Roles" tab, assign the appropriate roles + +:::tip Programmatic Role Management +You can use Logto's [Management API](https://docs.logto.io/integrate-logto/interact-with-management-api) to programmatically manage user roles. +::: + +## Register MCP client {#register-mcp-client} + +Since Logto does not support Dynamic Client Registration yet, you need to manually register your MCP client in Logto Console. + +### Third-party vs. first-party applications + +Before creating the application, you need to understand the difference: + +- **Third-party application**: Use this when the MCP client is developed by someone else (e.g., VS Code, Cursor, or other community tools). These clients need to access your users' data, but are not under your control. Users will see a consent screen asking them to authorize the MCP client to access their information. +- **First-party application**: Use this when you are building your own MCP client (e.g., an AI assistant embedded in your own product). In this case, both the MCP client and MCP server are managed by you, and the users are your own users. No consent screen is needed. + +### Application type + +Choose the application type based on how the MCP client runs: + +| MCP Client | Application Type | +| ---------- | ---------------- | +| VS Code, Cursor (desktop apps) | Native App | +| MCP Inspector (browser-based) | Single Page App (SPA) | + +### Register a third-party app + +Take VS Code as an example: + +1. Navigate to **Applications > Third-party apps** and click "Create application". +2. Select **Native App** as the application type (since VS Code is a desktop application). +3. Fill in the application name (e.g., "VS Code") and description. +4. Set the **Redirect URIs** (check the MCP client's documentation for the required URIs): + ``` + http://127.0.0.1 + https://vscode.dev/redirect + ``` +5. Click "Save changes". +6. Go to the app's **Permissions** tab, under **User** section, add the required permissions from your API resource (e.g., `create:todos`, `read:todos`, `delete:todos`). +7. Copy the "App ID" value for use in VS Code. + +### Register a first-party app + +If you are building your own MCP client: + +1. Navigate to **Applications** and click "Create application". +2. Select the appropriate application type based on your client (Native App, SPA, etc.). +3. Complete the setup following the in-app guide. +4. Copy the "App ID" (and "App Secret" if applicable) for use in your MCP client. diff --git a/docs/tutorials/todo-manager/README.mdx b/docs/tutorials/todo-manager/README.mdx index 3a8df94..14b68e7 100644 --- a/docs/tutorials/todo-manager/README.mdx +++ b/docs/tutorials/todo-manager/README.mdx @@ -4,11 +4,13 @@ sidebar_label: 'Tutorial: Build a todo manager' --- import { NpmLikeInstallation } from '@site/src/components/NpmLikeInstallation'; -import TabItem from '@theme/TabItem'; -import Tabs from '@theme/Tabs'; # Tutorial: Build a todo manager +:::tip Using a different authorization server? +This tutorial uses [Logto](https://logto.io) as the example authorization server. If you're using a different provider, check out our [Provider Guides](/docs/provider-guides/generic) for configuration steps. +::: + :::tip Python SDK available MCP Auth is also available for Python! Check out the [Python SDK repository](https://github.com/mcp-auth/python) for installation and usage. ::: @@ -67,10 +69,7 @@ sequenceDiagram To implement [role-based access control (RBAC)](https://auth.wiki/rbac) in your MCP server, your authorization server needs to support issuing access tokens with scopes. Scopes represent the permissions that a user has been granted. - - - -[Logto](https://logto.io) provides RBAC support through its API resources (conforming [RFC 8707: Resource Indicators for OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc8707)) and roles features. Here's how to set it up: +[Logto](https://logto.io) provides RBAC support through its API resources (conforming [RFC 8707: Resource Indicators for OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc8707)) and roles features. Here's a quick overview: 1. Sign in to [Logto Console](https://cloud.logto.io) (or your self-hosted Logto Console) @@ -98,24 +97,7 @@ To implement [role-based access control (RBAC)](https://auth.wiki/rbac) in your The scopes will be included in the JWT access token's `scope` claim as a space-separated string. - - - -OAuth 2.0 / OIDC providers typically support scope-based access control. When implementing RBAC: - -1. Define your required scopes in your authorization server -2. Configure your client to request these scopes during the authorization flow -3. Ensure your authorization server includes the granted scopes in the access token -4. The scopes are usually included in the JWT access token's `scope` claim - -Check your provider's documentation for specific details on: - -- How to define and manage scopes -- How scopes are included in the access token -- Any additional RBAC features like role management - - - +> 📖 See [Logto Provider Guide](/docs/provider-guides/logto) for detailed setup instructions. ### Validating tokens and checking permissions \{#validating-tokens-and-checking-permissions} @@ -218,12 +200,7 @@ To dive deeper into RBAC concepts and best practices, check out [Mastering RBAC: ## Configure authorization in your provider \{#configure-authorization-in-your-provider} -To implement the access control system we described earlier, you'll need to configure your authorization server to support the required scopes. Here's how to do it with different providers: - - - - -[Logto](https://logto.io) provides RBAC support through its API resources and roles features. Here's how to set it up: +To implement the access control system we described earlier, you'll need to configure your authorization server to support the required scopes. 1. Sign in to [Logto Console](https://cloud.logto.io) (or your self-hosted Logto Console) @@ -258,35 +235,6 @@ You can also use Logto's [Management API](https://docs.logto.io/integrate-logto/ When requesting an access token, Logto will include scopes in the token's `scope` claim based on the user's role permissions. - - - -For OAuth 2.0 or OpenID Connect providers, you'll need to configure the scopes that represent different permissions. The exact steps will depend on your provider, but generally: - -1. Define scopes: - - - Configure your authorization server to support: - - `create:todos` - - `read:todos` - - `delete:todos` - -2. Configure client: - - - Register or update your client to request these scopes - - Ensure the scopes are included in the access token - -3. Assign permissions: - - Use your provider's interface to grant appropriate scopes to users - - Some providers may support role-based management, while others might use direct scope assignments - - Check your provider's documentation for the recommended approach - -:::tip -Most providers will include the granted scopes in the access token's `scope` claim. The format is typically a space-separated string of scope values. -::: - - - - :::note[Trailing slash in resource indicator] Always include a trailing slash (`/`) in the resource indicator. Due to a current bug in the MCP official SDK, clients using the SDK will automatically append a trailing slash to resource identifiers when initiating auth requests. If your resource indicator doesn't include the trailing slash, resource validation will fail for those clients. (VS Code is not affected by this bug.) ::: @@ -512,14 +460,15 @@ When requesting access tokens from different authorization servers, you'll encou -While each provider may have its own specific requirements, the following steps will guide you through the process of integrating VS Code and the MCP server with provider-specific configurations. +While each provider may have its own specific requirements, the following steps will guide you through the process of integrating VS Code and the MCP server. ### Register MCP client as a third-party app \{#register-mcp-client-as-a-third-party-app} - - +:::info Why third-party app? +Since VS Code is not controlled by you (the MCP server operator), it should be registered as a third-party application. Users will see a consent screen when authorizing. -Integrating the todo manager with [Logto](https://logto.io) is straightforward as it's an OpenID Connect provider that supports resource indicators and scopes, allowing you to secure your todo API with `http://localhost:3001/` as the resource indicator. +If you're building your own MCP client, check the [Logto Provider Guide](/docs/provider-guides/logto#register-mcp-client) for first-party app registration. +::: Since Logto does not support Dynamic Client Registration yet, you will need to manually register your MCP client (VS Code) as a third-party app in your Logto tenant: @@ -538,39 +487,6 @@ Since Logto does not support Dynamic Client Registration yet, you will need to m 7. Go to the app's **Permissions** tab, under **User** section, add the `create:todos`, `read:todos`, and `delete:todos` permissions from the **Todo Manager** API resource you created earlier. 8. In the top card, you will see the "App ID" value. Copy it for later use. - - - -:::note -This is a generic OAuth 2.0 / OpenID Connect provider integration guide. Both OAuth 2.0 and OIDC follow similar steps as OIDC is built on top of OAuth 2.0. Check your provider's documentation for specific details. -::: - -If your provider supports Dynamic Client Registration, you may skip the manual registration; otherwise, you will need to manually register the MCP client: - -1. Sign in to your provider's console. - -2. Navigate to the "Applications" or "Clients" section, then create a new application or client. - -3. If your provider requires a client type, select "Native App" or "Public client". - -4. After creating the application, configure the redirect URIs. For VS Code, add the following: - - ``` - http://127.0.0.1 - https://vscode.dev/redirect - ``` - -5. Configure the required scopes/permissions for the application: - - ```text - create:todos read:todos delete:todos - ``` - -6. Find the "Client ID" or "Application ID" of the newly created application and copy it for later use. - - - - ### Set up MCP Auth \{#set-up-mcp-auth} First, install the MCP Auth SDK in your MCP server project. @@ -586,27 +502,7 @@ There are two ways to configure authorization servers: #### Configure protected resource metadata \{#configure-protected-resource-metadata} -First, get your authorization server's issuer URL: - - - - - -In Logto, you can find the issuer URL on your application details page within Logto Console, under the "Endpoints & Credentials / Issuer endpoint" section. It should look like `https://my-project.logto.app/oidc`. - - - - - -For OAuth 2.0 providers, you'll need to: - -1. Check your provider's documentation for the authorization server URL (often called issuer URL or base URL) -2. Some providers may expose this at `https://{your-domain}/.well-known/oauth-authorization-server` -3. Look in your provider's admin console under OAuth/API settings - - - - +First, get your authorization server's issuer URL. In Logto, you can find the issuer URL on your application details page within Logto Console, under the "Endpoints & Credentials / Issuer endpoint" section. It should look like `https://my-project.logto.app/oidc`. Now, configure the Protected Resource Metadata when building the MCP Auth instance: diff --git a/sidebars.ts b/sidebars.ts index 667d6c2..bb50f68 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -26,6 +26,16 @@ const sidebars: SidebarsConfig = { label: 'Tutorials', items: ['tutorials/todo-manager/README'], }, + { + type: 'category', + label: 'Provider Guides', + link: { + type: 'generated-index', + title: 'Provider Guides', + description: 'Detailed configuration guides for different authorization server providers.', + }, + items: ['provider-guides/logto', 'provider-guides/generic'], + }, { type: 'category', label: 'References', From f6ecc1a11b4877f05144f9b701cf9085ecbec3c9 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Sat, 24 Jan 2026 22:39:18 +0800 Subject: [PATCH 2/7] refactor: migrate whoami tutorial --- docs/tutorials/todo-manager/README.mdx | 2 +- docs/tutorials/whoami/README.mdx | 665 ++++++++++++++++++ .../whoami/_manual-metadata-fetching.mdx | 3 + docs/tutorials/whoami/_setup-oauth.mdx | 141 ++++ docs/tutorials/whoami/_setup-oidc.mdx | 143 ++++ docs/tutorials/whoami/_transpile-metadata.mdx | 31 + .../whoami/assets/inspector-first-run.png | Bin 0 -> 52293 bytes docs/tutorials/whoami/assets/result.png | Bin 0 -> 65611 bytes sidebars.ts | 7 +- 9 files changed, 985 insertions(+), 7 deletions(-) create mode 100644 docs/tutorials/whoami/README.mdx create mode 100644 docs/tutorials/whoami/_manual-metadata-fetching.mdx create mode 100644 docs/tutorials/whoami/_setup-oauth.mdx create mode 100644 docs/tutorials/whoami/_setup-oidc.mdx create mode 100644 docs/tutorials/whoami/_transpile-metadata.mdx create mode 100644 docs/tutorials/whoami/assets/inspector-first-run.png create mode 100644 docs/tutorials/whoami/assets/result.png diff --git a/docs/tutorials/todo-manager/README.mdx b/docs/tutorials/todo-manager/README.mdx index 14b68e7..a0dd2da 100644 --- a/docs/tutorials/todo-manager/README.mdx +++ b/docs/tutorials/todo-manager/README.mdx @@ -1,6 +1,6 @@ --- sidebar_position: 2 -sidebar_label: 'Tutorial: Build a todo manager' +sidebar_label: 'Build a todo manager' --- import { NpmLikeInstallation } from '@site/src/components/NpmLikeInstallation'; diff --git a/docs/tutorials/whoami/README.mdx b/docs/tutorials/whoami/README.mdx new file mode 100644 index 0000000..cc34c83 --- /dev/null +++ b/docs/tutorials/whoami/README.mdx @@ -0,0 +1,665 @@ +--- +sidebar_position: 3 +sidebar_label: 'Who am I?' +--- + +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +import SetupOauth from './_setup-oauth.mdx'; +import SetupOidc from './_setup-oidc.mdx'; + +# Tutorial: Who am I? + +This tutorial will guide you through the process of setting up MCP Auth to authenticate users and retrieve their identity information from the authorization server. + +After completing this tutorial, you will have: + +- ✅ A basic understanding of how to use MCP Auth to authenticate users. +- ✅ A MCP server that offers a tool to retrieve user identity information. + +## Overview \{#overview} + +The tutorial will involve the following components: + +- **MCP server**: A simple MCP server that uses MCP official SDKs to handle requests. +- **MCP inspector**: A visual testing tool for MCP servers. It also acts as an OAuth / OIDC client to initiate the authorization flow and retrieve access tokens. +- **Authorization server**: An OAuth 2.1 or OpenID Connect provider that manages user identities and issues access tokens. + +Here's a high-level diagram of the interaction between these components: + +```mermaid +sequenceDiagram + participant Client as MCP Inspector + participant Server as MCP Server + participant Auth as Authorization Server + + Client->>Server: Request tool `whoami` + Server->>Client: Return 401 Unauthorized + Client->>Auth: Initiate authorization flow + Auth->>Auth: Complete authorization flow + Auth->>Client: Redirect back with authorization code + Client->>Auth: Exchange code for access token + Auth->>Client: Return access token + Client->>Server: Request `whoami` with access token + Server->>Auth: Fetch user identity with access token + Auth->>Server: Return user identity + Server->>Client: Return user identity +``` + +## Understand your authorization server \{#understand-your-authorization-server} + +### Retrieving user identity information \{#retrieving-user-identity-information} + +To complete this tutorial, your authorization server should offer an API to retrieve user identity information: + + + + +[Logto](https://logto.io) is an OpenID Connect provider that supports the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. You can continue reading as we'll cover the scope configuration later. + + + + +[Keycloak](https://www.keycloak.org) is an open-source identity and access management solution that supports multiple protocols, including OpenID Connect (OIDC). As an OIDC provider, it implements the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. You can continue reading as we'll cover the scope configuration later. + + + + + +[Asgardeo](https://wso2.com/asgardeo) is a cloud-native identity as a service (IDaaS) platform that supports OAuth 2.0 and OpenID Connect (OIDC), providing robust identity and access management for modern applications. + +User information is encoded inside the ID token returned along with the access token. But as an OIDC provider, Asgardeo exposes a [UserInfo endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/request-user-info/) that allows applications to retrieve claims about the authenticated user in the payload. + +You can also discover this endpoint dynamically via the [OIDC discovery endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/discover-oidc-configs) or by navigating to the application's 'Info' tab in the Asgardeo Console. + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. + + + +Most OpenID Connect providers support the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. + +Check your provider's documentation to see if it supports this endpoint. If your provider supports [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), you can also check if the `userinfo_endpoint` is included in the discovery document (response from the `.well-known/openid-configuration` endpoint). + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. Check your provider's documentation to see the mapping of scopes to user identity claims. + + + + +While OAuth 2.0 does not define a standard way to retrieve user identity information, many providers implement their own endpoints to do so. Check your provider's documentation to see how to retrieve user identity information using an access token and what parameters are required to fetch such access token when invoking the authorization flow. + + + + +### Dynamic Client Registration \{#dynamic-client-registration} + +Dynamic Client Registration is not required for this tutorial, but it can be useful if you want to automate the MCP client registration process with your authorization server. Check [Is Dynamic Client Registration required?](/provider-list#is-dcr-required) for more details. + +## Set up the MCP server \{#set-up-the-mcp-server} + +We will use the [MCP official SDKs](https://github.com/modelcontextprotocol) to create a MCP server with a `whoami` tool that retrieves user identity information from the authorization server. + +### Create a new project \{#create-a-new-project} + + + + +```bash +mkdir mcp-server +cd mcp-server +uv init # Or use `pipenv` or `poetry` to create a new virtual environment +``` + + + + +Set up a new Node.js project: + +```bash +mkdir mcp-server +cd mcp-server +npm init -y # Or use `pnpm init` +npm pkg set type="module" +npm pkg set main="whoami.js" +npm pkg set scripts.start="node whoami.js" +``` + + + + +### Install the MCP SDK and dependencies \{#install-the-mcp-sdk-and-dependencies} + + + + +```bash +pip install "mcp[cli]" starlette uvicorn +``` + +Or any other package manager you prefer, such as `uv` or `poetry`. + + + + +```bash +npm install @modelcontextprotocol/sdk express +``` + +Or any other package manager you prefer, such as `pnpm` or `yarn`. + + + + +### Create the MCP server \{#create-the-mcp-server} + +First, let's create an MCP server that implements a `whoami` tool. + + + + +Create a file named `whoami.py` and add the following code: + +```python +from mcp.server.fastmcp import FastMCP +from starlette.applications import Starlette +from starlette.routing import Mount +from typing import Any + +mcp = FastMCP("WhoAmI") + +@mcp.tool() +def whoami() -> dict[str, Any]: + """A tool that returns the current user's information.""" + return {"error": "Not authenticated"} + +app = Starlette( + routes=[Mount('/', app=mcp.sse_app())] +) +``` + +Run the server with: + +```bash +uvicorn whoami:app --host 0.0.0.0 --port 3001 +``` + + + + +:::note +Since the current MCP inspector implementation does not handle authorization flows, we will use the SSE approach to set up the MCP server. We'll update the code here once the MCP inspector supports authorization flows. +::: + +You can also use `pnpm` or `yarn` if you prefer. + +Create a file named `whoami.js` and add the following code: + +```js +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import express from 'express'; + +// Create an MCP server +const server = new McpServer({ + name: 'WhoAmI', + version: '0.0.0', +}); + +// Add a tool to the server that returns the current user's information +server.tool('whoami', async () => { + return { + content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated' }) }], + }; +}); + +// Below is the boilerplate code from MCP SDK documentation +const PORT = 3001; +const app = express(); + +const transports = {}; + +app.get('/sse', async (_req, res) => { + const transport = new SSEServerTransport('/messages', res); + transports[transport.sessionId] = transport; + + res.on('close', () => { + delete transports[transport.sessionId]; + }); + + await server.connect(transport); +}); + +app.post('/messages', async (req, res) => { + const sessionId = String(req.query.sessionId); + const transport = transports[sessionId]; + if (transport) { + await transport.handlePostMessage(req, res, req.body); + } else { + res.status(400).send('No transport found for sessionId'); + } +}); + +app.listen(PORT); +``` + +Run the server with: + +```bash +npm start +``` + + + + +## Inspect the MCP server \{#inspect-the-mcp-server} + +### Clone and run MCP inspector \{#clone-and-run-mcp-inspector} + +Now that we have the MCP server running, we can use the MCP inspector to see if the `whoami` tool is available. + +Due to the limit of the current implementation, we've forked the [MCP inspector](https://github.com/mcp-auth/inspector) to make it more flexible and scalable for authentication and authorization. We've also submitted a pull request to the original repository to include our changes. + +To run the MCP inspector, you can use the following command (Node.js is required): + +```bash +git clone https://github.com/mcp-auth/inspector.git +cd inspector +npm install +npm run dev +``` + +Then, open your browser and navigate to `http://localhost:6274/` (or other URL shown in the terminal) to access the MCP inspector. + +### Connect MCP inspector to the MCP server \{#connect-mcp-inspector-to-the-mcp-server} + +Before we proceed, check the following configuration in MCP inspector: + +- **Transport Type**: Set to `SSE`. +- **URL**: Set to the URL of your MCP server. In our case, it should be `http://localhost:3001/sse`. + +Now you can click the "Connect" button to see if the MCP inspector can connect to the MCP server. If everything is okay, you should see the "Connected" status in the MCP inspector. + +### Checkpoint: Run the `whoami` tool \{#checkpoint-run-the-whoami-tool} + +1. In the top menu of the MCP inspector, click on the "Tools" tab. +2. Click on the "List Tools" button. +3. You should see the `whoami` tool listed on the page. Click on it to open the tool details. +4. You should see the "Run Tool" button in the right side. Click on it to run the tool. +5. You should see the tool result with the JSON response `{"error": "Not authenticated"}`. + +![MCP inspector first run](./assets/inspector-first-run.png) + +## Integrate with your authorization server \{#integrate-with-your-authorization-server} + +To complete this section, there are several considerations to take into account: + +
+**The issuer URL of your authorization server** + +This is usually the base URL of your authorization server, such as `https://auth.example.com`. Some providers may have a path like `https://example.logto.app/oidc`, so make sure to check your provider's documentation. + +
+ +
+**How to retrieve the authorization server metadata** + +- If your authorization server conforms to the [OAuth 2.0 Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) or [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), you can use the MCP Auth built-in utilities to fetch the metadata automatically. +- If your authorization server does not conform to these standards, you will need to manually specify the metadata URL or endpoints in the MCP server configuration. Check your provider's documentation for the specific endpoints. + +
+ +
+**How to register the MCP inspector as a client in your authorization server** + +- If your authorization server supports [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591), you can skip this step as the MCP inspector will automatically register itself as a client. +- If your authorization server does not support Dynamic Client Registration, you will need to manually register the MCP inspector as a client in your authorization server. + +
+ +
+**How to retrieve user identity information and how to configure the authorization request parameters** + +- For OpenID Connect providers: usually you need to request at least the `openid` and `profile` scopes when initiating the authorization flow. This will ensure that the access token returned by the authorization server contains the necessary scopes to access the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. + + Note: Some of the providers may not support the userinfo endpoint. + +- For OAuth 2.0 / OAuth 2.1 providers: check your provider's documentation to see how to retrieve user identity information using an access token and what parameters are required to fetch such access token when invoking the authorization flow. + +
+ +While each provider may have its own specific requirements, the following steps will guide you through the process of integrating the MCP inspector and MCP server with provider-specific configurations. + +### Register MCP inspector as a client \{#register-mcp-inspector-as-a-client} + + + + +Integrating with [Logto](https://logto.io) is straightforward as it's an OpenID Connect provider that supports the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. + +Since Logto does not support Dynamic Client Registration yet, you will need to manually register the MCP inspector as a client in your Logto tenant: + +1. Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. +2. Sign in to [Logto Console](https://cloud.logto.io) (or your self-hosted Logto Console). +3. Navigate to the "Applications" tab, click on "Create application". In the bottom of the page, click on "Create app without framework". +4. Fill in the application details, then click on "Create application": + - **Select an application type**: Choose "Single-page application". + - **Application name**: Enter a name for your application, e.g., "MCP Inspector". +5. In the "Settings / Redirect URIs" section, paste the **Redirect URL (auto-populated)** value you copied from the MCP inspector. Then click on "Save changes" in the bottom bar. +6. In the top card, you will see the "App ID" value. Copy it. +7. Go back to the MCP inspector and paste the "App ID" value in the "OAuth Configuration" section under "Client ID". +8. Enter the value `{"scope": "openid profile email"}` in the "Auth Params" field. This will ensure that the access token returned by Logto contains the necessary scopes to access the userinfo endpoint. + + + + +[Keycloak](https://www.keycloak.org) is an open-source identity and access management solution that supports OpenID Connect protocol. + +While Keycloak supports dynamic client registration, its client registration endpoint does not support CORS, preventing most MCP clients from registering directly. Therefore, we'll need to manually register our client. + +:::note +Although Keycloak can be installed in [various ways](https://www.keycloak.org/guides#getting-started) (bare metal, kubernetes, etc.), for this tutorial, we'll use Docker for a quick and straightforward setup. +::: + +Let's set up a Keycloak instance and configure it for our needs: + +1. First, run a Keycloak instance using Docker following the [official documentation](https://www.keycloak.org/getting-started/getting-started-docker): + +```bash +docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.2.4 start-dev +``` + +2. Access the Keycloak Admin Console (http://localhost:8080/admin) and log in with these credentials: + + - Username: `admin` + - Password: `admin` + +3. Create a new Realm: + + - Click "Create Realm" in the top-left corner + - Enter `mcp-realm` in the "Realm name" field + - Click "Create" + +4. Create a test user: + + - Click "Users" in the left menu + - Click "Create new user" + - Fill in the user details: + - Username: `testuser` + - First name and Last name can be any values + - Click "Create" + - In the "Credentials" tab, set a password and uncheck "Temporary" + +5. Register MCP Inspector as a client: + + - Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. + - In the Keycloak Admin Console, click "Clients" in the left menu + - Click "Create client" + - Fill in the client details: + - Client type: Select "OpenID Connect" + - Client ID: Enter `mcp-inspector` + - Click "Next" + - On the "Capability config" page: + - Ensure "Standard flow" is enabled + - Click "Next" + - On the "Login settings" page: + - Paste the previously copied MCP Inspector callback URL into "Valid redirect URIs" + - Enter `http://localhost:6274` in "Web origins" + - Click "Save" + - Copy the "Client ID" (which is `mcp-inspector`) + +6. Back in the MCP Inspector: + - Paste the copied Client ID into the "Client ID" field in the "OAuth Configuration" section + - Enter the following value in the "Auth Params" field to request the necessary scopes: + +```json +{ "scope": "openid profile email" } +``` + + + + +While Asgardeo supports dynamic client registration via a standard API, the endpoint is protected and requires an access token with the necessary permissions. In this tutorial, we'll register the client manually through the Asgardeo Console. + +:::note +If you don't have an Asgardeo account, you can [sign up for free](https://asgardeo.io). +::: + +Follow these steps to configure Asgardeo for MCP Inspector: + +1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization. + +2. Create a new application: + - Go to **Applications** → **New Application** + - Choose **Single-Page Application** + - Enter an application name like `MCP Inspector` + - In the **Authorized Redirect URLs** field, paste the **Redirect URL** copied from MCP Inspector client application (e.g.: `http://localhost:6274/oauth/callback`) + - Click **Create** + +3. Configure the protocol settings: + - Under the **Protocol** tab: + - Copy the **Client ID** that was auto generated. + - Ensure switching to `JWT` for the `Token Type` in **Access Token** section + - Click **Update** + +4. In MCP Inspector client application: + - Open the **OAuth Configuration** section + - Paste the copied **Client ID** + - Enter the following in the **Auth Params** field to request the necessary scopes: + +```json +{ "scope": "openid profile email" } +``` + + + +:::note +This is a generic OpenID Connect provider integration guide. Check your provider's documentation for specific details. +::: + +If your OpenID Connect provider supports Dynamic Client Registration, you can directly go to step 8 below to configure the MCP inspector; otherwise, you will need to manually register the MCP inspector as a client in your OpenID Connect provider: + +1. Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. +2. Sign in to your OpenID Connect provider's console. +3. Navigate to the "Applications" or "Clients" section, then create a new application or client. +4. If your provider requires a client type, select "Single-page application" or "Public client". +5. After creating the application, you will need to configure the redirect URI. Paste the **Redirect URL (auto-populated)** value you copied from the MCP inspector. +6. Find the "Client ID" or "Application ID" of the newly created application and copy it. +7. Go back to the MCP inspector and paste the "Client ID" value in the "OAuth Configuration" section under "Client ID". +8. For standard OpenID Connect providers, you can enter the following value in the "Auth Params" field to request the necessary scopes to access the userinfo endpoint: + +```json +{ "scope": "openid profile email" } +``` + + + + +:::note +This is a generic OAuth 2.0 / OAuth 2.1 provider integration guide. Check your provider's documentation for specific details. +::: + +If your OAuth 2.0 / OAuth 2.1 provider supports Dynamic Client Registration, you can directly go to step 8 below to configure the MCP inspector; otherwise, you will need to manually register the MCP inspector as a client in your OAuth 2.0 / OAuth 2.1 provider: + +1. Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. +2. Sign in to your OAuth 2.0 / OAuth 2.1 provider's console. +3. Navigate to the "Applications" or "Clients" section, then create a new application or client. +4. If your provider requires a client type, select "Single-page application" or "Public client". +5. After creating the application, you will need to configure the redirect URI. Paste the **Redirect URL (auto-populated)** value you copied from the MCP inspector. +6. Find the "Client ID" or "Application ID" of the newly created application and copy it. +7. Go back to the MCP inspector and paste the "Client ID" value in the "OAuth Configuration" section under "Client ID". +8. Read your provider's documentation to see how to retrieve access tokens for user identity information. You may need to specify the scopes or parameters required to fetch the access token. For example, if your provider requires the `profile` scope to access user identity information, you can enter the following value in the "Auth Params" field: + +```json +{ "scope": "profile" } +``` + + + + +### Set up MCP auth \{#set-up-mcp-auth} + +In your MCP server project, you need to install the MCP Auth SDK and configure it to use your authorization server metadata. + + + + +First, install the `mcpauth` package: + +```bash +pip install mcpauth +``` + +Or any other package manager you prefer, such as `uv` or `poetry`. + + + + +First, install the `mcp-auth` package: + +```bash +npm install mcp-auth +``` + + + + +MCP Auth requires the authorization server metadata to be able to initialize. Depending on your provider: + + + + + +The issuer URL can be found in your application details page in Logto Console, in the "Endpoints & Credentials / Issuer endpoint" section. It should look like `https://my-project.logto.app/oidc`. + + + + + + + +The issuer URL can be found in your Keycloak Admin Console. In your 'mcp-realm', navigate to "Realm settings / Endpoints" section and click on "OpenID Endpoint Configuration" link. The `issuer` field in the JSON document will contain your issuer URL, which should look like `http://localhost:8080/realms/mcp-realm`. + + + + + + + + You can find the issuer URL in the Asgardeo Console. Navigate to the created application, and open the **Info** tab. The **Issuer** field will be displayed there and should look like: + `https://api.asgardeo.io/t//oauth2/token` + + + + + + + +The following code also assumes that the authorization server supports the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. If your provider does not support this endpoint, you will need to check your provider's documentation for the specific endpoint and replace the userinfo endpoint variable with the correct URL. + + + + + + +As we mentioned earlier, OAuth 2.0 does not define a standard way to retrieve user identity information. The following code assumes that your provider has a specific endpoint to retrieve user identity information using an access token. You will need to check your provider's documentation for the specific endpoint and replace the userinfo endpoint variable with the correct URL. + + + + + + +### Update MCP server \{#update-mcp-server} + +We are almost done! It's time to update the MCP server to apply the MCP Auth route and middleware function, then make the `whoami` tool return the actual user identity information. + + + + +```python +@mcp.tool() +def whoami() -> dict[str, Any]: + """A tool that returns the current user's information.""" + return ( + mcp_auth.auth_info.claims + if mcp_auth.auth_info # This will be populated by the Bearer auth middleware + else {"error": "Not authenticated"} + ) + +# ... + +bearer_auth = Middleware(mcp_auth.bearer_auth_middleware(verify_access_token)) +app = Starlette( + routes=[ + # Add the metadata route (`/.well-known/oauth-authorization-server`) + mcp_auth.metadata_route(), + # Protect the MCP server with the Bearer auth middleware + Mount('/', app=mcp.sse_app(), middleware=[bearer_auth]), + ], +) +``` + + + + +```js +server.tool('whoami', ({ authInfo }) => { + return { + content: [ + { type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) }, + ], + }; +}); + +// ... + +app.use(mcpAuth.delegatedRouter()); +app.use(mcpAuth.bearerAuth(verifyToken)); +``` + + + + +## Checkpoint: Run the `whoami` tool with authentication \{#checkpoint-run-the-whoami-tool-with-authentication} + +Restart your MCP server and open the MCP inspector in your browser. When you click the "Connect" button, you should be redirected to your authorization server's sign-in page. + +Once you sign in and back to the MCP inspector, repeat the actions we did in the previous checkpoint to run the `whoami` tool. This time, you should see the user identity information returned by the authorization server. + +![MCP inspector whoami tool result](./assets/result.png) + + + + +:::info +Check out the [MCP Auth Python SDK repository](https://github.com/mcp-auth/python/blob/master/samples/server/whoami.py) for the complete code of the MCP server (OIDC version). +::: + + + + +:::info +Check out the [MCP Auth Node.js SDK repository](https://github.com/mcp-auth/js/blob/master/packages/sample-servers/src) for the complete code of the MCP server (OIDC version). This directory contains both TypeScript and JavaScript versions of the code. +::: + + + + +## Closing notes \{#closing-notes} + +🎊 Congratulations! You have successfully completed the tutorial. Let's recap what we've done: + +- Setting up a basic MCP server with the `whoami` tool +- Integrating the MCP server with an authorization server using MCP Auth +- Configuring the MCP Inspector to authenticate users and retrieve their identity information + +You may also want to explore some advanced topics, including: + +- Using [JWT (JSON Web Token)](https://auth.wiki/jwt) for authentication and authorization +- Leveraging [resource indicators (RFC 8707)](https://auth-wiki.logto.io/resource-indicator) to specify the resources being accessed +- Implementing custom access control mechanisms, such as [role-based access control (RBAC)](https://auth.wiki/rbac) or [attribute-based access control (ABAC)](https://auth.wiki/abac) + +Be sure to check out other tutorials and documentation to make the most of MCP Auth. diff --git a/docs/tutorials/whoami/_manual-metadata-fetching.mdx b/docs/tutorials/whoami/_manual-metadata-fetching.mdx new file mode 100644 index 0000000..d79c6ee --- /dev/null +++ b/docs/tutorials/whoami/_manual-metadata-fetching.mdx @@ -0,0 +1,3 @@ +:::note +If your provider does not support {props.oidc ? 'OpenID Connect Discovery' : 'OAuth 2.0 Authorization Server Metadata'}, you can manually specify the metadata URL or endpoints. Check [Other ways to initialize MCP Auth](/docs/configure-server/mcp-auth#other-ways) for more details. +::: diff --git a/docs/tutorials/whoami/_setup-oauth.mdx b/docs/tutorials/whoami/_setup-oauth.mdx new file mode 100644 index 0000000..40a172b --- /dev/null +++ b/docs/tutorials/whoami/_setup-oauth.mdx @@ -0,0 +1,141 @@ +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +import ManualMetadataFetching from './_manual-metadata-fetching.mdx'; +import MalformedMetadataTranspilation from './_transpile-metadata.mdx'; + + + + + +Update the `whoami.py` to include the MCP Auth configuration: + +```python +from mcpauth import MCPAuth +from mcpauth.config import AuthServerType +from mcpauth.utils import fetch_server_config + +auth_issuer = '' # Replace with your issuer endpoint +auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OAUTH) +mcp_auth = MCPAuth(server=auth_server_config) +``` + + + + +Update the `whoami.js` to include the MCP Auth configuration: + +```js +import { MCPAuth, fetchServerConfig } from 'mcp-auth'; + +const authIssuer = ''; // Replace with your issuer endpoint +const mcpAuth = new MCPAuth({ + server: await fetchServerConfig(authIssuer, { type: 'oauth' }), +}); +``` + + + + + + + +Now, we need to create a custom access token verifier that will fetch the user identity information from the authorization server using the access token provided by the MCP inspector. + + + + +```python +import pydantic +import requests +from mcpauth.exceptions import ( + MCPAuthTokenVerificationException, + MCPAuthTokenVerificationExceptionCode, +) +from mcpauth.types import AuthInfo + +def verify_access_token(token: str) -> AuthInfo: + """ + Verifies the provided Bearer token by fetching user information from the authorization server. + If the token is valid, it returns an `AuthInfo` object containing the user's information. + + :param token: The Bearer token to received from the MCP inspector. + """ + + try: + # The following code assumes your authorization server has an endpoint for fetching user info + # using the access token issued by the authorization flow. + # Adjust the URL and headers as needed based on your provider's API. + response = requests.get( + "https://your-authorization-server.com/userinfo", + headers={"Authorization": f"Bearer {token}"}, + ) + response.raise_for_status() # Ensure we raise an error for HTTP errors + json = response.json() # Parse the JSON response + + # The following code assumes the user info response is an object with a 'sub' field that + # identifies the user. You may need to adjust this based on your provider's API. + return AuthInfo( + token=token, + subject=json.get("sub"), + issuer=auth_issuer, # Use the configured issuer + claims=json, # Include all claims (JSON fields) returned by the endpoint + ) + # `AuthInfo` is a Pydantic model, so validation errors usually mean the response didn't match + # the expected structure + except pydantic.ValidationError as e: + raise MCPAuthTokenVerificationException( + MCPAuthTokenVerificationExceptionCode.INVALID_TOKEN, + cause=e, + ) + # Handle other exceptions that may occur during the request + except Exception as e: + raise MCPAuthTokenVerificationException( + MCPAuthTokenVerificationExceptionCode.TOKEN_VERIFICATION_FAILED, + cause=e, + ) +``` + + + + +```js +import { MCPAuthTokenVerificationError } from 'mcp-auth'; + +/** + * Verifies the provided Bearer token by fetching user information from the authorization server. + * If the token is valid, it returns an `AuthInfo` object containing the user's information. + */ +const verifyToken = async (token) => { + // The following code assumes your authorization server has an endpoint for fetching user info + // using the access token issued by the authorization flow. + // Adjust the URL and headers as needed based on your provider's API. + const response = await fetch('https://your-authorization-server.com/userinfo', { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new MCPAuthTokenVerificationError('token_verification_failed', response); + } + + const userInfo = await response.json(); + + // The following code assumes the user info response is an object with a 'sub' field that + // identifies the user. You may need to adjust this based on your provider's API. + if (typeof userInfo !== 'object' || userInfo === null || !('sub' in userInfo)) { + throw new MCPAuthTokenVerificationError('invalid_token', response); + } + + return { + token, + issuer: authIssuer, + subject: String(userInfo.sub), // Adjust this based on your provider's user ID field + clientId: '', // Client ID is not used in this example, but can be set if needed + scopes: [], + claims: userInfo, + }; +}; +``` + + + diff --git a/docs/tutorials/whoami/_setup-oidc.mdx b/docs/tutorials/whoami/_setup-oidc.mdx new file mode 100644 index 0000000..8a35bf1 --- /dev/null +++ b/docs/tutorials/whoami/_setup-oidc.mdx @@ -0,0 +1,143 @@ +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +import ManualMetadataFetching from './_manual-metadata-fetching.mdx'; +import MalformedMetadataTranspilation from './_transpile-metadata.mdx'; + + + + + +Update the `whoami.py` to include the MCP Auth configuration: + +```python +from mcpauth import MCPAuth +from mcpauth.config import AuthServerType +from mcpauth.utils import fetch_server_config + +auth_issuer = '' # Replace with your issuer endpoint +auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OIDC) +mcp_auth = MCPAuth(server=auth_server_config) +``` + + + + +Update the `whoami.js` to include the MCP Auth configuration: + +```js +import { MCPAuth, fetchServerConfig } from 'mcp-auth'; + +const authIssuer = ''; // Replace with your issuer endpoint +const mcpAuth = new MCPAuth({ + server: await fetchServerConfig(authIssuer, { type: 'oidc' }), +}); +``` + + + + +{props.showAlternative && } +{props.showAlternative && } + +Now, we need to create a custom access token verifier that will fetch the user identity information from the authorization server using the access token provided by the MCP inspector. + + + + +```python +import pydantic +import requests +from mcpauth.exceptions import ( + MCPAuthTokenVerificationException, + MCPAuthTokenVerificationExceptionCode, +) +from mcpauth.types import AuthInfo + +def verify_access_token(token: str) -> AuthInfo: + """ + Verifies the provided Bearer token by fetching user information from the authorization server. + If the token is valid, it returns an `AuthInfo` object containing the user's information. + + :param token: The Bearer token to received from the MCP inspector. + """ + + issuer = auth_server_config.metadata.issuer + endpoint = auth_server_config.metadata.userinfo_endpoint # The provider should support the userinfo endpoint + if not endpoint: + raise ValueError( + "Userinfo endpoint is not configured in the auth server metadata." + ) + + try: + response = requests.get( + endpoint, + headers={"Authorization": f"Bearer {token}"}, # Standard Bearer token header + ) + response.raise_for_status() # Ensure we raise an error for HTTP errors + json = response.json() # Parse the JSON response + return AuthInfo( + token=token, + subject=json.get("sub"), # 'sub' is a standard claim for the subject (user's ID) + issuer=issuer, # Use the issuer from the metadata + claims=json, # Include all claims (JSON fields) returned by the userinfo endpoint + ) + # `AuthInfo` is a Pydantic model, so validation errors usually mean the response didn't match + # the expected structure + except pydantic.ValidationError as e: + raise MCPAuthTokenVerificationException( + MCPAuthTokenVerificationExceptionCode.INVALID_TOKEN, + cause=e, + ) + # Handle other exceptions that may occur during the request + except Exception as e: + raise MCPAuthTokenVerificationException( + MCPAuthTokenVerificationExceptionCode.TOKEN_VERIFICATION_FAILED, + cause=e, + ) +``` + + + + +```js +import { MCPAuthTokenVerificationError } from 'mcp-auth'; + +/** + * Verifies the provided Bearer token by fetching user information from the authorization server. + * If the token is valid, it returns an `AuthInfo` object containing the user's information. + */ +const verifyToken = async (token) => { + const { issuer, userinfoEndpoint } = mcpAuth.config.server.metadata; + + if (!userinfoEndpoint) { + throw new Error('Userinfo endpoint is not configured in the server metadata'); + } + + const response = await fetch(userinfoEndpoint, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new MCPAuthTokenVerificationError('token_verification_failed', response); + } + + const userInfo = await response.json(); + + if (typeof userInfo !== 'object' || userInfo === null || !('sub' in userInfo)) { + throw new MCPAuthTokenVerificationError('invalid_token', response); + } + + return { + token, + issuer, + subject: String(userInfo.sub), // 'sub' is a standard claim for the subject (user's ID) + clientId: '', // Client ID is not used in this example, but can be set if needed + scopes: [], + claims: userInfo, + }; +}; +``` + + + diff --git a/docs/tutorials/whoami/_transpile-metadata.mdx b/docs/tutorials/whoami/_transpile-metadata.mdx new file mode 100644 index 0000000..d13ff01 --- /dev/null +++ b/docs/tutorials/whoami/_transpile-metadata.mdx @@ -0,0 +1,31 @@ +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +In some cases, the provider response may be malformed or not conforming to the expected metadata format. If you are confident that the provider is compliant, you can transpile the metadata via the config option: + + + + +```python +mcp_auth = MCPAuth( + server=fetch_server_config( + # ...other options + transpile_data=lambda data: {**data, 'response_types_supported': ['code']} # [!code highlight] + ) +) +``` + + + + +```ts +const mcpAuth = new MCPAuth({ + server: await fetchServerConfig(authIssuer, { + // ...other options + transpileData: (data) => ({ ...data, response_types_supported: ['code'] }), // [!code highlight] + }), +}); +``` + + + diff --git a/docs/tutorials/whoami/assets/inspector-first-run.png b/docs/tutorials/whoami/assets/inspector-first-run.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e05020396e74a85a8c4a9979e58afeeb3ee987 GIT binary patch literal 52293 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
0KXl$uZJ5}8y6eSNa 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 Q0EFX#r4%Ido*TUVUmTF-f&c&j literal 0 HcmV?d00001 diff --git a/docs/tutorials/whoami/assets/result.png b/docs/tutorials/whoami/assets/result.png new file mode 100644 index 0000000000000000000000000000000000000000..b7cded5beadf3914c39a0a51b7ea9231889b3e43 GIT binary patch literal 65611 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 Xnf%HDI* Date: Sat, 24 Jan 2026 23:03:48 +0800 Subject: [PATCH 3/7] refactor: remove python sdk usage from tutorial --- docs/tutorials/whoami/README.mdx | 301 +++++++------------------------ 1 file changed, 61 insertions(+), 240 deletions(-) diff --git a/docs/tutorials/whoami/README.mdx b/docs/tutorials/whoami/README.mdx index cc34c83..0fb0b1b 100644 --- a/docs/tutorials/whoami/README.mdx +++ b/docs/tutorials/whoami/README.mdx @@ -11,6 +11,10 @@ import SetupOidc from './_setup-oidc.mdx'; # Tutorial: Who am I? +:::tip Python SDK available +MCP Auth is also available for Python! Check out the [Python SDK repository](https://github.com/mcp-auth/python) for installation and usage. +::: + This tutorial will guide you through the process of setting up MCP Auth to authenticate users and retrieve their identity information from the authorization server. After completing this tutorial, you will have: @@ -23,14 +27,14 @@ After completing this tutorial, you will have: The tutorial will involve the following components: - **MCP server**: A simple MCP server that uses MCP official SDKs to handle requests. -- **MCP inspector**: A visual testing tool for MCP servers. It also acts as an OAuth / OIDC client to initiate the authorization flow and retrieve access tokens. +- **VS Code**: A code editor with built-in MCP support. It also acts as an OAuth / OIDC client to initiate the authorization flow and retrieve access tokens. - **Authorization server**: An OAuth 2.1 or OpenID Connect provider that manages user identities and issues access tokens. Here's a high-level diagram of the interaction between these components: ```mermaid sequenceDiagram - participant Client as MCP Inspector + participant Client as VS Code participant Server as MCP Server participant Auth as Authorization Server @@ -105,18 +109,6 @@ We will use the [MCP official SDKs](https://github.com/modelcontextprotocol) to ### Create a new project \{#create-a-new-project} - - - -```bash -mkdir mcp-server -cd mcp-server -uv init # Or use `pipenv` or `poetry` to create a new virtual environment -``` - - - - Set up a new Node.js project: ```bash @@ -128,68 +120,18 @@ npm pkg set main="whoami.js" npm pkg set scripts.start="node whoami.js" ``` - - - ### Install the MCP SDK and dependencies \{#install-the-mcp-sdk-and-dependencies} - - - -```bash -pip install "mcp[cli]" starlette uvicorn -``` - -Or any other package manager you prefer, such as `uv` or `poetry`. - - - - ```bash npm install @modelcontextprotocol/sdk express ``` Or any other package manager you prefer, such as `pnpm` or `yarn`. - - - ### Create the MCP server \{#create-the-mcp-server} First, let's create an MCP server that implements a `whoami` tool. - - - -Create a file named `whoami.py` and add the following code: - -```python -from mcp.server.fastmcp import FastMCP -from starlette.applications import Starlette -from starlette.routing import Mount -from typing import Any - -mcp = FastMCP("WhoAmI") - -@mcp.tool() -def whoami() -> dict[str, Any]: - """A tool that returns the current user's information.""" - return {"error": "Not authenticated"} - -app = Starlette( - routes=[Mount('/', app=mcp.sse_app())] -) -``` - -Run the server with: - -```bash -uvicorn whoami:app --host 0.0.0.0 --port 3001 -``` - - - - :::note Since the current MCP inspector implementation does not handle authorization flows, we will use the SSE approach to set up the MCP server. We'll update the code here once the MCP inspector supports authorization flows. ::: @@ -252,47 +194,6 @@ Run the server with: npm start ``` - - - -## Inspect the MCP server \{#inspect-the-mcp-server} - -### Clone and run MCP inspector \{#clone-and-run-mcp-inspector} - -Now that we have the MCP server running, we can use the MCP inspector to see if the `whoami` tool is available. - -Due to the limit of the current implementation, we've forked the [MCP inspector](https://github.com/mcp-auth/inspector) to make it more flexible and scalable for authentication and authorization. We've also submitted a pull request to the original repository to include our changes. - -To run the MCP inspector, you can use the following command (Node.js is required): - -```bash -git clone https://github.com/mcp-auth/inspector.git -cd inspector -npm install -npm run dev -``` - -Then, open your browser and navigate to `http://localhost:6274/` (or other URL shown in the terminal) to access the MCP inspector. - -### Connect MCP inspector to the MCP server \{#connect-mcp-inspector-to-the-mcp-server} - -Before we proceed, check the following configuration in MCP inspector: - -- **Transport Type**: Set to `SSE`. -- **URL**: Set to the URL of your MCP server. In our case, it should be `http://localhost:3001/sse`. - -Now you can click the "Connect" button to see if the MCP inspector can connect to the MCP server. If everything is okay, you should see the "Connected" status in the MCP inspector. - -### Checkpoint: Run the `whoami` tool \{#checkpoint-run-the-whoami-tool} - -1. In the top menu of the MCP inspector, click on the "Tools" tab. -2. Click on the "List Tools" button. -3. You should see the `whoami` tool listed on the page. Click on it to open the tool details. -4. You should see the "Run Tool" button in the right side. Click on it to run the tool. -5. You should see the tool result with the JSON response `{"error": "Not authenticated"}`. - -![MCP inspector first run](./assets/inspector-first-run.png) - ## Integrate with your authorization server \{#integrate-with-your-authorization-server} To complete this section, there are several considerations to take into account: @@ -313,10 +214,10 @@ This is usually the base URL of your authorization server, such as `https://auth
-**How to register the MCP inspector as a client in your authorization server** +**How to register VS Code as a client in your authorization server** -- If your authorization server supports [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591), you can skip this step as the MCP inspector will automatically register itself as a client. -- If your authorization server does not support Dynamic Client Registration, you will need to manually register the MCP inspector as a client in your authorization server. +- If your authorization server supports [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591), you can skip this step as VS Code will automatically register itself as a client. +- If your authorization server does not support Dynamic Client Registration, you will need to manually register VS Code as a client in your authorization server.
@@ -331,27 +232,28 @@ This is usually the base URL of your authorization server, such as `https://auth -While each provider may have its own specific requirements, the following steps will guide you through the process of integrating the MCP inspector and MCP server with provider-specific configurations. +While each provider may have its own specific requirements, the following steps will guide you through the process of integrating VS Code and MCP server with provider-specific configurations. -### Register MCP inspector as a client \{#register-mcp-inspector-as-a-client} +### Register VS Code as a client \{#register-vs-code-as-a-client} Integrating with [Logto](https://logto.io) is straightforward as it's an OpenID Connect provider that supports the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. -Since Logto does not support Dynamic Client Registration yet, you will need to manually register the MCP inspector as a client in your Logto tenant: +Since Logto does not support Dynamic Client Registration yet, you will need to manually register VS Code as a client in your Logto tenant: -1. Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. -2. Sign in to [Logto Console](https://cloud.logto.io) (or your self-hosted Logto Console). -3. Navigate to the "Applications" tab, click on "Create application". In the bottom of the page, click on "Create app without framework". -4. Fill in the application details, then click on "Create application": - - **Select an application type**: Choose "Single-page application". - - **Application name**: Enter a name for your application, e.g., "MCP Inspector". -5. In the "Settings / Redirect URIs" section, paste the **Redirect URL (auto-populated)** value you copied from the MCP inspector. Then click on "Save changes" in the bottom bar. -6. In the top card, you will see the "App ID" value. Copy it. -7. Go back to the MCP inspector and paste the "App ID" value in the "OAuth Configuration" section under "Client ID". -8. Enter the value `{"scope": "openid profile email"}` in the "Auth Params" field. This will ensure that the access token returned by Logto contains the necessary scopes to access the userinfo endpoint. +1. Sign in to [Logto Console](https://cloud.logto.io) (or your self-hosted Logto Console). +2. Navigate to the "Applications" tab, click on "Create application". In the bottom of the page, click on "Create app without framework". +3. Fill in the application details, then click on "Create application": + - **Select an application type**: Choose "Native app". + - **Application name**: Enter a name for your application, e.g., "VS Code". +4. In the "Settings / Redirect URIs" section, add the following redirect URIs for VS Code, then click on "Save changes" in the bottom bar: + ``` + http://127.0.0.1 + https://vscode.dev/redirect + ``` +5. In the top card, you will see the "App ID" value. Copy it for later use. @@ -393,31 +295,22 @@ docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADM - Click "Create" - In the "Credentials" tab, set a password and uncheck "Temporary" -5. Register MCP Inspector as a client: +5. Register VS Code as a client: - - Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. - In the Keycloak Admin Console, click "Clients" in the left menu - Click "Create client" - Fill in the client details: - Client type: Select "OpenID Connect" - - Client ID: Enter `mcp-inspector` + - Client ID: Enter `vscode` - Click "Next" - On the "Capability config" page: - Ensure "Standard flow" is enabled - Click "Next" - On the "Login settings" page: - - Paste the previously copied MCP Inspector callback URL into "Valid redirect URIs" - - Enter `http://localhost:6274` in "Web origins" + - Add `http://127.0.0.1/*` to "Valid redirect URIs" + - Add `https://vscode.dev/redirect` to "Valid redirect URIs" - Click "Save" - - Copy the "Client ID" (which is `mcp-inspector`) - -6. Back in the MCP Inspector: - - Paste the copied Client ID into the "Client ID" field in the "OAuth Configuration" section - - Enter the following value in the "Auth Params" field to request the necessary scopes: - -```json -{ "scope": "openid profile email" } -``` + - Copy the "Client ID" (which is `vscode`) for later use. @@ -428,31 +321,25 @@ While Asgardeo supports dynamic client registration via a standard API, the endp If you don't have an Asgardeo account, you can [sign up for free](https://asgardeo.io). ::: -Follow these steps to configure Asgardeo for MCP Inspector: +Follow these steps to configure Asgardeo for VS Code: 1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization. 2. Create a new application: - Go to **Applications** → **New Application** - - Choose **Single-Page Application** - - Enter an application name like `MCP Inspector` - - In the **Authorized Redirect URLs** field, paste the **Redirect URL** copied from MCP Inspector client application (e.g.: `http://localhost:6274/oauth/callback`) + - Choose **Standard-Based Application** → **OAuth 2.0/OpenID Connect** + - Enter an application name like `VS Code` + - In the **Authorized Redirect URLs** field, add: + - `http://127.0.0.1` + - `https://vscode.dev/redirect` - Click **Create** 3. Configure the protocol settings: - Under the **Protocol** tab: - - Copy the **Client ID** that was auto generated. + - Copy the **Client ID** that was auto generated for later use. - Ensure switching to `JWT` for the `Token Type` in **Access Token** section - Click **Update** -4. In MCP Inspector client application: - - Open the **OAuth Configuration** section - - Paste the copied **Client ID** - - Enter the following in the **Auth Params** field to request the necessary scopes: - -```json -{ "scope": "openid profile email" } -``` @@ -460,20 +347,15 @@ Follow these steps to configure Asgardeo for MCP Inspector: This is a generic OpenID Connect provider integration guide. Check your provider's documentation for specific details. ::: -If your OpenID Connect provider supports Dynamic Client Registration, you can directly go to step 8 below to configure the MCP inspector; otherwise, you will need to manually register the MCP inspector as a client in your OpenID Connect provider: - -1. Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. -2. Sign in to your OpenID Connect provider's console. -3. Navigate to the "Applications" or "Clients" section, then create a new application or client. -4. If your provider requires a client type, select "Single-page application" or "Public client". -5. After creating the application, you will need to configure the redirect URI. Paste the **Redirect URL (auto-populated)** value you copied from the MCP inspector. -6. Find the "Client ID" or "Application ID" of the newly created application and copy it. -7. Go back to the MCP inspector and paste the "Client ID" value in the "OAuth Configuration" section under "Client ID". -8. For standard OpenID Connect providers, you can enter the following value in the "Auth Params" field to request the necessary scopes to access the userinfo endpoint: +If your OpenID Connect provider supports Dynamic Client Registration, VS Code will automatically register itself; otherwise, you will need to manually register VS Code as a client in your OpenID Connect provider: -```json -{ "scope": "openid profile email" } -``` +1. Sign in to your OpenID Connect provider's console. +2. Navigate to the "Applications" or "Clients" section, then create a new application or client. +3. If your provider requires a client type, select "Native app" or "Public client". +4. Configure the redirect URIs with the following values: + - `http://127.0.0.1` + - `https://vscode.dev/redirect` +5. Find the "Client ID" or "Application ID" of the newly created application and copy it for later use. @@ -482,20 +364,15 @@ If your OpenID Connect provider supports Dynamic Client Registration, you can di This is a generic OAuth 2.0 / OAuth 2.1 provider integration guide. Check your provider's documentation for specific details. ::: -If your OAuth 2.0 / OAuth 2.1 provider supports Dynamic Client Registration, you can directly go to step 8 below to configure the MCP inspector; otherwise, you will need to manually register the MCP inspector as a client in your OAuth 2.0 / OAuth 2.1 provider: - -1. Open your MCP inspector, click on the "OAuth Configuration" button. Copy the **Redirect URL (auto-populated)** value, which should be something like `http://localhost:6274/oauth/callback`. -2. Sign in to your OAuth 2.0 / OAuth 2.1 provider's console. -3. Navigate to the "Applications" or "Clients" section, then create a new application or client. -4. If your provider requires a client type, select "Single-page application" or "Public client". -5. After creating the application, you will need to configure the redirect URI. Paste the **Redirect URL (auto-populated)** value you copied from the MCP inspector. -6. Find the "Client ID" or "Application ID" of the newly created application and copy it. -7. Go back to the MCP inspector and paste the "Client ID" value in the "OAuth Configuration" section under "Client ID". -8. Read your provider's documentation to see how to retrieve access tokens for user identity information. You may need to specify the scopes or parameters required to fetch the access token. For example, if your provider requires the `profile` scope to access user identity information, you can enter the following value in the "Auth Params" field: +If your OAuth 2.0 / OAuth 2.1 provider supports Dynamic Client Registration, VS Code will automatically register itself; otherwise, you will need to manually register VS Code as a client in your OAuth 2.0 / OAuth 2.1 provider: -```json -{ "scope": "profile" } -``` +1. Sign in to your OAuth 2.0 / OAuth 2.1 provider's console. +2. Navigate to the "Applications" or "Clients" section, then create a new application or client. +3. If your provider requires a client type, select "Native app" or "Public client". +4. Configure the redirect URIs with the following values: + - `http://127.0.0.1` + - `https://vscode.dev/redirect` +5. Find the "Client ID" or "Application ID" of the newly created application and copy it for later use. @@ -504,29 +381,12 @@ If your OAuth 2.0 / OAuth 2.1 provider supports Dynamic Client Registration, you In your MCP server project, you need to install the MCP Auth SDK and configure it to use your authorization server metadata. - - - -First, install the `mcpauth` package: - -```bash -pip install mcpauth -``` - -Or any other package manager you prefer, such as `uv` or `poetry`. - - - - First, install the `mcp-auth` package: ```bash npm install mcp-auth ``` - - - MCP Auth requires the authorization server metadata to be able to initialize. Depending on your provider: @@ -576,35 +436,6 @@ As we mentioned earlier, OAuth 2.0 does not define a standard way to retrieve us We are almost done! It's time to update the MCP server to apply the MCP Auth route and middleware function, then make the `whoami` tool return the actual user identity information. - - - -```python -@mcp.tool() -def whoami() -> dict[str, Any]: - """A tool that returns the current user's information.""" - return ( - mcp_auth.auth_info.claims - if mcp_auth.auth_info # This will be populated by the Bearer auth middleware - else {"error": "Not authenticated"} - ) - -# ... - -bearer_auth = Middleware(mcp_auth.bearer_auth_middleware(verify_access_token)) -app = Starlette( - routes=[ - # Add the metadata route (`/.well-known/oauth-authorization-server`) - mcp_auth.metadata_route(), - # Protect the MCP server with the Bearer auth middleware - Mount('/', app=mcp.sse_app(), middleware=[bearer_auth]), - ], -) -``` - - - - ```js server.tool('whoami', ({ authInfo }) => { return { @@ -620,41 +451,31 @@ app.use(mcpAuth.delegatedRouter()); app.use(mcpAuth.bearerAuth(verifyToken)); ``` - - - ## Checkpoint: Run the `whoami` tool with authentication \{#checkpoint-run-the-whoami-tool-with-authentication} -Restart your MCP server and open the MCP inspector in your browser. When you click the "Connect" button, you should be redirected to your authorization server's sign-in page. - -Once you sign in and back to the MCP inspector, repeat the actions we did in the previous checkpoint to run the `whoami` tool. This time, you should see the user identity information returned by the authorization server. +Restart your MCP server and connect VS Code to it. Here's how to connect with authentication: -![MCP inspector whoami tool result](./assets/result.png) +1. In VS Code, press `Command + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux) to open the Command Palette. +2. Type `MCP: Add Server...` and select it. +3. Choose `HTTP` as the server type. +4. Enter the MCP server URL: `http://localhost:3001/sse` +5. After an OAuth request is initiated, VS Code will prompt you to enter the **App ID** (Client ID). Enter the App ID you copied from your authorization server. +6. Since we don't have an **App Secret** (it's a public client), just press Enter to skip. +7. Complete the sign-in flow in your browser. - - - -:::info -Check out the [MCP Auth Python SDK repository](https://github.com/mcp-auth/python/blob/master/samples/server/whoami.py) for the complete code of the MCP server (OIDC version). -::: - - - +Once you sign in, you can use the `whoami` tool in VS Code. This time, you should see the user identity information returned by the authorization server. :::info Check out the [MCP Auth Node.js SDK repository](https://github.com/mcp-auth/js/blob/master/packages/sample-servers/src) for the complete code of the MCP server (OIDC version). This directory contains both TypeScript and JavaScript versions of the code. ::: - - - ## Closing notes \{#closing-notes} 🎊 Congratulations! You have successfully completed the tutorial. Let's recap what we've done: - Setting up a basic MCP server with the `whoami` tool - Integrating the MCP server with an authorization server using MCP Auth -- Configuring the MCP Inspector to authenticate users and retrieve their identity information +- Configuring VS Code to authenticate users and retrieve their identity information You may also want to explore some advanced topics, including: From 6a9dd9c4f74a2e2ae5cc3b14c1c2ab5d318f83a0 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Sat, 24 Jan 2026 23:09:14 +0800 Subject: [PATCH 4/7] refactor: update sample code --- docs/tutorials/whoami/README.mdx | 100 ++++++++++-------- docs/tutorials/whoami/_setup-oauth.mdx | 95 ++--------------- docs/tutorials/whoami/_setup-oidc.mdx | 94 +--------------- docs/tutorials/whoami/_transpile-metadata.mdx | 21 ---- 4 files changed, 67 insertions(+), 243 deletions(-) diff --git a/docs/tutorials/whoami/README.mdx b/docs/tutorials/whoami/README.mdx index 0fb0b1b..321cc32 100644 --- a/docs/tutorials/whoami/README.mdx +++ b/docs/tutorials/whoami/README.mdx @@ -132,57 +132,55 @@ Or any other package manager you prefer, such as `pnpm` or `yarn`. First, let's create an MCP server that implements a `whoami` tool. -:::note -Since the current MCP inspector implementation does not handle authorization flows, we will use the SSE approach to set up the MCP server. We'll update the code here once the MCP inspector supports authorization flows. -::: - You can also use `pnpm` or `yarn` if you prefer. Create a file named `whoami.js` and add the following code: ```js import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; -// Create an MCP server -const server = new McpServer({ - name: 'WhoAmI', - version: '0.0.0', -}); +// Factory function to create an MCP server instance +// In stateless mode, each request needs its own server instance +const createMcpServer = () => { + const mcpServer = new McpServer({ + name: 'WhoAmI', + version: '0.0.0', + }); -// Add a tool to the server that returns the current user's information -server.tool('whoami', async () => { - return { - content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated' }) }], - }; -}); + // Add a tool to the server that returns the current user's information + mcpServer.registerTool( + 'whoami', + { + description: 'Get the current user information', + inputSchema: {}, + }, + () => { + return { + content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated' }) }], + }; + } + ); + + return mcpServer; +}; -// Below is the boilerplate code from MCP SDK documentation const PORT = 3001; const app = express(); -const transports = {}; - -app.get('/sse', async (_req, res) => { - const transport = new SSEServerTransport('/messages', res); - transports[transport.sessionId] = transport; - - res.on('close', () => { - delete transports[transport.sessionId]; +app.post('/', async (request, response) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + const mcpServer = createMcpServer(); + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await mcpServer.connect(transport); + await transport.handleRequest(request, response, request.body); + response.on('close', () => { + transport.close(); + mcpServer.close(); }); - - await server.connect(transport); -}); - -app.post('/messages', async (req, res) => { - const sessionId = String(req.query.sessionId); - const transport = transports[sessionId]; - if (transport) { - await transport.handlePostMessage(req, res, req.body); - } else { - res.status(400).send('No transport found for sessionId'); - } }); app.listen(PORT); @@ -437,16 +435,28 @@ As we mentioned earlier, OAuth 2.0 does not define a standard way to retrieve us We are almost done! It's time to update the MCP server to apply the MCP Auth route and middleware function, then make the `whoami` tool return the actual user identity information. ```js -server.tool('whoami', ({ authInfo }) => { - return { - content: [ - { type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) }, - ], - }; -}); +// In the factory function, update the `whoami` tool to return the actual user identity +mcpServer.registerTool( + 'whoami', + { + description: 'Get the current user information', + inputSchema: {}, + }, + (_params, { authInfo }) => { + return { + content: [ + { + type: 'text', + text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }), + }, + ], + }; + } +); // ... +// Apply MCP Auth middleware before the MCP endpoint app.use(mcpAuth.delegatedRouter()); app.use(mcpAuth.bearerAuth(verifyToken)); ``` @@ -458,7 +468,7 @@ Restart your MCP server and connect VS Code to it. Here's how to connect with au 1. In VS Code, press `Command + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux) to open the Command Palette. 2. Type `MCP: Add Server...` and select it. 3. Choose `HTTP` as the server type. -4. Enter the MCP server URL: `http://localhost:3001/sse` +4. Enter the MCP server URL: `http://localhost:3001/` 5. After an OAuth request is initiated, VS Code will prompt you to enter the **App ID** (Client ID). Enter the App ID you copied from your authorization server. 6. Since we don't have an **App Secret** (it's a public client), just press Enter to skip. 7. Complete the sign-in flow in your browser. diff --git a/docs/tutorials/whoami/_setup-oauth.mdx b/docs/tutorials/whoami/_setup-oauth.mdx index 40a172b..8a72929 100644 --- a/docs/tutorials/whoami/_setup-oauth.mdx +++ b/docs/tutorials/whoami/_setup-oauth.mdx @@ -1,104 +1,24 @@ -import TabItem from '@theme/TabItem'; -import Tabs from '@theme/Tabs'; - import ManualMetadataFetching from './_manual-metadata-fetching.mdx'; import MalformedMetadataTranspilation from './_transpile-metadata.mdx'; - - - - -Update the `whoami.py` to include the MCP Auth configuration: - -```python -from mcpauth import MCPAuth -from mcpauth.config import AuthServerType -from mcpauth.utils import fetch_server_config - -auth_issuer = '' # Replace with your issuer endpoint -auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OAUTH) -mcp_auth = MCPAuth(server=auth_server_config) -``` - - - - Update the `whoami.js` to include the MCP Auth configuration: ```js -import { MCPAuth, fetchServerConfig } from 'mcp-auth'; +import { fetchServerConfig, MCPAuth } from 'mcp-auth'; const authIssuer = ''; // Replace with your issuer endpoint +const authServerConfig = await fetchServerConfig(authIssuer, { type: 'oauth' }); + const mcpAuth = new MCPAuth({ - server: await fetchServerConfig(authIssuer, { type: 'oauth' }), + server: authServerConfig, }); ``` - - - Now, we need to create a custom access token verifier that will fetch the user identity information from the authorization server using the access token provided by the MCP inspector. - - - -```python -import pydantic -import requests -from mcpauth.exceptions import ( - MCPAuthTokenVerificationException, - MCPAuthTokenVerificationExceptionCode, -) -from mcpauth.types import AuthInfo - -def verify_access_token(token: str) -> AuthInfo: - """ - Verifies the provided Bearer token by fetching user information from the authorization server. - If the token is valid, it returns an `AuthInfo` object containing the user's information. - - :param token: The Bearer token to received from the MCP inspector. - """ - - try: - # The following code assumes your authorization server has an endpoint for fetching user info - # using the access token issued by the authorization flow. - # Adjust the URL and headers as needed based on your provider's API. - response = requests.get( - "https://your-authorization-server.com/userinfo", - headers={"Authorization": f"Bearer {token}"}, - ) - response.raise_for_status() # Ensure we raise an error for HTTP errors - json = response.json() # Parse the JSON response - - # The following code assumes the user info response is an object with a 'sub' field that - # identifies the user. You may need to adjust this based on your provider's API. - return AuthInfo( - token=token, - subject=json.get("sub"), - issuer=auth_issuer, # Use the configured issuer - claims=json, # Include all claims (JSON fields) returned by the endpoint - ) - # `AuthInfo` is a Pydantic model, so validation errors usually mean the response didn't match - # the expected structure - except pydantic.ValidationError as e: - raise MCPAuthTokenVerificationException( - MCPAuthTokenVerificationExceptionCode.INVALID_TOKEN, - cause=e, - ) - # Handle other exceptions that may occur during the request - except Exception as e: - raise MCPAuthTokenVerificationException( - MCPAuthTokenVerificationExceptionCode.TOKEN_VERIFICATION_FAILED, - cause=e, - ) -``` - - - - ```js import { MCPAuthTokenVerificationError } from 'mcp-auth'; @@ -107,6 +27,8 @@ import { MCPAuthTokenVerificationError } from 'mcp-auth'; * If the token is valid, it returns an `AuthInfo` object containing the user's information. */ const verifyToken = async (token) => { + const { issuer } = authServerConfig.metadata; + // The following code assumes your authorization server has an endpoint for fetching user info // using the access token issued by the authorization flow. // Adjust the URL and headers as needed based on your provider's API. @@ -128,7 +50,7 @@ const verifyToken = async (token) => { return { token, - issuer: authIssuer, + issuer, subject: String(userInfo.sub), // Adjust this based on your provider's user ID field clientId: '', // Client ID is not used in this example, but can be set if needed scopes: [], @@ -136,6 +58,3 @@ const verifyToken = async (token) => { }; }; ``` - - - diff --git a/docs/tutorials/whoami/_setup-oidc.mdx b/docs/tutorials/whoami/_setup-oidc.mdx index 8a35bf1..16e832a 100644 --- a/docs/tutorials/whoami/_setup-oidc.mdx +++ b/docs/tutorials/whoami/_setup-oidc.mdx @@ -1,105 +1,24 @@ -import TabItem from '@theme/TabItem'; -import Tabs from '@theme/Tabs'; - import ManualMetadataFetching from './_manual-metadata-fetching.mdx'; import MalformedMetadataTranspilation from './_transpile-metadata.mdx'; - - - - -Update the `whoami.py` to include the MCP Auth configuration: - -```python -from mcpauth import MCPAuth -from mcpauth.config import AuthServerType -from mcpauth.utils import fetch_server_config - -auth_issuer = '' # Replace with your issuer endpoint -auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OIDC) -mcp_auth = MCPAuth(server=auth_server_config) -``` - - - - Update the `whoami.js` to include the MCP Auth configuration: ```js -import { MCPAuth, fetchServerConfig } from 'mcp-auth'; +import { fetchServerConfig, MCPAuth } from 'mcp-auth'; const authIssuer = ''; // Replace with your issuer endpoint +const authServerConfig = await fetchServerConfig(authIssuer, { type: 'oidc' }); + const mcpAuth = new MCPAuth({ - server: await fetchServerConfig(authIssuer, { type: 'oidc' }), + server: authServerConfig, }); ``` - - - {props.showAlternative && } {props.showAlternative && } Now, we need to create a custom access token verifier that will fetch the user identity information from the authorization server using the access token provided by the MCP inspector. - - - -```python -import pydantic -import requests -from mcpauth.exceptions import ( - MCPAuthTokenVerificationException, - MCPAuthTokenVerificationExceptionCode, -) -from mcpauth.types import AuthInfo - -def verify_access_token(token: str) -> AuthInfo: - """ - Verifies the provided Bearer token by fetching user information from the authorization server. - If the token is valid, it returns an `AuthInfo` object containing the user's information. - - :param token: The Bearer token to received from the MCP inspector. - """ - - issuer = auth_server_config.metadata.issuer - endpoint = auth_server_config.metadata.userinfo_endpoint # The provider should support the userinfo endpoint - if not endpoint: - raise ValueError( - "Userinfo endpoint is not configured in the auth server metadata." - ) - - try: - response = requests.get( - endpoint, - headers={"Authorization": f"Bearer {token}"}, # Standard Bearer token header - ) - response.raise_for_status() # Ensure we raise an error for HTTP errors - json = response.json() # Parse the JSON response - return AuthInfo( - token=token, - subject=json.get("sub"), # 'sub' is a standard claim for the subject (user's ID) - issuer=issuer, # Use the issuer from the metadata - claims=json, # Include all claims (JSON fields) returned by the userinfo endpoint - ) - # `AuthInfo` is a Pydantic model, so validation errors usually mean the response didn't match - # the expected structure - except pydantic.ValidationError as e: - raise MCPAuthTokenVerificationException( - MCPAuthTokenVerificationExceptionCode.INVALID_TOKEN, - cause=e, - ) - # Handle other exceptions that may occur during the request - except Exception as e: - raise MCPAuthTokenVerificationException( - MCPAuthTokenVerificationExceptionCode.TOKEN_VERIFICATION_FAILED, - cause=e, - ) -``` - - - - ```js import { MCPAuthTokenVerificationError } from 'mcp-auth'; @@ -108,7 +27,7 @@ import { MCPAuthTokenVerificationError } from 'mcp-auth'; * If the token is valid, it returns an `AuthInfo` object containing the user's information. */ const verifyToken = async (token) => { - const { issuer, userinfoEndpoint } = mcpAuth.config.server.metadata; + const { issuer, userinfoEndpoint } = authServerConfig.metadata; if (!userinfoEndpoint) { throw new Error('Userinfo endpoint is not configured in the server metadata'); @@ -138,6 +57,3 @@ const verifyToken = async (token) => { }; }; ``` - - - diff --git a/docs/tutorials/whoami/_transpile-metadata.mdx b/docs/tutorials/whoami/_transpile-metadata.mdx index d13ff01..55920ba 100644 --- a/docs/tutorials/whoami/_transpile-metadata.mdx +++ b/docs/tutorials/whoami/_transpile-metadata.mdx @@ -1,23 +1,5 @@ -import TabItem from '@theme/TabItem'; -import Tabs from '@theme/Tabs'; - In some cases, the provider response may be malformed or not conforming to the expected metadata format. If you are confident that the provider is compliant, you can transpile the metadata via the config option: - - - -```python -mcp_auth = MCPAuth( - server=fetch_server_config( - # ...other options - transpile_data=lambda data: {**data, 'response_types_supported': ['code']} # [!code highlight] - ) -) -``` - - - - ```ts const mcpAuth = new MCPAuth({ server: await fetchServerConfig(authIssuer, { @@ -26,6 +8,3 @@ const mcpAuth = new MCPAuth({ }), }); ``` - - - From 34ca05748071445b6363fb1dead1c7e79f1e4e21 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Sat, 24 Jan 2026 23:29:54 +0800 Subject: [PATCH 5/7] refactor: simplify whoami tutorial and add provider guides --- docs/provider-guides/asgardeo.mdx | 101 +++++++ docs/provider-guides/generic.mdx | 14 + docs/provider-guides/keycloak.mdx | 87 ++++++ docs/provider-guides/logto.mdx | 6 + docs/tutorials/whoami/README.mdx | 258 ++++-------------- .../whoami/_manual-metadata-fetching.mdx | 3 - docs/tutorials/whoami/_setup-oauth.mdx | 60 ---- docs/tutorials/whoami/_setup-oidc.mdx | 59 ---- docs/tutorials/whoami/_transpile-metadata.mdx | 10 - sidebars.ts | 7 +- 10 files changed, 269 insertions(+), 336 deletions(-) create mode 100644 docs/provider-guides/asgardeo.mdx create mode 100644 docs/provider-guides/keycloak.mdx delete mode 100644 docs/tutorials/whoami/_manual-metadata-fetching.mdx delete mode 100644 docs/tutorials/whoami/_setup-oauth.mdx delete mode 100644 docs/tutorials/whoami/_setup-oidc.mdx delete mode 100644 docs/tutorials/whoami/_transpile-metadata.mdx diff --git a/docs/provider-guides/asgardeo.mdx b/docs/provider-guides/asgardeo.mdx new file mode 100644 index 0000000..796da7e --- /dev/null +++ b/docs/provider-guides/asgardeo.mdx @@ -0,0 +1,101 @@ +--- +sidebar_position: 3 +sidebar_label: Asgardeo +--- + +# Asgardeo + +[Asgardeo](https://wso2.com/asgardeo) is a cloud-native identity as a service (IDaaS) platform that supports OAuth 2.0 and OpenID Connect (OIDC), providing robust identity and access management for modern applications. + +:::note +If you don't have an Asgardeo account, you can [sign up for free](https://asgardeo.io). +::: + +## Get issuer URL {#get-issuer-url} + +You can find the issuer URL in the Asgardeo Console: + +1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization +2. Navigate to the created application and open the **Info** tab +3. The **Issuer** field will be displayed there + +The issuer URL should look like: + +``` +https://api.asgardeo.io/t//oauth2/token +``` + +You can also discover this endpoint dynamically via the [OIDC discovery endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/discover-oidc-configs). + +## Create API resource and scopes {#create-api-resource-and-scopes} + +Asgardeo supports Role-Based Access Control (RBAC) and fine-grained authorization using API resources and scopes. + +1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization +2. Navigate to **API Authorization** in the left menu +3. Click **New API Resource** and fill in the details: + - **Identifier**: Your MCP server URL, e.g., `http://localhost:3001/` + - **Display Name**: e.g., "Todo Manager" +4. Add the scopes your MCP server needs, e.g.: + - `create:todos`: "Create new todo items" + - `read:todos`: "Read all todo items" + - `delete:todos`: "Delete any todo item" +5. Click **Create** + +The scopes will be included in the JWT access token's `scope` claim as a space-separated string. + +## Create roles {#create-roles} + +Roles make it easier to manage permissions for groups of users: + +1. Navigate to **User Management > Roles** in the left menu +2. Click **New Role** +3. Create roles with appropriate scopes, e.g.: + - **Admin**: Assign all scopes (`create:todos`, `read:todos`, `delete:todos`) + - **User**: Assign limited scopes (e.g., only `create:todos`) +4. For each role, select the scopes from your API resource + +Alternatively, you can configure roles at the application level: + +1. Navigate to **Applications** and select your application +2. Go to the **Roles** tab +3. Select "Application Role" as the audience type +4. Create and configure roles with their respective scope assignments + +## Assign roles to users {#assign-roles-to-users} + +1. Navigate to **User Management > Roles** +2. Select a role (e.g., "Admin" or "User") +3. Go to the **Users** tab +4. Click **Assign User** and select the users to assign to this role + +## Retrieving user identity {#retrieving-user-identity} + +User information is encoded inside the ID token returned along with the access token. But as an OIDC provider, Asgardeo exposes a [UserInfo endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/request-user-info/) that allows applications to retrieve claims about the authenticated user in the payload. + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. + +## Register MCP client {#register-mcp-client} + +While Asgardeo supports dynamic client registration via a standard API, the endpoint is protected and requires an access token with the necessary permissions. You'll need to register the client manually through the Asgardeo Console. + +### Register a client for VS Code + +1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization +2. Create a new application: + - Go to **Applications** → **New Application** + - Choose **Standard-Based Application** → **OAuth 2.0/OpenID Connect** + - Enter an application name like `VS Code` + - In the **Authorized Redirect URLs** field, add: + - `http://127.0.0.1` + - `https://vscode.dev/redirect` + - Click **Create** +3. Configure the protocol settings: + - Under the **Protocol** tab: + - Copy the **Client ID** for later use + - Ensure switching to `JWT` for the `Token Type` in **Access Token** section + - Click **Update** +4. Configure API authorization (if using RBAC): + - Go to the **API Authorization** tab + - Authorize the API resource you created earlier + - Select the scopes the application can request diff --git a/docs/provider-guides/generic.mdx b/docs/provider-guides/generic.mdx index 7a96d3a..36df693 100644 --- a/docs/provider-guides/generic.mdx +++ b/docs/provider-guides/generic.mdx @@ -35,6 +35,20 @@ You'll need to define scopes in your authorization server that represent the per Check your provider's documentation for specific instructions on scope management. +## Retrieving user identity {#retrieving-user-identity} + +### OIDC providers + +Most OpenID Connect providers support the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. + +Check your provider's documentation to see if it supports this endpoint. If your provider supports [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), you can also check if the `userinfo_endpoint` is included in the discovery document (response from the `.well-known/openid-configuration` endpoint). + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. Check your provider's documentation to see the mapping of scopes to user identity claims. + +### OAuth 2.0 providers + +While OAuth 2.0 does not define a standard way to retrieve user identity information, many providers implement their own endpoints to do so. Check your provider's documentation to see how to retrieve user identity information using an access token and what parameters are required to fetch such access token when invoking the authorization flow. + ## Token request parameters {#token-request-parameters} Different authorization servers use various approaches for specifying the target resource: diff --git a/docs/provider-guides/keycloak.mdx b/docs/provider-guides/keycloak.mdx new file mode 100644 index 0000000..ac26610 --- /dev/null +++ b/docs/provider-guides/keycloak.mdx @@ -0,0 +1,87 @@ +--- +sidebar_position: 2 +sidebar_label: Keycloak +--- + +# Keycloak + +[Keycloak](https://www.keycloak.org) is an open-source identity and access management solution that supports multiple protocols, including OpenID Connect (OIDC). As an OIDC provider, it implements the standard userinfo endpoint to retrieve user identity information. + +## Prerequisites {#prerequisites} + +:::note +Although Keycloak can be installed in [various ways](https://www.keycloak.org/guides#getting-started) (bare metal, kubernetes, etc.), for this guide, we'll use Docker for a quick and straightforward setup. +::: + +Run a Keycloak instance using Docker following the [official documentation](https://www.keycloak.org/getting-started/getting-started-docker): + +```bash +docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.2.4 start-dev +``` + +## Get issuer URL {#get-issuer-url} + +1. Access the Keycloak Admin Console (http://localhost:8080/admin) and log in with these credentials: + - Username: `admin` + - Password: `admin` +2. Navigate to "Realm settings" in the left menu +3. Click "Endpoints" then "OpenID Endpoint Configuration" +4. The `issuer` field in the JSON document will contain your issuer URL + +For a realm named `mcp-realm`, the issuer URL should look like: + +``` +http://localhost:8080/realms/mcp-realm +``` + +## Create a realm and test user {#create-realm-and-user} + +1. Create a new Realm: + - Click "Create Realm" in the top-left corner + - Enter a name in the "Realm name" field (e.g., `mcp-realm`) + - Click "Create" + +2. Create a test user: + - Click "Users" in the left menu + - Click "Create new user" + - Fill in the user details: + - Username: e.g., `testuser` + - First name and Last name can be any values + - Click "Create" + - In the "Credentials" tab, set a password and uncheck "Temporary" + +## Configure scopes {#configure-scopes} + +If your MCP server requires custom scopes (e.g., for RBAC): + +1. In the Keycloak Admin Console, navigate to "Client scopes" +2. Click "Create client scope" +3. Define the scope name (e.g., `create:todos`, `read:todos`, `delete:todos`) +4. Assign these scopes to clients or roles as needed + +## Retrieving user identity {#retrieving-user-identity} + +As an OIDC provider, Keycloak exposes a standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) that allows applications to retrieve claims about the authenticated user. + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. + +## Register MCP client {#register-mcp-client} + +While Keycloak supports dynamic client registration, its client registration endpoint does not support CORS, preventing most MCP clients from registering directly. Therefore, you'll need to manually register your client. + +### Register a client for VS Code + +1. In the Keycloak Admin Console, click "Clients" in the left menu +2. Click "Create client" +3. Fill in the client details: + - Client type: Select "OpenID Connect" + - Client ID: Enter a name (e.g., `vscode`) + - Click "Next" +4. On the "Capability config" page: + - Ensure "Standard flow" is enabled + - Click "Next" +5. On the "Login settings" page: + - Add `http://127.0.0.1/*` to "Valid redirect URIs" + - Add `https://vscode.dev/redirect` to "Valid redirect URIs" + - Click "Save" +6. Copy the "Client ID" for later use diff --git a/docs/provider-guides/logto.mdx b/docs/provider-guides/logto.mdx index c3a085f..af0ad39 100644 --- a/docs/provider-guides/logto.mdx +++ b/docs/provider-guides/logto.mdx @@ -52,6 +52,12 @@ Roles make it easier to manage permissions for groups of users: You can use Logto's [Management API](https://docs.logto.io/integrate-logto/interact-with-management-api) to programmatically manage user roles. ::: +## Retrieving user identity {#retrieving-user-identity} + +Logto is an OpenID Connect provider that supports the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. + +To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. + ## Register MCP client {#register-mcp-client} Since Logto does not support Dynamic Client Registration yet, you need to manually register your MCP client in Logto Console. diff --git a/docs/tutorials/whoami/README.mdx b/docs/tutorials/whoami/README.mdx index 321cc32..e1a8252 100644 --- a/docs/tutorials/whoami/README.mdx +++ b/docs/tutorials/whoami/README.mdx @@ -3,14 +3,12 @@ sidebar_position: 3 sidebar_label: 'Who am I?' --- -import TabItem from '@theme/TabItem'; -import Tabs from '@theme/Tabs'; - -import SetupOauth from './_setup-oauth.mdx'; -import SetupOidc from './_setup-oidc.mdx'; - # Tutorial: Who am I? +:::tip Using a different authorization server? +This tutorial uses [Logto](https://logto.io) as the example authorization server. If you're using a different provider, check out our [Provider Guides](/docs/provider-guides/generic) for configuration steps. +::: + :::tip Python SDK available MCP Auth is also available for Python! Check out the [Python SDK repository](https://github.com/mcp-auth/python) for installation and usage. ::: @@ -55,49 +53,13 @@ sequenceDiagram ### Retrieving user identity information \{#retrieving-user-identity-information} -To complete this tutorial, your authorization server should offer an API to retrieve user identity information: - - - +To complete this tutorial, your authorization server should offer an API to retrieve user identity information. [Logto](https://logto.io) is an OpenID Connect provider that supports the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. You can continue reading as we'll cover the scope configuration later. - - - -[Keycloak](https://www.keycloak.org) is an open-source identity and access management solution that supports multiple protocols, including OpenID Connect (OIDC). As an OIDC provider, it implements the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. - -To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. You can continue reading as we'll cover the scope configuration later. - - - - - -[Asgardeo](https://wso2.com/asgardeo) is a cloud-native identity as a service (IDaaS) platform that supports OAuth 2.0 and OpenID Connect (OIDC), providing robust identity and access management for modern applications. - -User information is encoded inside the ID token returned along with the access token. But as an OIDC provider, Asgardeo exposes a [UserInfo endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/request-user-info/) that allows applications to retrieve claims about the authenticated user in the payload. - -You can also discover this endpoint dynamically via the [OIDC discovery endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/discover-oidc-configs) or by navigating to the application's 'Info' tab in the Asgardeo Console. - -To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. - - - -Most OpenID Connect providers support the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. - -Check your provider's documentation to see if it supports this endpoint. If your provider supports [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), you can also check if the `userinfo_endpoint` is included in the discovery document (response from the `.well-known/openid-configuration` endpoint). - -To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. Check your provider's documentation to see the mapping of scopes to user identity claims. - - - - -While OAuth 2.0 does not define a standard way to retrieve user identity information, many providers implement their own endpoints to do so. Check your provider's documentation to see how to retrieve user identity information using an access token and what parameters are required to fetch such access token when invoking the authorization flow. - - - +> 📖 See [Generic OAuth 2.0 / OIDC Provider Guide](/docs/provider-guides/generic#retrieving-user-identity) for details on user identity retrieval with other providers. ### Dynamic Client Registration \{#dynamic-client-registration} @@ -222,21 +184,16 @@ This is usually the base URL of your authorization server, such as `https://auth
**How to retrieve user identity information and how to configure the authorization request parameters** -- For OpenID Connect providers: usually you need to request at least the `openid` and `profile` scopes when initiating the authorization flow. This will ensure that the access token returned by the authorization server contains the necessary scopes to access the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. +For OpenID Connect providers like Logto: you need to request at least the `openid` and `profile` scopes when initiating the authorization flow. This will ensure that the access token returned by the authorization server contains the necessary scopes to access the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. - Note: Some of the providers may not support the userinfo endpoint. - -- For OAuth 2.0 / OAuth 2.1 providers: check your provider's documentation to see how to retrieve user identity information using an access token and what parameters are required to fetch such access token when invoking the authorization flow. +> 📖 See [Generic OAuth 2.0 / OIDC Provider Guide](/docs/provider-guides/generic#retrieving-user-identity) for details on other providers.
-While each provider may have its own specific requirements, the following steps will guide you through the process of integrating VS Code and MCP server with provider-specific configurations. +While each provider may have its own specific requirements, the following steps will guide you through the process of integrating VS Code and MCP server with Logto. ### Register VS Code as a client \{#register-vs-code-as-a-client} - - - Integrating with [Logto](https://logto.io) is straightforward as it's an OpenID Connect provider that supports the standard [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. Since Logto does not support Dynamic Client Registration yet, you will need to manually register VS Code as a client in your Logto tenant: @@ -253,127 +210,7 @@ Since Logto does not support Dynamic Client Registration yet, you will need to m ``` 5. In the top card, you will see the "App ID" value. Copy it for later use. - - - -[Keycloak](https://www.keycloak.org) is an open-source identity and access management solution that supports OpenID Connect protocol. - -While Keycloak supports dynamic client registration, its client registration endpoint does not support CORS, preventing most MCP clients from registering directly. Therefore, we'll need to manually register our client. - -:::note -Although Keycloak can be installed in [various ways](https://www.keycloak.org/guides#getting-started) (bare metal, kubernetes, etc.), for this tutorial, we'll use Docker for a quick and straightforward setup. -::: - -Let's set up a Keycloak instance and configure it for our needs: - -1. First, run a Keycloak instance using Docker following the [official documentation](https://www.keycloak.org/getting-started/getting-started-docker): - -```bash -docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.2.4 start-dev -``` - -2. Access the Keycloak Admin Console (http://localhost:8080/admin) and log in with these credentials: - - - Username: `admin` - - Password: `admin` - -3. Create a new Realm: - - - Click "Create Realm" in the top-left corner - - Enter `mcp-realm` in the "Realm name" field - - Click "Create" - -4. Create a test user: - - - Click "Users" in the left menu - - Click "Create new user" - - Fill in the user details: - - Username: `testuser` - - First name and Last name can be any values - - Click "Create" - - In the "Credentials" tab, set a password and uncheck "Temporary" - -5. Register VS Code as a client: - - - In the Keycloak Admin Console, click "Clients" in the left menu - - Click "Create client" - - Fill in the client details: - - Client type: Select "OpenID Connect" - - Client ID: Enter `vscode` - - Click "Next" - - On the "Capability config" page: - - Ensure "Standard flow" is enabled - - Click "Next" - - On the "Login settings" page: - - Add `http://127.0.0.1/*` to "Valid redirect URIs" - - Add `https://vscode.dev/redirect` to "Valid redirect URIs" - - Click "Save" - - Copy the "Client ID" (which is `vscode`) for later use. - - - - -While Asgardeo supports dynamic client registration via a standard API, the endpoint is protected and requires an access token with the necessary permissions. In this tutorial, we'll register the client manually through the Asgardeo Console. - -:::note -If you don't have an Asgardeo account, you can [sign up for free](https://asgardeo.io). -::: - -Follow these steps to configure Asgardeo for VS Code: - -1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization. - -2. Create a new application: - - Go to **Applications** → **New Application** - - Choose **Standard-Based Application** → **OAuth 2.0/OpenID Connect** - - Enter an application name like `VS Code` - - In the **Authorized Redirect URLs** field, add: - - `http://127.0.0.1` - - `https://vscode.dev/redirect` - - Click **Create** - -3. Configure the protocol settings: - - Under the **Protocol** tab: - - Copy the **Client ID** that was auto generated for later use. - - Ensure switching to `JWT` for the `Token Type` in **Access Token** section - - Click **Update** - - - - -:::note -This is a generic OpenID Connect provider integration guide. Check your provider's documentation for specific details. -::: - -If your OpenID Connect provider supports Dynamic Client Registration, VS Code will automatically register itself; otherwise, you will need to manually register VS Code as a client in your OpenID Connect provider: - -1. Sign in to your OpenID Connect provider's console. -2. Navigate to the "Applications" or "Clients" section, then create a new application or client. -3. If your provider requires a client type, select "Native app" or "Public client". -4. Configure the redirect URIs with the following values: - - `http://127.0.0.1` - - `https://vscode.dev/redirect` -5. Find the "Client ID" or "Application ID" of the newly created application and copy it for later use. - - - - -:::note -This is a generic OAuth 2.0 / OAuth 2.1 provider integration guide. Check your provider's documentation for specific details. -::: - -If your OAuth 2.0 / OAuth 2.1 provider supports Dynamic Client Registration, VS Code will automatically register itself; otherwise, you will need to manually register VS Code as a client in your OAuth 2.0 / OAuth 2.1 provider: - -1. Sign in to your OAuth 2.0 / OAuth 2.1 provider's console. -2. Navigate to the "Applications" or "Clients" section, then create a new application or client. -3. If your provider requires a client type, select "Native app" or "Public client". -4. Configure the redirect URIs with the following values: - - `http://127.0.0.1` - - `https://vscode.dev/redirect` -5. Find the "Client ID" or "Application ID" of the newly created application and copy it for later use. - - - +> 📖 See [Logto Provider Guide](/docs/provider-guides/logto#register-mcp-client) for more details on registering MCP clients. ### Set up MCP auth \{#set-up-mcp-auth} @@ -385,50 +222,65 @@ First, install the `mcp-auth` package: npm install mcp-auth ``` -MCP Auth requires the authorization server metadata to be able to initialize. Depending on your provider: - - - - - -The issuer URL can be found in your application details page in Logto Console, in the "Endpoints & Credentials / Issuer endpoint" section. It should look like `https://my-project.logto.app/oidc`. +MCP Auth requires the authorization server metadata to be able to initialize. The issuer URL can be found in your application details page in Logto Console, in the "Endpoints & Credentials / Issuer endpoint" section. It should look like `https://my-project.logto.app/oidc`. - +Update the `whoami.js` to include the MCP Auth configuration: - - - - -The issuer URL can be found in your Keycloak Admin Console. In your 'mcp-realm', navigate to "Realm settings / Endpoints" section and click on "OpenID Endpoint Configuration" link. The `issuer` field in the JSON document will contain your issuer URL, which should look like `http://localhost:8080/realms/mcp-realm`. - - +```js +import { fetchServerConfig, MCPAuth } from 'mcp-auth'; - +const authIssuer = ''; // Replace with your issuer endpoint +const authServerConfig = await fetchServerConfig(authIssuer, { type: 'oidc' }); - +const mcpAuth = new MCPAuth({ + server: authServerConfig, +}); +``` - You can find the issuer URL in the Asgardeo Console. Navigate to the created application, and open the **Info** tab. The **Issuer** field will be displayed there and should look like: - `https://api.asgardeo.io/t//oauth2/token` +:::note +If your provider does not support OpenID Connect Discovery, you can manually specify the metadata URL or endpoints. Check [Other ways to initialize MCP Auth](/docs/configure-server/mcp-auth#other-ways) for more details. +::: - +Now, we need to create a custom access token verifier that will fetch the user identity information from the authorization server using the access token provided by the MCP client. - +```js +import { MCPAuthTokenVerificationError } from 'mcp-auth'; - +/** + * Verifies the provided Bearer token by fetching user information from the authorization server. + * If the token is valid, it returns an `AuthInfo` object containing the user's information. + */ +const verifyToken = async (token) => { + const { issuer, userinfoEndpoint } = authServerConfig.metadata; -The following code also assumes that the authorization server supports the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) to retrieve user identity information. If your provider does not support this endpoint, you will need to check your provider's documentation for the specific endpoint and replace the userinfo endpoint variable with the correct URL. + if (!userinfoEndpoint) { + throw new Error('Userinfo endpoint is not configured in the server metadata'); + } - + const response = await fetch(userinfoEndpoint, { + headers: { Authorization: `Bearer ${token}` }, + }); - - + if (!response.ok) { + throw new MCPAuthTokenVerificationError('token_verification_failed', response); + } -As we mentioned earlier, OAuth 2.0 does not define a standard way to retrieve user identity information. The following code assumes that your provider has a specific endpoint to retrieve user identity information using an access token. You will need to check your provider's documentation for the specific endpoint and replace the userinfo endpoint variable with the correct URL. + const userInfo = await response.json(); - + if (typeof userInfo !== 'object' || userInfo === null || !('sub' in userInfo)) { + throw new MCPAuthTokenVerificationError('invalid_token', response); + } - - + return { + token, + issuer, + subject: String(userInfo.sub), // 'sub' is a standard claim for the subject (user's ID) + clientId: '', // Client ID is not used in this example, but can be set if needed + scopes: [], + claims: userInfo, + }; +}; +``` ### Update MCP server \{#update-mcp-server} diff --git a/docs/tutorials/whoami/_manual-metadata-fetching.mdx b/docs/tutorials/whoami/_manual-metadata-fetching.mdx deleted file mode 100644 index d79c6ee..0000000 --- a/docs/tutorials/whoami/_manual-metadata-fetching.mdx +++ /dev/null @@ -1,3 +0,0 @@ -:::note -If your provider does not support {props.oidc ? 'OpenID Connect Discovery' : 'OAuth 2.0 Authorization Server Metadata'}, you can manually specify the metadata URL or endpoints. Check [Other ways to initialize MCP Auth](/docs/configure-server/mcp-auth#other-ways) for more details. -::: diff --git a/docs/tutorials/whoami/_setup-oauth.mdx b/docs/tutorials/whoami/_setup-oauth.mdx deleted file mode 100644 index 8a72929..0000000 --- a/docs/tutorials/whoami/_setup-oauth.mdx +++ /dev/null @@ -1,60 +0,0 @@ -import ManualMetadataFetching from './_manual-metadata-fetching.mdx'; -import MalformedMetadataTranspilation from './_transpile-metadata.mdx'; - -Update the `whoami.js` to include the MCP Auth configuration: - -```js -import { fetchServerConfig, MCPAuth } from 'mcp-auth'; - -const authIssuer = ''; // Replace with your issuer endpoint -const authServerConfig = await fetchServerConfig(authIssuer, { type: 'oauth' }); - -const mcpAuth = new MCPAuth({ - server: authServerConfig, -}); -``` - - - - -Now, we need to create a custom access token verifier that will fetch the user identity information from the authorization server using the access token provided by the MCP inspector. - -```js -import { MCPAuthTokenVerificationError } from 'mcp-auth'; - -/** - * Verifies the provided Bearer token by fetching user information from the authorization server. - * If the token is valid, it returns an `AuthInfo` object containing the user's information. - */ -const verifyToken = async (token) => { - const { issuer } = authServerConfig.metadata; - - // The following code assumes your authorization server has an endpoint for fetching user info - // using the access token issued by the authorization flow. - // Adjust the URL and headers as needed based on your provider's API. - const response = await fetch('https://your-authorization-server.com/userinfo', { - headers: { Authorization: `Bearer ${token}` }, - }); - - if (!response.ok) { - throw new MCPAuthTokenVerificationError('token_verification_failed', response); - } - - const userInfo = await response.json(); - - // The following code assumes the user info response is an object with a 'sub' field that - // identifies the user. You may need to adjust this based on your provider's API. - if (typeof userInfo !== 'object' || userInfo === null || !('sub' in userInfo)) { - throw new MCPAuthTokenVerificationError('invalid_token', response); - } - - return { - token, - issuer, - subject: String(userInfo.sub), // Adjust this based on your provider's user ID field - clientId: '', // Client ID is not used in this example, but can be set if needed - scopes: [], - claims: userInfo, - }; -}; -``` diff --git a/docs/tutorials/whoami/_setup-oidc.mdx b/docs/tutorials/whoami/_setup-oidc.mdx deleted file mode 100644 index 16e832a..0000000 --- a/docs/tutorials/whoami/_setup-oidc.mdx +++ /dev/null @@ -1,59 +0,0 @@ -import ManualMetadataFetching from './_manual-metadata-fetching.mdx'; -import MalformedMetadataTranspilation from './_transpile-metadata.mdx'; - -Update the `whoami.js` to include the MCP Auth configuration: - -```js -import { fetchServerConfig, MCPAuth } from 'mcp-auth'; - -const authIssuer = ''; // Replace with your issuer endpoint -const authServerConfig = await fetchServerConfig(authIssuer, { type: 'oidc' }); - -const mcpAuth = new MCPAuth({ - server: authServerConfig, -}); -``` - -{props.showAlternative && } -{props.showAlternative && } - -Now, we need to create a custom access token verifier that will fetch the user identity information from the authorization server using the access token provided by the MCP inspector. - -```js -import { MCPAuthTokenVerificationError } from 'mcp-auth'; - -/** - * Verifies the provided Bearer token by fetching user information from the authorization server. - * If the token is valid, it returns an `AuthInfo` object containing the user's information. - */ -const verifyToken = async (token) => { - const { issuer, userinfoEndpoint } = authServerConfig.metadata; - - if (!userinfoEndpoint) { - throw new Error('Userinfo endpoint is not configured in the server metadata'); - } - - const response = await fetch(userinfoEndpoint, { - headers: { Authorization: `Bearer ${token}` }, - }); - - if (!response.ok) { - throw new MCPAuthTokenVerificationError('token_verification_failed', response); - } - - const userInfo = await response.json(); - - if (typeof userInfo !== 'object' || userInfo === null || !('sub' in userInfo)) { - throw new MCPAuthTokenVerificationError('invalid_token', response); - } - - return { - token, - issuer, - subject: String(userInfo.sub), // 'sub' is a standard claim for the subject (user's ID) - clientId: '', // Client ID is not used in this example, but can be set if needed - scopes: [], - claims: userInfo, - }; -}; -``` diff --git a/docs/tutorials/whoami/_transpile-metadata.mdx b/docs/tutorials/whoami/_transpile-metadata.mdx deleted file mode 100644 index 55920ba..0000000 --- a/docs/tutorials/whoami/_transpile-metadata.mdx +++ /dev/null @@ -1,10 +0,0 @@ -In some cases, the provider response may be malformed or not conforming to the expected metadata format. If you are confident that the provider is compliant, you can transpile the metadata via the config option: - -```ts -const mcpAuth = new MCPAuth({ - server: await fetchServerConfig(authIssuer, { - // ...other options - transpileData: (data) => ({ ...data, response_types_supported: ['code'] }), // [!code highlight] - }), -}); -``` diff --git a/sidebars.ts b/sidebars.ts index f922a5f..2b4736c 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -29,7 +29,12 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'Provider Guides', - items: ['provider-guides/logto', 'provider-guides/generic'], + items: [ + 'provider-guides/logto', + 'provider-guides/keycloak', + 'provider-guides/asgardeo', + 'provider-guides/generic', + ], }, { type: 'category', From bee4db9385396300cbabb90fbb22e06f24f982b3 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Sat, 24 Jan 2026 23:32:15 +0800 Subject: [PATCH 6/7] docs: add provider guides landing page --- docs/provider-guides/README.mdx | 15 +++++++++++++++ docs/tutorials/todo-manager/README.mdx | 2 +- docs/tutorials/whoami/README.mdx | 2 +- sidebars.ts | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 docs/provider-guides/README.mdx diff --git a/docs/provider-guides/README.mdx b/docs/provider-guides/README.mdx new file mode 100644 index 0000000..89fc70d --- /dev/null +++ b/docs/provider-guides/README.mdx @@ -0,0 +1,15 @@ +--- +sidebar_label: Overview +--- + +import DocCardList from '@theme/DocCardList'; + +# Provider Guides + +This section contains configuration guides for various OAuth 2.0 and OpenID Connect providers. + + + +:::info Want to contribute? +If your provider is not listed, feel free to create a pull request in [mcp-auth/docs](https://github.com/mcp-auth/docs). +::: diff --git a/docs/tutorials/todo-manager/README.mdx b/docs/tutorials/todo-manager/README.mdx index a0dd2da..79a40ab 100644 --- a/docs/tutorials/todo-manager/README.mdx +++ b/docs/tutorials/todo-manager/README.mdx @@ -8,7 +8,7 @@ import { NpmLikeInstallation } from '@site/src/components/NpmLikeInstallation'; # Tutorial: Build a todo manager :::tip Using a different authorization server? -This tutorial uses [Logto](https://logto.io) as the example authorization server. If you're using a different provider, check out our [Provider Guides](/docs/provider-guides/generic) for configuration steps. +This tutorial uses [Logto](https://logto.io) as the example authorization server. If you're using a different provider, check out our [Provider Guides](/docs/provider-guides) for configuration steps. ::: :::tip Python SDK available diff --git a/docs/tutorials/whoami/README.mdx b/docs/tutorials/whoami/README.mdx index e1a8252..3e8ac64 100644 --- a/docs/tutorials/whoami/README.mdx +++ b/docs/tutorials/whoami/README.mdx @@ -6,7 +6,7 @@ sidebar_label: 'Who am I?' # Tutorial: Who am I? :::tip Using a different authorization server? -This tutorial uses [Logto](https://logto.io) as the example authorization server. If you're using a different provider, check out our [Provider Guides](/docs/provider-guides/generic) for configuration steps. +This tutorial uses [Logto](https://logto.io) as the example authorization server. If you're using a different provider, check out our [Provider Guides](/docs/provider-guides) for configuration steps. ::: :::tip Python SDK available diff --git a/sidebars.ts b/sidebars.ts index 2b4736c..69e0f5c 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -29,6 +29,7 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'Provider Guides', + link: { type: 'doc', id: 'provider-guides/README' }, items: [ 'provider-guides/logto', 'provider-guides/keycloak', From 4642574c1f8b8728b48d38950b211458766774e5 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Sat, 24 Jan 2026 23:52:59 +0800 Subject: [PATCH 7/7] refactor: remove Asgardeo from this branch (to be added via separate PR) --- docs/provider-guides/asgardeo.mdx | 101 ------------------------------ sidebars.ts | 1 - 2 files changed, 102 deletions(-) delete mode 100644 docs/provider-guides/asgardeo.mdx diff --git a/docs/provider-guides/asgardeo.mdx b/docs/provider-guides/asgardeo.mdx deleted file mode 100644 index 796da7e..0000000 --- a/docs/provider-guides/asgardeo.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -sidebar_position: 3 -sidebar_label: Asgardeo ---- - -# Asgardeo - -[Asgardeo](https://wso2.com/asgardeo) is a cloud-native identity as a service (IDaaS) platform that supports OAuth 2.0 and OpenID Connect (OIDC), providing robust identity and access management for modern applications. - -:::note -If you don't have an Asgardeo account, you can [sign up for free](https://asgardeo.io). -::: - -## Get issuer URL {#get-issuer-url} - -You can find the issuer URL in the Asgardeo Console: - -1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization -2. Navigate to the created application and open the **Info** tab -3. The **Issuer** field will be displayed there - -The issuer URL should look like: - -``` -https://api.asgardeo.io/t//oauth2/token -``` - -You can also discover this endpoint dynamically via the [OIDC discovery endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/discover-oidc-configs). - -## Create API resource and scopes {#create-api-resource-and-scopes} - -Asgardeo supports Role-Based Access Control (RBAC) and fine-grained authorization using API resources and scopes. - -1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization -2. Navigate to **API Authorization** in the left menu -3. Click **New API Resource** and fill in the details: - - **Identifier**: Your MCP server URL, e.g., `http://localhost:3001/` - - **Display Name**: e.g., "Todo Manager" -4. Add the scopes your MCP server needs, e.g.: - - `create:todos`: "Create new todo items" - - `read:todos`: "Read all todo items" - - `delete:todos`: "Delete any todo item" -5. Click **Create** - -The scopes will be included in the JWT access token's `scope` claim as a space-separated string. - -## Create roles {#create-roles} - -Roles make it easier to manage permissions for groups of users: - -1. Navigate to **User Management > Roles** in the left menu -2. Click **New Role** -3. Create roles with appropriate scopes, e.g.: - - **Admin**: Assign all scopes (`create:todos`, `read:todos`, `delete:todos`) - - **User**: Assign limited scopes (e.g., only `create:todos`) -4. For each role, select the scopes from your API resource - -Alternatively, you can configure roles at the application level: - -1. Navigate to **Applications** and select your application -2. Go to the **Roles** tab -3. Select "Application Role" as the audience type -4. Create and configure roles with their respective scope assignments - -## Assign roles to users {#assign-roles-to-users} - -1. Navigate to **User Management > Roles** -2. Select a role (e.g., "Admin" or "User") -3. Go to the **Users** tab -4. Click **Assign User** and select the users to assign to this role - -## Retrieving user identity {#retrieving-user-identity} - -User information is encoded inside the ID token returned along with the access token. But as an OIDC provider, Asgardeo exposes a [UserInfo endpoint](https://wso2.com/asgardeo/docs/guides/authentication/oidc/request-user-info/) that allows applications to retrieve claims about the authenticated user in the payload. - -To fetch an access token that can be used to access the userinfo endpoint, at least two scopes are required: `openid` and `profile`. - -## Register MCP client {#register-mcp-client} - -While Asgardeo supports dynamic client registration via a standard API, the endpoint is protected and requires an access token with the necessary permissions. You'll need to register the client manually through the Asgardeo Console. - -### Register a client for VS Code - -1. Log in to the [Asgardeo Console](https://console.asgardeo.io) and select your organization -2. Create a new application: - - Go to **Applications** → **New Application** - - Choose **Standard-Based Application** → **OAuth 2.0/OpenID Connect** - - Enter an application name like `VS Code` - - In the **Authorized Redirect URLs** field, add: - - `http://127.0.0.1` - - `https://vscode.dev/redirect` - - Click **Create** -3. Configure the protocol settings: - - Under the **Protocol** tab: - - Copy the **Client ID** for later use - - Ensure switching to `JWT` for the `Token Type` in **Access Token** section - - Click **Update** -4. Configure API authorization (if using RBAC): - - Go to the **API Authorization** tab - - Authorize the API resource you created earlier - - Select the scopes the application can request diff --git a/sidebars.ts b/sidebars.ts index 69e0f5c..05f64ca 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -33,7 +33,6 @@ const sidebars: SidebarsConfig = { items: [ 'provider-guides/logto', 'provider-guides/keycloak', - 'provider-guides/asgardeo', 'provider-guides/generic', ], },