Integrations

Model Context Protocol (MCP)

Publishing

Overview

Embabel Agent can expose your agents as MCP servers, making them available to external MCP clients such as Claude Desktop, VS Code extensions, or other MCP-compatible applications. The framework provides automatic publishing of agent goals as tools and prompts without requiring manual configuration.

Server Configuration

Configure MCP server functionality in your application.yml. The server type determines the execution mode:

spring:
  ai:
    mcp:
      server:
        type: SYNC  # or ASYNC
Server Types

Embabel Agent supports two MCP server execution modes controlled by the spring.ai.mcp.server.type property:

  • SYNC Mode (Default)
  • Blocking operations wrapped in reactive streams
  • Simpler to develop and debug
  • Suitable for most use cases
  • Better error handling and logging
spring:
  ai:
    mcp:
      server:
        type: SYNC
  • ASYNC Mode
  • True non-blocking reactive operations
  • Higher throughput for concurrent requests
  • More complex error handling
  • Suitable for high-performance scenarios
spring:
  ai:
    mcp:
      server:
        type: ASYNC
Transport Protocol

Embabel Agent uses SSE (Server-Sent Events) transport, exposing your MCP server at http://localhost:8080/sse. This is compatible with Claude Desktop, MCP Inspector, Cursor, and most desktop MCP clients.

  • Clients requiring Streamable HTTP
    Some clients (e.g., OpenWebUI) require Streamable HTTP transport instead of SSE. Use the mcpo proxy to bridge your SSE server:
uvx mcpo --port 8000 --server-type sse -- http://localhost:8080/sse

Then connect your client to http://localhost:8000.

Automatic Publishing
  • Tools
    Agent goals are automatically published as MCP tools when annotated with @Export(remote = true). The PerGoalMcpToolExportCallbackPublisher automatically discovers and exposes these goals without any additional configuration.
  • Prompts
    Prompts are automatically generated for each goal’s starting input types through the PerGoalStartingInputTypesPromptPublisher. This provides ready-to-use prompt templates based on your agent definitions.
Exposing Agent Goals as Tools

Agent goals become MCP tools automatically when annotated with @Export:

@Agent(
    goal = "Provide weather information",
    backstory = "Weather service agent"
)
public class WeatherAgent {

    @Goal
    @Export(remote = true)  // Automatically becomes MCP tool
    public String getWeather(
        @Param("location") String location,
        @Param("units") String units
    ) {
        return "Weather for " + location + " in " + units;
    }

    @Goal
    public String internalMethod() {
        // Not exposed to MCP (no @Export annotation)
        return "Internal use only";
    }
}
Exposing Embabel ToolObject and LlmReference types as tools

A common requirement is to expose existing Embabel functionality via MCP. For example, an LlmReference might be added to a PromptRunner but might also be used as an external tool via MCP.

To do this, use McpToolExport to create a bean of type McpToolExportCallbackPublisher.

For example, to expose a ToolishRag LLM reference as an MCP tool, define a Spring configuration class as follows:

@Configuration
public class RagMcpTools {

    @Bean
    McpToolExport ragTools( // ①
            SearchOperations searchOperations) {
        var toolishRag = new ToolishRag(
                "docs",
                "Embabel docs",
                searchOperations
        );
        return McpToolExport.fromLlmReference(toolishRag); // ②
    }
}
  1. Your bean should be of type McpToolExport
  2. Use McpToolExport.fromLlmReference to return the instance
Naming Strategies

When exporting tools, you can control how tool names are transformed using a naming strategy. This is useful for namespacing tools when exporting from multiple sources to avoid naming conflicts.

Using ToolObject with a naming strategy:

@Bean
public McpToolExport prefixedTools() {
    return McpToolExport.fromToolObject(
        new ToolObject(
            List.of(myToolInstance),
            name -> "myservice_" + name  // ①
        )
    );
}
  1. All tool names will be prefixed with myservice_

Common naming strategies include:

  • Prefix: { "namespace_$it" } - adds a prefix to avoid conflicts
  • Uppercase: { it.uppercase() } - converts to uppercase
  • Identity: StringTransformer.IDENTITY - preserves original names (default)

LlmReference naming:

When using fromLlmReference, the reference’s built-in naming strategy is applied automatically. This prefixes tool names with the lowercased, normalized reference name. For example, an LlmReference named "MyAPI" will prefix all tools with myapi_.

// Reference named "WeatherService" will prefix tools with "weatherservice_"
var reference = new MyWeatherReference();  // name = "WeatherService"
McpToolExport.fromLlmReference(reference);
// Tool "getWeather" becomes "weatherservice_getWeather"

Exporting multiple sources with different prefixes:

