SmartGateSmartGate

Anthropic Model Context Protocol: Names, Clients and Spec Work

Anthropic's implementation of the Model Context Protocol is visible in three places: the names its tooling puts on things, the surfaces its own clients and console expose, and the specification chapters the first implementer wrote and published.

Short answer: Anthropic's implementation of the Model Context Protocol is visible in three places: the names its tooling puts on things, the surfaces its own clients and console expose, and the specification chapters the first implementer wrote and published. This page reads those three in turn — from the organisation and package names down to the handler behind a tool a client calls by name — and leaves the client-versus-gateway question to its sibling pages.

Key takeaways

  • The names are the implementation. An organisation name, a package scope, a configuration key and a tool prefix are decisions the first implementer made and everyone else inherited.
  • Anthropic publishes the protocol, not a certification. The learning path is the specification, the SDKs and the reference servers.
  • The console is organised by name too. Plans, keys and team settings are addresses, and a redirect that preserves its context is the small version of getting naming right.
  • A request can carry a calling budget. A budget object built per caller is how a hosted deployment clamps spend without inventing a private protocol.
  • Tool names are the interface. A client that names a tool with a prefix has already told you which server it belongs to.
  • Next: decide your own naming before you write a tool, because a tool name is the one string every client will hard-code.

The names Anthropic's tooling puts on the protocol

Start with the nouns, because half of what looks like protocol detail is naming. The specification and the reference implementations live under one organisation name, and the language packages live under its scope, which is why a dependency line tells you that you are using the first implementer's tooling rather than a fork. A host that lists servers keeps them in one named object, and the file a desktop host reads is named after that host rather than after the protocol. Then there is the command a user types to register a server, which carries the protocol's abbreviation as a subcommand rather than as a separate tool.

The tool prefix is the name most engineers meet first. A client that reaches a tool through a server does not present the tool as a bare word; it presents a composed name that carries the server and the tool. That prefix exists so two servers can both expose a tool called search without a collision, and it means the server name you chose in a configuration file is now part of a string a model reads. Renaming a server is therefore a breaking change for anything that pinned the old name, which is the practical reason to name servers after what they do rather than after the machine they happen to run on.

Anthropic documents the client side of this, including the configuration commands and the tool naming (Claude Code and MCP), and the specification defines the request shapes underneath it. Where a name has to stay stable across a revision of the protocol itself, which revision you speak is the decision that matters, and the specification walkthrough takes the chapters one at a time.

How the model context protocol carries a caller's budget

The protocol's messages say nothing about cost, which means a deployment that wants to bound spend has to carry that bound itself. The shape below is what that looks like when the bound is built once, from the account a caller belongs to, and attached to the request:

# lib/settings/build-smartgate-context.ts — source lines 15–26 (buildClientBudget)
function buildClientBudget(team: {
  l2BudgetUnit: string | null;
  l2BudgetValue: bigint | null;
  l2EnforceBeta: boolean;
}) {
  if (!team.l2BudgetUnit || team.l2BudgetValue == null) return null;
  return {
    unit: team.l2BudgetUnit,
    value: Number(team.l2BudgetValue),
    enforce_beta: team.l2EnforceBeta,
  };
}

Read the two properties that matter. The function returns a budget object or nothing — and the nothing is not a zero. A caller with no budget configured is a different state from a caller whose budget is exhausted, and collapsing the two would either block a brand-new account or leave an unlimited one unmeasured. The pair it builds is a unit and a value with an enforcement flag beside them, so the accounting unit is explicit rather than assumed: the same number means different things under a token budget and a request budget, and a client that is told which one it is can size the work it sends.

The generalisation to your own endpoint is that per-caller context belongs on the request, not in a lookup table the server keeps by session. A budget read from the caller's own record at request time follows an upgrade immediately, and it survives a restart in which sessions are lost. The placement question — the platform's limit or the team policy, whichever is lower — is worked through in quotas per team, and the Anthropic-side view of what a request carries is in the platform's own API documentation.

Certification, plans and keys: the surfaces around the protocol

There is no Anthropic certification for the protocol. What the first implementer publishes is the specification, the SDKs and a set of reference servers, and the credential-shaped question is answered by reading that material rather than by passing an exam. The confusion is understandable — the ecosystem has courses and the word certification is used loosely around them — so the useful test is whether the artefact comes from the specification's own repository or from a third party's course catalogue.

