Wymcp. Context
(Wymcp v0.1.1)
View Source
Tool execution context and result builders.
Every tool receives a %Context{} as its first argument. The struct
carries the session reference, request metadata, and per-session
assigns. Module functions build MCP-compliant content arrays that
tools return in their result tuples.
Assigns
On the legacy lane, assigns merges per-request conn.assigns (set by
upstream plugs like auth) under the session's accumulated state —
session assigns win on collision, so accumulated tool state is not
overwritten by plug defaults. On the modern lane there is no session:
assigns is the filtered per-request conn.assigns alone, and a
{:ok, content, assigns_updates} return has nowhere to persist (the
next request is a fresh context).
This means auth plugs can store data in conn.assigns and tools will
see it in ctx.assigns without any process dictionary workarounds:
# In your auth plug:
{:ok, Plug.Conn.assign(conn, :current_scope, scope)}
# In your tool's run_action:
def run_action(:create, data, ctx) do
scope = ctx.assigns[:current_scope]
# ...
endInternal wymcp keys (:wymcp, :wymcp_session_pid, etc.) are filtered
out and not visible in ctx.assigns.
Tools can update session-persistent assigns by returning
{:ok, content, assigns_updates} where assigns_updates is a map
that gets merged into the session's assigns for future requests.
Tools
tools carries the tool modules resolved for this request — the
session's merged list on the legacy lane, the compile-time list on the
modern lane. Wymcp.Help answers from it; tools may read it for
introspection.
Era
era records which lane served the request — :modern or :legacy,
set by Wymcp.Methods.ToolsCall from the era classification
(docs/glossary.md, era classification). A hand-built context never
reached the wire and takes the struct default, :modern. Read it as
provenance; never branch on it — tools are defined once and serve both
eras. Wymcp.Telemetry carries the contract this field feeds.
era is legacy-only: it exists only because two eras do, and it goes at
the legacy decommission.
Design decisions
Result builders (text/1, json/1, image/2, audio/2) are pure
functions — no side effects, no process messages. This makes tools
easy to test in isolation. The %Context{} struct is there for future
phases when tools need to call sample/3 or elicit/4, which
communicate with the session GenServer.
The deliberate split between "build content" (pure) and "interact with session" (effectful) keeps the common case simple: most tools just compute a result and return it.
sequenceDiagram
autonumber
participant T as Tool
participant C as Context
participant S as Session
participant ST as Transport.Stream
participant CL as Client
T->>C: sample(ctx, prompt) or elicit(ctx, message, schema)
C->>S: check_capability
S-->>C: :ok
C->>S: await_client_response(request_id, message, timeout)
S->>ST: push request (ack to session)
ST->>CL: SSE event
ST-->>S: push ack
Note over S: caller held (server-request round trip)
CL->>S: POST response (deliver_response)
S-->>C: {:ok, result}
C-->>T: {:ok, result}
Summary
Functions
Asks the human user for structured input mid-tool-execution (form mode).
Sends a log message notification to the client via the SSE stream.
Sends a progress notification to the client via the SSE stream.
Asks the client's LLM a question mid-tool-execution.
Types
Functions
Asks the human user for structured input mid-tool-execution (form mode).
Pushes an elicitation/create request to the client via the SSE stream
and blocks until the user responds. The client renders a form based on
the JSON Schema and returns typed, validated data.
The schema must be a flat JSON Schema object (primitive properties
only, no nested objects). The client renders appropriate UI controls
for each field type.
On {:ok, response} the response includes an "action" field:
"accept" (user submitted), "decline" (user refused), or "cancel"
(user dismissed). When action is "accept", "content" contains the
validated form data. The error vocabulary is Wymcp.Context.sample/3's,
in full — :not_supported (client lacks the elicitation capability),
:no_session,
:unencodable (the schema is the usual offender), :no_stream,
:disconnected, :stream_down, and the two-sense :timeout — plus
{:error, error_map} when the client answers with a JSON-RPC error.
Sends a log message notification to the client via the SSE stream.
The message is filtered against the session's configured log level
(set via logging/setLevel). Messages below the threshold are
silently dropped.
The data parameter can be any JSON-serializable term — a string
for simple messages, or a map/list for structured data.
On the modern lane there is no session and no logging capability (the
Logging feature is Deprecated in 2026-07-28, and wymcp's modern lane
implements none of it): a session-less context answers
{:error, :not_supported}. With a live session the always-:ok
contract holds — filtered-out and undeliverable messages still answer
:ok.
Sends a progress notification to the client via the SSE stream.
Only sends if the request included a progressToken in _meta.
The progress value must increase with each call. The total
and message parameters are optional.
No-ops silently when there is no progress token or no session — this lets tools call report_progress unconditionally without checking whether the client requested progress updates.
Asks the client's LLM a question mid-tool-execution.
Pushes a sampling/createMessage request to the client via the SSE
stream and blocks until the client responds. The client has full
discretion over model selection, prompt modification, and approval.
opts are merged into the params of the request. Common options:
"maxTokens"(integer, required by spec but defaults to 1024)"modelPreferences"(map with"hints", priority axes)"systemPrompt"(string)"temperature"(float)
Returns the client's {:ok, result} or {:error, error_map} (the
client answered with a JSON-RPC error), or one of these tuples — the
full vocabulary:
{:error, :not_supported}— the client did not declare thesamplingcapability.{:error, :no_session}— no session (session_pidis nil), or a dead one: sessions end at DELETE, idle timeout, or crash and are never restarted, and this surface answers values, never exits.{:error, :unencodable}—prompt/optsproduced a requestJSONcannot encode; answered synchronously in the calling process, with aLogger.warningnaming the session.{:error, :no_stream}— the session has no registered SSE stream.{:error, :disconnected}— the stream's chunk write failed; the client is gone.{:error, :stream_down}— the stream process died before acknowledging the push.{:error, :timeout}— the push-leg ack window (~5 s) expired (a wedged stream), or the client never answered within the sampling timeout; a:timeoutarriving only after the full timeout means the request was delivered.