Skip to content

fix: explain missing env vars at first use instead of dying silently - #110

Merged
tylerdavis merged 1 commit into
mainfrom
fix/mcp-missing-key-context
Aug 5, 2026
Merged

fix: explain missing env vars at first use instead of dying silently#110
tylerdavis merged 1 commit into
mainfrom
fix/mcp-missing-key-context

Conversation

@tylerdavis

Copy link
Copy Markdown
Owner

Problem

Adding graph as an MCP server to a fresh project with api_key = "${ANTHROPIC_API_KEY}" and the variable unset made graph mcp serve exit before the MCP handshake: config loading hard-failed on any unset ${VAR}. The client reported only a dead connection; the error went to stderr (client logs), which the agent never reads. No context, anywhere an agent can see it.

Fix — two layers

1. Missing ${VAR}s in [providers.*] / [mcp.*] defer to first use. The config loads; the entry records the unset variable (missing_env, value keeps its literal ${VAR} text). A provider becomes configured but not usable — resolving a model to it errors with e.g.

provider 'anthropic' is configured but not usable: environment variable ANTHROPIC_API_KEY (providers.anthropic.api_key) is not set

An MCP server entry refuses to connect the same way rather than starting a child with an empty secret. Everything that never touches the entry — plan authoring, listing, output-mode plans, serving — keeps working without the key. A missing variable anywhere else ([settings], prompts, paths) still fails the load loudly, as before. The bedrock stub and openai_compat without base_url now use the same unavailable-at-resolve mechanism instead of failing the whole router at startup.

2. graph mcp serve never dies over a bad config. If the config cannot load at all, the server still answers initialize, appends the load error to the MCP instructions the model reads, and returns it from every tool call. Config is re-read per request, so fixing the file or environment heals the server in place — no restart.

Tests

  • graph-config: deferral for providers and mcp (with field paths), loud failure elsewhere
  • graph-llm: resolve on a poisoned provider names the variable and config path
  • graph-mcp: connect refusal names the variable
  • e2e over real stdio JSON-RPC: the reported scenario end-to-end (serves, key-free plan runs, graph_plan_draft errors naming the variable) and the unloadable-config case (initialize instructions + per-call error)
  • The hostile-config probe test moved its ${VAR} tripwire to [settings] so it still proves the project layer isn't read

Docs updated in the same PR: configuration reference, quickstart, MCP servers/serve pages, scripting contract, ci-checks cookbook, models-and-providers.

🤖 Generated with Claude Code

An unset ${VAR} anywhere in config failed the whole load, so 'graph mcp
serve' launched without ANTHROPIC_API_KEY exited before the MCP handshake
— the client saw a dead connection and the agent behind it got no context
at all.

Missing variables inside [providers.*] and [mcp.*] are now deferred: the
config loads, the entry records what is unset, and the first call that
needs it errors naming the variable and the config path that references
it. Providers become 'configured but not usable' (resolve-time error);
MCP servers refuse to connect rather than start with an empty secret.
Everything that never touches the entry — authoring, listing, key-free
plans — keeps working. Missing variables anywhere else still fail the
load loudly.

'graph mcp serve' no longer exits over a config that cannot load either:
it serves, appends the load error to the MCP instructions at initialize,
and returns it from every tool call. The config is re-read per request,
so fixing it heals the server in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Aug 5, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Graph 🟢 Ready View Preview Aug 5, 2026, 10:20 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@graph-graph

graph-graph Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

graph_review_9000 · Defers unset ${VAR} errors in [providers.]/[mcp.] config entries to first use instead of failing config load, and makes graph mcp serve resilient to a bad config by surfacing the error in MCP instructions/tool results rather than dying before the handshake.

At a glance

Pass Area Status
1 Stated Intent ✅ Pass
2 Pipeline/Control-Step Invariants ⚪ N/A
3 Template/Shape Engine ⚪ N/A
4 Rust/Async/Storage Footguns ✅ Pass
5 Plan Composition/Model Routing ✅ Pass
6 Test Coverage ✅ Pass
7 Repo Conventions/Docs Parity ✅ Pass
8 CLI Surface/Copy ✅ Pass

🧵 0 new · 0 open · 0 resolved this run · 0 declined

N/A · ✅ Pass

Stated Intent — The diff matches the PR body's two-layer description: deferred missing_env in graph-config, ModelRouter/manager consuming it, and mcp_server/mod.rs serving through load failures, all backed by tests.

Pipeline/Control-Step Invariants — Diff doesn't touch the plan pipeline or control-step execution.

Template/Shape Engine — Diff doesn't touch the template engine, shape inference, or shape cache.

Rust/Async/Storage Footguns — No unwrap/panic added on reachable paths; MCP manager still refuses before spawning a child and doesn't bypass McpManager::shutdown; no Store or FileStore changes.

Plan Composition/Model Routing — ModelRouter's unavailable-provider path errors with a specific reason rather than swallowing it, the startup fallback-provider check still fires for genuinely unknown providers, and role fallback-to-default logic is untouched.

Test Coverage — New deferral, resolve-time, connect-refusal, and e2e serve/instructions/tool-call behaviors are all directly exercised by new unit and integration tests.

Repo Conventions/Docs Parity — Docs across configuration, quickstart, mcp-server(s), scripting-contract, models-and-providers, and ci-checks were all updated in the same PR to reflect the new deferred-error semantics.

CLI Surface/Copy — New instructions/error text is clear and consistent with existing phrasing; stdout/stderr and exit-code contracts are unaffected since this is all MCP-server and error-message copy.

@tylerdavis
tylerdavis merged commit 59baee8 into main Aug 5, 2026
5 checks passed
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.

1 participant