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
/
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
No line in this hunk matches that.