Skip to content

reload-credentials-redesign - #761

Merged
jeffreyaven merged 7 commits into
mainfrom
feat/reload-credentials-redesign
Sep 9, 2026
Merged

jeffreyaven merged 7 commits into
mainfrom
feat/reload-credentials-redesign

Conversation

@jeffreyaven

Copy link
Copy Markdown
Member

Summary

Redesigns the reload_credentials MCP tool around a demonstrated bug, moves MCP tool descriptions out of Go literals into pkg/mcp_server/content/tools/*.md, and aligns the server instructions and tool descriptions so agents spend zero turns on auth in the happy path and at most two calls (reload + retry) in any failure path.

The bug

Found on a fresh stdio session (0.11.669, Windows, aws provider installed, valid creds in the process env):

  1. reload_credentials unscoped, before any query: no results
  2. reload_credentials provider aws, before any aws query: cannot find AUTH context for provider = 'aws'
  3. run_select_query against aws.s3.buckets: 75 rows
  4. reload_credentials provider aws, after the query: aws / aws_signing_v4 / env:AWS_SECRET_ACCESS_KEY / ok

Root cause: the report read the auth context registry, which is populated lazily on a provider's first query. The env re-source ran before the failing lookup, so the shipped behaviour was a silently working reload with a misleading failure report.

reload_credentials design

The handler is now three ordered phases, and the report is a pure function of post-reload state.

  1. Re-source the configured env file into the process env (keys absent from the file are left untouched). A configured file that has gone missing is an explicit error naming the path.
  2. Invalidate auth contexts registered lazily from provider documents (all, or the scoped provider's). Contexts supplied at startup via --auth are user configuration, not a cache, and are retained.
  3. Report every installed provider (the list_providers set) against the now-current env; the provider arg is a filter and an unknown name is the only scoped error.

Each row gains a changed field: a sha256 comparison of the resolved credential material (successor chain included) before and after the re-source. Values are compared, never emitted. changed: false after a failure is the agent's stop signal.

Invariants: an installed provider always produces a row; the report is identical before and after a provider's first query; a second reload against an unchanged file is idempotent (changed: false throughout); no secret values in any output or error text.

Finding that changed the design

The work order assumed "lazy registration rebuilds on next query". It does not: doc-default auth registers only when the provider object is first created (GetProvider !ok branch). Dropping a context alone leaves a cached provider with no auth and every later query fails. InvalidateAuthContexts therefore drops the cached provider object together with its doc-registered context, tracked via a new docAuthProviders set on the handler context, and never touches --auth contexts. Lock order matches GetProvider (providers mutex outer, auth mutex inner).

Query tool errors

Credential resolution failures on run_select_query, run_mutation_query and run_lifecycle_operation now read:

credential resolution failed for provider 'aws' (credentials error: credentialsenvvar references empty string) - fix the configured env file, call reload_credentials, then retry

The provider is parsed from the statement (queryProviderName, first provider-qualified table or EXEC method). execQuery now propagates statement errors: mutation, lifecycle and pull_provider previously collapsed any error into {messages: [], timestamp}, which read as success.

Tool descriptions mastered as markdown

Tool descriptions were inline Go strings across server.go and query_library.go while instructions, prompts and resources already lived under pkg/mcp_server/content. They now live at content/tools/<tool_name>.md (frontmatter name must match the file stem; body is the prose; soft line breaks collapse to spaces, blank lines keep paragraph breaks) and load through the same embedded content framework behind a private toolDescriptions carrier. addToolWithGate attaches the description and returns an error for a registered tool with no file, so registration failures surface at server construction like prompt and resource failures. Published bytes were verified identical to the removed literals for all 16 tools.

The descriptions were then reworded for turn economy: server_info and reload_credentials are on-demand, never session-start or pre-query steps; list_methods is for composing from scratch, not for executing a vetted library template; validate_select_query is a pre-flight for expensive or failing queries, not routine. The server instructions were updated to say the same thing (defaults sqlite3 and safe, credentials resolve at query time, fail -> fix file -> reload -> retry, 401 retry gated on changed: true, query library before drill-down, EXEC statement shape).

Design notes to debate once

  • Status vocabulary kept as ok / unresolved / not_checked. The work order's table said not_resolved; existing docs, tests and the plugin smoke test use unresolved, and changed is the field that actually answers the rotation question.
  • execQuery error propagation is a behaviour change beyond credentials: any failing mutation, lifecycle op or provider pull now returns an error instead of an empty success payload. Required for the error text, and honest.
  • Canonical prompts untouched. cloud_audit, cost_cleanup, drift_report, iam_access_review, public_exposure_scan, getting_started and create_deploy_stack still call reload_credentials at session setup as a multi-cloud status probe. That is the work order's scenario 4 (idempotent status check when the user asks which providers resolve), not the preemptive anti-pattern, so left as is.
  • validate_select_query requires credentials. A draft description claimed otherwise; checked against the binary, EXPLAIN resolves credentials and reports the credential error before a bad column. The description says so now.
  • Data carriers follow the private interface + constructor pattern (toolDescriptions / newToolDescriptions). No new package-level globals.

Test plan

Robot (test/robot/functional/mcp.robot), all driven through a new scripted stdio harness run_stdio_credential_script in mcp_stdio_client.py against the existing stackql_auth_testing dummy-credential provider (custom auth via DD_API_KEY / DD_APPLICATION_KEY; the static_auth mock rejects any other key value):

  • MCP Stdio Reload Credentials Reports Installed Providers On Fresh Session - unscoped and scoped before any query, unknown provider is the only scoped error, no secret values.
  • MCP Stdio Reload Credentials Rotates Key After Auth Context Registered - stale key rejected by the mock, rotate the file, one reload reports changed: true, next query uses the new key, no-op reload shows changed: false, deleted file is an explicit error naming the path.
  • MCP Stdio Reload Credentials Rotates Key Before First Query - full report pre-query, first query uses the rotated key.
  • MCP Stdio Reload Credentials Recovers From Failed Resolution - select and mutation error text name the provider and the recovery, unchanged reload shows changed: false, fixed reload changed: true, retry succeeds.
  • MCP Stdio Reload Credentials Sources Env File Mid Session - existing scenario, assertions tightened to the new error text and row shape.
  • MCP HTTP Pull Provider Installs Known Provider - was passing vacuously (local-mode REGISTRY PULL error swallowed); now runs against a new full_access server on 9927 backed by the mocked HTTP registry with a throwaway approot and asserts successfully installed.
  • MCP HTTP Current Revision Client Runs Without Handshake Or Session - the stateless roundtrip now opens with server/discover and asserts the 2026-07-28 revision and the embedded instructions are served.

Removed: MCP Stdio Reload Credentials Without Auth Contexts (asserted the empty report that was the bug) and its helper.

Go unit tests: credentialFingerprint, queryProviderName, error text; loadToolDescriptionsFrom validation and prose collapsing; two-way coverage between published tools and content/tools files; a disk-read check that the markdown prose is what tools/list publishes.

Local run: full Mcp robot suite 64/69 with the 4 known version-stamp environmental failures on an unstamped binary, plus the retargeted pull scenario passing separately. golangci-lint clean on touched packages.

Docs

docs/mcp.md and pkg/mcp_server/README.md: reload semantics, changed field, per-process reload note for replicated stateless HTTP deployments (rotation there belongs at the deployment layer), and the content/tools directory in the embedded content section.

Out of scope

  • Multi-replica rotation (documented only).
  • Loading creds from an arbitrary file without touching the process env (different tool).
  • query_library_search reporting empty required_params for aws/s3/buckets-list (library index metadata, likely the query library repo).
  • The MCP Implementation version is hardcoded v0.1.1 at server.go; wiring the real build version through is a separate one-liner.

jeffreyaven and others added 6 commits September 8, 2026 17:18
…ontexts, changed flag, actionable credential errors

The reload_credentials MCP tool read the lazily populated auth context
registry, so an unscoped call before any query returned no rows and a
scoped call for an installed, credentialed provider errored with
"cannot find AUTH context" (after the env re-source had already run).

The handler is now three ordered phases: re-source the configured env
file (a configured file that has gone missing is an explicit error
naming the path), invalidate auth contexts registered lazily from
provider documents (--auth contexts are retained; the cached provider
object is dropped with its context because doc auth only registers on
provider creation), then report every installed provider against the
now-current environment. Scoping is a filter; an unknown provider name
is the only scoped error. Each row carries a new `changed` field: a
sha256 comparison of the resolved credential material (successor chain
included) before and after the re-source, never emitted.

Query tools: credential resolution failures on run_select_query,
run_mutation_query and run_lifecycle_operation now name the provider
(parsed from the statement) and the recovery sequence (fix the env
file, call reload_credentials, retry). execQuery propagates statement
errors, which mutation, lifecycle and pull_provider previously
swallowed into an empty success payload.

Tool description rewritten to forbid preemptive calls and to make
`changed: false` after a failure the stop signal.

Tests: scripted stdio harness (run_stdio_credential_script) driving the
stackql_auth_testing dummy-credential provider; robot scenarios for
fresh-session scoped/unscoped/unknown, rotation after a registered
context (stale key rejected by the mock, new key used post-reload),
rotation before first query, recovery from failed resolution (select
and mutation error text, no-op reload), missing env file. The
pull_provider scenario now runs against a mocked HTTP registry server
(9927) so the install is real rather than a swallowed local-mode error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Tool descriptions were inline Go string literals spread across server.go
and query_library.go while instructions, prompts and resources were
already authored as markdown under pkg/mcp_server/content. Move the 16
descriptions to content/tools/<tool_name>.md (frontmatter `name`, body
is the description prose; soft line breaks collapse to spaces, blank
lines keep paragraph breaks) and load them through the same embedded
content framework.

addToolWithGate attaches the mastered description and returns an error
when a registered tool has none, so registration failures surface at
server construction like prompt and resource failures do. Published
bytes are unchanged for every tool. The build-time gate test now checks
coverage in both directions: every published tool has a file and every
file names a published tool.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lishes

Reads content/tools/run_select_query.md from disk independently of the
loader, strips the frontmatter and joins the wrapped lines, then compares
with the description returned by an in-process tools/list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…TP server

The 2026-07-28 stateless roundtrip now opens with server/discover, the
SEP-2575 replacement for the initialize handshake, and the 9926
scenario asserts the response lists the revision and carries the
embedded content/instructions text. Closes the black-box gap where the
new-revision instructions path was only covered by the in-process unit
test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
server_info and reload_credentials are on-demand recovery and
diagnostic tools, never session-start steps; the instructions said
otherwise. Session guidance now states the defaults (sqlite3, safe),
that credentials resolve at query time with no preceding step, and the
fail -> fix file -> reload -> retry sequence with changed: false as the
stop signal. The 401 recovery rule gates the retry on changed: true.
Discovery points at the query library before the drill-down, dialect
rules describe validate_select_query as an on-demand pre-flight rather
than a routine step, and the mutation shapes gain the EXEC statement
form behind run_lifecycle_operation.

validate_select_query: the "no credentials required" claim was checked
against the binary and is false (EXPLAIN resolves credentials and fails
before reporting a bad column when they are missing); the description
now says so. Trailing newlines restored on the tool files.

The stateless discover scenario anchors on the "# Discovery workflow"
heading rather than a sentence that the rewrite reworded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jeffreyaven jeffreyaven self-assigned this Sep 8, 2026
Comment thread test/python/stackql_test_tooling/mcp_stdio_client.py Fixed
… scripted driver

The Sources Env File Mid Session scenario now runs through
run_stdio_credential_script like the other reload scenarios, and the
superseded run_stdio_credential_reload_roundtrip helper is removed. This
also clears the CodeQL clear-text-storage alert (pre-existing on main)
that the helper's secret_value write carried.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jeffreyaven
jeffreyaven merged commit d8762e3 into main Sep 9, 2026
26 checks passed
@jeffreyaven
jeffreyaven deleted the feat/reload-credentials-redesign branch September 9, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants