You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A Model Context Protocol (MCP) server built with Spring AI and the Okta Java SDK that exposes tools for AI assistants (GitHub Copilot, Claude, etc.) to manage Okta users, groups, and applications programmatically.
Architecture
MCP Client (GitHub Copilot / Claude / AI Agent)
│ stdin / stdout (JSON-RPC over stdio)
▼
┌──────────────────────────────────────┐
│ Spring Boot MCP Server (stdio) │
│ ┌────────────────────────────────┐ │
│ │ Spring AI MCP │ │
│ │ 15+ @Tool methods │ │
│ └───────────────┬────────────────┘ │
│ │ │
│ OktaClientProvider (lazy init) │
└──────────────────┼───────────────────┘
│ Okta Java SDK v25 (SSWS / Private Key JWT)
▼
Okta Management API
The server runs as a stdio process — the MCP client spawns it and communicates via JSON-RPC over stdin/stdout. All logging goes to stderr so stdout stays clean for the MCP protocol.
Prerequisites
Tool
Version
Java
21+
Maven
3.9+ (or use the included ./mvnw)
Okta org
Developer or production
1 — Okta Setup
Option A — API Token (simplest for development)
In the Okta Admin Console → Security → API → Tokens
Click Create Token, copy the value → this is OKTA_CLIENT_TOKEN
Option B — OAuth2 Service App (recommended for production)
All credentials are passed via environment variables — never commit secrets to source control.
For production, prefer Option B (OAuth2 Private Key JWT) over API tokens.
The SSWS token option is labeled as a dev fallback in the server logs.
Project Structure
okta-mcp-poc/
├── pom.xml
├── scripts/
│ └── run-mcp.sh # Wrapper script used by VS Code MCP client
└── src/main/
├── java/com/okta/mcp/
│ ├── OktaMcpApplication.java # Spring Boot entry point
│ ├── config/
│ │ ├── McpTransportConfig.java # Stdio transport with lenient JSON parsing
│ │ ├── OktaClientProvider.java # Lazy Okta SDK client initialization
│ │ └── ToolConfig.java # Registers all @Tool methods with Spring AI MCP
│ └── tools/
│ ├── OktaUserManagementTool.java # User CRUD tools
│ ├── OktaGroupsTool.java # Group management tools
│ └── OktaApplicationsTool.java # Application query tools
└── resources/
├── application.properties # Spring Boot & Okta SDK config
└── logback-spring.xml # Routes all logs to stderr
POC Findings
What worked well
Spring AI MCP + stdio transport is a clean fit for a local MCP server. No HTTP server, no port conflicts, no TLS — the client spawns the process and talks JSON-RPC over stdin/stdout.
@Tool + MethodToolCallbackProvider made wiring Okta SDK calls into MCP tools straightforward — no boilerplate beyond the annotation.
Okta SDK v25 covers all required user, group, and application APIs under separate typed API classes (UserApi, GroupApi, ApplicationApi, etc.).
Dual auth modes (SSWS token for dev, Private Key JWT for production) work with the same codebase by inspecting the presence of OKTA_PRIVATE_KEY / OKTA_KEY_ID at client-build time.
Challenges & workarounds
Problem
Root cause
Fix
MCP handshake fails with -32603 on VS Code
VS Code sends an "elicitation" field in ClientCapabilities not yet modelled by MCP SDK 0.7.0 — Jackson throws on unknown fields
McpTransportConfig supplies a custom ObjectMapper with FAIL_ON_UNKNOWN_PROPERTIES disabled
Spring context crashes before MCP handshake
Okta SDK makes OIDC-discovery HTTP calls when ApiClient is constructed at startup
OktaClientProvider defers ApiClient construction to the first actual tool call (lazy double-checked locking)
stdout corruption
Spring Boot banner and logs go to stdout by default, corrupting the JSON-RPC stream
spring.main.banner-mode=off + logback-spring.xml routes all logging to stderr
SDK v25 API surface changes
UserCredentials replaced by UserCredentialsWritable; deactivate moved to UserLifecycleApi; etc.
Addressed per method with inline SDK-version comments
Limitations of this POC
No persistent sessions: each tool call re-uses the lazily cached ApiClient, but the process exits when the MCP client disconnects.
Pagination is manual: callers must pass the after cursor from one call to the next.
No write-back of private-key JWTs to a secrets store — credentials are environment variables only.