From 472f4468d712ec31f947f260f3ac17504c86a4db Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 01:39:41 +0000 Subject: [PATCH 01/13] docs: add best practices for server and client implementors Adds docs/best-practices.md covering guidance for hosting Server Cards and for discovering/installing servers from an AI Catalog in-session. --- README.md | 8 ++ docs/best-practices.md | 191 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 docs/best-practices.md diff --git a/README.md b/README.md index 235cff3..45c9ae8 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,12 @@ A Server Card describes **remote connectivity only**. Metadata for locally-insta Vendors who genuinely need to attach install hints to a Server Card can use namespaced [`_meta`](https://modelcontextprotocol.io/specification/latest/basic#meta) extension metadata, which remains the card's extension point. +## Best practices + +For recommendations on serving Server Cards (for server implementors) and on discovering +and installing servers from catalogs (for client implementors), see +[best-practices.md](docs/best-practices.md). + ## Layout ``` @@ -73,6 +79,8 @@ The `schema.ts` in this repo is deliberately structured to be copy-pasted into t ## References +- [Best practices for server and client implementors](docs/best-practices.md) +- [Discovery (AI Catalog and Server Card location)](docs/discovery.md) - [SEP-2127: MCP Server Cards (PR)](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127) - [SEP-2133: Extensions framework (PR)](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133) - Abandoned core spec PR (superseded by this repo): [modelcontextprotocol#2652](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2652) diff --git a/docs/best-practices.md b/docs/best-practices.md new file mode 100644 index 0000000..a59c741 --- /dev/null +++ b/docs/best-practices.md @@ -0,0 +1,191 @@ +# Best Practices + +Practical guidance for the two sides of the Server Card ecosystem: people **hosting** +remote MCP servers, and people **building MCP clients** that discover and connect to them. + +This document is advisory. The normative mechanics for Server Cards live in the +[README](../README.md) and [discovery.md](./discovery.md), and the catalog format is defined +by the [AI Catalog specification](https://github.com/Agent-Card/ai-catalog) — this page only +collects recommendations on top of them. + +## Best Practices for Server Implementors + +- **If you host a remote MCP server, we highly recommend you serve a Server Card.** The + card is your server's **connection entry point**: it advertises how to connect — + transport endpoints, supported protocol versions, and a hint at the incoming + requirements a client should expect (such as authentication) — before the client connects, + and without prior configuration. This is valuable on its own: a client that already knows + your MCP URL can point at the card directly, no catalog traversal required. Keep in mind + the card is advisory and read before connecting, so clients reconcile it against the live + connection and + [MUST NOT treat it as authoritative for access control](./discovery.md#consistency-with-runtime-behavior) — + the connection itself remains the source of truth. (The spec's coverage of these incoming + requirements is still expanding — see the pending + [comprehensive auth scenarios](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/13) + and + [optional tool metadata](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/30) + discussions.) +- **Fill out your card completely.** Populate every applicable field — not just the + required minimum. Optional identity fields (`title`, `description`, `icons`, + `repository`, `websiteUrl`) and fully-specified transport metadata make your server + easier to discover, present, and connect to. The card is also the natural home for any + vendor-specific extension data via namespaced [`_meta`](https://modelcontextprotocol.io/specification/latest/basic#meta). +- **Server Cards describe remote connectivity only.** If your server is **not remote**, + there is nothing to serve a card for — Server Cards exist to advertise remote transport + endpoints, and locally-installable server metadata lives in the + [MCP Registry](https://github.com/modelcontextprotocol/registry)'s `server.json` schema + instead (see [Relationship to the MCP Registry](../README.md#relationship-to-the-mcp-registry)). +- **Internal-only but still remote? Serve a card anyway.** Even if your server is not + meant for the public, a card is still worth publishing — some clients may discover and + connect to you this way within your organization. +- **Also link your Server Card from an [AI Catalog](https://github.com/Agent-Card/ai-catalog) + entry.** A card lets a client connect once it has your URL; a catalog is what lets clients + _find_ that URL in the first place — so publish both. An AI Catalog is a cross-protocol + discovery document (served at `/.well-known/ai-catalog.json`) that can index your MCP + server alongside other AI artifacts; see [discovery.md](./discovery.md). Publish it at the + domain people associate with your service: + - For a **public server**, that is your **primary domain** — the domain humans or agents + would naturally associate with your service. + - For an **internal enterprise** server, that is wherever an internal team would first + encounter you — for example the domain hosting your REST API or the other resources a + team becomes aware of _before_ they learn you also expose MCP. + +## Best Practices for Client Implementors + +When a client interacts with a domain that publishes an +[AI Catalog](https://github.com/Agent-Card/ai-catalog) — a `/.well-known/ai-catalog.json` +discovery document that indexes MCP servers alongside other AI artifacts — it can offer to +connect the corresponding server in-session. The probe itself is cheap: one asynchronous, +well-known `GET /.well-known/ai-catalog.json`, run in the background so it never blocks what +the user asked for. + +Where you wire that probe in is a design decision with real range, and there is no single +right answer — **do what fits your client.** You can watch _broadly_, inspecting outbound +requests and keeping a running, cached list of which domains expose a catalog; or _narrowly_, +probing only when a domain enters the session with clear intent. Broader coverage finds more +servers at the cost of more requests, more noise, and more domains learning the user touched +them; narrower coverage is cheaper and quieter but misses some. The mechanisms below — +grounded in [Goose](https://goose-docs.ai/), Block's open-source MCP agent, whose extensions +are themselves MCP servers — span that range, from a network-wide egress sniff to a single +tool hook. Pick the ones that match your architecture and your users' expectations; the +shapes generalize to any client. + +### Watch outbound traffic at the egress boundary + +If your client already mediates network access, that chokepoint is the broadest place to +discover catalogs — it sees every domain the agent actually reaches, not just the ones a +particular tool or file surfaced. Goose's +[macOS sandbox](https://goose-docs.ai/docs/guides/sandbox/) is built exactly this way: the +seatbelt sandbox denies direct network access and forces all outbound traffic through a local +proxy, which evaluates each connection's destination domain against a `blocked.txt` list. +Discovery can ride the same seam as that filtering — as a new destination domain appears at +the proxy, fire a background probe for it and keep a cached `domain → catalog` map (misses +included). This is the comprehensive end of the spectrum, and it composes with the allow / +deny boundary you may already run; the cost of that breadth is noise — most domains publish +no catalog, so the caching and rate-limiting in +[Keep probing cheap](#keep-probing-cheap-and-let-enterprises-scope-it) matter most here. + +### Probe on a deliberate fetch + +The strongest signal is an intentional fetch: the user or the agent chose to retrieve a page +from a domain. Goose exposes exactly this through its +[lifecycle hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks) — a +`hooks.json` maps events such as `PreToolUse` / `PostToolUse` to scripts, with a `matcher` +regular expression that selects which tool the rule runs for (the docs match tool names like +`developer__shell|developer__text_editor`). Match a `PreToolUse` rule to a web-fetch tool — +Goose's Computer Controller extension, for instance, exposes a web-scrape tool (`web_scrape` +in current builds) that retrieves a page — and the hook receives the tool input as JSON, +including the target URL. From there it can fire the catalog probe for that host before or +alongside the fetch, without modifying the tool itself. The same hook shape works for any +URL-bearing tool you choose to match, so you stay in control of _which_ tools trigger a +probe. + +### Probe the domains a project already points at + +A session also carries domains the user has _deliberately_ put in front of the agent: links +in a `.goosehints` file (Goose injects these into the system prompt and supports literal +`https://` URLs), an `AGENTS.md`, or a recipe's configuration. A `SessionStart` or +`UserPromptSubmit` hook can probe that bounded set once per session — these are the domains +the project is built around, a naturally bounded set. + +### Offer a hit as a one-click extension install + +Because a Goose extension _is_ an MCP server, an AI Catalog entry maps straight onto Goose's +existing [install path](https://goose-docs.ai/docs/getting-started/using-extensions) — no +new machinery. When a probe finds an entry, surface it to the user (interrupting the turn or +presenting it passively is your call) and offer a `goose://extension?...` deep link, the same +format Goose's extensions directory generates: + +``` +goose://extension?url=&type=streamable_http&id=&name=&description= +``` + +All parameters are URL-encoded; alternatively, write the equivalent block into Goose's +`config.yaml`. Either way the server is added and connected **mid-turn**. + +### Show the endorsement chain, not just the endpoint + +The domain that publishes a catalog is frequently **not** the domain that hosts the server it +points at. `github.com`'s AI Catalog references a server on `api.githubcopilot.com`; a Google +catalog may point at `googleapis.com`. A prompt that names only the endpoint asks the user to +trust a domain they may not recognize as belonging to the service — which is +indistinguishable, from the user's side, from a phishing prompt. + +Surface the **chain** instead: the domain the user actually interacted with, the fact that it +endorsed the entry, and the domain that will own the connection — "**github.com** lists an MCP +server hosted at **api.githubcopilot.com**." The endorsement is the trust signal the user can +evaluate; the raw endpoint is not. Name both, and make it clear which one the credentials and +traffic will go to. + +### Make installs reachable from outside the session + +In-session discovery is not the only path to a Server Card. A user reading a vendor's docs, a +README, or a catalog listing in a browser is at exactly the moment of intent — and the install +should be one click from there, not a copy-paste of a JSON blob into a config file. + +Clients should therefore **register a URL scheme or install-link handler** so any web page can +hand them a server (Goose's `goose://extension?...`, VS Code's install links). Keep the link +carrying the **catalog entry or Server Card URL** rather than a snapshot of the transport +details, so the card stays the source of truth and the client re-reads it at install time +instead of pinning values that may have moved. The same handler then serves both paths: a probe +that fires mid-session, and a link the user clicked on the open web. + +### Gate the install behind the permission model the user already knows + +Connecting a freshly discovered server is exactly the kind of action a client already gates, +so reuse that model rather than inventing a separate consent flow. Goose has +[permission modes](https://goose-docs.ai/docs/guides/managing-tools/goose-permissions) — +**Completely Autonomous**, **Manual Approval**, **Smart Approval**, **Chat Only** — plus +per-tool levels. Under Manual or Smart Approval, surface "install and connect ``?" as +a one-time approval the user answers with the familiar _always allow_ / _ask before_ / +_never allow_ choices. Discovery should not introduce a consent vocabulary of its own — the +user already knows this one. + +### De-duplicate, and let the user turn it off + +Discovery is only useful if it stays quiet. A client that re-offers a server the user already +runs, or that surfaces every entry on a busy catalog at once, trains the user to dismiss the +prompt without reading it — which costs you the one moment where the endorsement chain above +actually gets evaluated. + +- **Track what is already installed.** Resolve a discovered entry back to a server the user has + configured — match on the Server Card's `name`, and on the transport URLs in `remotes[]` for + servers added before any card existed — and stay silent on a hit. +- **Do not surface a whole catalog.** A domain may list many servers. Offer the one that fits + what the user is doing, or a short ranked set; a wall of options is a dismissal. +- **Remember a "no."** Give the user a durable _don't ask me again_ — per server, and per + domain — and honor it across sessions. A declined install is a preference, not a + per-turn answer. + +### Keep probing cheap, and let enterprises scope it + +Run probes asynchronously and never block the operation the user asked for. Cache the result +per domain — _including misses_, since most domains publish no catalog — and honor the +catalog's `Cache-Control` response headers so you do not re-probe on every touch. When a cached +entry does expire, revalidate rather than refetch: store the `ETag` a catalog or card endpoint +returns and send it back as `If-None-Match`, so an unchanged document costs you a `304` and no +body. Because +each probe reveals to the domain that the user interacted with it, give enterprises control: +an organization might disable in-flight discovery entirely, restrict it to an allowlist, or +route a find into an **IT escalation path that adds the server to a managed gateway** instead +of letting users install servers ad hoc. From 52126c324634d4d3a265472fd032b4846c04e776 Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 01:43:54 +0000 Subject: [PATCH 02/13] docs: restructure server implementor guidance into prose subsections Matches the shape of the client implementor section: an intro paragraph followed by ### subsections, rather than a flat bullet list. --- docs/best-practices.md | 97 +++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index a59c741..7abea4d 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -10,45 +10,64 @@ collects recommendations on top of them. ## Best Practices for Server Implementors -- **If you host a remote MCP server, we highly recommend you serve a Server Card.** The - card is your server's **connection entry point**: it advertises how to connect — - transport endpoints, supported protocol versions, and a hint at the incoming - requirements a client should expect (such as authentication) — before the client connects, - and without prior configuration. This is valuable on its own: a client that already knows - your MCP URL can point at the card directly, no catalog traversal required. Keep in mind - the card is advisory and read before connecting, so clients reconcile it against the live - connection and - [MUST NOT treat it as authoritative for access control](./discovery.md#consistency-with-runtime-behavior) — - the connection itself remains the source of truth. (The spec's coverage of these incoming - requirements is still expanding — see the pending - [comprehensive auth scenarios](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/13) - and - [optional tool metadata](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/30) - discussions.) -- **Fill out your card completely.** Populate every applicable field — not just the - required minimum. Optional identity fields (`title`, `description`, `icons`, - `repository`, `websiteUrl`) and fully-specified transport metadata make your server - easier to discover, present, and connect to. The card is also the natural home for any - vendor-specific extension data via namespaced [`_meta`](https://modelcontextprotocol.io/specification/latest/basic#meta). -- **Server Cards describe remote connectivity only.** If your server is **not remote**, - there is nothing to serve a card for — Server Cards exist to advertise remote transport - endpoints, and locally-installable server metadata lives in the - [MCP Registry](https://github.com/modelcontextprotocol/registry)'s `server.json` schema - instead (see [Relationship to the MCP Registry](../README.md#relationship-to-the-mcp-registry)). -- **Internal-only but still remote? Serve a card anyway.** Even if your server is not - meant for the public, a card is still worth publishing — some clients may discover and - connect to you this way within your organization. -- **Also link your Server Card from an [AI Catalog](https://github.com/Agent-Card/ai-catalog) - entry.** A card lets a client connect once it has your URL; a catalog is what lets clients - _find_ that URL in the first place — so publish both. An AI Catalog is a cross-protocol - discovery document (served at `/.well-known/ai-catalog.json`) that can index your MCP - server alongside other AI artifacts; see [discovery.md](./discovery.md). Publish it at the - domain people associate with your service: - - For a **public server**, that is your **primary domain** — the domain humans or agents - would naturally associate with your service. - - For an **internal enterprise** server, that is wherever an internal team would first - encounter you — for example the domain hosting your REST API or the other resources a - team becomes aware of _before_ they learn you also expose MCP. +If you host a remote MCP server, we highly recommend you serve a Server Card — and publish an +AI Catalog entry that points at it. The two do different jobs: the card is how a client +_connects_ to you, and the catalog is how a client _finds_ you in the first place. The guidance +below covers what to put in the card, what does not belong in one, and where to publish the +catalog entry. + +### Serve a card: it is your connection entry point + +A Server Card advertises how to connect — transport endpoints, supported protocol versions, and +a hint at the incoming requirements a client should expect (such as authentication) — before +the client connects, and without prior configuration. This is valuable on its own, with no +catalog involved: a client that already knows your MCP URL can point at the card directly, no +traversal required. + +Keep in mind the card is advisory and read before connecting, so clients reconcile it against +the live connection and +[MUST NOT treat it as authoritative for access control](./discovery.md#consistency-with-runtime-behavior) — +the connection itself remains the source of truth. The spec's coverage of these incoming +requirements is still expanding; see the pending +[comprehensive auth scenarios](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/13) +and +[optional tool metadata](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/30) +discussions. + +### Fill out your card completely + +Populate every applicable field — not just the required minimum. Optional identity fields +(`title`, `description`, `icons`, `repository`, `websiteUrl`) and fully-specified transport +metadata make your server easier to discover, present, and connect to. The card is also the +natural home for any vendor-specific extension data, via namespaced +[`_meta`](https://modelcontextprotocol.io/specification/latest/basic#meta). + +### Server Cards describe remote connectivity only + +If your server is **not remote**, there is nothing to serve a card for — Server Cards exist to +advertise remote transport endpoints, and locally-installable server metadata lives in the +[MCP Registry](https://github.com/modelcontextprotocol/registry)'s `server.json` schema instead +(see [Relationship to the MCP Registry](../README.md#relationship-to-the-mcp-registry)). + +Internal-only but still remote is a different case: serve a card anyway. Even if your server is +not meant for the public, a card is still worth publishing — some clients may discover and +connect to you this way within your organization. + +### Also link your card from an AI Catalog entry + +A card lets a client connect once it has your URL; an +[AI Catalog](https://github.com/Agent-Card/ai-catalog) is what lets clients find that URL in the +first place — so publish both. An AI Catalog is a cross-protocol discovery document (served at +`/.well-known/ai-catalog.json`) that can index your MCP server alongside other AI artifacts; see +[discovery.md](./discovery.md). + +Publish it at the domain people associate with your service: + +- For a **public server**, that is your **primary domain** — the domain humans or agents would + naturally associate with your service. +- For an **internal enterprise** server, that is wherever an internal team would first encounter + you — for example the domain hosting your REST API or the other resources a team becomes aware + of _before_ they learn you also expose MCP. ## Best Practices for Client Implementors From 05925424bcd05f42b3f29bd9a1270c999578619a Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 01:49:43 +0000 Subject: [PATCH 03/13] docs: lead client guidance with the value case, group by concern Opens with why in-session discovery is worth building, states a default-on recommendation for user-entered URLs, and marks broader triggers (tool results, egress) as opt-in for now. Regroups the subsections into trigger points, install, and security, and adds flow diagrams for the recommended flow and the trigger spectrum. --- docs/best-practices.md | 209 ++++++++++++++++++++++++++--------------- 1 file changed, 131 insertions(+), 78 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 7abea4d..6fc54d0 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -71,67 +71,117 @@ Publish it at the domain people associate with your service: ## Best Practices for Client Implementors -When a client interacts with a domain that publishes an -[AI Catalog](https://github.com/Agent-Card/ai-catalog) — a `/.well-known/ai-catalog.json` -discovery document that indexes MCP servers alongside other AI artifacts — it can offer to -connect the corresponding server in-session. The probe itself is cheap: one asynchronous, -well-known `GET /.well-known/ai-catalog.json`, run in the background so it never blocks what -the user asked for. - -Where you wire that probe in is a design decision with real range, and there is no single -right answer — **do what fits your client.** You can watch _broadly_, inspecting outbound -requests and keeping a running, cached list of which domains expose a catalog; or _narrowly_, -probing only when a domain enters the session with clear intent. Broader coverage finds more -servers at the cost of more requests, more noise, and more domains learning the user touched -them; narrower coverage is cheaper and quieter but misses some. The mechanisms below — -grounded in [Goose](https://goose-docs.ai/), Block's open-source MCP agent, whose extensions -are themselves MCP servers — span that range, from a network-wide egress sniff to a single -tool hook. Pick the ones that match your architecture and your users' expectations; the -shapes generalize to any client. - -### Watch outbound traffic at the egress boundary - -If your client already mediates network access, that chokepoint is the broadest place to -discover catalogs — it sees every domain the agent actually reaches, not just the ones a -particular tool or file surfaced. Goose's -[macOS sandbox](https://goose-docs.ai/docs/guides/sandbox/) is built exactly this way: the -seatbelt sandbox denies direct network access and forces all outbound traffic through a local -proxy, which evaluates each connection's destination domain against a `blocked.txt` list. -Discovery can ride the same seam as that filtering — as a new destination domain appears at -the proxy, fire a background probe for it and keep a cached `domain → catalog` map (misses -included). This is the comprehensive end of the spectrum, and it composes with the allow / -deny boundary you may already run; the cost of that breadth is noise — most domains publish -no catalog, so the caching and rate-limiting in -[Keep probing cheap](#keep-probing-cheap-and-let-enterprises-scope-it) matter most here. - -### Probe on a deliberate fetch - -The strongest signal is an intentional fetch: the user or the agent chose to retrieve a page -from a domain. Goose exposes exactly this through its -[lifecycle hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks) — a -`hooks.json` maps events such as `PreToolUse` / `PostToolUse` to scripts, with a `matcher` -regular expression that selects which tool the rule runs for (the docs match tool names like -`developer__shell|developer__text_editor`). Match a `PreToolUse` rule to a web-fetch tool — -Goose's Computer Controller extension, for instance, exposes a web-scrape tool (`web_scrape` -in current builds) that retrieves a page — and the hook receives the tool input as JSON, -including the target URL. From there it can fire the catalog probe for that host before or -alongside the fetch, without modifying the tool itself. The same hook shape works for any -URL-bearing tool you choose to match, so you stay in control of _which_ tools trigger a -probe. - -### Probe the domains a project already points at - -A session also carries domains the user has _deliberately_ put in front of the agent: links +### Why you want this + +Every MCP server your client can reach is capability you did not have to build. A user who +connects one gets more done without leaving you, and comes back for it — servers make a client +more useful and stickier, at someone else's development cost. That has never been the hard +part. The hard part is that connecting a server is a chore the user has to go and do somewhere +else, out of band, before it can help them — so most users never do it, and the capability +sits unclaimed. + +A standardized place to publish Server Cards is what closes that gap. Because a service can +now advertise how to connect to it at a well-known location, your client can offer the +connection **at the moment the user is already referencing that service** — mid-session, in +context, instead of in a settings pane the user visits a week later, if ever. + +If you wire that in where we recommend below, it costs you remarkably little: + +- **No new UX to design.** You already ask users to approve consequential actions. This is one + more approval, in the flow they already know. +- **No new trust to establish.** You only ever offer servers published by a domain the user + themselves just put in front of you. You are not recommending anyone — the domain the user + named is. +- **No discovery problem to solve.** No ranking, no index, no crawl, no editorial judgment. + The user supplied the domain; you are just asking it what it offers. + +The outcome compounds in every direction: the user gets a capability exactly when they need +it, your client gets stickier, the service gets reached by agents that would otherwise have +scraped it or given up, and the pie grows as complementary AI services get strung together. + +### Where to trigger discovery + +The probe itself is always the same and always cheap: one asynchronous +`GET /.well-known/ai-catalog.json`, run in the background so it never blocks what the user +asked for. The design decision is not _how_ to probe — it is **which moments** in a session +should trigger one. That question has real range, and the answer is not the same for every +client. + +#### Start here: probe the domains a user hands you + +**At minimum, we recommend a default-on experience that probes any domain a user enters as a +URL.** This is the strongest signal in the session and the safest place to begin: the user +typed or pasted the domain themselves, so there is no ambiguity about intent, no inference, +and no domain touched that the user did not already name. + +```mermaid +flowchart TD + A[User enters a URL in the session] --> B[Probe domain's /.well-known/ai-catalog.json] + B --> C{AI Catalog found?} + C -->|No| D[Cache the miss, stay silent] + C -->|Yes| E[Select application/mcp-server-card+json entries] + E --> F{Already installed, or previously declined?} + F -->|Yes| D + F -->|No| G[Fetch the Server Card from the entry's url] + G --> H["Offer install — naming the endorsement chain"] + H --> I[User approves in the existing permission flow] + I --> J[Server connected mid-turn] +``` + +A closely related and equally bounded set: the domains a **project** already points at — links in a `.goosehints` file (Goose injects these into the system prompt and supports literal `https://` URLs), an `AGENTS.md`, or a recipe's configuration. A `SessionStart` or -`UserPromptSubmit` hook can probe that bounded set once per session — these are the domains -the project is built around, a naturally bounded set. +`UserPromptSubmit` hook can probe that set once per session. These are the domains the project +is built around, the user put them there deliberately, and there are only ever a handful. + +#### Expand carefully: broader triggers, off by default for now + +Beyond user-supplied URLs, the same probe can hang off progressively broader signals. These +find more servers and touch more domains, and the trade is the same each time: **broader +coverage, at the cost of more requests, more noise, and more domains learning the user +interacted with them.** + +```mermaid +flowchart LR + A["User-entered URLs
(recommended, default-on)"] --> P[Shared probe + cache] + B["Project files
(.goosehints, AGENTS.md, recipes)"] --> P + C["Tool-call results
(web fetch, scrape)"] -.opt-in.-> P + D["Network egress boundary
(every domain reached)"] -.opt-in.-> P + P --> E[domain → catalog map, misses cached] + E --> F[Install offer] +``` -### Offer a hit as a one-click extension install +- **Tool-call results.** The user or the agent chose to retrieve a page, which is close to + direct intent. Goose exposes this through its + [lifecycle hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks) — a + `hooks.json` maps events such as `PreToolUse` / `PostToolUse` to scripts, with a `matcher` + regular expression selecting which tool the rule runs for (the docs match tool names like + `developer__shell|developer__text_editor`). Match a `PreToolUse` rule to a web-fetch tool — + Goose's Computer Controller extension, for instance, exposes a web-scrape tool (`web_scrape` + in current builds) — and the hook receives the tool input as JSON, including the target URL, + so it can fire the probe without modifying the tool itself. +- **The network egress boundary.** The broadest option: if your client already mediates + network access, that chokepoint sees every domain the agent actually reaches. Goose's + [macOS sandbox](https://goose-docs.ai/docs/guides/sandbox/) is built this way — the seatbelt + sandbox denies direct network access and forces outbound traffic through a local proxy that + evaluates each destination against a `blocked.txt` list. Discovery can ride the same seam as + that filtering. It composes with an allow/deny boundary you may already run, but it is also + where the noise is worst: most domains publish no catalog, so the caching in + [Keep probing cheap](#keep-probing-cheap-and-let-enterprises-scope-it) matters most here. + +**We do not recommend turning these on by default at this time.** Ship them opt-in, behind a +setting, while the ecosystem and the interaction pattern are still young — the default-on +case above is the one where the user's intent is unambiguous, and it is worth learning how +that experience lands before widening the aperture. As implementations gather evidence, this +guidance may change. + +### Turning a hit into a connection + +#### Offer it as a one-click install Because a Goose extension _is_ an MCP server, an AI Catalog entry maps straight onto Goose's -existing [install path](https://goose-docs.ai/docs/getting-started/using-extensions) — no -new machinery. When a probe finds an entry, surface it to the user (interrupting the turn or +existing [install path](https://goose-docs.ai/docs/getting-started/using-extensions) — no new +machinery. When a probe finds an entry, surface it to the user (interrupting the turn or presenting it passively is your call) and offer a `goose://extension?...` deep link, the same format Goose's extensions directory generates: @@ -142,7 +192,22 @@ goose://extension?url=&type=streamable_http&id=&name= Date: Sun, 19 Jul 2026 19:10:11 -0700 Subject: [PATCH 04/13] Refinements --- README.md | 2 +- docs/best-practices.md | 45 ++++++++++++++++-------------------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 45c9ae8..5dfe1dd 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Vendors who genuinely need to attach install hints to a Server Card can use name ## Best practices For recommendations on serving Server Cards (for server implementors) and on discovering -and installing servers from catalogs (for client implementors), see +and installing servers by way of AI Catalogs (for client implementors), see [best-practices.md](docs/best-practices.md). ## Layout diff --git a/docs/best-practices.md b/docs/best-practices.md index 6fc54d0..163399f 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -1,4 +1,4 @@ -# Best Practices +# MCP Server Card Best Practices Practical guidance for the two sides of the Server Card ecosystem: people **hosting** remote MCP servers, and people **building MCP clients** that discover and connect to them. @@ -21,31 +21,23 @@ catalog entry. A Server Card advertises how to connect — transport endpoints, supported protocol versions, and a hint at the incoming requirements a client should expect (such as authentication) — before the client connects, and without prior configuration. This is valuable on its own, with no -catalog involved: a client that already knows your MCP URL can point at the card directly, no -traversal required. +catalog involved: a client that already knows your MCP URL can point at the card directly. Keep in mind the card is advisory and read before connecting, so clients reconcile it against the live connection and [MUST NOT treat it as authoritative for access control](./discovery.md#consistency-with-runtime-behavior) — -the connection itself remains the source of truth. The spec's coverage of these incoming -requirements is still expanding; see the pending -[comprehensive auth scenarios](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/13) -and -[optional tool metadata](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/30) -discussions. +the connection itself remains the source of truth. ### Fill out your card completely Populate every applicable field — not just the required minimum. Optional identity fields (`title`, `description`, `icons`, `repository`, `websiteUrl`) and fully-specified transport -metadata make your server easier to discover, present, and connect to. The card is also the -natural home for any vendor-specific extension data, via namespaced -[`_meta`](https://modelcontextprotocol.io/specification/latest/basic#meta). +metadata make your server easier to discover, present, and connect to. ### Server Cards describe remote connectivity only If your server is **not remote**, there is nothing to serve a card for — Server Cards exist to -advertise remote transport endpoints, and locally-installable server metadata lives in the +advertise remote transport endpoints only. Locally-installable server metadata lives in the [MCP Registry](https://github.com/modelcontextprotocol/registry)'s `server.json` schema instead (see [Relationship to the MCP Registry](../README.md#relationship-to-the-mcp-registry)). @@ -53,7 +45,7 @@ Internal-only but still remote is a different case: serve a card anyway. Even if not meant for the public, a card is still worth publishing — some clients may discover and connect to you this way within your organization. -### Also link your card from an AI Catalog entry +### Link your card from an AI Catalog entry A card lets a client connect once it has your URL; an [AI Catalog](https://github.com/Agent-Card/ai-catalog) is what lets clients find that URL in the @@ -69,26 +61,25 @@ Publish it at the domain people associate with your service: you — for example the domain hosting your REST API or the other resources a team becomes aware of _before_ they learn you also expose MCP. -## Best Practices for Client Implementors +Review the guidance below for Client Implementors to determine the appropriate domain for your service. For example, for GitHub, it would be common for the user of a coding agent to paste a URL like `https://github.com/modelcontextprotocol/experimental-ext-server-card/pull/36` into a session. So `github.com/.well-known/ai-catalog.json` is an excellent place to put your AI catalog - not `githubcopilot.com/.well-known/ai-catalog.json`, where GitHub's MCP server [happens to live](https://github.com/github/github-mcp-server). -### Why you want this +## Best Practices for Client Implementors Every MCP server your client can reach is capability you did not have to build. A user who connects one gets more done without leaving you, and comes back for it — servers make a client -more useful and stickier, at someone else's development cost. That has never been the hard -part. The hard part is that connecting a server is a chore the user has to go and do somewhere +more useful and stickier, at someone else's development cost. The hard part is that connecting a server is a chore the user has to go and do somewhere else, out of band, before it can help them — so most users never do it, and the capability sits unclaimed. -A standardized place to publish Server Cards is what closes that gap. Because a service can -now advertise how to connect to it at a well-known location, your client can offer the +As per above guidance for server implementors, we now have a way for them to advertise +their service in an easy-to-find, standardized location. Using this, your client can offer the connection **at the moment the user is already referencing that service** — mid-session, in -context, instead of in a settings pane the user visits a week later, if ever. +context, instead of in a settings pane the user never visits. If you wire that in where we recommend below, it costs you remarkably little: -- **No new UX to design.** You already ask users to approve consequential actions. This is one - more approval, in the flow they already know. +- **No new UX to design.** You already ask users to approve consequential actions like tool calls. This is one + more approval, in a flow they already know. - **No new trust to establish.** You only ever offer servers published by a domain the user themselves just put in front of you. You are not recommending anyone — the domain the user named is. @@ -104,8 +95,7 @@ scraped it or given up, and the pie grows as complementary AI services get strun The probe itself is always the same and always cheap: one asynchronous `GET /.well-known/ai-catalog.json`, run in the background so it never blocks what the user asked for. The design decision is not _how_ to probe — it is **which moments** in a session -should trigger one. That question has real range, and the answer is not the same for every -client. +should trigger one. #### Start here: probe the domains a user hands you @@ -157,8 +147,7 @@ flowchart LR `hooks.json` maps events such as `PreToolUse` / `PostToolUse` to scripts, with a `matcher` regular expression selecting which tool the rule runs for (the docs match tool names like `developer__shell|developer__text_editor`). Match a `PreToolUse` rule to a web-fetch tool — - Goose's Computer Controller extension, for instance, exposes a web-scrape tool (`web_scrape` - in current builds) — and the hook receives the tool input as JSON, including the target URL, + Goose's Computer Controller extension, for instance, exposes a web-scrape tool — and the hook receives the tool input as JSON, including the target URL, so it can fire the probe without modifying the tool itself. - **The network egress boundary.** The broadest option: if your client already mediates network access, that chokepoint sees every domain the agent actually reaches. Goose's @@ -179,7 +168,7 @@ guidance may change. #### Offer it as a one-click install -Because a Goose extension _is_ an MCP server, an AI Catalog entry maps straight onto Goose's +A Goose extension _is_ an MCP server, so an MCP Server Card maps straight onto Goose's existing [install path](https://goose-docs.ai/docs/getting-started/using-extensions) — no new machinery. When a probe finds an entry, surface it to the user (interrupting the turn or presenting it passively is your call) and offer a `goose://extension?...` deep link, the same From d57f37a3a42e2013bab1f0affe678590b9d2c7cd Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 02:12:16 +0000 Subject: [PATCH 05/13] docs: tighten client install consent, drop out-of-session install path - Remove the URL-scheme / install-link guidance as nonstandard - Add a minimal prompt mockup showing the endorsement chain - Require explicit approval for every install; offer session-scoped vs persistent accept instead of a blanket always-allow - Soften the catalog-culling advice to an invitation to experiment - Expand the enterprise path into a pre-filled request-to-IT flow --- docs/best-practices.md | 140 ++++++++++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 38 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 163399f..61d238f 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -114,8 +114,10 @@ flowchart TD F -->|Yes| D F -->|No| G[Fetch the Server Card from the entry's url] G --> H["Offer install — naming the endorsement chain"] - H --> I[User approves in the existing permission flow] - I --> J[Server connected mid-turn] + H --> I{User approves?} + I -->|This session| J[Server connected mid-turn] + I -->|Always| K[Server connected and persisted to config] + I -->|Not now| L[Remember the decline] ``` A closely related and equally bounded set: the domains a **project** already points at — links @@ -164,9 +166,7 @@ case above is the one where the user's intent is unambiguous, and it is worth le that experience lands before widening the aperture. As implementations gather evidence, this guidance may change. -### Turning a hit into a connection - -#### Offer it as a one-click install +### Turn a hit into a one-click install A Goose extension _is_ an MCP server, so an MCP Server Card maps straight onto Goose's existing [install path](https://goose-docs.ai/docs/getting-started/using-extensions) — no new @@ -181,19 +181,6 @@ goose://extension?url=&type=streamable_http&id=&name=`?" as -a one-time approval the user answers with the familiar _always allow_ / _ask before_ / -_never allow_ choices. Discovery should not introduce a consent vocabulary of its own — the -user already knows this one. +endorsed the entry, and the domain that will own the connection. The endorsement is the trust +signal the user can evaluate; the raw endpoint is not. Name both, and make it clear which one +the credentials and traffic will go to. + +A minimal prompt carrying that chain: + +``` +┌──────────────────────────────────────────────────────────┐ +│ Connect an MCP server? │ +│ │ +│ github.com lists a server it does not host: │ +│ │ +│ GitHub github.com │ +│ └── hosted at api.githubcopilot.com │ +│ │ +│ Requests and any credentials you approve will go to │ +│ api.githubcopilot.com. │ +│ │ +│ [ Not now ] [ This session ] [ Always ] │ +└──────────────────────────────────────────────────────────┘ +``` + +Two things are doing the work here. The endorsing domain is the one the user recognizes and +just interacted with, so it leads. The hosting domain is named as a **consequence** — where +traffic and credentials actually go — rather than as a bare URL the user is asked to +pattern-match against a brand they know. + +#### Always ask before installing — and be stricter here than you are with tools + +Reuse the consent surface your users already know; do not invent a second vocabulary for +discovery. But hold discovery to a **higher bar than tool calls**. Approving a tool call +authorizes one action by code the user already chose to run. Installing a discovered server +adds a new, unvetted counterparty to the session — one that will expose its own tools, receive +its own inputs, and persist if the user lets it. That asymmetry means the autonomy settings a +client offers for tools should not simply extend to installs: + +- **A discovered server MUST NOT be installed automatically.** No "completely autonomous" mode + should silently connect one. The approval dialog is load-bearing: the endorsing domain is the + user's only trust signal, and skipping the prompt discards it. +- **There is no blanket "always allow."** An install approval is scoped to _that server_, never + to discovery in general and never to a domain's future entries. + +Offer the user two accept paths, which is the distinction the prompt above encodes: + +- **This session** — connect now, discard at session end. The right default for a server the + user is trying out; a mistake costs one session. +- **Always** — persist the server to the user's configuration, as if they had installed it + themselves. This is the equivalent of _always allow_, scoped to that one server. + +Together with **Not now**, that is the whole vocabulary. A user who wants a server permanently +can say so in one click, and a user who is merely curious is not talked into a permanent +change to their setup. #### De-duplicate, and let the user turn it off @@ -231,8 +255,11 @@ actually gets evaluated. - **Track what is already installed.** Resolve a discovered entry back to a server the user has configured — match on the Server Card's `name`, and on the transport URLs in `remotes[]` for servers added before any card existed — and stay silent on a hit. -- **Do not surface a whole catalog.** A domain may list many servers. Offer the one that fits - what the user is doing, or a short ranked set; a wall of options is a dismissal. +- **Consider culling a long catalog.** A domain may list many servers. Presenting the full list + is usually fine — most catalogs are short. But if a catalog is long, there is room to get + creative: an inference step over the entries' descriptions, weighed against what the user is + actually doing in the session, can surface the two or three that fit and tuck the rest behind + a "show all." This is an open design space, and we would like to see what clients try. - **Remember a "no."** Give the user a durable _don't ask me again_ — per server, and per domain — and honor it across sessions. A declined install is a preference, not a per-turn answer. @@ -247,6 +274,43 @@ returns and send it back as `If-None-Match`, so an unchanged document costs you body. Because each probe reveals to the domain that the user interacted with it, give enterprises -control: an organization might disable in-flight discovery entirely, restrict it to an -allowlist, or route a find into an **IT escalation path that adds the server to a managed -gateway** instead of letting users install servers ad hoc. +control: an organization might disable in-session discovery entirely, or restrict it to an +allowlist of domains. + +Outright disabling is the blunt option, and it throws away the signal. In a managed +environment, the interesting move is to **keep discovering and change what a hit does**. An +employee who hits a useful server is exactly the demand signal a platform team wants, and today +that signal is lost — the user shrugs, works around it, and nobody learns the server was +wanted. Instead of offering a direct install, route the find into the organization's existing +approval path: the client swaps the install button for a request, pre-filled from the catalog +entry and Server Card it already fetched, so the user supplies intent and the client supplies +the technical detail. + +``` +┌──────────────────────────────────────────────────────────┐ +│ Request this server from IT │ +│ │ +│ Direct installs are disabled by your organization. │ +│ │ +│ Server GitHub │ +│ Listed by github.com │ +│ Hosted at api.githubcopilot.com │ +│ Auth OAuth 2.0 │ +│ │ +│ Why do you need it? │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Reviewing PRs in the session — would let the agent │ │ +│ │ read diffs directly instead of pasting them. │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ [ Cancel ] [ Send request ] │ +└──────────────────────────────────────────────────────────┘ +``` + +Where that request lands is the organization's business — a ticket, a Slack approval, a pull +request against a gateway's server list. What matters for a client implementor is that the +Server Card gives you everything the approver needs (identity, endpoint, transport, auth +posture) without the user having to hunt for it, and that an approved request ends with the +server added to the **managed gateway** the whole org already connects through, rather than +installed ad hoc on one laptop. The user gets a path instead of a dead end, and the platform +team gets a queue of real, evidenced demand. From de65e765fa70c9da8330fe449ebe6918252808da Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 02:47:30 +0000 Subject: [PATCH 06/13] docs: demote Goose from framing device to illustration Each client mechanism now leads with the general shape any client can implement; Goose specifics follow in parentheses as one concrete instance. Drops the verbatim goose:// deep-link template. --- docs/best-practices.md | 71 ++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 61d238f..81a1ecb 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -97,6 +97,11 @@ The probe itself is always the same and always cheap: one asynchronous asked for. The design decision is not _how_ to probe — it is **which moments** in a session should trigger one. +Where a concrete example helps, this section points at [Goose](https://goose-docs.ai/), Block's +open-source MCP agent, simply because it is open source and its hooks are easy to read. Nothing +here is Goose-specific — every mechanism below has an equivalent in any client that runs tools, +reads project files, or mediates network access. + #### Start here: probe the domains a user hands you **At minimum, we recommend a default-on experience that probes any domain a user enters as a @@ -121,10 +126,10 @@ flowchart TD ``` A closely related and equally bounded set: the domains a **project** already points at — links -in a `.goosehints` file (Goose injects these into the system prompt and supports literal -`https://` URLs), an `AGENTS.md`, or a recipe's configuration. A `SessionStart` or -`UserPromptSubmit` hook can probe that set once per session. These are the domains the project -is built around, the user put them there deliberately, and there are only ever a handful. +in an `AGENTS.md`, a project config, or whatever hints file your client injects into the system +prompt (in Goose, `.goosehints`, which supports literal `https://` URLs). Probe that set once at +session start rather than on every turn. These are domains the project is built around, the user +put them there deliberately, and there are only ever a handful. #### Expand carefully: broader triggers, off by default for now @@ -136,7 +141,7 @@ interacted with them.** ```mermaid flowchart LR A["User-entered URLs
(recommended, default-on)"] --> P[Shared probe + cache] - B["Project files
(.goosehints, AGENTS.md, recipes)"] --> P + B["Project files
(AGENTS.md, hints files, recipes)"] --> P C["Tool-call results
(web fetch, scrape)"] -.opt-in.-> P D["Network egress boundary
(every domain reached)"] -.opt-in.-> P P --> E[domain → catalog map, misses cached] @@ -144,21 +149,22 @@ flowchart LR ``` - **Tool-call results.** The user or the agent chose to retrieve a page, which is close to - direct intent. Goose exposes this through its - [lifecycle hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks) — a - `hooks.json` maps events such as `PreToolUse` / `PostToolUse` to scripts, with a `matcher` - regular expression selecting which tool the rule runs for (the docs match tool names like - `developer__shell|developer__text_editor`). Match a `PreToolUse` rule to a web-fetch tool — - Goose's Computer Controller extension, for instance, exposes a web-scrape tool — and the hook receives the tool input as JSON, including the target URL, - so it can fire the probe without modifying the tool itself. -- **The network egress boundary.** The broadest option: if your client already mediates - network access, that chokepoint sees every domain the agent actually reaches. Goose's - [macOS sandbox](https://goose-docs.ai/docs/guides/sandbox/) is built this way — the seatbelt - sandbox denies direct network access and forces outbound traffic through a local proxy that - evaluates each destination against a `blocked.txt` list. Discovery can ride the same seam as - that filtering. It composes with an allow/deny boundary you may already run, but it is also - where the noise is worst: most domains publish no catalog, so the caching in + direct intent. If your client can intercept tool calls, select the URL-bearing ones — a web + fetch or scrape — and read the target host out of the tool's input before or alongside the + call. This needs no change to the tool itself, and you stay in control of _which_ tools + trigger a probe. (Goose implements this shape with + [lifecycle hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks): a `hooks.json` + maps `PreToolUse` / `PostToolUse` to scripts, with a `matcher` regex selecting the tool and + the tool input handed to the hook as JSON.) +- **The network egress boundary.** The broadest option: if your client already mediates network + access, that chokepoint sees every domain the agent actually reaches, not just the ones a tool + or file surfaced — so discovery can ride the same seam as the filtering you already do there. + It composes with an allow/deny boundary you may already run, but it is also where the noise is + worst: most domains publish no catalog, so the caching in [Keep probing cheap](#keep-probing-cheap-and-let-enterprises-scope-it) matters most here. + (Goose's [macOS sandbox](https://goose-docs.ai/docs/guides/sandbox/) is built this way — a + seatbelt sandbox denies direct network access and forces outbound traffic through a local + proxy that checks each destination against a list.) **We do not recommend turning these on by default at this time.** Ship them opt-in, behind a setting, while the ecosystem and the interaction pattern are still young — the default-on @@ -168,18 +174,21 @@ guidance may change. ### Turn a hit into a one-click install -A Goose extension _is_ an MCP server, so an MCP Server Card maps straight onto Goose's -existing [install path](https://goose-docs.ai/docs/getting-started/using-extensions) — no new -machinery. When a probe finds an entry, surface it to the user (interrupting the turn or -presenting it passively is your call) and offer a `goose://extension?...` deep link, the same -format Goose's extensions directory generates: - -``` -goose://extension?url=&type=streamable_http&id=&name=&description= -``` - -All parameters are URL-encoded; alternatively, write the equivalent block into Goose's -`config.yaml`. Either way the server is added and connected **mid-turn**. +You almost certainly do not need new machinery for this. Your client already has a way to add +an MCP server — a config file, an install command, a deep link from an extensions directory — +and a Server Card carries exactly what that path needs: the endpoint, the transport, and the +identity to display. Discovery just supplies those values from a catalog instead of from a user +who typed them. + +So when a probe finds an entry, surface it (interrupting the turn or presenting it passively is +your call) and route the accept into the install path you already have. The one thing worth +insisting on is that the server is added and connected **mid-turn** — the value here is the +user not having to leave what they were doing. + +For a concrete instance: in Goose an extension _is_ an MCP server, so a card maps onto its +existing [install path](https://goose-docs.ai/docs/getting-started/using-extensions) directly — +either the `goose://extension?...` deep link its extensions directory generates, or the +equivalent block written into `config.yaml`. ### Security and trust considerations From 2ce630b52454bb706b53773add51e2b783853fdd Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 03:07:23 +0000 Subject: [PATCH 07/13] docs: correct factual errors, add SSRF bounds, drop endorsement framing - description is required, not optional; remove the auth field from the IT-request mock (no such field exists; issue #13 is v2) - Goose is AAIF-hosted, not Block's; drop the 404 sandbox link; hooks live in a plugin's hooks/hooks.json per the Open Plugins spec - Genericize the GitHub mockups to example.com / mcp-host-saas.com - Replace 'endorsement' with 'listing', and note the trust manifest is the spec's actual mechanism where present - Add probe bounds: no IP literals, loopback, link-local or private ranges; re-check after DNS; no ambient credentials; bound redirects - De-dup on the server endpoint rather than self-asserted name, and move the check after the card fetch - Note catalog nesting, depth cap and cycle tracking; note inline entries - Lowercase the invented MUST NOT; cut 'at someone else's development cost' --- docs/best-practices.md | 139 ++++++++++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 81a1ecb..26f2d77 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -30,9 +30,9 @@ the connection itself remains the source of truth. ### Fill out your card completely -Populate every applicable field — not just the required minimum. Optional identity fields -(`title`, `description`, `icons`, `repository`, `websiteUrl`) and fully-specified transport -metadata make your server easier to discover, present, and connect to. +Populate every applicable field — not just the required minimum. `description` is required, but +the optional identity fields (`title`, `icons`, `repository`, `websiteUrl`) and fully-specified +transport metadata are what make your server easy to discover, present, and connect to. ### Server Cards describe remote connectivity only @@ -61,15 +61,15 @@ Publish it at the domain people associate with your service: you — for example the domain hosting your REST API or the other resources a team becomes aware of _before_ they learn you also expose MCP. -Review the guidance below for Client Implementors to determine the appropriate domain for your service. For example, for GitHub, it would be common for the user of a coding agent to paste a URL like `https://github.com/modelcontextprotocol/experimental-ext-server-card/pull/36` into a session. So `github.com/.well-known/ai-catalog.json` is an excellent place to put your AI catalog - not `githubcopilot.com/.well-known/ai-catalog.json`, where GitHub's MCP server [happens to live](https://github.com/github/github-mcp-server). +Review the guidance below for Client Implementors to determine the appropriate domain for your service. For example, for GitHub, it would be common for the user of a coding agent to paste a URL like `https://github.com/modelcontextprotocol/experimental-ext-server-card/pull/36` into a session. So `github.com/.well-known/ai-catalog.json` is an excellent place to put your AI catalog - not `api.githubcopilot.com/.well-known/ai-catalog.json`, where GitHub's MCP server [happens to live](https://github.com/github/github-mcp-server). ## Best Practices for Client Implementors Every MCP server your client can reach is capability you did not have to build. A user who connects one gets more done without leaving you, and comes back for it — servers make a client -more useful and stickier, at someone else's development cost. The hard part is that connecting a server is a chore the user has to go and do somewhere -else, out of band, before it can help them — so most users never do it, and the capability -sits unclaimed. +more useful and stickier. The hard part is that connecting a server is a chore the user has to +go and do somewhere else, out of band, before it can help them — so most users never do it, and +the capability sits unclaimed. As per above guidance for server implementors, we now have a way for them to advertise their service in an easy-to-find, standardized location. Using this, your client can offer the @@ -97,8 +97,9 @@ The probe itself is always the same and always cheap: one asynchronous asked for. The design decision is not _how_ to probe — it is **which moments** in a session should trigger one. -Where a concrete example helps, this section points at [Goose](https://goose-docs.ai/), Block's -open-source MCP agent, simply because it is open source and its hooks are easy to read. Nothing +Where a concrete example helps, this section points at [Goose](https://goose-docs.ai/), an +open-source MCP agent hosted by the Agentic AI Foundation, simply because its hooks and install +path are easy to read. Nothing here is Goose-specific — every mechanism below has an equivalent in any client that runs tools, reads project files, or mediates network access. @@ -115,16 +116,24 @@ flowchart TD B --> C{AI Catalog found?} C -->|No| D[Cache the miss, stay silent] C -->|Yes| E[Select application/mcp-server-card+json entries] - E --> F{Already installed, or previously declined?} + E --> G[Resolve the Server Card for each entry] + G --> F{Endpoint already installed, or previously declined?} F -->|Yes| D - F -->|No| G[Fetch the Server Card from the entry's url] - G --> H["Offer install — naming the endorsement chain"] + F -->|No| H["Offer install — naming the listing chain"] H --> I{User approves?} I -->|This session| J[Server connected mid-turn] I -->|Always| K[Server connected and persisted to config] I -->|Not now| L[Remember the decline] ``` +Two details worth getting right when you walk the entries. A catalog can **nest** — an entry +whose `type` is `application/ai-catalog+json` points at another catalog rather than an artifact +— so a client that filters for `application/mcp-server-card+json` and stops will silently miss +servers on any domain that organizes that way. Recurse, but bound it: the specification +recommends a maximum depth of 4, and you should track visited URLs so a cycle cannot walk you in +circles. Second, an entry carries its artifact either by reference or inline, so not every entry +requires a fetch. + A closely related and equally bounded set: the domains a **project** already points at — links in an `AGENTS.md`, a project config, or whatever hints file your client injects into the system prompt (in Goose, `.goosehints`, which supports literal `https://` URLs). Probe that set once at @@ -153,18 +162,20 @@ flowchart LR fetch or scrape — and read the target host out of the tool's input before or alongside the call. This needs no change to the tool itself, and you stay in control of _which_ tools trigger a probe. (Goose implements this shape with - [lifecycle hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks): a `hooks.json` - maps `PreToolUse` / `PostToolUse` to scripts, with a `matcher` regex selecting the tool and - the tool input handed to the hook as JSON.) + [lifecycle hooks](https://goose-docs.ai/docs/guides/context-engineering/hooks), following the + cross-tool [Open Plugins](https://open-plugins.com/agent-builders/components/hooks) spec: a + plugin's `hooks/hooks.json` maps `PreToolUse` / `PostToolUse` to scripts, with a `matcher` + regex selecting the tool and the tool input handed to the hook as JSON. Note the hook layer + runs local commands, so it is itself a surface worth trusting deliberately.) - **The network egress boundary.** The broadest option: if your client already mediates network access, that chokepoint sees every domain the agent actually reaches, not just the ones a tool or file surfaced — so discovery can ride the same seam as the filtering you already do there. It composes with an allow/deny boundary you may already run, but it is also where the noise is worst: most domains publish no catalog, so the caching in - [Keep probing cheap](#keep-probing-cheap-and-let-enterprises-scope-it) matters most here. - (Goose's [macOS sandbox](https://goose-docs.ai/docs/guides/sandbox/) is built this way — a - seatbelt sandbox denies direct network access and forces outbound traffic through a local - proxy that checks each destination against a list.) + [Keep probing cheap](#keep-probing-cheap-and-let-enterprises-scope-it) matters most here. (A + sandbox that denies the agent direct network access and tunnels its traffic through a local + proxy already has this chokepoint; the proxy sees every destination before the connection is + made.) **We do not recommend turning these on by default at this time.** Ship them opt-in, behind a setting, while the ecosystem and the interaction pattern are still young — the default-on @@ -192,18 +203,16 @@ equivalent block written into `config.yaml`. ### Security and trust considerations -#### Show the endorsement chain, not just the endpoint +#### Show the listing chain, not just the endpoint -The domain that publishes a catalog is frequently **not** the domain that hosts the server it -points at. `github.com`'s AI Catalog references a server on `api.githubcopilot.com`; a Google -catalog may point at `googleapis.com`. A prompt that names only the endpoint asks the user to -trust a domain they may not recognize as belonging to the service — which is -indistinguishable, from the user's side, from a phishing prompt. +The domain that publishes a catalog is often **not** the domain that hosts the server it points +at — a catalog on `example.com` may list a server running on `mcp-host-saas.com`. A prompt that +names only the endpoint asks the user to trust a domain they may not recognize as belonging to +the service, which is indistinguishable, from the user's side, from a phishing prompt. Surface the **chain** instead: the domain the user actually interacted with, the fact that it -endorsed the entry, and the domain that will own the connection. The endorsement is the trust -signal the user can evaluate; the raw endpoint is not. Name both, and make it clear which one -the credentials and traffic will go to. +listed the entry, and the domain that will own the connection. Name both, and make it clear +which one the credentials and traffic will go to. A minimal prompt carrying that chain: @@ -211,22 +220,46 @@ A minimal prompt carrying that chain: ┌──────────────────────────────────────────────────────────┐ │ Connect an MCP server? │ │ │ -│ github.com lists a server it does not host: │ +│ example.com lists a server it does not host: │ │ │ -│ GitHub github.com │ -│ └── hosted at api.githubcopilot.com │ +│ Example example.com │ +│ └── hosted at mcp-host-saas.com │ │ │ │ Requests and any credentials you approve will go to │ -│ api.githubcopilot.com. │ +│ mcp-host-saas.com. │ │ │ │ [ Not now ] [ This session ] [ Always ] │ └──────────────────────────────────────────────────────────┘ ``` -Two things are doing the work here. The endorsing domain is the one the user recognizes and -just interacted with, so it leads. The hosting domain is named as a **consequence** — where -traffic and credentials actually go — rather than as a bare URL the user is asked to -pattern-match against a brand they know. +Two things are doing the work here. The listing domain is the one the user recognizes and just +interacted with, so it leads. The hosting domain is named as a **consequence** — where traffic +and credentials actually go — rather than as a bare URL the user is asked to pattern-match +against a brand they know. + +Be careful what you claim for that chain. A listing is an assertion by whoever controls the +domain, not a verified statement about the server: the AI Catalog specification attaches no +endorsement semantics to publication, and a compromised catalog host can inject entries. Where +an entry carries a +[trust manifest](https://github.com/Agent-Card/ai-catalog/blob/main/specification/ai-catalog.md), +that — publisher identity and attestations — is the mechanism built for this decision, and the +listing chain is what you fall back on when it is absent. + +#### Do not let a probe reach where a user could not + +A probe is an outbound request to a host the client picked, which makes it a request-forgery +primitive if you leave it unbounded. Restrict it to publicly resolvable hosts: reject IP +literals, loopback, link-local, and private ranges, and re-check after DNS resolution rather +than only on the hostname, so a name that resolves inward is caught. `169.254.169.254` and its +equivalents are the case that matters most — a cloud instance probing its own metadata endpoint +is a credential disclosure, not a discovery miss. + +The rest is ordinary hygiene, and worth stating because the probe is automatic and unattended: +send no cookies, credentials, or ambient authorization; cap the response size and the number of +entries you will follow; and bound redirects, resolving each hop under the same rules — a +redirect is the easy way around a check applied only to the URL you started with. In a managed +environment, an internal host that _does_ serve a catalog is best reached through the enterprise +controls below rather than by relaxing these rules. #### Always ask before installing — and be stricter here than you are with tools @@ -237,9 +270,9 @@ adds a new, unvetted counterparty to the session — one that will expose its ow its own inputs, and persist if the user lets it. That asymmetry means the autonomy settings a client offers for tools should not simply extend to installs: -- **A discovered server MUST NOT be installed automatically.** No "completely autonomous" mode - should silently connect one. The approval dialog is load-bearing: the endorsing domain is the - user's only trust signal, and skipping the prompt discards it. +- **A discovered server should never be installed automatically.** No "completely autonomous" + mode should silently connect one. The approval dialog is load-bearing: the listing chain is + the user's main signal, and skipping the prompt discards it. - **There is no blanket "always allow."** An install approval is scoped to _that server_, never to discovery in general and never to a domain's future entries. @@ -258,12 +291,16 @@ change to their setup. Discovery is only useful if it stays quiet. A client that re-offers a server the user already runs, or that surfaces every entry on a busy catalog at once, trains the user to dismiss the -prompt without reading it — which costs you the one moment where the endorsement chain above +prompt without reading it — which costs you the one moment where the listing chain above actually gets evaluated. -- **Track what is already installed.** Resolve a discovered entry back to a server the user has - configured — match on the Server Card's `name`, and on the transport URLs in `remotes[]` for - servers added before any card existed — and stay silent on a hit. +- **Track what is already installed, keyed on the server's endpoint.** Match a discovered entry + against the user's configured servers on the transport URLs in the card's `remotes[]` — the + thing you would actually connect to. Resist keying on the card's `name` or the entry's + `identifier`: both are strings the catalog asserts about itself, so a hostile entry that + claims a name you already run would silence the prompt by design. Endpoint matching has no + such failure mode, since an entry that names the same endpoint _is_ the same server. Note this + means the de-dup check happens after you fetch the card, not before. - **Consider culling a long catalog.** A domain may list many servers. Presenting the full list is usually fine — most catalogs are short. But if a catalog is long, there is room to get creative: an inference step over the entries' descriptions, weighed against what the user is @@ -301,15 +338,15 @@ the technical detail. │ │ │ Direct installs are disabled by your organization. │ │ │ -│ Server GitHub │ -│ Listed by github.com │ -│ Hosted at api.githubcopilot.com │ -│ Auth OAuth 2.0 │ +│ Server Example │ +│ Listed by example.com │ +│ Hosted at mcp-host-saas.com │ +│ Transport streamable HTTP │ │ │ │ Why do you need it? │ │ ┌────────────────────────────────────────────────────┐ │ -│ │ Reviewing PRs in the session — would let the agent │ │ -│ │ read diffs directly instead of pasting them. │ │ +│ │ Reviewing tickets in the session — would let the │ │ +│ │ agent read them directly instead of pasting them. │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ [ Cancel ] [ Send request ] │ @@ -318,8 +355,8 @@ the technical detail. Where that request lands is the organization's business — a ticket, a Slack approval, a pull request against a gateway's server list. What matters for a client implementor is that the -Server Card gives you everything the approver needs (identity, endpoint, transport, auth -posture) without the user having to hunt for it, and that an approved request ends with the +Server Card gives you most of what the approver needs (identity, endpoint, transport, protocol +versions) without the user having to hunt for it, and that an approved request ends with the server added to the **managed gateway** the whole org already connects through, rather than installed ad hoc on one laptop. The user gets a path instead of a dead end, and the platform team gets a queue of real, evidenced demand. From 1649820634e692fc12365a2697e06f73171d0d9f Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 15:26:46 +0000 Subject: [PATCH 08/13] docs: recommend .well-known, point at Link header and alternatives The specification identifies a catalog by media type rather than path, and its discovery procedure checks a Link header and an HTML element ahead of the well-known URI. Recommend .well-known as the primary location for both sides, and tell servers to advertise and clients to honor the other two. Cite ai-catalog.io/spec throughout. --- docs/best-practices.md | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 26f2d77..3acfaec 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -5,7 +5,7 @@ remote MCP servers, and people **building MCP clients** that discover and connec This document is advisory. The normative mechanics for Server Cards live in the [README](../README.md) and [discovery.md](./discovery.md), and the catalog format is defined -by the [AI Catalog specification](https://github.com/Agent-Card/ai-catalog) — this page only +by the [AI Catalog specification](https://ai-catalog.io/spec/) — this page only collects recommendations on top of them. ## Best Practices for Server Implementors @@ -48,10 +48,18 @@ connect to you this way within your organization. ### Link your card from an AI Catalog entry A card lets a client connect once it has your URL; an -[AI Catalog](https://github.com/Agent-Card/ai-catalog) is what lets clients find that URL in the -first place — so publish both. An AI Catalog is a cross-protocol discovery document (served at -`/.well-known/ai-catalog.json`) that can index your MCP server alongside other AI artifacts; see -[discovery.md](./discovery.md). +[AI Catalog](https://ai-catalog.io/spec/) is what lets clients find that URL in the first place — +so publish both. An AI Catalog is a cross-protocol discovery document that can index your MCP +server alongside other AI artifacts; see [discovery.md](./discovery.md). + +**Serve it at `/.well-known/ai-catalog.json`.** The specification allows a catalog to live at +any URL — it is identified by its media type, not its path — and defines two other ways to point +at one: an HTTP [`Link` header](https://ai-catalog.io/spec/#link-relation-discovery) with +`rel="ai-catalog"`, and an HTML `` in the document head. Support those +too; they are how you advertise a catalog you cannot or do not want to put at the domain root, +and clients are expected to check them first. But publish at the well-known path as well if +anything can reach it. It is the one location a client can try without having first fetched +something from you, which is exactly the case in-session discovery depends on. Publish it at the domain people associate with your service: @@ -92,10 +100,22 @@ scraped it or given up, and the pie grows as complementary AI services get strun ### Where to trigger discovery -The probe itself is always the same and always cheap: one asynchronous -`GET /.well-known/ai-catalog.json`, run in the background so it never blocks what the user -asked for. The design decision is not _how_ to probe — it is **which moments** in a session -should trigger one. +The probe itself is cheap: one asynchronous `GET /.well-known/ai-catalog.json`, run in the +background so it never blocks what the user asked for. That well-known path is the probe worth +building first — it is the only one you can try against a bare domain, without having fetched +anything from it. + +It is not the only one, though, and a client that stops there will miss catalogs. The +specification's +[discovery procedure](https://ai-catalog.io/spec/#link-relation-discovery) checks two other +places _ahead_ of the well-known path: an HTTP `Link` header with `rel="ai-catalog"`, and an +HTML `` in the document head. Honor both — when you are already fetching +a page (the tool-call trigger below), the header and the markup are in your hands anyway, so +reading them costs nothing and catches every host that publishes a catalog somewhere other than +the domain root. Enterprise registries in particular often cannot use `.well-known` at all. + +With that settled, the design decision is not _how_ to probe — it is **which moments** in a +session should trigger one. Where a concrete example helps, this section points at [Goose](https://goose-docs.ai/), an open-source MCP agent hosted by the Agentic AI Foundation, simply because its hooks and install @@ -112,7 +132,7 @@ and no domain touched that the user did not already name. ```mermaid flowchart TD - A[User enters a URL in the session] --> B[Probe domain's /.well-known/ai-catalog.json] + A[User enters a URL in the session] --> B["Probe for a catalog — Link header, <link>, then /.well-known/ai-catalog.json"] B --> C{AI Catalog found?} C -->|No| D[Cache the miss, stay silent] C -->|Yes| E[Select application/mcp-server-card+json entries] @@ -241,7 +261,7 @@ Be careful what you claim for that chain. A listing is an assertion by whoever c domain, not a verified statement about the server: the AI Catalog specification attaches no endorsement semantics to publication, and a compromised catalog host can inject entries. Where an entry carries a -[trust manifest](https://github.com/Agent-Card/ai-catalog/blob/main/specification/ai-catalog.md), +[trust manifest](https://ai-catalog.io/spec/#trust-manifest), that — publisher identity and attestations — is the mechanism built for this decision, and the listing chain is what you fall back on when it is absent. From b8b59eb763f9bc0c2fe44053803debb24bf86bae Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 15:29:00 +0000 Subject: [PATCH 09/13] docs: explain only .well-known, track other discovery modes in #43 Keeps the guidance to the one mechanism the extension implements today, worded so additional modes can be added without a rewrite. --- docs/best-practices.md | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 3acfaec..dfdf061 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -52,14 +52,11 @@ A card lets a client connect once it has your URL; an so publish both. An AI Catalog is a cross-protocol discovery document that can index your MCP server alongside other AI artifacts; see [discovery.md](./discovery.md). -**Serve it at `/.well-known/ai-catalog.json`.** The specification allows a catalog to live at -any URL — it is identified by its media type, not its path — and defines two other ways to point -at one: an HTTP [`Link` header](https://ai-catalog.io/spec/#link-relation-discovery) with -`rel="ai-catalog"`, and an HTML `` in the document head. Support those -too; they are how you advertise a catalog you cannot or do not want to put at the domain root, -and clients are expected to check them first. But publish at the well-known path as well if -anything can reach it. It is the one location a client can try without having first fetched -something from you, which is exactly the case in-session discovery depends on. +**Serve it at `/.well-known/ai-catalog.json`.** That is the one location a client can try +against a bare domain, with nothing fetched from you first — which is exactly what in-session +discovery depends on. (The specification allows a catalog to live elsewhere, and defines other +ways to point at one; support for those is tracked in +[#43](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/43).) Publish it at the domain people associate with your service: @@ -101,21 +98,14 @@ scraped it or given up, and the pie grows as complementary AI services get strun ### Where to trigger discovery The probe itself is cheap: one asynchronous `GET /.well-known/ai-catalog.json`, run in the -background so it never blocks what the user asked for. That well-known path is the probe worth -building first — it is the only one you can try against a bare domain, without having fetched -anything from it. - -It is not the only one, though, and a client that stops there will miss catalogs. The -specification's -[discovery procedure](https://ai-catalog.io/spec/#link-relation-discovery) checks two other -places _ahead_ of the well-known path: an HTTP `Link` header with `rel="ai-catalog"`, and an -HTML `` in the document head. Honor both — when you are already fetching -a page (the tool-call trigger below), the header and the markup are in your hands anyway, so -reading them costs nothing and catches every host that publishes a catalog somewhere other than -the domain root. Enterprise registries in particular often cannot use `.well-known` at all. - -With that settled, the design decision is not _how_ to probe — it is **which moments** in a -session should trigger one. +background so it never blocks what the user asked for. Build that one first — it is the probe +you can run against a bare domain, without having fetched anything from it. Additional ways to +locate a catalog are tracked in +[#43](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/43); expect +this list to grow, and keep the resolution step separate from the triggers below so it can. + +The design decision, then, is not _how_ to probe — it is **which moments** in a session should +trigger one. Where a concrete example helps, this section points at [Goose](https://goose-docs.ai/), an open-source MCP agent hosted by the Agentic AI Foundation, simply because its hooks and install @@ -132,7 +122,7 @@ and no domain touched that the user did not already name. ```mermaid flowchart TD - A[User enters a URL in the session] --> B["Probe for a catalog — Link header, <link>, then /.well-known/ai-catalog.json"] + A[User enters a URL in the session] --> B[Probe domain's /.well-known/ai-catalog.json] B --> C{AI Catalog found?} C -->|No| D[Cache the miss, stay silent] C -->|Yes| E[Select application/mcp-server-card+json entries] From 78a4e5e97c8156d477e54674e1a8dda4857a72d7 Mon Sep 17 00:00:00 2001 From: tadasant Date: Mon, 20 Jul 2026 15:52:29 +0000 Subject: [PATCH 10/13] docs: drop nested-catalog guidance, track in #44 Keeps the client flow to the flat catalog the extension resolves today, worded so entry handling can gain cases. --- docs/best-practices.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index dfdf061..a0b29c0 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -136,13 +136,11 @@ flowchart TD I -->|Not now| L[Remember the decline] ``` -Two details worth getting right when you walk the entries. A catalog can **nest** — an entry -whose `type` is `application/ai-catalog+json` points at another catalog rather than an artifact -— so a client that filters for `application/mcp-server-card+json` and stops will silently miss -servers on any domain that organizes that way. Recurse, but bound it: the specification -recommends a maximum depth of 4, and you should track visited URLs so a cycle cannot walk you in -circles. Second, an entry carries its artifact either by reference or inline, so not every entry -requires a fetch. +One detail worth getting right when you walk the entries: an entry carries its artifact either +by reference or inline, so not every entry requires a fetch. Catalogs can also be organized in +ways this extension does not yet resolve — see +[#44](https://github.com/modelcontextprotocol/experimental-ext-server-card/issues/44) — so treat +"read the entries" as a step that will gain cases, not a fixed one. A closely related and equally bounded set: the domains a **project** already points at — links in an `AGENTS.md`, a project config, or whatever hints file your client injects into the system From b5d16da0364ccf734c8ebdc404d9c9effd433a98 Mon Sep 17 00:00:00 2001 From: Tadas Antanavicius Date: Fri, 24 Jul 2026 11:34:34 +0200 Subject: [PATCH 11/13] Update docs/best-practices.md Co-authored-by: Sam Morrow --- docs/best-practices.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index a0b29c0..9674c30 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -70,11 +70,7 @@ Review the guidance below for Client Implementors to determine the appropriate d ## Best Practices for Client Implementors -Every MCP server your client can reach is capability you did not have to build. A user who -connects one gets more done without leaving you, and comes back for it — servers make a client -more useful and stickier. The hard part is that connecting a server is a chore the user has to -go and do somewhere else, out of band, before it can help them — so most users never do it, and -the capability sits unclaimed. +Every MCP server your client can discover organically helps the user to stay in the flow while giving them the capabilities they need. The hard part is that connecting a server is often a chore where the user has to leave their flow to configure it out of band, before it can help them — so most users never do it. As per above guidance for server implementors, we now have a way for them to advertise their service in an easy-to-find, standardized location. Using this, your client can offer the From e370342c4fb6fcb0284cddaff44292b930970daf Mon Sep 17 00:00:00 2001 From: tadasant Date: Fri, 24 Jul 2026 09:36:30 +0000 Subject: [PATCH 12/13] docs: wrap prose consistently in best-practices.md Several paragraphs were left unwrapped while the rest of the file wraps at ~95 columns. Reflow them so the file is internally consistent. The only line still over 100 is an atomic markdown link that cannot be broken. Deliberately not adding a repo-wide prettier proseWrap setting here: it would reflow discovery.md, which PR #42 is actively rewriting. --- docs/best-practices.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 9674c30..933c9b6 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -66,11 +66,19 @@ Publish it at the domain people associate with your service: you — for example the domain hosting your REST API or the other resources a team becomes aware of _before_ they learn you also expose MCP. -Review the guidance below for Client Implementors to determine the appropriate domain for your service. For example, for GitHub, it would be common for the user of a coding agent to paste a URL like `https://github.com/modelcontextprotocol/experimental-ext-server-card/pull/36` into a session. So `github.com/.well-known/ai-catalog.json` is an excellent place to put your AI catalog - not `api.githubcopilot.com/.well-known/ai-catalog.json`, where GitHub's MCP server [happens to live](https://github.com/github/github-mcp-server). +Review the guidance below for Client Implementors to determine the appropriate domain for your +service. For example, for GitHub, it would be common for the user of a coding agent to paste a +URL like `https://github.com/modelcontextprotocol/experimental-ext-server-card/pull/36` into a +session. So `github.com/.well-known/ai-catalog.json` is an excellent place to put your AI +catalog - not `api.githubcopilot.com/.well-known/ai-catalog.json`, where GitHub's MCP server +[happens to live](https://github.com/github/github-mcp-server). ## Best Practices for Client Implementors -Every MCP server your client can discover organically helps the user to stay in the flow while giving them the capabilities they need. The hard part is that connecting a server is often a chore where the user has to leave their flow to configure it out of band, before it can help them — so most users never do it. +Every MCP server your client can discover organically helps the user to stay in the flow while +giving them the capabilities they need. The hard part is that connecting a server is often a +chore where the user has to leave their flow to configure it out of band, before it can help +them — so most users never do it. As per above guidance for server implementors, we now have a way for them to advertise their service in an easy-to-find, standardized location. Using this, your client can offer the @@ -79,8 +87,8 @@ context, instead of in a settings pane the user never visits. If you wire that in where we recommend below, it costs you remarkably little: -- **No new UX to design.** You already ask users to approve consequential actions like tool calls. This is one - more approval, in a flow they already know. +- **No new UX to design.** You already ask users to approve consequential actions like tool + calls. This is one more approval, in a flow they already know. - **No new trust to establish.** You only ever offer servers published by a domain the user themselves just put in front of you. You are not recommending anyone — the domain the user named is. @@ -105,9 +113,8 @@ trigger one. Where a concrete example helps, this section points at [Goose](https://goose-docs.ai/), an open-source MCP agent hosted by the Agentic AI Foundation, simply because its hooks and install -path are easy to read. Nothing -here is Goose-specific — every mechanism below has an equivalent in any client that runs tools, -reads project files, or mediates network access. +path are easy to read. Nothing here is Goose-specific — every mechanism below has an equivalent +in any client that runs tools, reads project files, or mediates network access. #### Start here: probe the domains a user hands you From 1b064889e33557e6452cfe791d366082620c2783 Mon Sep 17 00:00:00 2001 From: tadasant Date: Fri, 24 Jul 2026 09:36:39 +0000 Subject: [PATCH 13/13] docs: 'recommending anything' over 'anyone' What the client declines to recommend is a server, not a party. --- docs/best-practices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/best-practices.md b/docs/best-practices.md index 933c9b6..2d1d003 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -90,7 +90,7 @@ If you wire that in where we recommend below, it costs you remarkably little: - **No new UX to design.** You already ask users to approve consequential actions like tool calls. This is one more approval, in a flow they already know. - **No new trust to establish.** You only ever offer servers published by a domain the user - themselves just put in front of you. You are not recommending anyone — the domain the user + themselves just put in front of you. You are not recommending anything — the domain the user named is. - **No discovery problem to solve.** No ranking, no index, no crawl, no editorial judgment. The user supplied the domain; you are just asking it what it offers.