---
title: "Research notes"
description: "38 notes on MCP client behaviour that a support matrix cannot express, each citing the source it was read from."
canonical: https://canimcp.io/research/
last-updated: 2026-11-11
---

# Research notes

38 notes on client behaviour that a support matrix cannot express. Each cites the repository, commit and path it was read from.

- [Cline declares an empty capability object on both surfaces](/research/cline-declares-no-client-capabilities.md) (idiosyncrasy): Both of Cline's MCP implementations send `capabilities: {}` in initialize. No roots, no sampling, no elicitation, no tasks — and none of the four are implemented anywhere, so the declaration is accurate.
- [A Cline server configured by URL alone gets SSE, not Streamable HTTP](/research/cline-defaults-a-url-server-to-sse.md) (idiosyncrasy): Both surfaces resolve a URL-based server with no explicit type to SSE. The extension's schema says why in a comment - reordering the union would break configs written before Streamable HTTP existed - so the transport deprecated in 2025-03-26 stays the default.
- [Cline's CLI speaks stdio through a hand-written client fixed at 2024-11-05](/research/cline-hand-rolls-stdio-in-its-cli.md) (idiosyncrasy): Three of Cline's four MCP paths use the official TypeScript SDK and negotiate 2025-11-25. The fourth, stdio in @cline/core, is a hand-written JSON-RPC client that sends 2024-11-05, ignores the version it gets back, and falls back to LSP-style Content-Length framing.
- [Cline lists resources and prompts into a UI tab and has no way to fetch either](/research/cline-lists-resources-and-prompts-but-never-reads-them.md) (idiosyncrasy): The VS Code extension calls resources/list, resources/templates/list and prompts/list on every connect and renders the results in the server configuration panel. There is no resources/read and no prompts/get anywhere in the repository, so nothing can act on what was listed.
- [Cline's CLI rewrites `npx mcp-remote <url>` into a native Streamable HTTP transport](/research/cline-rewrites-mcp-remote-proxy-configs.md) (idiosyncrasy): A stdio config of exactly `npx [-y] mcp-remote <url>`, with no env and no other arguments, is replaced by a streamableHttp transport before any process is spawned. The extension does not do this, so one settings file describes two different connections.
- [Codex ships preferring 2025-06-18 and has never spoken 2025-11-25](/research/codex-defaults-to-2025-06-18.md) (idiosyncrasy): Codex's default protocol mode pins initialize to 2025-06-18. The alternative mode goes straight to 2026-07-28 and sits behind an under-development feature flag that is off by default, so 2025-11-25 is not a version Codex ever offers.
- [Codex takes CIMD over DCR, with a client identifier hosted on chatgpt.com](/research/codex-prefers-a-chatgpt-hosted-client-identifier.md) (idiosyncrasy): When an authorization server advertises client_id_metadata_document_supported and accepts public clients, Codex registers with an HTTPS client identifier under chatgpt.com keyed by the ephemeral loopback callback id, and only falls back to Dynamic Client Registration otherwise.
- [Codex discards the tools/list cursor unless the 2026-07-28 mode is on](/research/codex-reads-one-page-of-tools.md) (idiosyncrasy): The paginator that collects a server's tool catalog is fed a nextCursor of None whenever Codex is in its default protocol mode, so it stops after the first page. No warning is emitted and the truncated catalog looks complete.
- [fast-agent's capability declaration varies from server to server](/research/fast-agent-declares-capabilities-per-server.md) (idiosyncrasy): fast-agent builds its client capability object per connection, from that server's config. Sampling and elicitation are on by default, roots only when the server entry lists them. Two servers in one config file can see two different clients.
- [fast-agent opens every connection with a 2026-07-28 discover probe](/research/fast-agent-leads-with-the-modern-discover-probe.md) (idiosyncrasy): A default fast-agent install sends `server/discover` at `2026-07-28` as the first request on every transport, and falls back to the `initialize` handshake only when that fails. It is the first client read here that leads with the stateless era rather than the handshake.
- [fast-agent declares roots.listChanged and never sends the notification](/research/fast-agent-never-sends-roots-list-changed.md) (gap): The Python SDK sets `roots.listChanged: true` whenever a client passes a roots callback, with no way to opt out. fast-agent's roots come from static config, so it never sends the notification. Two SDKs, three clients, same gap.
- [fast-agent reads resources and prompts, but only a person can trigger either](/research/fast-agent-resources-need-a-user-to-ask.md) (nuance): fast-agent calls `resources/read` and `prompts/get` properly, and gives the model no way to call either. Resources arrive as `@server:uri` mentions a user typed; prompts arrive through `/prompt` or the agent API. Three clients, three different answers to the same SHOULD.
- [fast-agent implements SEP-2640 skills over MCP with wire types it defines itself](/research/fast-agent-ships-skills-over-mcp-ahead-of-the-sdk.md) (in-flight): fast-agent calls `skills/list`, `skills/get` and `resources/directory/read` against servers declaring the `io.modelcontextprotocol/skills` extension, verifying every resource against a SHA-256 digest. The Python SDK has none of these types, so fast-agent declares them itself.
- [fast-agent watches server state through subscriptions/listen, and only there](/research/fast-agent-subscribes-only-on-the-modern-path.md) (nuance): On a 2026-07-28 connection fast-agent opens a `subscriptions/listen` stream and refreshes its whole cached view on any event. On the fallback handshake path it acts on `tools/list_changed` and drops the prompts and resources equivalents.
- [Gemini CLI was retired for consumers and its successor is closed source](/research/gemini-cli-succeeded-by-closed-source-antigravity.md) (gap): Gemini CLI stopped serving consumer accounts on 2026-06-18 and is now maintenance-only. Antigravity CLI replaces it as a closed-source Go binary, so its MCP behaviour cannot be read from source.
- [goose never sends the client name every registry lists it under](/research/goose-client-name-varies-by-surface.md) (idiosyncrasy): goose sends clientInfo.name as goose-desktop or goose-cli, not goose, and an embedder can override it entirely. The two surfaces also differ on MCP Apps support.
- [goose only answers elicitation raised inside a tool call](/research/goose-elicitation-needs-tool-call-context.md) (nuance): create_elicitation resolves a session id and a tool-call request id from the request context and errors if either is missing, so an elicitation raised outside a tool call fails rather than prompting.
- [goose emits a roots notification that 2026-07-28 removed, and rmcp does not stop it](/research/goose-sends-removed-roots-notification.md) (idiosyncrasy): notify_roots_list_changed fires on every working-directory change with no version check, goose prefers 2026-07-28 where the notification no longer exists, and rmcp 3.1.4 gates nothing on the outbound notification path.
- [Hermes declares URL-mode elicitation and declines every URL-mode request](/research/hermes-declares-url-elicitation-it-declines.md) (gap): mcp 2.3.0 declares both `form` and `url` elicitation for any callback, and the probe saw both on the wire from Hermes. The Hermes callback declines every URL-mode request, and an accepted form request returns empty content.
- [A Hermes server configured by URL alone gets Streamable HTTP](/research/hermes-defaults-a-url-server-to-streamable-http.md) (idiosyncrasy): A bare URL gets Streamable HTTP. Since `a24c12d1`, Hermes falls back to SSE when the first Streamable HTTP connect is rejected with a transport-mismatch status, and keeps SSE for later reconnects. It is still the first client read here whose default is the current transport.
- [Hermes never set clientInfo, so servers saw the SDK's placeholder name](/research/hermes-goes-on-the-wire-as-mcp.md) (gap): At `a24c12d1` Hermes passed no `client_info`, so handshakes carried the SDK placeholder `mcp` 0.1.0. At `9dcf7714` it sends `hermes-agent` with its base version, as the probe saw on the wire. Older builds still send `mcp`.
- [Hermes reverses its SDK's auto mode and tries initialize before server/discover](/research/hermes-leads-with-the-legacy-handshake.md) (nuance): On the same mcp Python SDK as fast-agent, Hermes inverts the default. Its `protocol: auto` sends the legacy `initialize` handshake first and probes `server/discover` only when a server rejects it as modern-only. The stated reason: almost every server today is handshake-era.
- [Hermes gives the model resources and prompts under a fifth set of tool names](/research/hermes-names-its-resource-tools-a-fifth-way.md) (nuance): Hermes materializes per-server `list_resources`, `read_resource`, `list_prompts` and `get_prompt` tools under an `mcp__<server>__` prefix. The model can read a resource, but the names match neither Codex and opencode's convention nor OpenClaw's.
- [Hermes runs one MCP client across every surface, and a second one it does not expose](/research/hermes-runs-one-mcp-client-for-every-surface.md) (nuance): CLI, desktop, gateway, TUI gateway, ACP adapter, cron and web all use the same `tools/mcp_tool*` client. No TypeScript SDK, and no hand-rolled client that carries a session. The one other `ClientSession` is internal, drives a computer-use server, and declares nothing.
- [Hermes seeds MCP-Protocol-Version from the handshake era, not the newest revision](/research/hermes-seeds-the-protocol-header-from-the-handshake-version.md) (nuance): On HTTP, Hermes defaults `MCP-Protocol-Version` to the SDK's `LATEST_HANDSHAKE_VERSION` rather than `LATEST_PROTOCOL_VERSION`. The header has to agree with the body, and the body on that path is still an `initialize` handshake.
- [Kilo Code adds CIMD and a pre-connect token refresh to opencode's OAuth path](/research/kilocode-adds-cimd-and-pre-connect-token-refresh.md) (nuance): Kilo offers a hosted client metadata document as its OAuth client id when no client is configured, ahead of DCR. It refreshes an expired stored token before connecting, because some servers answer an expired token with 403 and SDK 1.32.1 only retries 401.
- [Kilo Code is built on opencode now, and one MCP client serves the CLI, VS Code and JetBrains](/research/kilocode-runs-opencodes-mcp-client-behind-every-surface.md) (misconception): Kilo Code started as a Cline fork. At 278edf3a its MCP client is opencode's, with Kilo's edits marked kilocode_change. The VS Code and JetBrains extensions spawn `kilo serve` and have no MCP client of their own. main ships a patched SDK 1.32.1; the v2 branch uses the split SDK.
- [Nothing in the spec says the model can read a resource](/research/model-initiated-resource-read.md) (in-flight): Resources are specified as application-driven, with no guidance on whether the model itself can pull one by URI. The PR adding a SHOULD has been open and conflicted since April.
- [A default OpenClaw install declares no client capabilities at all](/research/openclaw-declares-nothing-unless-mcp-apps-is-on.md) (idiosyncrasy): OpenClaw's only client capability is an MCP Apps UI extension, and it is built behind `mcp.apps.enabled`, which defaults to off. A default install sends an empty capability object. The node host surface passes no capabilities under any setting.
- [An OpenClaw server configured by URL alone gets SSE, not Streamable HTTP](/research/openclaw-defaults-a-url-server-to-sse.md) (idiosyncrasy): OpenClaw only builds a Streamable HTTP transport when the config names it explicitly. With no `type` or `transport` field the resolver falls through to SSE, the transport deprecated in 2025-03-26. Cline does the same thing for a different stated reason.
- [OpenClaw gives the model resources and prompts under a fourth set of tool names](/research/openclaw-names-its-resource-tools-a-fourth-way.md) (nuance): OpenClaw materializes per-server `resources_list`, `resources_read`, `prompts_list` and `prompts_get` tools, so the model can read a resource. The names match neither Codex and opencode's convention nor anything in the spec.
- [OpenClaw handles the expired Streamable HTTP session in its own code](/research/openclaw-recovers-expired-http-sessions-without-patching-the-sdk.md) (nuance): A stateful server signals an expired session with HTTP 404, and the stock SDK surfaces it as a terminal error. opencode patches the SDK to recover. OpenClaw detects the same condition in its own lifecycle code, on both surfaces, and refuses to replay the failed call.
- [OpenClaw scrubs MCP tool descriptions for prompt injection, and one of its three rules does nothing](/research/openclaw-scrubs-tool-descriptions-with-three-regexes.md) (nuance): Server-supplied tool descriptions pass a sanitizer before reaching the model on both surfaces. It redacts two fixed phrasings of "ignore previous instructions" and truncates at 1,200 characters. Its third rule rewrites "system prompt" to itself, changing only whitespace.
- [opencode declares roots and nothing else, with sampling, elicitation and tasks commented out](/research/opencode-declares-only-roots.md) (idiosyncrasy): The client capability object lists sampling, elicitation and tasks, each commented out above a link to the issue tracking it. Only roots is live, so a server that offers any of the other three never gets asked.
- [opencode keeps only the text parts of an MCP prompt, and fills arguments positionally](/research/opencode-drops-non-text-prompt-content.md) (nuance): Server prompts become slash commands, but a prompt message carrying an image or an embedded resource maps to an empty string and is joined into the template. Named arguments are also replaced positionally, so argument order decides the result.
- [opencode ships a patched MCP SDK that recovers an expired HTTP session](/research/opencode-patches-its-sdk-for-session-recovery.md) (idiosyncrasy): opencode applies a bun patch to @modelcontextprotocol/sdk 1.29.0 that clears the session and retries once when a request carrying a session id gets a 404. Stock 1.29.0 has no recovery path, so opencode's Streamable HTTP behaviour is not the SDK's.
- [opencode retries tools/list with outputSchema stripped when the SDK rejects one](/research/opencode-retries-tools-list-without-output-schema.md) (idiosyncrasy): If the SDK's validation of a tools/list result fails in a way that looks schema-related, opencode re-requests the page against a result schema with outputSchema omitted. The match is a regex over the error message, so the fallback both misses and over-fires.
- [Capability negotiation cannot express which capabilities a peer actually uses](/research/utilized-capability-declarations.md) (gap): MCP's initialize handshake declares what each side offers, never what it consumes, so servers cannot gracefully degrade. The SEP proposing a fix was closed dormant on 2026-06-26 with no replacement named.
