Skip to main content
Version: Next

Custom AI Tools

CommandKit allows you to create custom tools that AI models can use to extend their capabilities beyond built-in Discord functions. You can define tools in two ways,

  • one with the createTool function, which is a simple way to create tools that can be used by AI models. This is a wrapper around the tool function of ai-sdk which enables the tool to receive context (AiContext which contains client and message objects) and parameters.
  • another with the tool function directly, which does not provide context argument. You may use the useAIContext hook to access the context in the tool function if needed.

This guide will walk you through creating custom tools, validating parameters, and integrating with external APIs.

Creating Basic Tools

Use the createTool function to define custom tools:

src/tools/math.ts
import { createTool } from '@commandkit/ai';
import { z } from 'zod';

export const calculator = createTool({
name: 'calculator',
description: 'Perform mathematical calculations',
parameters: z.object({
expression: z
.string()
.describe(
'Mathematical expression to evaluate (e.g., "2 + 2", "sqrt(16)")',
),
}),
async execute(ctx, params) {
const { expression } = params;

try {
// Use a safe math evaluator (never use eval() directly!)
const result = evaluateMathExpression(expression);

return {
expression,
result,
success: true,
};
} catch (error) {
return {
expression,
error: error.message,
success: false,
};
}
},
});

See the Tool Registration section to learn how to register this tool with your AI model.

Parameter Validation

Use Zod schemas for comprehensive parameter validation:

src/tools/weather.ts
import { z } from 'zod';

export const getWeather = createTool({
name: 'getWeather',
description: 'Get current weather information for any location',
parameters: z.object({
location: z.string().min(1).describe('City name or coordinates'),
units: z.enum(['metric', 'imperial', 'kelvin']).default('metric'),
includeHourly: z
.boolean()
.default(false)
.describe('Include hourly forecast'),
days: z
.number()
.min(1)
.max(7)
.default(1)
.describe('Number of forecast days'),
}),
async execute(ctx, params) {
const { location, units, includeHourly, days } = params;

// Fetch weather data from API
const weatherData = await fetchWeatherAPI({
location,
units,
forecast: days > 1,
hourly: includeHourly,
});

return {
location: weatherData.location,
current: {
temperature: weatherData.current.temp,
condition: weatherData.current.condition,
humidity: weatherData.current.humidity,
windSpeed: weatherData.current.windSpeed,
},
forecast: weatherData.forecast?.slice(0, days),
units,
};
},
});

Database Integration

Create tools that interact with databases:

src/tools/user-profile.ts
export const getUserProfile = createTool({
name: 'getUserProfile',
description: 'Get user profile information from the database',
parameters: z.object({
userId: z.string().describe('Discord user ID'),
includeStats: z
.boolean()
.default(false)
.describe('Include user statistics'),
}),
async execute(ctx, params) {
const { userId, includeStats } = params;

try {
const profile = await database.user.findUnique({
where: { discordId: userId },
include: {
stats: includeStats,
preferences: true,
},
});

if (!profile) {
return {
error: 'User profile not found',
userId,
};
}

return {
userId,
profile: {
level: profile.level,
experience: profile.experience,
joinedAt: profile.createdAt,
preferences: profile.preferences,
...(includeStats && { stats: profile.stats }),
},
};
} catch (error) {
return {
error: 'Failed to fetch user profile',
userId,
};
}
},
});

export const updateUserProfile = createTool({
name: 'updateUserProfile',
description: 'Update user profile settings',
parameters: z.object({
userId: z.string().describe('Discord user ID'),
updates: z.object({
nickname: z.string().optional(),
bio: z.string().max(500).optional(),
timezone: z.string().optional(),
notifications: z.boolean().optional(),
}),
}),
async execute(ctx, params) {
const { userId, updates } = params;

// Verify the user has permission to update this profile
if (
ctx.message.author.id !== userId &&
!isModeratorOrAdmin(ctx.message.member)
) {
return {
error: 'You can only update your own profile',
userId,
};
}

try {
const updatedProfile = await database.user.update({
where: { discordId: userId },
data: updates,
});

return {
success: true,
userId,
updatedFields: Object.keys(updates),
};
} catch (error) {
return {
error: 'Failed to update profile',
userId,
};
}
},
});

API Integration Tools

Create tools that interact with external APIs:

src/tools/github.ts
export const searchGitHubRepos = createTool({
name: 'searchGitHubRepos',
description: 'Search for GitHub repositories',
parameters: z.object({
query: z.string().describe('Search query for repositories'),
language: z.string().optional().describe('Filter by programming language'),
sort: z.enum(['stars', 'forks', 'updated']).default('stars'),
limit: z.number().min(1).max(20).default(5),
}),
async execute(ctx, params) {
const { query, language, sort, limit } = params;

try {
const searchQuery = language ? `${query} language:${language}` : query;

const response = await fetch(
`https://api.github.com/search/repositories?q=${encodeURIComponent(searchQuery)}&sort=${sort}&per_page=${limit}`,
{
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
},
},
);

if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}

const data = await response.json();

return {
query,
totalCount: data.total_count,
repositories: data.items.map((repo) => ({
name: repo.name,
fullName: repo.full_name,
description: repo.description,
stars: repo.stargazers_count,
forks: repo.forks_count,
language: repo.language,
url: repo.html_url,
lastUpdated: repo.updated_at,
})),
};
} catch (error) {
return {
error: `Failed to search repositories: ${error.message}`,
query,
};
}
},
});

File System Tools

Create tools for file operations (use with caution):

src/tools/files.ts
export const listFiles = createTool({
name: 'listFiles',
description: 'List files in a directory (admin only)',
parameters: z.object({
directory: z.string().describe('Directory path to list'),
includeHidden: z.boolean().default(false),
}),
async execute(ctx, params) {
// Security check
if (!isAdmin(ctx.message.member)) {
return {
error: 'This command requires administrator permissions',
};
}

const { directory, includeHidden } = params;

try {
const fs = await import('fs/promises');
const path = await import('path');

// Sanitize path to prevent directory traversal
const safePath = path.resolve(process.cwd(), directory);
if (!safePath.startsWith(process.cwd())) {
throw new Error('Invalid directory path');
}

const entries = await fs.readdir(safePath, { withFileTypes: true });
const files = entries
.filter((entry) => includeHidden || !entry.name.startsWith('.'))
.map((entry) => ({
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file',
path: path.join(safePath, entry.name),
}));

return {
directory: safePath,
files,
count: files.length,
};
} catch (error) {
return {
error: `Failed to list files: ${error.message}`,
directory,
};
}
},
});

Moderation Tools

Create tools for server moderation:

src/tools/moderation.ts
export const moderateContent = createTool({
name: 'moderateContent',
description: 'Check if content violates server rules',
parameters: z.object({
content: z.string().describe('Content to moderate'),
strictMode: z
.boolean()
.default(false)
.describe('Use strict moderation rules'),
}),
async execute(ctx, params) {
const { content, strictMode } = params;

// Check for various violations
const violations = [];

// Profanity check
if (containsProfanity(content)) {
violations.push('profanity');
}

// Spam check
if (isSpam(content)) {
violations.push('spam');
}

// Link check
if (containsSuspiciousLinks(content)) {
violations.push('suspicious_links');
}

// Strict mode additional checks
if (strictMode) {
if (containsCapSpam(content)) {
violations.push('excessive_caps');
}

if (containsRepeatedChars(content)) {
violations.push('repeated_characters');
}
}

return {
content: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
violations,
isClean: violations.length === 0,
severity:
violations.length > 2
? 'high'
: violations.length > 0
? 'medium'
: 'low',
recommendations: getRecommendations(violations),
};
},
});

export const timeoutUser = createTool({
name: 'timeoutUser',
description: 'Timeout a user (moderator only)',
parameters: z.object({
userId: z.string().describe('User ID to timeout'),
duration: z
.number()
.min(60)
.max(2419200)
.describe('Timeout duration in seconds'),
reason: z.string().optional().describe('Reason for timeout'),
}),
async execute(ctx, params) {
const { userId, duration, reason } = params;

// Permission check
if (!ctx.message.member?.permissions.has('ModerateMembers')) {
return {
error: 'You need Moderate Members permission to use this command',
};
}

try {
const member = await ctx.message.guild?.members.fetch(userId);
if (!member) {
return {
error: 'Member not found',
userId,
};
}

await member.timeout(duration * 1000, reason || 'No reason provided');

return {
success: true,
userId,
duration,
reason,
moderator: ctx.message.author.id,
};
} catch (error) {
return {
error: `Failed to timeout user: ${error.message}`,
userId,
};
}
},
});

Utility Tools

Create general utility tools:

src/tools/utilities.ts
export const generateQR = createTool({
name: 'generateQR',
description: 'Generate a QR code for text or URL',
parameters: z.object({
data: z.string().describe('Text or URL to encode'),
size: z.enum(['small', 'medium', 'large']).default('medium'),
format: z.enum(['png', 'svg']).default('png'),
}),
async execute(ctx, params) {
const { data, size, format } = params;

try {
const QRCode = await import('qrcode');

const sizeMap = { small: 128, medium: 256, large: 512 };
const qrSize = sizeMap[size];

if (format === 'svg') {
const svg = await QRCode.toString(data, {
type: 'svg',
width: qrSize,
});

return {
success: true,
data: data.substring(0, 50),
format: 'svg',
svg,
};
} else {
const buffer = await QRCode.toBuffer(data, {
width: qrSize,
});

const base64 = buffer.toString('base64');

return {
success: true,
data: data.substring(0, 50),
format: 'png',
image: `data:image/png;base64,${base64}`,
};
}
} catch (error) {
return {
error: `Failed to generate QR code: ${error.message}`,
};
}
},
});

export const shortenUrl = createTool({
name: 'shortenUrl',
description: 'Shorten a long URL',
parameters: z.object({
url: z.string().url().describe('URL to shorten'),
customAlias: z
.string()
.optional()
.describe('Custom alias for the shortened URL'),
}),
async execute(ctx, params) {
const { url, customAlias } = params;

try {
// Use a URL shortening service API
const response = await fetch('https://api.short.io/links', {
method: 'POST',
headers: {
Authorization: process.env.SHORT_IO_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
originalURL: url,
domain: 'short.io',
...(customAlias && { path: customAlias }),
}),
});

if (!response.ok) {
throw new Error(`URL shortening failed: ${response.status}`);
}

const data = await response.json();

return {
originalUrl: url,
shortUrl: data.shortURL,
alias: data.path,
createdAt: new Date().toISOString(),
};
} catch (error) {
return {
error: `Failed to shorten URL: ${error.message}`,
originalUrl: url,
};
}
},
});

Tool Registration

Register your custom tools by adding them to the AI model configuration:

src/ai.ts
import { configureAI } from '@commandkit/ai';
import { calculator } from './tools/math';
import { getWeather } from './tools/weather';
import { searchGitHubRepos } from './tools/github';

configureAI({
selectAiModel: async (ctx, message) => ({
model: myModel,
// Add your custom tools to use with the AI model
tools: {
calculator,
getWeather,
searchGitHubRepos,
},
}),
// ... other configuration
});

Best Practices

  1. Security: Always validate user permissions for sensitive operations
  2. Error Handling: Return structured error information instead of throwing
  3. Rate Limiting: Implement rate limiting for API calls
  4. Validation: Use Zod schemas for comprehensive parameter validation
  5. Documentation: Provide clear descriptions for tools and parameters
  6. Testing: Test tools independently before integration
// Good: Structured error handling
return {
error: 'Specific error message',
code: 'ERROR_CODE',
retryable: false,
};

// Good: Permission checks
if (!hasPermission(ctx.message.member, 'required_permission')) {
return { error: 'Insufficient permissions' };
}

// Good: Input sanitization
const sanitizedInput = sanitize(params.userInput);