Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · mcp

Understanding Authorization in MCP changeddocs/2026-07-28/tutorials/security/authorization

Nearest release: v2.1.221, published 23 hours after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 2 Aug 2026 22:20 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 20:07 UTC.

Upstream edited
Recorded here
Lines+949added
Lines−969removed
From line 16 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits2to this page, all time

The whole hunk

from line 16, old and new numbered
/
lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 16
1616* You're building for enterprise environments with strict access controls
1717* You want to implement rate limiting or usage tracking per user
1818 
19<Tip>
20 **Authorization for Local MCP Servers**
21 
22 For MCP servers using the [STDIO transport](/specification/latest/basic/transports#stdio), you can use environment-based credentials or credentials provided by third-party libraries embedded directly in the MCP server instead. Because a STDIO-built MCP server runs locally, it has access to a range of flexible options when it comes to acquiring user credentials that may or may not rely on in-browser authentication and authorization flows.
23 
24 OAuth flows, in turn, are designed for HTTP-based transports where the MCP server is remotely-hosted and the client uses OAuth to establish that a user is authorized to access said remote server.
25</Tip>
19> [!TIP]
20>
21> **Authorization for Local MCP Servers**
22>
23> For MCP servers using the [STDIO transport](/specification/latest/basic/transports#stdio), you can use environment-based credentials or credentials provided by third-party libraries embedded directly in the MCP server instead. Because a STDIO-built MCP server runs locally, it has access to a range of flexible options when it comes to acquiring user credentials that may or may not rely on in-browser authentication and authorization flows.
24>
25> OAuth flows, in turn, are designed for HTTP-based transports where the MCP server is remotely-hosted and the client uses OAuth to establish that a user is authorized to access said remote server.
2626 
2727## The Authorization Flow: Step by Step
2828 
2929Let's walk through what happens when a client wants to connect to your protected MCP server:
3030 
31<Steps>
32 <Step title="Initial Handshake">
33 When your MCP client first tries to connect, your server responds with a `401 Unauthorized` and tells the client where to find authorization information, captured in a [Protected Resource Metadata (PRM) document](https://datatracker.ietf.org/doc/html/rfc9728). The document is hosted by the MCP server, follows a predictable path pattern, and is provided to the client in the `resource_metadata` parameter within the `WWW-Authenticate` header.
34 
35 ```http theme={null}
36 HTTP/1.1 401 Unauthorized
37 WWW-Authenticate: Bearer realm="mcp",
38 resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
39 ```
40 
41 This tells the client that authorization is required for the MCP server and where to get the necessary information to kickstart the authorization flow.
42 </Step>
43 
44 <Step title="Protected Resource Metadata Discovery">
45 With the URI pointer to the PRM document, the client will fetch the metadata to learn about the authorization server, supported scopes, and other resource information. The data is typically encapsulated in a JSON blob, similar to the one below.
46 
47 ```json theme={null}
48 {
49 "resource": "https://your-server.com/mcp",
50 "authorization_servers": ["https://auth.your-server.com"],
51 "scopes_supported": ["mcp:tools", "mcp:resources"]
52 }
53 ```
54 
55 You can see a more comprehensive example in [RFC 9728 Section 3.2](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r).
56 </Step>
57 
58 <Step title="Authorization Server Discovery">
59 Next, the client discovers what the authorization server can do by fetching its metadata. If the PRM document lists more than one authorization server, the client can decide which one to use.
60 
61 With an authorization server selected, the client will then construct a standard metadata URI and issue a request to the [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) or [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) endpoints (depending on authorization server support)
62 and retrieve another set of metadata properties that will allow it to know the endpoints it needs to complete the authorization flow.
63 
64 ```json theme={null}
65 {
66 "issuer": "https://auth.your-server.com",
67 "authorization_endpoint": "https://auth.your-server.com/authorize",
68 "token_endpoint": "https://auth.your-server.com/token",
69 "registration_endpoint": "https://auth.your-server.com/register"
70 }
71 ```
72 </Step>
73 
74 <Step title="Client Registration">
75 With all the metadata out of the way, the client now needs to make sure that it's registered with the authorization server. This can be done in two ways.
76 
77 First, the client can be **pre-registered** with a given authorization server, in which case it can have embedded client registration information that it uses to complete the authorization flow.
78 
79 Alternatively, the client can use **Dynamic Client Registration** (DCR) to dynamically register itself with the authorization server. The latter scenario requires the authorization server to support DCR. If the authorization server does support DCR, the client will send a request to the `registration_endpoint` with its information:
80 
81 ```json theme={null}
82 {
83 "client_name": "My MCP Client",
84 "redirect_uris": ["http://localhost:3000/callback"],
85 "grant_types": ["authorization_code", "refresh_token"],
86 "response_types": ["code"]
87 }
88 ```
89 
90 If the registration succeeds, the authorization server will return a JSON blob with client registration information.
91 
92 <Tip>
93 **No DCR or Pre-Registration**
94 
95 In case an MCP client connects to an MCP server that doesn't use an authorization server that supports DCR and the client is not pre-registered with said authorization server, it's the responsibility of the client developer to provide an affordance for the end-user to enter client information manually.
96 </Tip>
97 </Step>
98 
99 <Step title="User Authorization">
100 The client will now need to open a browser to the `/authorize` endpoint, where the user can log in and grant the required permissions. The authorization server will then redirect back to the client with an authorization code that the client exchanges for tokens:
101 
102 ```json theme={null}
103 {
104 "access_token": "eyJhbGciOiJSUzI1NiIs...",
105 "refresh_token": "def502...",
106 "token_type": "Bearer",
107 "expires_in": 3600
108 }
109 ```
110 
111 The access token is what the client will use to authenticate requests to the MCP server. This step follows standard [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) conventions.
112 </Step>
113 
114 <Step title="Making Authenticated Requests">
115 Finally, the client can make requests to your MCP server using the access token embedded in the `Authorization` header:
116 
117 ```http theme={null}
118 GET /mcp HTTP/1.1
119 Host: your-server.com
120 Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
121 ```
122 
123 The MCP server will need to validate the token and process the request if the token is valid and has the required permissions.
124 </Step>
125</Steps>
31**1. Initial Handshake**
32 
33When your MCP client first tries to connect, your server responds with a `401 Unauthorized` and tells the client where to find authorization information, captured in a [Protected Resource Metadata (PRM) document](https://datatracker.ietf.org/doc/html/rfc9728). The document is hosted by the MCP server, follows a predictable path pattern, and is provided to the client in the `resource_metadata` parameter within the `WWW-Authenticate` header.
34 
35```http theme={null}
36HTTP/1.1 401 Unauthorized
37WWW-Authenticate: Bearer realm="mcp",
38 resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
39```
40 
41This tells the client that authorization is required for the MCP server and where to get the necessary information to kickstart the authorization flow.
42 
43**2. Protected Resource Metadata Discovery**
44 
45With the URI pointer to the PRM document, the client will fetch the metadata to learn about the authorization server, supported scopes, and other resource information. The data is typically encapsulated in a JSON blob, similar to the one below.
46 
47```json theme={null}
48{
49 "resource": "https://your-server.com/mcp",
50 "authorization_servers": ["https://auth.your-server.com"],
51 "scopes_supported": ["mcp:tools", "mcp:resources"]
52}
53```
54 
55You can see a more comprehensive example in [RFC 9728 Section 3.2](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r).
56 
57**3. Authorization Server Discovery**
58 
59Next, the client discovers what the authorization server can do by fetching its metadata. If the PRM document lists more than one authorization server, the client can decide which one to use.
60 
61With an authorization server selected, the client will then construct a standard metadata URI and issue a request to the [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) or [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) endpoints (depending on authorization server support)
62and retrieve another set of metadata properties that will allow it to know the endpoints it needs to complete the authorization flow.
63 
64```json theme={null}
65{
66 "issuer": "https://auth.your-server.com",
67 "authorization_endpoint": "https://auth.your-server.com/authorize",
68 "token_endpoint": "https://auth.your-server.com/token",
69 "registration_endpoint": "https://auth.your-server.com/register"
70}
71```
72 
73**4. Client Registration**
74 
75With all the metadata out of the way, the client now needs to make sure that it's registered with the authorization server. This can be done in two ways.
76 
77First, the client can be **pre-registered** with a given authorization server, in which case it can have embedded client registration information that it uses to complete the authorization flow.
78 
79Alternatively, the client can use **Dynamic Client Registration** (DCR) to dynamically register itself with the authorization server. The latter scenario requires the authorization server to support DCR. If the authorization server does support DCR, the client will send a request to the `registration_endpoint` with its information:
80 
81```json theme={null}
82{
83 "client_name": "My MCP Client",
84 "redirect_uris": ["http://localhost:3000/callback"],
85 "grant_types": ["authorization_code", "refresh_token"],
86 "response_types": ["code"]
87}
88```
89 
90If the registration succeeds, the authorization server will return a JSON blob with client registration information.
91 
92> [!TIP]
93>
94> **No DCR or Pre-Registration**
95>
96> In case an MCP client connects to an MCP server that doesn't use an authorization server that supports DCR and the client is not pre-registered with said authorization server, it's the responsibility of the client developer to provide an affordance for the end-user to enter client information manually.
97 
98**5. User Authorization**
99 
100The client will now need to open a browser to the `/authorize` endpoint, where the user can log in and grant the required permissions. The authorization server will then redirect back to the client with an authorization code that the client exchanges for tokens:
101 
102```json theme={null}
103{
104 "access_token": "eyJhbGciOiJSUzI1NiIs...",
105 "refresh_token": "def502...",
106 "token_type": "Bearer",
107 "expires_in": 3600
108}
109```
110 
111The access token is what the client will use to authenticate requests to the MCP server. This step follows standard [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) conventions.
112 
113**6. Making Authenticated Requests**
114 
115Finally, the client can make requests to your MCP server using the access token embedded in the `Authorization` header:
116 
117```http theme={null}
118GET /mcp HTTP/1.1
119Host: your-server.com
120Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
121```
122 
123The MCP server will need to validate the token and process the request if the token is valid and has the required permissions.
126124 
127125## Implementation Example
128126 
from line 138
140138 
141139This command will pull the Keycloak container image locally and bootstrap the basic configuration. It will run on port `8080` and have an `admin` user with `admin` password.
142140 
143<Warning>
144 **Not for Production**
145 
146 The configuration above may be suitable for testing and experimentation; however, you should never use it in production. Refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) guide for additional details on how to deploy the authorization server for scenarios that require reliability, security, and high availability.
147</Warning>
141> [!WARNING]
142>
143> **Not for Production**
144>
145> The configuration above may be suitable for testing and experimentation; however, you should never use it in production. Refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) guide for additional details on how to deploy the authorization server for scenarios that require reliability, security, and high availability.
148146 
149147You will be able to access the Keycloak authorization server from your browser at `http://localhost:8080`.
150148 
151<Frame>
152 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-browser.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=cba689d986e113cbe937d732ac0558b6" alt="Keycloak admin dashboard authentication dialog." width="1834" height="1450" data-path="images/tutorial-authorization/keycloak-browser.png" />
153</Frame>
149![Keycloak admin dashboard authentication dialog.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-browser.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=cba689d986e113cbe937d732ac0558b6)
154150 
155151When running with the default configuration, Keycloak will already support many of the capabilities that we need for MCP servers, including Dynamic Client Registration. You can check this by looking at the OIDC configuration, available at:
156152 
from line 158
162158 
163159Go to **Client scopes** in the Keycloak dashboard and create a new `mcp:tools` scope. We will use this to access all of the tools on our MCP server.
164160 
165<Frame>
166 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-scopes.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=3cd49dc2e070027609ae495751e0db58" alt="Configuring Keycloak scopes." width="1999" height="1710" data-path="images/tutorial-authorization/keycloak-scopes.png" />
167</Frame>
161![Configuring Keycloak scopes.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-scopes.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=3cd49dc2e070027609ae495751e0db58)
168162 
169163After creating the scope, make sure that you assign its type to **Default** and have flipped the **Include in token scope** switch, as this will be needed for token validation.
170164 
from line 166
172166 
173167To do this, open your `mcp:tools` client scope and click on **Mappers**, followed by **Configure a new mapper**. Select **Audience**.
174168 
175<Frame>
176 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/scope-add-audience.gif?s=6ea9cf20c397f4c79c491c2e39019272" alt="Configuring an audience for a token in Keycloak." width="1080" height="921" data-path="images/tutorial-authorization/scope-add-audience.gif" />
177</Frame>
169![Configuring an audience for a token in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/scope-add-audience.gif?s=6ea9cf20c397f4c79c491c2e39019272)
178170 
179171For **Name**, use `audience-config`. Add a value for **Included Custom Audience**, set to `http://localhost:3000`. This will be the URI of our test server.
180172 
181<Warning>
182 **Not for Production**
183 
184 The audience configuration above is meant for testing. For production scenarios, additional set-up and configuration will be required to ensure that audiences are properly constrained for issued tokens. Specifically, the audience needs to be based on the resource parameter passed from the client, not a fixed value.
185</Warning>
173> [!WARNING]
174>
175> **Not for Production**
176>
177> The audience configuration above is meant for testing. For production scenarios, additional set-up and configuration will be required to ensure that audiences are properly constrained for issued tokens. Specifically, the audience needs to be based on the resource parameter passed from the client, not a fixed value.
186178 
187179Now, navigate to **Clients**, then **Client registration**, and then **Trusted Hosts**. Disable the **Client URIs Must Match** setting and add the hosts from which you're testing. You can get your current host IP by running the `ifconfig` command on Linux or macOS, or `ipconfig` on Windows. You can see the IP address you need to add by looking at the keycloak logs for a line that looks like `Failed to verify remote host : 192.168.215.1`. Check that the IP address is associated with your host. This may be for a bridge network depending on your docker setup.
188180 
189<Frame>
190 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client.gif?s=b5d40b36a5f1ea1e818821bb8ea77f6b" alt="Setting up client registration details in Keycloak." width="1199" height="1027" data-path="images/tutorial-authorization/keycloak-client.gif" />
191</Frame>
192 
193<Warning>
194 **Getting the Host**
195 
196 If you are running Keycloak from a container, you will also be able to see the host IP from the Terminal in the container logs.
197</Warning>
181![Setting up client registration details in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client.gif?s=b5d40b36a5f1ea1e818821bb8ea77f6b)
182 
183> [!WARNING]
184>
185> **Getting the Host**
186>
187> If you are running Keycloak from a container, you will also be able to see the host IP from the Terminal in the container logs.
198188 
199189Lastly, we need to register a new client that we can use with the **MCP server itself** to talk to Keycloak for things like [token introspection](https://oauth.net/2/token-introspection/). To do that:
200190 
from line 198
208198 
209199When you open the client details, go to **Credentials** and take note of the **Client Secret**.
210200 
211<Frame>
212 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client-auth.gif?s=7152c41a5746994fd399024bc4659e40" alt="Creating a new client in Keycloak." width="1200" height="1023" data-path="images/tutorial-authorization/keycloak-client-auth.gif" />
213</Frame>
214 
215<Warning>
216 **Handling Secrets**
217 
218 Never embed client credentials directly in your code. We recommend using environment variables or specialized solutions for secret storage.
219</Warning>
201![Creating a new client in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client-auth.gif?s=7152c41a5746994fd399024bc4659e40)
202 
203> [!WARNING]
204>
205> **Handling Secrets**
206>
207> Never embed client credentials directly in your code. We recommend using environment variables or specialized solutions for secret storage.
220208 
221209With Keycloak configured, every time the authorization flow is triggered, your MCP server will receive a token like this:
222210 
from line 234
246234}.[Signature]
247235```
248236 
249<Warning>
250 **Embedded Audience**
251 
252 Notice the `aud` claim embedded in the token - it's currently set to be the URI of the test MCP server and it's inferred from the scope that we've previously configured. This will be important in our implementation to validate.
253</Warning>
237> [!WARNING]
238>
239> **Embedded Audience**
240>
241> Notice the `aud` claim embedded in the token - it's currently set to be the URI of the test MCP server and it's inferred from the scope that we've previously configured. This will be important in our implementation to validate.
254242 
255243### MCP Server Setup
256244 
from line 246
258246 
259247For our testing purposes, we will create an extremely simple MCP server that exposes two tools - one for addition and another for multiplication. The server will require authorization to access these.
260248 
261<Tabs>
262 <Tab title="TypeScript">
263 You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
264 
265 Prior to running the code below, ensure that you have a `.env` file with the following content:
266 
267 ```env theme={null}
268 # Server host/port
269 HOST=localhost
270 PORT=3000
271 
272 # Auth server location
273 AUTH_HOST=localhost
274 AUTH_PORT=8080
275 AUTH_REALM=master
276 
277 # Keycloak OAuth client credentials
278 OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
279 OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
280 ```
281 
282 `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
283 
284 In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
285 
286 ```typescript theme={null}
287 import "dotenv/config";
288 import express from "express";
289 import { randomUUID } from "node:crypto";
290 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
291 import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
292 import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
293 import { z } from "zod";
294 import cors from "cors";
295 import {
296 mcpAuthMetadataRouter,
297 getOAuthProtectedResourceMetadataUrl,
298 } from "@modelcontextprotocol/sdk/server/auth/router.js";
299 import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
300 import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
301 import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
302 const CONFIG = {
303 host: process.env.HOST || "localhost",
304 port: Number(process.env.PORT) || 3000,
305 auth: {
306 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
307 port: Number(process.env.AUTH_PORT) || 8080,
308 realm: process.env.AUTH_REALM || "master",
309 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
310 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
311 },
312 };
313 
314 function createOAuthUrls() {
315 const authBaseUrl = new URL(
316 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
317 );
318 return {
319 issuer: authBaseUrl.toString(),
320 introspection_endpoint: new URL(
321 "protocol/openid-connect/token/introspect",
322 authBaseUrl,
323 ).toString(),
324 authorization_endpoint: new URL(
325 "protocol/openid-connect/auth",
326 authBaseUrl,
327 ).toString(),
328 token_endpoint: new URL(
329 "protocol/openid-connect/token",
330 authBaseUrl,
331 ).toString(),
332 };
333 }
334 
335 function createRequestLogger() {
336 return (req: any, res: any, next: any) => {
337 const start = Date.now();
338 res.on("finish", () => {
339 const ms = Date.now() - start;
340 console.log(
341 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
342 );
343 });
344 next();
345 };
346 }
347 
348 const app = express();
349 
350 app.use(
351 express.json({
352 verify: (req: any, _res, buf) => {
353 req.rawBody = buf?.toString() ?? "";
354 },
355 }),
356 );
357 
358 app.use(
359 cors({
360 origin: "*",
361 exposedHeaders: ["Mcp-Session-Id"],
362 }),
363 );
364 
365 app.use(createRequestLogger());
366 
367 const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
368 const oauthUrls = createOAuthUrls();
369 
370 const oauthMetadata: OAuthMetadata = {
371 ...oauthUrls,
372 response_types_supported: ["code"],
373 };
374 
375 const tokenVerifier = {
376 verifyAccessToken: async (token: string) => {
377 const endpoint = oauthMetadata.introspection_endpoint;
378 
379 if (!endpoint) {
380 console.error("[auth] no introspection endpoint in metadata");
381 throw new Error("No token verification endpoint available in metadata");
382 }
383 
384 const params = new URLSearchParams({
385 token: token,
386 client_id: CONFIG.auth.clientId,
387 });
388 
389 if (CONFIG.auth.clientSecret) {
390 params.set("client_secret", CONFIG.auth.clientSecret);
391 }
392 
393 let response: Response;
394 try {
395 response = await fetch(endpoint, {
396 method: "POST",
397 headers: {
398 "Content-Type": "application/x-www-form-urlencoded",
399 },
400 body: params.toString(),
401 });
402 } catch (e) {
403 console.error("[auth] introspection fetch threw", e);
404 throw e;
405 }
406 
407 if (!response.ok) {
408 const txt = await response.text();
409 console.error("[auth] introspection non-OK", { status: response.status });
410 
411 try {
412 const obj = JSON.parse(txt);
413 console.log(JSON.stringify(obj, null, 2));
414 } catch {
415 console.error(txt);
416 }
417 throw new Error(`Invalid or expired token: ${txt}`);
418 }
419 
420 let data: any;
421 try {
422 data = await response.json();
423 } catch (e) {
424 const txt = await response.text();
425 console.error("[auth] failed to parse introspection JSON", {
426 error: String(e),
427 body: txt,
428 });
429 throw e;
430 }
431 
432 if (data.active === false) {
433 throw new Error("Inactive token");
434 }
435 
436 if (!data.aud) {
437 throw new Error("Resource indicator (aud) missing");
438 }
439 
440 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
441 const allowed = audiences.some((a) => {
442 try {
443 return checkResourceAllowed({
444 requestedResource: a,
445 configuredResource: mcpServerUrl,
446 });
447 } catch {
448 // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
449 // Those are never our resource, so treat them as "no match" instead of crashing.
450 return false;
451 }
452 });
453 if (!allowed) {
454 throw new Error(
455 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
456 );
457 }
458 
459 return {
460 token,
461 clientId: data.client_id,
462 scopes: data.scope ? data.scope.split(" ") : [],
463 expiresAt: data.exp,
464 };
465 },
466 };
467 app.use(
468 mcpAuthMetadataRouter({
469 oauthMetadata,
470 resourceServerUrl: mcpServerUrl,
471 scopesSupported: ["mcp:tools"],
472 resourceName: "MCP Demo Server",
473 }),
474 );
475 
476 const authMiddleware = requireBearerAuth({
477 verifier: tokenVerifier,
478 requiredScopes: [],
479 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
480 });
481 
482 const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
483 
484 function createMcpServer() {
485 const server = new McpServer({
486 name: "example-server",
487 version: "1.0.0",
488 });
489 
490 server.registerTool(
491 "add",
492 {
493 title: "Addition Tool",
494 description: "Add two numbers together",
495 inputSchema: {
496 a: z.number().describe("First number to add"),
497 b: z.number().describe("Second number to add"),
498 },
499 },
500 async ({ a, b }) => ({
501 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
502 }),
503 );
504 
505 server.registerTool(
506 "multiply",
507 {
508 title: "Multiplication Tool",
509 description: "Multiply two numbers together",
510 inputSchema: {
511 x: z.number().describe("First number to multiply"),
512 y: z.number().describe("Second number to multiply"),
513 },
514 },
515 async ({ x, y }) => ({
516 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
517 }),
518 );
519 
520 return server;
521 }
522 
523 const mcpPostHandler = async (req: express.Request, res: express.Response) => {
524 const sessionId = req.headers["mcp-session-id"] as string | undefined;
525 let transport: StreamableHTTPServerTransport;
526 
527 if (sessionId && transports[sessionId]) {
528 transport = transports[sessionId];
529 } else if (!sessionId && isInitializeRequest(req.body)) {
530 transport = new StreamableHTTPServerTransport({
531 sessionIdGenerator: () => randomUUID(),
532 onsessioninitialized: (sessionId) => {
533 transports[sessionId] = transport;
534 },
535 });
536 
537 transport.onclose = () => {
538 if (transport.sessionId) {
539 delete transports[transport.sessionId];
540 }
541 };
542 
543 const server = createMcpServer();
544 await server.connect(transport);
545 } else {
546 res.status(400).json({
547 jsonrpc: "2.0",
548 error: {
549 code: -32000,
550 message: "Bad Request: No valid session ID provided",
551 },
552 id: null,
553 });
554 return;
555 }
556 
557 await transport.handleRequest(req, res, req.body);
558 };
559 
560 const handleSessionRequest = async (
561 req: express.Request,
562 res: express.Response,
563 ) => {
564 const sessionId = req.headers["mcp-session-id"] as string | undefined;
565 if (!sessionId || !transports[sessionId]) {
566 res.status(400).send("Invalid or missing session ID");
567 return;
568 }
569 
570 const transport = transports[sessionId];
571 await transport.handleRequest(req, res);
572 };
573 
574 app.post("/", authMiddleware, mcpPostHandler);
575 app.get("/", authMiddleware, handleSessionRequest);
576 app.delete("/", authMiddleware, handleSessionRequest);
577 
578 app.listen(CONFIG.port, CONFIG.host, () => {
579 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
580 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
249**TypeScript**
250 
251You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252 
253Prior to running the code below, ensure that you have a `.env` file with the following content:
254 
255```env theme={null}
256# Server host/port
257HOST=localhost
258PORT=3000
259 
260# Auth server location
261AUTH_HOST=localhost
262AUTH_PORT=8080
263AUTH_REALM=master
264 
265# Keycloak OAuth client credentials
266OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
268```
269 
270`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
271 
272In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
273 
274```typescript theme={null}
275import "dotenv/config";
276import express from "express";
277import { randomUUID } from "node:crypto";
278import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281import { z } from "zod";
282import cors from "cors";
283import {
284 mcpAuthMetadataRouter,
285 getOAuthProtectedResourceMetadataUrl,
286} from "@modelcontextprotocol/sdk/server/auth/router.js";
287import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290const CONFIG = {
291 host: process.env.HOST || "localhost",
292 port: Number(process.env.PORT) || 3000,
293 auth: {
294 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
295 port: Number(process.env.AUTH_PORT) || 8080,
296 realm: process.env.AUTH_REALM || "master",
297 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
298 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
299 },
300};
301 
302function createOAuthUrls() {
303 const authBaseUrl = new URL(
304 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
305 );
306 return {
307 issuer: authBaseUrl.toString(),
308 introspection_endpoint: new URL(
309 "protocol/openid-connect/token/introspect",
310 authBaseUrl,
311 ).toString(),
312 authorization_endpoint: new URL(
313 "protocol/openid-connect/auth",
314 authBaseUrl,
315 ).toString(),
316 token_endpoint: new URL(
317 "protocol/openid-connect/token",
318 authBaseUrl,
319 ).toString(),
320 };
321}
322 
323function createRequestLogger() {
324 return (req: any, res: any, next: any) => {
325 const start = Date.now();
326 res.on("finish", () => {
327 const ms = Date.now() - start;
581328 console.log(
582 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
583330 );
584331 });
585 ```
586 
587 When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
588 
589 For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
590 </Tab>
591 
592 <Tab title="Python">
593 You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
594 
595 To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
596 
597 Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
598 
599 ```python theme={null}
600 """Configuration settings for the MCP auth server."""
601 
602 import os
603 
604 
605 class Config:
606 """Configuration class that loads from environment variables with sensible defaults."""
607 
608 # Server settings
609 HOST: str = os.getenv("HOST", "localhost")
610 PORT: int = int(os.getenv("PORT", "3000"))
611 
612 # Auth server settings
613 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
614 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
615 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
616 
617 # OAuth client settings
618 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
619 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
620 
621 # Scope required on every token
622 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
623 
624 @property
625 def server_url(self) -> str:
626 """Build the server URL."""
627 return f"http://{self.HOST}:{self.PORT}"
628 
629 @property
630 def auth_base_url(self) -> str:
631 """Build the auth server base URL."""
632 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
633 
634 
635 # Global configuration instance
636 config = Config()
637 ```
638 
639 `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
640 
641 The server implementation is as follows:
642 
643 ```python theme={null}
644 import datetime
645 import logging
646 from typing import Any
647 from urllib.parse import urljoin
648 
649 from pydantic import AnyHttpUrl
650 
651 from mcp.server import MCPServer
652 from mcp.server.auth.settings import AuthSettings
653 
654 from .config import config
655 from .token_verifier import IntrospectionTokenVerifier
656 
657 logger = logging.getLogger(__name__)
658 
659 
660 def create_oauth_urls() -> dict[str, str]:
661 """Create OAuth URLs based on configuration (Keycloak-style)."""
662 auth_base_url = config.auth_base_url
663 
332 next();
333 };
334}
335 
336const app = express();
337 
338app.use(
339 express.json({
340 verify: (req: any, _res, buf) => {
341 req.rawBody = buf?.toString() ?? "";
342 },
343 }),
344);
345 
346app.use(
347 cors({
348 origin: "*",
349 exposedHeaders: ["Mcp-Session-Id"],
350 }),
351);
352 
353app.use(createRequestLogger());
354 
355const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356const oauthUrls = createOAuthUrls();
357 
358const oauthMetadata: OAuthMetadata = {
359 ...oauthUrls,
360 response_types_supported: ["code"],
361};
362 
363const tokenVerifier = {
364 verifyAccessToken: async (token: string) => {
365 const endpoint = oauthMetadata.introspection_endpoint;
366 
367 if (!endpoint) {
368 console.error("[auth] no introspection endpoint in metadata");
369 throw new Error("No token verification endpoint available in metadata");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) => {
430 try {
431 return checkResourceAllowed({
432 requestedResource: a,
433 configuredResource: mcpServerUrl,
434 });
435 } catch {
436 // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
437 // Those are never our resource, so treat them as "no match" instead of crashing.
438 return false;
439 }
440 });
441 if (!allowed) {
442 throw new Error(
443 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
444 );
445 }
446 
447 return {
448 token,
449 clientId: data.client_id,
450 scopes: data.scope ? data.scope.split(" ") : [],
451 expiresAt: data.exp,
452 };
453 },
454};
455app.use(
456 mcpAuthMetadataRouter({
457 oauthMetadata,
458 resourceServerUrl: mcpServerUrl,
459 scopesSupported: ["mcp:tools"],
460 resourceName: "MCP Demo Server",
461 }),
462);
463 
464const authMiddleware = requireBearerAuth({
465 verifier: tokenVerifier,
466 requiredScopes: [],
467 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
468});
469 
470const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
471 
472function createMcpServer() {
473 const server = new McpServer({
474 name: "example-server",
475 version: "1.0.0",
476 });
477 
478 server.registerTool(
479 "add",
480 {
481 title: "Addition Tool",
482 description: "Add two numbers together",
483 inputSchema: {
484 a: z.number().describe("First number to add"),
485 b: z.number().describe("Second number to add"),
486 },
487 },
488 async ({ a, b }) => ({
489 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
490 }),
491 );
492 
493 server.registerTool(
494 "multiply",
495 {
496 title: "Multiplication Tool",
497 description: "Multiply two numbers together",
498 inputSchema: {
499 x: z.number().describe("First number to multiply"),
500 y: z.number().describe("Second number to multiply"),
501 },
502 },
503 async ({ x, y }) => ({
504 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
505 }),
506 );
507 
508 return server;
509}
510 
511const mcpPostHandler = async (req: express.Request, res: express.Response) => {
512 const sessionId = req.headers["mcp-session-id"] as string | undefined;
513 let transport: StreamableHTTPServerTransport;
514 
515 if (sessionId && transports[sessionId]) {
516 transport = transports[sessionId];
517 } else if (!sessionId && isInitializeRequest(req.body)) {
518 transport = new StreamableHTTPServerTransport({
519 sessionIdGenerator: () => randomUUID(),
520 onsessioninitialized: (sessionId) => {
521 transports[sessionId] = transport;
522 },
523 });
524 
525 transport.onclose = () => {
526 if (transport.sessionId) {
527 delete transports[transport.sessionId];
528 }
529 };
530 
531 const server = createMcpServer();
532 await server.connect(transport);
533 } else {
534 res.status(400).json({
535 jsonrpc: "2.0",
536 error: {
537 code: -32000,
538 message: "Bad Request: No valid session ID provided",
539 },
540 id: null,
541 });
542 return;
543 }
544 
545 await transport.handleRequest(req, res, req.body);
546};
547 
548const handleSessionRequest = async (
549 req: express.Request,
550 res: express.Response,
551) => {
552 const sessionId = req.headers["mcp-session-id"] as string | undefined;
553 if (!sessionId || !transports[sessionId]) {
554 res.status(400).send("Invalid or missing session ID");
555 return;
556 }
557 
558 const transport = transports[sessionId];
559 await transport.handleRequest(req, res);
560};
561 
562app.post("/", authMiddleware, mcpPostHandler);
563app.get("/", authMiddleware, handleSessionRequest);
564app.delete("/", authMiddleware, handleSessionRequest);
565 
566app.listen(CONFIG.port, CONFIG.host, () => {
567 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
568 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
569 console.log(
570 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
571 );
572});
573```
574 
575When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
576 
577For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
578 
579**Python**
580 
581You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
582 
583To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
584 
585Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
586 
587```python theme={null}
588"""Configuration settings for the MCP auth server."""
589 
590import os
591 
592 
593class Config:
594 """Configuration class that loads from environment variables with sensible defaults."""
595 
596 # Server settings
597 HOST: str = os.getenv("HOST", "localhost")
598 PORT: int = int(os.getenv("PORT", "3000"))
599 
600 # Auth server settings
601 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
602 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
603 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
604 
605 # OAuth client settings
606 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
607 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
608 
609 # Scope required on every token
610 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
611 
612 @property
613 def server_url(self) -> str:
614 """Build the server URL."""
615 return f"http://{self.HOST}:{self.PORT}"
616 
617 @property
618 def auth_base_url(self) -> str:
619 """Build the auth server base URL."""
620 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
621 
622 
623# Global configuration instance
624config = Config()
625```
626 
627`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
628 
629The server implementation is as follows:
630 
631```python theme={null}
632import datetime
633import logging
634from typing import Any
635from urllib.parse import urljoin
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server import MCPServer
640from mcp.server.auth.settings import AuthSettings
641 
642from .config import config
643from .token_verifier import IntrospectionTokenVerifier
644 
645logger = logging.getLogger(__name__)
646 
647 
648def create_oauth_urls() -> dict[str, str]:
649 """Create OAuth URLs based on configuration (Keycloak-style)."""
650 auth_base_url = config.auth_base_url
651 
652 return {
653 "issuer": auth_base_url,
654 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
655 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
656 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
657 }
658 
659 
660def create_server() -> MCPServer:
661 """Create and configure the MCP server."""
662 
663 oauth_urls = create_oauth_urls()
664 
665 token_verifier = IntrospectionTokenVerifier(
666 introspection_endpoint=oauth_urls["introspection_endpoint"],
667 server_url=config.server_url,
668 client_id=config.OAUTH_CLIENT_ID,
669 client_secret=config.OAUTH_CLIENT_SECRET,
670 )
671 
672 app = MCPServer(
673 name="MCP Resource Server",
674 instructions="Resource Server that validates tokens via Authorization Server introspection",
675 debug=True,
676 token_verifier=token_verifier,
677 auth=AuthSettings(
678 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
679 required_scopes=[config.MCP_SCOPE],
680 resource_server_url=AnyHttpUrl(config.server_url),
681 ),
682 )
683 
684 @app.tool()
685 async def add_numbers(a: float, b: float) -> dict[str, Any]:
686 """
687 Add two numbers together.
688 This tool demonstrates basic arithmetic operations with OAuth authentication.
689 
690 Args:
691 a: The first number to add
692 b: The second number to add
693 """
694 result = a + b
664695 return {
665 "issuer": auth_base_url,
666 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
667 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
668 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
696 "operation": "addition",
697 "operand_a": a,
698 "operand_b": b,
699 "result": result,
700 "timestamp": datetime.datetime.now().isoformat(),
669701 }
670702 
671 
672 def create_server() -> MCPServer:
673 """Create and configure the MCP server."""
674 
675 oauth_urls = create_oauth_urls()
676 
677 token_verifier = IntrospectionTokenVerifier(
678 introspection_endpoint=oauth_urls["introspection_endpoint"],
679 server_url=config.server_url,
680 client_id=config.OAUTH_CLIENT_ID,
681 client_secret=config.OAUTH_CLIENT_SECRET,
703 @app.tool()
704 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
705 """
706 Multiply two numbers together.
707 This tool demonstrates basic arithmetic operations with OAuth authentication.
708 
709 Args:
710 x: The first number to multiply
711 y: The second number to multiply
712 """
713 result = x * y
714 return {
715 "operation": "multiplication",
716 "operand_x": x,
717 "operand_y": y,
718 "result": result,
719 "timestamp": datetime.datetime.now().isoformat(),
720 }
721 
722 return app
723 
724 
725def main() -> int:
726 """
727 Run the MCP Resource Server.
728 
729 This server:
730 - Provides RFC 9728 Protected Resource Metadata
731 - Validates tokens via Authorization Server introspection
732 - Serves MCP tools requiring authentication
733 
734 Configuration is loaded from config.py and environment variables.
735 """
736 logging.basicConfig(level=logging.INFO)
737 
738 oauth_urls = create_oauth_urls()
739 
740 try:
741 mcp_server = create_server()
742 
743 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
744 logger.info("Authorization Server: %s", oauth_urls["issuer"])
745 
746 mcp_server.run(
747 transport="streamable-http",
748 host=config.HOST,
749 port=config.PORT,
750 streamable_http_path="/",
682751 )
683 
684 app = MCPServer(
685 name="MCP Resource Server",
686 instructions="Resource Server that validates tokens via Authorization Server introspection",
687 debug=True,
688 token_verifier=token_verifier,
689 auth=AuthSettings(
690 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
691 required_scopes=[config.MCP_SCOPE],
692 resource_server_url=AnyHttpUrl(config.server_url),
693 ),
694 )
695 
696 @app.tool()
697 async def add_numbers(a: float, b: float) -> dict[str, Any]:
698 """
699 Add two numbers together.
700 This tool demonstrates basic arithmetic operations with OAuth authentication.
701 
702 Args:
703 a: The first number to add
704 b: The second number to add
705 """
706 result = a + b
707 return {
708 "operation": "addition",
709 "operand_a": a,
710 "operand_b": b,
711 "result": result,
712 "timestamp": datetime.datetime.now().isoformat(),
752 return 0
753 
754 except Exception:
755 logger.exception("Server error")
756 return 1
757 
758 
759if __name__ == "__main__":
760 exit(main())
761```
762 
763Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts.
764 
765```python theme={null}
766"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
767 
768import logging
769from typing import Any
770 
771import httpx2
772 
773from mcp.server.auth.provider import AccessToken, TokenVerifier
774from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
775 
776logger = logging.getLogger(__name__)
777 
778 
779class IntrospectionTokenVerifier(TokenVerifier):
780 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
781 
782 def __init__(
783 self,
784 introspection_endpoint: str,
785 server_url: str,
786 client_id: str,
787 client_secret: str,
788 ):
789 self.introspection_endpoint = introspection_endpoint
790 self.server_url = server_url
791 self.client_id = client_id
792 self.client_secret = client_secret
793 self.resource_url = resource_url_from_server_url(server_url)
794 
795 async def verify_token(self, token: str) -> AccessToken | None:
796 """Verify token via introspection endpoint."""
797 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
798 return None
799 
800 timeout = httpx2.Timeout(10.0, connect=5.0)
801 limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
802 
803 async with httpx2.AsyncClient(
804 timeout=timeout,
805 limits=limits,
806 verify=True,
807 ) as client:
808 try:
809 form_data = {
810 "token": token,
811 "client_id": self.client_id,
812 }
813 # Only send client_secret when one is configured
814 # Public clients authenticate with client_id alone.
815 if self.client_secret:
816 form_data["client_secret"] = self.client_secret
817 headers = {"Content-Type": "application/x-www-form-urlencoded"}
818 
819 response = await client.post(
820 self.introspection_endpoint,
821 data=form_data,
822 headers=headers,
823 )
824 
825 if response.status_code != 200:
826 return None
827 
828 data = response.json()
829 if not data.get("active", False):
830 return None
831 
832 if not self._validate_resource(data):
833 return None
834 
835 return AccessToken(
836 token=token,
837 client_id=data.get("client_id", "unknown"),
838 scopes=data.get("scope", "").split() if data.get("scope") else [],
839 expires_at=data.get("exp"),
840 # AccessToken.resource is `str | None`. Keycloak returns `aud`
841 # as a *list* here (e.g. ["test-client", "http://localhost:3000",
842 # "account"]); passing that list straight in raises a pydantic
843 # ValidationError that the broad `except` below turns into a
844 # silent 401. We already confirmed this server's resource is a
845 # valid audience in `_validate_resource`, so record that.
846 resource=self.resource_url,
847 subject=data.get("sub"), # RFC 7662 subject (resource owner)
848 claims=data,
849 )
850 
851 except Exception:
852 logger.exception("Token introspection failed")
853 return None
854 
855 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
856 """Validate token was issued for this resource server.
857 
858 Rules:
859 - Reject if 'aud' missing.
860 - Accept if any audience entry matches the derived resource URL.
861 - Supports string or list forms per JWT spec.
862 """
863 if not self.server_url or not self.resource_url:
864 return False
865 
866 aud: list[str] | str | None = token_data.get("aud")
867 if isinstance(aud, list):
868 return any(self._is_valid_resource(a) for a in aud)
869 if isinstance(aud, str):
870 return self._is_valid_resource(aud)
871 return False
872 
873 def _is_valid_resource(self, resource: str) -> bool:
874 """Check if the given resource matches our server."""
875 return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
876```
877 
878For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
879 
880**Python MCP Server**
881 
882In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
883 
884```toml theme={null}
885[project]
886name = "mcp-simple-auth"
887version = "0.1.0"
888description = "A simple MCP server demonstrating OAuth authentication"
889requires-python = ">=3.10"
890authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
891license = { text = "MIT" }
892dependencies = [
893 "httpx2>=2.5.0",
894 "mcp>=2.0.0rc1",
895 "pydantic>=2.0",
896]
897 
898[project.scripts]
899mcp-simple-auth-rs = "mcp_server.server:main"
900 
901[build-system]
902requires = ["hatchling"]
903build-backend = "hatchling.build"
904 
905[tool.hatch.build.targets.wheel]
906packages = ["mcp_server"]
907 
908[dependency-groups]
909dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
910```
911 
912Then run the commands below to start the server.
913 
914```bash theme={null}
915uv sync
916uv run mcp-simple-auth-rs
917```
918 
919**C#**
920 
921You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
922 
923To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
924 
925In the root of your server folder, create two files, `Program.cs` and `ProtectedMcpServer.csproj`, and a `Tools` folder. Fill `Program.cs` with:
926 
927```csharp theme={null}
928using Microsoft.AspNetCore.Authentication.JwtBearer;
929using Microsoft.IdentityModel.Tokens;
930using ModelContextProtocol.AspNetCore.Authentication;
931using ProtectedMcpServer.Tools;
932using System.Security.Claims;
933 
934var builder = WebApplication.CreateBuilder(args);
935 
936var serverUrl = "http://localhost:3000/";
937var authorizationServerUrl = "http://localhost:8080/realms/master/";
938 
939builder.Services.AddAuthentication(options =>
940{
941 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
942 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
943})
944.AddJwtBearer(options =>
945{
946 options.Authority = authorizationServerUrl;
947 var normalizedServerAudience = serverUrl.TrimEnd('/');
948 options.TokenValidationParameters = new TokenValidationParameters
949 {
950 ValidIssuer = authorizationServerUrl,
951 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
952 AudienceValidator = (audiences, securityToken, validationParameters) =>
953 {
954 if (audiences == null) return false;
955 foreach (var aud in audiences)
956 {
957 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
958 {
959 return true;
960 }
713961 }
714 
715 @app.tool()
716 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
717 """
718 Multiply two numbers together.
719 This tool demonstrates basic arithmetic operations with OAuth authentication.
720 
721 Args:
722 x: The first number to multiply
723 y: The second number to multiply
724 """
725 result = x * y
726 return {
727 "operation": "multiplication",
728 "operand_x": x,
729 "operand_y": y,
730 "result": result,
731 "timestamp": datetime.datetime.now().isoformat(),
732 }
733 
734 return app
735 
736 
737 def main() -> int:
738 """
739 Run the MCP Resource Server.
740 
741 This server:
742 - Provides RFC 9728 Protected Resource Metadata
743 - Validates tokens via Authorization Server introspection
744 - Serves MCP tools requiring authentication
745 
746 Configuration is loaded from config.py and environment variables.
747 """
748 logging.basicConfig(level=logging.INFO)
749 
750 oauth_urls = create_oauth_urls()
751 
752 try:
753 mcp_server = create_server()
754 
755 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
756 logger.info("Authorization Server: %s", oauth_urls["issuer"])
757 
758 mcp_server.run(
759 transport="streamable-http",
760 host=config.HOST,
761 port=config.PORT,
762 streamable_http_path="/",
763 )
764 return 0
765 
766 except Exception:
767 logger.exception("Server error")
768 return 1
769 
770 
771 if __name__ == "__main__":
772 exit(main())
773 ```
774 
775 Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts.
776 
777 ```python theme={null}
778 """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
779 
780 import logging
781 from typing import Any
782 
783 import httpx2
784 
785 from mcp.server.auth.provider import AccessToken, TokenVerifier
786 from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
787 
788 logger = logging.getLogger(__name__)
789 
790 
791 class IntrospectionTokenVerifier(TokenVerifier):
792 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
793 
794 def __init__(
795 self,
796 introspection_endpoint: str,
797 server_url: str,
798 client_id: str,
799 client_secret: str,
800 ):
801 self.introspection_endpoint = introspection_endpoint
802 self.server_url = server_url
803 self.client_id = client_id
804 self.client_secret = client_secret
805 self.resource_url = resource_url_from_server_url(server_url)
806 
807 async def verify_token(self, token: str) -> AccessToken | None:
808 """Verify token via introspection endpoint."""
809 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
810 return None
811 
812 timeout = httpx2.Timeout(10.0, connect=5.0)
813 limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
814 
815 async with httpx2.AsyncClient(
816 timeout=timeout,
817 limits=limits,
818 verify=True,
819 ) as client:
820 try:
821 form_data = {
822 "token": token,
823 "client_id": self.client_id,
824 }
825 # Only send client_secret when one is configured
826 # Public clients authenticate with client_id alone.
827 if self.client_secret:
828 form_data["client_secret"] = self.client_secret
829 headers = {"Content-Type": "application/x-www-form-urlencoded"}
830 
831 response = await client.post(
832 self.introspection_endpoint,
833 data=form_data,
834 headers=headers,
835 )
836 
837 if response.status_code != 200:
838 return None
839 
840 data = response.json()
841 if not data.get("active", False):
842 return None
843 
844 if not self._validate_resource(data):
845 return None
846 
847 return AccessToken(
848 token=token,
849 client_id=data.get("client_id", "unknown"),
850 scopes=data.get("scope", "").split() if data.get("scope") else [],
851 expires_at=data.get("exp"),
852 # AccessToken.resource is `str | None`. Keycloak returns `aud`
853 # as a *list* here (e.g. ["test-client", "http://localhost:3000",
854 # "account"]); passing that list straight in raises a pydantic
855 # ValidationError that the broad `except` below turns into a
856 # silent 401. We already confirmed this server's resource is a
857 # valid audience in `_validate_resource`, so record that.
858 resource=self.resource_url,
859 subject=data.get("sub"), # RFC 7662 subject (resource owner)
860 claims=data,
861 )
862 
863 except Exception:
864 logger.exception("Token introspection failed")
865 return None
866 
867 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
868 """Validate token was issued for this resource server.
869 
870 Rules:
871 - Reject if 'aud' missing.
872 - Accept if any audience entry matches the derived resource URL.
873 - Supports string or list forms per JWT spec.
874 """
875 if not self.server_url or not self.resource_url:
876 return False
877 
878 aud: list[str] | str | None = token_data.get("aud")
879 if isinstance(aud, list):
880 return any(self._is_valid_resource(a) for a in aud)
881 if isinstance(aud, str):
882 return self._is_valid_resource(aud)
883 return False
884 
885 def _is_valid_resource(self, resource: str) -> bool:
886 """Check if the given resource matches our server."""
887 return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
888 ```
889 
890 For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
891 
892 **Python MCP Server**
893 
894 In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
895 
896 ```toml theme={null}
897 [project]
898 name = "mcp-simple-auth"
899 version = "0.1.0"
900 description = "A simple MCP server demonstrating OAuth authentication"
901 requires-python = ">=3.10"
902 authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
903 license = { text = "MIT" }
904 dependencies = [
905 "httpx2>=2.5.0",
906 "mcp>=2.0.0rc1",
907 "pydantic>=2.0",
908 ]
909 
910 [project.scripts]
911 mcp-simple-auth-rs = "mcp_server.server:main"
912 
913 [build-system]
914 requires = ["hatchling"]
915 build-backend = "hatchling.build"
916 
917 [tool.hatch.build.targets.wheel]
918 packages = ["mcp_server"]
919 
920 [dependency-groups]
921 dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
922 ```
923 
924 Then run the commands below to start the server.
925 
926 ```bash theme={null}
927 uv sync
928 uv run mcp-simple-auth-rs
929 ```
930 </Tab>
931 
932 <Tab title="C#">
933 You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
934 
935 To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
936 
937 In the root of your server folder, create two files, `Program.cs` and `ProtectedMcpServer.csproj`, and a `Tools` folder. Fill `Program.cs` with:
938 
939 ```csharp theme={null}
940 using Microsoft.AspNetCore.Authentication.JwtBearer;
941 using Microsoft.IdentityModel.Tokens;
942 using ModelContextProtocol.AspNetCore.Authentication;
943 using ProtectedMcpServer.Tools;
944 using System.Security.Claims;
945 
946 var builder = WebApplication.CreateBuilder(args);
947 
948 var serverUrl = "http://localhost:3000/";
949 var authorizationServerUrl = "http://localhost:8080/realms/master/";
950 
951 builder.Services.AddAuthentication(options =>
962 return false;
963 }
964 };
965 
966 options.RequireHttpsMetadata = false; // Set to true in production
967 
968 options.Events = new JwtBearerEvents
952969 {
953 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
954 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
955 })
956 .AddJwtBearer(options =>
970 OnTokenValidated = context =>
971 {
972 var name = context.Principal?.Identity?.Name ?? "unknown";
973 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
974 Console.WriteLine($"Token validated for: {name} ({email})");
975 return Task.CompletedTask;
976 },
977 OnAuthenticationFailed = context =>
978 {
979 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
980 return Task.CompletedTask;
981 },
982 };
983})
984.AddMcp(options =>
985{
986 options.ResourceMetadata = new()
957987 {
958 options.Authority = authorizationServerUrl;
959 var normalizedServerAudience = serverUrl.TrimEnd('/');
960 options.TokenValidationParameters = new TokenValidationParameters
961 {
962 ValidIssuer = authorizationServerUrl,
963 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
964 AudienceValidator = (audiences, securityToken, validationParameters) =>
965 {
966 if (audiences == null) return false;
967 foreach (var aud in audiences)
968 {
969 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
970 {
971 return true;
972 }
973 }
974 return false;
975 }
976 };
977 
978 options.RequireHttpsMetadata = false; // Set to true in production
979 
980 options.Events = new JwtBearerEvents
981 {
982 OnTokenValidated = context =>
983 {
984 var name = context.Principal?.Identity?.Name ?? "unknown";
985 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
986 Console.WriteLine($"Token validated for: {name} ({email})");
987 return Task.CompletedTask;
988 },
989 OnAuthenticationFailed = context =>
990 {
991 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
992 return Task.CompletedTask;
993 },
994 };
995 })
996 .AddMcp(options =>
988 Resource = serverUrl,
989 ResourceDocumentation = "https://docs.example.com/api/math",
990 AuthorizationServers = { authorizationServerUrl },
991 ScopesSupported = ["mcp:tools"]
992 };
993});
994 
995builder.Services.AddAuthorization();
996 
997builder.Services.AddHttpContextAccessor();
998builder.Services.AddMcpServer()
999 .WithTools<MathTools>()
1000 .WithHttpTransport();
1001 
1002var app = builder.Build();
1003 
1004app.UseAuthentication();
1005app.UseAuthorization();
1006 
1007app.MapMcp().RequireAuthorization();
1008 
1009Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1010Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1011Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1012Console.WriteLine("Exposed Math tools: Add, Multiply");
1013Console.WriteLine("Press Ctrl+C to stop the server");
1014 
1015app.Run(serverUrl);
1016```
1017 
1018Fill `ProtectedMcpServer.csproj` with:
1019 
1020```xml theme={null}
1021<Project Sdk="Microsoft.NET.Sdk.Web">
1022 
1023 <PropertyGroup>
1024 <TargetFramework>net9.0</TargetFramework>
1025 <Nullable>enable</Nullable>
1026 <ImplicitUsings>enable</ImplicitUsings>
1027 <!-- Identifier for the local secret store, not a secret itself. -->
1028 <UserSecretsId>local-authorization-mcp-server</UserSecretsId>
1029 </PropertyGroup>
1030 
1031 <ItemGroup>
1032 <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.18" />
1033 <PackageReference Include="ModelContextProtocol" Version="2.0.0" />
1034 <PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.0.0" />
1035 </ItemGroup>
1036 
1037</Project>
1038```
1039 
1040In the `Tools` folder, create `MathTools.cs` and fill it with:
1041 
1042```csharp theme={null}
1043using System.ComponentModel;
1044using ModelContextProtocol.Server;
1045 
1046namespace ProtectedMcpServer.Tools;
1047 
1048[McpServerToolType]
1049public sealed class MathTools
1050{
1051 [McpServerTool, Description("Add two numbers together.")]
1052 public Task<double> Add(
1053 [Description("First operand")] double a,
1054 [Description("Second operand")] double b)
9971055 {
998 options.ResourceMetadata = new()
999 {
1000 Resource = serverUrl,
1001 ResourceDocumentation = "https://docs.example.com/api/math",
1002 AuthorizationServers = { authorizationServerUrl },
1003 ScopesSupported = ["mcp:tools"]
1004 };
1005 });
1006 
1007 builder.Services.AddAuthorization();
1008 
1009 builder.Services.AddHttpContextAccessor();
1010 builder.Services.AddMcpServer()
1011 .WithTools<MathTools>()
1012 .WithHttpTransport();
1013 
1014 var app = builder.Build();
1015 
1016 app.UseAuthentication();
1017 app.UseAuthorization();
1018 
1019 app.MapMcp().RequireAuthorization();
1020 
1021 Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1022 Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1023 Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1024 Console.WriteLine("Exposed Math tools: Add, Multiply");
1025 Console.WriteLine("Press Ctrl+C to stop the server");
1026 
1027 app.Run(serverUrl);
1028 ```
1029 
1030 Fill `ProtectedMcpServer.csproj` with:
1031 
1032 ```xml theme={null}
1033 <Project Sdk="Microsoft.NET.Sdk.Web">
1034 
1035 <PropertyGroup>
1036 <TargetFramework>net9.0</TargetFramework>
1037 <Nullable>enable</Nullable>
1038 <ImplicitUsings>enable</ImplicitUsings>
1039 <!-- Identifier for the local secret store, not a secret itself. -->
1040 <UserSecretsId>local-authorization-mcp-server</UserSecretsId>
1041 </PropertyGroup>
1042 
1043 <ItemGroup>
1044 <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.18" />
1045 <PackageReference Include="ModelContextProtocol" Version="2.0.0" />
1046 <PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.0.0" />
1047 </ItemGroup>
1048 
1049 </Project>
1050 ```
1051 
1052 In the `Tools` folder, create `MathTools.cs` and fill it with:
1053 
1054 ```csharp theme={null}
1055 using System.ComponentModel;
1056 using ModelContextProtocol.Server;
1057 
1058 namespace ProtectedMcpServer.Tools;
1059 
1060 [McpServerToolType]
1061 public sealed class MathTools
1056 return Task.FromResult(a + b);
1057 }
1058 
1059 [McpServerTool, Description("Multiply two numbers together.")]
1060 public Task<double> Multiply(
1061 [Description("First operand")] double a,
1062 [Description("Second operand")] double b)
10621063 {
1063 [McpServerTool, Description("Add two numbers together.")]
1064 public Task<double> Add(
1065 [Description("First operand")] double a,
1066 [Description("Second operand")] double b)
1067 {
1068 return Task.FromResult(a + b);
1069 }
1070 
1071 [McpServerTool, Description("Multiply two numbers together.")]
1072 public Task<double> Multiply(
1073 [Description("First operand")] double a,
1074 [Description("Second operand")] double b)
1075 {
1076 return Task.FromResult(a * b);
1077 }
1064 return Task.FromResult(a * b);
10781065 }
1079 ```
1080 
1081 Then from the server's root, run:
1082 
1083 ```bash theme={null}
1084 dotnet run
1085 ```
1086 
1087 For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
1088 </Tab>
1089</Tabs>
1066}
1067```
1068 
1069Then from the server's root, run:
1070 
1071```bash theme={null}
1072dotnet run
1073```
1074 
1075For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
10901076 
10911077## Testing the MCP Server
10921078 
from line 1089
11031089 
11041090On connection, you will be taken to the browser, where you will be prompted to consent to Visual Studio Code having access to the `mcp:tools` scope.
11051091 
1106<Frame>
1107 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-vscode.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=d5183fb7c257993aed1b2246f0bbbb27" alt="Keycloak consent form for VS Code." width="1915" height="1536" data-path="images/tutorial-authorization/keycloak-vscode.png" />
1108</Frame>
1092![Keycloak consent form for VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-vscode.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=d5183fb7c257993aed1b2246f0bbbb27)
11091093 
11101094After consenting, you will see the tools listed right above the server entry in `mcp.json`.
11111095 
1112<Frame>
1113 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=f7c34d1bf115fe6934e01b4a5a91168b" alt="Tools listed in VS Code." width="496" height="160" data-path="images/tutorial-authorization/tools-vs-code.png" />
1114</Frame>
1096![Tools listed in VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=f7c34d1bf115fe6934e01b4a5a91168b)
11151097 
11161098You will be able to invoke individual tools with the help of the `#` sign in the chat view.
11171099 
1118<Frame>
1119 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code-invoke.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=76cbef68e48821a3c5467bd20c7e89fe" alt="Invoking MCP tools in VS Code." width="1276" height="396" data-path="images/tutorial-authorization/tools-vs-code-invoke.png" />
1120</Frame>
1100![Invoking MCP tools in VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code-invoke.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=76cbef68e48821a3c5467bd20c7e89fe)
11211101 
11221102## Common Pitfalls and How to Avoid Them
11231103 
11241104 
Feedback