The console is the other surface, and it is organised the way any product with plans is organised: keys, usage, billing and team settings each have an address, and each address has a name. The excerpt below is the smallest possible statement of that idea — a route that no longer owns a page sends the visitor to the named section that does, preserving whatever the visitor arrived with:

# app/[locale]/(protected)/dashboard/settings/billing/redirect-client.tsx — source lines 6–18 (SettingsBillingRedirectClient)
function SettingsBillingRedirectClient() {
  const searchParams = useSearchParams();

  useEffect(() => {
    const qs = searchParams.toString();
    const target = `/dashboard/settings${qs ? `?${qs}` : ""}#billing`;
    window.location.replace(target);
  }, [searchParams]);

  return (
    <p className="py-8 text-sm text-muted-foreground">Redirecting to billing settings…</p>
  );
}

Two details are worth stealing for your own documentation. The destination is named by a fragment, so the page and the section are separate identities: the route can move without breaking a link that names the section a reader meant. And the query string is carried across, which is how a link keeps its context — a campaign, a workspace, a referral — through a redirect that would otherwise flatten it. Neither line is protocol work, and both are the kind of naming decision that decides whether a deep link into your documentation survives the next reorganisation. The tool list in detail is the other surface a client reads, and unlike the console it is defined by the specification.

The directory question: how a named server gets found

Directories of MCP servers exist in several forms — an official registry with publisher metadata, a curated list in a repository, and the informal list every team keeps in a wiki. They answer a discovery question, not a trust question: a row tells you that a server exists and who claims to own it, and nothing about what its tools do with your data. The naming conventions from the previous sections are what make a directory useful at all, because a scoped package name and a stable server name are the two things a reader can search on.

Resolution is the other half of naming, and it is worth noticing that the pattern generalises:

# app/[locale]/(protected)/dashboard/settings/team/redirect-client.tsx — source lines 5–13 (SettingsTeamRedirectClient)
function SettingsTeamRedirectClient() {
  useEffect(() => {
    window.location.replace("/dashboard/settings#team");
  }, []);

  return (
    <p className="py-8 text-sm text-muted-foreground">Redirecting to team settings…</p>
  );
}

The function is three lines and the decision inside it is the transferable part: an unqualified path resolves to a default destination, and the destination it names is the section, not the page. Apply that to a server directory and the value of a naming scheme becomes obvious — if every entry resolves through one default, then the names in the listing are the only thing a user has to learn. If instead each integration invents its own address, the directory becomes a list of instructions rather than a list of servers, and discovery moves back into a support thread.

For a local setup the directory is mostly irrelevant, because you name one server and keep it. It starts to matter when a team shares a server or when a hosted endpoint's metadata is what another team searches on. Claude clients and gateways covers how the client side of this page's naming story behaves in practice, and the wider set is catalogued beside it, which is what a directory is for.

What "context" means here, and the tool that trims it

The word context in the protocol's name means the material a model can see when it answers, and the size of that material is a property of the model and the client rather than of the protocol. That is why context management shows up as ordinary tooling: something has to decide what fits. The module below wraps a prompt compressor behind a tool, and its structure is a case study in making a lossy operation safe to expose:

