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 themcpoproxy 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). ThePerGoalMcpToolExportCallbackPublisherautomatically discovers and exposes these goals without any additional configuration. - Prompts
Prompts are automatically generated for each goal’s starting input types through thePerGoalStartingInputTypesPromptPublisher. 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); // ②
}
}
- Your bean should be of type
McpToolExport - Use
McpToolExport.fromLlmReferenceto 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 // ①
)
);
}
- 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_") // ①
)
);
}
- 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") // ①
)
);
}
- 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
AbstractMcpServerConfigurationprovides 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)
}
}
}
- Empty prefix means JWT claim values like
news:readmap directly to Spring Security authorities, sohasAuthority('news:read')in a@SecureAgentToolexpression works without anySCOPE_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 { ... } // ②
}
- Class-level annotation applies to every
@Actionin this agent. - Both
extractTopic(the intermediate step) andproduceDigest(the goal action) requirenews: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 Group | Required Connections | Capabilities |
|---|---|---|
| Web Tools | brave-search-mcp, fetch-mcp, wikipedia-mcp, or docker-mcp | Web search, URL fetching, Wikipedia queries |
| Maps | google-maps-mcp or docker-mcp | Geocoding, directions, place search |
| Browser Automation | puppeteer-mcp or docker-mcp | Page navigation, screenshots, form interaction |
| GitHub | github-mcp or docker-mcp | Issues, 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") // ②
);
}
- Bean created if any listed connection is configured
- 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>
You can use multiple exporters simultaneously (e.g., Langfuse for traces + Prometheus for metrics).
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:
| Property | Default | Description |
|---|---|---|
embabel.observability.enabled | true | Master switch for observability |
embabel.observability.service-name | embabel-agent | Service name in traces |
embabel.observability.trace-agent-events | true | Agent lifecycle (creation, execution, completion, failures) |
embabel.observability.trace-tool-calls | true | Tool invocations with input/output |
embabel.observability.trace-tool-loop | true | Tool loop execution |
embabel.observability.trace-llm-calls | true | LLM calls with token usage |
embabel.observability.trace-planning | true | Planning and replanning iterations |
embabel.observability.trace-state-transitions | true | Workflow state changes |
embabel.observability.trace-lifecycle-states | true | WAITING, PAUSED, STUCK states |
embabel.observability.trace-rag | true | RAG events (request, response, pipeline) |
embabel.observability.trace-ranking | true | Ranking/selection events (agent routing) |
embabel.observability.trace-dynamic-agent-creation | true | Dynamic agent creation events |
embabel.observability.trace-http-details | false | HTTP request/response details (bodies, headers) |
embabel.observability.trace-tracked-operations | true | @Tracked annotation aspect |
embabel.observability.mdc-propagation | true | Propagate agent context into SLF4J MDC |
embabel.observability.metrics-enabled | true | Micrometer business metrics (counters, gauges) |
embabel.observability.max-attribute-length | 4000 | Max 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:
| Type | Description |
|---|---|
CUSTOM | General-purpose (default) |
PROCESSING | Data processing operation |
VALIDATION | Validation or verification step |
TRANSFORMATION | Data transformation |
EXTERNAL_CALL | External service/API call |
COMPUTATION | Computation 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
@Tracked uses Spring AOP proxies.
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 Key | Description | Set on | Removed on |
|---|---|---|---|
embabel.agent.run_id | Agent process ID | Agent creation | Agent completed/failed/killed |
embabel.agent.name | Agent name | Agent creation | Agent completed/failed/killed |
embabel.action.name | Current action name | Action start | Action 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
| Backend | Type | Module |
|---|---|---|
| Langfuse | Traces | opentelemetry-exporter-langfuse |
| Zipkin | Traces | opentelemetry-exporter-zipkin |
| OTLP (Jaeger, Tempo) | Traces | opentelemetry-exporter-otlp |
| Prometheus | Metrics | micrometer-registry-prometheus |
For full details, see the Observability Module Documentation.




