paperclipai/paperclip · warning

"tool" is required and must be a string

Error message

"tool" is required and must be a string

What it means

Returned as HTTP 400 by POST /api/plugins/tools/execute (server/src/routes/plugins.ts:1015) when the body's `tool` field is missing, empty, or not a string. Tool names are strings in the dispatcher's registry, conventionally namespaced as '<pluginKey>:<toolName>' (the tools listing endpoint filters with exactly that prefix scheme).

Source

Thrown at server/src/routes/plugins.ts:1030

  router.post("/plugins/tools/execute", async (req, res) => {
    assertBoardOrAgent(req);

    if (!toolDeps) {
      res.status(501).json({ error: "Plugin tool dispatch is not enabled" });
      return;
    }

    const body = (req.body as PluginToolExecuteRequest | undefined);
    if (!body) {
      res.status(400).json({ error: "Request body is required" });
      return;
    }

    const { tool, parameters, runContext } = body;

    // Validate required fields
    if (!tool || typeof tool !== "string") {
      res.status(400).json({ error: '"tool" is required and must be a string' });
      return;
    }

    if (!runContext || typeof runContext !== "object") {
      res.status(400).json({ error: '"runContext" is required and must be an object' });
      return;
    }

    if (!runContext.agentId || !runContext.runId || !runContext.companyId || !runContext.projectId) {
      res.status(400).json({
        error: '"runContext" must include agentId, runId, companyId, and projectId',
      });
      return;
    }

    assertCompanyAccess(req, runContext.companyId);
    const scopeError = await validateToolRunContextScope(runContext);
    if (scopeError) {

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Set `tool` to the tool's namespaced name string, e.g. 'acme.linear:list_issues', exactly as returned by GET /api/plugins/tools
  2. Populate execute calls from the descriptors fetched from the tools-listing endpoint rather than hand-assembling names
  3. Add a client-side check that tool is a non-empty string before sending the request

Example fix

// before
await api.executePluginTool({ parameters: { limit: 10 }, runContext });

// after
await api.executePluginTool({
  tool: `${plugin.pluginKey}:${toolName}`,
  parameters: { limit: 10 },
  runContext,
});
Defensive patterns

Strategy: validation

Validate before calling

if (!tool || typeof tool !== "string") {
  throw new Error("tool must be a non-empty namespaced string like 'pluginKey:toolName'");
}
await api.executePluginTool({ tool, parameters, runContext });

Type guard

const isNamespacedToolName = (t: unknown): t is `${string}:${string}` =>
  typeof t === "string" && /^[^:\s]+:[^:\s]+$/.test(t);

Prevention

When it happens

Trigger: POSTing {"parameters": ..., "runContext": ...} with no `tool`; passing a numeric id or an object {name, pluginId} instead of the namespaced string; sending an empty string after trimming user input. Each fails the !tool || typeof tool !== 'string' check with this 400.

Common situations: Clients building the tool field from a dropdown value that is sometimes undefined; assuming an opaque tool id (UUID/number) instead of the namespaced name; schema drift between a tool-descriptor list and the execute payload.

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18). Data as JSON: /api/errors/ef7cf42de9101b98. Report an issue: GitHub.