Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/investigations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
168 changes: 168 additions & 0 deletions docs/investigations/mcp_server_entry_points_and_transports.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 2 additions & 0 deletions docs/principles/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion docs/tools/markitdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading