3‐Configuration - makbn/mcp_mediator GitHub Wiki
Configuration
The MCP Mediator provides robust configuration capabilities that give you complete control over observability and error handling. You can configure these natively via the McpMediatorConfigurationBuilder.
Observability & Tracing (OpenTelemetry)
The MCP Mediator is fully instrumented with OpenTelemetry, enabling you to trace the lifecycle of MCP tool calls from request to response, allowing easy integration with Datadog, Jaeger, Zipkin, or any OpenTelemetry-compatible backend.
Enabling Tracing
By default, the mediator uses OpenTelemetry.noop() to guarantee zero performance overhead unless requested. To enable tracing, provide your initialized OpenTelemetry instance when building the configuration.
OpenTelemetry openTelemetry = // ... initialize OpenTelemetry (e.g., GlobalOpenTelemetry.get() or AutoConfiguredOpenTelemetrySdk)
McpMediatorDefaultConfiguration config = McpMediatorConfigurationBuilder.builder()
.createDefault()
.serverName("my_mcp_server")
.openTelemetry(openTelemetry) // Inject OpenTelemetry instance
.build();
DefaultMcpMediator mediator = new DefaultMcpMediator(config);
Datadog Integration Example
To stream your traces directly to Datadog via the OTLP protocol, configure your OpenTelemetry SDK as follows:
OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("https://trace.agent.datadoghq.com")
.addHeader("DD-API-KEY", System.getenv("DD_API_KEY"))
.build();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
.build();
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.buildAndRegisterGlobal();
// Pass to MCP Mediator
McpMediatorDefaultConfiguration config = McpMediatorConfigurationBuilder.builder()
.createDefault()
.openTelemetry(openTelemetry)
.build();
Expected Trace Data
When tracing is enabled, you will observe the following Spans in your telemetry dashboards (like Datadog):
-
mcp_tool_call(Edge Span)- Trigger: When an external MCP Client invokes an MCP tool.
- Attributes:
mcp.tool.name: The exact string identifier of the invoked tool (e.g.,docker_start_container).
- Status: Returns
StatusCode.ERRORnatively if the execution triggers a Java exception, along with exception details attached directly to the span.
-
mcp_handler_execute(Internal Execution Span)- Trigger: When the request is routed and internally invoked against the respective adapter.
- Attributes:
mcp.request.type: The simple class name of the underlying request payload.
- Status: Inherently captures latency and success rates.
-
mcp_tool_execution- Trigger: Wraps dynamic proxy invocations, especially for dynamic
ProxyMcpMediatorrouting. - Attributes: Identifies proxied tool configurations and potential downstream invocation errors.
- Trigger: Wraps dynamic proxy invocations, especially for dynamic
Comprehensive Error Handling
The MCP protocol specifies that servers must not crash on internal errors but must return a standardized error object to the client (isError=true). The MCP Mediator automates this via the McpMediatorExceptionHandler.
Default Behavior
If a tool or service throws an Exception during execution, the internal execution engine traps it. The DefaultMcpMediatorExceptionHandler intercepts the exception, records it to your logs, and returns the exception's message as an MCP Text Content block flagged with isError=true.
Custom Error Handlers
You can intercept and format error messages—or silence specific exceptions completely—by registering your own McpMediatorExceptionHandler.
McpMediatorExceptionHandler customHandler = new McpMediatorExceptionHandler() {
@Override
public String handleException(McpMediatorRequest<?> request, Exception ex) {
if (ex instanceof AccessDeniedException) {
return "You do not have permissions to run this MCP tool.";
}
return "An internal server error occurred: " + ex.getMessage();
}
};
McpMediatorDefaultConfiguration config = McpMediatorConfigurationBuilder.builder()
.createDefault()
.serverName("my_mcp_server")
.exceptionHandler(customHandler)
.build();
This guarantees resilient execution, preventing unhandled runtime exceptions from crashing the server loop and terminating the client connection.
Security & Authorization Interceptors
While traditional HTTP endpoints use headers for token-based authorization (like JWTs or Basic Auth), MCP requests (especially over STDIO) rely purely on JSON payloads. The Mediator provides the McpMediatorInterceptor interface, allowing you to intercept raw incoming requests, extract tokens embedded within the arguments payload, and bridge them into Spring Security seamlessly.
Creating an Interceptor
You can implement an interceptor to extract a token from the incoming JSON request:
import io.github.makbn.mcp.mediator.api.McpMediatorInterceptor;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
public class JwtMcpInterceptor implements McpMediatorInterceptor {
@Override
public void intercept(Object request) throws Exception {
if (request instanceof CallToolRequest callReq) {
// Suppose the client sends the token in a "token" argument
Object tokenObj = callReq.arguments().get("token");
if (tokenObj instanceof String token) {
// Validate your token and create Spring authentication
var auth = new UsernamePasswordAuthenticationToken("user", token, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
throw new SecurityException("Unauthorized: Missing or invalid token");
}
}
}
}
Thread Context Propagation
Because the MCP Mediator runs tools asynchronously in a background ExecutorService, standard ThreadLocal contexts (like SecurityContextHolder) will be lost when crossing thread boundaries unless correctly propagated.
To fix this, you must configure the mediator to use Spring's DelegatingSecurityContextExecutorService, which safely transfers the security context to the background execution threads.
import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService;
import java.util.concurrent.Executors;
// 1. Create a delegated thread pool
ExecutorService delegatedExecutor = new DelegatingSecurityContextExecutorService(
Executors.newCachedThreadPool()
);
// 2. Build configuration with your interceptor and delegated executor
McpMediatorDefaultConfiguration config = McpMediatorConfigurationBuilder.builder()
.createDefault()
.serverName("secure_mcp_server")
.addInterceptor(new JwtMcpInterceptor())
.executorService(delegatedExecutor) // Crucial for Spring Security ThreadLocal propagation!
.build();
DefaultMcpMediator mediator = new DefaultMcpMediator(config);
By supplying both the interceptor and the executor service, you emulate OncePerRequestFilter-style authorization entirely natively within the MCP STDIO environment!