diff --git a/docs/investigations/index.md b/docs/investigations/index.md index 1337d3f..bce3f29 100644 --- a/docs/investigations/index.md +++ b/docs/investigations/index.md @@ -18,3 +18,4 @@ date. Where an investigation deliberately stops short of choosing, it says so. - [skills.sh: repository layout, selective install and telemetry](skills_sh_repository_layout_and_telemetry.md) - where the skills installer looks, how a user takes one skill rather than all, and what it reports back - [Sharing one user-level AGENTS.md across harnesses](sharing_one_user_level_agents_md_across_harnesses.md) - a crowded tool category that almost entirely refuses to write to the home directory, and why - [Spell-checking prose in a pre-commit hook](spell_checking_prose_in_a_pre_commit_hook.md) - why two spell checkers catch different mistakes, and the one word that shows it +- [What an MCP server can be, and who can reach one](mcp_server_entry_points_and_transports.md) - two transports, five shapes and six callers, and the two variables that decide which are reachable diff --git a/docs/investigations/mcp_server_entry_points_and_transports.md b/docs/investigations/mcp_server_entry_points_and_transports.md new file mode 100644 index 0000000..be540a9 --- /dev/null +++ b/docs/investigations/mcp_server_entry_points_and_transports.md @@ -0,0 +1,168 @@ +--- +type: Investigation +title: What an MCP server can be, and who can reach one +description: The two standard transports, the five shapes a server takes, and the six places a call + can come from, read against the two variables that decide which of them are available. +tags: +- ai-agents +- agents +- mcp +- tooling +- architecture +status: draft +generated: + by: opencode/claude-opus-5 + at: '2026-10-14T12:00:00Z' +stale_after: '2027-04-30' +sources: +- id: mcp-transports + resource: https://modelcontextprotocol.io/specification/2026-07-28/basic/transports + title: 'Model Context Protocol: Transports' + last_modified: '2026-10-14' +- id: mcp-architecture + resource: https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture + title: 'Model Context Protocol: Architecture' + last_modified: '2026-10-14' +- id: mcp-server-concepts + resource: https://modelcontextprotocol.io/docs/2026-07-28/learn/server-concepts + title: 'Model Context Protocol: Server concepts' + last_modified: '2026-10-14' +- id: mcp-authorization + resource: https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization + title: 'Model Context Protocol: Authorization' + last_modified: '2026-10-14' +--- +**Context** - the word "server" suggests something running somewhere, with a port and an +uptime. Most MCP servers are not that. The question was what an MCP server actually is as an +artefact, and which consumers can reach one, before committing to where any particular one +should live. + +- **Read on**: 2026-10-14 +- **Method**: the specification read directly at revision 2026-07-28; the entry points + enumerated from the harnesses in use here and from the hosted products that consume MCP; + [markitdown](../tools/markitdown.md) read as the worked example of a server that is also a + command. + +The survey stops short of recommending a transport for any particular server. What it does +settle is which question decides that, and the answer is not a technical preference. + +## A server is a program, not a service + +The specification defines two standard transports, and calls a transport a binding: it decides +how messages are framed and delivered, not what they mean[^mcp-transports]. + +**stdio is a subprocess the client launches**, speaking newline-delimited JSON-RPC over its +standard streams[^mcp-transports]. No port, no daemon, no network. It starts when the harness +starts and dies with it. This is most of what exists today. + +**Streamable HTTP is an endpoint**, where each message is a POST and the reply arrives as JSON +or as a stream scoped to that request[^mcp-transports]. The hosted servers are this, and so is +anything that has to be reached from a machine that is not yours. + +So a stdio server's configuration is a command line: + +```json +{ "command": "uvx", "args": ["markitdown-mcp"] } +``` + +That is not a service you run. It is a program the harness launches on demand, exactly like a +command-line tool, because it is one. It happens to write JSON-RPC on stdout instead of text a +person reads. + +## What the protocol adds over a command line + +[markitdown](../tools/markitdown.md) ships both ways, which makes the comparison concrete. +`markitdown report.pdf` and `convert_to_markdown(uri=...)` do the same work. + +Three things change in the wrapping. + +**The model is told the tool exists.** It asks for the server's tool list and gets names, +descriptions and JSON schemas back[^mcp-server-concepts]. With a command, somebody has to have +put `--help` into the context, or the model guesses the flags. + +**The call and the result are typed.** Arguments are a validated object rather than a string +to shell-quote, and the result comes back structured rather than as stdout to pattern-match. + +**The process stays alive between calls.** State lives for as long as the process does, so a +server can hold an open browser, a warm database connection or a loaded index. A command pays +its startup cost every invocation. + +The cost is a configuration entry, a process to supervise, and a tool definition occupying +context on every turn whether it gets used or not. + +## Five shapes, from a script to a tenancy + +The category is much wider than "wrapper around an API", and the shapes have little in common +beyond the protocol. + +- **A pure function**, holding nothing: a converter, a formatter, a unit calculation. Honestly + close to a command with a schema bolted on. +- **Scoped access to a local resource**: a filesystem, a SQLite file, a git repository. The + value over a shell is that the server decides which paths are reachable. +- **A stateful machine**: a browser, a REPL, a long-lived kernel. The surviving process is the + whole point, and a command cannot do this at all. +- **A facade over a remote API**: an issue tracker, a chat platform. It holds the credential so + the model never sees it. +- **A provider of readable material rather than callable tools.** The protocol carries + resources and prompt templates alongside tools[^mcp-server-concepts], and almost nobody uses + them. The most interesting unexplored corner. + +## Six places a call comes from + +A harness with MCP enabled is the obvious consumer and about one sixth of the picture. + +| Entry point | Example | Can spawn a subprocess? | Credential comes from | +| --- | --- | --- | --- | +| Interactive coding harness | opencode, VS Code, Cursor | Yes | your environment | +| Desktop chat application | Claude Desktop | Yes | your environment | +| Headless agent on someone's CI | a coding agent in a build container | In principle, but unattended | a provisioned secret | +| Hosted web product | a connector in a browser-based assistant | No | OAuth, per user | +| Your own code as the client | an SDK in a script or an application | Your choice | your choice | +| A person, by hand | an inspector or a test harness | Yes | yours | + +The last row is worth keeping in view. A human calling the tool list and then a tool proves the +protocol does not require a model to be present, which is the cheapest way to test a server. + +The desktop chat application is separated from the coding harness deliberately. It has the same +power to launch a subprocess and no repository, no working directory and no project +conventions, so anything assuming "there is a project here" breaks on it. + +## Two variables decide which of those are available + +Every row above is a position on two axes, and neither is about the protocol. + +**Who launches the process.** A consumer running on a machine you control, with permission to +fork children, can use stdio. One that is not cannot, and needs the server to already exist +somewhere as an endpoint. + +**Who holds the credential.** A local stdio server reads your environment variables and your +configuration files, because it runs as you. A remote server has no "you" to borrow from, so it +needs its own authorisation story, which the specification builds on OAuth[^mcp-authorization]. + +Those two make three tiers rather than two, and the middle one is easy to miss. A build +container and a hosted web product are both "not my machine", but the container can carry a +secret handed to it in advance whilst the web product needs a user to complete an interactive +flow. Designing for "local or remote" puts those two in one bucket and they do not behave +alike. + +## Two shapes that sit outside the grid + +**The gateway** is an MCP server to its clients and an MCP client to many servers upstream. It +fronts twenty of them as one endpoint, and adds authorisation, auditing, rate limits and tool +filtering on the way through. This is why client and server are roles held per connection +rather than properties of a program[^mcp-architecture]. + +**An agent exposed as a server** lets another agent call it as a tool. That is composition +rather than capability, and it is the point at which "what is a tool" and "what is an agent" +stop being answerable separately. + +## Bottom line + +**An MCP server is a calling convention for handing a model a capability**, uniform across +everything from a forty-line script to a multi-tenant service. Asking whether something "could +just be a command-line tool" usually has the answer that it already is one, and the protocol is +the adapter that makes it legible to a model rather than to a person at a prompt. + +Which transport a given server speaks is therefore decided by its furthest intended consumer +rather than by its own convenience, and that choice is expensive to revisit: +[give an MCP server an HTTP surface before anything needs one](../principles/give_an_mcp_server_an_http_surface_before_anything_needs_one.md). diff --git a/docs/principles/give_an_mcp_server_an_http_surface_before_anything_needs_one.md b/docs/principles/give_an_mcp_server_an_http_surface_before_anything_needs_one.md new file mode 100644 index 0000000..26b46a7 --- /dev/null +++ b/docs/principles/give_an_mcp_server_an_http_surface_before_anything_needs_one.md @@ -0,0 +1,43 @@ +--- +type: Principle +title: Give an MCP server an HTTP surface before anything needs one +description: Speak streamable HTTP from the first version, because the consumer that cannot launch a + subprocess turns every later move into a rewrite rather than a deployment. +tags: +- principle +- mcp +- ai-agents +- agents +- architecture +status: draft +generated: + by: opencode/claude-opus-5 + at: '2026-10-14T12:00:00Z' +--- +**Claim.** Build an MCP server to speak streamable HTTP from its first version, even whilst the +only thing calling it is a harness on the same machine that would be happy with stdio. + +**Why.** A stdio server is a subprocess its client launches, so it can only be reached by a +consumer that runs on a machine you control and may fork children. A build container, a hosted +assistant and a colleague's laptop are all outside that. The moment one of them wants the +capability, a stdio-only server has to grow an endpoint, an identity it never had and a +concept of which user a call acts as, which is a rewrite dressed as a configuration change. +Binding the same handlers to an HTTP listener whilst the server is small costs a dependency and +an afternoon, and it converts every later move into a deployment question. + +**Keep stdio as well.** This is not a choice between the two. Local development stays faster +over stdio, an inspector attaches to it more easily, and a harness on the same machine has no +reason to pay for HTTP. Ship both bindings over one set of handlers and let the consumer pick. + +**The authorisation work is the part worth starting early**, and it is the part a local server +never forces. Running as the logged-in user means reading their environment and acting as them, +which is an answer you get for free and lose the moment the process moves. Writing the server +so a caller's identity arrives as an argument rather than as ambient state keeps that answer +replaceable. + +**What this is not.** It is not an argument for hosting the server. Where it runs is a separate +decision with its own costs, and most servers should stay on the machine that needs them. This +principle only says that the decision should remain open. + +The survey this rests on is +[What an MCP server can be, and who can reach one](../investigations/mcp_server_entry_points_and_transports.md). diff --git a/docs/principles/index.md b/docs/principles/index.md index 4961e66..44a54ab 100644 --- a/docs/principles/index.md +++ b/docs/principles/index.md @@ -53,3 +53,5 @@ most general. - [Write in a calm, quantified, settled-fact voice - not a promotional one](write_in_a_calm_quantified_settled_fact_voice_not_a_promotional_one.md) - the voice these pages are written in, and its tells on both sides - [Give every cross-cutting concept one definitional home](give_every_cross_cutting_concept_one_definitional_home.md) - single source of truth, applied to prose instead of code - [Route a rule to the layer that reaches whoever must obey it](route_a_rule_to_the_layer_that_reaches_whoever_must_obey_it.md) - a page, a hook, a skill or an always-on instruction, chosen by who has to obey +- [Subscribe to shared instruction text rather than pasting a copy](subscribe_to_shared_instruction_text_rather_than_pasting_a_copy.md) - what happens to a block of prose that no later version can reach +- [Give an MCP server an HTTP surface before anything needs one](give_an_mcp_server_an_http_surface_before_anything_needs_one.md) - the consumer that cannot launch a subprocess, and the rewrite it causes if you wait for it diff --git a/docs/tools/markitdown.md b/docs/tools/markitdown.md index 3c993a3..2a15841 100644 --- a/docs/tools/markitdown.md +++ b/docs/tools/markitdown.md @@ -76,7 +76,9 @@ and a wiped cache makes the next start slow again rather than broken. An agent that cannot read a PDF cannot file what is in one, and this bundle is written by agents. Declaring it as an MCP server rather than leaving it to the shell means the capability is present without anyone remembering the command, which matters for exactly the tool you reach -for twice a month. +for twice a month. It is the plainest example of the shape described in +[what an MCP server can be, and who can reach one](../investigations/mcp_server_entry_points_and_transports.md): +a subprocess the harness launches, holding nothing between calls. It is declared in the base layer of [my opencode configuration](opencode.md), because which model is answering has no bearing on how a DOCX is parsed. That is the general shape: