Two fixes and two new tools, bundled because they form one coherent unit: the
fixes harden the result path that the new tools depend on, and together they
close the discover -> pull -> query loop for agents (today an agent can only
use providers a human has already pulled from a shell).
Fix 1 - empty result sets reported as extraction failures
extractQueryResults (internal/stackql/mcpbackend/mcp_service_stackql.go)
returns (rv, ok && len(rv) > 0), and the caller maps a false second return to
fmt.Errorf("failed to extract query results"). A zero-row result is a valid
empty list. Any list_* / run_select_query returning no rows errors instead
of rendering empty. Repro: fresh approot, no providers pulled, list_providers
-> "failed to extract query results".
Fix: return (rv, ok); let the renderer's **no results** branch handle empty.
Fix 2 - literal/expression columns render as Go nullable wrappers
RenderTable/dataRow and RenderKV (pkg/mcp_server/render/render.go)
format with fmt.Sprintf("%v", v) assuming scalars. Literal columns carry a
pointer-to-nullable-wrapper, so cells render as &{ok true}. Repro:
run_select_query SELECT 1 as n, 'ok' as status -> | &{ok true} | &{1 true} |.
Provider-backed columns are unaffected (unwrapped upstream).
Fix: unwrap helper (check valid, take value) applied before formatting in
dataRow and the RenderKV loop.
Feature 1 - list_registry tool
Lets an agent see providers available in the registry to pull, distinct from
list_providers (which shows only pulled/installed providers).
- Renderer: Table. Inputs: optional
provider (lists versions for that
provider; omitted lists all available).
- New interrogator method
GetRegistryList(provider string) building
REGISTRY LIST [<provider>]; backend ListRegistry routes through
runPreprocessedQueryJSON.
- Registered with
selectGate("list_registry") - read-only, allowed in all
modes.
Feature 2 - pull_providers tool
Lets an agent install a provider into the approot cache so subsequent queries
resolve.
- Renderer: KV ({messages, timestamp}). Inputs:
provider (required),
version (optional).
- New interrogator method
GetRegistryPull(provider, version string) building
REGISTRY PULL <provider> [<version>]; backend PullProvider routes through
execQuery.
- Gating decision (flagged for review): pulling fetches remote content and
writes local state but does not touch any cloud control/data plane. Proposed
default: allow in all modes (it is not a cloud mutation). If a human-in-the-
loop is wanted, classify it as a new registry_pull query class gated like
lifecycle (refused in read_only, elicitation-prompted in safe). Defaulting to
allow keeps autonomous agent flows unblocked.
Why CI didn't catch the fixes
No test asserts on an empty result set, and query/metadata tests assert on
structuredContent JSON + row counts, never on rendered cell values; every test
SELECT hits a real provider table, so no test runs the literal/no-FROM shape
that exposes the wrapper.
Tests added (test/robot/functional/mcp.robot + render_test.go)
- Empty-result
list_providers/SELECT renders cleanly, no error.
- Literal SELECT cell equals
ok, does not contain &{.
- Unit: nullable wrapper value renders as unwrapped scalar.
list_registry returns a non-empty available-provider set (against the test
registry).
pull_providers for a known provider returns a timestamp; the provider is
then visible via list_providers.
Impact / risk
Low. Fixes are additive on the read path; provider-backed output unchanged. New
tools are additive. Only open decision is the pull_providers gating class.
Two fixes and two new tools, bundled because they form one coherent unit: the
fixes harden the result path that the new tools depend on, and together they
close the discover -> pull -> query loop for agents (today an agent can only
use providers a human has already pulled from a shell).
Fix 1 - empty result sets reported as extraction failures
extractQueryResults(internal/stackql/mcpbackend/mcp_service_stackql.go)returns
(rv, ok && len(rv) > 0), and the caller maps a false second return tofmt.Errorf("failed to extract query results"). A zero-row result is a validempty list. Any
list_*/run_select_queryreturning no rows errors insteadof rendering empty. Repro: fresh approot, no providers pulled,
list_providers-> "failed to extract query results".
Fix: return
(rv, ok); let the renderer's**no results**branch handle empty.Fix 2 - literal/expression columns render as Go nullable wrappers
RenderTable/dataRowandRenderKV(pkg/mcp_server/render/render.go)format with
fmt.Sprintf("%v", v)assuming scalars. Literal columns carry apointer-to-nullable-wrapper, so cells render as
&{ok true}. Repro:run_select_querySELECT 1 as n, 'ok' as status->| &{ok true} | &{1 true} |.Provider-backed columns are unaffected (unwrapped upstream).
Fix: unwrap helper (check valid, take value) applied before formatting in
dataRowand theRenderKVloop.Feature 1 - list_registry tool
Lets an agent see providers available in the registry to pull, distinct from
list_providers (which shows only pulled/installed providers).
provider(lists versions for thatprovider; omitted lists all available).
GetRegistryList(provider string)buildingREGISTRY LIST [<provider>]; backendListRegistryroutes throughrunPreprocessedQueryJSON.selectGate("list_registry")- read-only, allowed in allmodes.
Feature 2 - pull_providers tool
Lets an agent install a provider into the approot cache so subsequent queries
resolve.
provider(required),version(optional).GetRegistryPull(provider, version string)buildingREGISTRY PULL <provider> [<version>]; backendPullProviderroutes throughexecQuery.writes local state but does not touch any cloud control/data plane. Proposed
default: allow in all modes (it is not a cloud mutation). If a human-in-the-
loop is wanted, classify it as a new
registry_pullquery class gated likelifecycle (refused in read_only, elicitation-prompted in safe). Defaulting to
allow keeps autonomous agent flows unblocked.
Why CI didn't catch the fixes
No test asserts on an empty result set, and query/metadata tests assert on
structuredContent JSON + row counts, never on rendered cell values; every test
SELECT hits a real provider table, so no test runs the literal/no-FROM shape
that exposes the wrapper.
Tests added (test/robot/functional/mcp.robot + render_test.go)
list_providers/SELECT renders cleanly, no error.ok, does not contain&{.list_registryreturns a non-empty available-provider set (against the testregistry).
pull_providersfor a known provider returns a timestamp; the provider isthen visible via
list_providers.Impact / risk
Low. Fixes are additive on the read path; provider-backed output unchanged. New
tools are additive. Only open decision is the pull_providers gating class.