Skip to main content
Standalone tools let you expose logic to MCP clients that doesn’t belong on any HTTP route. Drop a file into server/tools/ and anvil mcp picks it up automatically — no registration, no config change needed.

File shape

Every file in server/tools/ must default-export a function. Alongside the default export, provide an inputSchema and a description:
// server/tools/wordCount.ts
import { z } from 'zod';

export const description = 'Count the words in a piece of text';
export const inputSchema = z.object({ text: z.string() });

export default async function wordCount(args: { text: string }) {
  const words = args.text.trim().split(/\s+/).filter(Boolean);
  return { words: words.length, characters: args.text.length };
}
default
function
required
The tool implementation. Receives the validated arguments as its first argument and may return any serializable value. Async functions are supported.
inputSchema
z.ZodObject
Describes the arguments the tool accepts. Pass a Zod object and Anvil converts it to JSON Schema automatically. When omitted (or when the export is not a Zod schema), Anvil falls back to an empty schema — so always export a Zod object to get input validation and accurate schema advertisement.
description
string
A plain-English description shown to MCP clients and AI agents. You can also export meta.description if you prefer to group metadata together.
name
string
An explicit tool name. When omitted, Anvil uses the filename without its extension (e.g. wordCount.tswordCount). Must match [a-zA-Z0-9_-]{1,64}.

Multi-parameter tools

Tools can accept any number of inputs. Define them all in the Zod object:
// server/tools/add.ts
import { z } from 'zod';

export const description = 'Add two numbers';
export const inputSchema = z.object({ a: z.number(), b: z.number() });

export default async function add(args: { a: number; b: number }) {
  return { sum: args.a + args.b };
}
When an MCP client calls this tool, Anvil validates the incoming arguments against the Zod schema before your function runs — any constraint violations are returned as a graceful tool error, not a protocol crash.

Generating the input schema dynamically

If you need constructs that Zod can’t express losslessly — or you’re generating tool definitions programmatically — build the Zod schema at module load time rather than inlining it:
// server/tools/search.ts
import { z } from 'zod';

export const description = 'Full-text search over the product catalog';

const filtersSchema = z.object({
  category: z.string().optional(),
  inStock: z.boolean().optional(),
});

export const inputSchema = z.object({
  query: z.string().min(1),
  limit: z.number().int().min(1).max(100).default(10),
  filters: filtersSchema.optional(),
});

export default async function search(args: {
  query: string;
  limit?: number;
  filters?: { category?: string; inStock?: boolean };
}) {
  // ... implementation
  return { results: [] };
}
Always export a Zod object as inputSchema. Anvil uses the Zod schema both to advertise the tool’s shape to MCP clients (via JSON Schema conversion) and to validate arguments at call time. If inputSchema is absent or not a Zod schema, Anvil falls back to an empty schema and skips validation.
Files whose names start with _ are ignored by the tool scanner. Use this convention for shared helpers you want to co-locate in server/tools/ without exposing them as tools.

How tools are served

When anvil mcp starts, it scans server/tools/ for all .ts, .mts, .js, and .mjs files (recursive, sorted alphabetically, excluding _-prefixed files and .d.ts declaration files). Each valid file becomes one MCP tool. Standalone tools are merged with route-derived tools into a single toolset. Tool names must be unique across both sources — a collision between a file in server/tools/ and a route with the same derived name is a hard startup error.
anvil mcp
# [anvil] mcp (Streamable HTTP) on http://localhost:3100/mcp
# [anvil] 3 tool(s): get_users_by_id, wordCount, add
Keep server/tools/ files focused: one exported function per file, co-located with its schema. This makes each tool easy to test in isolation and keeps the MCP surface easy to audit.