Writing Prompts¶
Prompts are reusable message templates that clients can invoke by name, optionally supplying arguments. A prompt is any Java method annotated with @McpPrompt on a Spring bean.
Defining a Prompt¶
Annotate methods with @McpPrompt and register the enclosing class as a Spring bean:
import com.callibrity.mocapi.api.prompts.McpPrompt;
import com.callibrity.mocapi.model.GetPromptResult;
import com.callibrity.mocapi.model.PromptMessage;
import com.callibrity.mocapi.model.Role;
import com.callibrity.mocapi.model.TextContent;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class SummarizationPrompts {
@McpPrompt(name = "summarize", description = "Summarize the provided text")
public GetPromptResult summarize(String text) {
return new GetPromptResult(
"Text summarization prompt",
List.of(new PromptMessage(
Role.USER,
new TextContent("Summarize the following:\n\n" + text, null))));
}
}
Any bean-hood mechanism works — @Component, @Service, or a @Bean factory method. The framework scans every bean for @McpPrompt methods and registers one handler per annotated method.
Each registered prompt (and any enum-typed argument's completion candidates) is logged at INFO level during startup. See Startup Logging for the full catalog.
Prompt Method Basics¶
A @McpPrompt method always returns GetPromptResult. Method parameters bind to the incoming prompt arguments -- each parameter name matches an argument key.
@McpPrompt(name = "translate", description = "Translate text to a target language")
public GetPromptResult translate(String text, String targetLanguage) {
return new GetPromptResult(
"Translation prompt",
List.of(new PromptMessage(
Role.USER,
new TextContent(
"Translate the following into " + targetLanguage + ":\n\n" + text, null))));
}
Naming¶
If you omit name, the framework generates one from the class and method names. For a class SummarizationPrompts with method summarize, the generated name is summarization-prompts.summarize.
You can also set a title and description:
@McpPrompt(
name = "code-review",
title = "Code Review",
description = "Review a code snippet for bugs and style issues")
public GetPromptResult codeReview(String code) { ... }
Argument Descriptions¶
Use Swagger's @Schema annotation to document arguments (surfaced in the prompt's descriptor):
import io.swagger.v3.oas.annotations.media.Schema;
@McpPrompt(name = "summarize", description = "Summarize text at a specified detail level")
public GetPromptResult summarize(
@Schema(description = "The text to summarize") String text,
@Schema(description = "brief, standard, or detailed") @jakarta.annotation.Nullable Detail detail) {
...
}
public enum Detail { BRIEF, STANDARD, DETAILED }
Optional Arguments¶
By default every parameter is required. Mark a parameter optional with either @Nullable or @Schema(requiredMode = NOT_REQUIRED):
@McpPrompt(name = "summarize", description = "Summarize text")
public GetPromptResult summarize(
String text,
@Nullable Detail detail) {
var level = detail == null ? Detail.STANDARD : detail;
...
}
If the client omits an optional argument, the parameter receives null.
Argument Type Conversion¶
Prompt arguments arrive on the wire as strings. Mocapi converts each argument to the parameter's declared type via Spring's ConversionService, so method parameters can be any type the ConversionService knows how to produce from a String:
- Strings (no conversion)
- Primitives and boxed primitives (
int,long,boolean,double, ...) - Enums (case-insensitive by default)
java.timetypes (LocalDate,Instant, ...)- Anything you register a custom
Converter<String, T>for
@McpPrompt(name = "schedule", description = "Generate a scheduling prompt")
public GetPromptResult schedule(
String event,
LocalDate date,
@Nullable Duration duration) {
...
}
If a conversion fails, the client receives a JSON-RPC error describing which argument couldn't be converted.
Receiving the Whole Arguments Map¶
If your method declares a single Map<String, String> parameter, it receives the entire untyped argument map:
@McpPrompt(name = "dynamic", description = "Pass all arguments through")
public GetPromptResult dynamic(Map<String, String> args) {
return buildPrompt(args);
}
This is useful when argument names are determined dynamically or when you want to sidestep type conversion entirely.
Custom Parameter Resolvers¶
By default, prompt parameters are pulled from the incoming Map<String, String> via mocapi's built-in string resolver (with Spring ConversionService for type conversion). You can layer your own resolver in front of that fallback to bind bespoke parameter types — for example a "current tenant" annotation populated from the session:
public final class CurrentTenantResolver implements ParameterResolver<Map<String, String>> {
@Override
public boolean supports(ParameterInfo info) {
return info.parameter().isAnnotationPresent(CurrentTenant.class)
&& info.resolvedType() == String.class;
}
@Override
public Object resolve(ParameterInfo info, Map<String, String> args) {
var jwt = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
return jwt.getToken().getClaimAsString("tenant");
}
}
Attach it via a customizer bean:
@Bean
GetPromptHandlerCustomizer currentTenantResolverCustomizer() {
CurrentTenantResolver resolver = new CurrentTenantResolver();
return config -> config.resolver(resolver);
}
@McpPrompt(name = "tenant-brief")
public GetPromptResult brief(@CurrentTenant String tenant, String topic) {
return ...;
}
User resolvers run ahead of the string-map fallback, so a specific supports() check always wins over the generic string conversion. Resolver selection is first-match-wins per Methodical's semantics. The same pattern applies to ReadResourceTemplateHandlerCustomizer for URI-template handlers and ReadResourceHandlerCustomizer for static resources.
Externalizing Metadata¶
Every string attribute on @McpPrompt (name, title, description) supports Spring's ${...} property placeholder syntax, so long descriptions don't have to live inline on the annotation. See Externalizing Annotation Metadata.
Argument Completions (autocomplete)¶
MCP clients can call completion/complete to fetch suggested values while a user types a prompt argument. Mocapi surfaces completions automatically when an argument's value space is statically knowable:
-
Java
enumtype — the enum constants become the completion candidates (in declaration order).The client asking for completions on thepublic enum Detail { BRIEF, STANDARD, DETAILED } @McpPrompt(name = "summarize") public GetPromptResult summarize(String text, Detail detail) { ... }detailargument gets["BRIEF", "STANDARD", "DETAILED"], prefix-filtered by whatever the user has typed. -
@Schema(allowableValues = { ... })on a non-enum parameter — useful when you want to keep the Java signature asStringbut still constrain the accepted values.@McpPrompt(name = "summarize") public GetPromptResult summarize( String text, @Schema(allowableValues = {"BRIEF", "STANDARD", "DETAILED"}) String detail) { ... }
Parameters that are plain strings, numbers, or any type without an enum or @Schema.allowableValues contribute no completions. The completion/complete response is empty in that case; nothing breaks. Prompts that accept a whole Map<String, String> also contribute nothing — they have no declared arguments.
Prefix matching is case-insensitive (so a user typing br still sees BRIEF), but the value returned is always the canonical declared form — matching what Spring's default ConversionService will accept when the prompt is later invoked.
Return Values¶
Prompts must return GetPromptResult:
public record GetPromptResult(String description, List<PromptMessage> messages) { }
Each PromptMessage has a Role (USER or ASSISTANT) and Content. Content can be TextContent, ImageContent, AudioContent, EmbeddedResource, or ResourceLink.
Multi-Message Prompts¶
A prompt can emit a conversation, not just a single message:
@McpPrompt(name = "few-shot", description = "Few-shot classification prompt")
public GetPromptResult fewShot(String input) {
return new GetPromptResult(
"Few-shot classification",
List.of(
new PromptMessage(Role.USER, new TextContent("Classify: 'I love this!'", null)),
new PromptMessage(Role.ASSISTANT, new TextContent("positive", null)),
new PromptMessage(Role.USER, new TextContent("Classify: 'Terrible.'", null)),
new PromptMessage(Role.ASSISTANT, new TextContent("negative", null)),
new PromptMessage(Role.USER, new TextContent("Classify: '" + input + "'", null))));
}
Embedded Resources¶
Reference a resource inline:
import com.callibrity.mocapi.model.EmbeddedResource;
import com.callibrity.mocapi.model.TextResourceContents;
@McpPrompt(name = "analyze-doc", description = "Analyze an embedded document")
public GetPromptResult analyzeDoc(String uri) {
return new GetPromptResult(
"Document analysis",
List.of(
new PromptMessage(
Role.USER,
new EmbeddedResource(
new TextResourceContents(uri, "text/plain", loadDocument(uri)),
null)),
new PromptMessage(
Role.USER,
new TextContent("Analyze the document above.", null))));
}
Prompt Templates¶
For anything beyond trivial string concatenation, use a PromptTemplate. Mocapi ships two engine implementations; each provides a PromptTemplateFactory Spring bean that you inject into your prompt bean.
The core interfaces¶
Both live in com.callibrity.mocapi.api.prompts.template:
public interface PromptTemplate {
GetPromptResult render(Map<String, String> args);
}
public interface PromptTemplateFactory {
PromptTemplate create(Role role, String description, String template);
default PromptTemplate create(Role role, String template) { ... }
}
The factory takes raw template source as a String — you load it from wherever you like (classpath, filesystem, database, inline literal). Compiled templates are reusable: create(...) once at construction, render(...) many times.
Available engines¶
| Module | Engine | Syntax | Features |
|---|---|---|---|
mocapi-prompts-spring |
Spring's PropertyPlaceholderHelper |
${name}, ${name:default}, \${name} escape |
Zero extra dependencies; pure substitution |
mocapi-prompts-mustache |
JMustache | {{name}}, {{#section}}...{{/section}}, partials |
Conditionals and iteration via sections |
Add one to your build:
<dependency>
<groupId>com.callibrity.mocapi</groupId>
<artifactId>mocapi-prompts-spring</artifactId>
<version>${mocapi.version}</version>
</dependency>
Its auto-configuration registers a default PromptTemplateFactory bean. If both modules are on the classpath, only the first one seen wins — most apps should pick one. Users with their own bean override both by declaring @Bean PromptTemplateFactory (our auto-configs use @ConditionalOnMissingBean).
Using a template from a @McpPrompt¶
Compile templates once at construction. Render them inside the method:
import com.callibrity.mocapi.api.prompts.template.PromptTemplate;
import com.callibrity.mocapi.api.prompts.template.PromptTemplateFactory;
import com.callibrity.mocapi.model.GetPromptResult;
import com.callibrity.mocapi.model.Role;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@Component
public class SummarizationPrompts {
private final PromptTemplate summarize;
public SummarizationPrompts(PromptTemplateFactory factory) throws IOException {
var source =
new ClassPathResource("prompts/summarize.mustache")
.getContentAsString(StandardCharsets.UTF_8);
this.summarize = factory.create(Role.USER, "Summarize the provided text", source);
}
@McpPrompt(name = "summarize", description = "Summarize text")
public GetPromptResult summarize(String text, @Nullable Detail detail) {
return summarize.render(Map.of(
"text", text,
"detail", detail == null ? "standard" : detail.name().toLowerCase()));
}
}
The template source in src/main/resources/prompts/summarize.mustache:
Summarize the following text at {{detail}} detail:
{{text}}
Multi-message templates¶
A single template renders into exactly one PromptMessage with the role supplied at create(...) time. For multi-message prompts, compile several templates and compose them:
public FewShotPrompts(PromptTemplateFactory factory) {
this.intro = factory.create(Role.USER, load("intro.mustache"));
this.userTurn = factory.create(Role.USER, load("user-turn.mustache"));
this.assistantTurn = factory.create(Role.ASSISTANT, load("assistant-turn.mustache"));
}
@McpPrompt(name = "few-shot")
public GetPromptResult fewShot(String input) {
var messages = new ArrayList<PromptMessage>();
messages.addAll(intro.render(Map.of()).messages());
messages.addAll(userTurn.render(Map.of("text", "I love this!")).messages());
messages.addAll(assistantTurn.render(Map.of("label", "positive")).messages());
messages.addAll(userTurn.render(Map.of("text", input)).messages());
return new GetPromptResult("Few-shot classification", messages);
}
Customizing a factory¶
Both factories expose a constructor that accepts a pre-configured engine object, so you can register a custom PromptTemplateFactory bean when the defaults aren't right. For example, to use {{name}} delimiters with the Spring-based engine:
@Bean
PromptTemplateFactory promptTemplateFactory() {
return new SpringPromptTemplateFactory(
new PropertyPlaceholderHelper("{{", "}}", ":", '\\', true));
}
Your bean wins thanks to @ConditionalOnMissingBean(PromptTemplateFactory.class).
Mid-execution interaction (progress and elicitation)¶
A prompt handler may declare an McpPromptContext parameter to report
progress or elicit input while it runs — the same surface tool handlers
get, scoped to prompts/get (ADR-0025, ADR-0024). The parameter is
resolved by the framework and never appears as a prompt argument.
@McpPrompt(name = "summarize", description = "Summarizes a topic")
public GetPromptResult summarize(String topic, McpPromptContext ctx) {
var p = ctx.percentProgress();
p.complete(0.5, "gathering sources");
// ... build the prompt ...
p.complete(1.0, "done");
return result;
}
See the interactive tools guide for the full progress and elicitation API and the replay/idempotency contract.