MCP Resources, Prompts and Sampling Through a Gateway
Tools are the primitive a model calls; resources are addressed by URI and chosen by the application; prompts are templates a user fills; sampling runs the other way, from server to client. A hosted gateway serves tools, so it can rate-limit, meter and log those.
Short answer: Tools are the primitive a model calls; resources are addressed by URI and chosen by the application; prompts are templates a user fills; sampling runs the other way, from server to client. A hosted gateway serves tools, so it can rate-limit, meter and log those. For the other three it can only state the boundary: it does not advertise a URI space it does not hold, and it does not ask your client to run a model it does not pay for.
Key takeaways
- Four primitives, two directions. Tools, resources and prompts are started by the client side of the connection; sampling is a request the server sends to the client's model.
- A gateway that does not hold a resource cannot serve it. The registration function builds a
tools-only surface, and the correct answer for
resources/liston such a server is an error. - Orchestration is the alternative to sampling. Instead of asking the client for a completion, the gateway runs its own pipeline whose steps are its own tools.
- Limits attach to the credential. The plan catalog carries per-key and per-team request limits; there is no per-primitive rate line, because the metered surface is the tool call.
- Absent primitives leave absent data. A usage report can only group what was served, so an empty resources panel and an empty resources column in the aggregate are the same fact.
- Do this next: call
tools/list,resources/listandprompts/liston the server you are evaluating and write down which of the three answers.
mcp transport decides which primitives are even possible
Before any governance question, the transport decides what the connection can carry. A local stdio server and a hosted HTTP endpoint are not two ways of doing the same thing — they differ in whether the server can speak first:
- stdio is full duplex for the life of the process. The server shares a pair of pipes with the client, so it can answer, notify and initiate. Sampling, the logging utility and resource change notifications all have somewhere to go.
- A Streamable HTTP server with no session answers requests. One POST goes in, one JSON object comes back, and the connection the server would need in order to ask the client anything does not exist between calls. Server-initiated requests have no channel.
- A streaming HTTP response creates a channel, but only inside one request. Where the server answers a POST with an event stream, it can send a server-to-client request while that call is still open — which is how sampling works over HTTP at all, and why it is unavailable to a server that answers statelessly.
The practical consequence for a gateway is that "does it support sampling" is a question about the transport topology rather than about a feature flag. A stateless endpoint cannot ask your client for a completion, so it must either fail the request that wanted one or run the composition itself — which is exactly the choice the pipeline tool makes, and it is why the pipeline is where the interesting server-side orchestration lives.
What sits beside the tools, and who chooses each one
MCP's server features are tools, resources and prompts, and the client also declares capabilities of its own — roots for the file boundaries it exposes, sampling for the model calls it is willing to run. The three server features differ in the actor who decides. A tool is model-driven: the model reads the list and chooses. A resource is application-driven: it has a URI, the application decides what to attach. A prompt is user-driven: it is a template with arguments that a person selects. The wire methods differ accordingly — tools are one method pair, resources and prompts each have their own discovery and read pair (resources, prompts).
The reference page for the schemas is the MCP tools reference; this page is about the other question — what a gateway in the middle can do about each primitive. That is a governance question, and the answer differs sharply per primitive. One can be metered and limited. Two cannot be served at all by a server that does not hold them. And the fourth runs in the opposite direction, which is where the interesting engineering decision lives.
mcp sampling runs the other way, so a gateway orchestrates instead
Sampling inverts the connection. Instead of the client asking the server to do something, the server asks the client to run a model completion, which requires the client to declare the sampling capability during initialisation and is deliberately human-approved on most hosts (sampling). A hosted gateway does not need it, because the orchestration it would otherwise request is already server-side:
# backend/smartgate/api/mcp.py — source lines 343–356 (register_mcp_tools)
payload = await engine.run(
ctx,
template=template,
steps=steps,
inputs=run_inputs,
)
data = payload if isinstance(payload, dict) else {"result": payload}
merged_data = {
**data,
"results": payload.get("results") or [],
}
steps_meta = payload.get("pipeline_steps") or []
pipeline_ok = bool(steps_meta) and all(s.get("success") for s in steps_meta)
failed_step = next((s for s in steps_meta if not s.get("success")), None)
The engine call takes a template or an explicit step list plus the run inputs, and everything above it is accounting: the payload is folded into a dict, the step metadata is inspected, and the run is successful only when every step reported success. That is the design choice worth naming. A server that wants a model call and has no sampling capability has exactly two options: fail, or run the composition itself. This one runs it itself — the steps are the gateway's own tools, each step is individually audited, and the client's model is never involved, which also means the gateway never bills a completion it did not measure. If your agent already has a model loop and only needs tools, this is a feature. If you were counting on the server to ask your host for a summarisation step, it will not, and the pipeline template is the closest equivalent.
agent2agent handoffs and a template that resolves its own arguments
An MCP prompt is a template whose arguments a person supplies. The same shape appears inside the gateway, with the argument author on the other side of the wire:
# backend/smartgate/core/pipeline.py — source lines 211–248 (resolve_pipeline_step_params)
if module_name == "context_gate" and not (resolved.get("text") or "").strip():
text = (inputs.get("text") or "").strip()
if not text:
for prev in reversed(pipeline_ctx.prior_results()):
if prev.success and prev.data:
text = _fetch_text(prev.data) or ""
if text:
break
if text:
resolved["text"] = _truncate_pipeline_text(text, pipeline_template)
if module_name == "dedup" and not resolved.get("texts"):
texts: List[str] = []
for prev in pipeline_ctx.prior_results():
if not prev.success or not prev.data:
continue
fetched = _fetch_text(prev.data)
if fetched:
texts.append(fetched)
continue
texts.extend(_search_snippets(prev.data))
if texts:
resolved["texts"] = texts
if module_name == "memory" and resolved.get("action") == "add":
if not (resolved.get("text") or "").strip():
for prev in reversed(pipeline_ctx.prior_results()):
if not prev.success or not prev.data:
continue
text = _fetch_text(prev.data)
if not text:
snippets = _search_snippets(prev.data)
text = "\n\n".join(snippets) if snippets else ""
if text:
resolved["text"] = text
break
return resolved
Five branches, one rule each: a context-gate step with no text takes the run's text or the text of a prior fetch; a dedup step with no passages collects what the earlier steps produced; a memory write with no text falls back to the fetched text or the combined search snippets. This is what makes a multi-step run usable as one call — the caller supplies intent, and the server fills the inputs a step needs from what it already has. The comparison with agent-to-agent handoffs is exact: when one agent delegates to another, the delegating side has to supply the arguments explicitly, because the receiving agent has no history to resolve against. Both designs work. The difference is where the argument resolution happens and therefore what has to be recorded: in the server-side case one row covers the whole run, and in a peer handoff the payload on the wire is the audit trail.
An mcp router meters credentials, not primitives
The limits a request meets are per key and per team, and the plan catalog is where they come from:
# lib/plan-rate-limits.ts — source lines 5–12 (getPlanRateLimits)
function getPlanRateLimits(plan: Plan) {
const f = getPlanCatalogEntry(plan).features;
return {
restWriteRpm: f.max_team_rpm,
mcpRpmPerKey: f.mcp_rpm_per_key,
mcpRpmTeamCeiling: f.mcp_rpm_team_ceiling,
};
}
Three numbers, and the shape of the triple is the interesting part. restWriteRpm covers the REST
surface, mcpRpmPerKey covers the tool surface per credential, and mcpRpmTeamCeiling caps the
team's total — so one noisy key cannot consume the team's whole allowance, and a team cannot exceed
its plan by adding keys. What is absent is equally informative: there is no rate line per primitive,
because the primitives that are not served have no requests to rate. That is the honest consequence
of a tools-only surface. If you are comparing gateways, the question that separates the products is
not how many primitives are listed but whether the limit attaches to the identity or to the
endpoint: an endpoint limit is a denial-of-service control, and an identity limit is a cost control.
Attribution and logging for the primitives a gateway does serve
Rows are written per call and aggregated later, and the aggregation is opinionated about what counts:
# lib/analytics/aggregate-entries.ts — source lines 16–32 (aggregateAuditEntries)
function aggregateAuditEntries(
entries: AuditEntry[],
opts: { includePlayground: boolean },
) {
const filtered = opts.includePlayground
? entries
: entries.filter((entry) => routeOf(entry) !== "playground");
const dailyMap = new Map<
string,
{ tokens: number; compress: number; fetch: number; calls: number }
>();
const toolMap = new Map<string, { tokens: number; calls: number }>();
const routeMap = new Map<string, number>();
for (const entry of filtered) {
if (!entry.success) continue;
# lib/analytics/aggregate-entries.ts — source lines 56–69 (aggregateAuditEntries)
return {
totalCalls: filtered.filter((entry) => entry.success).length,
dailyUsage: [...dailyMap.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([date, value]) => ({ date, ...value })),
byTool: [...toolMap.entries()].map(([tool, value]) => ({
tool,
...value,
})),
byRoute: [...routeMap.entries()].map(([route, tokens]) => ({
route,
tokens,
})),
};
Two filters and three groupings. Playground traffic is excluded by default because a person clicking in a dashboard is not the same demand signal as an agent running unattended, and failed calls are excluded because a rejected call is not usage. What remains is grouped by day, by tool and by route — and here is the point for this page: those are the only three attributions available, because tools and routes are the only two things a tools-only server produces. A resources read would have to appear as a route or a tool to be counted, and a gateway that serves neither creates no rows to group. Read an empty column in a usage table as a statement about the surface, not as a bug in the report — and check the filter flags before you conclude that a quiet week was quiet.
How to prove your mcp tool call surface advertises no resources
The cheapest way to answer "which primitives does this server actually implement" is to ask it as a client, and the test harness for that is small enough to read:
# backend/tools/cursor_mcp_deep_test.py — source lines 57–73 (mcp_tool_call)
def mcp_tool_call(mcp_url: str, api_key: str, tool: str, args: dict[str, Any]) -> dict[str, Any]:
"""One SSE session per tool — avoids session expiry on long runs."""
last_exc: Exception | None = None
for attempt in range(3):
client = McpSseClient(mcp_url, api_key)
try:
client.connect()
time.sleep(0.3 * (attempt + 1))
client.initialize()
return client.call_tool(tool, args)
except Exception as exc:
last_exc = exc
if "404" not in str(exc) or attempt == 2:
raise
finally:
client.close()
raise last_exc or RuntimeError("mcp_tool_call failed")
One session per tool call — the docstring says why: a long run outlives a session — then connect,
initialise, and call the tool. Note what the harness never does. There is no resources/read leg,
no prompts/get, no capability negotiation beyond the handshake, because a tools-only server has
nothing to negotiate for. The 404 retry is worth copying into your own smoke tests: it is the
symptom of a session that expired between calls rather than of a missing tool, and a harness that
treats it as a hard failure reports a broken server that is only a stale one. Run the same shape
against any gateway you are evaluating: one script that lists all three primitives tells you more
than a features page. A script is the cheapest instrument for that answer; when you would rather
watch the exchange, the options and what each one can show are compared in
MCP inspector alternatives.
What a client sees when a primitive is missing
Every client implementation faces the same gap between "the server is reachable" and "the server implements this method", and the difference between a good client and a fragile one is what it does with the gap. JSON-RPC answers an unsupported method with a method-not-found error, and that error is a statement about the capability set rather than a failure to report. A client that treats it as a crash fails its whole session because a resource list was empty; a client that treats it as an answer records "this server has no resources" once and stops asking.
Three habits are worth building into your own client, whichever SDK you use. Probe, do not
assume. Ask for each primitive once, cache the answer against the server URL, and re-probe only
when the configuration changes. Send only the capabilities you implement. A client that declares
sampling and then cannot answer a sampling/createMessage request has made itself the problem, and
the server is entitled to fail the call it was serving. Keep the probe in your smoke test. One
script that lists tools, resources and prompts and prints three verdicts will answer the question
that a features comparison cannot, and it keeps answering it after every upgrade on both sides.
That is also the honest reading of an empty panel. A host UI that shows a Resources tab with nothing in it is not broken and not waiting for configuration — it is rendering the response, which said there is nothing to render. Compare the three verdicts before you file a bug, and the bug you file, if any, will be the real one. Everything in those three habits is the client's own job, and the rest of that job — configuration, headers and transport — is worked through in MCP client for AI agents.
Whether a missing primitive is an error or an empty list also depends on the mcp protocol version in play, because the declared capabilities are what the client renders against.
The mcp playground, and the store the primitives would need
A first-party playground reads the same endpoint your agents use, which is why the state behind it has to be decided at boot:
# lib/redis/config.ts — source lines 38–42 (resolveRedisMode)
function resolveRedisMode(): RedisMode {
if (isUpstashRestConfigured()) return "upstash";
if (isTcpRedisConfigured()) return "tcp";
return "none";
}
Three modes — a REST-based store, a direct Redis URL, or nothing — and the mode is resolved rather than assumed. The playground is the surface where that matters most visibly: it shows the limits and the counters that the same keys are governed by, and those counters only agree across instances when the store is shared. The primitives question follows from the same place. A playground can render whatever the server advertises; there is no panel for a URI space or a prompt library that the server does not hold, which is why the absence you see in a playground is the same absence a client sees over the wire. If you need resources and prompts in your own stack, run a server that implements them next to the gateway — and read the MCP gateway page for where the gateway's own limits sit relative to it.
What a gateway can govern, primitive by primitive
| Primitive | Who starts it | Wire method | What a gateway can do | What it cannot do |
|---|---|---|---|---|
| Tools | The model, by name | tools/list, tools/call |
Advertise a list, meter tokens, rate limit per key, audit every call | Choose which tool the model picks |
| Resources | The application, by URI | resources/list, resources/read |
Nothing, unless the server implements the URI space | Invent a resource it does not hold |
| Prompts | The user, from a template | prompts/list, prompts/get |
Nothing, unless the server implements the templates | Fill a template it does not own |
| Sampling | The server, toward the client | sampling/createMessage |
Decline it by never asking | Bill a completion it did not run |
The honest reading is that a gateway's relationship with these four is asymmetric, and pretending otherwise is how a product page ends up promising a feature it cannot meter. Tools are the surface where control is possible: a named call, an argument shape, an identity and a token count. Resources and prompts are server features that cannot be proxied into existence, and sampling is a capability the client grants rather than one the gateway charges for. What a gateway can do for all four is be explicit — advertise what it serves, error on what it does not, and record every call it did serve.
Primitives are what an agent hands to a client; when agents hand work to each other, the same governance question arrives as the a2a protocol, with tasks in place of tools.
A resources-first server beside the gateway
The practical answer for a team that genuinely needs resources and prompts is not to wait for a
gateway to grow them — it is to run two servers. A local stdio server over the directory you care
about serves resources/list and resources/read for that tree, and it can serve prompts whose
arguments are paths in it. The hosted gateway stays what it is: one authenticated endpoint for the
calls your agents make to the outside world. Both appear as separate entries in the host's config,
both are visible in the client's own list, and neither has to pretend to be the other.
Two rules keep that split honest. Let the URI space live where the read permission lives. A resource is a pointer into something a server can actually open, so a gateway that reaches the same content through a fetch tool is not an alternative URI space — it is a different access model with different semantics, one call at a time and one audit row each. Keep the governance on the call that costs money. The limits, the budget cap and the retention window exist for calls that reach a provider or a metered API; a read from a local file server has none of those costs and should not inherit the same ceiling. Where the two meet, the tool call is the unit, and the MCP server page describes the endpoint that serves it. Writing that second server is a smaller job than it sounds, and the Python MCP server tutorial carries one from the SDK's fifteen lines to the deployment that governs it.
How to get started
- Ask the server what it implements. Call
tools/list, thenresources/listandprompts/list; a method-not-found error on the last two is a normal answer, not a broken host. - Point a host at the endpoint using the generated block from the connect page, and confirm which primitives its UI shows as available.
- Read the tools page in the docs before you plan around resources: the seven tools cover fetch, search, compression, dedup, memory, budget and pipeline, which is a different shape from a URI space.
- Watch one pipeline run in the logs view and compare it with what a sampling-capable server would have had to ask your client to do.
- Then read the sibling pages — MCP server for the endpoint and session model, and Model Context Protocol explained for where the protocol stops and your stack starts. A server walked from its configuration entry to a recorded first call is in MCP server example.
Start on the free tier — 2M tokens a month, all seven tools, 120 MCP requests a minute per key — and compare what each plan rates and retains on the pricing page.
Frequently Asked Questions
Does the gateway serve resources or prompts?
Its MCP surface is tools. Resources and prompts are server features that require the server to hold the URI space or the templates, and a gateway does not — so the boundary is stated rather than faked.
What is sampling, in one sentence?
A request that travels from the server to the client asking it to run a model completion, which the client must have declared as a capability during initialisation.
How do I tell which primitives a server implements?
By asking. Call the discovery method for each one and read the response; an unsupported primitive answers with a method-not-found error, which is the expected answer rather than a fault.
Why is there no per-resource rate limit?
Because a limit needs a metered unit. The metered unit here is the tool call, keyed to a credential, and the plan catalog carries a per-key value inside a per-team ceiling.
Do failed calls count against my usage?
They are recorded as failed and excluded from the aggregate. The rows stay for incident review; the totals only count calls that ran.
Can I use my own resources workflow alongside the gateway?
Yes. Run a server that implements resources next to the gateway and keep the governance limits on the calls that pass through it; the two surfaces do not have to be the same server.
Limitations and what this does not do
- This page is not the schema reference. The shapes of
resources/read,prompts/getand the notification methods belong on the MCP tools reference and in the specification; here they appear only as far as governance needs them. - A gateway cannot add a primitive by configuration. Resources and prompts are implementation, not settings, and no plan tier changes that.
- The pipeline is not sampling. It runs the server's own tools, so it cannot summarise with your model, and it cannot use a prompt your client holds.
- Absent rows are not evidence of absent demand. A team that only needed tools will show no resource traffic, and so will a team that wanted resources and found none.
- The tool list is stable; the surface is not eternal. These are seven tools today, and a future
revision could add a resource-backed tool — read
tools/listrather than a cached copy of a page.
Sources
- Model Context Protocol — server resources: https://modelcontextprotocol.io/specification/2026-07-28/server/resources
- Model Context Protocol — server prompts: https://modelcontextprotocol.io/specification/2026-07-28/server/prompts
- Model Context Protocol — client sampling: https://modelcontextprotocol.io/specification/2026-07-28/client/sampling
- Model Context Protocol — server tools, the primitive a gateway meters: https://modelcontextprotocol.io/specification/2025-06-18/server/tools
- Model Context Protocol — latest specification revision: https://modelcontextprotocol.io/specification/2026-07-28
- Anthropic — introducing the Model Context Protocol: https://www.anthropic.com/news/model-context-protocol
- SmartGate — tools and connect documentation: https://smartgate.network/docs/tools