#
tokens: 14854/50000 1/180 files (page 4/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 4. Use http://codebase.md/feed-mob/fm-mcp-servers?page={x} to view the full context.

# Directory Structure

```
├── .claude
│   └── settings.local.json
├── .cursor
│   └── rules
│       └── typescript-indentation.mdc
├── .github
│   └── workflows
│       ├── publish-civitai-records.yml
│       ├── publish-github-issues.yml
│       ├── publish-imagekit.yml
│       ├── publish-n8n-nodes-feedmob-direct-spend-visualizer.yml
│       ├── publish-reporting-packages.yml
│       └── publish-work-journals.yml
├── .gitignore
├── AGENTS.md
├── package-lock.json
├── README.md
├── src
│   ├── applovin-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── appsamurai-reporting
│   │   ├── .env.example
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── civitai-records
│   │   ├── .gitignore
│   │   ├── build.sh
│   │   ├── docker-compose.yml
│   │   ├── docs
│   │   │   └── civitai-owner-createrole.md
│   │   ├── env.sample
│   │   ├── infra
│   │   │   └── db-init
│   │   │       ├── 01-roles.sql
│   │   │       ├── 02_functions.sql
│   │   │       ├── 03_init.sql
│   │   │       ├── 04_create_prompts.sql
│   │   │       ├── 05_create_assets.sql
│   │   │       ├── 06_create_civitai_posts.sql
│   │   │       ├── 07_add_constraints_and_input_assets.sql
│   │   │       ├── 08_migration_add_on_behalf_of.sql
│   │   │       ├── 09_migration_update_functions_for_on_behalf_of.sql
│   │   │       ├── 10_update_on_behalf_of_for_existing_records.sql
│   │   │       ├── 11_create_asset_stats.sql
│   │   │       ├── 12_fix_trigger_for_tables_without_on_behalf_of.sql
│   │   │       └── 13_add_columns_to_asset_stats.sql
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── prisma
│   │   │   └── schema.prisma
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── lib
│   │   │   │   ├── __tests__
│   │   │   │   │   ├── detectRemoteAssetType.test.ts
│   │   │   │   │   └── sha256.test.ts
│   │   │   │   ├── civitaiApi.ts
│   │   │   │   ├── detectRemoteAssetType.ts
│   │   │   │   ├── handleDatabaseError.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── sha256.ts
│   │   │   ├── prompts
│   │   │   │   ├── civitai-media-engagement.md
│   │   │   │   ├── civitaiMediaEngagement.ts
│   │   │   │   ├── record-civitai-workflow.md
│   │   │   │   └── recordCivitaiWorkflow.ts
│   │   │   ├── server.ts
│   │   │   └── tools
│   │   │       ├── calculateSha256.ts
│   │   │       ├── createAsset.ts
│   │   │       ├── createCivitaiPost.ts
│   │   │       ├── createPrompt.ts
│   │   │       ├── fetchCivitaiPostAssets.ts
│   │   │       ├── findAsset.ts
│   │   │       ├── getMediaEngagementGuide.ts
│   │   │       ├── getWorkflowGuide.ts
│   │   │       ├── listCivitaiPosts.ts
│   │   │       ├── syncPostAssetStats.ts
│   │   │       └── updateAsset.ts
│   │   └── tsconfig.json
│   ├── feedmob-reporting
│   │   ├── .env.example
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── api.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── femini-reporting
│   │   ├── Dockerfile
│   │   ├── femini_mcp_guide.md
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── github-issues
│   │   ├── common
│   │   │   ├── errors.ts
│   │   │   ├── types.ts
│   │   │   ├── utils.ts
│   │   │   └── version.ts
│   │   ├── index.ts
│   │   ├── operations
│   │   │   ├── issues.ts
│   │   │   └── search.ts
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   └── tsconfig.json
│   ├── imagekit
│   │   ├── env.sample
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── server.ts
│   │   │   ├── services
│   │   │   │   ├── imageKitUpload.ts
│   │   │   │   └── imageUploader.ts
│   │   │   └── tools
│   │   │       ├── cropAndWatermark.ts
│   │   │       └── uploadFile.ts
│   │   └── tsconfig.json
│   ├── impact-radius-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── fm_impact_radius_mapping.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── yarn.lock
│   ├── inmobi-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── ironsource-aura-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── ironsource-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── jampp-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── kayzen-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   └── kayzen-client.ts
│   │   └── tsconfig.json
│   ├── liftoff-reporting
│   │   ├── .env.example
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── mintegral-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── n8n-nodes-feedmob-direct-spend-visualizer
│   │   ├── AGENTS.md
│   │   ├── credentials
│   │   │   └── FeedmobDirectSpendVisualizerApi.credentials.ts
│   │   ├── index.ts
│   │   ├── nodes
│   │   │   └── FeedmobDirectSpendVisualizer
│   │   │       ├── FeedmobDirectSpendVisualizer.node.ts
│   │   │       └── logo.svg
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   ├── build-assets.js
│   │   │   └── setup-plugin.js
│   │   ├── tsconfig.json
│   │   └── types.d.ts
│   ├── n8n-nodes-sensor-tower
│   │   ├── credentials
│   │   │   └── SensorTowerApi.credentials.ts
│   │   ├── index.ts
│   │   ├── MCP_to_n8n_Guide_zh.md
│   │   ├── nodes
│   │   │   └── SensorTower
│   │   │       ├── logo.svg
│   │   │       └── SensorTower.node.ts
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   └── types.d.ts
│   ├── rtb-house-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── samsung-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── sensor-tower-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── singular-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── smadex-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── tapjoy-reporting
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── user-activity-reporting
│   │   ├── .env.example
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── api.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── work-journals
│       ├── package-lock.json
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/sensor-tower-reporting/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fetch from "node-fetch";
import dotenv from "dotenv";

dotenv.config();

const server = new McpServer({
  name: "Sensor Tower Reporting MCP Server",
  version: "0.1.2"
});

const SENSOR_TOWER_BASE_URL = process.env.SENSOR_TOWER_BASE_URL || 'https://api.sensortower.com';
const AUTH_TOKEN = process.env.AUTH_TOKEN;

/**
 * Custom error classes
 */
class CapturedError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'CapturedError';
  }
}

class SensorTowerApiError extends CapturedError {
  constructor(message: string) {
    super(message);
    this.name = 'SensorTowerApiError';
  }
}

class ConfigurationError extends CapturedError {
  constructor(message: string) {
    super(message);
    this.name = 'ConfigurationError';
  }
}

/**
 * Utility functions
 */
function validateConfiguration(): void {
  if (!AUTH_TOKEN) {
    throw new ConfigurationError('AUTH_TOKEN environment variable is required');
  }
}

/**
 * Sensor Tower API Service Class
 */
class SensorTowerApiService {
  constructor() {
    validateConfiguration();
  }

  /**
   * Fetch app metadata from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appIds Array of app IDs to fetch metadata for (limited to 100)
   * @param country Country code (defaults to 'US')
   * @returns App metadata
   */
  async fetchAppMetadata(
    os: string,
    appIds: string[],
    country: string = 'US'
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      if (appIds.length > 100) {
        throw new SensorTowerApiError('Maximum of 100 app IDs allowed per request.');
      }

      const appIdsParam = appIds.join(',');
      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/apps?app_ids=${appIdsParam}&country=${country}&auth_token=${AUTH_TOKEN}`;

      console.error(`Fetching app metadata for ${os}, app IDs: ${appIdsParam}, country: ${country}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching app metadata:', error);
      throw new SensorTowerApiError(`Failed to fetch app metadata: ${error.message}`);
    }
  }

  /**
   * Fetch top in-app purchases for iOS apps
   * @param appIds Array of app IDs to fetch top in-app purchases for (limited to 100)
   * @param country Country code (defaults to 'US')
   * @returns Top in-app purchases data
   */
  async fetchTopInAppPurchases(
    appIds: string[],
    country: string = 'US'
  ): Promise<any> {
    try {
      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      if (appIds.length > 100) {
        throw new SensorTowerApiError('Maximum of 100 app IDs allowed per request.');
      }

      const appIdsParam = appIds.join(',');
      const url = `${SENSOR_TOWER_BASE_URL}/v1/ios/apps/top_in_app_purchases?app_ids=${appIdsParam}&country=${country}&auth_token=${AUTH_TOKEN}`;

      console.error(`Fetching top in-app purchases for app IDs: ${appIdsParam}, country: ${country}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching top in-app purchases:', error);
      throw new SensorTowerApiError(`Failed to fetch top in-app purchases: ${error.message}`);
    }
  }
  /**
   * Fetch compact sales report estimates from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param startDate Start date in YYYY-MM-DD format
   * @param endDate End date in YYYY-MM-DD format
   * @param appIds Optional array of app IDs
   * @param publisherIds Optional array of publisher IDs
   * @param unifiedAppIds Optional array of unified app IDs
   * @param unifiedPublisherIds Optional array of unified publisher IDs
   * @param categories Optional array of categories
   * @param dateGranularity Optional date granularity for aggregation
   * @param dataModel Optional data model specification
   * @returns Compact sales report estimates
   */
  async fetchCompactSalesReportEstimates(
    os: string,
    startDate: string,
    endDate: string,
    appIds?: string[],
    publisherIds?: string[],
    unifiedAppIds?: string[],
    unifiedPublisherIds?: string[],
    categories?: string[],
    dateGranularity?: string,
    dataModel?: string
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      // Validate dates
      const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
      if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
        throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
      }

      // Validate that at least one of app_ids, publisher_ids, unified_app_ids, unified_publisher_ids, or categories is provided
      if (
        (!appIds || appIds.length === 0) &&
        (!publisherIds || publisherIds.length === 0) &&
        (!unifiedAppIds || unifiedAppIds.length === 0) &&
        (!unifiedPublisherIds || unifiedPublisherIds.length === 0) &&
        (!categories || categories.length === 0)
      ) {
        throw new SensorTowerApiError('At least one of App ID, Publisher ID, Unified App ID, Unified Publisher ID, or Category is required.');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('start_date', startDate);
      queryParams.append('end_date', endDate);
      queryParams.append('auth_token', AUTH_TOKEN!);

      // Add optional parameters if provided
      if (appIds && appIds.length > 0) {
        queryParams.append('app_ids', appIds.join(','));
      }

      if (publisherIds && publisherIds.length > 0) {
        publisherIds.forEach(id => {
          queryParams.append('publisher_ids[]', id);
        });
      }

      if (unifiedAppIds && unifiedAppIds.length > 0) {
        queryParams.append('unified_app_ids', unifiedAppIds.join(','));
      }

      if (unifiedPublisherIds && unifiedPublisherIds.length > 0) {
        queryParams.append('unified_publisher_ids', unifiedPublisherIds.join(','));
      }

      if (categories && categories.length > 0) {
        queryParams.append('categories', categories.join(','));
      }

      if (dateGranularity) {
        queryParams.append('date_granularity', dateGranularity);
      }

      if (dataModel) {
        queryParams.append('data_model', dataModel);
      }

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/compact_sales_report_estimates?${queryParams.toString()}`;

      console.error(`Fetching compact sales report estimates for ${os}, start date: ${startDate}, end date: ${endDate}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching compact sales report estimates:', error);
      throw new SensorTowerApiError(`Failed to fetch compact sales report estimates: ${error.message}`);
    }
  }

  /**
   * Fetch active user estimates from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appIds Array of app IDs (maximum 500)
   * @param timePeriod Time period for aggregation ('day', 'week', or 'month')
   * @param startDate Start date in YYYY-MM-DD format
   * @param endDate End date in YYYY-MM-DD format
   * @param countries Optional array of country codes
   * @param dataModel Optional data model specification
   * @returns Active user estimates
   */
  async fetchActiveUsers(
    os: string,
    appIds: string[],
    timePeriod: string,
    startDate: string,
    endDate: string,
    countries?: string[],
    dataModel?: string
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      if (appIds.length > 500) {
        throw new SensorTowerApiError('Maximum of 500 app IDs allowed per request.');
      }

      if (!['day', 'week', 'month'].includes(timePeriod.toLowerCase())) {
        throw new SensorTowerApiError('Invalid time period. Must be "day", "week", or "month".');
      }

      // Validate dates
      const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
      if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
        throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('app_ids', appIds.join(','));
      queryParams.append('time_period', timePeriod);
      queryParams.append('start_date', startDate);
      queryParams.append('end_date', endDate);
      queryParams.append('auth_token', AUTH_TOKEN!);

      // Add optional parameters if provided
      if (countries && countries.length > 0) {
        queryParams.append('countries', countries.join(','));
      }

      if (dataModel) {
        queryParams.append('data_model', dataModel);
      }

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/usage/active_users?${queryParams.toString()}`;

      console.error(`Fetching active users for ${os}, app IDs: ${appIds.join(',')}, time period: ${timePeriod}, start date: ${startDate}, end date: ${endDate}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching active users:', error);
      throw new SensorTowerApiError(`Failed to fetch active users: ${error.message}`);
    }
  }

  /**
   * Fetch category ranking history from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appIds Array of app IDs
   * @param category Category ID
   * @param chartTypeIds Array of chart type IDs
   * @param countries Array of country codes
   * @param startDate Optional start date in YYYY-MM-DD format (defaults to 90 days ago)
   * @param endDate Optional end date in YYYY-MM-DD format (defaults to today)
   * @param isHourly Optional boolean for hourly rankings (only for iOS)
   * @returns Category ranking history
   */
  async fetchCategoryHistory(
    os: string,
    appIds: string[],
    category: string,
    chartTypeIds: string[],
    countries: string[],
    startDate?: string,
    endDate?: string,
    isHourly?: boolean
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      if (!category) {
        throw new SensorTowerApiError('Category ID is required.');
      }

      if (!chartTypeIds || chartTypeIds.length === 0) {
        throw new SensorTowerApiError('At least one chart type ID is required.');
      }

      if (!countries || countries.length === 0) {
        throw new SensorTowerApiError('At least one country code is required.');
      }

      // Validate dates if provided
      const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
      if (startDate && !dateRegex.test(startDate)) {
        throw new SensorTowerApiError('Start date must be in YYYY-MM-DD format.');
      }
      if (endDate && !dateRegex.test(endDate)) {
        throw new SensorTowerApiError('End date must be in YYYY-MM-DD format.');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('app_ids', appIds.join(','));
      queryParams.append('category', category);
      queryParams.append('chart_type_ids', chartTypeIds.join(','));
      queryParams.append('countries', countries.join(','));
      queryParams.append('auth_token', AUTH_TOKEN!);

      // Add optional parameters if provided
      if (startDate) {
        queryParams.append('start_date', startDate);
      }

      if (endDate) {
        queryParams.append('end_date', endDate);
      }

      if (isHourly !== undefined) {
        queryParams.append('is_hourly', isHourly.toString());
      }

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/category/category_history?${queryParams.toString()}`;

      console.error(`Fetching category history for ${os}, app IDs: ${appIds.join(',')}, category: ${category}, chart types: ${chartTypeIds.join(',')}, countries: ${countries.join(',')}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching category history:', error);
      throw new SensorTowerApiError(`Failed to fetch category history: ${error.message}`);
    }
  }

  /**
   * Fetch category ranking summary from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appId App ID
   * @param country Country code
   * @returns Category ranking summary
   */
  async fetchCategoryRankingSummary(
    os: string,
    appId: string,
    country: string
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appId) {
        throw new SensorTowerApiError('App ID is required.');
      }

      if (!country) {
        throw new SensorTowerApiError('Country code is required.');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('app_id', appId);
      queryParams.append('country', country);
      queryParams.append('auth_token', AUTH_TOKEN!);

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/category/category_ranking_summary?${queryParams.toString()}`;

      console.error(`Fetching category ranking summary for ${os}, app ID: ${appId}, country: ${country}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching category ranking summary:', error);
      throw new SensorTowerApiError(`Failed to fetch category ranking summary: ${error.message}`);
    }
  }

  /**
   * Fetch network analysis (impressions share of voice) from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appIds Array of app IDs to fetch SOV for
   * @param startDate Start date in YYYY-MM-DD format
   * @param endDate End date in YYYY-MM-DD format
   * @param period Time period to calculate Share of Voice for ('day')
   * @param networks Optional array of networks to return results for
   * @param countries Optional array of country codes to return results for
   * @returns Network analysis data
   */
  async fetchNetworkAnalysis(
    os: string,
    appIds: string[],
    startDate: string,
    endDate: string,
    period: string,
    networks?: string[],
    countries?: string[]
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      // Validate dates
      const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
      if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
        throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
      }

      // Validate period
      if (period !== 'day') {
        throw new SensorTowerApiError('Period must be "day".');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('app_ids', appIds.join(','));
      queryParams.append('start_date', startDate);
      queryParams.append('end_date', endDate);
      queryParams.append('period', period);
      queryParams.append('auth_token', AUTH_TOKEN!);

      // Add optional parameters if provided
      if (networks && networks.length > 0) {
        queryParams.append('networks', networks.join(','));
      }

      if (countries && countries.length > 0) {
        queryParams.append('countries', countries.join(','));
      }

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/ad_intel/network_analysis?${queryParams.toString()}`;

      console.error(`Fetching network analysis for ${os}, app IDs: ${appIds.join(',')}, start date: ${startDate}, end date: ${endDate}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching network analysis:', error);
      throw new SensorTowerApiError(`Failed to fetch network analysis: ${error.message}`);
    }
  }

  /**
   * Fetch network analysis rank data from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appIds Array of app IDs to fetch ranks for
   * @param startDate Start date in YYYY-MM-DD format
   * @param endDate End date in YYYY-MM-DD format
   * @param period Time period to calculate ranks for ('day')
   * @param networks Optional array of networks to return results for
   * @param countries Optional array of country codes to return results for
   * @returns Network analysis rank data
   */
  async fetchNetworkAnalysisRank(
    os: string,
    appIds: string[],
    startDate: string,
    endDate: string,
    period: string,
    networks?: string[],
    countries?: string[]
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      // Validate dates
      const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
      if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
        throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
      }

      // Validate period
      if (period !== 'day') {
        throw new SensorTowerApiError('Period must be "day".');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('app_ids', appIds.join(','));
      queryParams.append('start_date', startDate);
      queryParams.append('end_date', endDate);
      queryParams.append('period', period);
      queryParams.append('auth_token', AUTH_TOKEN!);

      // Add optional parameters if provided
      if (networks && networks.length > 0) {
        queryParams.append('networks', networks.join(','));
      }

      if (countries && countries.length > 0) {
        queryParams.append('countries', countries.join(','));
      }

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/ad_intel/network_analysis/rank?${queryParams.toString()}`;

      console.error(`Fetching network analysis rank for ${os}, app IDs: ${appIds.join(',')}, start date: ${startDate}, end date: ${endDate}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching network analysis rank:', error);
      throw new SensorTowerApiError(`Failed to fetch network analysis rank: ${error.message}`);
    }
  }

  /**
   * Fetch retention data for apps from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appIds Array of app IDs (maximum 500)
   * @param dateGranularity Aggregate estimates by granularity ('all_time' or 'quarterly')
   * @param startDate Start date in YYYY-MM-DD format
   * @param endDate Optional end date in YYYY-MM-DD format
   * @param country Optional country code (defaults to Worldwide)
   * @returns Retention data for apps
   */
  async fetchRetention(
    os: string,
    appIds: string[],
    dateGranularity: string,
    startDate: string,
    endDate?: string,
    country?: string
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      if (appIds.length > 500) {
        throw new SensorTowerApiError('Maximum of 500 app IDs allowed per request.');
      }

      if (!['all_time', 'quarterly'].includes(dateGranularity)) {
        throw new SensorTowerApiError('Invalid date granularity. Must be "all_time" or "quarterly".');
      }

      // Validate dates
      const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
      if (!dateRegex.test(startDate)) {
        throw new SensorTowerApiError('Start date must be in YYYY-MM-DD format.');
      }
      
      if (endDate && !dateRegex.test(endDate)) {
        throw new SensorTowerApiError('End date must be in YYYY-MM-DD format.');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('app_ids', appIds.join(','));
      queryParams.append('date_granularity', dateGranularity);
      queryParams.append('start_date', startDate);
      queryParams.append('auth_token', AUTH_TOKEN!);

      // Add optional parameters if provided
      if (endDate) {
        queryParams.append('end_date', endDate);
      }

      if (country) {
        queryParams.append('country', country);
      }

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/usage/retention?${queryParams.toString()}`;

      console.error(`Fetching retention data for ${os}, app IDs: ${appIds.join(',')}, date granularity: ${dateGranularity}, start date: ${startDate}${endDate ? `, end date: ${endDate}` : ''}${country ? `, country: ${country}` : ''}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching retention data:', error);
      throw new SensorTowerApiError(`Failed to fetch retention data: ${error.message}`);
    }
  }

  /**
   * Fetch app downloads by sources from Sensor Tower API
   * @param os Operating system ('ios' or 'android')
   * @param appIds Array of unified app IDs
   * @param countries Array of country codes
   * @param startDate Start date in YYYY-MM-DD format
   * @param endDate End date in YYYY-MM-DD format
   * @param dateGranularity Optional date granularity for aggregation ('daily' or 'monthly')
   * @returns Downloads by sources data
   */
  async fetchDownloadsBySources(
    os: string,
    appIds: string[],
    countries: string[],
    startDate: string,
    endDate: string,
    dateGranularity?: string
  ): Promise<any> {
    try {
      if (!['ios', 'android'].includes(os.toLowerCase())) {
        throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
      }

      if (!appIds || appIds.length === 0) {
        throw new SensorTowerApiError('At least one app ID is required.');
      }

      if (!countries || countries.length === 0) {
        throw new SensorTowerApiError('At least one country code is required.');
      }

      // Validate dates
      const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
      if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
        throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
      }

      // Validate date granularity if provided
      if (dateGranularity && !['daily', 'monthly'].includes(dateGranularity)) {
        throw new SensorTowerApiError('Date granularity must be "daily" or "monthly".');
      }

      // Build URL with query parameters
      const queryParams = new URLSearchParams();
      queryParams.append('app_ids', appIds.join(','));
      queryParams.append('countries', countries.join(','));
      queryParams.append('start_date', startDate);
      queryParams.append('end_date', endDate);
      queryParams.append('auth_token', AUTH_TOKEN!);

      // Add optional parameters if provided
      if (dateGranularity) {
        queryParams.append('date_granularity', dateGranularity);
      }

      const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/downloads_by_sources?${queryParams.toString()}`;

      console.error(`Fetching downloads by sources for ${os}, app IDs: ${appIds.join(',')}, countries: ${countries.join(',')}, start date: ${startDate}, end date: ${endDate}`);

      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        }
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorBody}`);
      }

      const data = await response.json();
      return data;
    } catch (error: any) {
      console.error('Error fetching downloads by sources:', error);
      throw new SensorTowerApiError(`Failed to fetch downloads by sources: ${error.message}`);
    }
  }
}

// Input validation schemas
const osSchema = z.string()
  .refine(val => ['ios', 'android'].includes(val.toLowerCase()), "OS must be 'ios' or 'android'")
  .describe("Operating System ('ios' or 'android')");

const appIdsSchema = z.string()
  .describe("App IDs of apps, separated by commas (limited to 100)")
  .refine(val => val.split(',').length <= 100, "Maximum of 100 app IDs allowed");

const countrySchema = z.string().optional().default('US')
  .describe("Country Code (defaults to 'US')");

// Schemas for compact sales report estimates
const dateSchema = z.string()
  .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format")
  .describe("Date in YYYY-MM-DD format");

const appIdsOptionalSchema = z.string().optional()
  .describe("IDs of apps, separated by commas (limited to 100)");

const publisherIdsSchema = z.array(z.string()).optional()
  .describe("Publisher IDs of apps");

const unifiedAppIdsSchema = z.string().optional()
  .describe("IDs of unified apps, separated by commas");

const unifiedPublisherIdsSchema = z.string().optional()
  .describe("IDs of unified publishers, separated by commas");

const categoriesSchema = z.string().optional()
  .describe("Categories, separated by commas, see Category IDs");

const dateGranularitySchema = z.string().optional()
  .describe("Aggregate estimates by granularity");

