MCP Advanced — Sampling, Elicitation, Resources, Prompts
Tools are the headline feature of MCP. This module covers the four capabilities most tutorials skip — and they're where MCP stops being "remote function calling" and starts being a genuinely two-way protocol between client and server.
Learning objectives
- Beginner: A single MCP server exposing a handful of @Tool methods and nothing else — most real-world MCP servers start here.
- Intermediate: Add @McpResource for read-only config/data access and progress notifications for one genuinely slow tool, improving UX without changing tool logic.
- Advanced: A tool using sampling to summarize fetched content via the client's model, with elicitation gating any destructive action behind explicit human confirmation — full two-way protocol use, not just one-directional tool invocation.
◆ The problem
Not everything a server exposes is an action. Sometimes a client just needs to read a piece of data — a file's contents, a config value — without the connotation of "performing an operation" that a Tool carries.
An MCP Resource is addressable, typically read-only data a server exposes by URI — deliberately distinct from a Tool, which performs an action (and which the model decides whether to invoke). A client (or the human user, via the host application) can browse and fetch resources directly.
@McpResource(uri = "config://app/settings", description = "Current application configuration") public String getSettings() { return objectMapper.writeValueAsString(configService.getCurrentSettings()); }
💻 Code example
@McpResource(uri = "config://app/settings", description = "Current application configuration") public String getSettings() { return objectMapper.writeValueAsString(configService.getCurrentSettings()); }
An MCP Prompt is a server-exposed, parameterized prompt template — the server-side equivalent of Module 03's client-side PromptTemplate, but discoverable and invokable by any connected client, standardizing a common interaction the same way a Tool standardizes an action.
@McpPrompt(name = "summarize-ticket", description = "Summarize a support ticket for a handoff") public GetPromptResult summarizeTicket(@McpArg(name = "ticketId") String ticketId) { String promptText = "Summarize ticket %s in 3 bullet points for handoff to another agent.".formatted(ticketId); return GetPromptResult.builder(List.of(new PromptMessage(Role.USER, TextContent.builder(promptText).build()))) .build(); }
💻 Code example
@McpPrompt(name = "summarize-ticket", description = "Summarize a support ticket for a handoff") public GetPromptResult summarizeTicket(@McpArg(name = "ticketId") String ticketId) { String promptText = "Summarize ticket %s in 3 bullet points for handoff to another agent.".formatted(ticketId); return GetPromptResult.builder(List.of(new PromptMessage(Role.USER, TextContent.builder(promptText).build()))) .build(); }
◆ The problem
Some tool calls are slow (a large file scan, a multi-step database migration). A client waiting silently for one big response has no way to show the user meaningful progress, or distinguish "still working" from "hung."
MCP lets a server stream progress notifications back to the client during a long-running tool execution — a delivery-tracker for tool calls, rather than an opaque black box until final completion.
@McpTool(description = "Migrate all records from the legacy schema") public String migrateRecords(McpSyncRequestContext context) { for (int i = 0; i < totalBatches; i++) { processBatch(i); context.progress(p -> p.progress(i + 1).total(totalBatches) .message("Batch " + (i + 1) + "/" + totalBatches)); } return "Migration complete: " + totalBatches + " batches processed"; }
💻 Code example
@McpTool(description = "Migrate all records from the legacy schema") public String migrateRecords(McpSyncRequestContext context) { for (int i = 0; i < totalBatches; i++) { processBatch(i); context.progress(p -> p.progress(i + 1).total(totalBatches) .message("Batch " + (i + 1) + "/" + totalBatches)); } return "Migration complete: " + totalBatches + " batches processed"; }
◆ The problem
A tool on your MCP server needs to do something LLM-like internally — say, summarizing a large fetched document before returning it — but the server itself has no API key or model configured; only the connecting client does.
Sampling inverts the usual direction of an MCP interaction: instead of the client asking the server to do something, the server asks the connected client to run an LLM completion on its behalf, using whatever model the client is configured with. The tool "borrows" the client's model rather than needing its own credentials or provider integration.
@McpTool(description = "Fetch and summarize a web page") public String fetchAndSummarize(McpSyncRequestContext context, String url) { String rawContent = webFetcher.fetch(url); if (!context.sampleEnabled()) { return "This client doesn't support sampling — cannot summarize."; } // the CLIENT's model runs this, not the server's — the server has no model of its own CreateMessageResult result = context.sample( s -> s.message("Summarize this in 3 sentences: " + rawContent)); return result.content().text(); }
◆ Under the hood — why this matters
Sampling means tool authors don't need to provision, pay for, or manage their own LLM credentials just to add an LLM-powered step inside a tool — cost and model choice stay entirely with whoever's operating the client, and a single tool server can be used by clients running completely different underlying models without any server-side change.
💻 Code example
@McpTool(description = "Fetch and summarize a web page") public String fetchAndSummarize(McpSyncRequestContext context, String url) { String rawContent = webFetcher.fetch(url); if (!context.sampleEnabled()) { return "This client doesn't support sampling — cannot summarize."; } // the CLIENT's model runs this, not the server's — the server has no model of its own CreateMessageResult result = context.sample( s -> s.message("Summarize this in 3 sentences: " + rawContent)); return result.content().text(); }
◆ The problem
A tool is about to do something consequential — say, cancel a subscription — but a required piece of information is missing, or the action needs explicit human confirmation before proceeding. Failing outright, or guessing, are both bad options.
Elicitation lets a server-side tool pause mid-execution and ask the human user (relayed through the client) a direct question, then resume once an answer comes back — a genuine human-in-the-loop mechanism, not just a text response the model might ignore.
public record ConfirmationResponse(boolean confirmed) {} @McpTool(description = "Cancel a customer's subscription") public String cancelSubscription(McpSyncRequestContext context, String subscriptionId) { if (!context.elicitEnabled()) { return "This client doesn't support elicitation — cannot confirm cancellation."; } StructuredElicitResult<ConfirmationResponse> result = context.elicit( e -> e.message("Confirm cancellation of subscription " + subscriptionId + "? This cannot be undone."), ConfirmationResponse.class); if (result.action() != ElicitResult.Action.ACCEPT) { return "Cancellation aborted by user."; } subscriptionService.cancel(subscriptionId); return "Subscription cancelled."; }
▲ Pitfall
Elicitation is only as safe as the host application's UX around it — if a client auto-approves elicitation requests without genuinely surfacing them to the human (e.g. to avoid interrupting flow), you've built the appearance of a human-in-the-loop safety control without the actual safety. Treat elicitation as meaningful only if the host guarantees a real human sees and answers the prompt.
💻 Code example
public record ConfirmationResponse(boolean confirmed) {} @McpTool(description = "Cancel a customer's subscription") public String cancelSubscription(McpSyncRequestContext context, String subscriptionId) { if (!context.elicitEnabled()) { return "This client doesn't support elicitation — cannot confirm cancellation."; } StructuredElicitResult<ConfirmationResponse> result = context.elicit( e -> e.message("Confirm cancellation of subscription " + subscriptionId + "? This cannot be undone."), ConfirmationResponse.class); if (result.action() != ElicitResult.Action.ACCEPT) { return "Cancellation aborted by user."; } subscriptionService.cancel(subscriptionId); return "Subscription cancelled."; }
| Annotation | Exposes |
|---|---|
| @McpTool | An action the model can decide to invoke (Module 12). |
| @McpResource | Addressable, typically read-only data. |
| @McpPrompt | A reusable, server-defined prompt template. |
| @McpComplete | Auto-completion suggestions for a prompt argument or a resource URI template variable. |
◆ Note
@McpArg isn't a fifth capability annotation — it's a parameter-level annotation used inside an @McpPrompt method to name and describe each argument (see the Prompts subtopic above).
✓ Quick recap
What's the core difference between an MCP Tool and an MCP Resource? A Tool performs an action the model decides to invoke; a Resource is addressable, typically read-only data. In sampling, whose model actually runs the completion — the server's or the client's? The client's — the server "borrows" it via context.sample() rather than needing its own model/API key. Why is elicitation only meaningfully safe if the host application surfaces it properly? A client that auto-approves elicitation requests provides the appearance of human-in-the-loop safety without the substance.
Want a visual for this concept?
Generate a diagram tailored to “MCP Advanced — Sampling, Elicitation, Resources, Prompts” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →