03 Wire protocol

Eight envelopes, one per line

The semantic layer — frames, queries, capabilities — is defined independently of transport. The current binding is NDJSON: every message is a single JSON envelope, internally tagged by type.

Over stdio, exactly one envelope travels per line — an envelope must not contain a literal newline. Over streamable HTTP, one envelope is the request body and one is the response body.

CGP is not JSON-RPC. There is no jsonrpc member and no method/params split. The lifecycle is informed by MCP — handshake before payload — but the framing is its own. A JSON-RPC binding may be specified later as an alternate encoding of the same semantic layer.

The envelope vocabulary

Table 1 — Envelope types in contextgraph/1.0-draft. verify/verified are additive and capability-gated.
typeDirectionPayload
handshakehost → providerprotocol_version
handshake_ackprovider → hostprotocol_version, provider, capabilities
queryhost → providerquery (ContextQuery), optional id
framesprovider → hostresult (ContextQueryResult), optional id
verifyhost → providerrequest (VerifyRequest)
verifiedprovider → hostresponse (VerifyResponse)
shutdownhost → provider
errorprovider → hostmessage, optional id, code

Handshake and lifecycle

The host opens with handshake; the provider replies with handshake_ack carrying its protocol version, identity, and capabilities. No query payload moves before this exchange completes. The acknowledged version must share the host’s major family (H1); provider name and version must be non-empty (H2); a version-family mismatch must surface as a named error, never a hang (H3).

A complete stdio session, in wire orderndjson
{"type":"handshake","protocol_version":"contextgraph/1.0-draft"}
{"type":"handshake_ack","protocol_version":"contextgraph/1.0-draft","provider":{…},"capabilities":{…}}
{"type":"query","id":"q1","query":{…}}
{"type":"frames","id":"q1","result":{"frames":[…],"truncated":false}}
{"type":"verify","request":{"frames":[{"provider_id":"docs","frame_id":"doc:1","content_digest":"sha256:…"}]}}
{"type":"verified","response":{"verdicts":[{"frame":{…},"status":"valid"}]}}
{"type":"shutdown"}

Versioning

Version grammar (ABNF)
version-string = "contextgraph/" major "." minor [ "-draft" ]
major          = 1*DIGIT
minor          = 1*DIGIT

The major family is the substring up to the first dot. Two versions interoperate if and only if they share a major family: contextgraph/1.0-draft and contextgraph/1.0 both belong to contextgraph/1 and interoperate; contextgraph/2.0 does not. This is what lets the spec drop -draft at freeze without a flag day. A new optional field is a minor change; a removed or renamed field requires a new major family.

Correlation

query, frames, and error may carry an opaque id. A provider that declares capabilities.correlation must echo a request’s id verbatim on the corresponding reply (H4) — and a host must not send an id to a provider that never declared the capability. An envelope with no id is a notification: it expects no reply, the shape a future push extension needs.

A realistic queryjson
{
  "type": "query",
  "id": "q1",
  "query": {
    "goal": "why does the retry loop give up",
    "query_text": "retry loop",
    "kinds": ["snippet"],
    "anchors": ["file:///repo/src/net.rs"],
    "max_frames": 8,
    "max_tokens": 2000,
    "as_of": "2026-07-01T00:00:00Z"
  }
}

Errors

Six codes form an open vocabulary: bad_request, unsupported_kind, budget_unsatisfiable, unavailable, shutting_down, internal. A host treats an unknown or absent code as internal. Robustness is part of the contract: a provider must ignore-or-error a malformed line, never crash (R1), and must tear down cleanly on shutdown (R2).