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 ¶
- Variables
- func EmbeddedAssets() fs.FS
- type AppPreview
- type AppSource
- type ContractsSource
- type ElicitationRequest
- type ElicitationResponse
- type Elicitor
- type FixtureKind
- type FixtureSource
- type Inspector
- type InvokeRequest
- type InvokeResponse
- type Options
- type ProjectFixture
- type RPCDirection
- type RPCEntry
- type Relay
- func (r *Relay) Close() error
- func (r *Relay) Dropped() int64
- func (r *Relay) LogRPC(direction RPCDirection, method string, payload json.RawMessage)
- func (r *Relay) ObsURL() string
- func (r *Relay) RPCLog() []RPCEntry
- func (r *Relay) Run(ctx context.Context)
- func (r *Relay) Subscribe() (<-chan []byte, func())
- func (r *Relay) Subscribers() int
- type ServerInfo
- type ToolInvoker
- type Verdict
- type VerdictSource
Constants ¶
This section is empty.
Variables ¶
var ErrClosed = errors.New("dockyard/internal/inspector: inspector closed")
ErrClosed is returned by Inspector.Serve after Inspector.Close.
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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
// 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 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 ¶
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 ¶
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 ¶
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 ¶
ObsURL reports the configured obs/v1 stream URL, or "" when obs streaming is disabled.
func (*Relay) RPCLog ¶
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 ¶
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 ¶
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 ¶
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.