{"record":{"id":"ef7cf42de9101b98","repo":"paperclipai/paperclip","slug":"tool-is-required-and-must-be-a-string","errorCode":null,"errorMessage":"\"tool\" is required and must be a string","messagePattern":"\"tool\" is required and must be a string","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"server/src/routes/plugins.ts","lineNumber":1030,"sourceCode":"  router.post(\"/plugins/tools/execute\", async (req, res) => {\n    assertBoardOrAgent(req);\n\n    if (!toolDeps) {\n      res.status(501).json({ error: \"Plugin tool dispatch is not enabled\" });\n      return;\n    }\n\n    const body = (req.body as PluginToolExecuteRequest | undefined);\n    if (!body) {\n      res.status(400).json({ error: \"Request body is required\" });\n      return;\n    }\n\n    const { tool, parameters, runContext } = body;\n\n    // Validate required fields\n    if (!tool || typeof tool !== \"string\") {\n      res.status(400).json({ error: '\"tool\" is required and must be a string' });\n      return;\n    }\n\n    if (!runContext || typeof runContext !== \"object\") {\n      res.status(400).json({ error: '\"runContext\" is required and must be an object' });\n      return;\n    }\n\n    if (!runContext.agentId || !runContext.runId || !runContext.companyId || !runContext.projectId) {\n      res.status(400).json({\n        error: '\"runContext\" must include agentId, runId, companyId, and projectId',\n      });\n      return;\n    }\n\n    assertCompanyAccess(req, runContext.companyId);\n    const scopeError = await validateToolRunContextScope(runContext);\n    if (scopeError) {","sourceCodeStart":1012,"sourceCodeEnd":1048,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/plugins.ts#L1012-L1048","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait api.executePluginTool({ parameters: { limit: 10 }, runContext });\n\n// after\nawait api.executePluginTool({\n  tool: `${plugin.pluginKey}:${toolName}`,\n  parameters: { limit: 10 },\n  runContext,\n});","handlingStrategy":"validation","validationCode":"if (!tool || typeof tool !== \"string\") {\n  throw new Error(\"tool must be a non-empty namespaced string like 'pluginKey:toolName'\");\n}\nawait api.executePluginTool({ tool, parameters, runContext });","typeGuard":"const isNamespacedToolName = (t: unknown): t is `${string}:${string}` =>\n  typeof t === \"string\" && /^[^:\\s]+:[^:\\s]+$/.test(t);","tryCatchPattern":null,"preventionTips":["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"],"tags":["plugins","plugin-tools","validation","http-400","rest-api"],"backgroundTag":"request-validation-failed","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}