●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Beyond Tools in MCP: Designing with Resources, Prompts, and Sampling
Cramming everything into MCP tools hits a wall fast. Here is how resources, prompts, and sampling untangle a server, told through a real wallpaper-app asset manager I cut from 14 tools down to 5.
I'm Masaki Hirokawa, an artist and indie developer. I want to walk back through the moment I started writing an MCP server so Claude could work directly with my wallpaper-app assets. My first server implemented every capability as a tool. Listing assets, reading metadata, running a fixed review routine, generating captions — all tools. It worked, but once the tool count crept up to fourteen, Claude started hesitating over which one to call, and I lost track of where things lived myself.
That was when it clicked that MCP offers resource, prompt, and sampling alongside tool, and I had been forcing things that belonged elsewhere into tools. Using the real refactor that cut those fourteen tools down to five, this piece lays out how to choose among the three primitives. The subject is the asset-management server for the wallpaper apps I've built as an indie developer since 2014 — apps that have passed 50 million downloads across iOS and Android.
What happens when tools carry everything
When you start with MCP, the first thing you reach for is almost always a tool. Register a function with server.tool() and Claude can call it — that simplicity is the draw. I went the same way. But expressing everything through tools surfaces a few concrete problems.
The first is that Claude's selection accuracy drops. Tools are presented to the model as operations with side effects. When read-only data fetches are also tools, the model has to judge "is this safe to call?" every time, and the more similarly named tools pile up, the more often it grabs the wrong one. On my server I kept both get_wallpaper_meta and get_wallpaper_info, and Claude regularly called the opposite of what I wanted.
The second is that routines can't be reused. "Review this wallpaper's metadata for the App Store" follows nearly the same steps every time. As a tool, the steps live in the prompt I rewrite on every call instead of accumulating on the server side.
The third is that there's nowhere to put light inference inside server processing. If I want to generate a human-facing caption from a filename, the server could hold an API key and call Claude itself — but then the server becomes the billing party, and cost management for a one-person operation gets complicated fast.
Here is a simplified version of the "everything is a tool" state before the refactor.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";const server = new McpServer({ name: "wallpaper-assets", version: "1.0.0" });// Read-only, yet all expressed as toolsserver.tool("list_wallpapers", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(await db.listAll()) }],}));server.tool("get_wallpaper_meta", { id: z.string() }, async ({ id }) => ({ content: [{ type: "text", text: JSON.stringify(await db.meta(id)) }],}));// A fixed review routine, also a tool (the caller rewrites the prompt each time)server.tool("review_metadata", { id: z.string() }, async ({ id }) => ({ content: [{ type: "text", text: `Review this metadata: ${JSON.stringify(await db.meta(id))}` }],}));// ... eleven more tools follow
That review_metadata is the telling one. All it does is assemble a review prompt and return it; it's a template, not an operation. It should have been a prompt from the start.
Resources: hand read-only data over as an address
A resource exposes the server's readable data under a URI. It's reachable at an address like wallpaper://1024, and the absence of side effects is expressed in its type. From Claude's vantage point it stops being "an operation to decide whether to call" and becomes "material it can reference," which separates it from the tool-selection noise.
The wallpaper list and individual metadata are exactly read-only data, so I moved them to resources.
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";// The list is a fixed-URI resourceserver.resource("wallpaper-list", "wallpaper://list", async (uri) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(await db.listAll()), }],}));// Individual metadata via a templated URIserver.resource( "wallpaper", new ResourceTemplate("wallpaper://{id}", { list: undefined }), async (uri, { id }) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(await db.meta(String(id))), }], }));
That change alone removed three tools — list_wallpapers, get_wallpaper_meta, and get_wallpaper_info. Fetching data consolidated onto resources, and the mix-ups between similarly named tools dissolved on their own. I strongly recommend placing this line — reads are resources, writes and operations are tools — as the first fork in the design. What stays a tool is only the state-changing work, like re-running classification or regenerating a thumbnail.
One caveat: resources follow a "the client reads them explicitly" model. Claude doesn't silently read every resource in the flow of a conversation. When you want it to work with a specific wallpaper, the client has to attach that resource to the context or you have to nudge it to reference the resource. Misread this and it feels like "I made it a resource but Claude won't look at it" — but that's the designed behavior.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Concrete rules for choosing between resources, prompts, and sampling (reads become resources, human-triggered routines become prompts, in-server inference becomes sampling)
✦The actual refactor that took my 6-app wallpaper asset server from 14 tools to 5, with the TypeScript code
✦The client-support pitfall around sampling, and a fallback design that degrades quietly instead of breaking
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
A prompt registers a reusable message template on the server side. It takes arguments and returns a pre-assembled message sequence. In many clients it shows up like a slash command that the user picks and launches.
That's what the earlier review_metadata tool actually wanted to be. Moving it from tool to prompt turns the review routine into a server asset, so the caller no longer rewrites the prompt.
server.prompt( "review_for_store", { id: z.string(), locale: z.string().default("en") }, async ({ id, locale }) => { const meta = await db.meta(id); return { messages: [ { role: "user", content: { type: "text", text: [ `Review the following wallpaper metadata for the ${locale} App Store / Google Play.`, "Lenses: title appeal, description length limits, banned phrasing, category fit.", "Give the corrected copy itself rather than a bulleted list of suggestions.", "", JSON.stringify(meta, null, 2), ].join("\n"), }, }, ], }; });
The payoff of keeping this as a prompt is that writing the routine carefully once lets all six apps run reviews at the same quality. In my case I concentrated the checks for phrasing that tends to get rejected in store review (overblown claims like "best" or "No.1") into this prompt. I used to hand-write the prompt through a tool every time, so the lenses drifted from app to app; fixing them in a prompt erased that drift.
The difference between prompt and tool in one line: a prompt is "a user-initiated standing request," a tool is "an operation the model calls when needed." Reviews, summaries, naming-convention checks — work that a human launches by saying "do this" — suits a prompt.
Sampling: borrow the client's inference from the server
Sampling is the most misunderstood of the three. It's the mechanism by which the server asks the client's LLM to run inference. Rather than the server holding its own API key and calling Claude, it sends a sampling/createMessage request to the client; the client (with the user's approval) generates with the model at hand and returns the result to the server.
This shines when you want to slip a bit of light natural-language generation into the middle of server processing. In my case, when a new wallpaper is ingested, I wanted a caption draft built from the filename and color information. The value of sampling is achieving this without the server becoming the billing party.
server.tool( "ingest_wallpaper", { id: z.string(), filename: z.string() }, async ({ id, filename }) => { const palette = await analyzePalette(id); // existing color analysis // Ask the client's LLM to draft a caption const result = await server.server.createMessage({ messages: [{ role: "user", content: { type: "text", text: `From the filename "${filename}" and dominant colors ${palette.join(", ")}, ` + `give three wallpaper-app captions, each under 8 words.`, }, }], maxTokens: 200, }); const caption = result.content.type === "text" ? result.content.text : ""; await db.saveCaptionDraft(id, caption); return { content: [{ type: "text", text: `Saved caption drafts:\n${caption}` }] }; });
The key point is that whether server.server.createMessage() succeeds depends entirely on whether the connected client supports sampling. Send it to a client that doesn't, and it errors. The next section handles that real-world constraint.
How to choose among the three
Here is the decision framework. The order I ask myself is fixed.
First, is it state-changing or read-only? If read-only, consider a resource; if it changes state, consider a tool. Second, is it a user-initiated routine? Work launched with "do this," like review or summary, becomes a prompt. Third, do I want to slip natural-language generation into the middle of server processing? If so, consider sampling — but always verify client support.
Spelled out as concrete mappings:
Fetching the wallpaper list and individual metadata → resource (no side effects, reference material)
Re-running classification, regenerating thumbnails, updating the publish flag → tool (state-changing)
Store-facing metadata review and naming-convention checks → prompt (human-initiated routine)
Caption drafting at ingest time → sampling used inside a tool
Applying this, the fourteen tools came down to five: reclassify, regenerate thumbnail, update publish flag, ingest, and bulk-replace tags. The eight read-only ones moved to resources, the review ones moved to prompts, and the server's responsibilities split cleanly. Claude's tool mix-ups have been essentially zero since this refactor.
How I actually rebuilt the wallpaper asset server
Doing the whole migration at once breaks things, so I went in stages. I first added the resources and ran them in parallel with the read-only tools. Only after confirming in conversation that Claude reads the resources correctly did I delete the tool versions. Keeping that parallel window helped me catch the "Claude stopped referencing it once it became a resource" pitfall early.
Next came the move to prompts. When rewriting the review tools as prompts, I designed the argument defaults carefully. Defaulting locale to en means I only pass ja for the Japanese store, which shaves friction off daily work. In indie development, trimming small frictions like this compounds.
I advanced the sampling integration most cautiously. Caption generation is strictly a "draft," and I keep final decisions in my own hands. I didn't adopt a design that fully automates rewriting store copy tied directly to AdMob revenue — the quality variance scared me. Drafts are saved, and I review and adopt them. That's a judgment scaled to one developer's risk tolerance, and there isn't a single right answer.
Heavy work like color analysis and thumbnail generation stayed in the server's native implementation, as before. I limit sampling to light natural-language generation and never throw image processing at an LLM. When adding primitives becomes the goal in itself, complexity balloons, so I keep asking "does this step truly need inference?"
The real constraint of client support
A beautiful design won't run if the client you connect doesn't support it. This surfaces especially with sampling. Tools and resources are supported by nearly every client, but prompt and sampling support varies. Depending on sampling in production outright risks the entire ingest feature dropping the moment you switch clients.
What I use is a fallback that degrades quietly when sampling isn't available. I wrap the createMessage call in try/catch; on failure it skips only caption generation and still completes the ingest.
async function tryGenerateCaption(filename: string, palette: string[]): Promise<string | null> { try { const result = await server.server.createMessage({ messages: [{ role: "user", content: { type: "text", text: `One caption under 8 words from "${filename}" and colors ${palette.join(", ")}.` }, }], maxTokens: 100, }); return result.content.type === "text" ? result.content.text : null; } catch { // Client without sampling support, or the user declined return null; // never stop the ingest itself }}
Since adopting this, I've been able to connect the same server from Claude Desktop and Claude Code without thinking about the support gap. The "convenience kicks in where it's available and nothing breaks where it isn't" mindset pays off most for people who move between several environments in indie work.
One more practical note: sampling is designed to go through user approval. Some clients pop a confirmation dialog every time, so it's a poor fit for batch workflows that ingest many wallpapers at once. I review ingests one image at a time, so it never bit me, but if you're after unattended bulk processing, there are cases where an independent server-side API call beats sampling. Here again, the operational call — how far to widen the automation — comes first.
If you want a next step, write out your own MCP server's tool list and sort each entry into read / operation / routine / inference. Seeing how many reads are hiding among your tools makes the first refactor to run obvious. I hope this helps anyone whose tool list has grown past the point of being legible.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.