Integrations
Model Context Protocol (MCP)
Section titled “Model Context Protocol (MCP)”Publishing
Section titled “Publishing”Overview
Section titled “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
Section titled “Server Configuration”Configure MCP server functionality in your application.yml.
The server type determines the execution mode:
spring: ai: mcp: server: type: SYNC # or ASYNCServer Types
Section titled “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: SYNCASYNC 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: ASYNCTransport Protocol
Section titled “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/sseThen connect your client to http://localhost:8000.
Automatic Publishing
Section titled “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
Section titled “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"; }}@Agent( goal = "Provide weather information", backstory = "Weather service agent")class WeatherAgent {
@Goal @Export(remote = true) // Automatically becomes MCP tool fun getWeather( @Param("location") location: String, @Param("units") units: String ): String { return "Weather for $location in $units" }
@Goal fun internalMethod(): String { // Not exposed to MCP (no @Export annotation) return "Internal use only" }}Exposing Embabel ToolObject and LlmReference types as tools
Section titled “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:
@Configurationpublic class RagMcpTools {
@Bean McpToolExport ragTools( (1) SearchOperations searchOperations) { var toolishRag = new ToolishRag( "docs", "Embabel docs", searchOperations ); return McpToolExport.fromLlmReference(toolishRag); (2) }}@Configurationclass RagMcpTools {
@Bean fun ragTools( (1) searchOperations: SearchOperations ): McpToolExport { val toolishRag = ToolishRag( "docs", "Embabel docs", searchOperations ) return McpToolExport.fromLlmReference(toolishRag) (2) }}- Your bean should be of type
McpToolExport - Use
McpToolExport.fromLlmReferenceto return the instance
Naming Strategies
Section titled “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:
@Beanpublic McpToolExport prefixedTools() { return McpToolExport.fromToolObject( new ToolObject( List.of(myToolInstance), name -> "myservice_" + name (1) ) );}@Beanfun prefixedTools(): McpToolExport { return McpToolExport.fromToolObject( ToolObject( objects = listOf(myToolInstance), namingStrategy = { "myservice_$it" } (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"// Reference named "WeatherService" will prefix tools with "weatherservice_"val reference = MyWeatherReference() // name = "WeatherService"McpToolExport.fromLlmReference(reference)// Tool "getWeather" becomes "weatherservice_getWeather"Exporting multiple sources with different prefixes:
@Beanpublic McpToolExport multiSourceTools() { return McpToolExport.fromToolObjects( List.of( new ToolObject( List.of(weatherTools), name -> "weather_" + name ), new ToolObject( List.of(stockTools), name -> "stocks_" + name ) ) );}@Beanfun multiSourceTools(): McpToolExport { return McpToolExport.fromToolObjects( listOf( ToolObject( objects = listOf(weatherTools), namingStrategy = { "weather_$it" } ), ToolObject( objects = listOf(stockTools), namingStrategy = { "stocks_$it" } ) ) )}Filtering Tools
Section titled “Filtering Tools”You can filter which tools are exported using the filter property on ToolObject:
@Beanpublic McpToolExport filteredTools() { return McpToolExport.fromToolObject( new ToolObject( List.of(myToolInstance), StringTransformer.IDENTITY, name -> name.startsWith("public_") (1) ) );}@Beanfun filteredTools(): McpToolExport { return McpToolExport.fromToolObject( ToolObject( objects = listOf(myToolInstance), filter = { it.startsWith("public_") } (1) ) )}- Only tools whose names start with
public_will be exported
You can combine naming strategies and filters:
@Beanpublic McpToolExport combinedTools() { return McpToolExport.fromToolObject( new ToolObject( List.of(myToolInstance), name -> "api_" + name, name -> !name.startsWith("internal") (1) ) );}@Beanfun combinedTools(): McpToolExport { return McpToolExport.fromToolObject( ToolObject( objects = listOf(myToolInstance), namingStrategy = { "api_$it" }, filter = { !it.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
Section titled “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:
@Componentpublic 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; }}@Componentclass CalculatorTools {
@McpTool(name = "add", description = "Add two numbers together") fun add( @McpToolParam(description = "First number", required = true) a: Int, @McpToolParam(description = "Second number", required = true) b: Int ): Int { return a + b }
@McpTool(name = "multiply", description = "Multiply two numbers") fun multiply( @McpToolParam(description = "First number", required = true) x: Double, @McpToolParam(description = "Second number", required = true) y: Double ): Double { 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
Section titled “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
Section titled “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
Section titled “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)
Section titled “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@EnableWebSecurityclass 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("") (1) } 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 insteadLayer 2 — Method-level (@SecureAgentTool)
Section titled “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')") (1)class NewsDigestAgent {
@Action fun extractTopic(userInput: UserInput, context: OperationContext): NewsTopic { ... } (2)
@AchievesGoal(description = "Produce news digest", export = Export(remote = true, name = "newsDigest", startingInputTypes = [UserInput::class])) @Action fun produceDigest(topic: NewsTopic, context: OperationContext): NewsDigest { ... } (2)}@Agent(description = "Curated news digest agent")@SecureAgentTool("hasAuthority('news:read')") (1)public class NewsDigestAgent {
@Action public NewsTopic extractTopic(UserInput userInput, OperationContext context) { ... } (2)
@AchievesGoal(description = "Produce news digest", export = @Export(remote = true, name = "newsDigest", startingInputTypes = {UserInput.class})) @Action public NewsDigest produceDigest(NewsTopic topic, OperationContext context) { ... } (2)}- 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
Section titled “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
Section titled “Consuming”Embabel Agent can consume external MCP servers as tool sources, automatically organizing them into Tool Groups that agents can use.
Docker Tools Integration
Section titled “Docker Tools Integration”Configuration Approaches
Section titled “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
Section titled “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
Section titled “How It Works”The @ConditionalOnMcpConnection annotation checks for configured connections at startup:
@Bean@ConditionalOnMcpConnection({"github-mcp", "docker-mcp"}) (1)public ToolGroup githubToolsGroup() { return new McpToolGroup( CoreToolGroups.GITHUB_DESCRIPTION, "docker-github", mcpSyncClients, tool -> tool.toolDefinition().name().contains("create_issue") (2) );}@Bean@ConditionalOnMcpConnection("github-mcp", "docker-mcp") (1)fun githubToolsGroup(): ToolGroup { return McpToolGroup( description = CoreToolGroups.GITHUB_DESCRIPTION, name = "docker-github", clients = mcpSyncClients, filter = { it.toolDefinition.name().contains("create_issue") } (2) )}- Bean created if any listed connection is configured
- Filter selects which MCP tools belong to this group
Custom Tool Groups
Section titled “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_suffixObservability
Section titled “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.).
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
Section titled “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/spansFor 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
Section titled “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
Section titled “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
Section titled “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: completedMDC Log Correlation
Section titled “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 requestTo disable MDC propagation:
embabel: observability: mdc-propagation: falseSupported Backends
Section titled “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.