Features beyond the three core primitives. Most are optional, a few are near-free wins.
instructions — system prompt injectionOne line of config, lands directly in Claude's system prompt. Use it for tool-use hints that don't fit in individual tool descriptions.
const server = new McpServer(
{ name: "my-server", version: "1.0.0" },
{ instructions: "Always call search_items before get_item — IDs aren't guessable." },
);
mcp = FastMCP("my-server", instructions="Always call search_items before get_item — IDs aren't guessable.")
This is the highest-leverage one-liner in the spec. If Claude keeps misusing your tools, put the fix here.
If your tool logic needs LLM inference (summarize, classify, generate), don't ship your own model client. Ask the host to do it.
// Inside a tool handler
const result = await extra.sendRequest({
method: "sampling/createMessage",
params: {
messages: [{ role: "user", content: { type: "text", text: `Summarize: ${doc}` } }],
maxTokens: 500,
},
}, CreateMessageResultSchema);
# fastmcp
response = await ctx.sample("Summarize this document", context=doc)
Requires client support — check clientCapabilities.sampling first. Model preference hints are substring-matched ("claude-3-5" matches any Claude 3.5 variant).
Instead of hardcoding a root directory, ask the host which directories the user approved.
const caps = server.getClientCapabilities();
if (caps?.roots) {
const { roots } = await server.server.listRoots();
// roots: [{ uri: "file:///home/user/project", name: "My Project" }]
}
roots = await ctx.list_roots()
Particularly relevant for MCPB local servers — see build-mcpb/references/local-security.md.
Better than stderr for remote servers. Client can filter by level.
// In a tool handler
await extra.sendNotification({
method: "notifications/message",
params: { level: "info", logger: "my-tool", data: { msg: "Processing", count: 42 } },
});
await ctx.info("Processing", count=42) # also: ctx.debug, ctx.warning, ctx.error
Levels follow syslog: debug, info, notice, warning, error, critical, alert, emergency. Client sets minimum via logging/setLevel.
Client sends a progressToken in request _meta. Server emits progress notifications against it.
async (args, extra) => {
const token = extra._meta?.progressToken;
for (let i = 0; i < 100; i++) {
if (token !== undefined) {
await extra.sendNotification({
method: "notifications/progress",
params: { progressToken: token, progress: i, total: 100, message: `Step ${i}` },
});
}
await doStep(i);
}
return { content: [{ type: "text", text: "Done" }] };
}
async def long_task(ctx: Context) -> str:
for i in range(100):
await ctx.report_progress(progress=i, total=100, message=f"Step {i}")
await do_step(i)
return "Done"
Long tools should check the SDK-provided AbortSignal:
async (args, extra) => {
for (const item of items) {
if (extra.signal.aborted) throw new Error("Cancelled");
await process(item);
}
}
fastmcp handles this via asyncio cancellation — no explicit check needed if your handler is properly async.
If you've registered prompts or resource templates with arguments, you can offer autocomplete:
server.registerPrompt("query", {
argsSchema: {
table: completable(z.string(), async (partial) => tables.filter(t => t.startsWith(partial))),
},
}, ...);
Low priority unless your prompts have many valid values.
| Feature | Server declares | Client must support | Fallback if not |
|---|---|---|---|
instructions |
implicit | — | — (always works) |
| Logging | logging: {} |
— | stderr |
| Progress | — | sends progressToken |
silently skip |
| Sampling | — | sampling: {} |
bring your own LLM |
| Elicitation | — | elicitation: {} |
return text, ask Claude to relay |
| Roots | — | roots: {} |
config env var |
Check client caps via server.getClientCapabilities() (TS) or ctx.session.client_params.capabilities (fastmcp) before using the bottom three.