inspector

package
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: May 30, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package inspector implements the host-side core of Dockyard's local inspector — the single test/debug surface for exercising an MCP server and its Apps without a real host (RFC §12).

The inspector is the lone client-shaped component Dockyard ships, and it is kept narrowly so: it is dev-mode-gated, localhost-only, and **operator-initiated only** — every mutating call the inspector issues happens as the direct result of an operator's deliberate UI action through the localhost-bound listener. Phase 27's security re-audit re-cast the pre-existing "read-only" framing as **operator-initiated only** to match the surface that grew through D-131 (operator-initiated `tools/call`) and D-134 (operator-initiated elicitation `tasks/result`); the older "read-only" wording was honest before those decisions and overpromised after them. The new framing remains within P4: the inspector is the lone client-shaped component, dev-mode-gated, localhost-bound, and never an arbitrary-execution proxy — and every mutating call has a named operator trigger, an audited code path, and a corresponding decision entry.

This package enforces the localhost-only property mechanically — New refuses any non-loopback bind address with ErrNonLoopbackBind, and the listener is never opened for a non-loopback address. The CVE-2025-49596 RCE in the official MCP Inspector's proxy is the cautionary tale (brief 05 §4.2): the inspector relays only what the UI needs, and is never an arbitrary-execution proxy.

What this package builds:

  • Inspector — a localhost HTTP server that serves the embedded web/inspector frontend and relays the obs/v1 SSE stream and a JSON-RPC log to it. It is a reusable concurrent artifact: many UI clients may connect and disconnect concurrently.
  • The obs/v1 relay — a pure SSE client of runtime/obs's SSE sink (P2: the inspector consumes the public obs/v1 contract, it never reads runtime internals).
  • The read-only `/api/verdicts` and `/api/contracts` sources — the Verdicts panel reuses internal/validate.Run (see VerdictsFromValidate), and the fixture switcher derives its fixtures from the generated tool contracts (P1). Both are optional Options fields; when unset the endpoints answer with an empty array so the UI renders its four-state empty state.
  • The read-only `/api/apps` source — AppsFromServer renders the attached server's ui:// Apps by a read-only resources/list + resources/read of the server (RFC §12 line 711 — the inspector renders the server's Apps).
  • The operator-initiated `/api/tools/invoke` surface — ToolsFromServer opens a short-lived MCP client session, calls `tools/call` once, and closes (D-131). The operator triggers this through the UI's Invoke button; no other actor can.
  • The operator-initiated `/api/tasks/elicitation` surface — ElicitationFromServer posts a single `tasks/result` JSON-RPC frame to deliver an App's elicitation reply (D-134). The operator triggers this through the App preview's Approve / Reject button; no other actor can.
  • The host-half of the ui/ bridge, the fixture switcher, per-tool analytics, capability-set emulation, and task-lifecycle rendering all live in the web/inspector frontend; this package serves that frontend and the App preview HTML, and is consumed by the `dockyard inspect` CLI verb.

The inspector was built across Phase 22 (the core — the HTTP backend, the relay, the obs view), Phase 23 (the advanced surface — verdicts, contracts, and the `dockyard inspect` command), Phase 24's finishing pass (D-131 operator-initiated `tools/call`), and Phase 25 (D-134 operator-initiated elicitation forwarding). Phase 27's security re-audit captures the production `mcp.NewClient` set (this package's AppsFromServer + ToolsFromServer + ElicitationFromServer, plus `internal/installpkg`'s boot check D-088) in `test/integration/phase27_inspector_security_test.go`; any new mcp.NewClient call site outside that set is a P4 violation that fails the audit before it can merge.

The inspector is not a production MCP client. It performs four operator- initiated client-shaped operations: it relays a server's obs/v1 SSE stream (read-only); it renders the server's Apps via a read-only `resources/list + resources/read` of the server's ui:// resources (D-103); it issues a short-lived `tools/call` when the operator clicks Invoke (D-131); and it posts a `tasks/result` frame when the operator clicks Approve / Reject inside an App's elicitation prompt (D-134). It stays dev-gated and localhost-only, never holds a long-lived client, and is never an arbitrary-execution proxy (P4).

Package inspector — on-disk fixture loader (Phase 24, D-126).

The Phase 23 fixture switcher synthesises structuredContent from the tool's generated output schema. That is structurally correct (P1) but the synthesised values are placeholders — "sample-value" strings, 42 numbers — so the App's dispatcher does not see a kind it knows ("chart" / "table" / "metric_card") and the rendered widget can never be the realistic data the template ships.

The fixture-loader below complements the switcher: when a project on disk carries a `fixtures/<tool>/<kind>.json` tree (the analytics-widgets template ships eighteen), the inspector loads them, and the frontend switcher prefers the on-disk payload over the synthesised one. The schema-derived path remains the fallback for projects that ship no fixtures, so the existing behaviour is preserved.

The loader is read-only. It performs no `tools/call`; it only reads files the developer's own project carries. RFC §12 / P4 (the inspector is a dev surface, never a production MCP client) is preserved.

Index

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("dockyard/internal/inspector: inspector closed")

ErrClosed is returned by Inspector.Serve after Inspector.Close.

View Source
var ErrNonLoopbackBind = errors.New(
	"dockyard/internal/inspector: refuses non-loopback bind address " +
		"(the inspector is dev-mode-only and localhost-bound)")

ErrNonLoopbackBind is returned (wrapped) by New when the inspector is asked to bind a non-loopback address. The inspector is dev-mode-gated and localhost-only; a non-loopback bind is rejected before the listener opens and is never served. This is the binding RFC §12 acceptance criterion and the CVE-2025-49596 lesson (brief 05 §4.2).

Functions

func EmbeddedAssets

func EmbeddedAssets() fs.FS

EmbeddedAssets returns the embedded inspector frontend rooted at its dist/ directory, ready to pass as Options.Assets. When no real bundle has been staged (the dist/ tree carries only its .gitkeep anchor), the inspector serves its built-in placeholder page (see assets.go) — the backend is always usable.

func PromptsFromServer added in v1.1.0

func PromptsFromServer(baseURL string) (PromptSource, PromptInvoker)

PromptsFromServer adapts a running MCP server, named by its base URL, into a PromptSource + PromptInvoker pair. It is the v1.1 wave A (D-163) extension of the inspector's operator-initiated client- shaped surfaces — the pattern D-103 set for resources/read, D-131 set for tools/call, D-134 set for tasks/result.

