Follow Discord
Sweep 10 Nov 2026 · 02:18Z Build v2.1.322 539 read Stable v2.1.311 Latest v2.1.322 Next v2.1.322 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · mcp

Understanding Authorization in MCP changeddocs/draft/tutorials/security/authorization

Upstream edited this page at 28 Jul 2026 15:42 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 30 Oct 2026 18:37 UTC.

Upstream edited
Recorded here
Lines+768added
Lines−768removed
From line 246 where the diff opens
First seen 26 Aug 2026 this site's first read of the page
Recorded edits8to this page, all time

The whole hunk

from line 246, 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 246
246246 
247247For 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.
248248 
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;
328 console.log(
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
330 );
331 });
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
695 return {
696 "operation": "addition",
697 "operand_a": a,
698 "operand_b": b,
699 "result": result,
700 "timestamp": datetime.datetime.now().isoformat(),
701 }
702 
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="/",
751 )
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 
925```csharp theme={null}
926using Microsoft.AspNetCore.Authentication.JwtBearer;
927using Microsoft.IdentityModel.Tokens;
928using ModelContextProtocol.AspNetCore.Authentication;
929using ProtectedMcpServer.Tools;
930using System.Security.Claims;
931 
932var builder = WebApplication.CreateBuilder(args);
933 
934var serverUrl = "http://localhost:3000/";
935var authorizationServerUrl = "http://localhost:8080/realms/master/";
936 
937builder.Services.AddAuthentication(options =>
938{
939 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
940 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
941})
942.AddJwtBearer(options =>
943{
944 options.Authority = authorizationServerUrl;
945 var normalizedServerAudience = serverUrl.TrimEnd('/');
946 options.TokenValidationParameters = new TokenValidationParameters
947 {
948 ValidIssuer = authorizationServerUrl,
949 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
950 AudienceValidator = (audiences, securityToken, validationParameters) =>
951 {
952 if (audiences == null) return false;
953 foreach (var aud in audiences)
954 {
955 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
956 {
957 return true;
958 }
959 }
960 return false;
961 }
962 };
963 
964 options.RequireHttpsMetadata = false; // Set to true in production
965 
966 options.Events = new JwtBearerEvents
967 {
968 OnTokenValidated = context =>
969 {
970 var name = context.Principal?.Identity?.Name ?? "unknown";
971 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
972 Console.WriteLine($"Token validated for: {name} ({email})");
973 return Task.CompletedTask;
974 },
975 OnAuthenticationFailed = context =>
976 {
977 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
978 return Task.CompletedTask;
979 },
980 };
981})
982.AddMcp(options =>
983{
984 options.ResourceMetadata = new()
985 {
986 Resource = new Uri(serverUrl),
987 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
988 AuthorizationServers = { new Uri(authorizationServerUrl) },
989 ScopesSupported = ["mcp:tools"]
990 };
991});
992 
993builder.Services.AddAuthorization();
994 
995builder.Services.AddHttpContextAccessor();
996builder.Services.AddMcpServer()
997 .WithTools<MathTools>()
998 .WithHttpTransport();
999 
1000var app = builder.Build();
1001 
1002app.UseAuthentication();
1003app.UseAuthorization();
1004 
1005app.MapMcp().RequireAuthorization();
1006 
1007Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1008Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1009Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1010Console.WriteLine("Exposed Math tools: Add, Multiply");
1011Console.WriteLine("Press Ctrl+C to stop the server");
1012 
1013app.Run(serverUrl);
1014```
1015 
1016For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
249> [tab] TypeScript
250>
251> You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252>
253> Prior 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
257> HOST=localhost
258> PORT=3000
259>
260> # Auth server location
261> AUTH_HOST=localhost
262> AUTH_PORT=8080
263> AUTH_REALM=master
264>
265> # Keycloak OAuth client credentials
266> OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267> OAUTH_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>
272> 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.
273>
274> ```typescript theme={null}
275> import "dotenv/config";
276> import express from "express";
277> import { randomUUID } from "node:crypto";
278> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279> import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280> import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281> import { z } from "zod";
282> import cors from "cors";
283> import {
284> mcpAuthMetadataRouter,
285> getOAuthProtectedResourceMetadataUrl,
286> } from "@modelcontextprotocol/sdk/server/auth/router.js";
287> import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288> import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289> import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290> const 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>
302> function 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>
323> function createRequestLogger() {
324> return (req: any, res: any, next: any) => {
325> const start = Date.now();
326> res.on("finish", () => {
327> const ms = Date.now() - start;
328> console.log(
329> `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
330> );
331> });
332> next();
333> };
334> }
335>
336> const app = express();
337>
338> app.use(
339> express.json({
340> verify: (req: any, _res, buf) => {
341> req.rawBody = buf?.toString() ?? "";
342> },
343> }),
344> );
345>
346> app.use(
347> cors({
348> origin: "*",
349> exposedHeaders: ["Mcp-Session-Id"],
350> }),
351> );
352>
353> app.use(createRequestLogger());
354>
355> const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356> const oauthUrls = createOAuthUrls();
357>
358> const oauthMetadata: OAuthMetadata = {
359> ...oauthUrls,
360> response_types_supported: ["code"],
361> };
362>
363> const 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> };
455> app.use(
456> mcpAuthMetadataRouter({
457> oauthMetadata,
458> resourceServerUrl: mcpServerUrl,
459> scopesSupported: ["mcp:tools"],
460> resourceName: "MCP Demo Server",
461> }),
462> );
463>
464> const authMiddleware = requireBearerAuth({
465> verifier: tokenVerifier,
466> requiredScopes: [],
467> resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
468> });
469>
470> const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
471>
472> function 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>
511> const 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>
548> const 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>
562> app.post("/", authMiddleware, mcpPostHandler);
563> app.get("/", authMiddleware, handleSessionRequest);
564> app.delete("/", authMiddleware, handleSessionRequest);
565>
566> app.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>
575> When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
576>
577> For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
578 
579> [tab] Python
580>
581> You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
582>
583> 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.
584>
585> 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:
586>
587> ```python theme={null}
588> """Configuration settings for the MCP auth server."""
589>
590> import os
591>
592>
593> class 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
624> config = 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>
629> The server implementation is as follows:
630>
631> ```python theme={null}
632> import datetime
633> import logging
634> from typing import Any
635> from urllib.parse import urljoin
636>
637> from pydantic import AnyHttpUrl
638>
639> from mcp.server import MCPServer
640> from mcp.server.auth.settings import AuthSettings
641>
642> from .config import config
643> from .token_verifier import IntrospectionTokenVerifier
644>
645> logger = logging.getLogger(__name__)
646>
647>
648> def 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>
660> def 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
695> return {
696> "operation": "addition",
697> "operand_a": a,
698> "operand_b": b,
699> "result": result,
700> "timestamp": datetime.datetime.now().isoformat(),
701> }
702>
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>
725> def 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="/",
751> )
752> return 0
753>
754> except Exception:
755> logger.exception("Server error")
756> return 1
757>
758>
759> if __name__ == "__main__":
760> exit(main())
761> ```
762>
763> 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.
764>
765> ```python theme={null}
766> """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
767>
768> import logging
769> from typing import Any
770>
771> import httpx2
772>
773> from mcp.server.auth.provider import AccessToken, TokenVerifier
774> from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
775>
776> logger = logging.getLogger(__name__)
777>
778>
779> class 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>
878> For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
879>
880> **Python MCP Server**
881>
882> 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:
883>
884> ```toml theme={null}
885> [project]
886> name = "mcp-simple-auth"
887> version = "0.1.0"
888> description = "A simple MCP server demonstrating OAuth authentication"
889> requires-python = ">=3.10"
890> authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
891> license = { text = "MIT" }
892> dependencies = [
893> "httpx2>=2.5.0",
894> "mcp>=2.0.0rc1",
895> "pydantic>=2.0",
896> ]
897>
898> [project.scripts]
899> mcp-simple-auth-rs = "mcp_server.server:main"
900>
901> [build-system]
902> requires = ["hatchling"]
903> build-backend = "hatchling.build"
904>
905> [tool.hatch.build.targets.wheel]
906> packages = ["mcp_server"]
907>
908> [dependency-groups]
909> dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
910> ```
911>
912> Then run the commands below to start the server.
913>
914> ```bash theme={null}
915> uv sync
916> uv run mcp-simple-auth-rs
917> ```
918 
919> [tab] C#
920>
921> You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
922>
923> 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.
924>
925> ```csharp theme={null}
926> using Microsoft.AspNetCore.Authentication.JwtBearer;
927> using Microsoft.IdentityModel.Tokens;
928> using ModelContextProtocol.AspNetCore.Authentication;
929> using ProtectedMcpServer.Tools;
930> using System.Security.Claims;
931>
932> var builder = WebApplication.CreateBuilder(args);
933>
934> var serverUrl = "http://localhost:3000/";
935> var authorizationServerUrl = "http://localhost:8080/realms/master/";
936>
937> builder.Services.AddAuthentication(options =>
938> {
939> options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
940> options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
941> })
942> .AddJwtBearer(options =>
943> {
944> options.Authority = authorizationServerUrl;
945> var normalizedServerAudience = serverUrl.TrimEnd('/');
946> options.TokenValidationParameters = new TokenValidationParameters
947> {
948> ValidIssuer = authorizationServerUrl,
949> ValidAudiences = new[] { normalizedServerAudience, serverUrl },
950> AudienceValidator = (audiences, securityToken, validationParameters) =>
951> {
952> if (audiences == null) return false;
953> foreach (var aud in audiences)
954> {
955> if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
956> {
957> return true;
958> }
959> }
960> return false;
961> }
962> };
963>
964> options.RequireHttpsMetadata = false; // Set to true in production
965>
966> options.Events = new JwtBearerEvents
967> {
968> OnTokenValidated = context =>
969> {
970> var name = context.Principal?.Identity?.Name ?? "unknown";
971> var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
972> Console.WriteLine($"Token validated for: {name} ({email})");
973> return Task.CompletedTask;
974> },
975> OnAuthenticationFailed = context =>
976> {
977> Console.WriteLine($"Authentication failed: {context.Exception.Message}");
978> return Task.CompletedTask;
979> },
980> };
981> })
982> .AddMcp(options =>
983> {
984> options.ResourceMetadata = new()
985> {
986> Resource = new Uri(serverUrl),
987> ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
988> AuthorizationServers = { new Uri(authorizationServerUrl) },
989> ScopesSupported = ["mcp:tools"]
990> };
991> });
992>
993> builder.Services.AddAuthorization();
994>
995> builder.Services.AddHttpContextAccessor();
996> builder.Services.AddMcpServer()
997> .WithTools<MathTools>()
998> .WithHttpTransport();
999>
1000> var app = builder.Build();
1001>
1002> app.UseAuthentication();
1003> app.UseAuthorization();
1004>
1005> app.MapMcp().RequireAuthorization();
1006>
1007> Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1008> Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1009> Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1010> Console.WriteLine("Exposed Math tools: Add, Multiply");
1011> Console.WriteLine("Press Ctrl+C to stop the server");
1012>
1013> app.Run(serverUrl);
1014> ```
1015>
1016> For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
10171017 
10181018## Testing the MCP Server
10191019 
10201020 
Feedback