Repository navigation
Chapter_3_Other_Aircraft
Flight log, SELENIUM-FLOW, Dispatch.
Every sortie so far was flown from one cockpit. Claude Code reads resources, asks for progress, and was the client every decision in Chapter 2 was proved against. Then Dr K put a different airframe on the field: VS Code Copilot, flown by Sonnet, in another project.
It flew — once it was told
?resources=off. Until then the server looked half empty to it: the manual, the flow library and the session status were all published as resources, and Copilot's model has no way to read one. The progress bar worked first time. And typing#selenium-flowoffered one thing, which turned out to be theasserttool.This chapter is about the cockpits we did not build for — what each one can actually reach, and what the server should say to the one it is talking to.
Planned first, then built. Dr K answered five questions in the first pass (§F3.1
to §F3.5) and the sixth in the second (§F3.6), and added two rules of their own
making: n8n reaches resources through mcp-kb, not through this server, and
nothing here keeps backwards compatibility while Dr K is its only user —
which took --stateless with it (§F3.7). Part III is ticked against the build.
What it was planned from:
- The Copilot sortie, Dr K's report of it, above.
- Research into how VS Code implements MCP, how Claude Code reads resources, what the protocol says about directories, and what a good tool description is — recorded in Part I with its sources, because several of the decisions below rest on facts about other people's software that will change.
-
mcp-kb, the sibling at/projects/modules/mcp-kb, which has already built the generic resource mirror this chapter is weighing (§F3.6).
Measured against VS Code's documentation and its issue tracker, September 2026:
-
Resources are a UI feature. A user browses them (MCP: Browse Resources)
or attaches one to a message (Add Context → MCP Resources). The agent is
given no tool to list or read a resource. That is open as
microsoft/vscode#291004,
which reads the source to prove it —
listResources()andreadResource()are called only from UI code — and has no maintainer answer. - So everything this server publishes only as a resource is invisible to
Copilot's model:
skill://selenium-flow/SKILL.md,flow://flows,flow://schema,session://current,session://files,secret://secrets. The mirror tools exist for exactly this client, and the default hid them. That is the whole of why?resources=offfixed the sortie. - Resource templates are filled in a quick pick, and VS Code asks the server's completions for suggestions. Ours offer none.
-
Names are shown as they are published. VS Code's guidance is title case
(
Application Logs); ours are Python function names (flows_resource). -
#references tools, tool sets and context items. An MCP server appears as a tool set under its own name. Nothing on the server side takes part in#beyond listing tools, so the#selenium-flow→assertoddity is almost certainly VS Code labelling the set by its first tool alphabetically. Not confirmed; see the open questions. -
Tool annotations are used:
titleis shown when a tool runs, and a tool markedreadOnlyHintruns without a confirmation dialog. -
Progress renders the message, and a bar once a
totalis known (September 2025). Confirmed on this server by Dr K. -
More than 128 tools across every enabled server and extension triggers
virtual tools: VS Code hides groups behind
activate_*stubs. We publish 21, so it is not near — but every mirror tool counts toward it. - VS Code identifies itself in
clientInfoasVisual Studio Code(seen in its own MCP trace logs, v1.105).
Confirmed by reading the tool schemas this session has loaded:
| Tool | Does |
|---|---|
ListMcpResourcesTool(server?) |
resources/list across servers, each row tagged with its server |
ReadMcpResourceTool(server, uri) |
resources/read |
ReadMcpResourceDirTool(server, uri) |
resources/directory/read — one level, and only against a server that declared support |
So in Claude Code the model reads resources itself, and the mirror tools are rightly hidden.
It is SEP-2640, part of the Skills over MCP extension: a server declares
capabilities.extensions["io.modelcontextprotocol/skills"].directoryRead: true,
and a client must not call it otherwise. A directory is a resource with
mimeType: inode/directory, listed one level at a time, with pagination shaped
like resources/list. FastMCP 4.0.x does not implement it, which is why this
server publishes skill://selenium-flow/_manifest instead.
- Anthropic (Writing effective tools for agents; Define tools): describe it as you would to a new hire — what it does, when to use it and when not, what each parameter means, the caveats — "at least 3–4 sentences, more if the tool is complex". Parameter names should be unambiguous, errors should say how to fix the call, and fewer, more capable tools beat many overlapping ones.
- The one large study (arXiv 2602.14878, 856 MCP tools): 56% fail to state their purpose clearly. Descriptions augmented with every component raised task success by a median 5.85 points and took 67% more steps, regressing in a sixth of cases; compact descriptions with the high-impact components performed statistically the same, and removing examples did not hurt.
The two agree on shape and disagree on nothing: a clear purpose, when to use it,
what the arguments mean, the caveat that bites. What neither supports is an
essay. Ours total 3,027 words across 21 tools; open_session and
screenshot are about 300 each, most of it teaching that belongs in the skill.
§F3.1 — Decision (Dr K's): the resources default is decided by who is calling; an explicit value wins
Today a client is assumed to read resources unless it says otherwise. Instead:
-
?resources=orX-MCP-Resources, when given, decides. Unchanged. -
Otherwise the identified client decides, from one named table:
Visual Studio Code(and its Insiders build) does not read resources. - An unknown client defaults to reading them, as now — the assumption stays that a client is spec-complete.
Footguns, which the build has to test for:
-
Where
clientInfolives depends on the protocol generation. On2025-xxit arrives once, ininitialize, and is held by the transport session; on2026-07-28it rides in every request's_meta. FastMCP exposes neither as a documented accessor;apps.supported()already probes the session for capabilities and is the precedent. -
--statelesshad no transport session, so a legacy-protocol client served statelessly could not be identified. Moot: the flag is gone (§F3.7). -
Clients cache their tool list. A listing is decided per request, so the
first
tools/listmust already see the identity — detection cannot depend on anything learned later in the session.
The instructions every client receives say to read
skill://selenium-flow/SKILL.md first. Copilot cannot. A client that gets the
mirror tools is told to call the skill tool instead. FastMCP has on_initialize
and on_discover middleware hooks, so the text can vary per client; if neither
can see the identity at that moment, the fallback is one sentence naming both.
A regex sweep, for concepts earlier chapters removed, found:
-
selenium_flow_skillstill promises to explain "whether you must pass session_id" (§F2.12 removed it); -
both prompts,
build_flowandrepair_flow, still describe a saved mode in which you "never pass asession_id".
A regex only finds the words someone remembered. The build reads every tool,
resource and prompt description, and every refusal that names a tool, against the
current contract. Error text is included: a refusal said "list_flows shows what
there is" during this research, which goes stale the moment §F3.6 changes the
mirrors. Guard: extend test_no_published_schema_carries_a_note_for_developers
into a test over descriptions and prompts for the removed vocabulary.
For the person browsing them in VS Code: Current Session, Saved Flows,
Flow Document Schema, Kept Files, Secrets, and one per skill file. Both
templates offer completions — flow://flows/{name} the caller's flow names,
session://files/{name} the caller's file names — answered for the caller who
asked, never across sessions.
Verify first: that FastMCP 4.0.x serves completion/complete for resource
templates, and that the handler can see the caller's session name.
The budget, from Part I: one sentence of purpose; when to use it, and the nearest tool to use instead; what any argument means that its schema cannot say; the one caveat that bites. Most tools in 40–120 words. Anything that is teaching — strategy, examples, why — moves to the skill, which the instructions point at.
Measured, not guessed. The word count per tool is recorded before and after,
and the published schemas stay byte-identical apart from description. A
description that loses a sentence a pilot relied on is the regression to watch;
the three pilot reports in Chapter 2 name several.
§F3.6 — Decision (Dr K's): two generic mirrors replace seven specific ones, and every text names a URI
Proposed: a tool-only client gets list_resources() and read_resource(uri),
exactly the vocabulary mcp-kb uses (kubed/mcp_kb/mcp/tools.py), instead of
current_session, selenium_flow_skill, session_files, list_flows,
get_flow, flow_schema and list_secrets.
Would it work? Yes — proved before recommending it. FastMCP's
ResourcesAsTools transform was added to this server and driven over HTTP by
two callers:
-
read_resource("session://current")answeredaliceto alice andbobto bob: a read inside a tool call runs in the same request context, so the caller's session name survives; -
read_resource("flow://flows/login")returned alice's flow to her and refused bob with the ordinary "no flow called 'login'" — the middleware and scoping apply to the nested read; - the listing was 17 rows, 5.5 KB, templates included as
uri_template.
For it:
-
Two tools where there were seven, and the vocabulary is the protocol's: an
agent that can drive
resources/readalready knows these. -
One grammar across servers. An agent given
mcp-kband this server learnslist_resources/read_resourceonce. - Nothing to keep in step. A resource added later is readable through the mirror the day it ships; today it needs its own mirror tool, and forgetting one is invisible to every test that runs in a resource-reading client.
- Fewer tool tokens for a client that needs mirrors, against VS Code's limit and everyone's context.
The footguns, and what each needs:
-
Binary resources.
session://files/{name}isapplication/octet-stream.ResourcesAsToolsbase64-encodes binary into the tool result, so a model reading a downloaded PDF would put megabytes of text into its own context. The mirror refuses binary and answers with the file's name, size, type and HTTP link. That alone rules out the transform as-is: hand-written, asmcp-kbdid, or a subclass. -
The MCP Apps shell is listed.
ui://selenium-flow/componentis HTML for a host's iframe, not something to read. The listing omitsui://. -
Typed arguments become a URI to build.
get_flow(name="login")becomesread_resource("flow://flows/login"). A model can get a template wrong, so a miss must answer with the templates and a URI that would have worked — the §F2.16 rule that a refusal names the shape. -
Guidance moves from the tool list to the listing.
list_secretscarries 141 words about how secrets are bound; under this proposal a model sees it only after callinglist_resources. Resource descriptions have to carry what the mirror tools' descriptions carry now, and the instructions should name the three URIs a first session needs, so it costs no extra round trip. -
Name collisions in clients that do not namespace. n8n's MCP Client Tool
hands tool names to the agent as they are. An n8n agent wired to
mcp-kband this server would see tworead_resourcetools. VS Code and Claude Code prefix by server; n8n has to be measured before this ships, since it is the first client the mirrors were built for. -
One annotation for every read.
session://currentasks the Grid;skill://reads packaged files. A single tool is annotated for the widest: read-only, open world. - Fifty-one references to the old names across 15 files — code, skill, prompts, the OpenAPI builder and error text. Each becomes stale in the same commit, which is §F3.3's guard earning its keep.
-
Clients that cached the old tool list keep calling
get_flowafter a rollout. PerCONTRIBUTING.md's rule on changing shapes, the old names should keep working, unlisted, for a release.
What this does not include: read_resource_dir. resources/directory/read
is SEP-2640, gated behind the skills extension, and FastMCP does not serve it. A
mirror of a method the server does not implement would be invented vocabulary.
This server ships one skill with a _manifest; directories belong to mcp-kb,
whose whole catalogue is directories.
And what it points at. mcp-kb is meant to be the server for "read the
resource at this URI". Whether this server's skill should one day be served from
there — rather than embedded here, where it is guaranteed to describe this
version — is an open question, not a proposal.
Recommendation, as written before it was answered: adopt it, hand-written, with footguns 1–4 and 8 designed in, and footgun 5 measured in n8n first.
Answered, and wider than asked. Dr K took the generic mirror and made it the
rule for all text: every description, hint, error, prompt and skill page names
a resource by URI — session://current, flow://flows/{name},
skill://selenium-flow/references/FLOWS.md — so a client that reads resources
knows what to do with one, and a client that cannot is given the two tools that
take the same URIs. And two footguns were answered by decision rather than
design:
-
Footgun 5, n8n, is not this server's. n8n keeps resources on here, so it is
shown no mirror tools at all, and
mcp-kbproxies this server's resources into its own catalogue; an n8n agent is told every URI is read throughmcp-kb. There is oneread_resourcein that agent, and it ismcp-kb's. - Footgun 8, cached tool lists, is not worth a release of aliases (§F3.7).
As built (mcp/mirror.py): images read back as MCP image content; any other
binary is described — its type, size, and for a session file that
session://files carries its link — never base64; ui:// is neither listed nor
readable; a URI that is not there is answered with the templates. session_files
survives only as the MCP App it also was, listed to a host that renders apps.
The OpenAPI spec marks each read endpoint x-mcp-resource with its URI, and the
wiki page for it shows the read.
This server, and mcp-kb, have one user. So a change that breaks a shape breaks
it and updates every reference in the same pull request: no deprecation window,
no retired name kept callable, no legacy mode kept for a deployment nobody runs.
CONTRIBUTING.md's rule on changing an argument's shape is the policy for the
day there is a second user, and Dr K says when that is.
--stateless is removed under it. It dropped MCP transport sessions so more
than one replica could serve /mcp — and a transport session is where a legacy
client's initialize, and so its identity (§F3.1), is remembered. One replica
runs; the flag went.
One pull request, built in this order because each step changes what the next one reads. Every item was proved by breaking it on purpose and watching its test fail.
-
mcp/clients.pyreadsclientInfofrom_metaon2026-07-28and from the session'sinitializeon2025;NO_RESOURCESnames VS Code. -
reads_resources(): explicit value, else identified client, else true — tested on both protocol generations through the real client. - Instructions per client, answered on
initializeandserver/discover. -
--statelessremoved (§F3.7).
- Every tool, resource and prompt description, and every refusal that named a
retired tool, now names a URI. Also found stale:
keep_filetold a callerupload_file(path=...)where it meanskept=; SKILL.md's rule 4 still described flatxpath/css;build_flowsaid to scopeoutline"withcss". - 3,156 words across 24 tools became 1,578. The longest are now
save_flowandoutline, around 120 each.press_key's sixty key names moved to its refusal, which already listed them. - Guard: a test reads everything an agent is given from the running server — descriptions, both kinds of instructions, rendered prompts, every skill file — and fails on a removed name.
- Names a person reads:
Current Session,Saved Flows,Saved Flow,Flow Document Schema,Session Files,Session File,Secrets. - Completions for
flow://flows/{name},session://files/{name}andrepair_flow'sflow, read from the caller's own listings.
-
Measure n8n with two servers exposingAnswered by decision: n8n reads this server's resources throughread_resource.mcp-kb. -
list_resources/read_resource: noui://, images as images, other binary described, a miss names the templates. - Resource descriptions carry what the retired mirror tools said.
-
Old mirror names callable for one release.Removed outright (§F3.7); every reference updated.
-
write(secret=...)beside an unrelated bad argument no longer also says "needs text". - Chapter 2's §F2.14 says what
outlineshipped.
Everything #39 promised held against the deployed pod, read as Claude Code and as VS Code — a real PNG came back an image, a real PDF was described in 233 characters. Three faults turned up beside it, none of them #39's, and all three were built in the next pull request:
-
Saved files went missing, for two unrelated reasons, measured on the Grid with Chrome 152. A PDF saved from a plain-http page was held as an insecure download — Chrome's own words, read off
chrome://downloads— and never reached the store, while PNGs were not held; no feature flag or Safe Browsing preference releases it, and only the insecure-content setting does, which also lets an https page load http scripts that could read a secret this server types (Copilot, #40). Separately, a page with no origin (about:blank,data:) is allowed exactly one download, and the automatic-downloads preference — which is applied, aschrome://prefs-internalsshows — does not reach an opaque origin. A second tab would get round it, and Dr K ruled that out: switching tabs is not this server's to do, since it would also drop the session out of a frame. Firefox saved correctly everywhere.Both were the same mistake, and the fix was to stop making it. A screenshot and a PDF are bytes this server already holds — WebDriver's
printand screenshot commands hand them back, they are never files in the browser. They were given back to the page as a download only so they would land in the Grid's store beside the site's own downloads (§F2.9), and so inherited every download Chrome refuses. First the insecure-content setting was made an opt-in, then a per-browser argument — Dr K: "shouldn't this be on the call that needs it?" — and asking why a download was involved at all ended it. Screenshots and prints are written straight to the session's kept files; the Grid's store holds only what a site downloads. The cost is accepted: they needFLOW_DATA_DIR, and they stay until an operator deletes them. A second file of the same name is kept asname (1), the way a browser names a second download, since overwriting would break a link already handed out.open_session(insecure=true)survives with the one job no setting covered, a self-signed certificate.save_pdfbecameprint(format=…), withpdfandhtml— Dr K's framing, a browser's "print as…". Screenshot stayed its own tool: its output is an image for the model to see, a print's is a file for a person, and of screenshot's eight arguments onlyurlandfilenamemean anything to a print. A print takes the two print options that change what comes out,landscapeandbackground; MHTML is Chrome-only and waits for a need. -
A failed resource read quoted the Grid's internal URL. FastMCP wraps a resource's exception verbatim before any middleware sees it; the rewrite and the log filter #38 built for tool calls now cover reads too.
-
GET /secretswas never in the OpenAPI spec, so the one readable resource had no HTTP documentation and no wiki page.
-
§F3.6 — adopt the generic mirror?Closed: yes, and every text names a URI. n8n reads throughmcp-kb. -
Why does
#selenium-flowofferassert? Believed to be VS Code labelling a tool set by its first tool. Worth one check in VS Code: does#selenium-flowin a prompt enable the whole server? -
Prompts in tool-only clients. n8n cannot see
build_floworrepair_flow.mcp-kbanswers with?prompts=offand FastMCP'sPromptsAsTools. VS Code does support prompts, as slash commands. Wanted here, or is a person always in a client that has them? -
Should this server's skill be served by
mcp-kbone day? Nearer now:mcp-kbwill proxy this server's resources for n8n anyway,skill://among them. - Which other clients belong in §F3.1's table? Only VS Code is measured. ChatGPT, Cursor and n8n are candidates; none goes in without a measurement.
The action pages are generated from openapi.yaml, which is itself generated from the live MCP tool schemas — so they describe the server that shipped, not the one someone remembered. Prose belongs in wiki-notes/<tool>.md in the repo.
selenium-flow · MIT
Start here
Guides
Lifecycle
Going places
Doing things
Getting things out
Site data
Console and network