@Bean
public McpToolExport multiSourceTools() {
    return McpToolExport.fromToolObjects(
        List.of(
            new ToolObject(
                List.of(weatherTools),
                name -> "weather_" + name
            ),
            new ToolObject(
                List.of(stockTools),
                name -> "stocks_" + name
            )
        )
    );
}
Filtering Tools

You can filter which tools are exported using the filter property on ToolObject:

@Bean
public McpToolExport filteredTools() {
    return McpToolExport.fromToolObject(
        new ToolObject(
            List.of(myToolInstance),
            StringTransformer.IDENTITY,
            name -> name.startsWith("public_")  // ①
        )
    );
}
  1. Only tools whose names start with public_ will be exported

You can combine naming strategies and filters:

@Bean
public McpToolExport combinedTools() {
    return McpToolExport.fromToolObject(
        new ToolObject(
            List.of(myToolInstance),
            name -> "api_" + name,
            name -> !name.startsWith("internal")  // ①
        )
    );
}
  1. The filter is applied to the original tool name before the naming strategy transforms it
Exposing Tools on Spring Components in Spring AI style

It is also possible to expose tools on Spring components as with regular Spring AI.

For example:

@Component
public class CalculatorTools {

    @McpTool(name = "add", description = "Add two numbers together")
    public int add(
            @McpToolParam(description = "First number", required = true) int a,
            @McpToolParam(description = "Second number", required = true) int b) {
        return a + b;
    }

    @McpTool(name = "multiply", description = "Multiply two numbers")
    public double multiply(
            @McpToolParam(description = "First number", required = true) double x,
            @McpToolParam(description = "Second number", required = true) double y) {
        return x * y;
    }
}

Of course, you can inject the Embabel Ai interface to help do the work of the tools if you wish, or invoke other agents from within the tool methods.

For further information, see the Spring AI MCP Annotations Reference.

Server Architecture

The MCP server implementation uses several design patterns:

  • Template Method Pattern
  • AbstractMcpServerConfiguration provides common initialization logic
  • Concrete implementations (McpSyncServerConfiguration, McpAsyncServerConfiguration) handle mode-specific details
  • Strategy Pattern
  • Server strategies abstract sync vs async operations
  • Mode-specific implementations handle tool, resource, and prompt management
  • Publisher Pattern
  • Tools, resources, and prompts are discovered through publisher interfaces
  • Automatic registration and lifecycle management
  • Event-driven initialization ensures proper timing
Built-in Tools

Every MCP server includes a built-in helloBanner tool that displays server information:

{
  "type": "banner",
  "mode": "SYNC",
  "lines": [
    "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
    "Embabel Agent MCP SYNC Server",
    "Version: 0.3.0-SNAPSHOT",
    "Java: 21.0.2+13-LTS-58",
    "Started: 2025-01-17T14:23:47.785Z",
    "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
  ]
}

Security

Embabel MCP servers support two complementary layers of security that work together. Think of them like a building with a reception desk and locked office doors: the HTTP filter chain is the reception desk that turns away anyone without a badge, and @SecureAgentTool is the locked door on each individual office that checks what the badge actually permits.

Layer 1 — HTTP transport (filter chain)

All requests to MCP endpoints (/sse/***, /mcp/****, /message/***) must carry a valid JWT Bearer token or they are rejected with 401 Unauthorized before the GOAP planner is invoked.

Configure a SecurityFilterChain and a JWT resource server in your Spring Security setup:

@Configuration
@EnableWebSecurity
class McpSecurityConfiguration {

    @Bean
    fun mcpFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .securityMatcher("/sse/**", "/mcp/**", "/message/**")
            .authorizeHttpRequests { it.anyRequest().authenticated() }
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .oauth2ResourceServer { oauth2 ->
                oauth2.jwt { jwt ->
                    jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
                }
            }
            .csrf { it.disable() }
        return http.build()
    }

    @Bean
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val authoritiesConverter = JwtGrantedAuthoritiesConverter().apply {
            setAuthoritiesClaimName("authorities")
            setAuthorityPrefix("")  // ①
        }
        return JwtAuthenticationConverter().apply {
            setJwtGrantedAuthoritiesConverter(authoritiesConverter)
        }
    }
}
  1. Empty prefix means JWT claim values like news:read map directly to Spring Security authorities, so hasAuthority('news:read') in a @SecureAgentTool expression works without any SCOPE_ prefix.

Configure JWT validation in application.yml:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:keys/public.pem  # local dev
          jws-algorithms: RS256
          # For production, use issuer-uri or jwk-set-uri instead
Layer 2 — Method-level (@SecureAgentTool)

Enforces per-action authorization inside the GOAP execution pipeline, after the HTTP layer has validated the token. Place @SecureAgentTool on the @Agent class to protect every @Action in that agent:

@Agent(description = "Curated news digest agent")
@SecureAgentTool("hasAuthority('news:read')")  // ①
class NewsDigestAgent {

    @Action
    fun extractTopic(userInput: UserInput, context: OperationContext): NewsTopic { ... } // ②

    @AchievesGoal(description = "Produce news digest",
                  export = Export(remote = true, name = "newsDigest",
                                  startingInputTypes = [UserInput::class]))
    @Action
    fun produceDigest(topic: NewsTopic, context: OperationContext): NewsDigest { ... }  // ②
}
  1. Class-level annotation applies to every @Action in this agent.
  2. Both extractTopic (the intermediate step) and produceDigest (the goal action) require news:read — without class-level security, intermediate actions run freely before the goal action’s check fires, potentially burning LLM tokens on an unauthorised request.

See @SecureAgentTool for the full annotation reference including supported SpEL expressions and method-level override behaviour.

Dependency
<dependency>
    <groupId>com.embabel.agent</groupId>
    <artifactId>embabel-agent-starter-mcpserver-security</artifactId>
    <version>$\{embabel-agent.version}</version>
</dependency>

The starter auto-configures SecureAgentToolAspect and wires the Spring Security MethodSecurityExpressionHandler. No additional @EnableMethodSecurity is required.

Consuming

Embabel Agent can consume external MCP servers as tool sources, automatically organizing them into Tool Groups that agents can use.

Docker Tools Integration
Configuration Approaches
  • Docker MCP Gateway (Recommended)
    Uses Docker Desktop’s MCP Toolkit extension as a single gateway to multiple tools:
spring:
  ai:
    mcp:
      client:
        type: SYNC
        stdio:
          connections:
            docker-mcp:
              command: docker
              args: [mcp, gateway, run]
  • Individual Containers
    Run each MCP server as a separate Docker container:
spring:
  ai:
    mcp:
      client:
        type: SYNC
        stdio:
          connections:
            brave-search-mcp:
              command: docker
              args: [run, -i, --rm, -e, BRAVE_API_KEY, mcp/brave-search]
              env:
                BRAVE_API_KEY: $\{BRAVE_API_KEY}
Available Tool Groups

Tool Groups are conditionally created based on configured MCP connections using @ConditionalOnMcpConnection:

Tool GroupRequired ConnectionsCapabilities
Web Toolsbrave-search-mcp, fetch-mcp, wikipedia-mcp, or docker-mcpWeb search, URL fetching, Wikipedia queries
Mapsgoogle-maps-mcp or docker-mcpGeocoding, directions, place search
Browser Automationpuppeteer-mcp or docker-mcpPage navigation, screenshots, form interaction
GitHubgithub-mcp or docker-mcpIssues, pull requests, comments
How It Works

The @ConditionalOnMcpConnection annotation checks for configured connections at startup:

@Bean
@ConditionalOnMcpConnection({"github-mcp", "docker-mcp"})  // ①
public ToolGroup githubToolsGroup() {
    return new McpToolGroup(
        CoreToolGroups.GITHUB_DESCRIPTION,
        "docker-github",
        mcpSyncClients,
        tool -> tool.toolDefinition().name().contains("create_issue")  // ②
    );
}
  1. Bean created if any listed connection is configured
  2. Filter selects which MCP tools belong to this group
Custom Tool Groups

Define custom groups via configuration properties:

embabel:
  agent:
    platform:
      tools:
        includes:
          my-tools:
            description: "Custom tool collection"
            provider: "MyOrg"
            tools:
              - tool_name_suffix

A2A

Observability

Embabel Agent provides a unified observability module that automatically traces agent lifecycle, actions, LLM calls, tool invocations, and more — with zero code changes. It integrates with any OpenTelemetry-compatible backend (Zipkin, Langfuse, Jaeger, Prometheus, etc.).

Setup

Add the observability starter to your pom.xml:

<dependency>
    <groupId>com.embabel.agent</groupId>
    <artifactId>embabel-agent-starter-observability</artifactId>
    <version>$\{embabel-agent.version}</version>
</dependency>

Then add an exporter dependency. For example, Zipkin:

<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>

Or Langfuse for LLM-focused observability:

<dependency>
    <groupId>com.quantpulsar</groupId>
    <artifactId>opentelemetry-exporter-langfuse</artifactId>
    <version>0.4.0</version>
</dependency>

Configuration

Enable observability and configure your exporter in application.yml:

embabel:
  observability:
    enabled: true
    service-name: my-agent-app

management:
  tracing:
    enabled: true
    sampling:
      probability: 1.0

  # Zipkin
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

For Langfuse:

management:
  langfuse:
    enabled: true
    endpoint: https://cloud.langfuse.com/api/public/otel  # or self-hosted URL
    public-key: pk-lf-...
    secret-key: sk-lf-...

What Gets Traced

All tracing is automatic once the module is on the classpath. The following events are captured as OpenTelemetry spans, organized in a parent-child hierarchy:

Agent: CustomerServiceAgent (trace root)
├── planning:formulated [iteration=1, actions=3]
├── Action: AnalyzeRequest
│   └── ChatModel: gpt-4 (Spring AI)
│       └── tool:searchKnowledgeBase
├── Action: GenerateResponse
│   └── ChatModel: gpt-4 (Spring AI)
├── goal:achieved [RequestProcessed]
└── status: completed [duration=2340ms]

Tracing Configuration Properties

All tracing options are enabled by default and can be toggled individually:

PropertyDefaultDescription
embabel.observability.enabledtrueMaster switch for observability
embabel.observability.service-nameembabel-agentService name in traces
embabel.observability.trace-agent-eventstrueAgent lifecycle (creation, execution, completion, failures)
embabel.observability.trace-tool-callstrueTool invocations with input/output
embabel.observability.trace-tool-looptrueTool loop execution
embabel.observability.trace-llm-callstrueLLM calls with token usage
embabel.observability.trace-planningtruePlanning and replanning iterations
embabel.observability.trace-state-transitionstrueWorkflow state changes
embabel.observability.trace-lifecycle-statestrueWAITING, PAUSED, STUCK states
embabel.observability.trace-ragtrueRAG events (request, response, pipeline)
embabel.observability.trace-rankingtrueRanking/selection events (agent routing)
embabel.observability.trace-dynamic-agent-creationtrueDynamic agent creation events
embabel.observability.trace-http-detailsfalseHTTP request/response details (bodies, headers)
embabel.observability.trace-tracked-operationstrue@Tracked annotation aspect
embabel.observability.mdc-propagationtruePropagate agent context into SLF4J MDC
embabel.observability.metrics-enabledtrueMicrometer business metrics (counters, gauges)
embabel.observability.max-attribute-length4000Max span attribute length before truncation

Custom Operation Tracking with @Tracked

The @Tracked annotation lets you add observability spans to your own methods. Inputs, outputs, duration, and errors are captured automatically.

@Tracked("enrichCustomer")
public Customer enrich(Customer input) {
    // Automatically creates a span with method arguments and return value
}

You can specify a type and description for richer traces:

@Tracked(
    value = "callPaymentApi",
    type = TrackType.EXTERNAL_CALL,
    description = "Payment gateway call"
)
public PaymentResult processPayment(Order order) {
    // ...
}

Available track types:

TypeDescription
CUSTOMGeneral-purpose (default)
PROCESSINGData processing operation
VALIDATIONValidation or verification step
TRANSFORMATIONData transformation
EXTERNAL_CALLExternal service/API call
COMPUTATIONComputation or calculation

When called within an agent execution, @Tracked spans are automatically nested under the current action:

Agent: CustomerServiceAgent
├── Action: ProcessOrder
│   ├── @Tracked: enrichCustomer (PROCESSING)
│   ├── ChatModel: gpt-4
│   └── @Tracked: callPaymentApi (EXTERNAL_CALL)
└── status: completed

Internal method calls within the same class are not intercepted. Extract tracked methods into a separate @Component bean for the annotation to work.

MDC Log Correlation

Agent context is automatically propagated into SLF4J MDC, enabling log filtering by agent run or action.

MDC keys set automatically:

MDC KeyDescriptionSet onRemoved on
embabel.agent.run_idAgent process IDAgent creationAgent completed/failed/killed
embabel.agent.nameAgent nameAgent creationAgent completed/failed/killed
embabel.action.nameCurrent action nameAction startAction result

Example Logback pattern:

<pattern>%d\{HH:mm:ss.SSS} [%thread] %-5level %logger\{36} [runId=%X\{embabel.agent.run_id} agent=%X\{embabel.agent.name} action=%X\{embabel.action.name}] - %msg%n</pattern>

This produces logs like:

14:23:45.123 [main] INFO  c.e.MyService [runId=abc-123 agent=CustomerServiceAgent action=AnalyzeRequest] - Processing request

To disable MDC propagation:

embabel:
  observability:
    mdc-propagation: false

Supported Backends

BackendTypeModule
LangfuseTracesopentelemetry-exporter-langfuse
ZipkinTracesopentelemetry-exporter-zipkin
OTLP (Jaeger, Tempo)Tracesopentelemetry-exporter-otlp
PrometheusMetricsmicrometer-registry-prometheus

For full details, see the Observability Module Documentation.

Was this page helpful?

Share