Skip to content

Commit 08447ae

Browse files
authored
Update User Guide with Mcp Server Security content (#1535)
* Update User Guide with Mcp Server Security content * Update McpServer directory loation for KDoc compiler * Polish docs * Review feedback: broaden NOTE to link both Spring Security JWT docs and MCP-specific example
1 parent f4635ba commit 08447ae

File tree

4 files changed

+312
-2
lines changed

4 files changed

+312
-2
lines changed

embabel-agent-docs/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,8 @@
205205
<dir>${project.parent.basedir}/embabel-agent-autoconfigure/embabel-agent-platform-autoconfigure/src/main/java</dir>
206206
<dir>${project.parent.basedir}/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/java</dir>
207207
<dir>${project.parent.basedir}/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/kotlin</dir>
208-
<dir>${project.parent.basedir}/embabel-agent-mcpserver/src/main/kotlin</dir>
208+
<dir>${project.parent.basedir}/embabel-agent-mcp/embabel-agent-mcpserver/src/main/kotlin</dir>
209+
<dir>${project.parent.basedir}/embabel-agent-mcp/embabel-agent-mcp-security/src/main/kotlin</dir>
209210
<dir>${project.parent.basedir}/embabel-agent-rag/embabel-agent-rag-lucene/src/main/kotlin</dir>
210211
<dir>${project.parent.basedir}/embabel-agent-shell/src/main/kotlin</dir>
211212
</sourceDirectories>

embabel-agent-docs/src/main/asciidoc/reference/annotations/page.adoc

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,166 @@ The only special thing about it is its ability to use the `OperationContext` par
10231023

10241024
The `@AchievesGoal` annotation can be added to an `@Action` method to indicate that the completion of the action achieves a specific goal.
10251025

1026+
[[reference.annotations_secure_agent_tool]]
1027+
==== The `@SecureAgentTool` annotation
1028+
1029+
`@SecureAgentTool` declares the security contract for an Embabel `@Action` method or `@Agent`
1030+
class exposed as a remote MCP tool.
1031+
It accepts a Spring Security SpEL expression evaluated against the current `Authentication`
1032+
at the point of tool invocation, before Embabel's GOAP planner executes the action body.
1033+
1034+
===== Placement
1035+
1036+
`@SecureAgentTool` can be placed on the `@Agent` class to protect every `@Action` uniformly,
1037+
or on individual methods for finer-grained control.
1038+
Method-level annotation takes precedence over class-level when both are present.
1039+
1040+
**Class-level** — one annotation secures all actions in the agent, including intermediate steps
1041+
that run before the goal-achieving action:
1042+
1043+
[tabs]
1044+
====
1045+
Kotlin::
1046+
+
1047+
[source,kotlin]
1048+
----
1049+
@Agent(description = "Research a topic and return a news digest")
1050+
@SecureAgentTool("hasAuthority('news:read')") // <1>
1051+
class NewsDigestAgent {
1052+
1053+
@Action
1054+
fun extractTopic(userInput: UserInput, context: OperationContext): NewsTopic { ... } // <2>
1055+
1056+
@AchievesGoal(description = "Produce a curated news digest",
1057+
export = Export(remote = true, name = "newsDigest",
1058+
startingInputTypes = [UserInput::class]))
1059+
@Action
1060+
fun produceDigest(topic: NewsTopic, context: OperationContext): NewsDigest { ... } // <2>
1061+
}
1062+
----
1063+
1064+
Java::
1065+
+
1066+
[source,java]
1067+
----
1068+
@Agent(description = "Research a topic and return a news digest")
1069+
@SecureAgentTool("hasAuthority('news:read')") // <1>
1070+
public class NewsDigestAgent {
1071+
1072+
@Action
1073+
public NewsTopic extractTopic(UserInput userInput, OperationContext context) { ... } // <2>
1074+
1075+
@AchievesGoal(description = "Produce a curated news digest",
1076+
export = @Export(remote = true, name = "newsDigest",
1077+
startingInputTypes = {UserInput.class}))
1078+
@Action
1079+
public NewsDigest produceDigest(NewsTopic topic, OperationContext context) { ... } // <2>
1080+
}
1081+
----
1082+
====
1083+
<1> One annotation on the class protects every `@Action` in the agent.
1084+
<2> Both `extractTopic` and `produceDigest` require `news:read`.
1085+
Without class-level protection, intermediate actions like `extractTopic` would run freely
1086+
before the security check on the goal-achieving action fires.
1087+
1088+
**Method-level override** — a method-level annotation takes precedence over the class-level
1089+
expression, allowing one action to require elevated authority:
1090+
1091+
[tabs]
1092+
====
1093+
Kotlin::
1094+
+
1095+
[source,kotlin]
1096+
----
1097+
@Agent(description = "Market intelligence agent")
1098+
@SecureAgentTool("hasAuthority('market:read')") // <1>
1099+
class MarketIntelligenceAgent {
1100+
1101+
@Action
1102+
fun gatherIntelligence(subject: AnalysisSubject, context: OperationContext): String { ... }
1103+
1104+
@SecureAgentTool("hasAuthority('market:admin')") // <2>
1105+
@AchievesGoal(description = "Produce market report")
1106+
@Action
1107+
fun synthesiseReport(
1108+
subject: AnalysisSubject,
1109+
rawIntelligence: String,
1110+
context: OperationContext
1111+
): MarketIntelligenceReport { ... }
1112+
}
1113+
----
1114+
1115+
Java::
1116+
+
1117+
[source,java]
1118+
----
1119+
@Agent(description = "Market intelligence agent")
1120+
@SecureAgentTool("hasAuthority('market:read')") // <1>
1121+
public class MarketIntelligenceAgent {
1122+
1123+
@Action
1124+
public String gatherIntelligence(AnalysisSubject subject, OperationContext context) { ... }
1125+
1126+
@SecureAgentTool("hasAuthority('market:admin')") // <2>
1127+
@AchievesGoal(description = "Produce market report")
1128+
@Action
1129+
public MarketIntelligenceReport synthesiseReport(
1130+
AnalysisSubject subject,
1131+
String rawIntelligence,
1132+
OperationContext context) { ... }
1133+
}
1134+
----
1135+
====
1136+
<1> All actions default to requiring `market:read`.
1137+
<2> `synthesiseReport` requires `market:admin` — the method-level annotation overrides the class.
1138+
1139+
===== Supported expressions
1140+
1141+
Any Spring Security SpEL expression is valid:
1142+
1143+
[cols="2,3",options="header"]
1144+
|===
1145+
|Expression |Meaning
1146+
1147+
|`hasAuthority('finance:read')`
1148+
|Principal must carry this exact authority
1149+
1150+
|`hasAnyAuthority('finance:read', 'finance:admin')`
1151+
|Principal must carry at least one of the listed authorities
1152+
1153+
|`hasRole('ADMIN')`
1154+
|Principal must carry `ROLE_ADMIN` (the `ROLE_` prefix is added automatically)
1155+
1156+
|`isAuthenticated()`
1157+
|Any authenticated principal, regardless of authorities
1158+
1159+
|`hasAuthority('payments:write') and #request.amount < 10000`
1160+
|Combines an authority check with a method parameter expression
1161+
|===
1162+
1163+
===== Setup
1164+
1165+
Add the MCP security starter to your `pom.xml`:
1166+
1167+
[source,xml]
1168+
----
1169+
<dependency>
1170+
<groupId>com.embabel.agent</groupId>
1171+
<artifactId>embabel-agent-starter-mcpserver-security</artifactId>
1172+
<version>${embabel-agent.version}</version>
1173+
</dependency>
1174+
----
1175+
1176+
The starter auto-configures `SecureAgentToolAspect` and the required Spring Security
1177+
`MethodSecurityExpressionHandler`.
1178+
No additional `@EnableMethodSecurity` annotation is required.
1179+
1180+
NOTE: `@SecureAgentTool` is a method-level security control, not an HTTP-level one.
1181+
For production use, combine it with a `SecurityFilterChain` that validates JWT Bearer tokens
1182+
so unauthenticated requests are rejected before reaching the GOAP planner.
1183+
See the https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html[Spring Security JWT Resource Server documentation] for general setup,
1184+
or xref:reference.integrations__mcp_security[MCP Security] for an MCP-specific example.
1185+
10261186
==== Implementing the `StuckHandler` interface
10271187

10281188
If an annotated agent class implements the `StuckHandler` interface, it can handle situations where an action is stuck itself.

embabel-agent-docs/src/main/asciidoc/reference/guardrails/page.adoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,4 +472,12 @@ In summary, guardrails and bean validators are complementary but distinct:
472472
- **Bean validation** ensures objects are well-formed and meet business constraints
473473
- **Guardrails** ensure AI interactions are safe and compliant with policies
474474

475-
Both can be enabled independently and serve different aspects of the AI safety stack.
475+
Both can be enabled independently and serve different aspects of the AI safety stack.
476+
477+
`@SecureAgentTool` is a third, orthogonal mechanism: it enforces _access control_ rather than
478+
content safety or data validity.
479+
Where guardrails ask "is this content acceptable?", `@SecureAgentTool` asks "is this principal
480+
allowed to invoke this agent action at all?"
481+
The two work well together — `@SecureAgentTool` prevents unauthorised principals from calling
482+
a tool, while guardrails validate the inputs and outputs of calls that are permitted.
483+
See xref:reference.annotations_secure_agent_tool[`@SecureAgentTool`] for details.

embabel-agent-docs/src/main/asciidoc/reference/integrations/page.adoc

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,147 @@ Every MCP server includes a built-in `helloBanner` tool that displays server inf
503503
}
504504
----
505505

