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
- Set `tool` to the tool's namespaced name string, e.g. 'acme.linear:list_issues', exactly as returned by GET /api/plugins/tools
- Populate execute calls from the descriptors fetched from the tools-listing endpoint rather than hand-assembling names
- 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
- Source tool names from GET /api/plugins/tools descriptors instead of assembling them by hand
- Use the '<pluginKey>:<toolName>' string form, never numeric ids or descriptor objects
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
- "runContext" is required and must be an object
- "runContext" must include agentId, runId, companyId, and pro
- Invalid status '${String(rawStatus)}'. Must be one of: ${PLU
- Request body is required
- Invalid file path
AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18).
Data as JSON: /api/errors/ef7cf42de9101b98.
Report an issue: GitHub.