# mcp-secure The [mcp-server](../mcp-server) article's order-lookup tools, behind a real OAuth2 resource server: JWT validation, one scope per tool, unauthenticated tool discovery rejected outright, and every tool call audit-logged through MDC -- including denied calls, not only successful ones. Companion code for [Securing an MCP Server with Spring Security 7](https://ankurm.com/spring-ai-2-0-mcp-server-security/) on [ankurm.com](https://ankurm.com). ## Versions | Component | Version | |---|---| | Spring Boot | 4.1.1 | | Spring AI | 2.0.1 | | Spring Security | 7.1.1 (managed by the Boot 4.1.1 parent) | | Java | 25 (LTS) | | nimbus-jose-jwt | 10.9.1 (pulled in transitively by `spring-security-oauth2-jose`) | ## Quickstart ```bash ./scripts/run-all.sh ``` Runs the full test suite against a real, running Spring Boot application on a random port, over the real Streamable HTTP MCP transport, with real signed JWTs -- no mocks, no external Authorization Server. Output lands in `output/`. ## What's here | File | What it does | |---|---| | [`security/DemoJwtIssuer.java`](src/main/java/com/ankurm/mcpsecure/security/DemoJwtIssuer.java) | Generates one RSA keypair per JVM and mints real signed JWTs against it -- stands in for a real Authorization Server so this module has no external process to run. | | [`security/McpSecurityConfig.java`](src/main/java/com/ankurm/mcpsecure/security/McpSecurityConfig.java) | The `SecurityFilterChain` requiring a valid bearer token on every request, and the `JwtDecoder` bean validating against `DemoJwtIssuer`'s public key. | | [`tools/SecureOrderTools.java`](src/main/java/com/ankurm/mcpsecure/tools/SecureOrderTools.java) | `lookup_order` behind `SCOPE_orders:read`, `refund_order` behind `SCOPE_orders:write` -- ordinary `@PreAuthorize` on ordinary `@McpTool` methods. | | [`audit/ToolAuditAspect.java`](src/main/java/com/ankurm/mcpsecure/audit/ToolAuditAspect.java) | Logs every tool call's subject, scopes, tool name and outcome through MDC -- ordered to still catch denied calls, see its Javadoc. | ## Output files | File | What it captures | |---|---| | `output/01-no-token-discovery-rejected.txt` | `initialize()` with no bearer token at all | | `output/02-read-scope-lookup-succeeds.txt` | A read-scoped token calling both tools | | `output/03-write-scope-refund-succeeds.txt` | A write-scoped token calling both tools | | `output/04-both-scopes-both-succeed.txt` | A token with both scopes | | `output/05-audit-log-both-outcomes.txt` | Real MDC contents of one successful and one denied call | ## Requirements Nothing beyond the JDK and Maven -- no external Authorization Server, no Docker. `DemoJwtIssuer` keeps the whole thing self-contained; see its Javadoc for what to swap in for production.