/**
* Tools Index
*
* Central registry for all MCP tools.
* Single source of truth - no duplication in routes.ts.
*/
import { z, ZodTypeAny } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import type { ToolDefinition, ToolMetadata, ToolContext } from './types';
import { userTools } from './user';
import { integrationTools } from './integrations-example';
// Re-export types
export type { ToolDefinition, ToolMetadata, ToolContext, ToolResult, ToolCategory } from './types';
/**
* All tools - single source of truth
* Google Calendar server: user info + calendar integration tools
*/
export const ALL_TOOLS: ToolDefinition[] = [
...userTools, // get_user_info, list_conversations
...integrationTools, // Google Calendar API tools from @jezweb/mcp-integrations
];
/**
* Register all tools with an MCP agent.
*
* @param registerFn The agent's registerTool method
* @param getContext Function to get tool context (props, authorizedFetch)
*/
export function registerAllTools(
registerFn: <T extends Record<string, ZodTypeAny>>(
name: string,
description: string,
schema: T,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: (args: z.infer<z.ZodObject<T>>, extra?: any) => any,
options?: {
defer_loading?: boolean;
category?: string;
tags?: string[];
requiresAuth?: string;
authScopes?: string[];
}
) => void,
getContext: () => ToolContext | undefined
): void {
for (const tool of ALL_TOOLS) {
registerFn(
tool.name,
tool.description,
tool.schema,
(args) => tool.handler(args, getContext()),
tool.metadata
);
}
}
/**
* Get metadata for all tools.
* Used by admin dashboard and AI chat for introspection.
*/
export function getToolsMetadata(): ToolMetadata[] {
return ALL_TOOLS.map(tool => {
// Convert Zod schema to JSON Schema
const zodSchema = z.object(tool.schema);
const jsonSchema = zodToJsonSchema(zodSchema, { target: 'openApi3' });
return {
name: tool.name,
description: tool.description,
inputSchema: jsonSchema as Record<string, unknown>,
defer_loading: tool.metadata?.defer_loading,
category: tool.metadata?.category,
tags: tool.metadata?.tags,
requiresAuth: tool.metadata?.requiresAuth,
authScopes: tool.metadata?.authScopes,
};
});
}
/**
* Get unique categories from all tools
*/
export function getToolCategories(): string[] {
const categories = new Set<string>();
for (const tool of ALL_TOOLS) {
if (tool.metadata?.category) {
categories.add(tool.metadata.category);
}
}
return Array.from(categories).sort();
}
/**
* Get unique tags from all tools
*/
export function getToolTags(): string[] {
const tags = new Set<string>();
for (const tool of ALL_TOOLS) {
for (const tag of tool.metadata?.tags || []) {
tags.add(tag);
}
}
return Array.from(tags).sort();
}
// ============================================================================
// Tool Search (Anthropic Standard)
// @see https://www.anthropic.com/engineering/advanced-tool-use
// ============================================================================
/**
* Get tools that should always be visible to the LLM.
*
* When using many tools (100+), expose only alwaysVisible tools initially.
* The LLM discovers others via searchTools().
*/
export function getAlwaysVisibleTools(): ToolDefinition[] {
return ALL_TOOLS.filter((tool) => tool.metadata?.alwaysVisible === true);
}
/**
* Search options for Tool Search
*/
export interface SearchToolsOptions {
/** Text query to match against name, description, tags */
query?: string;
/** Filter by category */
category?: string;
/** Filter by tags (matches any) */
tags?: string[];
/** Maximum results to return (default: 20) */
limit?: number;
/** Include always-visible tools in results (default: true) */
includeAlwaysVisible?: boolean;
}
/**
* Search for tools by query, category, or tags.
*
* This implements the Tool Search pattern for managing large tool sets.
* Returns matching tools sorted by relevance (exact name match first).
*/
export function searchTools(options: SearchToolsOptions = {}): ToolMetadata[] {
const {
query,
category,
tags,
limit = 20,
includeAlwaysVisible = true,
} = options;
const queryLower = query?.toLowerCase();
// Filter tools
const matches = ALL_TOOLS.filter((tool) => {
// Skip always-visible if requested
if (!includeAlwaysVisible && tool.metadata?.alwaysVisible) {
return false;
}
// Category filter
if (category && tool.metadata?.category !== category) {
return false;
}
// Tags filter (match any)
if (tags && tags.length > 0) {
const toolTags = tool.metadata?.tags || [];
if (!tags.some((t) => toolTags.includes(t))) {
return false;
}
}
// Query filter (matches name, description, or tags)
if (queryLower) {
const inName = tool.name.toLowerCase().includes(queryLower);
const inDesc = tool.description.toLowerCase().includes(queryLower);
const inTags = (tool.metadata?.tags || []).some((t) =>
t.toLowerCase().includes(queryLower)
);
if (!inName && !inDesc && !inTags) {
return false;
}
}
return true;
});
// Sort by relevance (exact name match first, then alphabetical)
matches.sort((a, b) => {
if (queryLower) {
const aExact = a.name.toLowerCase() === queryLower;
const bExact = b.name.toLowerCase() === queryLower;
if (aExact && !bExact) return -1;
if (!aExact && bExact) return 1;
}
return a.name.localeCompare(b.name);
});
// Convert to metadata and limit
return matches.slice(0, limit).map((tool) => {
const zodSchema = z.object(tool.schema);
const jsonSchema = zodToJsonSchema(zodSchema, { target: 'openApi3' });
return {
name: tool.name,
description: tool.description,
inputSchema: jsonSchema as Record<string, unknown>,
defer_loading: tool.metadata?.defer_loading,
category: tool.metadata?.category,
tags: tool.metadata?.tags,
alwaysVisible: tool.metadata?.alwaysVisible,
requiresAuth: tool.metadata?.requiresAuth,
authScopes: tool.metadata?.authScopes,
};
});
}
/**
* Create a search_tools tool definition for the MCP server.
*
* This tool allows the LLM to discover available tools dynamically.
* Register this alongside your other tools when using Tool Search.
*/
export const searchToolsTool: ToolDefinition = {
name: 'search_tools',
description:
'Search for available tools by query, category, or tags. Use this to discover tools for specific tasks.',
schema: {
query: z.string().optional().describe('Text to search for in tool names, descriptions, or tags'),
category: z.string().optional().describe('Filter by tool category'),
tags: z.array(z.string()).optional().describe('Filter by tags (matches any)'),
limit: z.number().min(1).max(50).default(20).describe('Maximum results to return'),
},
handler: async (args) => {
const results = searchTools({
query: args.query,
category: args.category,
tags: args.tags,
limit: args.limit,
includeAlwaysVisible: false, // Don't duplicate always-visible tools
});
if (results.length === 0) {
return {
content: [
{
type: 'text',
text: 'No tools found matching your criteria. Try a different query or check available categories with search_tools without arguments.',
},
],
};
}
const summary = results.map((t) => ({
name: t.name,
description: t.description,
category: t.category,
tags: t.tags,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
count: results.length,
tools: summary,
hint: 'Use the tool name directly to execute it.',
},
null,
2
),
},
],
};
},
metadata: {
category: 'utility',
tags: ['search', 'discovery', 'tools'],
alwaysVisible: true, // This tool is always available
},
};