structured-data-example.ts•13.4 kB
/**
 * @file Structured data example tool handler
 * @module handlers/tools/structured-data-example
 * 
 * @remarks
 * This tool demonstrates how MCP tools can return structured data
 * alongside text content. Structured data enables better integration,
 * type safety, and programmatic consumption of tool results.
 * 
 * The structured data pattern:
 * 1. Tool defines an output schema
 * 2. Tool returns both text and structured content
 * 3. Structured content conforms to the output schema
 * 4. Clients can validate and use the structured data
 * 
 * @see {@link https://modelcontextprotocol.io/specification/2025-06-18/server/tools | MCP Tools Specification}
 */
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import type { MCPToolContext } from '../../types/request-context.js';
import { sendOperationNotification } from '../notifications.js';
import { logger } from '../../utils/logger.js';
// Validation will be done by the framework
/**
 * Arguments for the structured data example tool
 */
interface StructuredDataExampleArgs {
  /** The type of structured data to return */
  dataType: 'user' | 'analytics' | 'weather' | 'product';
  /** Optional ID to fetch specific data */
  id?: string;
  /** Whether to include nested data */
  includeNested?: boolean;
  /** Whether to simulate validation errors */
  simulateError?: boolean;
}
/**
 * Example structured data interfaces
 */
interface UserData {
  id: string;
  username: string;
  email: string;
  created: string;
  profile: {
    displayName: string;
    bio?: string;
    avatarUrl?: string;
    verified: boolean;
  };
  stats: {
    posts: number;
    comments: number;
    karma: number;
  };
}
interface AnalyticsData {
  period: string;
  metrics: {
    views: number;
    uniqueVisitors: number;
    engagementRate: number;
    averageSessionDuration: number;
  };
  topContent: Array<{
    id: string;
    title: string;
    views: number;
    engagement: number;
  }>;
  trends: {
    viewsChange: number;
    engagementChange: number;
  };
}
interface WeatherData {
  location: {
    city: string;
    country: string;
    coordinates: {
      latitude: number;
      longitude: number;
    };
  };
  current: {
    temperature: number;
    feelsLike: number;
    humidity: number;
    windSpeed: number;
    conditions: string;
    icon: string;
  };
  forecast: Array<{
    date: string;
    high: number;
    low: number;
    conditions: string;
    precipitation: number;
  }>;
}
interface ProductData {
  id: string;
  name: string;
  description: string;
  price: {
    amount: number;
    currency: string;
    discount?: number;
  };
  inventory: {
    inStock: boolean;
    quantity: number;
    warehouse: string;
  };
  ratings: {
    average: number;
    count: number;
    distribution: Record<string, number>;
  };
}
/**
 * Example tool that demonstrates returning structured data
 * 
 * @remarks
 * This tool shows how to return properly structured data that
 * conforms to defined schemas. It demonstrates validation,
 * nested objects, arrays, and various data types.
 * 
 * The tool returns both human-readable text and machine-readable
 * structured data for maximum compatibility.
 * 
 * @param args - Tool arguments specifying data type to return
 * @param context - MCP context with session information
 * @returns Tool response with structured data
 */
export async function handleStructuredDataExample(
  args: StructuredDataExampleArgs,
  context: MCPToolContext
): Promise<CallToolResult> {
  try {
    // Input validation is handled by MCP framework
    // Send notification about the operation
    await sendOperationNotification(
      'structured_data_example',
      `Demonstrating ${args.dataType} structured data`,
      context.sessionId
    );
    // Simulate validation error if requested
    if (args.simulateError) {
      throw new Error('Simulated validation error: Invalid data format');
    }
    // Generate structured data based on type
    let structuredData: any;
    let textDescription: string;
    switch (args.dataType) {
      case 'user':
        const userData: UserData = {
          id: args.id || 'user_123',
          username: 'example_user',
          email: 'user@example.com',
          created: new Date('2023-01-15').toISOString(),
          profile: {
            displayName: 'Example User',
            bio: args.includeNested ? 'Software developer and Reddit enthusiast' : undefined,
            avatarUrl: args.includeNested ? 'https://example.com/avatar.jpg' : undefined,
            verified: true
          },
          stats: {
            posts: 42,
            comments: 156,
            karma: 1337
          }
        };
        structuredData = userData;
        textDescription = `User ${userData.username} (${userData.profile.displayName}) has ${userData.stats.karma} karma from ${userData.stats.posts} posts and ${userData.stats.comments} comments.`;
        break;
      case 'analytics':
        const analyticsData: AnalyticsData = {
          period: '2024-01',
          metrics: {
            views: 15420,
            uniqueVisitors: 8234,
            engagementRate: 0.42,
            averageSessionDuration: 245
          },
          topContent: args.includeNested ? [
            { id: 'post_1', title: 'Getting Started Guide', views: 3201, engagement: 0.56 },
            { id: 'post_2', title: 'Advanced Techniques', views: 2150, engagement: 0.48 },
            { id: 'post_3', title: 'Common Mistakes', views: 1892, engagement: 0.51 }
          ] : [],
          trends: {
            viewsChange: 0.15,
            engagementChange: 0.08
          }
        };
        structuredData = analyticsData;
        textDescription = `Analytics for ${analyticsData.period}: ${analyticsData.metrics.views} views, ${analyticsData.metrics.uniqueVisitors} unique visitors, ${(analyticsData.metrics.engagementRate * 100).toFixed(1)}% engagement rate.`;
        break;
      case 'weather':
        const weatherData: WeatherData = {
          location: {
            city: 'San Francisco',
            country: 'USA',
            coordinates: {
              latitude: 37.7749,
              longitude: -122.4194
            }
          },
          current: {
            temperature: 18,
            feelsLike: 16,
            humidity: 72,
            windSpeed: 15,
            conditions: 'Partly Cloudy',
            icon: '⛅'
          },
          forecast: args.includeNested ? [
            { date: '2024-01-21', high: 20, low: 14, conditions: 'Sunny', precipitation: 0 },
            { date: '2024-01-22', high: 19, low: 13, conditions: 'Cloudy', precipitation: 10 },
            { date: '2024-01-23', high: 17, low: 12, conditions: 'Rainy', precipitation: 80 }
          ] : []
        };
        structuredData = weatherData;
        textDescription = `Weather in ${weatherData.location.city}: ${weatherData.current.temperature}°C, ${weatherData.current.conditions}, ${weatherData.current.humidity}% humidity.`;
        break;
      case 'product':
        const productData: ProductData = {
          id: args.id || 'prod_789',
          name: 'Premium Widget Pro',
          description: 'High-quality widget with advanced features',
          price: {
            amount: 99.99,
            currency: 'USD',
            discount: args.includeNested ? 15 : undefined
          },
          inventory: {
            inStock: true,
            quantity: 250,
            warehouse: 'Warehouse A'
          },
          ratings: {
            average: 4.5,
            count: 328,
            distribution: args.includeNested ? {
              '5': 198,
              '4': 87,
              '3': 28,
              '2': 10,
              '1': 5
            } : {}
          }
        };
        structuredData = productData;
        const finalPrice = productData.price.discount 
          ? productData.price.amount * (1 - productData.price.discount / 100)
          : productData.price.amount;
        textDescription = `${productData.name}: ${productData.price.currency} ${finalPrice.toFixed(2)}${productData.price.discount ? ` (${productData.price.discount}% off)` : ''}, ${productData.ratings.average}/5 stars (${productData.ratings.count} reviews), ${productData.inventory.inStock ? 'In Stock' : 'Out of Stock'}.`;
        break;
    }
    // Return both text and structured content
    return {
      content: [
        {
          type: 'text',
          text: textDescription
        }
      ],
      // This is the key feature - returning structured data
      structuredContent: structuredData
    };
  } catch (error) {
    logger.error('❌ Structured data example failed', {
      error: error instanceof Error ? error.message : String(error),
      dataType: args.dataType
    });
    await sendOperationNotification(
      'structured_data_example',
      `Structured data example failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
      context.sessionId
    );
    throw error;
  }
}
/**
 * Get the output schema for this tool
 * 
 * @remarks
 * This function returns the JSON schema that describes the
 * structured output of this tool. Clients can use this to
 * validate the structured content.
 * 
 * @param dataType - The type of data schema to return
 * @returns JSON schema for the output
 */
export function getStructuredDataOutputSchema(dataType: string): any {
  const schemas: Record<string, any> = {
    user: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        username: { type: 'string' },
        email: { type: 'string' },
        created: { type: 'string', format: 'date-time' },
        profile: {
          type: 'object',
          properties: {
            displayName: { type: 'string' },
            bio: { type: 'string' },
            avatarUrl: { type: 'string', format: 'uri' },
            verified: { type: 'boolean' }
          },
          required: ['displayName', 'verified']
        },
        stats: {
          type: 'object',
          properties: {
            posts: { type: 'number' },
            comments: { type: 'number' },
            karma: { type: 'number' }
          },
          required: ['posts', 'comments', 'karma']
        }
      },
      required: ['id', 'username', 'email', 'created', 'profile', 'stats']
    },
    analytics: {
      type: 'object',
      properties: {
        period: { type: 'string' },
        metrics: {
          type: 'object',
          properties: {
            views: { type: 'number' },
            uniqueVisitors: { type: 'number' },
            engagementRate: { type: 'number', minimum: 0, maximum: 1 },
            averageSessionDuration: { type: 'number' }
          }
        },
        topContent: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id: { type: 'string' },
              title: { type: 'string' },
              views: { type: 'number' },
              engagement: { type: 'number' }
            }
          }
        },
        trends: {
          type: 'object',
          properties: {
            viewsChange: { type: 'number' },
            engagementChange: { type: 'number' }
          }
        }
      }
    },
    weather: {
      type: 'object',
      properties: {
        location: {
          type: 'object',
          properties: {
            city: { type: 'string' },
            country: { type: 'string' },
            coordinates: {
              type: 'object',
              properties: {
                latitude: { type: 'number' },
                longitude: { type: 'number' }
              }
            }
          }
        },
        current: {
          type: 'object',
          properties: {
            temperature: { type: 'number' },
            feelsLike: { type: 'number' },
            humidity: { type: 'number' },
            windSpeed: { type: 'number' },
            conditions: { type: 'string' },
            icon: { type: 'string' }
          }
        },
        forecast: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              date: { type: 'string' },
              high: { type: 'number' },
              low: { type: 'number' },
              conditions: { type: 'string' },
              precipitation: { type: 'number' }
            }
          }
        }
      }
    },
    product: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
        description: { type: 'string' },
        price: {
          type: 'object',
          properties: {
            amount: { type: 'number' },
            currency: { type: 'string' },
            discount: { type: 'number' }
          }
        },
        inventory: {
          type: 'object',
          properties: {
            inStock: { type: 'boolean' },
            quantity: { type: 'number' },
            warehouse: { type: 'string' }
          }
        },
        ratings: {
          type: 'object',
          properties: {
            average: { type: 'number' },
            count: { type: 'number' },
            distribution: {
              type: 'object',
              additionalProperties: { type: 'number' }
            }
          }
        }
      }
    }
  };
  return schemas[dataType] || {};
}