|
| 1 | +--- |
| 2 | +sidebar_position: 1 |
| 3 | +sidebar_label: Enable auth for MCP-powered apps |
| 4 | +--- |
| 5 | + |
| 6 | +import TabItem from '@theme/TabItem'; |
| 7 | +import Tabs from '@theme/Tabs'; |
| 8 | + |
| 9 | +# Enable auth for your MCP-powered apps with Logto |
| 10 | + |
| 11 | +This guide walks you through integrating Logto with your MCP server using [mcp-auth](https://mcp-auth.dev), allowing you to authenticate users and securely retrieve their identity information using the standard OpenID Connect flow. |
| 12 | + |
| 13 | +You'll learn how to: |
| 14 | + |
| 15 | +- Configure Logto as the authorization server for your MCP server. |
| 16 | +- Set up a “whoami” tool to return the current user's identity claims. |
| 17 | +- Test the flow with the MCP Inspector. |
| 18 | + |
| 19 | +After this tutorial, your MCP server will: |
| 20 | + |
| 21 | +- Authenticate users in your Logto tenant. |
| 22 | +- Return identity claims (`sub`, `username`, `name`, `email`, etc.) for the "whoami" tool invocation. |
| 23 | + |
| 24 | +Once the integration is complete, you can replace the MCP Inspector with your own MCP client, such as a web app, to access the tools and resources exposed by your MCP server. |
| 25 | + |
| 26 | +## Prerequisites |
| 27 | + |
| 28 | +- A [Logto Cloud](https://cloud.logto.io) (or self-hosted) tenant |
| 29 | +- Node.js or Python environment |
| 30 | + |
| 31 | +## Understanding the architecture |
| 32 | + |
| 33 | +- **MCP server**: The server that exposes tools and resources to MCP clients. |
| 34 | +- **MCP Inspector**: A MCP client used to initiate the authentication flow and test the integration. |
| 35 | +- **Logto**: Serves as the OpenID Connect provider (authorization server) and manages user identities. |
| 36 | + |
| 37 | +A non-normative sequence diagram illustrates the overall flow of the process: |
| 38 | + |
| 39 | +```mermaid |
| 40 | +sequenceDiagram |
| 41 | + participant Client as MCP Inspector |
| 42 | + participant Server as MCP Server |
| 43 | + participant Logto |
| 44 | +
|
| 45 | + Client->>Server: Request access to a tool |
| 46 | + Server->>Client: Not authenticated (401 Unauthorized) |
| 47 | + Client->>Server: Request OAuth 2.0 Authorization Server Metadata |
| 48 | + Server->>Client: Return metadata retrieved from Logto |
| 49 | + Client->>Logto: Redirect to Logto for authentication |
| 50 | + Logto->>Logto: User authenticates |
| 51 | + Logto->>Client: Redirect back to MCP server with authorization code |
| 52 | + Client->>Logto: Request access token using authorization code |
| 53 | + Logto->>Client: Return access token |
| 54 | + Client->>Server: Request tool with access token |
| 55 | + Server->>Logto: Request user info using access token |
| 56 | + Logto->>Server: Return user info |
| 57 | + Server->>Client: Return tool response |
| 58 | +``` |
| 59 | + |
| 60 | +:::note |
| 61 | +Due to MCP is quickly evolving, the above diagram may not be fully up to date. Please refer to the [mcp-auth](https://mcp-auth.dev) documentation for the latest information. |
| 62 | +::: |
| 63 | + |
| 64 | +## Set up app in Logto |
| 65 | + |
| 66 | +1. Sign in to your Logto Console. |
| 67 | +2. Go <CloudLink to="/applications">**Applications**</CloudLink> → **Create application** → **Create app without framework**. |
| 68 | +3. Choose type: Single-page app. |
| 69 | +4. Fill in the app name and other required fields, then click **Create application**. |
| 70 | +5. Save and copy the **App ID** and **Issuer endpoint**. |
| 71 | + |
| 72 | +## Set up the MCP server |
| 73 | + |
| 74 | +### Create project and install dependencies |
| 75 | + |
| 76 | +<Tabs groupId="sdk"> |
| 77 | +<TabItem value="python" label="Python"> |
| 78 | + |
| 79 | +```bash |
| 80 | +mkdir mcp-server |
| 81 | +cd mcp-server |
| 82 | +uv init # Or use your own project structure |
| 83 | +uv add "mcp[cli]" starlette uvicorn mcpauth # Or use any preferred package manager |
| 84 | +``` |
| 85 | + |
| 86 | +</TabItem> |
| 87 | +<TabItem value="node" label="Node.js"> |
| 88 | + |
| 89 | +```bash |
| 90 | +mkdir mcp-server |
| 91 | +cd mcp-server |
| 92 | +npm init -y |
| 93 | +npm install @modelcontextprotocol/sdk express mcp-auth # Or use any preferred package manager |
| 94 | +``` |
| 95 | + |
| 96 | +</TabItem> |
| 97 | +</Tabs> |
| 98 | + |
| 99 | +### Configure MCP auth with Logto |
| 100 | + |
| 101 | +Remember to replace `<your-logto-issuer-endpoint>` with the issuer endpoint you copied earlier. |
| 102 | + |
| 103 | +<Tabs groupId="sdk"> |
| 104 | +<TabItem value="python" label="Python"> |
| 105 | + |
| 106 | +**In `whoami.py`:** |
| 107 | + |
| 108 | +```python |
| 109 | +from mcpauth import MCPAuth |
| 110 | +from mcpauth.config import AuthServerType |
| 111 | +from mcpauth.utils import fetch_server_config |
| 112 | + |
| 113 | +auth_issuer = '<your-logto-issuer-endpoint>' |
| 114 | +auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OIDC) |
| 115 | +mcp_auth = MCPAuth(server=auth_server_config) |
| 116 | +``` |
| 117 | + |
| 118 | +</TabItem> |
| 119 | +<TabItem value="node" label="Node.js"> |
| 120 | + |
| 121 | +**In `whoami.js`:** |
| 122 | + |
| 123 | +```js |
| 124 | +import { MCPAuth, fetchServerConfig } from 'mcp-auth'; |
| 125 | + |
| 126 | +const authIssuer = '<your-logto-issuer-endpoint>'; |
| 127 | +const mcpAuth = new MCPAuth({ |
| 128 | + server: await fetchServerConfig(authIssuer, { type: 'oidc' }), |
| 129 | +}); |
| 130 | +``` |
| 131 | + |
| 132 | +</TabItem> |
| 133 | +</Tabs> |
| 134 | + |
| 135 | +### Implement token verification |
| 136 | + |
| 137 | +Since we're going to verify the access token and retrieve user info, we need to implement the access token verification as follows: |
| 138 | + |
| 139 | +<Tabs groupId="sdk"> |
| 140 | +<TabItem value="python" label="Python"> |
| 141 | + |
| 142 | +```python |
| 143 | +import requests |
| 144 | +from mcpauth.types import AuthInfo |
| 145 | + |
| 146 | +def verify_access_token(token: str) -> AuthInfo: |
| 147 | + endpoint = auth_server_config.metadata.userinfo_endpoint |
| 148 | + response = requests.get( |
| 149 | + endpoint, |
| 150 | + headers={"Authorization": f"Bearer {token}"}, |
| 151 | + ) |
| 152 | + response.raise_for_status() |
| 153 | + data = response.json() |
| 154 | + return AuthInfo( |
| 155 | + token=token, |
| 156 | + subject=data.get("sub"), |
| 157 | + issuer=auth_server_config.metadata.issuer, |
| 158 | + claims=data, |
| 159 | + ) |
| 160 | +``` |
| 161 | + |
| 162 | +</TabItem> |
| 163 | +<TabItem value="node" label="Node.js"> |
| 164 | + |
| 165 | +```js |
| 166 | +const verifyToken = async (token) => { |
| 167 | + const { userinfoEndpoint, issuer } = mcpAuth.config.server.metadata; |
| 168 | + const response = await fetch(userinfoEndpoint, { |
| 169 | + headers: { Authorization: `Bearer ${token}` }, |
| 170 | + }); |
| 171 | + if (!response.ok) throw new Error('Token verification failed'); |
| 172 | + const userInfo = await response.json(); |
| 173 | + return { |
| 174 | + token, |
| 175 | + issuer, |
| 176 | + subject: userInfo.sub, |
| 177 | + claims: userInfo, |
| 178 | + }; |
| 179 | +}; |
| 180 | +``` |
| 181 | + |
| 182 | +</TabItem> |
| 183 | +</Tabs> |
| 184 | + |
| 185 | +### Implement the "whoami" tool |
| 186 | + |
| 187 | +Now, let's implement the "whoami" tool that returns the current user's identity claims requesting the userinfo endpoint with the access token sent by the client. |
| 188 | + |
| 189 | +:::note |
| 190 | +We are using the SSE transport for the example due to the lack of official support for the Streamable HTTP transport in the current version of the SDK. Theoretically, you can use any HTTP-compatible transport. |
| 191 | +::: |
| 192 | + |
| 193 | +<Tabs groupId="sdk"> |
| 194 | +<TabItem value="python" label="Python"> |
| 195 | + |
| 196 | +```python |
| 197 | +from mcp.server.fastmcp import FastMCP |
| 198 | +from starlette.applications import Starlette |
| 199 | +from starlette.routing import Mount |
| 200 | +from starlette.middleware import Middleware |
| 201 | + |
| 202 | +mcp = FastMCP("WhoAmI") |
| 203 | + |
| 204 | +@mcp.tool() |
| 205 | +def whoami() -> dict: |
| 206 | + """ |
| 207 | + Returns the current user's identity information. |
| 208 | + """ |
| 209 | + return ( |
| 210 | + mcp_auth.auth_info.claims |
| 211 | + if mcp_auth.auth_info |
| 212 | + else {"error": "Not authenticated"} |
| 213 | + ) |
| 214 | + |
| 215 | +bearer_auth = Middleware(mcp_auth.bearer_auth_middleware(verify_access_token)) |
| 216 | +app = Starlette( |
| 217 | + routes=[ |
| 218 | + mcp_auth.metadata_route(), # Serves OIDC metadata for discovery |
| 219 | + Mount('/', app=mcp.sse_app(), middleware=[bearer_auth]), |
| 220 | + ], |
| 221 | +) |
| 222 | +``` |
| 223 | + |
| 224 | +Run the server with: |
| 225 | + |
| 226 | +```bash |
| 227 | +uvicorn whoami:app --host 0.0.0.0 --port 3001 |
| 228 | +``` |
| 229 | + |
| 230 | +</TabItem> |
| 231 | +<TabItem value="node" label="Node.js"> |
| 232 | + |
| 233 | +```js |
| 234 | +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| 235 | +import express from 'express'; |
| 236 | + |
| 237 | +// Create MCP server and register the whoami tool |
| 238 | +const server = new McpServer({ name: 'WhoAmI', version: '0.0.0' }); |
| 239 | +server.tool('whoami', ({ authInfo }) => ({ |
| 240 | + content: [ |
| 241 | + { type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) }, |
| 242 | + ], |
| 243 | +})); |
| 244 | + |
| 245 | +// Express app & MCP Auth middleware |
| 246 | +const app = express(); |
| 247 | +app.use(mcpAuth.delegatedRouter()); |
| 248 | +app.use(mcpAuth.bearerAuth(verifyToken)); |
| 249 | + |
| 250 | +// SSE transport (as in SDK docs) |
| 251 | +const transports = {}; |
| 252 | +app.get('/sse', async (_req, res) => { |
| 253 | + const transport = new SSEServerTransport('/messages', res); |
| 254 | + transports[transport.sessionId] = transport; |
| 255 | + res.on('close', () => delete transports[transport.sessionId]); |
| 256 | + await server.connect(transport); |
| 257 | +}); |
| 258 | +app.post('/messages', async (req, res) => { |
| 259 | + const sessionId = String(req.query.sessionId); |
| 260 | + const transport = transports[sessionId]; |
| 261 | + if (transport) await transport.handlePostMessage(req, res, req.body); |
| 262 | + else res.status(400).send('No transport found for sessionId'); |
| 263 | +}); |
| 264 | + |
| 265 | +app.listen(3001); |
| 266 | +``` |
| 267 | +
|
| 268 | +</TabItem> |
| 269 | +</Tabs> |
| 270 | +
|
| 271 | +## Test the integration |
| 272 | +
|
| 273 | +1. Start the MCP server (`uvicorn whoami:app ...` or `node whoami.js`). |
| 274 | +2. Start the MCP Inspector. |
| 275 | +
|
| 276 | + Due to the limit of the current MCP Inspector implementation, we need to use the forked version from mcp-auth: |
| 277 | +
|
| 278 | + ```bash |
| 279 | + git clone https://github.com/mcp-auth/inspector.git |
| 280 | + cd inspector |
| 281 | + npm install |
| 282 | + npm run dev |
| 283 | + ``` |
| 284 | +
|
| 285 | + Then, open the URL shown in the terminal. |
| 286 | +
|
| 287 | +3. In the MCP Inspector: |
| 288 | +
|
| 289 | + - **Transport Type**: `SSE` |
| 290 | + - **URL**: `http://localhost:3001/sse` |
| 291 | + - **OAuth Client ID**: Paste your Logto App ID |
| 292 | + - **Auth Params**: `{"scope": "openid profile email"}` |
| 293 | + - **Redirect URI**: This URL should be auto-populated. Copy it. |
| 294 | + |
| 295 | +4. Find the application you created earlier in the Logto Console, open the details page, and paste the redirect URI into the **Settings** / **Redirect URIs** section. Save the changes. |
| 296 | +5. Back in the MCP Inspector, click **Connect**. This should redirect you to the Logto sign-in experience. |
| 297 | +6. After signing in, you should be redirected back to the MCP Inspector. Go to **Tools** -> **List Tools** -> **whoami** -> **Run Tool**. |
| 298 | + |
| 299 | + You should see user claims, such as: |
| 300 | + |
| 301 | + ```json |
| 302 | + { |
| 303 | + "sub": "user_XXXX", |
| 304 | + "username": "alice", |
| 305 | + "name": "Alice Smith", |
| 306 | + "email": "alice@example.com" |
| 307 | + } |
| 308 | + ``` |
0 commit comments