# backend/smartgate/modules/context_gate/__init__.py — source lines 16–165 (ContextGateModule)
class ContextGateModule(SmartModule):
    """提示词压缩模块 — 包装 LLMLingua PromptCompressor。"""

    name = "context_gate"
    version = "0.1.0"
    description = "智能提示词压缩 (LLMLingua / LongLLMLingua / LLMLingua-2)"
    dependencies = []

    def __init__(self):
        self._compressor = None
        self._compress_timeout_s = 45.0

    async def initialize(self, resources, config):
        from smartgate.modules.context_gate.algorithm import PromptCompressor

        timeout_raw = config.get("context_gate.compress_timeout_s", 45)
        try:
            self._compress_timeout_s = float(timeout_raw)
        except (TypeError, ValueError):
            self._compress_timeout_s = 45.0
        if self._compress_timeout_s <= 0:
            self._compress_timeout_s = 45.0

        model_path, is_local = resolve_context_gate_model(config)
        logger.info(
            "context_gate loading model: %s (local=%s)",
            model_path, is_local,
        )

        def _load():
            model_config = {"trust_remote_code": True}
            if is_local:
                model_config["local_files_only"] = True
            return PromptCompressor(
                model_name=model_path,
                device_map="cpu",
                use_llmlingua2=True,
                model_config=model_config,
            )

        try:
            self._compressor = await asyncio.to_thread(_load)
        except Exception as e:
            logger.error(
                "context_gate failed to load model %s; compress disabled: %s",
                model_path,
                e,
                exc_info=True,
            )
            self._compressor = None

    async def shutdown(self):
        self._compressor = None

    MAX_INPUT_CHARS = 50_000

    async def process(self, ctx: ToolContext, **params) -> ToolResult:
        if self._compressor is None:
            return ToolResult(
                success=False,
                error="context_gate unavailable (model load failed; check logs and SMARTGATE_CONTEXT_GATE_MODEL)",
            )

        text = params.get("text", "")
        ratio = params.get("ratio", 0.5)
        purpose = (params.get("purpose") or "").strip()
        filter_only = bool(params.get("filter_only"))

        if not text:
            return ToolResult(success=False, error="text is required")

        max_allowed = PURPOSE_FILTER_MAX_CHARS if filter_only else self.MAX_INPUT_CHARS
        if len(text) > max_allowed:
            return ToolResult(
                success=False,
                error=f"text too long ({len(text)} chars), max {max_allowed}",
            )

        filtered = text
        purpose_applied = False
        paragraphs_kept: int | None = None
        paragraphs_total: int | None = None

        if purpose:
            pf = await asyncio.to_thread(filter_by_purpose, text, purpose)
            filtered = pf.filtered_text
            purpose_applied = pf.purpose_applied
            paragraphs_kept = pf.paragraphs_kept
            paragraphs_total = pf.paragraphs_total

            if filter_only:
                return ToolResult(
                    data={
                        "filtered_text": filtered,
                        "purpose_applied": purpose_applied,
                        "paragraphs_kept": paragraphs_kept,
                        "paragraphs_total": paragraphs_total,
                    }
                )

        if len(filtered) > self.MAX_INPUT_CHARS:
            return ToolResult(
                success=False,
                error=(
                    f"text too long after purpose filter ({len(filtered)} chars), "
                    f"max {self.MAX_INPUT_CHARS}"
                ),
            )

        try:
            result = await asyncio.wait_for(
                asyncio.to_thread(
                    self._compressor.compress_prompt,
                    filtered,
                    rate=ratio,
                ),
                timeout=self._compress_timeout_s,
            )
        except asyncio.TimeoutError:
            cap = max(400, int(len(filtered) * max(ratio, 0.3)))
            truncated = filtered[:cap]
            logger.warning(
                "context_gate compress timed out after %.0fs; passthrough %d chars",
                self._compress_timeout_s,
                len(truncated),
            )
            origin_est = max(1, len(filtered) // 4)
            return ToolResult(
                data={
                    "compressed": truncated,
                    "origin_tokens": origin_est,
                    "compressed_tokens": max(1, len(truncated) // 4),
                    "ratio": "1.0x",
                    "purpose_applied": purpose_applied,
                    "passthrough": True,
                    "timed_out": True,
                },
            )

        data = {
            "compressed": result.get("compressed_prompt", ""),
            "origin_tokens": result.get("origin_tokens", 0),
            "compressed_tokens": result.get("compressed_tokens", 0),
            "ratio": result.get("ratio", ratio),
            "purpose_applied": purpose_applied,
        }
        if purpose_applied:
            data["paragraphs_kept"] = paragraphs_kept
            data["paragraphs_total"] = paragraphs_total
        return ToolResult(data=data)

Four decisions are visible in it, and each one is a lesson for any tool that shortens something. The compressor is loaded on first use rather than at import, so the server starts without the model and pays for it only when a caller asks for compression. The input has a hard character ceiling, and the refusal names the limit — a caller can then chunk its own input instead of guessing. The purpose filter runs before compression and can return on its own, which is the option to keep the cheap filter and skip the expensive model entirely. And when compression exceeds its time budget the tool does not fail: it truncates, marks the result as a pass-through and reports that the timeout happened, so a caller can tell an estimate from a real compression.

That last property is the honest one, and it is easy to get wrong in the other direction. A tool that silently returned the original text as if it had been compressed would poison every saving measured downstream. For the client-side view of the same problem, including what a window is and how a client budgets it, staying inside the window is the page to read next; the platform documents its own windows and their limits (context windows).

How Claude Code names a memory tool, and what answers behind the name

A memory tool is a good test case for naming, because "memory" is the requirement and everything else is design. On the wire the tool arrives with the composed name described earlier: the server's name, then the tool's name, so a client can call it without ambiguity. Behind that name sits a handler whose job is to translate a protocol call into a module call and a record:

# backend/smartgate/api/v1/memory.py — source lines 12–40 (handle_memory_add)
@router.post("/api/v1/memory/add", response_model=SmartGateResponse)
async def handle_memory_add(req: MemoryRequest, request: Request):
    if not req.text and not req.messages:
        return SmartGateResponse(
            success=False,
            error={"code": "VALIDATION", "message": "add 需要非空的 text 或 messages"},
            meta={"processing_time_ms": 0, "token_used": 0},
        )

    registry = request.app.state.registry
    audit_hook = request.app.state.audit_hook
    ctx = ToolContext(team_id=team_id_from_request(request))

    start = time.perf_counter()
    result = await registry.get("memory").process(
        ctx, action="add", text=req.text, messages=req.messages, user_id=req.user_id,
    )
    elapsed_ms = (time.perf_counter() - start) * 1000

    await audit_hook(ctx, result, "memory_add", params={
        "text_length": len(req.text or ""), "user_id": req.user_id,
    })

    return SmartGateResponse(
        success=result.success,
        data=result.data,
        error={"code": "ERROR", "message": result.error} if not result.success else None,
        meta={**(result.meta or {}), "processing_time_ms": round(elapsed_ms, 2)},
    )

Three things in it are worth copying. The validation happens before anything is stored, and the failure it returns is the same response envelope as a success — a caller that gets one shape back cannot be broken by a validation error. The context the handler builds carries the tenant extracted from the request rather than taken from the arguments, which is what stops a caller from writing into somebody else's memory by asking politely. And the timing is measured around the module call and merged into the response's metadata, so the latency of one call is part of the answer rather than something reconstructed from a log later.

The naming lesson and the handler lesson are the same one: the name is a promise about the module behind it, and the response envelope is a promise about how every call ends. A server whose tools all answer through one envelope is instrumentable with a single decorator, and one whose tools each invent their own shape is instrumentable one at a time. What the protocol leaves to the implementation is the memory model itself — blocks an agent edits, entities linked into a graph, or plain text keyed by an identifier. Sampling and prompts is where the other server-side primitives are defined.

Anthropic's spec work: what the protocol owes to the first implementer

The protocol did not arrive finished, and the chapters that changed most are the ones the first implementer drove. Three contributions are worth knowing because they explain the shape of today's deployments. The transport chapter replaced a two-endpoint design with a single endpoint that accepts POST and may answer with either a body or an event stream, which is what made a hosted server stateless enough to scale — the older pairing still appears in discussions and serves nothing new. That change is also what turned a server into something you host rather than something you run, and hosting a protocol server covers the decisions a deployment takes on at that point. The security guidance was published as its own chapter rather than as advice in a blog post, and it names the mistakes it has seen: treating a session identifier as authentication, forwarding a caller's token upstream, and giving a whole fleet one credential. And the reference servers were published as small, single-purpose examples rather than as a framework, which kept the protocol independent of any one SDK's opinions.

The through-line is that each contribution is a document rather than a product feature, and that is the part worth imitating. If you extend the protocol — a private field in a metadata object, an envelope of your own around the standard one — write the extension down where your callers can find it, and keep it outside the parts of the message a client is entitled to validate. What a server is covers the endpoint side of those rules, and the chapters themselves are the normative source (specification).

How SmartGate compares with a first-implementer stack

What it is How it is named What it records
Anthropic's clients Hosts that speak the protocol and read a configuration file Server names and composed tool names Session-level, on the client
The specification and SDKs The normative text and the reference implementations Organisation and package scope Nothing; they are documents and libraries
A server you write Your tools behind the standard shapes Your own names, forever Whatever you instrument
SmartGate A hosted endpoint with seven tools behind one URL One server name, the platform's seven tool names One row per call, with per-key limits and a spend cap

The fourth row is the shortest configuration and the longest retention. Nothing about it is a new protocol: the tool names arrive composed, the configuration entry is one object, and the difference is that the record of a call exists without anyone writing an instrumentation layer. The free plan includes all seven tools with 2 million tokens a month and no card; the paid tiers extend retention and raise the ceilings (plans).

How to get started

  1. Read one configuration file end to end. Find the servers object, read the entry names, and note which of them you would have to rename if the machine moved.
  2. Look at a composed tool name in a live client. Seeing the prefix in a real call makes the naming rules concrete in a way a specification chapter does not.
  3. Pick names before you pick an implementation. Server name, tool names and the unit your budget is counted in — those three strings outlive your first version of the code.
  4. Attach the caller's context to the request. A budget object built per caller is cheaper than a session table that has to survive a restart.
  5. Expose one tool and instrument its envelope. One handler, one response shape, one record per call — the hosted endpoint does the same thing without you writing it.
  6. Read the sibling pages for the halves this one leaves out. Building a server in Python is the server half, and putting a gateway in front is the deployment half.

Starting on the free plan takes one entry in a configuration file and the platform's own seven tool names; start free needs no card. Enterprise questions start at the contact form. The other end of the cluster is the shared protocol walkthrough.

Frequently Asked Questions

Limitations and what this does not do

  • Three of the eight planned sections quote no code. Two matched no unique symbol and one was recorded as an abstention after several equally plausible candidates; all three are written from the published specification and the platform documentation. Where a section shows no fence, that is the reason and not an omission.
  • Naming is described, not prescribed. The conventions here are the ones the first implementer chose. A different SDK may present the same facts with different strings, and the specification is what is actually binding.
  • The console excerpts show a pattern, not a page inventory. Which sections exist behind a dashboard is a product decision that changes; the extractable idea is that a destination is named and that its context survives a redirect.
  • One memory handler is one implementation. The protocol defines how a tool is called, not what a memory model should be, and the excerpt says nothing about the storage underneath it.
  • Nothing here audits a deployment. Keys, retention and spend are named because naming them is this page's subject; enforcing them is a different page's job.

Method note

Every fence on this page was cut out of the slice body the SmartGate slice API returned and then re-asserted byte-for-byte as a substring of that body before publication; the first line inside a fence records the file and the exact source lines. The five symbols were pinned by whole-name containment (rule A level 2) and confirmed by the service's slot-proof endpoint before any prose was written here. Nothing was typed from an editor.

Five of the eight planned sections carry a fence. Of the three that do not, two matched no unique symbol and one was recorded as an abstention, which is why this page describes the directory and naming material from the published documentation instead of quoting an implementation of it. Demand figures come from this project's own keyword run, recorded in research_brief.md and search_volume.json: the three memory and retrieval phrasings measured about 50 US searches a month each, and the four protocol phrasings — how it works, certification, directory and meaning — about 30 each.

Slice provenance

# SERP keyword Symbol File Source lines How it was pinned sha256(12)
1 how does model context protocol work buildClientBudget lib/settings/build-smartgate-context.ts 15–26 rule A L2 → slot-proof af3f76f04a4c
2 model context protocol certification SettingsBillingRedirectClient app/[locale]/(protected)/dashboard/settings/billing/redirect-client.tsx 6–18 rule A L2 → slot-proof ac7f4e9ce935
3 model context protocol directory SettingsTeamRedirectClient app/[locale]/(protected)/dashboard/settings/team/redirect-client.tsx 5–13 rule A L2 → slot-proof e21ce8d96908
4 model context protocol meaning ContextGateModule backend/smartgate/modules/context_gate/__init__.py 16–165 rule A L2 → slot-proof 7f472210d81d
5 agent memory system handle_memory_add backend/smartgate/api/v1/memory.py 12–40 rule A L2 → slot-proof 5569c44ffbf1

Every fenced block above was cut from the slice body and re-asserted against it byte-for-byte before publication. 5 of 8 sections pinned, 1 abstentions, 2 misses.