mcp

package
v0.0.3-alpha Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package mcp is hyper's second surface: the MCP server §9 states, started by `hyper mcp` and speaking JSON-RPC over the stdio the client handed the process (§9, ADR-0088, issue #195).

**It is the one package that imports the SDK, and that is the whole of why it exists.** The SDK owns the transport, the handshake, `tools/list` and its paging, the notification plumbing and the JSON-RPC framing — every part of this surface that is the protocol's rather than hyper's. What it may never decide is what an answer *says*: it does not compose the text block, set `isError`, shape `structuredContent` or validate an output. No `render`, `run`, `store` or `cli` type is expressed in its terms — nothing here hands it a domain value to infer a schema from or to marshal on hyper's behalf — so the day it is replaced is a day one package changes.

**What crosses the boundary is a Call and an Answer.** A tool declares its arguments, builds the command line its command would have received, and hands it to the dispatch it was constructed with; the dispatch runs the command behind hyper's own destination and answers the rows. A tool holds no command logic, so there is no second place for a guardrail to be skipped, a Refusal to be reworded or a row to be reshaped: *ergonomics is the whole of the difference between the two* (§9), and making it the whole of the difference in the code is what keeps it true.

The Call is the argv and the two facts about the call itself that an argv cannot carry, both of them the protocol's rather than hyper's: the context the call is alive for, which the SDK cancels when the client cancels the request, and where a Step boundary goes, which is a notification where the client attached a progress token and nothing anywhere else (§9, ADR-0021, ADR-0092, issue #202).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Instructions

func Instructions(version string) string

Instructions is the orientation: what `hyper` is, the five artefacts, the loop an agent drives them through, where the record lives, the three commands that are the human's and why, that a Refusal is final, and a worked example of each artefact (§9, ADR-0093, ADR-0095, ADR-0096, ADR-0101, ADR-0113, ADR-0117, issues #209, #211, #212, #218, #233 and #237).

**It reaches an agent two ways, and the text is one text.** The `initialize` handshake carries it in the field the protocol has for exactly this — *instructions describing how to use the server and its features* — and `hyper project` writes it to `AGENTS.md` where a repository has none. Neither channel is sufficient alone: a client decides when it surfaces `instructions` and one harness carries it only inside a tool search, while a file reaches only a repository somebody has already run `project` in. Both are the same bytes, from here, because two orientations disagree the first time either is edited (ADR-0095, internal/cli's RunProject).

**So it is worded for a reader on either surface**, and that is a constraint on every sentence in it. It names commands rather than tools — `show` and not `run_show` — and it puts `install`, `store init` and `compact` out of reach as *the human's*, which is true of both surfaces, rather than as *absent from this one*, which is true only of the server. A text that said *no tool here writes a Definition* would be read as permission by the agent holding a terminal.

**It is a function of the version, and that is load-bearing rather than tidy.** The Repository declaration below pins which version of `hyper` may act, and the version that would act is the version of the binary the reader is standing next to (§9, ADR-0020). A constant here would teach every agent to author a pin that Refuses the gate on every repository but the one the text was written in.

**It is exported because internal/cli writes it**, `project` being the second channel — and the corpus one package over reads it for a second reason, to write the artefacts below into a repository and run `check` over them: what the orientation teaches is held to checking clean rather than to reading well (internal/cli's RunProject, internal/cli/instructions_test.go). The dependency runs one way, `internal/cli` → `internal/mcp`, and the surface still knows no command (server.go, ADR-0093).

Types

type Answer

type Answer struct {
	Rows      []render.Row
	Terminal  render.Row
	Rendering string
	Refusal   string
	Narration string
	Exit      int
}

Answer is one command's answer as a value: the rows, the terminal row that ends them, the rendering the command's page produced, and the exit code it returned.

It is the destination's retained state crossing the boundary unflattened — the rows as **values** and not as a stream of bytes — which is the property milestone 11 was prefactored for: the rows a page is written from and the rows an envelope carries are one list (destination.go, ADR-0026, issue #194).

Rendering is the page the command wrote into the destination's buffer. Most tools do not put it on the wire — an ordinary return's text block is one summary line (§9) — and it is filled on every answer anyway, because it is what `review`'s text block *is*: `review`'s row of §9's asymmetric table is a tool handing back the whole rendering, and a buffer that only some answers filled would be one that tool had to ask twice for (envelope.go, issue #198).

**Refusal and Narration are what a surface with no exit code needs the exit code's two other halves for** (§9, issue #196). A command that Refuses renders §8's Refusal where the CLI would have written it on stderr, and that rendering *is* the text block on this surface, exactly where the command exits `77`. A command that reports a usage error writes one human sentence where the CLI would have written it on stderr, and that sentence is the message of the protocol error a malformed call comes back as — so an agent reads the sentence a person would have read. Neither is narration this surface forwards: what is not read by the mapping is dropped, a tool's narration going nowhere (destination.go).

type Boundary

type Boundary struct {
	Token    any
	Position int
	Of       int
	Step     string
}

Boundary is one progress notification as it arrived: the token it was correlated with, the Step's position, how many the Run holds, and the Step's authored id.

It is this package's own shape and not the SDK's, for the reason every value crossing the boundary is: a driver outside this package holds what hyper said, not the type the transport said it in.

type Call

type Call struct {
	// Context is the handler's own, and the whole of what this surface has
	// in place of a signal. The client cancels a call by cancelling the
	// request; the SDK cancels this; and the drain §6 states reads it where
	// the next Step would start — the Step in flight finishes, no further
	// Step starts, and the Run closes its own entry `failed` (§6, §9,
	// ADR-0015, ADR-0092).
	//
	// It is never the parent of anything a command performs. A Step whose
	// context was this one would be a Step that stopped mid-call, which is
	// the ambiguity the drain exists to avoid.
	Context context.Context
	// Argv is the command line the tool built, exactly as the command would
	// have received it on the terminal.
	Argv []string
	// Progress is one Step boundary reaching the client, and it is **nil
	// where the client supplied no progress token** (Progress below). A
	// dispatch handed nil is one whose Run nobody is watching, which is a
	// state the engine already has a reading of.
	Progress Progress
}

Call is one tool call as the dispatch behind it receives it: the command line the tool built, the context the call is alive for, and where the Steps of a Run go as it performs them.

It is one value rather than three parameters because two of the three are facts about the **call** rather than about the command line, and the day a third such fact lands is a day no signature moves. Neither is expressed in the SDK's terms: what crosses is a context and a function, so the package on the other side still cannot name a frame, a session or a notification (§9).

type Declared

type Declared struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	Input       json.RawMessage `json:"inputSchema"`
	Output      json.RawMessage `json:"outputSchema"`
}

Declared is one tool as `tools/list` publishes it: what it is called, what a client is told it is for, and the two schemas.

The schemas stay raw, which is the whole point of the type. What a caller holds is the bytes the server sent, keys in the order this package wrote them, rather than a decoded shape re-encoded on the way back out.

type Dispatch

type Dispatch func(call Call) Answer

Dispatch runs one hyper command line and answers what it produced. It is a function rather than an interface with one method because that is the whole of what this package needs of the tool behind it: the Call goes in, the rows come out, and nothing here knows which command ran or what it read.

It is what keeps the import one-directional. The dispatch lives in internal/cli, where the commands and their destination are; this package is handed one and never reaches for it, so the SDK stays on this side of the boundary and the command surface stays on the other.

type Envelope

type Envelope struct {
	// Content is the unstructured half, and on every tool this milestone
	// builds it is one text block. §9's asymmetry — a summary line, a
	// check's rows beneath one, a review's full rendering, a Refusal's full
	// rendering — is a fact about which tool answered and which path it
	// took, so the member is a list and the composition is below.
	Content []TextBlock `json:"content"`
	// StructuredContent is the machine half: §8's rows, and the terminal
	// fact moved up beside them.
	//
	// **It is a pointer because its absence is a shape this surface
	// answers**: a tool that declined before it opened a row stream
	// answers `content` and the bit alone, which is MCP's own shape for a
	// tool error (§9, ADR-0102, issue #219). Why that is the answer rather
	// than an empty half is argued where the decision is taken
	// (structuredOf).
	StructuredContent *Structured `json:"structuredContent,omitempty"`
	// IsError means only *you did not get what you asked for*, which is true
	// of a Refusal and of a failure alike; it is not the outcome
	// discriminator, one bit not carrying three states (§9). Which exit code
	// sets it is the mapping's to say, and it is written whichever it is.
	IsError bool `json:"isError"`
}

Envelope is hyper's own reading of what a tool call answered: the shape §9 fixes, and the shape the corpus holds.

It is a type of this package's rather than the SDK's CallToolResult for two reasons that are one reason. The result carries members this surface never sets — an input request, a request state, a result type — so a corpus holding one would be asserting the SDK's shape rather than hyper's. And `isError` is `omitempty` there, so a result that carries the bit as `false` and one that does not carry it at all are the same value: §9 makes that bit the thing a Refusal is told from an ordinary return by, so it is written **always**, and a shape that could drop it is not the shape to state it in.

type Progress

type Progress func(position, of int, step string)

Progress is where a Run's Step boundaries go on this surface: the Step's position, how many the Run holds, and its authored id — the three §9's narration carries, at the same boundary §7 writes a Journal entry at.

It is narration, so it carries no machine contract and no row of its own: a caller reads what happened off the envelope when the call returns, and this is what stands between now and then on a surface with no scrollback.

**It is nil where the client supplied no progress token**, and that is the rule this type exists to carry rather than a detail of who builds one. A progress notification exists to be correlated with the request that is proceeding, so without a token there is nothing to attach one to and a server that sent one anyway would be speaking unasked, which is the one thing this server never does (§9, ADR-0021, ADR-0092). Nil rather than a function that drops what it is handed is what puts the decision in one place: the surface decides whether anybody is watching, and the Run behind it narrates or does not.

The Run naming itself before its first Step is the other event §9's narration carries, and this type has no member for it: it sends nothing here (ADR-0047, ADR-0092).

type Server

type Server struct {
	// contains filtered or unexported fields
}

A Server is the MCP server over one dispatch: the tool set, the version it announces itself at, and the way in to both.

It is constructed once and reached two ways — Server.Serve over the process's stdio, and Server.Call over the SDK's in-memory transports — so that what the corpus drives and what a client starts are one server with one tool set. A second assembly for the tests would be a corpus asserting a server nobody runs.

func NewServer

func NewServer(version string, dispatch Dispatch) *Server

NewServer is the server over one dispatch, at one version.

The tool set is not a parameter. It is this package's own table — a tool is a schema and an argv (tools.go) — and a server assembled with a different one would be a client's view of hyper that hyper never stated.

func (*Server) Call

func (s *Server) Call(ctx context.Context, tool string, arguments json.RawMessage) (Envelope, error)

Call drives one tool call against this server and answers the envelope that came back, read off the wire.

**The call is real; only the client is in-process** — the principle golden_serve_test.go already states for the TLS fixture. The transports are a net.Pipe, so the handshake, the framing, the `tools/call` round trip and the JSON of every row are the wire's; what is a fixture is that the client is a goroutine rather than an editor.

**The envelope is read from the recorded frame and not from the client's decoded value**, and that is not a shortcut but the only reading that can be checked. §8 fixes that a row's `type` is its first key and the rest follow declaration order; the SDK decodes `structuredContent` into `any`, which is a map, and a map re-encoded has its keys in whatever order the encoder chooses. A corpus holding that would be holding the harness's ordering rather than hyper's. So the client-side stream is teed as it is read and the answer is taken from the bytes that arrived.

It is the door the golden corpus drives a `call` case through, and it is here rather than in the harness for the reason the tool set is not a parameter: a harness that stood its own client would be one importing the SDK, and the SDK is reachable from this package and no other.

**It attaches no progress token**, which is what makes it the door that also says what a call with none gets: nothing but its envelope. A driver that wants the notifications supplies a token through Watched below.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context) error

Serve runs the server on the process's stdin and stdout until the client goes away, and answers whatever ended it.

**Nothing here holds a writer onto stdout.** The transport does, and it reaches os.Stdout itself: this is the one place §9's *stdout is the answer* is not true of the process — a stray write would corrupt a frame rather than merely mislead a reader — and what makes that structural is that there is nothing for a command to write to even by mistake. The destination behind every tool retains rows, a buffer and io.Discard, and no stream at all (destination.go, issue #194).

It listens on no port and outlives nothing. `Server.Run` returns when the session ends, which is when the client that started this process closes its end — *one process per client, dying with it* (§9, §13, ADR-0088).

func (*Server) Tools

func (s *Server) Tools(ctx context.Context) ([]Declared, error)

Tools is `tools/list` as a client receives it: the thirteen §9 states, each carrying the two schemas it publishes, read off the wire (§9, issue #204).

It is the third door onto this server and it exists for the reason the first two do — the SDK is reachable from this package and no other, so a driver that wanted the listing had nowhere to stand. What it is for is the corpus's schema golden: **a schema is the contract an agent writes its calls against**, and a schema that drifts between two releases is the one way this surface can break a caller without any answer changing (internal/cli/mcp_tools_test.go).

**The listing is read off the recorded frame**, for the reason Call's envelope is and with more riding on it: a schema's keys are its author's order, and the SDK decodes a published schema into `any`, which is a map. A golden holding a re-encoded map would hold the encoder's ordering and would change under a Go release rather than under an edit to this package.

**The order is the wire's and not the table's.** The SDK holds its tools in a set keyed by name and pages them in name order, so what a client receives is the thirteen alphabetically; §9's group order is the table's own, and the fence that holds it holds it there (tools.go, tool_set_test.go).

func (*Server) Watched

func (s *Server) Watched(ctx context.Context, tool string, arguments json.RawMessage, token any) (Envelope, Watching, error)

Watched is Call under a progress token, and it answers **everything the client saw** beside the envelope: the progress notifications the server sent during the call, and the method of anything else it sent unasked (§9, ADR-0021, issue #202).

token is the progress token this call carries, and **nil is a call that supplies none** — which is Call above, and the reason the two are one function: *a notification is sent where the client supplied a token and nowhere else* is a claim about the difference between two calls, and a driver that reached one of them through a second assembly of the server would be comparing two servers rather than two calls.

What arrives is read off the recorded frames for the reason the envelope is: what a driver holds should be the messages hyper sent, in the order they arrived on the wire, rather than a client-side handler's account of them.

type Structured

type Structured struct {
	// Outcome is §12's triple, and it is `run`'s alone among the thirteen:
	// a tool that is not a Run carries no `outcome` key at all, and this is
	// the member that is absent there (§9, issue #200). It is **not**
	// restated by any row and it is not `isError` — one bit does not carry
	// three states.
	Outcome string `json:"outcome,omitempty"`
	// RunID is the entry the Run wrote, whole (ADR-0047), and absent
	// exactly where no entry was written: the version pin gate and the
	// bootstrap `store-absent`, the two paths that decline before a Run is
	// identified, and the 75s that stand before `run.json` (§8, §9).
	RunID string `json:"run_id,omitempty"`
	// DryRun rides beside Outcome and is written **wherever Outcome is**,
	// the bare `false` included — §7's one exception to the absence rule
	// holding on this surface for the reason it holds in the Store: what a
	// reader that takes its absence for `false` gets wrong is unrecoverable.
	//
	// It is a pointer for that reason and no other. `false` and *this tool
	// is not a Run* are two different answers, and a bare bool could only
	// say one of them.
	DryRun *bool `json:"dry_run,omitempty"`
	// Rendering is the command's page on the structured channel: **the same
	// string the text block carries**, written on the one case of §9's
	// asymmetric table whose block is a rendering rather than a reading of
	// the members beside it — `review`'s (§9, ADR-0100, issue #217).
	//
	// **MCP's two halves are not symmetric, and this member is that
	// asymmetry read the way the protocol states it.** `structuredContent`
	// is the result — *servers MUST provide structured results that conform
	// to this schema* wherever an `outputSchema` is declared — and the text
	// block beside it is *the serialized JSON*, returned **for backwards
	// compatibility**. So `content` is by the protocol's own account the
	// redundant half, and a client that reads the structured half in
	// preference to it is reading the half the protocol made normative. A
	// promise carried only in `content` is a promise carried where nothing
	// obliges anyone to look, and `review`'s page is the one promise on
	// this surface that the rows beside it cannot be composed back into.
	//
	// **It stands above `rows` for the reason the summary line stands above
	// them in the block.** A reader of the structured half meets the keys in
	// the order they are written, and a page beneath a hundred-row array is
	// one met after the thing it exists to be read instead of.
	//
	// It is absent everywhere else, and the absences are two different
	// arguments. A listing and a `check` lose nothing: every member of their
	// summary line is already a key here — the counts are the rows, the
	// triple and the Run id are keys of their own, the marker is `truncated`
	// — and what `check` puts beneath its line is the row set `rows` already
	// is. A Refusal loses nothing this member could give it: MCP names no
	// structured channel for an error at all, its whole error mechanism
	// being `isError` and the `content` beside it, so `content` is the only
	// place the protocol puts a Refusal and every client that reads one
	// reads it there.
	Rendering string `json:"rendering,omitempty"`
	// Rows is §8's row set unchanged: one object per row, carrying the same
	// `type` discriminator, served as an array rather than a line stream. It
	// is `[]` where the command found nothing rather than absent, which is
	// why it is built with a length and never left nil.
	Rows []json.RawMessage `json:"rows"`
	// Truncated is what the terminal row carried, bare.
	Truncated json.RawMessage `json:"truncated"`
}

Structured is `structuredContent`: §8's row set as an array, and the terminal fact beside it.

**There is no terminal row inside `rows`.** An array's end is already its own end-of-stream marker — the thing §8's terminal row exists to be on a line stream with no framing — so what the terminal row *carried* moves up here and the row itself does not travel (§9).

Both members are carried as raw JSON because both are already decided elsewhere and this surface may not re-decide them. A row's key order, its absent members and its unabbreviated digests are §8's contract held by the row type that declared them (render.Row); `truncated` is the terminal row's own member, in whichever of §9's three shapes its command wrote — the bare boolean on a namespace listing with no axis to name, and the marker object on a command whose parameters can narrow what it cut. Re-typing either here would be this package holding a second opinion about a shape it does not own.

type TextBlock

type TextBlock struct {
	// Type is the content type, and it is `text` on every block this
	// surface writes. It is carried rather than assumed because it is a
	// discriminator a client reads, and a block that arrived as anything
	// else is a block this surface did not compose (envelopeFrom).
	Type string `json:"type"`
	// Text is what §9's asymmetry decides: one summary line on an ordinary
	// return, `check`'s rows beneath that line, `review`'s full rendering,
	// or a Refusal's.
	Text string `json:"text"`
}

TextBlock is one member of `content`: MCP's text content, and the only content type this surface produces. hyper answers rows and renderings, and neither is an image, an audio clip or a resource link.

type Watching

type Watching struct {
	// Progress is one member per `notifications/progress`, in arrival order.
	Progress []Boundary
	// Unasked is the method of every other message the server sent: a
	// logging notification, a server-initiated request, anything at all. It
	// is empty on every path — *hyper never speaks first* — and it is
	// collected rather than assumed so that a driver holds the claim rather
	// than restating it (§9, ADR-0021).
	Unasked []string
}

Watching is what a client saw beside one call's envelope.

The two members are §9's two claims about this server's own voice, in a shape a driver can hold: what it sent because it was asked to, and what it sent unasked — which is nothing, on every path, always (ADR-0021).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL