Skip to content
🐍 Looking for Python? See Python Dependency Injection | 📘 Looking for TypeScript? See TypeScript Dependency Injection

Dependency Injection (Java/Spring Boot)

Automatic wiring of capabilities between agents

MCP Mesh implements Distributed Dynamic Dependency Injection (DDDI) — dependencies are discovered and injected at runtime across the mesh, not at compile time.

Overview

MCP Mesh provides automatic dependency injection (DI) that connects agents based on their declared capabilities and dependencies. When a tool declares a dependency via @Selector, the mesh automatically injects a McpMeshTool<T> proxy that routes to the providing agent.

How It Works

  1. Declaration: Tool declares dependencies via @MeshTool(dependencies = @Selector(...))
  2. Registration: Agent registers with registry, advertising capabilities
  3. Resolution: Registry matches dependencies to providers
  4. Injection: Mesh injects McpMeshTool<T> instances as method parameters
  5. Invocation: Calling the proxy routes to the remote agent

Declaring Dependencies

Simple Dependency

@MeshTool(capability = "smart_greeting",
          description = "Greet with current date",
          dependencies = @Selector(capability = "date_service"))
public GreetingResponse smartGreet(
        @Param(value = "name", description = "The name to greet") String name,
        McpMeshTool<String> dateService) {

    if (dateService != null && dateService.isAvailable()) {
        String today = dateService.call();
        return new GreetingResponse("Hello " + name + "! Today is " + today);
    }
    return new GreetingResponse("Hello " + name + "!");
}

Important: Dependencies are injected as McpMeshTool<T> parameters on the method. They may be null if unavailable.

Dependencies with Filters

Use the @Selector annotation with tags or version to filter providers:

@MeshTool(capability = "report",
          description = "Generate report with formatted data",
          dependencies = @Selector(capability = "data_service",
                                    tags = {"+fast", "-deprecated"}))
public String generateReport(
        @Param(value = "query", description = "Report query") String query,
        McpMeshTool<String> dataService) {

    if (dataService == null || !dataService.isAvailable()) {
        return "Data service unavailable";
    }
    return dataService.call("query", query);
}

Component-level dependency declaration with @MeshDependsOn

@MeshInject and @MeshRoute(dependencies=...) work at the controller-method scope. For everything else — @Service beans, @Components, servlet Filters, @Scheduled jobs — declare the capabilities your bean needs with the class-level @MeshDependsOn annotation. The auto-configuration then registers a singleton McpMeshTool bean named by each capability, so you can wire it the standard Spring way.

@Service
@MeshDependsOn({
    @MeshDependency(capability = "list_holidays"),
    @MeshDependency(capability = "get_user_profile")
})
public class StaffSyncService {
    private final McpMeshTool<List<Holiday>> holidays;
    private final McpMeshTool<UserProfileResponse> profile;

    public StaffSyncService(
            @Qualifier("list_holidays") McpMeshTool<List<Holiday>> holidays,
            @Qualifier("get_user_profile") McpMeshTool<UserProfileResponse> profile) {
        this.holidays = holidays;
        this.profile = profile;
    }

    public List<Holiday> upcomingHolidays() {
        if (!holidays.isAvailable()) return List.of();
        return holidays.call();
    }
}

Field injection

@Component
@MeshDependsOn(@MeshDependency(capability = "list_holidays"))
public class HolidayChecker {
    @Autowired
    @Qualifier("list_holidays")
    private McpMeshTool<List<Holiday>> holidays;
}

Where to use it

Scenario Annotation
@MeshTool method needing remote helpers @MeshTool(dependencies = @Selector(...)) + parameter injection
@RestController handler method @MeshRoute(dependencies = {...}) + @MeshInject parameter
@Service / @Component / Filter / @Scheduled / any other Spring bean @MeshDependsOn + @Qualifier

@MeshDependsOn and @MeshInject/@MeshRoute are complementary — same heartbeat-driven proxy lifecycle, same auto-rewiring on topology change, same isAvailable() semantics. Pick the surface that matches where you need the dependency. If the same capability shows up via multiple sources the framework deduplicates: a single proxy and a single registry entry per capability name.

Tags, version, and bean-name conflicts

The @MeshDependency element accepts the same tags, version, expectedType, and schemaMode fields documented above for @MeshRoute. If a @MeshDependsOn capability happens to match the name of a user-owned Spring bean, the user's bean wins — the framework logs an ERROR naming the conflicting bean's class and every @MeshDependsOn-annotated class that declared the capability, then skips the proxy registration. Any consumer that @Qualifier-injected McpMeshTool<...> for that capability will fail context refresh with BeanNotOfRequiredTypeException. Resolve by renaming either the user bean or the capability.

Typed deserialization

When you set expectedType on @MeshDependency, the framework wires it into the McpMeshTool proxy's deserialization type at registration time. The first call returns the typed value directly — no extra setReturnType(...) step required:

@Service
@MeshDependsOn(@MeshDependency(
    capability   = "get_user_profile",
    expectedType = UserProfileResponse.class))
public class StaffSyncService {
    public StaffSyncService(@Qualifier("get_user_profile") McpMeshTool<UserProfileResponse> profile) {
        // profile.call(...) returns UserProfileResponse, not Map<String,Object>
    }
}

If you omit expectedType and the @Qualifier-injected field is declared as McpMeshTool<UserProfileResponse>, deserialization to that generic type is best-effort and the first call may return Map<String, Object> until something else (a @MeshRoute parameter with the same generic, an explicit setReturnType(...)) primes the proxy. Setting expectedType is the supported way to make typed responses work upfront from a @MeshDependsOn surface.

Discovery caveat — @Bean factory methods returning a supertype

@MeshDependsOn is read off the bean's resolved class via Spring's bean-definition metadata, then AnnotationUtils.findAnnotation walks the class and its supertypes. This covers @Component-scanned beans AND @Bean factory methods — both surfaces work as expected when the annotation is on the class Spring sees.

Shape Discovered?
@Component @MeshDependsOn(...) class Foo
@Bean public ConcreteFoo foo() where @MeshDependsOn is on ConcreteFoo ✓ — Spring's ResolvableType reports the concrete return type
@Bean public Foo foo() where @MeshDependsOn is on Foo (or any supertype Foo extends)
@Bean public Foo foo() { return new ConcreteFoo(); } where @MeshDependsOn is ONLY on ConcreteFoo ✗ — findAnnotation walks supertypes of the declared return type, not subtypes of it

Only the last shape is a real gap. If your factory method declares a supertype return, either narrow the declared return type to the concrete class, or place @MeshDependsOn on the supertype itself (or on the @Configuration class).

McpMeshTool<T> API Reference

The McpMeshTool<T> interface is the primary way to interact with remote capabilities. The type parameter T indicates the expected return type.

call() - No Arguments

Invoke the remote tool with no parameters:

McpMeshTool<String> dateService;
String today = dateService.call();

call(Record) - Structured Parameters

Pass a Java record whose field names become parameter names:

McpMeshTool<Integer> calculator;

record AddParams(int a, int b) {}
Integer sum = calculator.call(new AddParams(3, 5));  // sum = 8

call(key, value, ...) - Varargs

Pass parameters as key-value pairs:

McpMeshTool<String> greeting;
String result = greeting.call("name", "Alice", "language", "en");

isAvailable()

Check if the remote capability is currently reachable:

if (dateService != null && dateService.isAvailable()) {
    // Safe to call
}

getEndpoint()

Get the remote endpoint URL:

String url = dateService.getEndpoint();
// e.g., "http://localhost:9001"

getCapability()

Get the capability name this proxy represents:

String cap = dateService.getCapability();
// e.g., "date_service"

API Summary

Method Description Return Type
call() No-arg invocation T
call(record) Call with record fields as params T
call(k, v, ...) Call with key-value pairs T
isAvailable() Check provider reachability boolean
getEndpoint() Remote agent endpoint URL String
getCapability() Capability name of this dependency String

Type-Safe Responses

The generic type parameter T on McpMeshTool<T> controls response deserialization. The SDK automatically converts the remote JSON response to the specified type.

// Primitive types
McpMeshTool<Integer> calculator;
Integer sum = calculator.call(new AddParams(3, 5));

// String responses
McpMeshTool<String> dateService;
String today = dateService.call();

// Complex record types
McpMeshTool<Employee> employeeService;
Employee emp = employeeService.call("id", 42);
// Employee record is auto-deserialized from JSON

record Employee(int id, String name, String department) {}

Graceful Degradation

Dependencies may be unavailable if the providing agent is down or not yet started. Always handle null and check availability:

@MeshTool(capability = "agent_status",
          description = "Get status with dependency info",
          dependencies = @Selector(capability = "date_service"))
public AgentStatus getStatus(McpMeshTool<String> dateService) {
    boolean depAvailable = dateService != null && dateService.isAvailable();

    if (depAvailable) {
        String date = dateService.call();
        return new AgentStatus("operational", date);
    }
    return new AgentStatus("degraded", "date service unavailable");
}

record AgentStatus(String status, String info) {}

Or provide fallback values:

@MeshTool(capability = "time_service",
          description = "Get current time",
          dependencies = @Selector(capability = "date_service"))
public TimeResponse getTime(McpMeshTool<String> dateService) {
    if (dateService != null && dateService.isAvailable()) {
        return new TimeResponse(dateService.call());
    }
    // Fallback to local time
    return new TimeResponse(java.time.LocalDateTime.now().toString());
}

Auto-Rewiring

When topology changes (agents join/leave), the mesh:

  1. Detects change via heartbeat response
  2. Refreshes dependency proxies
  3. Routes to new providers automatically

No code changes needed - happens transparently.

Multiple Dependencies

A single tool can depend on multiple capabilities. Each dependency gets its own McpMeshTool<T> parameter:

@MeshTool(capability = "add_via_mesh",
          description = "Add two numbers using remote calculator",
          tags = {"math", "cross-agent", "java"},
          dependencies = @Selector(capability = "add"))
public CalculationResult addViaMesh(
        @Param(value = "a", description = "First number") int a,
        @Param(value = "b", description = "Second number") int b,
        McpMeshTool<Integer> calculator) {

    Integer sum = calculator.call(new AddParams(a, b));
    return new CalculationResult("add", a, b, sum);
}

record AddParams(int a, int b) {}
record CalculationResult(String op, int a, int b, int result) {}

LLM Injection

For @MeshLlm annotated tools, the LLM is injected as a MeshLlmAgent parameter:

@MeshLlm(providerSelector = @Selector(capability = "llm"),
         maxIterations = 5, systemPrompt = "You are a helpful analyst.")
@MeshTool(capability = "analyze",
          description = "AI-powered analysis",
          tags = {"analysis", "llm", "java"})
public AnalysisResult analyze(
        @Param(value = "query", description = "Analysis query") String query,
        MeshLlmAgent llm) {

    return llm.request()
              .user(query)
              .generate(AnalysisResult.class);
}

Complete Example

package com.example.assistant;

import io.mcpmesh.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MeshAgent(name = "assistant", version = "1.0.0",
           description = "Assistant with mesh dependencies", port = 9001)
@SpringBootApplication
public class AssistantAgentApplication {

    public static void main(String[] args) {
        SpringApplication.run(AssistantAgentApplication.class, args);
    }

    @MeshTool(capability = "smart_greeting",
              description = "Greet with current date from mesh",
              tags = {"greeting", "assistant", "java"},
              dependencies = @Selector(capability = "date_service"))
    public GreetingResponse smartGreet(
            @Param(value = "name", description = "The name to greet") String name,
            McpMeshTool<String> dateService) {

        if (dateService != null && dateService.isAvailable()) {
            String dateString = dateService.call();
            return new GreetingResponse(
                "Hello, " + name + "! Today is " + dateString);
        }
        return new GreetingResponse(
            "Hello, " + name + "! (date service unavailable)");
    }

    @MeshTool(capability = "agent_status",
              description = "Get agent status with dependency info",
              tags = {"status", "info", "java"},
              dependencies = @Selector(capability = "date_service"))
    public AgentStatus getStatus(McpMeshTool<String> dateService) {
        boolean available = dateService != null && dateService.isAvailable();
        String endpoint = available ? dateService.getEndpoint() : "N/A";
        String capability = available ? dateService.getCapability() : "N/A";

        return new AgentStatus("assistant", available, endpoint, capability);
    }

    record GreetingResponse(String message) {}
    record AgentStatus(String agent, boolean depAvailable,
                       String depEndpoint, String depCapability) {}
}

See Also

  • meshctl man capabilities --java - Declaring capabilities
  • meshctl man tags --java - Tag-based selection
  • meshctl man decorators --java - All Java annotations