mastra-ai/mastra · error · Error

Plugin tool "${name}" must be an object with a tool property

Error message

Plugin tool "${name}" must be an object with a tool property

What it means

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.

Source

Thrown at mastracode/sdk/src/plugins/loader.ts:307

  if (plugin.instructions === undefined) return undefined;
  const instructions =
    typeof plugin.instructions === 'function' ? await plugin.instructions(context) : plugin.instructions;
  if (typeof instructions !== 'string') {
    throw new Error('Plugin instructions must be a string');
  }
  const trimmed = instructions.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function normalizePluginToolEntries(entries: MastraCodePluginToolEntries): {
  tools: MastraCodePluginTools;
  renderConfigs: Record<string, MastraCodeToolRenderConfig>;
} {
  const tools: MastraCodePluginTools = {};
  const renderConfigs: Record<string, MastraCodeToolRenderConfig> = {};
  for (const [name, entry] of Object.entries(entries)) {
    if (!isToolEntryObject(entry)) {
      throw new Error(`Plugin tool "${name}" must be an object with a tool property`);
    }
    tools[name] = entry.tool;
    if (entry.render) renderConfigs[name] = entry.render;
  }
  return { tools, renderConfigs };
}

function isToolEntryObject(entry: MastraCodePluginToolEntries[string]): entry is MastraCodePluginToolEntries[string] {
  if (!entry || typeof entry !== 'object' || !('tool' in entry)) return false;
  const tool = (entry as { tool?: unknown }).tool;
  return !!tool && typeof tool === 'object' && !Array.isArray(tool);
}

function validatePluginConfigSchema(schema: unknown): MastraCodePluginConfigSchema | undefined {
  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return undefined;
  const validated: MastraCodePluginConfigSchema = {};
  for (const [key, option] of Object.entries(schema)) {
    if (!option || typeof option !== 'object' || Array.isArray(option)) continue;

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Wrap each tool entry: `tools: { myTool: { tool: myToolImplementation } }`.
  2. Add the `render` property alongside `tool` only if you supply a MastraCodeToolRenderConfig; it is optional.
  3. Check for entries that are undefined/null due to conditional construction and remove or populate them.
  4. Compare against another working plugin's tool export shape in the same repo.

Example fix

// before
export const tools = { weatherLookup: createTool({ ... }) }
// after
export const tools = { weatherLookup: { tool: createTool({ ... }) } }
Defensive patterns

Strategy: validation

Validate before calling

for (const [name, entry] of Object.entries(plugin.tools ?? {})) {
  if (!entry || typeof entry !== 'object' || !('tool' in entry)) {
    throw new TypeError(`Plugin tool "${name}" must be wrapped as { tool, render? }`);
  }
}

Type guard

function isToolEntryObject(v: unknown): v is { tool: unknown; render?: MastraCodeToolRenderConfig } {
  return typeof v === 'object' && v !== null && 'tool' in v;
}

Try / catch

try {
  await manager.registerPlugin(plugin);
} catch (err) {
  if (err instanceof Error && /must be an object with a tool property/.test(err.message)) {
    console.error(`Malformed tool export: ${err.message} — wrap entries as { tool: ... }`);
  } else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/dfc71f53b02a6f1d. Report an issue: GitHub.