Skip to content

mcpx: where the model comes from — a provider layer mcpx owns, model profiles, and life after MCP Sampling's deprecation #1925

Open

Description

@dezren39

Relocated from dezren39/darwin-nix#110 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #176

Summary

mcpx has no model and deliberately never had one: docs/consumer.md records that "a configured provider with an API key, was not built and should not be. It is the one that makes mcpx an agent." Everything model-shaped in mcpx today borrows the caller's model through MCP sampling.

Two facts have changed since that was written, and together they invert the conclusion:

  1. **MCP deprecated Sampling in 2026-07-28.** The spec changelog's Deprecated section, item 1 (SEP-2577) deprecates Roots, Sampling and Logging, and names the migration explicitly: "integrate directly with LLM provider APIs instead of Sampling." Sampling is not the long-term channel; the spec says so.
  2. Nothing in the opencode line will ever send mcpx a sampling request. docs/opencode-plugin.md §2.6: neither v1 nor v2 declares the sampling capability. v1 comments it out with issue links; v2 declares roots + elicitation only. The primary consumer of mcpx cannot answer the channel mcpx's only model path depends on.

So the question this issue settles is not "should mcpx sample?" — it is "where does mcpx's AI layer get a model, what does it expose, and how does it stay from becoming a second agent harness?"

Original notes

  • mcp-owned opencode api support
    • support multiple providers/models
    • name priority sets of models as an 'agent'
    • ? just embed opencode and call it? is there a golang or rust opencode?
    • proxy
    • respond to elicit/prompt

Current state (what mcpx does today, with file refs)

capability where state
sampling passthrough (upstream server → mcpx → caller) internal/elicit, docs/protocol.md §4.1 works; a decline becomes an error, since the spec has no decline result for sampling
sampling declared upstream docs/protocol.md:271; internal/mcpclient/modern.go:46-57 sampling declared to servers only when a handler is installed — and the daemon always installs one (internal/daemon/hooks.go:69 → internal/pool/pool.go:393-397), so in practice every upstream server is told sampling works, whether or not any caller can answer. roots is declared unconditionally (modern.go:52) and is deprecated by the same SEP.
sampling raised by mcpx itself /v1/intent, prompt.sample (internal/settings/consumer.go:105) exists, off by default (never)
the catalogue slice sent with a generation request prompt.catalogBudget (:119), prompt.maxTokens (:134), prompt.sampleTimeout (:127) bounded; sends the ranked slice and nothing else
generated script is validated before running internal/diagnose preflight, prompt.mode (:97) default script returns the script; runs only on explicit request
answering an elicitation as a client mcpx elicit, GET/POST /v1/elicit*, the plugin works; the broker is the store and the fallback
a model of mcpx's own — none
logging declared as a server internal/mcpserver/server.go:566 no longer declared since #171 (mcpx sends no notifications/message); logging/setLevel still answers. docs/protocol.md §2.2 still shows it declared for three revisions — stale.

Server.execScript running a recipe from the daemon is marked in docs/consumer.md as "a seam, not a design", pending POST /v1/exec — which now exists (internal/api/ops_exec.go:12). That seam should close as part of any work here; at 05c78b2 it still calls internal/runner directly (internal/daemon/consumer.go:509-517).

docs/consumer.md:192-198 still says a configured provider "was not built and should not be. It is the one that makes mcpx an agent." SEP-2577 names exactly that as the migration from Sampling, so that sentence must change in the same change that lands whatever this issue decides — otherwise it is another #159-class contradiction.

(Checked at 05c78b2. The PROTOCOL work in flight — conformance against the official schemas and a modern-first era probe — touches internal/mcpclient, internal/pool and docs/protocol.md; the capability rows above may change when it lands. Nothing in the recommendation depends on them.)

What "is there a golang or rust opencode" actually resolves to

Checked, because it decides the whole shape:

