{"record":{"id":"dfc71f53b02a6f1d","repo":"mastra-ai/mastra","slug":"plugin-tool-name-must-be-an-object-with-a-too","errorCode":null,"errorMessage":"Plugin tool \"${name}\" must be an object with a tool property","messagePattern":"Plugin tool \"(.+?)\" must be an object with a tool property","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mastracode/sdk/src/plugins/loader.ts","lineNumber":307,"sourceCode":"  if (plugin.instructions === undefined) return undefined;\n  const instructions =\n    typeof plugin.instructions === 'function' ? await plugin.instructions(context) : plugin.instructions;\n  if (typeof instructions !== 'string') {\n    throw new Error('Plugin instructions must be a string');\n  }\n  const trimmed = instructions.trim();\n  return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction normalizePluginToolEntries(entries: MastraCodePluginToolEntries): {\n  tools: MastraCodePluginTools;\n  renderConfigs: Record<string, MastraCodeToolRenderConfig>;\n} {\n  const tools: MastraCodePluginTools = {};\n  const renderConfigs: Record<string, MastraCodeToolRenderConfig> = {};\n  for (const [name, entry] of Object.entries(entries)) {\n    if (!isToolEntryObject(entry)) {\n      throw new Error(`Plugin tool \"${name}\" must be an object with a tool property`);\n    }\n    tools[name] = entry.tool;\n    if (entry.render) renderConfigs[name] = entry.render;\n  }\n  return { tools, renderConfigs };\n}\n\nfunction isToolEntryObject(entry: MastraCodePluginToolEntries[string]): entry is MastraCodePluginToolEntries[string] {\n  if (!entry || typeof entry !== 'object' || !('tool' in entry)) return false;\n  const tool = (entry as { tool?: unknown }).tool;\n  return !!tool && typeof tool === 'object' && !Array.isArray(tool);\n}\n\nfunction validatePluginConfigSchema(schema: unknown): MastraCodePluginConfigSchema | undefined {\n  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return undefined;\n  const validated: MastraCodePluginConfigSchema = {};\n  for (const [key, option] of Object.entries(schema)) {\n    if (!option || typeof option !== 'object' || Array.isArray(option)) continue;","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/plugins/loader.ts#L289-L325","documentation":"normalizePluginToolEntries iterates a plugin's exported tool map and requires every entry to be an object containing a `tool` property (checked by isToolEntryObject). A bare McpServer/Tool function or any non-object value throws 'Plugin tool \"<name>\" must be an object with a tool property'. This enforces the { tool, render? } wrapper format so render configs can be extracted uniformly.","triggerScenarios":"Exporting tools from a plugin as raw tool instances instead of wrappers, e.g. `tools: { searchTool }` or `tools: { searchTool: createTool({...}) }` instead of `tools: { searchTool: { tool: createTool({...}) } }`; or a value in the map is null/undefined/a function.","commonSituations":"Upgrading from an older plugin API where raw tool objects were accepted; copy-pasting tool definitions from core examples that do not use the plugin wrapper; a tool entry left undefined after a failed conditional assignment; a typo like `{ tool: myTool }` written as `myTool` at the entry level.","solutions":["Wrap each tool entry: `tools: { myTool: { tool: myToolImplementation } }`.","Add the `render` property alongside `tool` only if you supply a MastraCodeToolRenderConfig; it is optional.","Check for entries that are undefined/null due to conditional construction and remove or populate them.","Compare against another working plugin's tool export shape in the same repo."],"exampleFix":"// before\nexport const tools = { weatherLookup: createTool({ ... }) }\n// after\nexport const tools = { weatherLookup: { tool: createTool({ ... }) } }","handlingStrategy":"validation","validationCode":"for (const [name, entry] of Object.entries(plugin.tools ?? {})) {\n  if (!entry || typeof entry !== 'object' || !('tool' in entry)) {\n    throw new TypeError(`Plugin tool \"${name}\" must be wrapped as { tool, render? }`);\n  }\n}","typeGuard":"function isToolEntryObject(v: unknown): v is { tool: unknown; render?: MastraCodeToolRenderConfig } {\n  return typeof v === 'object' && v !== null && 'tool' in v;\n}","tryCatchPattern":"try {\n  await manager.registerPlugin(plugin);\n} catch (err) {\n  if (err instanceof Error && /must be an object with a tool property/.test(err.message)) {\n    console.error(`Malformed tool export: ${err.message} — wrap entries as { tool: ... }`);\n  } else throw err;\n}","preventionTips":["Always use the { tool, render? } wrapper shape in plugin tool exports, even for single tools.","Define plugin tools with a typed helper (e.g. definePluginTools()) that enforces the entry shape at compile time.","Add a smoke test that loads each plugin and asserts normalizePluginToolEntries succeeds."],"tags":["plugin","tools","shape-validation","sdk"],"backgroundTag":"plugin-tool-entry-shape-invalid","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T08:17:16.595Z"}