Skip to content

Recipe: debugging pipeline steps ​

Symptom inventory ​

When render-view returns but a widget shows an error or wrong data, the cause is almost always one of:

  1. The step didn't run (registry miss / missing required keys).
  2. The step ran but threw — its error landed in context.errors.
  3. The step ran but produced the wrong shape — widget reads from context.stepData[stepId].keys and finds nothing useful.

The framework's context.errors array surfaces (1) and (2) with a human-readable reason. Read it first.

Inspect the raw render-view result ​

sh
curl -sX POST http://localhost:3010/mcp \
  -H 'content-type: application/json' \
  -d @- <<'JSON' | jq '.result.structuredContent'
{
  "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": { "name": "render-view",
    "arguments": {
      "keys": { "articles:articleId": "1" },
      "steps": [{ "id": "article", "step": "articles:resolve-article" }],
      "layout": { "rows": [{ "row": [{ "widget": "articles:article-card", "span": 6 }] }] }
    }
  }
}
JSON

You'll see context.errors and per-step context.stepData. If errors is non-empty, you're past the manifest layer — start there.

Common error reasons + fixes ​

reasonCauseFix
Step "X" not registeredStep ID typo, or the module isn't in MCP_ACTIVE_MODULES.Check the registry: call get-framework-manifest.
Missing required keys: foo, barCaller didn't pass the keys; or a previous step didn't produce them.Inspect context.keys after the previous step ran (or pass them in keys).
<message from step>Step itself threw.Add a console.error inside the step or read your server logs.

Step didn't get a typed callTool ​

Symptoms: the step throws callTool is not a function or a TypeScript error at the call site. The plugin didn't inject the closure: callTool only exists if the plugin puts it into its own AppPlugin.appConfig (see the articles module's createArticlesCallTool). Check the plugin factory and that the step's app prefix matches definition.name.

Step ran but the widget renders empty ​

Common reasons:

  • The step returned keys: { item: result } but the widget reads context.keys.itemPayload. Reconcile the key name.
  • The step's _dataType doesn't match what a downstream check expects. dataType is a label — pick deliberate values ("articles:article", not "any").
  • The widget uses useToolQuery to fetch its own data and that call failed silently. Inspect the iframe's React DevTools or add a console.error inside the widget.

userId not propagating ​

render-view resolves the caller with resolvePipelineContext(ctx) and threads { userId, authenticated } through executePipeline → bindAppConfig → callTool. If your user-scoped callTool closure sees userId: undefined:

  • authenticated is absent too: the MCP request is unauthenticated. oauth? was omitted from createFrameworkApp, or the user hasn't logged in.
  • authenticated: true: the provider maps no id. The caller id is ctx.auth.user.id, then user.userId, then the verified token's sub — check what your provider's mapAuthInfo returns as user. Refuse per-user data for such a caller; never fall back to global scope.
  • The middleware order is wrong (oauth provider must run before org-gate / role-filter / framework tools). createFrameworkApp does this for you; if you boot the server manually, replicate the order shown in packages/core/src/tools/create-framework-app.ts.

Add transient logging to the executor ​

The executor is small enough to read straight through — packages/core/src/engine/pipeline-executor.ts. For deeper diagnostics, drop a console.log inside bindAppConfig to see what appConfig and ctx each step actually sees.

See also ​

Released under the MIT License.