Skip to main content

Overview

defineTool creates a SharedToolDefinition that you can register with createMCPServer. It wraps your handler so that plain object return values are automatically converted to the ToolResult format the MCP protocol expects.
function defineTool<TShape extends ZodRawShape, OShape extends ZodRawShape>(
  def: ToolDefinitionInput<TShape, OShape>
): SharedToolDefinition<TShape>

Parameters

name
string
required
Unique identifier for the tool. Use lowercase letters and underscores (e.g. get_profile, search_documents). This name is what LLMs see when deciding which tool to call.
description
string
required
Human-readable description shown to the LLM. Write this as a clear, precise statement of what the tool does and when to use it.
inputSchema
ZodObject<TShape>
required
Zod object schema that validates and types the tool’s input arguments. The inferred type is passed to your handler as the first argument.
inputSchema: z.object({
  query: z.string().describe("Search query"),
  limit: z.number().int().min(1).max(100).optional(),
})
handler
(args: z.infer<TSchema>, context: ToolContext) => Promise<ToolResult | Record<string, unknown>>
required
Async function that executes the tool. Receives validated args and a ToolContext object.Return either a full ToolResult or a plain object. Plain objects are automatically wrapped — see Return values.
title
string
Human-readable display name shown in client UIs. Separate from name, which is the machine identifier.
outputSchema
ZodRawShape | ZodObject<OShape>
Zod schema for the tool’s structured output. Accepts either a raw shape ({ field: z.string() }) or a ZodObject. Normalized internally to ZodRawShape. Used by MCP clients that support structured content.
requiresAuth
boolean
When true, the dispatcher automatically rejects calls where context.providerToken is absent, returning an authentication error before your handler runs. Defaults to false.
annotations
object
Behavioral hints for MCP clients. These are informational — they are not enforced by the SDK, but well-behaved clients may use them to surface warnings or adjust behavior.
meta
{ version?: string; last_update?: string }
Version metadata automatically injected into every handler result as tool_version and tool_last_update fields. Useful for tracking which version of a tool produced a given output.

Return values

Your handler can return either a full ToolResult or a plain object. Plain objectdefineTool wraps it automatically:
// You return:
return { greeting: "Hello, Alice!", count: 1 };

// The dispatcher receives:
{
  content: [{ type: "text", text: JSON.stringify({ greeting: "Hello, Alice!", count: 1 }, null, 2) }],
  structuredContent: { greeting: "Hello, Alice!", count: 1 },
}
Full ToolResult — passed through unchanged:
return {
  content: [{ type: "text", text: "Hello, Alice!" }],
  structuredContent: { greeting: "Hello, Alice!" },
  isError: false,
};

Helpers

toolFail

Creates a typed error factory with preset default fields. Call the returned function with an error message to produce a consistent error object.
import { toolFail } from "@phake/mcp";

const fail = toolFail({ ok: false, items: null });

// In your handler:
if (!args.spreadsheetId) {
  return fail("spreadsheet_id is required");
  // => { ok: false, items: null, error: "spreadsheet_id is required" }
}

assertProviderToken

Narrows context so that context.providerToken is typed as string (not string | undefined). Throws "Authentication required" if the token is absent. Use this in tools where you need a guaranteed token but prefer not to set requiresAuth: true on the definition itself.
import { assertProviderToken } from "@phake/mcp";

handler: async (_args, context) => {
  assertProviderToken(context); // throws if missing
  // context.providerToken is now string
  const res = await fetch("https://api.example.com/me", {
    headers: { Authorization: `Bearer ${context.providerToken}` },
  });
  return res.json();
},

Example

A complete tool definition using all key fields:
import { z } from "zod";
import { defineTool, toolFail } from "@phake/mcp";

const VERSION = "1.2.0";
const fail = toolFail({ ok: false, results: null });

export const searchDocumentsTool = defineTool({
  name: "search_documents",
  title: "Search Documents",
  description:
    "Full-text search across all documents the user has access to. " +
    "Returns up to `limit` matching documents sorted by relevance.",
  inputSchema: z.object({
    query: z.string().min(1).describe("Search query"),
    limit: z.number().int().min(1).max(50).optional().describe("Max results (default 10)"),
  }),
  outputSchema: z.object({
    ok: z.boolean(),
    results: z
      .array(
        z.object({
          id: z.string(),
          title: z.string(),
          snippet: z.string(),
        })
      )
      .nullable(),
  }),
  requiresAuth: true,
  annotations: {
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: true,
    openWorldHint: false,
  },
  meta: {
    version: VERSION,
    last_update: "2025-01-15",
  },
  handler: async (args, context) => {
    if (!args.query.trim()) {
      return fail("query must not be empty");
    }

    const response = await fetch(
      `https://api.example.com/search?q=${encodeURIComponent(args.query)}&limit=${args.limit ?? 10}`,
      { headers: context.resolvedHeaders }
    );

    if (!response.ok) {
      return fail(`API error: ${response.status}`);
    }

    const data = await response.json();
    return { ok: true, results: data.items };
  },
});