Skip to main content
The tool() function creates tool definitions for SDK-embedded MCP servers with automatic TypeScript type inference from Zod schemas.

Import

import { tool } from '@qwen-code/sdk';
import { z } from 'zod';
import type { SdkMcpToolDefinition } from '@qwen-code/sdk';

Signature

function tool<Schema extends ZodRawShape>(
  name: string,
  description: string,
  inputSchema: Schema,
  handler: (
    args: z.infer<ZodObject<Schema>>,
    extra: unknown
  ) => Promise<CallToolResult>
): SdkMcpToolDefinition<Schema>

Parameters

name
string
required
Tool name (1-64 characters). Must start with a letter and contain only letters, numbers, and underscores.
tool('calculate_sum', /* ... */)  // ✅ Valid
tool('get_weather', /* ... */)    // ✅ Valid
tool('123invalid', /* ... */)     // ❌ Invalid - starts with number
tool('my-tool', /* ... */)        // ❌ Invalid - contains hyphen
description
string
required
Human-readable description of what the tool does. This helps the AI understand when to use the tool.
tool(
  'search_docs',
  'Search technical documentation using semantic search',
  /* ... */
)
Write clear, concise descriptions. The AI uses these to decide when to invoke the tool.
inputSchema
ZodRawShape
required
Zod schema object defining the tool’s input parameters. The schema is used for:
  • Type inference (TypeScript types for handler args)
  • Runtime validation (invalid inputs are rejected)
  • AI guidance (schema sent to AI as JSON Schema)
tool(
  'create_user',
  'Create a new user account',
  {
    email: z.string().email().describe('User email address'),
    name: z.string().min(1).describe('User full name'),
    age: z.number().int().min(0).max(120).optional(),
  },
  async (args) => {
    // args is typed as:
    // { email: string; name: string; age?: number }
  }
)
handler
function
required
Async function that executes the tool and returns the result.Signature:
async (args: InferredType, extra: unknown) => Promise<CallToolResult>
  • args: Tool input, typed according to inputSchema
  • extra: Reserved for future use (currently unused)
  • Returns: CallToolResult with content blocks

Return Value

SdkMcpToolDefinition
object
A tool definition object that can be passed to createSdkMcpServer().
{
  name: string;
  description: string;
  inputSchema: Schema;
  handler: Function;
}

CallToolResult Format

The handler must return a CallToolResult object:
type CallToolResult = {
  content: Array<
    | { type: 'text'; text: string }
    | { type: 'image'; data: string; mimeType: string }
    | { type: 'resource'; uri: string; mimeType?: string; text?: string }
  >;
  isError?: boolean;
};

Text Content

Most common response type:
return {
  content: [{ type: 'text', text: 'Operation completed successfully' }],
};

Image Content

Return base64-encoded images:
return {
  content: [{
    type: 'image',
    data: 'iVBORw0KGgoAAAANSUhEUgAA...', // base64 data
    mimeType: 'image/png',
  }],
};

Multiple Content Blocks

Combine text and images:
return {
  content: [
    { type: 'text', text: 'Here is the visualization:' },
    { type: 'image', data: chartImageData, mimeType: 'image/png' },
    { type: 'text', text: 'Analysis complete.' },
  ],
};

Error Response

Indicate errors with isError: true:
return {
  content: [{ type: 'text', text: 'Failed to connect to database' }],
  isError: true,
};

Examples

Simple Calculator

import { z } from 'zod';
import { tool } from '@qwen-code/sdk';

const addTool = tool(
  'add',
  'Add two numbers together',
  {
    a: z.number().describe('First number'),
    b: z.number().describe('Second number'),
  },
  async (args) => {
    const result = args.a + args.b;
    return {
      content: [{ type: 'text', text: String(result) }],
    };
  }
);

File Operations

import { z } from 'zod';
import { tool } from '@qwen-code/sdk';
import * as fs from 'fs/promises';

const writeFileTool = tool(
  'write_custom_file',
  'Write content to a file in the custom directory',
  {
    filename: z.string().describe('Name of the file'),
    content: z.string().describe('Content to write'),
  },
  async (args) => {
    try {
      const path = `./custom-dir/${args.filename}`;
      await fs.writeFile(path, args.content, 'utf-8');
      
      return {
        content: [{
          type: 'text',
          text: `Successfully wrote ${args.content.length} bytes to ${path}`,
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: 'text',
          text: `Error writing file: ${error.message}`,
        }],
        isError: true,
      };
    }
  }
);

API Integration

import { z } from 'zod';
import { tool } from '@qwen-code/sdk';

const weatherTool = tool(
  'get_weather',
  'Get current weather for a city using OpenWeatherMap API',
  {
    city: z.string().describe('City name'),
    units: z.enum(['metric', 'imperial']).default('metric').describe('Temperature units'),
  },
  async (args) => {
    try {
      const apiKey = process.env.OPENWEATHER_API_KEY;
      const url = `https://api.openweathermap.org/data/2.5/weather?q=${args.city}&units=${args.units}&appid=${apiKey}`;
      
      const response = await fetch(url);
      const data = await response.json();
      
      if (!response.ok) {
        return {
          content: [{ type: 'text', text: `Error: ${data.message}` }],
          isError: true,
        };
      }
      
      const temp = data.main.temp;
      const description = data.weather[0].description;
      const unit = args.units === 'metric' ? '°C' : '°F';
      
      return {
        content: [{
          type: 'text',
          text: `Weather in ${args.city}: ${description}, ${temp}${unit}`,
        }],
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Failed to fetch weather: ${error.message}` }],
        isError: true,
      };
    }
  }
);

Database Query

import { z } from 'zod';
import { tool } from '@qwen-code/sdk';
import { db } from './database';

const queryUsersTool = tool(
  'query_users',
  'Search users in the database',
  {
    email: z.string().email().optional().describe('Filter by email'),
    minAge: z.number().int().min(0).optional().describe('Minimum age'),
    limit: z.number().int().min(1).max(100).default(10).describe('Max results'),
  },
  async (args) => {
    try {
      let query = db.users.select();
      
      if (args.email) {
        query = query.where('email', '=', args.email);
      }
      
      if (args.minAge !== undefined) {
        query = query.where('age', '>=', args.minAge);
      }
      
      const users = await query.limit(args.limit).execute();
      
      return {
        content: [{
          type: 'text',
          text: `Found ${users.length} users:\n${JSON.stringify(users, null, 2)}`,
        }],
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Database error: ${error.message}` }],
        isError: true,
      };
    }
  }
);

Complex Schema with Nested Objects

import { z } from 'zod';
import { tool } from '@qwen-code/sdk';

const createProjectTool = tool(
  'create_project',
  'Create a new project with configuration',
  {
    name: z.string().min(1).describe('Project name'),
    config: z.object({
      language: z.enum(['typescript', 'javascript', 'python']),
      framework: z.string().optional(),
      dependencies: z.array(z.string()).default([]),
    }).describe('Project configuration'),
    metadata: z.record(z.string()).optional().describe('Additional metadata'),
  },
  async (args) => {
    // args is fully typed:
    // {
    //   name: string;
    //   config: {
    //     language: 'typescript' | 'javascript' | 'python';
    //     framework?: string;
    //     dependencies: string[];
    //   };
    //   metadata?: Record<string, string>;
    // }
    
    const project = {
      id: Math.random().toString(36),
      ...args,
      createdAt: new Date().toISOString(),
    };
    
    return {
      content: [{
        type: 'text',
        text: `Created project: ${JSON.stringify(project, null, 2)}`,
      }],
    };
  }
);

Tool with No Input

import { z } from 'zod';
import { tool } from '@qwen-code/sdk';

const getTimeTool = tool(
  'get_current_time',
  'Get the current server time',
  {}, // Empty schema for no parameters
  async () => {
    return {
      content: [{
        type: 'text',
        text: new Date().toISOString(),
      }],
    };
  }
);

Schema Validation

Supported Zod Types

The tool function supports all Zod schema types:
{
  // Primitives
  str: z.string(),
  num: z.number(),
  bool: z.boolean(),
  
  // Constrained
  email: z.string().email(),
  url: z.string().url(),
  positiveInt: z.number().int().positive(),
  
  // Enums
  status: z.enum(['active', 'inactive', 'pending']),
  
  // Arrays
  tags: z.array(z.string()),
  
  // Objects
  user: z.object({
    name: z.string(),
    age: z.number(),
  }),
  
  // Optional/Default
  optional: z.string().optional(),
  withDefault: z.number().default(42),
  
  // Union types
  idOrName: z.union([z.number(), z.string()]),
  
  // Records/Dictionaries
  metadata: z.record(z.string()),
}

Descriptions

Add descriptions to help the AI understand parameters:
tool(
  'search',
  'Search for items',
  {
    query: z.string()
      .min(1)
      .describe('Search query (minimum 1 character)'),
    
    filters: z.object({
      category: z.string().optional()
        .describe('Filter by category name'),
      
      priceRange: z.object({
        min: z.number().optional()
          .describe('Minimum price in USD'),
        max: z.number().optional()
          .describe('Maximum price in USD'),
      }).optional(),
    }).optional().describe('Optional search filters'),
  },
  async (args) => { /* ... */ }
)

Validation Rules

Tool Name Validation

The tool name must:
  • Be 1-64 characters long
  • Start with a letter (a-z, A-Z)
  • Contain only letters, numbers, and underscores
// ✅ Valid names
tool('calculate', /* ... */)
tool('get_user', /* ... */)
tool('searchDocs', /* ... */)
tool('tool_123', /* ... */)

// ❌ Invalid names
tool('123tool', /* ... */)      // Starts with number
tool('my-tool', /* ... */)      // Contains hyphen
tool('user@search', /* ... */)  // Contains @
tool('', /* ... */)             // Empty string

Input Validation

Zod validates inputs at runtime:
const emailTool = tool(
  'send_email',
  'Send an email',
  {
    to: z.string().email(),
    subject: z.string().min(1),
  },
  async (args) => {
    // args.to is guaranteed to be a valid email
    // args.subject is guaranteed to be non-empty
  }
);

// If AI passes invalid data:
// { to: 'not-an-email', subject: '' }
// The tool will return a validation error automatically

Error Handling

Validation Errors

Invalid inputs are automatically rejected:
// Tool receives: { age: 'not a number' }
// Zod validation fails
// Tool returns error to AI automatically

Handler Errors

Catch and format errors in your handler:
const tool = tool(
  'risky_operation',
  'Perform a risky operation',
  { input: z.string() },
  async (args) => {
    try {
      const result = await performRiskyOperation(args.input);
      return {
        content: [{ type: 'text', text: result }],
      };
    } catch (error) {
      // Return formatted error
      return {
        content: [{
          type: 'text',
          text: `Operation failed: ${error.message}`,
        }],
        isError: true,
      };
    }
  }
);

Timeout Errors

Tools that take too long will timeout:
// Configure timeout in query options
options: {
  mcpServers: { myServer: server },
  timeout: {
    mcpRequest: 600000, // 10 minutes
  },
}

Type Safety

The tool function provides full type inference:
import { z } from 'zod';
import { tool } from '@qwen-code/sdk';

const myTool = tool(
  'process_data',
  'Process some data',
  {
    id: z.number(),
    name: z.string(),
    tags: z.array(z.string()),
    metadata: z.record(z.string()).optional(),
  },
  async (args) => {
    // TypeScript knows:
    args.id      // number
    args.name    // string
    args.tags    // string[]
    args.metadata // Record<string, string> | undefined
    
    // This would be a TypeScript error:
    // args.id.toUpperCase() // Error: number has no toUpperCase
    
    return {
      content: [{ type: 'text', text: 'Done' }],
    };
  }
);

Best Practices

1. Write Clear Descriptions

// ❌ Bad
tool('search', 'Searches stuff', /* ... */)

// ✅ Good
tool(
  'search',
  'Search the product catalog using keywords. Returns up to 50 matching products.',
  /* ... */
)

2. Validate and Constrain Inputs

tool(
  'create_user',
  'Create a new user',
  {
    email: z.string().email(),
    age: z.number().int().min(0).max(120),
    username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
  },
  /* ... */
)

3. Use Optional and Default Values

tool(
  'search',
  'Search items',
  {
    query: z.string(),
    limit: z.number().int().min(1).max(100).default(10),
    offset: z.number().int().min(0).default(0).optional(),
  },
  /* ... */
)

4. Handle Errors Gracefully

const tool = tool(
  'fetch_data',
  'Fetch data from API',
  { url: z.string().url() },
  async (args) => {
    try {
      const response = await fetch(args.url);
      const data = await response.text();
      return { content: [{ type: 'text', text: data }] };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Fetch failed: ${error.message}` }],
        isError: true,
      };
    }
  }
);

5. Return Structured Data

const tool = tool(
  'get_stats',
  'Get usage statistics',
  {},
  async () => {
    const stats = await getStats();
    
    // Format as readable text or JSON
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(stats, null, 2),
      }],
    };
  }
);

See Also