Documentation
¶
Overview ¶
Package protocol layers MCP admission on top of the version-agnostic jsonrpc decoder: supported-version negotiation and adapters (MCP-PROTO-010/011), peer-role- and direction-aware method admission against the Culvert-reviewed registry (MCP-PROTO-002/016), session lifecycle validation (MCP-PROTO-012), and the listener-independent terminal-status transport primitive (MCP-PROTO-017).
Nothing here binds a socket, speaks HTTP, or holds a stream. The transport primitive is a pure decision function a future PR-5 listener will consult; PR-1 ships the decision, not the listener.
Index ¶
- func AdmittedMethods() []string
- func IsExplicitlyRejected(v Version) bool
- func IsSupported(v Version) bool
- func LifecycleAdmits(s State, method string) bool
- func ValidateRegistry() error
- type Adapter
- type Admission
- type Capability
- type Direction
- type Handling
- type Negotiation
- type PeerRole
- type State
- type TransportCondition
- type TransportDecision
- type Version
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AdmittedMethods ¶
func AdmittedMethods() []string
AdmittedMethods returns the admitted method names (registry order). Fresh copy.
func IsExplicitlyRejected ¶
IsExplicitlyRejected reports whether v is a known, named-excluded revision (as opposed to an unknown future/garbage string). Both are denied; this only aids diagnostics.
func IsSupported ¶
IsSupported reports whether v is in the reviewed allowlist.
func LifecycleAdmits ¶
LifecycleAdmits reports whether method is legal in state s, treating the handshake methods as one-time and gating everything else behind establishment. It is a pure function; the session package holds the state under its own lock.
- StateNew admits ONLY initialize (pre-negotiation bootstrap).
- StateInitializing admits notifications/initialized (to complete the handshake) and ping.
- StateInitialized admits every method EXCEPT the one-time handshake pair, so a duplicate initialize or a second notifications/initialized is an invalid-lifecycle error, not a silent no-op.
- StateClosed admits nothing.
func ValidateRegistry ¶
func ValidateRegistry() error
ValidateRegistry asserts the forward/reverse parity invariants of the admitted registry at runtime: every admitted method resolves to exactly one of {kernel-terminal, a per-capability decision point} — never both, never neither — and no method name is duplicated. It returns an error describing the first violation, or nil. A test calls this so a future registry edit that breaks parity fails the build.
Types ¶
type Adapter ¶
type Adapter interface {
Version() Version
Normalize(msg jsonrpc.Message) (jsonrpc.Message, error)
}
Adapter normalizes a decoded message from a specific supported version into the kernel's single internal, version-agnostic representation, so no downstream stage ever branches on protocol version (MCP-PROTO-011). For the V1 admitted six methods the two supported revisions share an identical envelope shape, so normalization is the identity — but the per-version adapter boundary is real and proven equivalent by tests, keeping the door open for a future revision with wire differences without leaking version into downstream code.
func AdapterFor ¶
AdapterFor returns the adapter for a supported version. An unsupported version has no adapter (default deny at negotiation, MCP-PROTO-010 / §8): the caller must never reach here with an unsupported version.
type Admission ¶
type Admission struct {
Handling Handling
// DecisionPoint is the named downstream owner for HandlingDecisionPoint.
DecisionPoint string
// Reason is set for HandlingRejected (always ReasonUnsupportedMethod).
Reason mcperr.Reason
// Detail is a fixed, non-hostile description for a rejection.
Detail string
}
Admission is the result of admitting a method on a leg/direction/capability.
func Admit ¶
Admit resolves how a method is handled for the given capability, requestor direction and wire class. It is the sole admission gate:
- a method absent from the registry is rejected (MCP-PROTO-016);
- a message whose wire class disagrees with the method's role is rejected — a notification-only method carrying an id, or a request method sent without one (the "notifications with ids" / "requests without ids" guard);
- a non-bidirectional method originated by the server (reverse channel) is rejected, so server-originated sampling/elicitation/roots and any other reverse request never dispatch (MCP-PROTO-015 / §5).
Admission is intentionally version-agnostic: the registry is Culvert's reviewed set, NOT "whatever the negotiated version contains". A method valid in the negotiated spec version but absent here is rejected all the same.
type Capability ¶
type Capability int
Capability is one of the two MCP surfaces. They share the strict parser but carry independent admission decision points, limits and namespaces (ADR-0024 §D-13): a method admitted on both surfaces still resolves to a different downstream owner per surface.
const ( // Gateway is the business MCP Security Gateway surface (Capability B). Gateway Capability = iota // Management is the read-only + draft/validate/simulate surface (Capability A). Management )
type Direction ¶
type Direction int
Direction is the requestor direction of a message — who originated the request. It is a first-class dimension of correlation state (session, direction, id): the same JSON-RPC id may be outstanding in BOTH directions at once, and one direction must never touch the other's state (MCP-PROTO-015). It is also an admission input: reverse-channel (server-originated) requests are not proxied in V1.
const ( // ClientOriginated — the natural client→server request flow (the client, or // Culvert acting as a client toward an upstream server, is the requestor). ClientOriginated Direction = iota // ServerOriginated — the reverse channel (the server is the requestor). In V1 // server-originated requests are rejected at admission. ServerOriginated )
type Handling ¶
type Handling int
Handling is how the kernel treats an admitted method.
const ( // HandlingRejected — the method is not admitted; there is no dispatch path. HandlingRejected Handling = iota // HandlingKernelTerminal — the kernel handles and answers the method itself and // never dispatches it downstream. HandlingKernelTerminal // HandlingDecisionPoint — the method is admitted and dispatched to exactly one // named downstream decision point (implemented in a later slice, not PR-1). HandlingDecisionPoint )
type Negotiation ¶
type Negotiation struct {
Requested Version
// Selected is always a supported version.
Selected Version
// Accepted is true when Requested was directly supported (Selected == Requested).
Accepted bool
// CounterOffered is true when Requested was unsupported and Culvert is
// counter-offering Selected (the client then accepts or terminates). This is
// the 200-InitializeResult path, preferred over a 4xx that would recruit a
// legacy probe (MCP-PROTO-017).
CounterOffered bool
}
Negotiation is the outcome of an initialize-body version negotiation.
func Negotiate ¶
func Negotiate(requested Version) Negotiation
Negotiate performs the initialize-body version decision. A supported requested version is accepted as-is; any other requested version yields a counter-offer of the primary supported version. Culvert never adopts an unsupported version and never silently downgrades to a legacy adapter (MCP-PROTO-010).
type PeerRole ¶
type PeerRole int
PeerRole identifies which untrusted leg a message arrived on. The SAME strict parser serves both (MCP-PROTO-015); only admission may differ.
type State ¶
type State int
State is a session's protocol lifecycle state. Lifecycle is validated so a session cannot skip establishment (MCP-PROTO-012): before negotiation the only admissible method is initialize (MCP-PROTO-002); the handshake methods are one-time.
const ( // StateNew — created, not yet initialized. Only initialize is admissible. StateNew State = iota // StateInitializing — initialize accepted, awaiting notifications/initialized. StateInitializing // StateInitialized — steady state; all admitted non-handshake methods allowed. StateInitialized // StateClosed — terminal; nothing is admissible. StateClosed )
func LifecycleNext ¶
LifecycleNext returns the state after a method that LifecycleAdmits accepted. Only the two handshake methods advance the state; everything else leaves it unchanged.
type TransportCondition ¶
type TransportCondition int
TransportCondition is a transport-layer situation whose terminal HTTP status the kernel decides. These are the security-motivated rejection cases from the D-1 baseline / MCP-PROTO-017. This is a PURE DECISION FUNCTION: it maps a condition to a terminal status and asserts the no-stream invariant. It binds no socket, opens no SSE stream, and knows nothing about HTTP transport beyond the status code — a future PR-5 listener consults it.
const ( // CondSessionlessMissingVersion — a sessionless / first request with no // MCP-Protocol-Version header. D-1 CLOSED: reject with 400 (never silently // assume 2025-03-26). CondSessionlessMissingVersion TransportCondition = iota // CondInvalidVersionHeader — an invalid or unsupported MCP-Protocol-Version // header on a subsequent request → 400. CondInvalidVersionHeader // CondMissingSessionID — a required session identifier is missing → 400. CondMissingSessionID // CondUnknownOrTerminatedSession — an unknown or terminated session → 404. CondUnknownOrTerminatedSession // CondDeleteUnsupported — an unsupported DELETE → 405. CondDeleteUnsupported // CondGetWithoutNegotiatedContext — a GET without a valid negotiated context → // terminal 405, and NO text/event-stream is opened (no legacy SSE, no // pre-negotiation held stream). CondGetWithoutNegotiatedContext // CondInitializeVersionUnsupported — an initialize whose requested version is // unsupported. Preferred handling is a 200 InitializeResult counter-offer of a // supported version — NOT a 4xx — so a spec-conformant or catch-any SDK client // is never recruited into the legacy 2024-11-05 probe. CondInitializeVersionUnsupported )
type TransportDecision ¶
type TransportDecision struct {
// Status is the HTTP status the listener must return.
Status int
// RetainStream is ALWAYS false: no condition here ever allocates or holds a
// stream. It is a field (not an implicit) so the invariant is visible and
// testable — "N rejected clients ⇒ zero retained streams" begins here.
RetainStream bool
// CounterOffer is true only for the initialize counter-offer (a 200 carrying a
// supported-version InitializeResult), false for every terminal 4xx.
CounterOffer bool
// Reason is a stable machine reason string.
Reason string
}
TransportDecision is the terminal outcome for a transport condition.
func DecideTransport ¶
func DecideTransport(cond TransportCondition) TransportDecision
DecideTransport maps a transport condition to its terminal decision. Every decision retains zero streams; only the initialize-unsupported case is a non-4xx (a 200 counter-offer). There is deliberately no case that returns a legacy SSE stream, an endpoint event, or an automatic fallback.
type Version ¶
type Version string
Version is an MCP protocol revision string.
const ( // VersionPrimary is the primary supported revision. VersionPrimary Version = "2025-11-25" // VersionFloor is the compatibility floor. VersionFloor Version = "2025-06-18" )
The frozen V1 baseline (D-1 CLOSED). The supported set is a finite reviewed allowlist; every other revision — including the explicitly-rejected ones below — is denied at negotiation with no best-effort interpretation and no silent downgrade (MCP-PROTO-010).
func SupportedVersions ¶
func SupportedVersions() []Version
SupportedVersions returns the allowlist (primary first) for callers that need to advertise or test it. The returned slice is a fresh copy.