fix: explain missing env vars at first use instead of dying silently - #110
Conversation
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>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
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
🧵 0 new · 0 open · 0 resolved this run · 0 declined ⚪ N/A · ✅ PassStated 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. |
Problem
Adding graph as an MCP server to a fresh project with
api_key = "${ANTHROPIC_API_KEY}"and the variable unset madegraph mcp serveexit 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.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 andopenai_compatwithoutbase_urlnow use the same unavailable-at-resolve mechanism instead of failing the whole router at startup.2.
graph mcp servenever dies over a bad config. If the config cannot load at all, the server still answersinitialize, appends the load error to the MCPinstructionsthe 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_plan_drafterrors naming the variable) and the unloadable-config case (initialize instructions + per-call error)${VAR}tripwire to[settings]so it still proves the project layer isn't readDocs updated in the same PR: configuration reference, quickstart, MCP servers/serve pages, scripting contract, ci-checks cookbook, models-and-providers.
🤖 Generated with Claude Code