|
| 1 | +import TabItem from '@theme/TabItem'; |
| 2 | +import Tabs from '@theme/Tabs'; |
| 3 | + |
| 4 | +## Set up the MCP server |
| 5 | + |
| 6 | +### Create project and install dependencies |
| 7 | + |
| 8 | +<Tabs groupId="sdk"> |
| 9 | +<TabItem value="python" label="Python"> |
| 10 | + |
| 11 | +```bash |
| 12 | +mkdir mcp-server |
| 13 | +cd mcp-server |
| 14 | +uv init # Or use your own project structure |
| 15 | +uv add "mcp[cli]" starlette uvicorn mcpauth # Or use any preferred package manager |
| 16 | +``` |
| 17 | + |
| 18 | +</TabItem> |
| 19 | +<TabItem value="node" label="Node.js"> |
| 20 | + |
| 21 | +```bash |
| 22 | +mkdir mcp-server |
| 23 | +cd mcp-server |
| 24 | +npm init -y |
| 25 | +npm install @modelcontextprotocol/sdk express mcp-auth # Or use any preferred package manager |
| 26 | +``` |
| 27 | + |
| 28 | +</TabItem> |
| 29 | +</Tabs> |
| 30 | + |
| 31 | +### Configure MCP auth with Logto |
| 32 | + |
| 33 | +Remember to replace `<your-logto-issuer-endpoint>` with the issuer endpoint you copied earlier. |
| 34 | + |
| 35 | +<Tabs groupId="sdk"> |
| 36 | +<TabItem value="python" label="Python"> |
| 37 | + |
| 38 | +**In `whoami.py`:** |
| 39 | + |
| 40 | +```python |
| 41 | +from mcpauth import MCPAuth |
| 42 | +from mcpauth.config import AuthServerType |
| 43 | +from mcpauth.utils import fetch_server_config |
| 44 | + |
| 45 | +auth_issuer = '<your-logto-issuer-endpoint>' |
| 46 | +auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OIDC) |
| 47 | +mcp_auth = MCPAuth(server=auth_server_config) |
| 48 | +``` |
| 49 | + |
| 50 | +</TabItem> |
| 51 | +<TabItem value="node" label="Node.js"> |
| 52 | + |
| 53 | +**In `whoami.js`:** |
| 54 | + |
| 55 | +```js |
| 56 | +import { MCPAuth, fetchServerConfig } from 'mcp-auth'; |
| 57 | + |
| 58 | +const authIssuer = '<your-logto-issuer-endpoint>'; |
| 59 | +const mcpAuth = new MCPAuth({ |
| 60 | + server: await fetchServerConfig(authIssuer, { type: 'oidc' }), |
| 61 | +}); |
| 62 | +``` |
| 63 | + |
| 64 | +</TabItem> |
| 65 | +</Tabs> |
| 66 | + |
| 67 | +### Implement token verification |
| 68 | + |
| 69 | +Since we're going to verify the access token and retrieve user info, we need to implement the access token verification as follows: |
| 70 | + |
| 71 | +<Tabs groupId="sdk"> |
| 72 | +<TabItem value="python" label="Python"> |
| 73 | + |
| 74 | +```python |
| 75 | +import requests |
| 76 | +from mcpauth.types import AuthInfo |
| 77 | + |
| 78 | +def verify_access_token(token: str) -> AuthInfo: |
| 79 | + endpoint = auth_server_config.metadata.userinfo_endpoint |
| 80 | + response = requests.get( |
| 81 | + endpoint, |
| 82 | + headers={"Authorization": f"Bearer {token}"}, |
| 83 | + ) |
| 84 | + response.raise_for_status() |
| 85 | + data = response.json() |
| 86 | + return AuthInfo( |
| 87 | + token=token, |
| 88 | + subject=data.get("sub"), |
| 89 | + issuer=auth_server_config.metadata.issuer, |
| 90 | + claims=data, |
| 91 | + ) |
| 92 | +``` |
| 93 | + |
| 94 | +</TabItem> |
| 95 | +<TabItem value="node" label="Node.js"> |
| 96 | + |
| 97 | +```js |
| 98 | +const verifyToken = async (token) => { |
| 99 | + const { userinfoEndpoint, issuer } = mcpAuth.config.server.metadata; |
| 100 | + const response = await fetch(userinfoEndpoint, { |
| 101 | + headers: { Authorization: `Bearer ${token}` }, |
| 102 | + }); |
| 103 | + if (!response.ok) throw new Error('Token verification failed'); |
| 104 | + const userInfo = await response.json(); |
| 105 | + return { |
| 106 | + token, |
| 107 | + issuer, |
| 108 | + subject: userInfo.sub, |
| 109 | + claims: userInfo, |
| 110 | + }; |
| 111 | +}; |
| 112 | +``` |
| 113 | + |
| 114 | +</TabItem> |
| 115 | +</Tabs> |
| 116 | + |
| 117 | +### Implement the "whoami" tool |
| 118 | + |
| 119 | +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. |
| 120 | + |
| 121 | +:::note |
| 122 | +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. |
| 123 | +::: |
| 124 | + |
| 125 | +<Tabs groupId="sdk"> |
| 126 | +<TabItem value="python" label="Python"> |
| 127 | + |
| 128 | +```python |
| 129 | +from mcp.server.fastmcp import FastMCP |
| 130 | +from starlette.applications import Starlette |
| 131 | +from starlette.routing import Mount |
| 132 | +from starlette.middleware import Middleware |
| 133 | + |
| 134 | +mcp = FastMCP("WhoAmI") |
| 135 | + |
| 136 | +@mcp.tool() |
| 137 | +def whoami() -> dict: |
| 138 | + """ |
| 139 | + Returns the current user's identity information. |
| 140 | + """ |
| 141 | + return ( |
| 142 | + mcp_auth.auth_info.claims |
| 143 | + if mcp_auth.auth_info |
| 144 | + else {"error": "Not authenticated"} |
| 145 | + ) |
| 146 | + |
| 147 | +bearer_auth = Middleware(mcp_auth.bearer_auth_middleware(verify_access_token)) |
| 148 | +app = Starlette( |
| 149 | + routes=[ |
| 150 | + mcp_auth.metadata_route(), # Serves OIDC metadata for discovery |
| 151 | + Mount('/', app=mcp.sse_app(), middleware=[bearer_auth]), |
| 152 | + ], |
| 153 | +) |
| 154 | +``` |
| 155 | + |
| 156 | +Run the server with: |
| 157 | + |
| 158 | +```bash |
| 159 | +uvicorn whoami:app --host 0.0.0.0 --port 3001 |
| 160 | +``` |
| 161 | + |
| 162 | +</TabItem> |
| 163 | +<TabItem value="node" label="Node.js"> |
| 164 | + |
| 165 | +```js |
| 166 | +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| 167 | +import express from 'express'; |
| 168 | + |
| 169 | +// Create MCP server and register the whoami tool |
| 170 | +const server = new McpServer({ name: 'WhoAmI', version: '0.0.0' }); |
| 171 | +server.tool('whoami', ({ authInfo }) => ({ |
| 172 | + content: [ |
| 173 | + { type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) }, |
| 174 | + ], |
| 175 | +})); |
| 176 | + |
| 177 | +// Express app & MCP Auth middleware |
| 178 | +const app = express(); |
| 179 | +app.use(mcpAuth.delegatedRouter()); |
| 180 | +app.use(mcpAuth.bearerAuth(verifyToken)); |
| 181 | + |
| 182 | +// SSE transport (as in SDK docs) |
| 183 | +const transports = {}; |
| 184 | +app.get('/sse', async (_req, res) => { |
| 185 | + const transport = new SSEServerTransport('/messages', res); |
| 186 | + transports[transport.sessionId] = transport; |
| 187 | + res.on('close', () => delete transports[transport.sessionId]); |
| 188 | + await server.connect(transport); |
| 189 | +}); |
| 190 | +app.post('/messages', async (req, res) => { |
| 191 | + const sessionId = String(req.query.sessionId); |
| 192 | + const transport = transports[sessionId]; |
| 193 | + if (transport) await transport.handlePostMessage(req, res, req.body); |
| 194 | + else res.status(400).send('No transport found for sessionId'); |
| 195 | +}); |
| 196 | + |
| 197 | +app.listen(3001); |
| 198 | +``` |
| 199 | +
|
| 200 | +Run the server with: |
| 201 | +
|
| 202 | +```bash |
| 203 | +node whoami.js |
| 204 | +``` |
| 205 | +
|
| 206 | +</TabItem> |
| 207 | +</Tabs> |
0 commit comments