Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2024-2026 Embabel Pty Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.example.secured

import com.embabel.agent.domain.library.HasContent
import com.fasterxml.jackson.annotation.JsonClassDescription
import com.fasterxml.jackson.annotation.JsonPropertyDescription

/**
* The subject of a market analysis request, capturing the entity or sector
* to be analysed and the geographic scope of the report.
*/
@JsonClassDescription("Subject of market analysis")
data class AnalysisSubject(
@get:JsonPropertyDescription("Company name, sector, or market segment to analyse")
val subject: String,
@get:JsonPropertyDescription("Geographic region of focus, e.g. US, EU, APAC, or Global")
val region: String = "Global",
)

/**
* A single observation about a competitor or comparable entity identified
* during the competitive landscape analysis.
*/
@JsonClassDescription("Key player in the competitive landscape")
data class CompetitorInsight(
@get:JsonPropertyDescription("Name of the competitor or comparable entity")
val name: String,
@get:JsonPropertyDescription("Notable recent development or positioning")
val insight: String,
)

/**
* Container for a list of [CompetitorInsight] items.
*
* Wraps `List<CompetitorInsight>` as a named type to avoid generic type erasure
* during LLM-driven JSON deserialisation.
*/
@JsonClassDescription("A list of competitor insights")
data class CompetitorInsightList(
val items: List<CompetitorInsight>,
)

/**
* A single entry in a SWOT analysis, classifying an observation as a
* Strength, Weakness, Opportunity, or Threat.
*/
@JsonClassDescription("A single SWOT observation")
data class SwotEntry(
@get:JsonPropertyDescription("One of: Strength, Weakness, Opportunity, Threat")
val category: String,
@get:JsonPropertyDescription("Concise description of this SWOT item")
val description: String,
)

/**
* Container for a list of [SwotEntry] items.
*
* Wraps `List<SwotEntry>` as a named type to avoid generic type erasure
* during LLM-driven JSON deserialisation.
*/
@JsonClassDescription("A list of SWOT entries")
data class SwotEntryList(
val items: List<SwotEntry>,
)

/**
* Container for a list of key market trend statements.
*
* Wraps `List<String>` as a named type to avoid generic type erasure
* during LLM-driven JSON deserialisation.
*/
@JsonClassDescription("A list of key trend statements")
data class KeyTrendList(
val items: List<String>,
)

/**
* A structured market intelligence report, containing an executive summary,
* SWOT analysis, competitive landscape, and key trend observations for a
* given subject and region.
*
* Implements [HasContent] so the report can be consumed by downstream agents
* or exported as a content asset.
*/
@JsonClassDescription("Market intelligence report")
data class MarketIntelligenceReport(
/** Company name, sector, or market segment that was analysed. */
val subject: String,
/** Geographic scope of the report (e.g. `US`, `EU`, `Global`). */
val region: String,
/** Four-sentence executive summary suitable for a senior decision-maker. */
val executiveSummary: String,
/** SWOT analysis entries, 2–3 items per category. */
val swot: List<SwotEntry>,
/** Top competitors or comparable entities with strategic positioning notes. */
val competitors: List<CompetitorInsight>,
/** Five most significant market trends, each expressed as a single sentence. */
val keyTrends: List<String>,
override val content: String,
) : HasContent
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2024-2026 Embabel Pty Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.example.secured

import com.embabel.agent.domain.library.HasContent
import com.fasterxml.jackson.annotation.JsonClassDescription
import com.fasterxml.jackson.annotation.JsonPropertyDescription

/**
* A single news item within a [NewsDigest], carrying a headline, summary, and source URL.
*/
@JsonClassDescription("A single news item in a digest")
data class DigestItem(
@get:JsonPropertyDescription("Headline of the news item")
val headline: String,
@get:JsonPropertyDescription("Brief summary of the news item")
val summary: String,
@get:JsonPropertyDescription("Source URL")
val url: String,
)

/**
* Container for a list of [DigestItem] values.
*
* Wraps `List<DigestItem>` as a named type to avoid generic type erasure during
* LLM-driven JSON deserialisation. Using `createObject<List<DigestItem>>` loses
* the type parameter at runtime and Jackson returns `List<LinkedHashMap>`;
* wrapping in a named class preserves the element type.
*/
@JsonClassDescription("A list of news digest items")
data class DigestItemList(
@get:JsonPropertyDescription("The list of news items")
val items: List<DigestItem>,
)

/**
* A research topic extracted from freeform user input, optionally narrowed
* to a specific focus area within that topic.
*/
@JsonClassDescription("A research topic extracted from user input")
data class NewsTopic(
@get:JsonPropertyDescription("The topic to research")
val topic: String,
@get:JsonPropertyDescription("Optional focus area within the topic")
val focusArea: String = "",
)

/**
* A curated news digest produced by a news digest agent, containing a list of
* [DigestItem] entries and a short editorial narrative.
*
* Implements [HasContent] so the digest can be consumed by downstream agents
* or exported as a content asset.
*/
@JsonClassDescription("A curated news digest for a given topic")
data class NewsDigest(
/** The topic that was researched. */
val topic: String,
/** The curated list of news items. */
val items: List<DigestItem>,
override val content: String,
) : HasContent
4 changes: 4 additions & 0 deletions examples-common/src/main/resources/keys/payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"sub": "test-user",
"authorities": ["news:read", "market:admin"]
}
28 changes: 28 additions & 0 deletions examples-common/src/main/resources/keys/private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCsZmpt0WUccPig
kg+iOnDELbjAnJiAuCjBm01jS2bcYDgrSvblKhAucy3vGuQLWp3jaZvnvEA1Dfx7
Qb0Wk/0eWlPCTVClThg6mFIBPoIzsiW/YFPUQUWu7Rb98Cn7XnuQyKrYBThxUF8p
lo2fqEyb2TCPddfWffcPIFGBE+hlenJWSDt8bSnWObh/uQB57mlW1stA9BFEbAAt
SCOTjjXVlrHu/GjPN+yvOuXZUbOTlz5rBQ73yh2+N8IGmOatM8Zy9OeqROU16MUO
FXpWLLozDnORV5GHDxu5t1VBHuDZj+Nr6eLlqF5XG9TghjQtpZQjdw+FytBiPF8x
8it5yOzJAgMBAAECggEAIk4q5XliZltwjfsik0SPxenXNrSygAKlUYlGx/hsGnQn
GS5MnONW3vdab5bpQ/0fJ+6O8Hj8SdJjqF83cjO2jYCp5CMgZhR5K8e9obO/kuvj
KQbRB46JgDp7dcKBiByP+AqGPIAlcB7vIMZtnhSOUgDQ8esgLPVZ4S+sKEQ+3VpA
7O5+fSVA70BZPPmhDVfhDziKSVbk2xRtMBZXE7mxqpvvHiPGmaCcRxQ3Wvh4dbzp
1o4OlQEM7UREoemIWhqiQD+0pLQOMwivL5waKPC6uyYJREaNIdFKWHlX1Dv5UmKV
LG0MyWCRH+86KU884hbXEYUKcr2qLcbgiCTzdGXW1QKBgQDvdO1HOsxwePWqN6Zq
XhfjlqcwxatEvstpd+LvEUAG3JkOGwgDbBxmbwc9uyz4XGXUm0tW+h3ioRvmSYaN
q4s4CfaDMPt055B6F9+5xRzfnLUF2iKerJSegqgP4FY5GT1VIuHNbaDUcWNLl+sL
iywSz7M46W6Q9mqZ603fruX1rwKBgQC4T4NJTWQ4vG2up9AJ7bo/LW/dB+7AHbfT
alF+M0Ifa12FKfAaIstRBO2MTD0zs+/WJHPK5M7rT1bUfcy2po3VFPTzzpE1lvrJ
9vNEwcRhpZZLVjJtjkjcOxcJ6AB5JENAtK4nILnj/bPIqN2fm6sWpgipwnXyDJJW
t5XqM/ZbBwKBgDeg6VKn5UCnySKPJRkG3PPhVtvsv/oLfQP2dwDk+APgEaqGOxlY
b0yYtIzxw+O6y2lV3m3JU1IH7YxVPm4BESmmnt8hbPlepmDG1RG6KhgEtsUyROKp
4yjj35k4m69OHsJ9hQmNU1SnNtMj6tXWshl4qQEqsaX5qRAy6vdxETk/AoGAFRkV
ADkZtdzr+UhAakKKRV0gJv8Xd2pvdoX/GiEn+ozBwdKRJ4zAFWApCRxZDSgV1j+J
+bnDbfBBEkXsPyRFrBSgDaChwq6rup7ILiC4KC1aTPlrAFt/P6aL9phWJYffIvML
1HLMlKqf/+JErFaXeRo8d6JI+hIyVjbhtFOKadUCgYEAsvBIHf0wTdUgwWZFosuG
3aede28L8UEG6iXTlz0V1ZiFdNkKdOQpIaWlHETxEKPbd41ewArknGyA2fOY+5ao
UFp9L53l25FJxBB2jpYhUNK+F2cMLNE5f2E8d+qvOdjTdHiB2a4JWVwU/vQdeoYc
0n6N0lW2xA5HptnkARPkW9A=
-----END PRIVATE KEY-----
28 changes: 28 additions & 0 deletions examples-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,34 @@
</plugins>
</build>
</profile>
<profile>
<id>enable-secured-agent-mcp-server</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-starter-mcpserver</artifactId>
</dependency>
<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-starter-mcpserver-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.embabel.example.JavaAgentSecuredMcpServerApplication</mainClass>
</configuration>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>enable-agent-mcp-server</id>
<activation>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2024-2026 Embabel Pty Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.example;

import com.embabel.example.common.support.McpServers;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

/**
* Spring Boot application that runs the secured Embabel agent MCP server.
*
* <p>Extends the base MCP server with two additional security layers:
* <ul>
* <li><b>HTTP layer:</b> JWT Bearer token required on all {@code /sse/**} and
* {@code /mcp/**} requests, enforced by {@code SecuredAgentSecurityConfiguration}
* with {@code spring-security-oauth2-resource-server}.</li>
* <li><b>Method layer:</b> {@code @SecureAgentTool} on each {@code @AchievesGoal} action
* enforces per-tool authority checks via {@code SecureAgentToolConfiguration} and
* {@code SecureAgentToolAspect}.</li>
* </ul>
*
* <h2>Agents exposed</h2>
* <table>
* <tr><th>Agent</th><th>Required authority</th></tr>
* <tr><td>{@code NewsDigestAgent}</td><td>{@code news:read}</td></tr>
* <tr><td>{@code MarketIntelligenceAgent}</td><td>{@code market:admin}</td></tr>
* </table>
*
* <h2>OAuth2 configuration</h2>
* <p>Configure your issuer URI in {@code application-secured.yml}:
* <pre>{@code
* spring:
* security:
* oauth2:
* resourceserver:
* jwt:
* issuer-uri: https://your-idp.example.com
* }</pre>
*
* <p>MCP clients must pass a signed JWT carrying the required authorities:
* <pre>{@code
* Authorization: Bearer <signed-jwt-with-required-authorities>
* }</pre>
* Use {@code McpSyncHttpClientRequestCustomizer} on the client side to attach the header.
*
* @see com.embabel.agent.config.mcpserver.security.SecuredAgentSecurityConfiguration
* @see com.embabel.agent.config.mcpserver.security.SecureAgentToolConfiguration
*/
@SpringBootApplication
@ConfigurationPropertiesScan(
basePackages = {
"com.embabel.example"
}
)
public class JavaAgentSecuredMcpServerApplication {

/**
* Application entry point.
*
* <p>Activates the {@link McpServers#DOCKER}, {@link McpServers#DOCKER_DESKTOP},
* and {@code secured} profiles, enabling the Docker-based MCP transport and
* JWT security configuration.
*
* @param args command-line arguments forwarded to Spring Boot
*/
public static void main(String[] args) {
SpringApplication app = new SpringApplication(JavaAgentSecuredMcpServerApplication.class);
app.setAdditionalProfiles(
McpServers.DOCKER,
McpServers.DOCKER_DESKTOP,
McpServers.SECURED_PROFILE
);
app.run(args);
}
}
Loading
Loading