Each call opens a fresh, short-lived MCP client session, makes one RPC, and closes — no long-lived production client. The inspector stays within P4: the lone client-shaped component, dev-mode-gated, localhost-bound (the listener's requireLoopback gate enforces it).

A nil baseURL yields a pair that returns an error on call — without an attached server there is nothing to list or invoke, and the frontend's PromptsPanel surfaces the error in its panel state.

Types

type AppPreview

type AppPreview struct {
	// URI is the App's ui:// resource URI.
	URI string `json:"uri"`
	// Name is the App's display name (the resource Name, falling back to URI).
	Name string `json:"name"`
	// HTML is the App's HTML document, read from the server's ui:// resource.
	HTML string `json:"html"`
}

AppPreview is one MCP App the inspector can render — its ui:// resource URI, a display name, and the App's HTML document. It is the inspector's own type: no raw MCP SDK struct leaks through it (P3). The frontend's App-frame sets HTML as the sandboxed iframe's srcdoc.

type AppSource

type AppSource func(ctx context.Context) ([]AppPreview, error)

AppSource produces the MCP Apps the inspector can render, on demand. The inspector calls it per `GET /api/apps` request. It is read-only: it performs a resources/list and a resources/read against the attached server and never a mutating call (RFC §12, P4). When the source cannot reach a server, or the server registers no ui:// resource, it returns an empty slice and a nil error — the App-frame renders its honest "No App attached" empty state.

func AppsFromServer

func AppsFromServer(baseURL string) AppSource

AppsFromServer adapts a running MCP server, named by its base URL, into an AppSource. It is the App-render path RFC §12 line 711 makes binding: the inspector "renders the server's Apps". Obtaining an App's UI is a read-only resources/read of the server's ui:// resource(s) — within P4 (the inspector is the lone client-shaped surface, a dev surface; a read-only resource read is not a production MCP client and not arbitrary execution). See D-103, which extends D-099: the inspector additionally performs read-only resources/list and resources/read to render Apps — still no mutation, still dev-gated and localhost-only.

The returned source opens a fresh, short-lived MCP client session per call, reads every ui:// resource, and closes the session — it holds no long-lived client. An empty baseURL yields a source that returns no Apps (the inspector is detached). A connect or read failure is returned as a typed error; the `/api/apps` handler maps it to the App-frame's error state.

type ContractsSource

type ContractsSource func() json.RawMessage

ContractsSource produces the attached server's generated tool contracts as a JSON array, on demand. The inspector calls it per `GET /api/contracts` request. It is content-free of any runtime internal — it returns the same generated-contract JSON the inspector frontend's contract model decodes.

func ContractsFromProject

func ContractsFromProject(projectDir string) ContractsSource

ContractsFromProject adapts a Dockyard project's manifest and its generated JSON Schema files into a ContractsSource rooted at projectDir. It is the seam RFC §12 names: the inspector's fixture switcher derives its fixtures from the project's generated tool contracts (P1 — contract-first), never from a hand-written schema.

The returned source reads, per request, the project's `dockyard.app.yaml` for the tool list and each tool's generated `<tool>_input.schema.json` / `<tool>_output.schema.json` from internal/contracts/ (the files `dockyard generate` writes). A tool whose schema file is missing — the project was never `dockyard generate`d — still yields a contract row with an empty schema; the fixture switcher renders its four-state empty state for that tool rather than crashing.

When the project has no manifest at all (the inspector is attached to a remote `--url` with no local project), the source returns an empty array and the Fixtures / Tools panels degrade to their honest empty state. The source never panics and never returns a nil json.RawMessage.

type ElicitationRequest

type ElicitationRequest struct {
	// TaskID is the id of the task the elicitation answers. Required.
	TaskID string `json:"taskId"`
	// Data is the user's reply, opaque to the inspector — the receiving
	// handler decodes it against its own contract. May be absent when
	// Declined is true.
	Data json.RawMessage `json:"data,omitempty"`
	// Declined is the explicit "user declined to answer" signal. The
	// MCP Tasks experimental spec carries this through as the
	// elicited-input's declined flag.
	Declined bool `json:"declined,omitempty"`
}

ElicitationRequest is the typed request the inspector frontend POSTs to `/api/tasks/elicitation` (Phase 25 / D-134). The body carries the task id the elicitation answers and the App-supplied reply payload.

Distinct from the bridge's ElicitationResponseParams (which is the View→host postMessage shape): this is the inspector's HTTP shape. The inspector backend translates one into the other.

type ElicitationResponse

type ElicitationResponse struct {
	// TaskID echoes the id of the task the elicitation was delivered to.
	TaskID string `json:"taskId"`
	// Delivered is true when the attached server accepted the
	// elicitation; false (with a non-empty Error) when the server
	// refused.
	Delivered bool `json:"delivered"`
	// Error is the server's typed error message when Delivered is
	// false. Absent on a successful delivery.
	Error string `json:"error,omitempty"`
}

ElicitationResponse is the inspector's reply to a successful elicitation delivery. The reply is a bare acknowledgement — the App observes the task's terminal status through the subsequent `tool-result` push or through the inspector's Tasks panel, not through this response.

type Elicitor

type Elicitor func(ctx context.Context, req ElicitationRequest) (*ElicitationResponse, error)

Elicitor delivers one operator-initiated elicitation-response to the attached MCP server (Phase 25 / D-134). The inspector calls it per `POST /api/tasks/elicitation` request. Like ToolInvoker it is the lone mutating surface the inspector exposes for this operation: localhost-only via the listener's `requireLoopback` gate; the operator is the one driving the write through the UI (the App's "Approve" / "Reject" button); the inspector never speaks tasks/* on its own.

Returns a typed error when the underlying server call fails (a connect or RPC error). The `/api/tasks/elicitation` handler maps a non-nil error to HTTP 502 with a typed JSON body so the inspector frontend surfaces an honest error state.

func ElicitationFromServer

func ElicitationFromServer(baseURL string) Elicitor

ElicitationFromServer adapts a running MCP server, named by its base URL, into an Elicitor. The implementation speaks raw JSON-RPC: the MCP Tasks methods (tasks/result etc.) sit outside the go-sdk's dispatch table (the experimental extension — RFC §8.2), so a real Tasks client posts them as plain JSON-RPC frames over the same streamable-HTTP endpoint the server already serves. This is the same pattern the R2 integration test uses (`r2_tasks_mount_test.go`'s `r2RPC` helper) — D-134 routes a single such frame through the inspector's HTTP boundary.

A nil baseURL yields a source that returns an error — without an attached server there is no task to resume, and the inspector frontend surfaces the error.

type FixtureKind

type FixtureKind string

FixtureKind is one of the six fixture kinds (RFC §12, brief 04 §2.2).

const (
	FixtureHappy      FixtureKind = "happy"
	FixtureEmpty      FixtureKind = "empty"
	FixtureError      FixtureKind = "error"
	FixturePermission FixtureKind = "permission"
	FixtureSlow       FixtureKind = "slow"
	FixtureLarge      FixtureKind = "large"
)

The six fixture kinds the switcher offers.

type FixtureSource

type FixtureSource func() ([]ProjectFixture, error)

FixtureSource produces the project's on-disk fixtures, by tool. It is invoked per `GET /api/fixtures` request — the inspector re-reads on every request so a developer can edit a fixture without restarting.

func FixturesFromDir

func FixturesFromDir(dir string) FixtureSource

FixturesFromDir builds a FixtureSource that reads from `<dir>/fixtures/`. An empty dir, or a missing fixtures/ subtree, returns an empty slice and a nil error — the inspector degrades to the schema-derived synthetic fixtures. A malformed JSON file is a typed error so the developer notices.

type Inspector

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

Inspector is the inspector's localhost HTTP backend (RFC §12). It serves the web/inspector frontend and relays the obs/v1 stream and JSON-RPC log to it, read-only. It is dev-mode-gated and localhost-only: New guarantees the bind address is a loopback interface.

Inspector is a reusable concurrent artifact: Inspector.Serve runs the HTTP server, many UI clients may connect and disconnect concurrently, and Inspector.Close is idempotent and safe to call concurrently with Serve.

func New

func New(opts Options) (*Inspector, error)

New constructs an Inspector bound to a loopback address. A non-loopback, wildcard, or malformed bind address is rejected with ErrNonLoopbackBind — the listener is NOT opened. The returned Inspector is not yet serving; call Inspector.Serve.

func (*Inspector) Addr

func (i *Inspector) Addr() string

Addr returns the inspector's resolved listen address, including the OS-assigned port when the construction address used port 0. It is always a loopback address.

func (*Inspector) Close

func (i *Inspector) Close() error

Close shuts the inspector down. It stops the HTTP listener and is idempotent — a second call is a no-op (CLAUDE.md §5, the Closer contract). Close is safe to call concurrently with Serve.

func (*Inspector) Serve

func (i *Inspector) Serve(ctx context.Context) error

Serve runs the inspector HTTP server until ctx is cancelled or Inspector.Close is called. It blocks. Serve may be called once; a second call returns ErrClosed. A clean shutdown (ctx cancelled or Close) returns nil.

func (*Inspector) URL

func (i *Inspector) URL() string

URL returns the http:// URL the inspector UI is served at.

type InvokeRequest

type InvokeRequest struct {
	// Tool is the registered tool name to call. Required.
	Tool string `json:"tool"`
	// Arguments is the typed input the handler receives. The wire shape is
	// json.RawMessage so the inspector never decodes user input into a runtime
	// struct (P3 — no raw protocol struct leaks; the runtime/server schema
	// validates the payload at the catalog edge before the handler runs).
	Arguments json.RawMessage `json:"arguments"`
}

InvokeRequest is the operator's typed request to `POST /api/tools/invoke`. The body is small on purpose: a tool name and a JSON-object arguments value. The inspector frontend builds Arguments from the tool's generated input JSON Schema (P1 — the schema is the source of truth for the form's shape).

type InvokeResponse

type InvokeResponse struct {
	// Content is the MCP CallToolResult content[]: text and any non-structured
	// content parts the tool emitted. Marshalled as JSON so the inspector can
	// surface a faithful view in its result viewer.
	Content json.RawMessage `json:"content,omitempty"`
	// StructuredContent is the tool's typed structured payload — the value the
	// App-frame's pushToolResult path consumes. May be omitted (a tool that
	// emits only text content).
	StructuredContent json.RawMessage `json:"structuredContent,omitempty"`
	// IsError is the MCP CallToolResult.isError flag: a tool-level error
	// reported to the host rather than a protocol-level error.
	IsError bool `json:"isError,omitempty"`
}

InvokeResponse is the JSON the inspector returns from a successful `POST /api/tools/invoke`. Content carries the model-facing text parts; the inspector frontend reads only StructuredContent for its App preview render (the same path the Fixtures switcher's pushToolResult flows through — D-129). IsError mirrors the MCP CallToolResult.isError flag: a tool that returned a typed error to the host is still a successful RPC, surfaced here so the inspector can render the error state without conflating it with a transport-level failure.

This is the inspector's own type — no raw MCP SDK struct leaks through it (P3, mirroring AppPreview and Verdict).

type Options

type Options struct {
	// Addr is the loopback listen address. Empty selects [defaultAddr] (an
	// OS-assigned loopback port). A non-loopback or wildcard address is rejected
	// by [New] with [ErrNonLoopbackBind].
	Addr string

	// ServerInfo is the identity of the attached MCP server, surfaced read-only
	// in the inspector UI. The zero value is tolerated (an unknown server).
	ServerInfo ServerInfo

	// Relay sources the obs/v1 SSE stream and the JSON-RPC log the inspector
	// relays to its UI. When nil, the relay endpoints report an empty stream —
	// the inspector still serves its UI (the four-state empty state).
	Relay *Relay

	// Assets is the embedded web/inspector frontend (its built dist/ tree). When
	// nil, the inspector serves a minimal built-in placeholder so the backend is
	// usable before `vite build` has run — the Go build never depends on the
	// frontend being built.
	Assets fs.FS

	// Verdicts is the read-only source for the inspector's Verdicts panel —
	// contract-drift, schema-validation, and spec-compliance results (RFC §12).
	// When nil, `GET /api/verdicts` returns an empty array and the UI renders
	// its four-state empty state. Use [VerdictsFromValidate] to source it from
	// the `dockyard validate` engine.
	Verdicts VerdictSource

	// Contracts is the read-only source for the inspector's generated tool
	// contracts — the JSON array the fixture switcher derives its fixtures
	// from (RFC §12, §6 — P1, contract-first). When nil, `GET /api/contracts`
	// returns an empty array and the Fixtures/Tools panels render their
	// four-state empty state. The source returns the contracts as a JSON
	// array of `{name, description, inputSchema, outputSchema}` objects.
	Contracts ContractsSource

	// Apps is the read-only source for the inspector's App-preview frame — the
	// attached server's ui:// App resources, read via a read-only resources/read
	// (RFC §12 line 711 — the inspector renders the server's Apps; D-103). When
	// nil, `GET /api/apps` answers with an empty array and the App-frame renders
	// its "No App attached" empty state. Use [AppsFromServer] to source it from
	// a running MCP server's ui:// resources.
	Apps AppSource

	// Fixtures is the read-only source for the inspector's on-disk fixture
	// loader (RFC §12, Phase 24 / D-126). When nil, `GET /api/fixtures` answers
	// with an empty array and the Fixtures switcher falls back to its
	// schema-derived synthetic fixtures (the Phase 23 default). Use
	// [FixturesFromDir] to source it from the developer's project directory.
	Fixtures FixtureSource

	// Invoker performs one operator-initiated tools/call against the attached
	// MCP server (RFC §12, P4; D-131). When nil, `POST /api/tools/invoke`
	// answers 503 — the inspector is detached. Use [ToolsFromServer] to source
	// it from a running MCP server. D-131 extends D-099 + D-103: the inspector
	// additionally issues real tools/call to the attached server when an
	// operator initiates it through the UI; still within P4 (the inspector is
	// the lone client-shaped component, dev-mode-gated, localhost-bound; the
	// operator is the one driving the write — not arbitrary off-localhost
	// execution).
	Invoker ToolInvoker

	// Elicitor delivers one operator-initiated elicitation-response to the
	// attached MCP server (Phase 25 / D-134). When nil,
	// `POST /api/tasks/elicitation` answers 503 — the inspector is detached or
	// the operator has not wired an elicitor. Use [ElicitationFromServer] to
	// source it from a running MCP server. D-134 extends D-131: the inspector
	// additionally forwards an App's elicitation-response notification to the
	// attached server's `tasks/result` endpoint, on the operator's
	// deliberate "Approve" / "Reject" click in the App preview. Still within
	// P4 (localhost-bound, operator-driven, no long-lived client).
	Elicitor Elicitor

	// Prompts is the read-only source for the inspector's Prompts panel
	// (v1.1 Wave A; D-163). When nil, `GET /api/prompts` answers with an
	// empty array and the Prompts panel renders its four-state empty
	// state. Use [PromptsFromServer] to source it from a running MCP
	// server's prompts/list call.
	Prompts PromptSource

	// PromptInvoker performs one operator-initiated prompts/get against
	// the attached MCP server (v1.1 Wave A; D-163 — extends D-131's
	// operator-initiated framing to a third client-shaped surface).
	// When nil, `POST /api/prompts/get` answers 503. Use
	// [PromptsFromServer] which returns this paired with the source.
	PromptInvoker PromptInvoker

	// Logger is the structured logger. When nil, a no-op logger is used.
	Logger *slog.Logger
}

Options configures a new Inspector.

type ProjectFixture

type ProjectFixture struct {
	// Tool is the tool name this fixture belongs to (e.g. "create_chart").
	Tool string `json:"tool"`
	// Kind is one of the six FixtureKind values.
	Kind FixtureKind `json:"kind"`
	// Description is the human-readable summary from the JSON.
	Description string `json:"description,omitempty"`
	// State is the UI state the fixture stands for ("ready"/"empty"/...).
	State string `json:"state"`
	// Input is the model-side input — surfaced for the inspector's RPC log
	// and the integration test, never used by the App's renderer.
	Input map[string]any `json:"input,omitempty"`
	// StructuredContent is the payload the App's dispatcher receives. When
	// the on-disk fixture carries `output_override`, it is used verbatim;
	// otherwise the loader derives it from `input` by pinning `kind` and
	// `state` so the App's discriminator routes correctly.
	StructuredContent map[string]any `json:"structuredContent"`
}

ProjectFixture is one fixture surfaced to the frontend switcher. It is the inspector's own type — no JSON wire shape from a template leaks through unchanged (the loader normalises every field).

type PromptArgumentInfo added in v1.1.0

type PromptArgumentInfo struct {
	Name        string `json:"name"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	Required    bool   `json:"required,omitempty"`
}

PromptArgumentInfo describes one argument an MCP Prompt accepts, as surfaced to the inspector frontend. The shape mirrors the runtime's PromptArgument verbatim so the inspector form generates the same fields a host would surface to a user. Strings only — MCP prompt arguments are flat string-keyed maps (D-152).

type PromptGetMessage added in v1.1.0

type PromptGetMessage struct {
	Role string `json:"role"`
	Text string `json:"text"`
}

PromptGetMessage is one rendered prompt message — a role plus a text body. Mirrors runtime/server.PromptMessage's text-only shape (D-151).

type PromptGetRequest added in v1.1.0

type PromptGetRequest struct {
	Name      string            `json:"name"`
	Arguments map[string]string `json:"arguments,omitempty"`
}

PromptGetRequest is the operator's typed request to `POST /api/prompts/get`: the prompt name and its flat string-keyed argument map. The map shape comes straight from the MCP wire format (D-152 — prompt arguments are flat strings).

type PromptGetResponse added in v1.1.0

type PromptGetResponse struct {
	// Description is the optional rendered description override; falls
	// back to the registered PromptDef.Description on the server.
	Description string `json:"description,omitempty"`
	// Messages is the rendered messages, in order.
	Messages []PromptGetMessage `json:"messages"`
	// Error, when non-empty, carries a server-side prompts/get error —
	// a successful RPC where the server reported a typed error. The
	// frontend renders the panel's error region without conflating it
	// with a transport-level failure (the 200-with-error pattern
	// D-131 set for tools/invoke; D-163 extends it to prompts/get).
	Error string `json:"error,omitempty"`
}

PromptGetResponse is the JSON the inspector returns from a successful `POST /api/prompts/get`. Messages is the rendered message list; an empty Messages slice serialises as `[]` so the frontend renders "rendered no messages" rather than null.

type PromptInfo added in v1.1.0

type PromptInfo struct {
	Name        string               `json:"name"`
	Title       string               `json:"title,omitempty"`
	Description string               `json:"description,omitempty"`
	Arguments   []PromptArgumentInfo `json:"arguments,omitempty"`
}

PromptInfo describes one registered MCP Prompt the inspector lists on `GET /api/prompts`. It is the inspector's own type — no raw SDK struct leaks (P3, mirroring InvokeResponse and AppPreview).

type PromptInvoker added in v1.1.0

type PromptInvoker func(ctx context.Context, req PromptGetRequest) (*PromptGetResponse, error)

PromptInvoker performs one operator-initiated prompts/get against the attached MCP server and returns the rendered messages. The inspector calls it per `POST /api/prompts/get` request. It is the lone client-shaped mutating-shaped surface for prompts (D-163 extends D-131 to a third operator-initiated read).

"Mutating-shaped" rather than mutating: a prompts/get is a read in the MCP semantic sense (the host pulls a rendered template — no side-effect on the server), but the request body is operator authored and the inspector treats it under the same operator- initiated framing as tools/call so the audit shape is uniform.

type PromptSource added in v1.1.0

type PromptSource func(ctx context.Context) ([]PromptInfo, error)

PromptSource produces the attached server's registered prompts. The inspector calls it per `GET /api/prompts` request. It is the read-only listing surface (D-103 read-only pattern, extended).

type RPCDirection

type RPCDirection string

RPCDirection is the direction of a logged JSON-RPC message relative to the MCP server: a request/notification inbound to the server, or a response outbound from it.

const (
	// RPCInbound is a message sent toward the MCP server (a request, a
	// notification, or a ui/* bridge call).
	RPCInbound RPCDirection = "inbound"
	// RPCOutbound is a message returned from the MCP server (a response).
	RPCOutbound RPCDirection = "outbound"
)

type RPCEntry

type RPCEntry struct {
	// Seq is a monotonic sequence number, assigned by the relay on append.
	Seq int64 `json:"seq"`
	// Timestamp is when the entry was logged, in UTC.
	Timestamp time.Time `json:"timestamp"`
	// Direction is inbound (toward the server) or outbound (from it).
	Direction RPCDirection `json:"direction"`
	// Method is the JSON-RPC method (e.g. "tools/call", "ui/initialize"). It is
	// empty for a response entry.
	Method string `json:"method,omitempty"`
	// Payload is the JSON-RPC message payload, JSON-encoded.
	Payload json.RawMessage `json:"payload,omitempty"`
}

RPCEntry is one entry in the inspector's read-only JSON-RPC log. It is a content-free-by-default record of wire traffic — method, direction, and the JSON payload — surfaced in the inspector's RPC panel. It is the inspector's own type: no raw SDK struct leaks through it (P3).

type Relay

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

Relay is the inspector's read-only bridge between a running MCP server's observability surfaces and the inspector UI. It does two things, both read-only (RFC §12 — the inspector is never an arbitrary-execution proxy):

  • it is a pure SSE *client* of runtime/obs's obs/v1 SSE sink (P2 — it consumes the public obs/v1 contract, never runtime internals), and fans the stream out to every connected inspector UI client;
  • it holds a bounded ring of recent JSON-RPC log entries for the RPC panel.

Relay is a reusable concurrent artifact: Relay.Run streams from the obs sink, many UI clients may Relay.Subscribe concurrently, and Relay.LogRPC is safe from any goroutine.

func NewRelay

func NewRelay(obsURL string) *Relay

NewRelay constructs a Relay. obsURL is the obs/v1 SSE stream URL of the attached runtime/obs SSE sink (e.g. "http://127.0.0.1:54321/obs/v1/stream"); an empty obsURL disables obs streaming but the relay still serves an empty stream and the RPC log. The relay does not connect until Relay.Run.

func (*Relay) Close

func (r *Relay) Close() error

Close releases the relay: it deregisters every subscriber and stops accepting new ones. It is idempotent. After Close, Relay.Subscribe returns a closed channel and Relay.LogRPC is a no-op.

func (*Relay) Dropped

func (r *Relay) Dropped() int64

Dropped reports the total obs events dropped to a slow inspector UI client. It is monotonically non-decreasing.

func (*Relay) LogRPC

func (r *Relay) LogRPC(direction RPCDirection, method string, payload json.RawMessage)

LogRPC appends a JSON-RPC log entry to the relay's bounded ring. It is safe from any goroutine. direction and method classify the entry; payload is the raw JSON-RPC message. The relay assigns the sequence number and timestamp.

func (*Relay) ObsURL

func (r *Relay) ObsURL() string

ObsURL reports the configured obs/v1 stream URL, or "" when obs streaming is disabled.

func (*Relay) RPCLog

func (r *Relay) RPCLog() []RPCEntry

RPCLog returns a snapshot copy of the current JSON-RPC log, oldest first. The returned slice is safe for the caller to retain.

func (*Relay) Run

func (r *Relay) Run(ctx context.Context)

Run connects to the obs/v1 SSE sink and fans every received event out to connected inspector UI clients until ctx is cancelled. It is the relay's pure SSE-client loop. Run reconnects on a dropped upstream connection with a small backoff — a dev server restart does not kill the inspector's stream. Run returns when ctx is cancelled. With no obsURL configured, Run returns immediately.

func (*Relay) Subscribe

func (r *Relay) Subscribe() (<-chan []byte, func())

Subscribe registers an inspector UI client for the relayed obs stream. It returns a receive channel of obs/v1 event JSON payloads and an unsubscribe function. The channel is bounded; a slow consumer has events dropped (counted by Relay.Dropped) — the relay never blocks (CLAUDE.md §8). The unsubscribe function is idempotent.

func (*Relay) Subscribers

func (r *Relay) Subscribers() int

Subscribers reports the count of connected inspector UI stream clients.

type ServerInfo

type ServerInfo struct {
	// Name is the connected server's name.
	Name string `json:"name"`
	// Version is the connected server's version.
	Version string `json:"version"`
	// Transport is the MCP transport in use: stdio | http | inmem.
	Transport string `json:"transport"`
}

ServerInfo is the read-only identity of the MCP server the inspector is attached to. It is surfaced verbatim in the inspector's PageHeader — no raw SDK or runtime struct leaks through it (P2/P3).

type ToolInvoker

type ToolInvoker func(ctx context.Context, req InvokeRequest) (*InvokeResponse, error)

ToolInvoker performs one operator-initiated tools/call against the attached MCP server and returns the result. The inspector calls it per `POST /api/tools/invoke` request. It is the lone mutating surface in the inspector backend — gated by the operator's UI action and by the inspector's localhost-only bind (RFC §12, P4; D-131 extends D-099 + D-103 to add operator-initiated tools/call to the inspector's read-only attach).

The Arguments is the raw JSON object the operator supplied; the runtime/server schema validates it at the catalog edge before the typed handler runs (P1 — the generated input JSON Schema is the source of truth).

func ToolsFromServer

func ToolsFromServer(baseURL string) ToolInvoker

ToolsFromServer adapts a running MCP server, named by its base URL, into a ToolInvoker. It is the operator-initiated tools/call path D-131 makes binding: the inspector additionally issues real tools/call to the attached server when an operator initiates it through the UI. This stays within P4:

  • The inspector remains the lone client-shaped surface, dev-mode-gated, localhost-only (the listener's `requireLoopback` gate already enforces this — see [requireLoopback]).
  • Each invocation opens a short-lived client session, calls one tool, and closes — no long-lived production client.
  • The operator is the one driving the write through the UI, not an off-localhost actor; symmetric to D-103's read-only resources/read for App rendering.

A nil baseURL yields a source that returns an error — without an attached server there is no tool to call, and the inspector frontend surfaces the error in its result region.

type Verdict

type Verdict struct {
	// Check is the verdict's check class: "manifest", "schema",
	// "spec-compliance", "stale-codegen", … — the validate Check taxonomy.
	Check string `json:"check"`
	// Severity is the rendered tone: "ok" | "warn" | "error".
	Severity string `json:"severity"`
	// Message is the human-facing, actionable description.
	Message string `json:"message"`
}

Verdict is one row of the inspector's Verdicts panel — a contract-drift, schema-validation, or spec-compliance result surfaced read-only to the inspector UI as an ok / warn / error chip (RFC §12). It is the inspector's own type: no raw validate or codegen struct leaks through it (P3).

type VerdictSource

type VerdictSource func() []Verdict

VerdictSource produces the current verdict set on demand. The inspector calls it per `GET /api/verdicts` request so the verdicts reflect the project as it is now — re-running the checks, never caching a stale result.

func VerdictsFromValidate

func VerdictsFromValidate(projectDir string) VerdictSource

VerdictsFromValidate adapts internal/validate.Run into a VerdictSource rooted at projectDir. It is the seam RFC §12 names: the Verdicts panel reuses the `dockyard validate` engine — contract-drift (stale-codegen), schema validation, and spec compliance — rather than reimplementing the checks.

The returned source maps every validate Diagnostic to a Verdict row. When a run produces no diagnostics at all, it returns a single "ok" verdict so the panel renders a clean ready state rather than an empty one. When validation cannot run at all (a missing project), it returns one "error" verdict describing the fault — the panel degrades gracefully, never blank.

Jump to

Keyboard shortcuts

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