506+
[[reference.integrations__mcp_security]]
507+
===== Security
508+
509+
Embabel MCP servers support two complementary layers of security that work together.
510+
Think of them like a building with a reception desk and locked office doors: the HTTP filter
511+
chain is the reception desk that turns away anyone without a badge, and `@SecureAgentTool`
512+
is the locked door on each individual office that checks what the badge actually permits.
513+
514+
====== Layer 1 — HTTP transport (filter chain)
515+
516+
All requests to MCP endpoints (`/sse/**`, `/mcp/**`, `/message/**`) must carry a valid JWT
517+
Bearer token or they are rejected with `401 Unauthorized` before the GOAP planner is invoked.
518+
519+
Configure a `SecurityFilterChain` and a JWT resource server in your Spring Security setup:
520+
521+
[tabs]
522+
====
523+
Kotlin::
524+
+
525+
[source,kotlin]
526+
----
527+
@Configuration
528+
@EnableWebSecurity
529+
class McpSecurityConfiguration {
530+
531+
@Bean
532+
fun mcpFilterChain(http: HttpSecurity): SecurityFilterChain {
533+
http
534+
.securityMatcher("/sse/**", "/mcp/**", "/message/**")
535+
.authorizeHttpRequests { it.anyRequest().authenticated() }
536+
.sessionManagement {
537+
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
538+
}
539+
.oauth2ResourceServer { oauth2 ->
540+
oauth2.jwt { jwt ->
541+
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
542+
}
543+
}
544+
.csrf { it.disable() }
545+
return http.build()
546+
}
547+
548+
@Bean
549+
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
550+
val authoritiesConverter = JwtGrantedAuthoritiesConverter().apply {
551+
setAuthoritiesClaimName("authorities")
552+
setAuthorityPrefix("") // <1>
553+
}
554+
return JwtAuthenticationConverter().apply {
555+
setJwtGrantedAuthoritiesConverter(authoritiesConverter)
556+
}
557+
}
558+
}
559+
----
560+
====
561+
<1> Empty prefix means JWT claim values like `news:read` map directly to Spring Security
562+
authorities, so `hasAuthority('news:read')` in a `@SecureAgentTool` expression works without
563+
any `SCOPE_` prefix.
564+
565+
Configure JWT validation in `application.yml`:
566+
567+
[source,yaml]
568+
----
569+
spring:
570+
security:
571+
oauth2:
572+
resourceserver:
573+
jwt:
574+
public-key-location: classpath:keys/public.pem # local dev
575+
jws-algorithms: RS256
576+
# For production, use issuer-uri or jwk-set-uri instead
577+
----
578+
579+
====== Layer 2 — Method-level (`@SecureAgentTool`)
580+
581+
Enforces per-action authorization inside the GOAP execution pipeline, after the HTTP layer
582+
has validated the token.
583+
Place `@SecureAgentTool` on the `@Agent` class to protect every `@Action` in that agent:
584+
585+
[tabs]
586+
====
587+
Kotlin::
588+
+
589+
[source,kotlin]
590+
----
591+
@Agent(description = "Curated news digest agent")
592+
@SecureAgentTool("hasAuthority('news:read')") // <1>
593+
class NewsDigestAgent {
594+
595+
@Action
596+
fun extractTopic(userInput: UserInput, context: OperationContext): NewsTopic { ... } // <2>
597+
598+
@AchievesGoal(description = "Produce news digest",
599+
export = Export(remote = true, name = "newsDigest",
600+
startingInputTypes = [UserInput::class]))
601+
@Action
602+
fun produceDigest(topic: NewsTopic, context: OperationContext): NewsDigest { ... } // <2>
603+
}
604+
----
605+
606+
Java::
607+
+
608+
[source,java]
609+
----
610+
@Agent(description = "Curated news digest agent")
611+
@SecureAgentTool("hasAuthority('news:read')") // <1>
612+
public class NewsDigestAgent {
613+
614+
@Action
615+
public NewsTopic extractTopic(UserInput userInput, OperationContext context) { ... } // <2>
616+
617+
@AchievesGoal(description = "Produce news digest",
618+
export = @Export(remote = true, name = "newsDigest",
619+
startingInputTypes = {UserInput.class}))
620+
@Action
621+
public NewsDigest produceDigest(NewsTopic topic, OperationContext context) { ... } // <2>
622+
}
623+
----
624+
====
625+
<1> Class-level annotation applies to every `@Action` in this agent.
626+
<2> Both `extractTopic` (the intermediate step) and `produceDigest` (the goal action) require
627+
`news:read` — without class-level security, intermediate actions run freely before the goal
628+
action's check fires, potentially burning LLM tokens on an unauthorised request.
629+
630+
See xref:reference.annotations_secure_agent_tool[`@SecureAgentTool`] for the full annotation
631+
reference including supported SpEL expressions and method-level override behaviour.
632+
633+
====== Dependency
634+
635+
[source,xml]
636+
----
637+
<dependency>
638+
<groupId>com.embabel.agent</groupId>
639+
<artifactId>embabel-agent-starter-mcpserver-security</artifactId>
640+
<version>${embabel-agent.version}</version>
641+
</dependency>
642+
----
643+
644+
The starter auto-configures `SecureAgentToolAspect` and wires the Spring Security
645+
`MethodSecurityExpressionHandler`. No additional `@EnableMethodSecurity` is required.
646+
506647
[[reference.integrations__mcp_consuming]]
507648
===== Consuming
508649

0 commit comments

Comments
 (0)