A secure Spring AI Model Context Protocol (MCP) server implementation using HashiCorp Vault for API key management and AppRole authentication.
This project demonstrates how to secure a Spring AI MCP server using HashiCorp Vault for API key management. The API key is stored securely in Vault and retrieved at runtime using AppRole authentication.
Note: This project uses HashiCorp Vault for centralized secret management, as an alternative to the Spring Security approach described in the DZone article on securing Spring AI MCP servers. While the DZone article demonstrates API key authentication using Spring Security with the mcp-server-security module, this project uses Vault for secret storage and management, providing centralized secret management, audit logging, and version control capabilities.
Centralized Secret Management:
- Single source of truth for secrets across multiple services and environments
- Eliminates secret sprawl: Secrets scattered across code, config files, and multiple stores are managed in one place
- Consistent security policies: Uniform rules for rotation schedules, least-privilege access, and encryption standards across all applications
API Key Management Comparison:
| Aspect | With Vault | Without Vault |
|---|---|---|
| Storage | Centralized, encrypted storage | Code, config files, environment variables (plaintext or simple encryption) |
| Access Control | Fine-grained policy-based access control | File permissions or environment variable management |
| Audit Logging | Automatic logging of all secret access | Manual logging or no logging |
| Version Control | Secret versioning and rollback support | Difficult version management |
| Secret Rotation | Automated rotation support (when implemented) | Manual rotation, application restart required |
| Multi-Environment | Automatic management of different secrets per environment | Manual management via environment variables or config files |
| Compliance | Audit trails and compliance requirement support | Additional implementation required |
Advantages Over Spring Security:
While Spring Security excels at application-level authentication and authorization, Vault provides the following advantages for secret management:
- Centralized Management: Unified secret management across multiple services and environments
- Dynamic Secrets: Support for short-lived secrets that are generated on-demand and automatically expire
- Automated Rotation: Automatic secret rotation for enhanced security
- Audit Logging: Detailed audit trails for all secret access
- Compliance: Support for regulatory requirements such as PCI-DSS and HIPAA
- Encryption-as-a-Service: Encryption/decryption services via Transit Secret Engine
- Multiple Authentication Methods: Support for various authentication methods including AppRole, Kubernetes, and AWS IAM
Use Cases:
- Microservices architecture requiring secret sharing across multiple services
- Regulated industries requiring audit trails and compliance
- Cloud/multi-cloud environments requiring consistent secret management
- Environments requiring automatic secret rotation
- Organizations needing centralized security policy enforcement
Considerations:
- Vault requires separate infrastructure operations and additional resources for high-availability setups
- Network latency and dependency on Vault availability
- Initial setup and learning curve required
This project demonstrates a basic implementation of API key management using Vault, and the advantages listed above can be leveraged in production environments.
MCP Inspector/Client
│
│ HTTP Request (X-API-KEY header)
▼
Spring Boot Application
│
├── API Key Interceptor (validates X-API-KEY header)
│
└── MCP Server Endpoints (Spring AI MCP)
│
│ Vault API calls
▼
HashiCorp Vault (Docker)
└── API Key Storage (secret/mcp, key: api-key)
- Docker and Docker Compose
- Java 21 or higher
- Maven 3.6 or higher
Run the following command:
./setup.shThis script automatically:
- Starts Vault container (skips if already running)
- Waits for Vault to be ready
- Enables KV secrets engine
- Enables AppRole authentication
- Creates AppRole for Spring Boot application
- Stores API key in Vault
- Automatically generates
.envfile andinspector-config.json(with Role ID, Secret ID, and API Key) - Builds the Spring Boot application
- Starts the application and MCP Inspector
- Waits for all services to be ready
The application will start on http://localhost:8080 and MCP Inspector will start on http://localhost:6274
Fixed Demo Credentials:
- Role ID:
demo-role-id-12345 - Secret ID:
demo-secret-id-67890 - API Key: Generated automatically (check
.envorinspector-config.json)
Access URLs:
- Spring Boot Application:
http://localhost:8080 - MCP Inspector UI:
http://localhost:6274
To clean up all containers, volumes, and generated files:
./clean.shThis script:
- Stops and removes all Docker containers and volumes
- Removes generated files (
.env,inspector-config.json) - Cleans build artifacts (
target/)
npm install -g @modelcontextprotocol/inspectorWhen using Docker Compose, MCP Inspector starts automatically:
docker-compose upThe MCP Inspector UI is accessible at http://localhost:6274.
Note: When using Docker Compose, the inspector-config.json file is generated but not automatically loaded to avoid proxy mode header issues. You need to manually connect in the UI. See the Troubleshooting section for details.
The inspector-config.json file is automatically generated during Vault initialization. It contains the API key from Vault and is ready to use:
mcp-inspector --config inspector-config.jsonIf you need to create the configuration manually, check the API key from the .env file:
source .envThen create inspector-config.json:
{
"mcpServers": {
"spring-mcp": {
"url": "http://localhost:8080",
"headers": {
"X-API-KEY": "${MCP_API_KEY}"
}
}
}
}Alternatively, you can copy the API key directly from the .env file.
mcp-inspector --config inspector-config.jsonThe server provides the following MCP tools:
get-server-info: Get information about the MCP serverecho: Echo back the provided message
And resources:
info://server: Server information resource
Once you have MCP Inspector running (either via Docker Compose or locally), follow these steps to verify the Spring Boot MCP Server:
Open your browser and navigate to:
- Docker Compose:
http://localhost:6274 - Local: The URL shown in the terminal after running
mcp-inspector
In the MCP Inspector UI, configure the connection in the left sidebar:
- Transport Type: Select "Streamable HTTP" from the dropdown
- URL: Enter
http://app:8080/mcp(for Docker Compose) orhttp://localhost:8080/mcp(for local)- Note: The endpoint is
/mcpfor STREAMABLE protocol
- Note: The endpoint is
- Connection Type: Select "Via Proxy" (or automatically set)
- Authentication: Click to expand the Authentication section
- Header Name: Enter
X-API-KEY - Header Value: Enter the API key from your
.envfile orinspector-config.json- You can find it by running:
grep MCP_API_KEY .env | cut -d= -f2 - Or check the
inspector-config.jsonfile
- You can find it by running:
- Header Name: Enter
- Click the Connect button
- After clicking Connect, check the connection status
- If connection is successful, you should see server information
- If you see "Invalid Authorization Header" or 401 errors:
- Verify the API key is correct in the Authentication section
- Ensure the header name is exactly
X-API-KEY(case-sensitive) - Check that the API key matches the one in Vault
Once connected successfully, you can see multiple tabs in the MCP Inspector main screen:
Tools Tab:
- Click the "Tools" tab in the top tab menu (or use keyboard shortcut
t) - You should see a list of available tools:
get-server-info: Tool to get server informationecho: Tool to echo back a message
How to Execute Tools:
- Click on a tool from the tools list (e.g.,
get-server-info) - The tool's schema and description will be displayed
- If needed, enter parameters in the input fields:
get-server-info: No parameters requiredecho: Enter a test message in themessagefield (e.g.,"Hello MCP!")
- Click the Execute or Run button
- Results will be displayed in JSON format in the bottom or right panel
Expected Results:
get-server-infoexecution: Returns server name, version, status informationechoexecution: Returns the input message with "Echo: " prefix
Resources Tab:
- Click the "Resources" tab in the top tab menu (or use keyboard shortcut
r) - You should see a list of available resources:
info://server: Server information resource
How to Use Resources:
- Click on
info://serverfrom the resources list - Review the resource metadata (MIME type, description, etc.)
- Click the Read or Load button to fetch the resource content
- Verify the resource content in the results panel
- Look for a Logs, Tracing, Debugging, or JSON-RPC tab/section
- Check for any connection errors or authentication issues
- Verify that requests are being made with the
X-API-KEYheader - If you see
401 Unauthorizederrors:- Verify the API key in
inspector-config.jsonmatches the one in Vault - Check that the header is being sent correctly
- Verify the API key in
To confirm authentication is working:
- Check the logs/debugging section for successful requests
- All tool calls and resource fetches should complete without 401 errors
- If authentication fails, you'll see
401 Unauthorizedin the logs
When everything is working correctly:
- Server appears in the Servers/Connections list and shows as connected
- Tools tab shows two tools:
get-server-infoandecho - Resources tab shows one resource:
info://server - Tools execute successfully and return expected JSON responses
- Resources can be fetched and display the expected content
- No authentication errors (401) in the logs
- All requests complete successfully
All MCP endpoints (/mcp/**) require the X-API-KEY header. The API key is:
- Stored in HashiCorp Vault at
secret/mcpwith keyapi-key - Retrieved by Spring Boot using AppRole authentication
- Validated by the
ApiKeyInterceptoron each request
Vault Secret Path:
- Path:
secret/mcp - Key:
api-key - Read command:
vault kv get -field=api-key secret/mcp - Via Docker:
docker-compose exec vault vault kv get -field=api-key secret/mcp
The following sequence diagram illustrates the complete authentication flow:
sequenceDiagram
participant MCPInspector as MCP Inspector
participant SpringApp as Spring Boot App
participant ApiKeyInterceptor as ApiKeyInterceptor
participant ApiKeyProps as ApiKeyProperties
participant VaultClient as Spring Cloud Vault
participant Vault as HashiCorp Vault
MCPInspector->>SpringApp: HTTP Request<br/>Header: X-API-KEY
SpringApp->>ApiKeyInterceptor: Intercept Request
ApiKeyInterceptor->>ApiKeyInterceptor: Extract X-API-KEY header
ApiKeyInterceptor->>ApiKeyProps: Get valid API key
ApiKeyProps->>VaultClient: (Already loaded at startup)
VaultClient->>Vault: AppRole Login<br/>role-id + secret-id
Vault-->>VaultClient: Vault Token
VaultClient->>Vault: Read secret/mcp/api-key
Vault-->>VaultClient: API Key Value
VaultClient-->>ApiKeyProps: api-key.value
ApiKeyProps-->>ApiKeyInterceptor: Valid API Key
ApiKeyInterceptor->>ApiKeyInterceptor: Compare keys
alt Keys Match
ApiKeyInterceptor->>SpringApp: Allow Request
SpringApp->>MCPInspector: MCP Response
else Keys Mismatch
ApiKeyInterceptor->>MCPInspector: 401 Unauthorized
end
graph TB
subgraph Client["Client Layer"]
MCPInspector[MCP Inspector]
end
subgraph SpringBoot["Spring Boot Application"]
Interceptor[ApiKeyInterceptor<br/>X-API-KEY validation]
Props[ApiKeyProperties<br/>api-key.value from Vault]
MCPEndpoints[MCP Server Endpoints<br/>/sse, /mcp/message]
end
subgraph VaultIntegration["Vault Integration"]
VaultConfig[Spring Cloud Vault Config<br/>AppRole Authentication]
VaultClient[Vault Client<br/>role-id + secret-id]
end
subgraph VaultServer["HashiCorp Vault"]
AppRole[AppRole: spring-boot-app<br/>role-id: demo-role-id-12345]
Secrets["KV Secrets Engine<br/>secret/mcp<br/>key: api-key"]
end
MCPInspector -->|"1. HTTP Request<br/>X-API-KEY header"| Interceptor
Interceptor -->|"2. Get valid key"| Props
Props -->|"3. Value from Vault<br/>(loaded at startup)"| VaultConfig
VaultConfig -->|"4. AppRole Auth"| VaultClient
VaultClient -->|"5. Login"| AppRole
AppRole -->|"6. Return Token"| VaultClient
VaultClient -->|"7. Read Secret"| Secrets
Secrets -->|"8. API Key Value"| VaultClient
VaultClient -->|"9. Bind to Properties"| Props
Interceptor -->|"10. Validate"| MCPEndpoints
MCPEndpoints -->|"11. MCP Response"| MCPInspector
File: src/main/java/com/example/mcp/config/ApiKeyProperties.java
How it works:
- Spring Cloud Vault authenticates with Vault using AppRole at application startup
- Uses
VaultVersionedKeyValueOperationsto read fromsecret/mcppath - Extracts the
api-keykey from the secret data - Vault path:
secret/mcp(KV v2 engine) - Key:
api-key - Read command:
vault kv get -field=api-key secret/mcp
File: src/main/java/com/example/mcp/interceptor/ApiKeyInterceptor.java
Validation process:
- Extracts API key from
X-API-KEYheader in HTTP request - Retrieves valid API key from
ApiKeyProperties(loaded from Vault) - Compares the two values - if they match, request proceeds; otherwise returns 401 Unauthorized
Registration: The interceptor is registered in WebMvcConfig for all /mcp/** and /sse paths, excluding /actuator/** and /.
Configuration: src/main/resources/application.yml
spring:
cloud:
vault:
authentication: APPROLE
app-role:
role-id: ${SPRING_CLOUD_VAULT_APP_ROLE_ROLE_ID}
secret-id: ${SPRING_CLOUD_VAULT_APP_ROLE_SECRET_ID}Authentication process:
- At application startup, reads role-id and secret-id from environment variables
- Authenticates with Vault using
/auth/approle/loginendpoint - On successful authentication, receives Vault token
- Uses the token to read secrets with appropriate permissions
Script: scripts/vault-init.sh
Initialization process:
- Enables KV secrets engine
- Enables AppRole authentication method
- Creates AppRole and sets fixed role-id:
demo-role-id-12345 - Creates fixed secret-id using Custom Secret ID API:
demo-secret-id-67890 - Generates API key and stores it in Vault
- Generates
.envfile andinspector-config.jsonwith credentials
Fixed Demo Credentials:
- Role ID:
demo-role-id-12345(fixed) - Secret ID:
demo-secret-id-67890(fixed, created via Custom Secret ID API) - API Key: Randomly generated each time (security maintained)
curl -H "X-API-KEY: <your-api-key>" \
http://localhost:8080/mcp/v1/toolsThe application uses Spring Cloud Vault to connect to HashiCorp Vault:
spring:
cloud:
vault:
uri: http://vault:8200
authentication: APPROLE
app-role:
role-id: ${SPRING_CLOUD_VAULT_APP_ROLE_ROLE_ID}
secret-id: ${SPRING_CLOUD_VAULT_APP_ROLE_SECRET_ID}
kv:
enabled: true
backend: secret
default-context: mcpspring:
ai:
mcp:
server:
name: mcp-server
version: 1.0.0
type: SYNC
protocol: STREAMABLE
capabilities:
tool: true
resource: trueThis section explains the key code components related to API key management using Vault.
File: src/main/java/com/example/mcp/config/ApiKeyProperties.java
This class loads the API key from Vault at application startup and stores it in memory.
@PostConstruct
public void init() {
VaultVersionedKeyValueOperations kvOps = vaultOperations.opsForVersionedKeyValue("secret");
Versioned<Map<String, Object>> secret = kvOps.get("mcp");
if (secret != null && secret.getData() != null) {
Object apiKeyValue = secret.getData().get("api-key");
if (apiKeyValue != null) {
this.apiKey = apiKeyValue.toString();
}
}
}Key Points:
@PostConstruct: Executes at Spring Bean initialization time, reading the API key from Vault only onceVaultVersionedKeyValueOperations: Uses KV v2 engine to read versioned secrets from Vault- Vault Path:
secret/mcp(for KV v2 engine, the actual path issecret/data/mcp) - Key:
api-key- The key name within the secret data - Reading Method: Reads the entire secret with
kvOps.get("mcp")and then extracts theapi-keykey
Limitations:
- The current implementation reads only once at startup, so application restart is required if the API key is changed in Vault
- For dynamic updates,
@RefreshScopeand Spring Cloud Vault's refresh functionality can be utilized
File: src/main/java/com/example/mcp/interceptor/ApiKeyInterceptor.java
This interceptor validates the API key for all MCP endpoint requests.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String providedApiKey = request.getHeader(API_KEY_HEADER);
String validApiKey = apiKeyProperties.getValue();
if (validApiKey == null || !validApiKey.equals(providedApiKey)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}Key Points:
- Extracts the API key provided by the client from the
X-API-KEYheader - Retrieves the valid API key loaded from Vault via
ApiKeyProperties - Compares the two values and returns
401 Unauthorizedif they don't match - Implements simple API key authentication without Spring Security
File: src/main/resources/application.yml
spring:
cloud:
vault:
uri: ${SPRING_CLOUD_VAULT_URI:http://localhost:8200}
authentication: ${SPRING_CLOUD_VAULT_AUTHENTICATION:APPROLE}
app-role:
role-id: ${SPRING_CLOUD_VAULT_APP_ROLE_ROLE_ID:}
secret-id: ${SPRING_CLOUD_VAULT_APP_ROLE_SECRET_ID:}
kv:
enabled: true
backend: secret
default-context: mcp
profile-separator: "/"
application-name: mcp
backend-version: 2Configuration Explanation:
authentication: APPROLE: Uses AppRole authentication methodapp-role.role-id/app-role.secret-id: Reads AppRole credentials from environment variableskv.enabled: true: Enables KV Secret Enginekv.backend: secret: Mount path of the Secret Enginekv.application-name: mcp: Application name (used for secret path construction)kv.backend-version: 2: Uses KV v2 engine
Secret Path Construction:
- Since
application-nameismcp,ApiKeyPropertiesreads secrets from thesecret/mcppath - For KV v2 engine, the actual Vault path is
secret/data/mcp, but Spring Cloud Vault handles this automatically
File: src/main/java/com/example/mcp/config/WebMvcConfig.java
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiKeyInterceptor)
.addPathPatterns("/mcp/**", "/sse")
.excludePathPatterns("/actuator/**", "/");
}Key Points:
- Applies API key validation to
/mcp/**and/ssepaths - Excludes
/actuator/**and/paths so health check and info endpoints are accessible without authentication
File: scripts/vault-init.sh
# Store API key in secret/mcp (application-name path)
# ApiKeyProperties reads from secret/mcp with key "api-key"
vault kv put secret/mcp api-key="$API_KEY" 2>&1 || trueKey Points:
vault kv put secret/mcp api-key="$API_KEY": Uses KV v2 engine to store the API key atsecret/mcppath with theapi-keykey- This path and key must match what
ApiKeyPropertiesreads - Uses a path that matches the
application-name: mcpconfiguration
Vault Secret Structure:
secret/
└── data/
└── mcp/
└── api-key: "<generated-api-key>"
-
Vault Initialization (
vault-init.sh):- Creates AppRole and sets fixed Role ID/Secret ID
- Generates API key and stores it in
secret/mcpwith keyapi-key
-
Application Startup:
- Spring Cloud Vault reads Role ID/Secret ID from environment variables and authenticates with Vault
ApiKeyPropertiesreads theapi-keykey fromsecret/mcppath in@PostConstructand stores it in memory
-
Request Processing:
- Client sends request with
X-API-KEYheader ApiKeyInterceptorcompares the API key from the header with the valid API key fromApiKeyProperties- If they match, the request is processed; otherwise, 401 is returned
- Client sends request with
spring-ai-mcp-server-with-an-api-key-via-vault/
├── docker-compose.yml
├── Dockerfile
├── pom.xml
├── scripts/
│ └── vault-init.sh
├── src/
│ └── main/
│ ├── java/com/example/mcp/
│ │ ├── McpApplication.java
│ │ ├── config/
│ │ │ ├── VaultConfig.java
│ │ │ ├── ApiKeyProperties.java
│ │ │ └── WebMvcConfig.java
│ │ ├── interceptor/
│ │ │ └── ApiKeyInterceptor.java
│ │ └── service/
│ │ └── McpService.java
│ └── resources/
│ └── application.yml
└── README.md
This project uses the following approach for API key authentication:
- Uses custom
ApiKeyInterceptorfor authentication - No Spring Security dependency required
- API key stored in HashiCorp Vault (centralized secret management)
- API key retrieved via Spring Cloud Vault with AppRole authentication
- API key format:
X-API-KEY: <secret>(simple secret value) - Plain text comparison (no bcrypt hashing in current implementation)
- Vault provides centralized secret management, audit logging, and version control
Alternative Approaches:
- Spring Security with
SecurityFilterChainandmcp-server-securitymodule ApiKeyEntityRepositoryfor in-memory or database-backed key storage- bcrypt hashing for API key secrets
- API key format:
X-API-key: <id>.<secret>(ID and secret combined)
- API keys are stored only in Vault (never hardcoded)
- AppRole authentication for machine-to-machine authentication
- All MCP endpoints are protected by the API key interceptor
- Vault AppRole credentials are managed via environment variables
- For production, configure Vault properly (not dev mode)
- AppRole policies enforce least privilege principle
Important Note: The current implementation reads the API key from Vault only once at application startup (in @PostConstruct). This means:
- API key changes in Vault require application restart to take effect
- The current implementation doesn't fully leverage Vault's dynamic secret management capabilities
- In this demo, Vault serves primarily as a secure storage mechanism rather than a dynamic secret management system
Vault provides value in production environments:
- Centralized Secret Management: Single source of truth for secrets across multiple services and environments
- Audit Logging: All secret access is logged, providing compliance and security audit trails
- Version Control: KV v2 engine supports secret versioning and rollback capabilities
- Access Control: Fine-grained policies control who can access which secrets
- Secret Rotation: While not implemented in this demo, Vault supports dynamic secret rotation without application restarts
- Multi-Environment Support: Different secrets for dev, staging, and production environments
Future Improvements (not implemented in this demo):
- Use
@RefreshScopewith Spring Cloud Vault to enable dynamic secret updates via/actuator/refreshendpoint - Implement periodic secret refresh to pick up changes without restart
- Leverage Vault's dynamic secrets (e.g., database credentials) that rotate automatically
- Use Vault's secret leasing for time-bound access
If you encounter "Connection Error - Check if your MCP server is running and proxy token is correct" when using MCP Inspector:
-
Verify MCP Server is Running:
curl http://localhost:8080/
You should see a JSON response with server information.
-
Check API Key:
grep MCP_API_KEY .env | cut -d= -f2Or check the
inspector-config.jsonfile. -
Manual Connection in MCP Inspector UI (Recommended):
The
inspector-config.jsonfile is not automatically loaded in Docker Compose setup to avoid proxy mode header issues. Use manual connection instead:- Open
http://localhost:6274in your browser - In the left sidebar, manually configure the connection:
- Transport Type: "Streamable HTTP"
- URL:
http://app:8080/mcp(correct for Docker Compose setup) - Connection Type: "Direct" or "Via Proxy" (both should work)
- Authentication: Click to expand the Authentication section
- Header Name:
X-API-KEY - Header Value: Your API key from
.envfilegrep MCP_API_KEY .env | cut -d= -f2
- Header Name:
- Click Connect
- Open
-
Verify Docker Containers:
docker-compose ps
Ensure all containers are running and healthy:
vault- Vault server (should be healthy)spring-mcp-app- Spring Boot application (should be healthy)mcp-inspector- MCP Inspector
-
Check API Key Loading from Vault:
docker-compose logs app | grep -i "api.*key"
You should see:
API key loaded successfully from Vault. Length: 43If you see errors, check Vault policy:
docker-compose exec vault vault policy read mcp-read-policy
The policy should include both
secret/data/mcpandsecret/data/mcp/*paths. -
Check Container Logs:
docker-compose logs app docker-compose logs mcp-inspector
Look for any errors or connection issues.
-
Restart Services:
docker-compose restart app mcp-inspector
Note: The inspector-config.json file is generated but not automatically loaded in Docker Compose to avoid proxy mode header forwarding issues. Always use manual connection in the MCP Inspector UI with the API key from the .env file.