gRPC Bridge Architecture
This document describes the gRPC bridge component that mediates communication between the Wanaku router and remote capability services.
Architecture Overview
The gRPC bridge implements a transport-agnostic architecture using composition:
┌─────────────────────────────────────────────────────────────────┐
│ Bridge Interfaces │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ ToolsBridge │ │ResourceBridge│ │CodeExecutorBridge │ │
│ └──────┬──────┘ └──────┬───────┘ └─────────┬─────────┘ │
│ ┌──────┴──────┐ ┌──────┴──────┐ ┌──────────┴──────────┐ │
│ │ McpBridge │ │ProvisionBridge│ │ │ │
│ └──────┬──────┘ └──────┬───────┘ │ │ │
├──────────┼────────────────┼──────────┼──────────────────────┤ │
│ │ │ │ │ │
│ ┌──────▼──────┐ ┌──────▼───────────┐ ┌─────▼──────────┐ │
│ │InvokerBridge│ │ResourceAcquirer- │ │CodeExecution- │ │
│ │ │ │Bridge │ │Bridge │ │
│ └──────┬──────┘ └──────┬───────────┘ └─────┬──────────┘ │
│ │ ┌───────┤ │ │
│ │ ┌─────▼───────▼──┐ │ │
│ │ │ProvisionerBridge│ │ │
│ │ └────────┬────────┘ │ │
├──────────┼───────────┼──────────────────────────┼───────────────┤
│ Transport Abstraction │
│ WanakuBridgeTransport │
├─────────────────────────────────────────────────────────────────┤
│ gRPC Implementation │
│ ┌─────────────────┐ ┌────────────────────┐ │
│ │ GrpcTransport │──────│ GrpcChannelManager │ │
│ └────────┬────────┘ └────────────────────┘ │
│ │ │
│ ┌────────┴────────────────────────────────┐ │
│ │ GrpcToolResponseTransformer │ │
│ │ GrpcResourceResponseTransformer │ │
│ └─────────────────────────────────────────┘ │
│ │
│ MCP Implementation │
│ ┌──────────────────┐ ┌────────────────┐ │
│ │ DefaultMcpBridge │────│ ForwardRegistry│ │
│ └──────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Key characteristics:
- Protocol independence via transport abstraction
- Async-first operations using Mutiny
Unitypes - Response transformation at the transport layer (bridges never see transport-specific types)
- Service discovery integration for dynamic endpoint resolution
- Event emission for observability
- Streaming support for code execution
- MCP-to-MCP forwarding via
McpBridge - Consolidated provisioning via
ProvisionerBridge - Composition over inheritance
Core Abstractions
Bridge Interface
Package: ai.wanaku.backend.bridge
Base marker interface for all bridge implementations.
public interface Bridge {
default String name() {
return this.getClass().getSimpleName();
}
}WanakuBridgeTransport Interface
Package: ai.wanaku.backend.bridge
Core transport abstraction defining the contract for all transport implementations.
public interface WanakuBridgeTransport {
ProvisioningReference provision(
String name,
String configData,
String secretsData,
ServiceTarget service
);
Uni<ToolResponse> invokeTool(
ToolInvokeRequest request,
ServiceTarget service
);
Uni<List<ResourceContents>> acquireResource(
ResourceRequest request,
ServiceTarget service,
ResourceManager.ResourceArguments arguments,
ResourceReference mcpResource
);
Iterator<CodeExecutionReply> executeCode(
CodeExecutionRequest request,
ServiceTarget service
);
HealthProbeReply probeHealth(
HealthProbeRequest request,
ServiceTarget service
);
}The async methods (invokeTool, acquireResource) return already-transformed domain types (e.g., ToolResponse, List<ResourceContents>) rather than transport-specific protobuf types. This keeps bridges protocol-agnostic.
This abstraction enables:
- Protocol independence (supports gRPC, HTTP, WebSocket)
- Testability (easy mocking)
- Separation of concerns
- Async-first design with Mutiny
Unitypes
Bridge Interfaces
ToolsBridge
Contract for tool proxy implementations.
public interface ToolsBridge extends Bridge {
Uni<ToolResponse> execute(ToolManager.ToolArguments toolArguments, CallableReference toolReference);
}ResourceBridge
Contract for resource proxy implementations.
public interface ResourceBridge extends Bridge {
Uni<ResourceResponse> read(
ResourceManager.ResourceArguments arguments,
ResourceReference mcpResource
);
}ProvisionBridge
Contract for provisioning configuration and secrets.
public interface ProvisionBridge {
ProvisioningReference provision(
String name, String configData, String secretsData, ServiceTarget service
);
}CodeExecutorBridge
Contract for code execution bridge implementations.
public interface CodeExecutorBridge extends Bridge {
Iterator<CodeExecutionReply> executeCode(
String engineType,
String language,
CodeExecutionRequest request
);
}gRPC Transport Layer
GrpcChannelManager
Package: ai.wanaku.backend.bridge.transports.grpc
Manages gRPC channel lifecycle.
class GrpcChannelManager {
private static final Map<ServiceTarget, ManagedChannel> CHANNEL_MAP =
new ConcurrentHashMap<>();
public ManagedChannel createChannel(ServiceTarget service) {
return CHANNEL_MAP.computeIfAbsent(service, s ->
ManagedChannelBuilder
.forTarget(s.toAddress())
.usePlaintext()
.build()
);
}
public void closeChannel() {
// No-op: channels are cached and reused
}
public void shutdown() {
// Closes all cached channels on application shutdown
CHANNEL_MAP.values().forEach(ch -> ch.shutdown());
CHANNEL_MAP.clear();
}
}Characteristics:
- Caches and reuses gRPC channels per service target (reduces connection overhead)
- Creates plaintext gRPC channels
- No-op
closeChannel()since channels are reused shutdown()closes all cached channels on application termination- Design allows for future SSL/TLS support
GrpcTransport
Package: ai.wanaku.backend.bridge.transports.grpc
Central coordinator for all gRPC transport operations.
public class GrpcTransport implements WanakuBridgeTransport {
private final GrpcChannelManager channelManager;
private final ToolResponseTransformer<ToolInvokeReply> toolTransformer =
new GrpcToolResponseTransformer();
private final ResourceResponseTransformer<ResourceReply> resourceTransformer =
new GrpcResourceResponseTransformer();
public GrpcTransport() {
this.channelManager = new GrpcChannelManager();
}
@Override
public ProvisioningReference provision(
String name, String configData, String secretsData,
ServiceTarget service) {
// Build Configuration and Secret protobuf objects
// Call ProvisionerGrpc stub directly
}
@Override
public Uni<ToolResponse> invokeTool(
ToolInvokeRequest request, ServiceTarget service) {
ManagedChannel channel = channelManager.createChannel(service);
ToolInvokerGrpc.ToolInvokerFutureStub stub =
ToolInvokerGrpc.newFutureStub(channel);
return Uni.createFrom().future(stub.invokeTool(request))
.map(toolTransformer::transformReply);
}
@Override
public Uni<List<ResourceContents>> acquireResource(
ResourceRequest request, ServiceTarget service,
ResourceManager.ResourceArguments arguments,
ResourceReference mcpResource) {
ManagedChannel channel = channelManager.createChannel(service);
ResourceAcquirerGrpc.ResourceAcquirerFutureStub stub =
ResourceAcquirerGrpc.newFutureStub(channel);
return Uni.createFrom().future(stub.resourceAcquire(request))
.map(reply -> resourceTransformer.transformReply(reply, arguments, mcpResource));
}
@Override
public Iterator<CodeExecutionReply> executeCode(
CodeExecutionRequest request, ServiceTarget service) {
ManagedChannel channel = channelManager.createChannel(service);
CodeExecutorGrpc.CodeExecutorBlockingStub stub =
CodeExecutorGrpc.newBlockingStub(channel);
return stub.executeCode(request);
}
@Override
public HealthProbeReply probeHealth(
HealthProbeRequest request, ServiceTarget service) {
ManagedChannel channel = channelManager.createChannel(service);
HealthProbeGrpc.HealthProbeBlockingStub stub =
HealthProbeGrpc.newBlockingStub(channel);
return stub.probe(request);
}
}Response Transformers
Package: ai.wanaku.backend.bridge and ai.wanaku.backend.bridge.transports.grpc
Response transformers convert transport-specific types into MCP domain types within the transport layer, ensuring bridges never handle protocol-specific types.
// Generic interface for tool response transformation
public interface ToolResponseTransformer<T> {
ToolResponse transformReply(T reply);
}
// Generic interface for resource response transformation
public interface ResourceResponseTransformer<T> {
List<ResourceContents> transformReply(
T reply,
ResourceManager.ResourceArguments arguments,
ResourceReference mcpResource
);
}
// gRPC-specific implementations
class GrpcToolResponseTransformer implements ToolResponseTransformer<ToolInvokeReply> {
@Override
public ToolResponse transformReply(ToolInvokeReply reply) {
// Converts protobuf ProtocolStringList to List<TextContent>
}
}
class GrpcResourceResponseTransformer implements ResourceResponseTransformer<ResourceReply> {
@Override
public List<ResourceContents> transformReply(
ResourceReply reply,
ResourceManager.ResourceArguments arguments,
ResourceReference mcpResource) {
// Builds TextResourceContents with proper MIME type and URI
}
}Bridge Implementations
InvokerBridge
Package: ai.wanaku.backend.bridge
Proxy for tool invocation services. Uses an async-first design with Uni<ToolResponse>.
public class InvokerBridge implements ToolsBridge {
private static final String SERVICE_TYPE_TOOL_INVOKER = "tool-invoker";
private final ServiceResolver serviceResolver;
private final WanakuBridgeTransport transport;
private final EventNotifier eventNotifier;
public InvokerBridge(
ServiceResolver serviceResolver,
WanakuBridgeTransport transport,
EventNotifier eventNotifier) {
this.serviceResolver = serviceResolver;
this.transport = transport;
this.eventNotifier = eventNotifier;
}
@Override
public Uni<ToolResponse> execute(
ToolManager.ToolArguments toolArguments, CallableReference toolReference) {
// 1. Resolve service (try TOOL_INVOKER, fallback to CODE_EXECUTION_ENGINE)
// 2. Build ToolInvokeRequest via InvokerToolExecutor.buildToolInvokeRequest()
// 3. Emit started event via EventNotifier
// 4. Delegate to transport.invokeTool(request, service)
// 5. Emit completed/failed event on response/failure
}
}InvokerToolExecutor
Package: ai.wanaku.backend.bridge
Static utility class providing helper methods for building tool invocation requests. Used by InvokerBridge to construct ToolInvokeRequest objects from tool references and arguments.
public final class InvokerToolExecutor {
private InvokerToolExecutor() {}
static ToolInvokeRequest buildToolInvokeRequest(
ToolReference toolReference,
ToolManager.ToolArguments toolArguments) {
// Filter out metadata and auth args before converting to string map
Map<String, Object> filteredArgs = filterOutReservedArgs(toolArguments.args());
Map<String, String> argumentsMap = CollectionsHelper.toStringStringMap(filteredArgs);
// Extract metadata headers from args (with prefix stripped)
Map<String, String> metadataHeaders = extractMetadataHeaders(toolArguments);
// Extract auth headers from args (with prefix stripped)
Map<String, String> authHeaders = extractAuthHeaders(toolArguments);
// Extract tool-defined headers from schema
Map<String, String> toolDefinedHeaders = extractHeaders(toolReference, toolArguments);
// Merge headers: metadata first, then tool-defined, then auth (auth wins on conflict)
Map<String, String> headers = new HashMap<>(metadataHeaders);
headers.putAll(toolDefinedHeaders);
headers.putAll(authHeaders);
String body = extractBody(toolReference, toolArguments);
return ToolInvokeRequest.newBuilder()
.setBody(body)
.setUri(toolReference.getUri())
.setConfigurationUri(
Objects.requireNonNullElse(
toolReference.getConfigurationURI(), ""))
.setSecretsUri(
Objects.requireNonNullElse(
toolReference.getSecretsURI(), ""))
.putAllHeaders(headers)
.putAllArguments(argumentsMap)
.build();
}
static Map<String, String> extractHeaders(
ToolReference toolReference,
ToolManager.ToolArguments toolArguments) {
// Filter properties where target == "header" and scope == "service"
}
static Map<String, String> extractMetadataHeaders(
ToolManager.ToolArguments toolArguments) {
// Extract args with "wanaku_meta_" prefix, strip prefix for header name
}
static Map<String, String> extractAuthHeaders(
ToolManager.ToolArguments toolArguments) {
// Extract args with "wanaku_auth_" prefix, strip prefix for header name
// These are treated as sensitive and redacted in logs/events
}
static Map<String, Object> filterOutReservedArgs(Map<String, Object> args) {
// Exclude args with "wanaku_meta_" or "wanaku_auth_" prefix from the arguments map
}
}EventNotifier
Package: ai.wanaku.backend.bridge
Wraps a MutinyEmitter<ToolCallEvent> to emit tool call lifecycle events for observability.
public class EventNotifier {
public EventNotifier(MutinyEmitter<ToolCallEvent> emitter);
public ToolCallEvent emitStartedEvent(
ToolManager.ToolArguments toolArguments,
ToolReference toolReference,
ServiceTarget service,
ToolInvokeRequest request);
public void emitCompletedEvent(String eventId, String content, long duration);
public void emitFailedEvent(
String eventId, ToolCallEvent.ErrorCategory category,
String errorMessage, long duration);
public ToolCallEvent.ErrorCategory categorizeException(Exception e);
}Exception Categorization:
| Exception Type | Category |
|---|---|
StatusRuntimeException | SERVICE_UNAVAILABLE |
ServiceUnavailableException | SERVICE_UNAVAILABLE |
NullPointerException | TOOL_DEFINITION_ERROR |
IllegalArgumentException | INVALID_ARGUMENTS |
| Other | UNKNOWN |
ResourceAcquirerBridge
Package: ai.wanaku.backend.bridge
Proxy for resource acquisition services. Uses ProvisionerBridge for service resolution and the async transport for resource reading.
public class ResourceAcquirerBridge implements ResourceBridge {
private static final String SERVICE_TYPE_RESOURCE_PROVIDER = "resource-provider";
private final ProvisionerBridge provisionerBridge;
private final WanakuBridgeTransport transport;
public ResourceAcquirerBridge(
ProvisionerBridge provisionerBridge,
WanakuBridgeTransport transport) {
this.provisionerBridge = provisionerBridge;
this.transport = transport;
}
@Override
public Uni<ResourceResponse> read(
ResourceManager.ResourceArguments arguments,
ResourceReference mcpResource) {
ServiceTarget service = provisionerBridge.resolveService(
mcpResource.getType(), SERVICE_TYPE_RESOURCE_PROVIDER);
ResourceRequest request = buildResourceRequest(mcpResource);
return transport.acquireResource(request, service, arguments, mcpResource)
.map(ResourceResponse::new);
}
private ResourceRequest buildResourceRequest(ResourceReference mcpResource) {
return ResourceRequest.newBuilder()
.setLocation(mcpResource.getLocation())
.setType(mcpResource.getType())
.setName(mcpResource.getName())
.setConfigurationURI(
Objects.requireNonNullElse(
mcpResource.getConfigurationURI(), ""))
.setSecretsURI(
Objects.requireNonNullElse(
mcpResource.getSecretsURI(), ""))
.build();
}
}ProvisionerBridge
Package: ai.wanaku.backend.bridge
Consolidates provisioning logic and service resolution shared between InvokerBridge and ResourceAcquirerBridge.
public class ProvisionerBridge implements ProvisionBridge {
private final ServiceResolver serviceResolver;
private final WanakuBridgeTransport transport;
public ProvisionerBridge(ServiceResolver serviceResolver,
WanakuBridgeTransport transport) {
this.serviceResolver = serviceResolver;
this.transport = transport;
}
@Override
public ProvisioningReference provision(
String name, String configData, String secretsData,
ServiceTarget service) {
return transport.provision(name, configData, secretsData, service);
}
public ServiceTarget resolveService(String type, String serviceType) {
ServiceTarget service = serviceResolver.resolve(type, serviceType);
if (service == null) {
throw new ServiceNotFoundException(
"There is no host registered for service " + type);
}
return service;
}
}CodeExecutionBridge
Package: ai.wanaku.backend.bridge
Proxy for code execution services with streaming support.
public class CodeExecutionBridge implements CodeExecutorBridge {
private static final String SERVICE_TYPE_CODE_EXECUTION = "code-execution-engine";
private final ServiceResolver serviceResolver;
private final WanakuBridgeTransport transport;
private final MutinyEmitter<ToolCallEvent> toolCallEventEmitter;
@Override
public Iterator<CodeExecutionReply> executeCode(
String engineType,
String language,
CodeExecutionRequest request) {
ServiceTarget service = resolveService(engineType, language);
ai.wanaku.core.exchange.CodeExecutionRequest grpcRequest =
buildGrpcRequest(engineType, language, request);
ToolCallEvent startedEvent = emitStartedEvent(engineType, language, request);
Instant startTime = Instant.now();
try {
Iterator<CodeExecutionReply> iterator =
transport.executeCode(grpcRequest, service);
return new CloseableCodeExecutionIterator(
iterator, startedEvent, startTime, this);
} catch (Exception e) {
emitCompletionEvent(startedEvent, startTime, true, e.getMessage());
throw e;
}
}
private ServiceTarget resolveService(String engineType, String language) {
ServiceTarget service = serviceResolver.resolveCodeExecution(
SERVICE_TYPE_CODE_EXECUTION, engineType, language);
if (service == null) {
throw new ServiceNotFoundException(
SERVICE_TYPE_CODE_EXECUTION, engineType + "/" + language);
}
return service;
}
private ai.wanaku.core.exchange.CodeExecutionRequest buildGrpcRequest(
String engineType,
String language,
CodeExecutionRequest request) {
String uri = String.format("code-execution-engine://%s/%s",
engineType, language);
String decodedCode = decodeBase64Code(request.getCode());
// Convert List<String> arguments to Map with indexed keys
Map<String, String> argMap = new HashMap<>();
List<String> args = request.getArguments();
for (int i = 0; i < args.size(); i++) {
argMap.put("arg" + i, args.get(i));
}
return ai.wanaku.core.exchange.CodeExecutionRequest.newBuilder()
.setUri(uri)
.setCode(decodedCode)
.putAllArguments(argMap)
.putAllEnvironment(request.getEnvironment())
.setTimeout(request.getTimeout())
.build();
}
private String decodeBase64Code(String base64Code) {
if (base64Code == null || base64Code.isEmpty()) {
return "";
}
try {
return new String(Base64.getDecoder().decode(base64Code));
} catch (IllegalArgumentException e) {
return base64Code; // Return raw if not valid base64
}
}
}CloseableCodeExecutionIterator
Wraps streaming iterator to ensure terminal events are emitted.
public static class CloseableCodeExecutionIterator
implements Iterator<CodeExecutionReply>, Closeable {
private final Iterator<CodeExecutionReply> delegate;
private final ToolCallEvent startedEvent;
private final Instant startTime;
private final CodeExecutionBridge bridge;
private boolean completed = false;
private boolean hasError = false;
private String lastErrorMessage = null;
@Override
public boolean hasNext() {
try {
boolean hasMore = delegate.hasNext();
if (!hasMore && !completed) {
bridge.emitCompletionEvent(
startedEvent, startTime, hasError, lastErrorMessage);
completed = true;
}
return hasMore;
} catch (Exception e) {
hasError = true;
lastErrorMessage = e.getMessage();
if (!completed) {
bridge.emitCompletionEvent(
startedEvent, startTime, true, e.getMessage());
completed = true;
}
throw e;
}
}
@Override
public CodeExecutionReply next() {
try {
CodeExecutionReply reply = delegate.next();
if (reply.getIsError()) {
hasError = true;
lastErrorMessage = reply.getContent(0);
}
return reply;
} catch (Exception e) {
hasError = true;
lastErrorMessage = e.getMessage();
if (!completed) {
bridge.emitCompletionEvent(
startedEvent, startTime, true, e.getMessage());
completed = true;
}
throw e;
}
}
@Override
public void close() {
if (!completed) {
bridge.emitCompletionEvent(
startedEvent, startTime, hasError, lastErrorMessage);
completed = true;
}
}
}gRPC Protocol Definitions
Provisioner Service
File: provision.proto
service Provisioner {
rpc Provision (ProvisionRequest) returns (ProvisionReply) {}
}
message Configuration {
PayloadType type = 1; // REFERENCE or BUILTIN
string name = 2;
string payload = 3;
}
message Secret {
PayloadType type = 1;
string name = 2;
string payload = 3;
}
message ProvisionRequest {
string uri = 1;
Configuration configuration = 2;
Secret secret = 3;
}
message ProvisionReply {
string configurationUri = 1;
string secretUri = 2;
map<string, PropertySchema> properties = 3;
}ToolInvoker Service
File: toolrequest.proto
service ToolInvoker {
rpc InvokeTool (ToolInvokeRequest) returns (ToolInvokeReply) {}
}
message ToolInvokeRequest {
string uri = 1;
string body = 2;
map<string, string> arguments = 3;
string configurationURI = 4;
string secretsURI = 5;
map<string, string> headers = 6;
}
message ToolInvokeReply {
bool isError = 1;
repeated string content = 2;
}ResourceAcquirer Service
File: resourcerequest.proto
service ResourceAcquirer {
rpc ResourceAcquire (ResourceRequest) returns (ResourceReply) {}
}
message ResourceRequest {
string location = 1;
string type = 2;
string name = 3;
map<string, string> params = 4;
string configurationURI = 5;
string secretsURI = 6;
}
message ResourceReply {
bool isError = 1;
repeated string content = 2;
}CodeExecutor Service
File: codeexecution.proto
service CodeExecutor {
rpc ExecuteCode (CodeExecutionRequest) returns (stream CodeExecutionReply) {}
}
message CodeExecutionRequest {
string uri = 1;
string body = 2;
string code = 3;
map<string, string> arguments = 4;
string configurationURI = 5;
string secretsURI = 6;
int64 timeout = 7;
map<string, string> environment = 8;
}
message CodeExecutionReply {
bool isError = 1;
repeated string content = 2;
OutputType outputType = 3;
ExecutionStatus status = 4;
int32 exitCode = 5;
int64 timestamp = 6;
}
enum OutputType {
STDOUT = 0;
STDERR = 1;
STATUS = 2;
COMPLETION = 3;
}
enum ExecutionStatus {
PENDING = 0;
RUNNING = 1;
COMPLETED = 2;
FAILED = 3;
CANCELLED = 4;
TIMEOUT = 5;
}Request/Response Flows
Tool Invocation Flow (Async)
┌─────────────────────────────────────────────────────────────────┐
│ 1. Client Request │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. InvokerBridge.execute(ToolArguments, CallableReference) │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. ServiceResolver.resolve(type, "tool-invoker") │
│ └─ Fallback: resolve(type, "code-execution-engine") │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. buildToolInvokeRequest() │
│ ├─ extractHeaders(): target="header", scope="service" │
│ ├─ extractBody(): BODY argument if defined │
│ └─ Convert arguments to Map<String, String> │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. GrpcTransport.invokeTool(request, service) │
│ ├─ GrpcChannelManager.createChannel(ServiceTarget) [cached] │
│ ├─ ToolInvokerGrpc.newFutureStub(channel) │
│ ├─ stub.invokeTool(request) │
│ └─ GrpcToolResponseTransformer.transformReply() │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. Return Uni<ToolResponse> │
└─────────────────────────────────────────────────────────────────┘Resource Acquisition Flow (Async)
┌─────────────────────────────────────────────────────────────────┐
│ 1. ResourceAcquirerBridge.read(ResourceArguments, Ref) │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. ProvisionerBridge.resolveService(type, "resource-provider") │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. buildResourceRequest() │
│ ├─ location: from ResourceReference │
│ ├─ type: from ResourceReference │
│ ├─ configurationURI: from ResourceReference (default "") │
│ └─ secretsURI: from ResourceReference (default "") │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. GrpcTransport.acquireResource(request, service, ...) │
│ ├─ GrpcChannelManager.createChannel(ServiceTarget) [cached] │
│ ├─ ResourceAcquirerGrpc.newFutureStub(channel) │
│ ├─ stub.resourceAcquire(request) │
│ └─ GrpcResourceResponseTransformer.transformReply() │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Return Uni<ResourceResponse> │
└─────────────────────────────────────────────────────────────────┘Code Execution Flow (Streaming)
┌─────────────────────────────────────────────────────────────────┐
│ 1. CodeExecutionBridge.executeCode(engineType, language, req) │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. ServiceResolver.resolveCodeExecution( │
│ "code-execution-engine", engineType, language) │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. buildGrpcRequest() │
│ ├─ uri: "code-execution-engine://{engineType}/{language}" │
│ ├─ code: Base64 decode from request │
│ ├─ arguments: List → Map (arg0, arg1, ...) │
│ └─ environment: from request │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Emit STARTED event │
│ ├─ Redact code: "[CODE]" │
│ └─ Redact arguments: emit count only │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. GrpcTransport.executeCode(request, service) │
│ ├─ GrpcChannelManager.createChannel(ServiceTarget) │
│ ├─ CodeExecutorGrpc.newBlockingStub(channel) │
│ └─ stub.executeCode(request) → Iterator │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. Wrap in CloseableCodeExecutionIterator │
│ ├─ Tracks completion state │
│ └─ Ensures terminal event on close/completion │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 7. Stream responses to client │
│ └─ Emit COMPLETED/FAILED when iterator exhausted or closed │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 8. Return Iterator<CodeExecutionReply> │
└─────────────────────────────────────────────────────────────────┘Provisioning Flow
┌─────────────────────────────────────────────────────────────────┐
│ 1. ProvisionerBridge.provision(name, configData, secretsData, │
│ service) │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. GrpcTransport.provision(name, configData, secretsData, svc) │
│ ├─ Build Configuration protobuf │
│ ├─ Build Secret protobuf │
│ ├─ Build ProvisionRequest │
│ ├─ ProvisionerGrpc.ProvisionerBlockingStub │
│ └─ stub.provision(request) → ProvisionReply │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Return ProvisioningReference(configUri, secretUri, props) │
└─────────────────────────────────────────────────────────────────┘Design Patterns
Transport Abstraction Pattern
// Interface defines protocol-independent contract with async-first design
interface WanakuBridgeTransport {
Uni<ToolResponse> invokeTool(ToolInvokeRequest request, ServiceTarget service);
}
// gRPC implementation with response transformation
class GrpcTransport implements WanakuBridgeTransport {
private final ToolResponseTransformer<ToolInvokeReply> transformer = ...;
public Uni<ToolResponse> invokeTool(ToolInvokeRequest request, ServiceTarget service) {
// gRPC FutureStub → Uni with transformer applied
}
}
// HTTP implementation (future)
class HttpTransport implements WanakuBridgeTransport {
public Uni<ToolResponse> invokeTool(ToolInvokeRequest request, ServiceTarget service) {
// HTTP-specific logic with its own transformer
}
}Composition Over Inheritance
// InvokerBridge delegates to transport for async execution
class InvokerBridge implements ToolsBridge {
private final ServiceResolver serviceResolver; // Service discovery
private final WanakuBridgeTransport transport; // Protocol handling
}
// ResourceAcquirerBridge delegates to ProvisionerBridge and transport
class ResourceAcquirerBridge implements ResourceBridge {
private final ProvisionerBridge provisionerBridge; // Shared provisioning + resolution
private final WanakuBridgeTransport transport; // Protocol handling
}
// Each component has single responsibilityService Locator Pattern
// ServiceResolver discovers services dynamically
ServiceTarget service = serviceResolver.resolve("my-tool", "tool-invoker");
// Bridges don't need hardcoded addressesIterator Wrapper Pattern
// Ensure cleanup even if iteration abandoned
try (CloseableCodeExecutionIterator iter = bridge.executeCode(...)) {
while (iter.hasNext()) {
CodeExecutionReply reply = iter.next();
// Process reply
}
} // Terminal event emitted on closeSecurity Features
| Feature | Implementation |
|---|---|
| Argument Redaction | Events omit actual argument values |
| Code Redaction | Code replaced with "[CODE]" in events |
| Error Sanitization | Safe error messages in responses |
| Plaintext (dev) | Default configuration for development |
| TLS Ready | Channel manager designed for future SSL/TLS |
File Locations
| Component | Path |
|---|---|
| Bridge.java | apps/wanaku-router-backend/.../bridge/ |
| ToolsBridge.java | apps/wanaku-router-backend/.../bridge/ |
| ResourceBridge.java | apps/wanaku-router-backend/.../bridge/ |
| CodeExecutorBridge.java | apps/wanaku-router-backend/.../bridge/ |
| ProvisionBridge.java | apps/wanaku-router-backend/.../bridge/ |
| McpBridge.java | apps/wanaku-router-backend/.../bridge/ |
| WanakuBridgeTransport.java | apps/wanaku-router-backend/.../bridge/ |
| InvokerBridge.java | apps/wanaku-router-backend/.../bridge/ |
| InvokerToolExecutor.java | apps/wanaku-router-backend/.../bridge/ |
| EventNotifier.java | apps/wanaku-router-backend/.../bridge/ |
| ResourceAcquirerBridge.java | apps/wanaku-router-backend/.../bridge/ |
| CodeExecutionBridge.java | apps/wanaku-router-backend/.../bridge/ |
| ProvisionerBridge.java | apps/wanaku-router-backend/.../bridge/ |
| DefaultMcpBridge.java | apps/wanaku-router-backend/.../bridge/ |
| ForwardClient.java | apps/wanaku-router-backend/.../bridge/ |
| ForwardRegistry.java | apps/wanaku-router-backend/.../bridge/ |
| ToolResponseTransformer.java | apps/wanaku-router-backend/.../bridge/ |
| ResourceResponseTransformer.java | apps/wanaku-router-backend/.../bridge/ |
| GrpcTransport.java | apps/wanaku-router-backend/.../bridge/transports/grpc/ |
| GrpcChannelManager.java | apps/wanaku-router-backend/.../bridge/transports/grpc/ |
| GrpcToolResponseTransformer.java | apps/wanaku-router-backend/.../bridge/transports/grpc/ |
| GrpcResourceResponseTransformer.java | apps/wanaku-router-backend/.../bridge/transports/grpc/ |
| provision.proto | core/core-exchange/src/main/proto/ |
| toolrequest.proto | core/core-exchange/src/main/proto/ |
| resourcerequest.proto | core/core-exchange/src/main/proto/ |
| codeexecution.proto | core/core-exchange/src/main/proto/ |
Resource Provider Protocol
This section describes the gRPC protocol used by resource provider capabilities.
Overview
Resource providers implement the ResourceAcquirerBridge interface. The router calls the provider to retrieve resource contents given a URI.
Request Schema
The router sends a ResourceRequest message:
message ResourceRequest {
string uri = 1; // The resource URI to retrieve
map<string, string> params = 2; // Optional query parameters
}uri— the resource URI as registered with Wanaku (e.g.,file:///data/report.txt)params— optional key-value parameters passed by the MCP client
Response Schema
The provider returns a ResourceResponse message:
message ResourceResponse {
repeated ResourceContent contents = 1;
ResourceError error = 2; // Present only on failure
}
message ResourceContent {
string uri = 1;
string mime_type = 2;
oneof content {
string text = 3;
bytes blob = 4;
}
}
message ResourceError {
int32 code = 1;
string message = 2;
}
```
- `contents` — one or more content items returned by the provider
- `error` — present only when the provider encountered an error
### Example Java Resource Provider
```java
@ApplicationScoped
public class FileResourceProvider implements ResourceAcquirerBridge {
@Override
public ResourceResponse acquire(ResourceRequest request) {
try {
Path path = Path.of(URI.create(request.getUri()));
String content = Files.readString(path);
return ResourceResponse.newBuilder()
.addContents(ResourceContent.newBuilder()
.setUri(request.getUri())
.setMimeType("text/plain")
.setText(content)
.build())
.build();
} catch (IOException e) {
return ResourceResponse.newBuilder()
.setError(ResourceError.newBuilder()
.setCode(404)
.setMessage("Resource not found: " + e.getMessage())
.build())
.build();
}
}
}
```
## Error Handling Patterns
### Tool Invoker Error Handling
Tool invokers should handle errors at two levels:
**Transport errors** (gRPC connection failures):
```java
try {
ToolResponse response = stub.invoke(request);
// handle response
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.UNAVAILABLE) {
// Capability service is down — return a friendly error
log.error("Capability unavailable: {}", e.getMessage());
return ToolResponse.error("Capability service is temporarily unavailable");
}
throw e;
}
```
**Application errors** (errors returned in the response):
```java
ToolResponse response = stub.invoke(request);
if (response.hasError()) {
log.warn("Tool invocation error: {} (code={})",
response.getError().getMessage(),
response.getError().getCode());
// Map error codes to MCP error types
return mapErrorToMcpResponse(response.getError());
}
```
### Resource Provider Error Handling
Resource providers should return structured errors rather than throwing exceptions:
```java
// GOOD: Return a structured error response
return ResourceResponse.newBuilder()
.setError(ResourceError.newBuilder()
.setCode(404)
.setMessage("Resource not found: " + uri)
.build())
.build();
// BAD: Throwing an exception causes an unstructured gRPC error
throw new RuntimeException("Resource not found");
```
**Common error codes:**
| Code | Meaning | When to use |
|------|---------|-------------|
| `400` | Bad request | URI is malformed or parameters are invalid |
| `404` | Not found | Resource URI does not exist |
| `403` | Forbidden | Access to the resource is not permitted |
| `500` | Internal error | Unexpected provider failure |
| `503` | Service unavailable | Transient failure; client should retry |
### Retry and Timeout Configuration
Configure retry and timeout behavior for gRPC calls:
```yaml
wanaku:
bridge:
grpc:
timeout-ms: 5000 # Per-call timeout in milliseconds
max-retries: 3 # Retry attempts on UNAVAILABLE errors
retry-delay-ms: 100 # Initial retry delay
```
## Testing gRPC Locally
### Using grpcurl
Test tool and resource providers with `grpcurl`:
```bash
# List available services
grpcurl -plaintext localhost:50051 list
# Call a tool provider
grpcurl -plaintext -d '{"name":"my-tool","arguments":{"input":"test"}}' \
localhost:50051 wanaku.ToolInvoker/Invoke
# Call a resource provider
grpcurl -plaintext -d '{"uri":"file:///data/test.txt"}' \
localhost:50051 wanaku.ResourceAcquirer/Acquire
```
### Using a Mock Server
For unit testing, implement a mock gRPC server:
```java
@QuarkusTest
class MyToolProviderTest {
@GrpcClient("test-server")
ToolInvokerGrpc.ToolInvokerBlockingStub stub;
@Test
void testToolInvocation() {
ToolResponse response = stub.invoke(
ToolRequest.newBuilder()
.setName("my-tool")
.putArguments("input", "hello")
.build()
);
assertThat(response.hasError()).isFalse();
assertThat(response.getContents(0).getText()).contains("expected");
}
}
```