nanocoai/nanoclaw · warning

Stripping cwd from stored MCP server without plugin provenan

Error message

Stripping cwd from stored MCP server without plugin provenance

What it means

A stored MCP server entry in the container_configs DB has a `cwd` set but no `pluginRoot` provenance. Since cwd is meant to resolve relative to a plugin root, without provenance nothing can resolve it, so the host strips it and warns. The runtime would pass the provenance-less server through untouched, making this sanitize layer the only protection.

Source

Thrown at src/container-config.ts:301

    }
    try {
      validateMcpServerName(name);
      const server = parseMcpServerConfig(entry as Record<string, unknown>);
      const pluginRoot = (entry as Record<string, unknown>).pluginRoot;
      if (
        server.type !== 'http' &&
        typeof pluginRoot === 'string' &&
        pluginRoot.startsWith(`${CONTAINER_PLUGINS_DIR}/`)
      ) {
        server.pluginRoot = pluginRoot;
      }
      if (server.type !== 'http' && server.cwd && !server.pluginRoot) {
        // cwd resolves against a plugin root; without provenance nothing can
        // resolve it. This strip is the ONLY layer (the runtime passes
        // provenance-less servers through untouched), and the breadcrumb
        // lands in host logs instead of nowhere.
        delete server.cwd;
        log.warn('Stripping cwd from stored MCP server without plugin provenance', { group: groupName, server: name });
      }
      servers[name] = server;
      // eslint-disable-next-line no-catch-all/no-catch-all -- validation failures are data errors, not bugs
    } catch (err) {
      log.warn('Dropping invalid stored MCP server', {
        group: groupName,
        server: name,
        reason: err instanceof Error ? err.message : String(err),
      });
    }
  }
  return servers;
}

/**
 * runtime_tier is an isolation control: dropping an unknown stored value would
 * silently compose the group at the default tier — a weaker boundary than the
 * one the value asked for. Fail closed instead: the group refuses to compose

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Remove `cwd` from the stored server config, or run the server command with an absolute path
  2. If the server came from a plugin, reinstall it via the plugin flow so `pluginRoot` is recorded
  3. Update the stored row via `ncl groups config update` / `add-mcp-server` with absolute paths

Example fix

// before
{"command":"node","args":["server.js"],"cwd":"./plugin"}
// after
{"command":"node","args":["/abs/path/to/plugin/server.js"]}
Defensive patterns

Strategy: validation

Validate before calling

const server = JSON.parse(row.mcp_servers)[name];
if (server.type !== 'http' && server.cwd && !server.pluginRoot) {
  delete server.cwd; // normalize before storing/using
}

Type guard

function hasResolvedCwd(s: McpServerConfig): boolean {
  return s.type === 'http' || !s.cwd || typeof s.pluginRoot === 'string';
}

Prevention

When it happens

Trigger: An MCP server config written directly to the container_configs table (e.g. via `ncl groups config add-mcp-server` or a manual DB edit / older migration) that includes `cwd` but omits `pluginRoot`. Only fires for non-`http` server types.

Common situations: Hand-crafted MCP server JSON from docs or an older schema version that predates plugin provenance; copying an MCP server config from a plugin-installed setup into a plain stored config.

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/9c2f82dfaec06b9a. Report an issue: GitHub.