candidate language what it is usable as a library inside mcpx?
sst/opencode TypeScript/Bun the opencode mcpx already plugs into No. It is an application, and embedding it means shipping Bun.
charmbracelet/crush Go, ~28k stars literally the original Go opencode codebase, renamed after the 2025 split (maintainers' own words in charmbracelet/crush#1097: "Crush is the continuation of the original OpenCode project… sst/opencode is a fork of that repo") Practically no. It is a TUI application, not a library; its packages are internal-shaped; and the repo is source-available, not OSI (GitHub reports the licence as NOASSERTION), which is a distribution problem for a Nix-packaged binary.
Google ADK for Go Go go get google.golang.org/adk — first-party, model-agnostic, MCP + tool-calling + multi-agent Yes, technically. Heavy.
Microsoft Agent Framework for Go Go first-party, public preview since July 2026 Yes. Preview-grade; missing RAG/CodeAct/declarative agents.
cloudwego/eino Go, ~12k stars LangGraph-like graphs, typed ChatModel/Tool/Retriever, ReAct + DeepAgent, Apache-2.0 Yes. The most capable OSS Go option.
tmc/langchaingo Go, ~9k stars broadest provider/vector-store surface, pre-1.0 (v0.1.14) Yes, with API churn.
firebase/genkit (Go) Go 1.0 GA, flows + dev UI + built-in OpenTelemetry Yes.
openai/openai-go, anthropics/anthropic-sdk-go, googleapis/go-genai Go plain provider clients, no agent loop Yes, and this is the low-dependency option.
Rust — rmcp (official MCP SDK), rig; OpenAI's Codex CLI is Rust. No Rust opencode. irrelevant — mcpx is Go

Two conclusions worth stating plainly:

  • The answer to "is there a golang opencode" is yes, and it is Crush — but it is an app under a source-available licence, not a library, so "just embed opencode and call it" is not available in the form the note imagines.
  • OpenAI and Anthropic still ship no first-party Go agent SDK (Python/TS only, as of mid-2026). Their plain Go clients exist and are fine. So an mcpx AI layer built on provider clients plus a hand-written loop is not a second-class choice in Go; for a single model with a handful of tools, the loop is ~40 lines and owning it beats adopting a framework.

Approaches

Approach A — least recommended: embed or shell out to an agent application

Ship Crush (or opencode, or claude -p) as a subprocess and hand it the prompt. Superficially the cheapest thing: no provider plumbing, no loop, someone else's model config.

Why not:

  • Licence and packaging. Crush is source-available (NOASSERTION); opencode means shipping Bun. mcpx's whole distribution story is one Go binary plus a Nix derivation, and Bump rustls-webpki from 0.100.1 to 0.100.2 in /sources/pavex/examples/realworld #44 (closed, fixed in chore(deps): bump build/buck/preludes/buck2-prelude from d956689 to 880be56 #53) spent real effort making the plugin binary-free. Adding a mandatory agent binary reverses that.
  • It is the harness, twice. The agent app has its own config, its own model list, its own MCP client, its own permission model, its own sessions. mcpx would then have two of each, disagreeing. docs/consumer.md's scope concern — "anything that makes mcpx call a model has to justify that it isn't rebuilding the harness" — is violated maximally here: you have not avoided rebuilding the harness, you have adopted a whole second one.
  • No structured control. You cannot bound the token budget, pin a model per operation, or get a typed result. You get whatever the app printed.
  • It does, however, have one genuine merit worth preserving as a fallback: for a user who already runs opencode, "use the model opencode is already configured with" is exactly right — which is what the v2 plugin's ctx.generate.text gives, without the subprocess. See C.

Approach B — middle: a Go agent framework (eino / ADK-Go / Genkit) as mcpx's AI layer

Adopt one framework, get the ReAct loop, tool-calling, streaming, interrupt/resume and multi-agent orchestration for free, and map "an agent" (the note's named priority set of models) onto the framework's agent type.

Facts and opinions:

  • eino is the strongest OSS option: Apache-2.0, typed components, real graph orchestration with branching and human-in-the-loop interrupt/resume — and interrupt/resume maps exactly onto mcpx's existing elicitation broker and 2026-07-28 MRTR input-required flow, which is a genuine architectural fit rather than a coincidence. ADK-Go and Microsoft Agent Framework for Go are first-party and better-supported bets but Gemini-first / Azure-first respectively, and the Go builds trail their Python siblings. Genkit Go is 1.0 GA and brings OpenTelemetry tracing — which mcpx would otherwise hand-roll onto /v1/log and /v1/stats.
  • The real cost is dependency weight and gravity. A framework brings a provider registry, a message type, a streaming abstraction and a state model, and every one of them competes with something mcpx already has: internal/tasks, internal/elicit, internal/logstore, internal/execsvc's Frame stream. The seams between "mcpx's stream" and "the framework's stream" are where this gets expensive.
  • It also over-buys. mcpx's model jobs are all small and one-shot: expand a search query, generate a script from an intent, explain an error, propose a repair, draft an adapter spec. None of them is an agent loop. A graph framework for five one-shot completions is the wrong ratio.
  • Worth revisiting if mcpx ever wants a genuinely agentic operation (the "install any language/tool/framework/library" case in Update module github.com/jackc/pgx/v5 to v5.5.4 [SECURITY] - autoclosed #113 is the one candidate).

Approach C — recommended: a thin provider interface mcpx owns, with sampling and the plugin as two of several backends

One internal package — call it internal/model — with one small interface, roughly:

type Provider interface {
    Name() string
    Generate(ctx context.Context, req Request) (Response, error) // messages, maxTokens, stop, optional JSON schema
}

and several implementations, chosen by configuration:

backend when it is right notes
sampling through the broker (today's path) an MCP client that declared sampling is driving keep it; it is correct per the legacy spec and costs nothing. But it is deprecated in 2026-07-28 and no opencode declares it, so it cannot be the only path.
the opencode plugin answering sampling via ctx.generate.text (opencode v2) an opencode user, no API key configured docs/opencode-plugin.md §2.5 — v2 plugins get one-shot completion; v1 has no route short of a full agent turn. This makes "use the model my harness already has" real, with no subprocess and no key. Depends on #47.
a configured provider (openai-go, anthropic-sdk-go, go-genai, or any OpenAI-compatible base URL) CI, headless, a daemon on another machine, or anyone who just wants it to work the thing docs/consumer.md refused. The 2026-07-28 changelog now names this as the migration from Sampling. Reading an existing OPENAI_API_KEY/ANTHROPIC_API_KEY/OPENAI_BASE_URL from the environment means most users configure nothing.
an OpenAI-compatible proxy endpoint a company gateway, LiteLLM, Ollama, LM Studio, opencode-zen falls out of the previous row for free — it is a base URL — and is the honest reading of the note's proxy bullet, see below.

Everything goes through the settings registry, not new endpoints. internal/settings already gives every setting a config key, an MCPX_* env var, a CLI flag, /v1/settings and an MCP tool, and prompt.* is already the precedent. Proposed:

setting meaning
model.provider auto | sampling | plugin | openai | anthropic | google | none; auto prefers a live sampling-capable client, then the plugin, then a configured key, then fails cleanly
model.baseURL, model.apiKeyEnv the proxy/compatible-endpoint case
model.default the model id used when an operation names none
model.agents.<name> the "agent" concept: a named ordered list of models, tried in order
model.budget, model.timeout hard bounds, per the existing prompt.maxTokens/prompt.sampleTimeout shape
<operation>.model per-operation override: prompt.model, search.model, repair.model — each can name a model id or an agent name

"Name priority sets of models as an 'agent'" is the best idea in this note and should be built literally. It is a fallback chain plus a policy, and it solves four problems at once that are otherwise solved four times: rate-limit failover, cost tiering (try the cheap local model, escalate on refusal or low confidence), capability routing (this operation needs tool-calling/JSON mode, that one does not), and offline degradation. Define it as:

"model": {
  "agents": {
    "cheap":   { "models": ["ollama/qwen3:8b", "openai/gpt-5-mini"], "maxTokens": 2000 },
    "careful": { "models": ["anthropic/claude-opus-5", "openai/gpt-5"], "temperature": 0 }
  }
}

and let every operation take model: "careful" or model: "openai/gpt-5" interchangeably. Do not call it "agent" in the config schema — the word already means something else in every harness mcpx sits under, and mcpx is explicitly not one. model.profiles or model.sets says the same thing without the collision; keep "agent" as documentation prose if the user prefers it.

On proxy, the note's bullet has three plausible readings and they should not be conflated:

  1. mcpx points at an OpenAI-compatible proxy — free, it is model.baseURL. Do this.
  2. mcpx is an OpenAI-compatible proxy — it serves /v1/chat/completions, forwards to a real provider, and injects mcpx's tools. This is genuinely interesting: it would let any OpenAI-compatible client get mcpx's whole catalogue with no MCP support at all, reusing exactly the pattern GET /v1/openapi.json already establishes (one declaration, many surfaces). It is also the single largest scope increase in this issue and should be its own issue if wanted. Note the collision: mcpx's API prefix is already /v1, and POST /v1/chat/completions would sit inside the ops table's namespace.
  3. mcpx proxies model calls on behalf of scripts — await mcpx.model.generate(...) inside an exec script, so a script can use a model without its own key. This is small, falls out of the Provider interface for nearly nothing, and is probably wanted. Put it behind the same off-by-default posture.

On respond to elicit/prompt, keep the existing direction rules from docs/protocol.md §3 and add exactly one thing: when an upstream server sends sampling/createMessage and no caller can answer, mcpx currently returns an error. With a configured provider it can answer itself — sampling.answerLocally (default off), bounded by model.budget. That is the case where mcpx-as-a-model genuinely earns its place: the alternative is a failed call. Symmetrically, mcpx may answer an upstream elicitation/create from a model only when the question is not a consent question; internal/elicit/policy.go's existing routing rule — a lone boolean confirm goes to a human, because consent is not the agent's to give — must hold, and a model is even less entitled to give consent than an agent is.

Recommended because it is the smallest thing that removes the hard dependency on a deprecated protocol feature that mcpx's main consumer does not implement; because every existing path (sampling, the plugin) survives as a backend rather than being replaced; because it is all settings, so a user who wants none of it configures nothing and nothing changes; and because it does not make mcpx an agent — it gives mcpx one function, Generate, which is what all five of its model-shaped jobs actually need.

The scope guard, restated

docs/consumer.md's rule should be kept and made checkable: no mcpx operation may call a model on a path where a deterministic answer exists. Diagnostics before repair, recipe matching before generation, lexical search before query expansion. The provider layer is a capability, not a strategy; every caller of it should be reviewable as "what happens when this returns an error", and the answer should always be "the deterministic result, plus a sentence saying the model was unavailable."

Open questions

  • Is model.provider: auto too clever? An explicit default of none (nothing calls a model until you say so) matches prompt.sample: never and is more predictable; auto is friendlier. Probably none for the daemon default and auto documented as the recommended setting.
  • Should the provider be resolved in the daemon or the CLI? docs/consumer.md records that the daemon had no access to settings until Bump build/buck/preludes/buck2-prelude from d956689 to 6f75473 #41 folded settings.Resolve in; the provider must live daemon-side since /v1/intent and the exec service are the callers.
  • Does a per-session model override make sense (_meta["ai.opencode/sessionID"] from build(deps): bump actions/checkout from 3 to 4 #47 gives session attribution for free)?
  • What is the model id namespace? provider/model (crush/opencode convention) is the obvious one and makes model.profiles entries self-describing.
  • Should the seam (Server.execScript calling internal/runner directly) close onto POST /v1/exec as part of this, or separately? It is one function and it is marked.
  • Reading OPENAI_API_KEY from the ambient environment by default: convenient, or a surprise? Probably opt-in via model.apiKeyEnv naming the variable rather than scanning for known ones.

Ease · Impact · Related

Ease L (M for the Provider interface + one provider + settings; L including profiles, plugin backend and local sampling answers; XL if mcpx-as-an-OpenAI-proxy is in scope — it should not be, split it) · Impact high — every other issue in this theme depends on where the model comes from.

Depends on / related: #41 (closed, fixed in #58 and #61: sampling passthrough, prompt.sample, /v1/intent — this generalises them), #47 (opencode v2 plugin, ctx.generate.text backend), #40 (closed, fixed in #57: ExecOptions, the shape settings-not-endpoints follows), #77 (protocol follow-ups — Sampling's deprecation belongs there too), #5 (config chain), #159 (the docs/consumer.md provider sentence is an instance), #172 (every model.* setting goes through the one registry). Callers of this layer: #109 (query expansion), #111 (rungs 2–6), #112 (prompt hooks), #113 (operation routing), #114 (spec generation).

Security/auth deferred; see tracker.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#110 (verbatim, oldest first):


    @dezren39 · 2026-09-30T07:21:48Z

    Integration pass. Placed in the DAG rooted at #181.


    @dezren39 · 2026-09-30T11:57:42Z

    Issues pass (2026-09-30, at 05c78b2).


    @dezren39 · 2026-09-30T16:58:23Z

    Cross-reference (SURFACES, 0002-autonomy-dial, merged in #228). prompt.sample and model.provider answer whether a model is consulted, a separate question from how far its output may go. A model answering an upstream server's question on the user's behalf is apply, bounded by the daemon ceiling autonomy.max, whatever this issue names the setting. A question routed to a human is never answered by a model at any level.


    @dezren39 · 2026-10-28T22:16:58Z

    Triage verdict (task 2149, repo-health-nix-pilot): CONFIRMED + RELOCATED — mcpx still has no provider layer of its own for generation; sampling via the broker remains the only model path. Implementation routes in developing-today/code; no fix packet in this repo.

    Verified against current main on both sides (nix 224c7b738e88c7a8e5d406987a7d88257e229db2, code d2cb60fe8e08cd6fee149b930187b610299c1fa7):

    Route: no small_fix packet in dezren39/nix — this is a design decision for the repo that owns the source. It routes with the source in developing-today/code, referencing #110 and tracker #176, via the #309 relocation pass; per the cross-repo rule, do not re-file it there. #110 stays open on the nix side until the design is settled there or transferred. The decision itself (own a provider layer vs stay sampling-only) is a design call for that pass, not something a triage can close as wontfix.

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Triage verdict — scope confirmed, not a duplicate

    Dedupe (checked 2026-10-29): searches across all developing-today/code issues for mcpx sampling, mcpx provider model, and mcpx "model profile" found no other open or closed issue covering an mcpx-owned provider layer. The only hits were this issue itself and the closed historical sampling work (#41, fixed in #58/#61) that this issue explicitly generalises. Not a duplicate; nothing to close.

    Scope confirmed against current main (a16a991cd65bcca0, checked via the GitHub API 2026-10-29):

    • pkgs/mcpx/internal/model does not exist yet — the provider layer is unbuilt, as the issue states.
    • internal/settings/consumer.go:105 (prompt.sample) and :119 (prompt.catalogBudget) are present as cited — the prompt.* settings precedent the recommendation builds on is real.
    • docs/consumer.md:192-198 still carries the "a configured provider … was not built and should not be" sentence — the contradiction the issue says must be fixed in the same change that lands the provider is still live.
    • The issue's premise stands: Sampling is deprecated by SEP-2577 in the 2026-07-28 spec, and neither opencode v1 nor v2 declares the sampling capability, so mcpx's only model path depends on a channel its primary consumer cannot answer.

    Rescope (slicing, not shrinking): the full issue is L/XL as written. It is sliced as follows, with this issue staying open as the tracker for the remainder:

    • Slice 1 — enqueued (task 3215, small_fix): internal/model Provider interface (Name + Generate) · model.* settings through the one registry in internal/settings (no new endpoints) with daemon default model.provider=none · one OpenAI-compatible configured-provider backend (model.baseURL/model.apiKeyEnv, opt-in by naming the env var) alongside the existing sampling path as a backend · the existing generation caller (prompt.sample / /v1/intent) routed through the interface · the docs/consumer.md sentence updated in the same change. Verification for the follow-up: go test ./... in pkgs/mcpx via CI. The issue's open questions are settled for slice 1 by its own recommended defaults (default none, provider/model id namespace, apiKeyEnv opt-in); the scope guard (no model call where a deterministic answer exists; provider error → deterministic result + model-unavailable note) is in the packet as an acceptance criterion.
    • Remaining on this issue (later slices, not yet enqueued): model.profiles fallback chains · the opencode-plugin backend (depends on build(deps): bump actions/checkout from 3 to 4 #47) · sampling.answerLocally · script-side mcpx.model.generate. mcpx-as-an-OpenAI-proxy stays out — per the issue itself, it gets its own issue if wanted, not a slice here.

    Labels updated to mirror the queued follow-up task.

  6. 220 remaining items

  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: triaged — already routed; final self-contained slice enqueued (task 7788, lane code-c, 2026-11-03)

    Verdict: STILL LIVE and correctly scoped; no duplicate work created. Fresh state:

  8. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/none). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  10. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
  11. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged — still live; all self-contained slices done; final remainder not yet routable (task 8723)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestsource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions