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.

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,
};
}
},
});

Parameter Validation

Use Zod schemas for comprehensive parameter validation:

src/tools/weather.ts
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,
tools: {
// Add your custom 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);