const dataModelSchema = z.string().optional()
  .describe("Specify the data model used to generate estimates");

// Schemas for active users
const timePeriodSchema = z.string()
  .refine(val => ['day', 'week', 'month'].includes(val.toLowerCase()), "Time period must be 'day', 'week', or 'month'")
  .describe("Aggregate estimates by time period ('day' for DAU, 'week' for WAU, 'month' for MAU)");

const appIdsRequiredSchema = z.string()
  .describe("IDs of apps, separated by commas (maximum 500)")
  .refine(val => val.split(',').length <= 500, "Maximum of 500 app IDs allowed");

const countriesSchema = z.string().optional()
  .describe("Countries to return results for, separated by commas, Country Codes");

// Schemas for downloads by sources
const unifiedAppIdsRequiredSchema = z.string()
  .describe("Unified app IDs, separated by commas");

const countriesRequiredForDownloadsSchema = z.string()
  .describe("Country codes, separated by commas. For worldwide data, use 'WW'.");

const dateGranularityDownloadsSchema = z.string().optional()
  .describe("Aggregate estimates by granularity (use 'daily' or 'monthly'). Defaults to 'monthly'.");

// Schemas for category history
const categorySchema = z.string()
  .describe("Category ID to return results for");

const chartTypeIdsSchema = z.string()
  .describe("IDs of the Chart Type, separated by commas");

const countriesRequiredSchema = z.string()
  .describe("Countries to return results for, separated by commas, Country Codes");

const isHourlySchema = z.boolean().optional()
  .describe("Hourly rankings (only for iOS)");

// Schemas for category ranking summary
const appIdSchema = z.string()
  .describe("ID of App");

const countryRequiredSchema = z.string()
  .describe("Country code to return results for");

// Schemas for network analysis
const networkAnalysisPeriodSchema = z.string()
  .refine(val => val === 'day', "Period must be 'day'")
  .describe("Time period to calculate Share of Voice for");

const networksSchema = z.string().optional()
  .describe("Networks to return results for, separated by commas (Networks)");

// Schemas for retention data
const retentionDateGranularitySchema = z.string()
  .refine(val => ['all_time', 'quarterly'].includes(val), "Date granularity must be 'all_time' or 'quarterly'")
  .describe("Aggregate estimates by granularity (use 'all_time', or 'quarterly')");

// Tool: Get App Metadata
server.tool("get_app_metadata",
  "Fetch app metadata from Sensor Tower API, such as app name, publisher, categories, description, screenshots, rating, etc.",
  {
    os: osSchema,
    appIds: appIdsSchema,
    country: countrySchema
  },
  async ({ os, appIds, country = 'US' }) => {
    try {
      console.error(`Fetching app metadata for ${os}, app IDs: ${appIds}, country: ${country}`);
      
      const appIdsArray = appIds.split(',').map(id => id.trim());
      const sensorTowerService = new SensorTowerApiService();
      const metadata = await sensorTowerService.fetchAppMetadata(os, appIdsArray, country);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              country,
              appIds: appIdsArray,
              data: metadata
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching app metadata: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              country,
              appIds: appIds.split(',').map(id => id.trim()),
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Top In-App Purchases
server.tool("get_top_in_app_purchases",
  "Fetches the top in-app purchases for particular iOS apps",
  {
    appIds: appIdsSchema,
    country: countrySchema
  },
  async ({ appIds, country = 'US' }) => {
    try {
      console.error(`Fetching top in-app purchases for app IDs: ${appIds}, country: ${country}`);
      
      const appIdsArray = appIds.split(',').map(id => id.trim());
      const sensorTowerService = new SensorTowerApiService();
      const inAppPurchasesData = await sensorTowerService.fetchTopInAppPurchases(appIdsArray, country);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              country,
              appIds: appIdsArray,
              data: inAppPurchasesData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching top in-app purchases: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              country,
              appIds: appIds.split(',').map(id => id.trim()),
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Compact Sales Report Estimates
server.tool("get_compact_sales_report_estimates",
  "Fetches download and revenue estimates of apps and publishers in compact format. All revenues are returned in cents.",
  {
    os: osSchema,
    startDate: dateSchema,
    endDate: dateSchema,
    appIds: appIdsOptionalSchema,
    publisherIds: publisherIdsSchema,
    unifiedAppIds: unifiedAppIdsSchema,
    unifiedPublisherIds: unifiedPublisherIdsSchema,
    categories: categoriesSchema,
    dateGranularity: dateGranularitySchema,
    dataModel: dataModelSchema
  },
  async ({ os, startDate, endDate, appIds, publisherIds, unifiedAppIds, unifiedPublisherIds, categories, dateGranularity, dataModel }) => {
    try {
      console.error(`Fetching compact sales report estimates for ${os}, start date: ${startDate}, end date: ${endDate}`);
      
      // Process string inputs into arrays
      const appIdsArray = appIds ? appIds.split(',').map(id => id.trim()) : undefined;
      const unifiedAppIdsArray = unifiedAppIds ? unifiedAppIds.split(',').map(id => id.trim()) : undefined;
      const unifiedPublisherIdsArray = unifiedPublisherIds ? unifiedPublisherIds.split(',').map(id => id.trim()) : undefined;
      const categoriesArray = categories ? categories.split(',').map(category => category.trim()) : undefined;
      
      const sensorTowerService = new SensorTowerApiService();
      const reportData = await sensorTowerService.fetchCompactSalesReportEstimates(
        os,
        startDate,
        endDate,
        appIdsArray,
        publisherIds,
        unifiedAppIdsArray,
        unifiedPublisherIdsArray,
        categoriesArray,
        dateGranularity,
        dataModel
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              startDate,
              endDate,
              appIds: appIdsArray,
              publisherIds,
              unifiedAppIds: unifiedAppIdsArray,
              unifiedPublisherIds: unifiedPublisherIdsArray,
              categories: categoriesArray,
              dateGranularity,
              dataModel,
              data: reportData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching compact sales report estimates: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              startDate,
              endDate,
              appIds: appIds ? appIds.split(',').map(id => id.trim()) : undefined,
              publisherIds,
              unifiedAppIds: unifiedAppIds ? unifiedAppIds.split(',').map(id => id.trim()) : undefined,
              unifiedPublisherIds: unifiedPublisherIds ? unifiedPublisherIds.split(',').map(id => id.trim()) : undefined,
              categories: categories ? categories.split(',').map(category => category.trim()) : undefined,
              dateGranularity,
              dataModel,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Active Users
server.tool("get_active_users",
  "Fetches active user estimates of apps per country by date and time period.",
  {
    os: osSchema,
    appIds: appIdsRequiredSchema,
    timePeriod: timePeriodSchema,
    startDate: dateSchema,
    endDate: dateSchema,
    countries: countriesSchema,
    dataModel: dataModelSchema
  },
  async ({ os, appIds, timePeriod, startDate, endDate, countries, dataModel }) => {
    try {
      console.error(`Fetching active users for ${os}, app IDs: ${appIds}, time period: ${timePeriod}, start date: ${startDate}, end date: ${endDate}`);
      
      // Process string inputs into arrays
      const appIdsArray = appIds.split(',').map(id => id.trim());
      const countriesArray = countries ? countries.split(',').map(country => country.trim()) : undefined;
      
      const sensorTowerService = new SensorTowerApiService();
      const activeUsersData = await sensorTowerService.fetchActiveUsers(
        os,
        appIdsArray,
        timePeriod,
        startDate,
        endDate,
        countriesArray,
        dataModel
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              appIds: appIdsArray,
              timePeriod,
              startDate,
              endDate,
              countries: countriesArray,
              dataModel,
              data: activeUsersData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching active users: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              appIds: appIds.split(',').map(id => id.trim()),
              timePeriod,
              startDate,
              endDate,
              countries: countries ? countries.split(',').map(country => country.trim()) : undefined,
              dataModel,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Category History
server.tool("get_category_history",
  "Fetches detailed category ranking history of a particular app, category, and chart type.",
  {
    os: osSchema,
    appIds: appIdsRequiredSchema,
    category: categorySchema,
    chartTypeIds: chartTypeIdsSchema,
    countries: countriesRequiredSchema,
    startDate: dateSchema.optional(),
    endDate: dateSchema.optional(),
    isHourly: isHourlySchema
  },
  async ({ os, appIds, category, chartTypeIds, countries, startDate, endDate, isHourly }) => {
    try {
      console.error(`Fetching category history for ${os}, app IDs: ${appIds}, category: ${category}, chart types: ${chartTypeIds}, countries: ${countries}`);
      
      // Process string inputs into arrays
      const appIdsArray = appIds.split(',').map(id => id.trim());
      const chartTypeIdsArray = chartTypeIds.split(',').map(id => id.trim());
      const countriesArray = countries.split(',').map(country => country.trim());
      
      const sensorTowerService = new SensorTowerApiService();
      const categoryHistoryData = await sensorTowerService.fetchCategoryHistory(
        os,
        appIdsArray,
        category,
        chartTypeIdsArray,
        countriesArray,
        startDate,
        endDate,
        isHourly
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              appIds: appIdsArray,
              category,
              chartTypeIds: chartTypeIdsArray,
              countries: countriesArray,
              startDate,
              endDate,
              isHourly,
              data: categoryHistoryData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching category history: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              appIds: appIds.split(',').map(id => id.trim()),
              category,
              chartTypeIds: chartTypeIds.split(',').map(id => id.trim()),
              countries: countries.split(',').map(country => country.trim()),
              startDate,
              endDate,
              isHourly,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Category Ranking Summary
server.tool("get_category_ranking_summary",
  "Fetches today's category ranking summary of a particular app with data on chart type, category, and rank.",
  {
    os: osSchema,
    appId: appIdSchema,
    country: countryRequiredSchema
  },
  async ({ os, appId, country }) => {
    try {
      console.error(`Fetching category ranking summary for ${os}, app ID: ${appId}, country: ${country}`);
      
      const sensorTowerService = new SensorTowerApiService();
      const rankingSummaryData = await sensorTowerService.fetchCategoryRankingSummary(
        os,
        appId,
        country
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              appId,
              country,
              data: rankingSummaryData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching category ranking summary: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              appId,
              country,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Network Analysis
server.tool("get_network_analysis",
  "Fetches the impressions share of voice (SOV) time series of the requested apps.",
  {
    os: osSchema,
    appIds: appIdsRequiredSchema,
    startDate: dateSchema,
    endDate: dateSchema,
    period: networkAnalysisPeriodSchema,
    networks: networksSchema,
    countries: countriesSchema
  },
  async ({ os, appIds, startDate, endDate, period, networks, countries }) => {
    try {
      console.error(`Fetching network analysis for ${os}, app IDs: ${appIds}, start date: ${startDate}, end date: ${endDate}`);
      
      // Process string inputs into arrays
      const appIdsArray = appIds.split(',').map(id => id.trim());
      const networksArray = networks ? networks.split(',').map(network => network.trim()) : undefined;
      const countriesArray = countries ? countries.split(',').map(country => country.trim()) : undefined;
      
      const sensorTowerService = new SensorTowerApiService();
      const networkAnalysisData = await sensorTowerService.fetchNetworkAnalysis(
        os,
        appIdsArray,
        startDate,
        endDate,
        period,
        networksArray,
        countriesArray
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              appIds: appIdsArray,
              startDate,
              endDate,
              period,
              networks: networksArray,
              countries: countriesArray,
              data: networkAnalysisData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching network analysis: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              appIds: appIds.split(',').map(id => id.trim()),
              startDate,
              endDate,
              period,
              networks: networks ? networks.split(',').map(network => network.trim()) : undefined,
              countries: countries ? countries.split(',').map(country => country.trim()) : undefined,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Network Analysis Rank
server.tool("get_network_analysis_rank",
  "Fetches the ranks for the countries, networks and dates of the requested apps.",
  {
    os: osSchema,
    appIds: appIdsRequiredSchema,
    startDate: dateSchema,
    endDate: dateSchema,
    period: networkAnalysisPeriodSchema,
    networks: networksSchema,
    countries: countriesSchema
  },
  async ({ os, appIds, startDate, endDate, period, networks, countries }) => {
    try {
      console.error(`Fetching network analysis rank for ${os}, app IDs: ${appIds}, start date: ${startDate}, end date: ${endDate}`);
      
      // Process string inputs into arrays
      const appIdsArray = appIds.split(',').map(id => id.trim());
      const networksArray = networks ? networks.split(',').map(network => network.trim()) : undefined;
      const countriesArray = countries ? countries.split(',').map(country => country.trim()) : undefined;
      
      const sensorTowerService = new SensorTowerApiService();
      const networkAnalysisRankData = await sensorTowerService.fetchNetworkAnalysisRank(
        os,
        appIdsArray,
        startDate,
        endDate,
        period,
        networksArray,
        countriesArray
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              appIds: appIdsArray,
              startDate,
              endDate,
              period,
              networks: networksArray,
              countries: countriesArray,
              data: networkAnalysisRankData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching network analysis rank: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              appIds: appIds.split(',').map(id => id.trim()),
              startDate,
              endDate,
              period,
              networks: networks ? networks.split(',').map(network => network.trim()) : undefined,
              countries: countries ? countries.split(',').map(country => country.trim()) : undefined,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Retention
server.tool("get_retention",
  "Fetches retention of apps (from day 1 to day 90), along with the baseline retention.",
  {
    os: osSchema,
    appIds: appIdsRequiredSchema,
    date_granularity: retentionDateGranularitySchema,
    start_date: dateSchema,
    end_date: dateSchema.optional(),
    country: countrySchema.optional()
  },
  async ({ os, appIds, date_granularity, start_date, end_date, country }) => {
    try {
      console.error(`Fetching retention data for ${os}, app IDs: ${appIds}, date granularity: ${date_granularity}, start date: ${start_date}${end_date ? `, end date: ${end_date}` : ''}${country ? `, country: ${country}` : ''}`);
      
      // Process string inputs into arrays
      const appIdsArray = appIds.split(',').map(id => id.trim());
      
      const sensorTowerService = new SensorTowerApiService();
      const retentionData = await sensorTowerService.fetchRetention(
        os,
        appIdsArray,
        date_granularity,
        start_date,
        end_date,
        country
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              appIds: appIdsArray,
              date_granularity,
              start_date,
              end_date,
              country,
              data: retentionData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching retention data: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              appIds: appIds.split(',').map(id => id.trim()),
              date_granularity,
              start_date,
              end_date,
              country,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

// Tool: Get Downloads By Sources
server.tool("get_downloads_by_sources",
  "Fetches app downloads by sources (organic, paid, and browser) with percentages and absolute values.",
  {
    os: osSchema,
    app_ids: unifiedAppIdsRequiredSchema,
    countries: countriesRequiredForDownloadsSchema,
    start_date: dateSchema,
    end_date: dateSchema,
    date_granularity: dateGranularityDownloadsSchema
  },
  async ({ os, app_ids, countries, start_date, end_date, date_granularity }) => {
    try {
      console.error(`Fetching downloads by sources for ${os}, app IDs: ${app_ids}, countries: ${countries}, start date: ${start_date}, end date: ${end_date}`);
      
      // Process string inputs into arrays
      const appIdsArray = app_ids.split(',').map(id => id.trim());
      const countriesArray = countries.split(',').map(country => country.trim());
      
      const sensorTowerService = new SensorTowerApiService();
      const downloadsBySourcesData = await sensorTowerService.fetchDownloadsBySources(
        os,
        appIdsArray,
        countriesArray,
        start_date,
        end_date,
        date_granularity
      );
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              os,
              app_ids: appIdsArray,
              countries: countriesArray,
              start_date,
              end_date,
              date_granularity,
              data: downloadsBySourcesData
            }, null, 2)
          }
        ]
      };
    } catch (error: any) {
      const errorMessage = `Error fetching downloads by sources: ${error.message}`;
      console.error(errorMessage);
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: errorMessage,
              os,
              app_ids: app_ids.split(',').map(id => id.trim()),
              countries: countries.split(',').map(country => country.trim()),
              start_date,
              end_date,
              date_granularity,
              timestamp: new Date().toISOString()
            }, null, 2)
          }
        ],
        isError: true
      };
    }
  }
);

async function runServer(): Promise<void> {
  try {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("Sensor Tower Reporting MCP Server running on stdio");
  } catch (error) {
    console.error("Failed to start server:", error);
    throw error;
  }
}

// Graceful shutdown handling
process.on('SIGINT', () => {
  console.error('Received SIGINT, shutting down gracefully...');
  process.exit(0);
});

process.on('SIGTERM', () => {
  console.error('Received SIGTERM, shutting down gracefully...');
  process.exit(0);
});

runServer().catch((error) => {
  console.error("Fatal error running server:", error);
  process.exit(1);
});

```
Page 4/4FirstPrevNextLast