Skip to content

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.time types (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 enum type — the enum constants become the completion candidates (in declaration order).

    public enum Detail { BRIEF, STANDARD, DETAILED }
    
    @McpPrompt(name = "summarize")
    public GetPromptResult summarize(String text, Detail detail) { ... }
    
    The client asking for completions on the detail argument 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 as String but 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.