#
tokens: 20067/50000 1/180 files (page 5/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 of 5. Use http://codebase.md/feed-mob/fm-mcp-servers?lines=true&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
   1 | #!/usr/bin/env node
   2 | 
   3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
   4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   5 | import { z } from "zod";
   6 | import fetch from "node-fetch";
   7 | import dotenv from "dotenv";
   8 | 
   9 | dotenv.config();
  10 | 
  11 | const server = new McpServer({
  12 |   name: "Sensor Tower Reporting MCP Server",
  13 |   version: "0.1.2"
  14 | });
  15 | 
  16 | const SENSOR_TOWER_BASE_URL = process.env.SENSOR_TOWER_BASE_URL || 'https://api.sensortower.com';
  17 | const AUTH_TOKEN = process.env.AUTH_TOKEN;
  18 | 
  19 | /**
  20 |  * Custom error classes
  21 |  */
  22 | class CapturedError extends Error {
  23 |   constructor(message: string) {
  24 |     super(message);
  25 |     this.name = 'CapturedError';
  26 |   }
  27 | }
  28 | 
  29 | class SensorTowerApiError extends CapturedError {
  30 |   constructor(message: string) {
  31 |     super(message);
  32 |     this.name = 'SensorTowerApiError';
  33 |   }
  34 | }
  35 | 
  36 | class ConfigurationError extends CapturedError {
  37 |   constructor(message: string) {
  38 |     super(message);
  39 |     this.name = 'ConfigurationError';
  40 |   }
  41 | }
  42 | 
  43 | /**
  44 |  * Utility functions
  45 |  */
  46 | function validateConfiguration(): void {
  47 |   if (!AUTH_TOKEN) {
  48 |     throw new ConfigurationError('AUTH_TOKEN environment variable is required');
  49 |   }
  50 | }
  51 | 
  52 | /**
  53 |  * Sensor Tower API Service Class
  54 |  */
  55 | class SensorTowerApiService {
  56 |   constructor() {
  57 |     validateConfiguration();
  58 |   }
  59 | 
  60 |   /**
  61 |    * Fetch app metadata from Sensor Tower API
  62 |    * @param os Operating system ('ios' or 'android')
  63 |    * @param appIds Array of app IDs to fetch metadata for (limited to 100)
  64 |    * @param country Country code (defaults to 'US')
  65 |    * @returns App metadata
  66 |    */
  67 |   async fetchAppMetadata(
  68 |     os: string,
  69 |     appIds: string[],
  70 |     country: string = 'US'
  71 |   ): Promise<any> {
  72 |     try {
  73 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
  74 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
  75 |       }
  76 | 
  77 |       if (!appIds || appIds.length === 0) {
  78 |         throw new SensorTowerApiError('At least one app ID is required.');
  79 |       }
  80 | 
  81 |       if (appIds.length > 100) {
  82 |         throw new SensorTowerApiError('Maximum of 100 app IDs allowed per request.');
  83 |       }
  84 | 
  85 |       const appIdsParam = appIds.join(',');
  86 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/apps?app_ids=${appIdsParam}&country=${country}&auth_token=${AUTH_TOKEN}`;
  87 | 
  88 |       console.error(`Fetching app metadata for ${os}, app IDs: ${appIdsParam}, country: ${country}`);
  89 | 
  90 |       const response = await fetch(url, {
  91 |         method: 'GET',
  92 |         headers: {
  93 |           'Content-Type': 'application/json',
  94 |           'Accept': 'application/json'
  95 |         }
  96 |       });
  97 | 
  98 |       if (!response.ok) {
  99 |         const errorBody = await response.text();
 100 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 101 |       }
 102 | 
 103 |       const data = await response.json();
 104 |       return data;
 105 |     } catch (error: any) {
 106 |       console.error('Error fetching app metadata:', error);
 107 |       throw new SensorTowerApiError(`Failed to fetch app metadata: ${error.message}`);
 108 |     }
 109 |   }
 110 | 
 111 |   /**
 112 |    * Fetch top in-app purchases for iOS apps
 113 |    * @param appIds Array of app IDs to fetch top in-app purchases for (limited to 100)
 114 |    * @param country Country code (defaults to 'US')
 115 |    * @returns Top in-app purchases data
 116 |    */
 117 |   async fetchTopInAppPurchases(
 118 |     appIds: string[],
 119 |     country: string = 'US'
 120 |   ): Promise<any> {
 121 |     try {
 122 |       if (!appIds || appIds.length === 0) {
 123 |         throw new SensorTowerApiError('At least one app ID is required.');
 124 |       }
 125 | 
 126 |       if (appIds.length > 100) {
 127 |         throw new SensorTowerApiError('Maximum of 100 app IDs allowed per request.');
 128 |       }
 129 | 
 130 |       const appIdsParam = appIds.join(',');
 131 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/ios/apps/top_in_app_purchases?app_ids=${appIdsParam}&country=${country}&auth_token=${AUTH_TOKEN}`;
 132 | 
 133 |       console.error(`Fetching top in-app purchases for app IDs: ${appIdsParam}, country: ${country}`);
 134 | 
 135 |       const response = await fetch(url, {
 136 |         method: 'GET',
 137 |         headers: {
 138 |           'Content-Type': 'application/json',
 139 |           'Accept': 'application/json'
 140 |         }
 141 |       });
 142 | 
 143 |       if (!response.ok) {
 144 |         const errorBody = await response.text();
 145 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 146 |       }
 147 | 
 148 |       const data = await response.json();
 149 |       return data;
 150 |     } catch (error: any) {
 151 |       console.error('Error fetching top in-app purchases:', error);
 152 |       throw new SensorTowerApiError(`Failed to fetch top in-app purchases: ${error.message}`);
 153 |     }
 154 |   }
 155 |   /**
 156 |    * Fetch compact sales report estimates from Sensor Tower API
 157 |    * @param os Operating system ('ios' or 'android')
 158 |    * @param startDate Start date in YYYY-MM-DD format
 159 |    * @param endDate End date in YYYY-MM-DD format
 160 |    * @param appIds Optional array of app IDs
 161 |    * @param publisherIds Optional array of publisher IDs
 162 |    * @param unifiedAppIds Optional array of unified app IDs
 163 |    * @param unifiedPublisherIds Optional array of unified publisher IDs
 164 |    * @param categories Optional array of categories
 165 |    * @param dateGranularity Optional date granularity for aggregation
 166 |    * @param dataModel Optional data model specification
 167 |    * @returns Compact sales report estimates
 168 |    */
 169 |   async fetchCompactSalesReportEstimates(
 170 |     os: string,
 171 |     startDate: string,
 172 |     endDate: string,
 173 |     appIds?: string[],
 174 |     publisherIds?: string[],
 175 |     unifiedAppIds?: string[],
 176 |     unifiedPublisherIds?: string[],
 177 |     categories?: string[],
 178 |     dateGranularity?: string,
 179 |     dataModel?: string
 180 |   ): Promise<any> {
 181 |     try {
 182 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 183 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 184 |       }
 185 | 
 186 |       // Validate dates
 187 |       const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 188 |       if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
 189 |         throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
 190 |       }
 191 | 
 192 |       // Validate that at least one of app_ids, publisher_ids, unified_app_ids, unified_publisher_ids, or categories is provided
 193 |       if (
 194 |         (!appIds || appIds.length === 0) &&
 195 |         (!publisherIds || publisherIds.length === 0) &&
 196 |         (!unifiedAppIds || unifiedAppIds.length === 0) &&
 197 |         (!unifiedPublisherIds || unifiedPublisherIds.length === 0) &&
 198 |         (!categories || categories.length === 0)
 199 |       ) {
 200 |         throw new SensorTowerApiError('At least one of App ID, Publisher ID, Unified App ID, Unified Publisher ID, or Category is required.');
 201 |       }
 202 | 
 203 |       // Build URL with query parameters
 204 |       const queryParams = new URLSearchParams();
 205 |       queryParams.append('start_date', startDate);
 206 |       queryParams.append('end_date', endDate);
 207 |       queryParams.append('auth_token', AUTH_TOKEN!);
 208 | 
 209 |       // Add optional parameters if provided
 210 |       if (appIds && appIds.length > 0) {
 211 |         queryParams.append('app_ids', appIds.join(','));
 212 |       }
 213 | 
 214 |       if (publisherIds && publisherIds.length > 0) {
 215 |         publisherIds.forEach(id => {
 216 |           queryParams.append('publisher_ids[]', id);
 217 |         });
 218 |       }
 219 | 
 220 |       if (unifiedAppIds && unifiedAppIds.length > 0) {
 221 |         queryParams.append('unified_app_ids', unifiedAppIds.join(','));
 222 |       }
 223 | 
 224 |       if (unifiedPublisherIds && unifiedPublisherIds.length > 0) {
 225 |         queryParams.append('unified_publisher_ids', unifiedPublisherIds.join(','));
 226 |       }
 227 | 
 228 |       if (categories && categories.length > 0) {
 229 |         queryParams.append('categories', categories.join(','));
 230 |       }
 231 | 
 232 |       if (dateGranularity) {
 233 |         queryParams.append('date_granularity', dateGranularity);
 234 |       }
 235 | 
 236 |       if (dataModel) {
 237 |         queryParams.append('data_model', dataModel);
 238 |       }
 239 | 
 240 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/compact_sales_report_estimates?${queryParams.toString()}`;
 241 | 
 242 |       console.error(`Fetching compact sales report estimates for ${os}, start date: ${startDate}, end date: ${endDate}`);
 243 | 
 244 |       const response = await fetch(url, {
 245 |         method: 'GET',
 246 |         headers: {
 247 |           'Content-Type': 'application/json',
 248 |           'Accept': 'application/json'
 249 |         }
 250 |       });
 251 | 
 252 |       if (!response.ok) {
 253 |         const errorBody = await response.text();
 254 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 255 |       }
 256 | 
 257 |       const data = await response.json();
 258 |       return data;
 259 |     } catch (error: any) {
 260 |       console.error('Error fetching compact sales report estimates:', error);
 261 |       throw new SensorTowerApiError(`Failed to fetch compact sales report estimates: ${error.message}`);
 262 |     }
 263 |   }
 264 | 
 265 |   /**
 266 |    * Fetch active user estimates from Sensor Tower API
 267 |    * @param os Operating system ('ios' or 'android')
 268 |    * @param appIds Array of app IDs (maximum 500)
 269 |    * @param timePeriod Time period for aggregation ('day', 'week', or 'month')
 270 |    * @param startDate Start date in YYYY-MM-DD format
 271 |    * @param endDate End date in YYYY-MM-DD format
 272 |    * @param countries Optional array of country codes
 273 |    * @param dataModel Optional data model specification
 274 |    * @returns Active user estimates
 275 |    */
 276 |   async fetchActiveUsers(
 277 |     os: string,
 278 |     appIds: string[],
 279 |     timePeriod: string,
 280 |     startDate: string,
 281 |     endDate: string,
 282 |     countries?: string[],
 283 |     dataModel?: string
 284 |   ): Promise<any> {
 285 |     try {
 286 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 287 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 288 |       }
 289 | 
 290 |       if (!appIds || appIds.length === 0) {
 291 |         throw new SensorTowerApiError('At least one app ID is required.');
 292 |       }
 293 | 
 294 |       if (appIds.length > 500) {
 295 |         throw new SensorTowerApiError('Maximum of 500 app IDs allowed per request.');
 296 |       }
 297 | 
 298 |       if (!['day', 'week', 'month'].includes(timePeriod.toLowerCase())) {
 299 |         throw new SensorTowerApiError('Invalid time period. Must be "day", "week", or "month".');
 300 |       }
 301 | 
 302 |       // Validate dates
 303 |       const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 304 |       if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
 305 |         throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
 306 |       }
 307 | 
 308 |       // Build URL with query parameters
 309 |       const queryParams = new URLSearchParams();
 310 |       queryParams.append('app_ids', appIds.join(','));
 311 |       queryParams.append('time_period', timePeriod);
 312 |       queryParams.append('start_date', startDate);
 313 |       queryParams.append('end_date', endDate);
 314 |       queryParams.append('auth_token', AUTH_TOKEN!);
 315 | 
 316 |       // Add optional parameters if provided
 317 |       if (countries && countries.length > 0) {
 318 |         queryParams.append('countries', countries.join(','));
 319 |       }
 320 | 
 321 |       if (dataModel) {
 322 |         queryParams.append('data_model', dataModel);
 323 |       }
 324 | 
 325 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/usage/active_users?${queryParams.toString()}`;
 326 | 
 327 |       console.error(`Fetching active users for ${os}, app IDs: ${appIds.join(',')}, time period: ${timePeriod}, start date: ${startDate}, end date: ${endDate}`);
 328 | 
 329 |       const response = await fetch(url, {
 330 |         method: 'GET',
 331 |         headers: {
 332 |           'Content-Type': 'application/json',
 333 |           'Accept': 'application/json'
 334 |         }
 335 |       });
 336 | 
 337 |       if (!response.ok) {
 338 |         const errorBody = await response.text();
 339 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 340 |       }
 341 | 
 342 |       const data = await response.json();
 343 |       return data;
 344 |     } catch (error: any) {
 345 |       console.error('Error fetching active users:', error);
 346 |       throw new SensorTowerApiError(`Failed to fetch active users: ${error.message}`);
 347 |     }
 348 |   }
 349 | 
 350 |   /**
 351 |    * Fetch category ranking history from Sensor Tower API
 352 |    * @param os Operating system ('ios' or 'android')
 353 |    * @param appIds Array of app IDs
 354 |    * @param category Category ID
 355 |    * @param chartTypeIds Array of chart type IDs
 356 |    * @param countries Array of country codes
 357 |    * @param startDate Optional start date in YYYY-MM-DD format (defaults to 90 days ago)
 358 |    * @param endDate Optional end date in YYYY-MM-DD format (defaults to today)
 359 |    * @param isHourly Optional boolean for hourly rankings (only for iOS)
 360 |    * @returns Category ranking history
 361 |    */
 362 |   async fetchCategoryHistory(
 363 |     os: string,
 364 |     appIds: string[],
 365 |     category: string,
 366 |     chartTypeIds: string[],
 367 |     countries: string[],
 368 |     startDate?: string,
 369 |     endDate?: string,
 370 |     isHourly?: boolean
 371 |   ): Promise<any> {
 372 |     try {
 373 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 374 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 375 |       }
 376 | 
 377 |       if (!appIds || appIds.length === 0) {
 378 |         throw new SensorTowerApiError('At least one app ID is required.');
 379 |       }
 380 | 
 381 |       if (!category) {
 382 |         throw new SensorTowerApiError('Category ID is required.');
 383 |       }
 384 | 
 385 |       if (!chartTypeIds || chartTypeIds.length === 0) {
 386 |         throw new SensorTowerApiError('At least one chart type ID is required.');
 387 |       }
 388 | 
 389 |       if (!countries || countries.length === 0) {
 390 |         throw new SensorTowerApiError('At least one country code is required.');
 391 |       }
 392 | 
 393 |       // Validate dates if provided
 394 |       const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 395 |       if (startDate && !dateRegex.test(startDate)) {
 396 |         throw new SensorTowerApiError('Start date must be in YYYY-MM-DD format.');
 397 |       }
 398 |       if (endDate && !dateRegex.test(endDate)) {
 399 |         throw new SensorTowerApiError('End date must be in YYYY-MM-DD format.');
 400 |       }
 401 | 
 402 |       // Build URL with query parameters
 403 |       const queryParams = new URLSearchParams();
 404 |       queryParams.append('app_ids', appIds.join(','));
 405 |       queryParams.append('category', category);
 406 |       queryParams.append('chart_type_ids', chartTypeIds.join(','));
 407 |       queryParams.append('countries', countries.join(','));
 408 |       queryParams.append('auth_token', AUTH_TOKEN!);
 409 | 
 410 |       // Add optional parameters if provided
 411 |       if (startDate) {
 412 |         queryParams.append('start_date', startDate);
 413 |       }
 414 | 
 415 |       if (endDate) {
 416 |         queryParams.append('end_date', endDate);
 417 |       }
 418 | 
 419 |       if (isHourly !== undefined) {
 420 |         queryParams.append('is_hourly', isHourly.toString());
 421 |       }
 422 | 
 423 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/category/category_history?${queryParams.toString()}`;
 424 | 
 425 |       console.error(`Fetching category history for ${os}, app IDs: ${appIds.join(',')}, category: ${category}, chart types: ${chartTypeIds.join(',')}, countries: ${countries.join(',')}`);
 426 | 
 427 |       const response = await fetch(url, {
 428 |         method: 'GET',
 429 |         headers: {
 430 |           'Content-Type': 'application/json',
 431 |           'Accept': 'application/json'
 432 |         }
 433 |       });
 434 | 
 435 |       if (!response.ok) {
 436 |         const errorBody = await response.text();
 437 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 438 |       }
 439 | 
 440 |       const data = await response.json();
 441 |       return data;
 442 |     } catch (error: any) {
 443 |       console.error('Error fetching category history:', error);
 444 |       throw new SensorTowerApiError(`Failed to fetch category history: ${error.message}`);
 445 |     }
 446 |   }
 447 | 
 448 |   /**
 449 |    * Fetch category ranking summary from Sensor Tower API
 450 |    * @param os Operating system ('ios' or 'android')
 451 |    * @param appId App ID
 452 |    * @param country Country code
 453 |    * @returns Category ranking summary
 454 |    */
 455 |   async fetchCategoryRankingSummary(
 456 |     os: string,
 457 |     appId: string,
 458 |     country: string
 459 |   ): Promise<any> {
 460 |     try {
 461 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 462 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 463 |       }
 464 | 
 465 |       if (!appId) {
 466 |         throw new SensorTowerApiError('App ID is required.');
 467 |       }
 468 | 
 469 |       if (!country) {
 470 |         throw new SensorTowerApiError('Country code is required.');
 471 |       }
 472 | 
 473 |       // Build URL with query parameters
 474 |       const queryParams = new URLSearchParams();
 475 |       queryParams.append('app_id', appId);
 476 |       queryParams.append('country', country);
 477 |       queryParams.append('auth_token', AUTH_TOKEN!);
 478 | 
 479 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/category/category_ranking_summary?${queryParams.toString()}`;
 480 | 
 481 |       console.error(`Fetching category ranking summary for ${os}, app ID: ${appId}, country: ${country}`);
 482 | 
 483 |       const response = await fetch(url, {
 484 |         method: 'GET',
 485 |         headers: {
 486 |           'Content-Type': 'application/json',
 487 |           'Accept': 'application/json'
 488 |         }
 489 |       });
 490 | 
 491 |       if (!response.ok) {
 492 |         const errorBody = await response.text();
 493 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 494 |       }
 495 | 
 496 |       const data = await response.json();
 497 |       return data;
 498 |     } catch (error: any) {
 499 |       console.error('Error fetching category ranking summary:', error);
 500 |       throw new SensorTowerApiError(`Failed to fetch category ranking summary: ${error.message}`);
 501 |     }
 502 |   }
 503 | 
 504 |   /**
 505 |    * Fetch network analysis (impressions share of voice) from Sensor Tower API
 506 |    * @param os Operating system ('ios' or 'android')
 507 |    * @param appIds Array of app IDs to fetch SOV for
 508 |    * @param startDate Start date in YYYY-MM-DD format
 509 |    * @param endDate End date in YYYY-MM-DD format
 510 |    * @param period Time period to calculate Share of Voice for ('day')
 511 |    * @param networks Optional array of networks to return results for
 512 |    * @param countries Optional array of country codes to return results for
 513 |    * @returns Network analysis data
 514 |    */
 515 |   async fetchNetworkAnalysis(
 516 |     os: string,
 517 |     appIds: string[],
 518 |     startDate: string,
 519 |     endDate: string,
 520 |     period: string,
 521 |     networks?: string[],
 522 |     countries?: string[]
 523 |   ): Promise<any> {
 524 |     try {
 525 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 526 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 527 |       }
 528 | 
 529 |       if (!appIds || appIds.length === 0) {
 530 |         throw new SensorTowerApiError('At least one app ID is required.');
 531 |       }
 532 | 
 533 |       // Validate dates
 534 |       const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 535 |       if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
 536 |         throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
 537 |       }
 538 | 
 539 |       // Validate period
 540 |       if (period !== 'day') {
 541 |         throw new SensorTowerApiError('Period must be "day".');
 542 |       }
 543 | 
 544 |       // Build URL with query parameters
 545 |       const queryParams = new URLSearchParams();
 546 |       queryParams.append('app_ids', appIds.join(','));
 547 |       queryParams.append('start_date', startDate);
 548 |       queryParams.append('end_date', endDate);
 549 |       queryParams.append('period', period);
 550 |       queryParams.append('auth_token', AUTH_TOKEN!);
 551 | 
 552 |       // Add optional parameters if provided
 553 |       if (networks && networks.length > 0) {
 554 |         queryParams.append('networks', networks.join(','));
 555 |       }
 556 | 
 557 |       if (countries && countries.length > 0) {
 558 |         queryParams.append('countries', countries.join(','));
 559 |       }
 560 | 
 561 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/ad_intel/network_analysis?${queryParams.toString()}`;
 562 | 
 563 |       console.error(`Fetching network analysis for ${os}, app IDs: ${appIds.join(',')}, start date: ${startDate}, end date: ${endDate}`);
 564 | 
 565 |       const response = await fetch(url, {
 566 |         method: 'GET',
 567 |         headers: {
 568 |           'Content-Type': 'application/json',
 569 |           'Accept': 'application/json'
 570 |         }
 571 |       });
 572 | 
 573 |       if (!response.ok) {
 574 |         const errorBody = await response.text();
 575 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 576 |       }
 577 | 
 578 |       const data = await response.json();
 579 |       return data;
 580 |     } catch (error: any) {
 581 |       console.error('Error fetching network analysis:', error);
 582 |       throw new SensorTowerApiError(`Failed to fetch network analysis: ${error.message}`);
 583 |     }
 584 |   }
 585 | 
 586 |   /**
 587 |    * Fetch network analysis rank data from Sensor Tower API
 588 |    * @param os Operating system ('ios' or 'android')
 589 |    * @param appIds Array of app IDs to fetch ranks for
 590 |    * @param startDate Start date in YYYY-MM-DD format
 591 |    * @param endDate End date in YYYY-MM-DD format
 592 |    * @param period Time period to calculate ranks for ('day')
 593 |    * @param networks Optional array of networks to return results for
 594 |    * @param countries Optional array of country codes to return results for
 595 |    * @returns Network analysis rank data
 596 |    */
 597 |   async fetchNetworkAnalysisRank(
 598 |     os: string,
 599 |     appIds: string[],
 600 |     startDate: string,
 601 |     endDate: string,
 602 |     period: string,
 603 |     networks?: string[],
 604 |     countries?: string[]
 605 |   ): Promise<any> {
 606 |     try {
 607 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 608 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 609 |       }
 610 | 
 611 |       if (!appIds || appIds.length === 0) {
 612 |         throw new SensorTowerApiError('At least one app ID is required.');
 613 |       }
 614 | 
 615 |       // Validate dates
 616 |       const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 617 |       if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
 618 |         throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
 619 |       }
 620 | 
 621 |       // Validate period
 622 |       if (period !== 'day') {
 623 |         throw new SensorTowerApiError('Period must be "day".');
 624 |       }
 625 | 
 626 |       // Build URL with query parameters
 627 |       const queryParams = new URLSearchParams();
 628 |       queryParams.append('app_ids', appIds.join(','));
 629 |       queryParams.append('start_date', startDate);
 630 |       queryParams.append('end_date', endDate);
 631 |       queryParams.append('period', period);
 632 |       queryParams.append('auth_token', AUTH_TOKEN!);
 633 | 
 634 |       // Add optional parameters if provided
 635 |       if (networks && networks.length > 0) {
 636 |         queryParams.append('networks', networks.join(','));
 637 |       }
 638 | 
 639 |       if (countries && countries.length > 0) {
 640 |         queryParams.append('countries', countries.join(','));
 641 |       }
 642 | 
 643 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/ad_intel/network_analysis/rank?${queryParams.toString()}`;
 644 | 
 645 |       console.error(`Fetching network analysis rank for ${os}, app IDs: ${appIds.join(',')}, start date: ${startDate}, end date: ${endDate}`);
 646 | 
 647 |       const response = await fetch(url, {
 648 |         method: 'GET',
 649 |         headers: {
 650 |           'Content-Type': 'application/json',
 651 |           'Accept': 'application/json'
 652 |         }
 653 |       });
 654 | 
 655 |       if (!response.ok) {
 656 |         const errorBody = await response.text();
 657 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 658 |       }
 659 | 
 660 |       const data = await response.json();
 661 |       return data;
 662 |     } catch (error: any) {
 663 |       console.error('Error fetching network analysis rank:', error);
 664 |       throw new SensorTowerApiError(`Failed to fetch network analysis rank: ${error.message}`);
 665 |     }
 666 |   }
 667 | 
 668 |   /**
 669 |    * Fetch retention data for apps from Sensor Tower API
 670 |    * @param os Operating system ('ios' or 'android')
 671 |    * @param appIds Array of app IDs (maximum 500)
 672 |    * @param dateGranularity Aggregate estimates by granularity ('all_time' or 'quarterly')
 673 |    * @param startDate Start date in YYYY-MM-DD format
 674 |    * @param endDate Optional end date in YYYY-MM-DD format
 675 |    * @param country Optional country code (defaults to Worldwide)
 676 |    * @returns Retention data for apps
 677 |    */
 678 |   async fetchRetention(
 679 |     os: string,
 680 |     appIds: string[],
 681 |     dateGranularity: string,
 682 |     startDate: string,
 683 |     endDate?: string,
 684 |     country?: string
 685 |   ): Promise<any> {
 686 |     try {
 687 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 688 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 689 |       }
 690 | 
 691 |       if (!appIds || appIds.length === 0) {
 692 |         throw new SensorTowerApiError('At least one app ID is required.');
 693 |       }
 694 | 
 695 |       if (appIds.length > 500) {
 696 |         throw new SensorTowerApiError('Maximum of 500 app IDs allowed per request.');
 697 |       }
 698 | 
 699 |       if (!['all_time', 'quarterly'].includes(dateGranularity)) {
 700 |         throw new SensorTowerApiError('Invalid date granularity. Must be "all_time" or "quarterly".');
 701 |       }
 702 | 
 703 |       // Validate dates
 704 |       const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 705 |       if (!dateRegex.test(startDate)) {
 706 |         throw new SensorTowerApiError('Start date must be in YYYY-MM-DD format.');
 707 |       }
 708 |       
 709 |       if (endDate && !dateRegex.test(endDate)) {
 710 |         throw new SensorTowerApiError('End date must be in YYYY-MM-DD format.');
 711 |       }
 712 | 
 713 |       // Build URL with query parameters
 714 |       const queryParams = new URLSearchParams();
 715 |       queryParams.append('app_ids', appIds.join(','));
 716 |       queryParams.append('date_granularity', dateGranularity);
 717 |       queryParams.append('start_date', startDate);
 718 |       queryParams.append('auth_token', AUTH_TOKEN!);
 719 | 
 720 |       // Add optional parameters if provided
 721 |       if (endDate) {
 722 |         queryParams.append('end_date', endDate);
 723 |       }
 724 | 
 725 |       if (country) {
 726 |         queryParams.append('country', country);
 727 |       }
 728 | 
 729 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/usage/retention?${queryParams.toString()}`;
 730 | 
 731 |       console.error(`Fetching retention data for ${os}, app IDs: ${appIds.join(',')}, date granularity: ${dateGranularity}, start date: ${startDate}${endDate ? `, end date: ${endDate}` : ''}${country ? `, country: ${country}` : ''}`);
 732 | 
 733 |       const response = await fetch(url, {
 734 |         method: 'GET',
 735 |         headers: {
 736 |           'Content-Type': 'application/json',
 737 |           'Accept': 'application/json'
 738 |         }
 739 |       });
 740 | 
 741 |       if (!response.ok) {
 742 |         const errorBody = await response.text();
 743 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 744 |       }
 745 | 
 746 |       const data = await response.json();
 747 |       return data;
 748 |     } catch (error: any) {
 749 |       console.error('Error fetching retention data:', error);
 750 |       throw new SensorTowerApiError(`Failed to fetch retention data: ${error.message}`);
 751 |     }
 752 |   }
 753 | 
 754 |   /**
 755 |    * Fetch app downloads by sources from Sensor Tower API
 756 |    * @param os Operating system ('ios' or 'android')
 757 |    * @param appIds Array of unified app IDs
 758 |    * @param countries Array of country codes
 759 |    * @param startDate Start date in YYYY-MM-DD format
 760 |    * @param endDate End date in YYYY-MM-DD format
 761 |    * @param dateGranularity Optional date granularity for aggregation ('daily' or 'monthly')
 762 |    * @returns Downloads by sources data
 763 |    */
 764 |   async fetchDownloadsBySources(
 765 |     os: string,
 766 |     appIds: string[],
 767 |     countries: string[],
 768 |     startDate: string,
 769 |     endDate: string,
 770 |     dateGranularity?: string
 771 |   ): Promise<any> {
 772 |     try {
 773 |       if (!['ios', 'android'].includes(os.toLowerCase())) {
 774 |         throw new SensorTowerApiError('Invalid OS. Must be "ios" or "android".');
 775 |       }
 776 | 
 777 |       if (!appIds || appIds.length === 0) {
 778 |         throw new SensorTowerApiError('At least one app ID is required.');
 779 |       }
 780 | 
 781 |       if (!countries || countries.length === 0) {
 782 |         throw new SensorTowerApiError('At least one country code is required.');
 783 |       }
 784 | 
 785 |       // Validate dates
 786 |       const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 787 |       if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
 788 |         throw new SensorTowerApiError('Dates must be in YYYY-MM-DD format.');
 789 |       }
 790 | 
 791 |       // Validate date granularity if provided
 792 |       if (dateGranularity && !['daily', 'monthly'].includes(dateGranularity)) {
 793 |         throw new SensorTowerApiError('Date granularity must be "daily" or "monthly".');
 794 |       }
 795 | 
 796 |       // Build URL with query parameters
 797 |       const queryParams = new URLSearchParams();
 798 |       queryParams.append('app_ids', appIds.join(','));
 799 |       queryParams.append('countries', countries.join(','));
 800 |       queryParams.append('start_date', startDate);
 801 |       queryParams.append('end_date', endDate);
 802 |       queryParams.append('auth_token', AUTH_TOKEN!);
 803 | 
 804 |       // Add optional parameters if provided
 805 |       if (dateGranularity) {
 806 |         queryParams.append('date_granularity', dateGranularity);
 807 |       }
 808 | 
 809 |       const url = `${SENSOR_TOWER_BASE_URL}/v1/${os.toLowerCase()}/downloads_by_sources?${queryParams.toString()}`;
 810 | 
 811 |       console.error(`Fetching downloads by sources for ${os}, app IDs: ${appIds.join(',')}, countries: ${countries.join(',')}, start date: ${startDate}, end date: ${endDate}`);
 812 | 
 813 |       const response = await fetch(url, {
 814 |         method: 'GET',
 815 |         headers: {
 816 |           'Content-Type': 'application/json',
 817 |           'Accept': 'application/json'
 818 |         }
 819 |       });
 820 | 
 821 |       if (!response.ok) {
 822 |         const errorBody = await response.text();
 823 |         throw new Error(`HTTP ${response.status}: ${errorBody}`);
 824 |       }
 825 | 
 826 |       const data = await response.json();
 827 |       return data;
 828 |     } catch (error: any) {
 829 |       console.error('Error fetching downloads by sources:', error);
 830 |       throw new SensorTowerApiError(`Failed to fetch downloads by sources: ${error.message}`);
 831 |     }
 832 |   }
 833 | }
 834 | 
 835 | // Input validation schemas
 836 | const osSchema = z.string()
 837 |   .refine(val => ['ios', 'android'].includes(val.toLowerCase()), "OS must be 'ios' or 'android'")
 838 |   .describe("Operating System ('ios' or 'android')");
 839 | 
 840 | const appIdsSchema = z.string()
 841 |   .describe("App IDs of apps, separated by commas (limited to 100)")
 842 |   .refine(val => val.split(',').length <= 100, "Maximum of 100 app IDs allowed");
 843 | 
 844 | const countrySchema = z.string().optional().default('US')
 845 |   .describe("Country Code (defaults to 'US')");
 846 | 
 847 | // Schemas for compact sales report estimates
 848 | const dateSchema = z.string()
 849 |   .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format")
 850 |   .describe("Date in YYYY-MM-DD format");
 851 | 
 852 | const appIdsOptionalSchema = z.string().optional()
 853 |   .describe("IDs of apps, separated by commas (limited to 100)");
 854 | 
 855 | const publisherIdsSchema = z.array(z.string()).optional()
 856 |   .describe("Publisher IDs of apps");
 857 | 
 858 | const unifiedAppIdsSchema = z.string().optional()
 859 |   .describe("IDs of unified apps, separated by commas");
 860 | 
 861 | const unifiedPublisherIdsSchema = z.string().optional()
 862 |   .describe("IDs of unified publishers, separated by commas");
 863 | 
 864 | const categoriesSchema = z.string().optional()
 865 |   .describe("Categories, separated by commas, see Category IDs");
 866 | 
 867 | const dateGranularitySchema = z.string().optional()
 868 |   .describe("Aggregate estimates by granularity");
 869 | 
 870 | const dataModelSchema = z.string().optional()
 871 |   .describe("Specify the data model used to generate estimates");
 872 | 
 873 | // Schemas for active users
 874 | const timePeriodSchema = z.string()
 875 |   .refine(val => ['day', 'week', 'month'].includes(val.toLowerCase()), "Time period must be 'day', 'week', or 'month'")
 876 |   .describe("Aggregate estimates by time period ('day' for DAU, 'week' for WAU, 'month' for MAU)");
 877 | 
 878 | const appIdsRequiredSchema = z.string()
 879 |   .describe("IDs of apps, separated by commas (maximum 500)")
 880 |   .refine(val => val.split(',').length <= 500, "Maximum of 500 app IDs allowed");
 881 | 
 882 | const countriesSchema = z.string().optional()
 883 |   .describe("Countries to return results for, separated by commas, Country Codes");
 884 | 
 885 | // Schemas for downloads by sources
 886 | const unifiedAppIdsRequiredSchema = z.string()
 887 |   .describe("Unified app IDs, separated by commas");
 888 | 
 889 | const countriesRequiredForDownloadsSchema = z.string()
 890 |   .describe("Country codes, separated by commas. For worldwide data, use 'WW'.");
 891 | 
 892 | const dateGranularityDownloadsSchema = z.string().optional()
 893 |   .describe("Aggregate estimates by granularity (use 'daily' or 'monthly'). Defaults to 'monthly'.");
 894 | 
 895 | // Schemas for category history
 896 | const categorySchema = z.string()
 897 |   .describe("Category ID to return results for");
 898 | 
 899 | const chartTypeIdsSchema = z.string()
 900 |   .describe("IDs of the Chart Type, separated by commas");
 901 | 
 902 | const countriesRequiredSchema = z.string()
 903 |   .describe("Countries to return results for, separated by commas, Country Codes");
 904 | 
 905 | const isHourlySchema = z.boolean().optional()
 906 |   .describe("Hourly rankings (only for iOS)");
 907 | 
 908 | // Schemas for category ranking summary
 909 | const appIdSchema = z.string()
 910 |   .describe("ID of App");
 911 | 
 912 | const countryRequiredSchema = z.string()
 913 |   .describe("Country code to return results for");
 914 | 
 915 | // Schemas for network analysis
 916 | const networkAnalysisPeriodSchema = z.string()
 917 |   .refine(val => val === 'day', "Period must be 'day'")
 918 |   .describe("Time period to calculate Share of Voice for");
 919 | 
 920 | const networksSchema = z.string().optional()
 921 |   .describe("Networks to return results for, separated by commas (Networks)");
 922 | 
 923 | // Schemas for retention data
 924 | const retentionDateGranularitySchema = z.string()
 925 |   .refine(val => ['all_time', 'quarterly'].includes(val), "Date granularity must be 'all_time' or 'quarterly'")
 926 |   .describe("Aggregate estimates by granularity (use 'all_time', or 'quarterly')");
 927 | 
 928 | // Tool: Get App Metadata
 929 | server.tool("get_app_metadata",
 930 |   "Fetch app metadata from Sensor Tower API, such as app name, publisher, categories, description, screenshots, rating, etc.",
 931 |   {
 932 |     os: osSchema,
 933 |     appIds: appIdsSchema,
 934 |     country: countrySchema
 935 |   },
 936 |   async ({ os, appIds, country = 'US' }) => {
 937 |     try {
 938 |       console.error(`Fetching app metadata for ${os}, app IDs: ${appIds}, country: ${country}`);
 939 |       
 940 |       const appIdsArray = appIds.split(',').map(id => id.trim());
 941 |       const sensorTowerService = new SensorTowerApiService();
 942 |       const metadata = await sensorTowerService.fetchAppMetadata(os, appIdsArray, country);
 943 |       
 944 |       return {
 945 |         content: [
 946 |           {
 947 |             type: "text",
 948 |             text: JSON.stringify({
 949 |               os,
 950 |               country,
 951 |               appIds: appIdsArray,
 952 |               data: metadata
 953 |             }, null, 2)
 954 |           }
 955 |         ]
 956 |       };
 957 |     } catch (error: any) {
 958 |       const errorMessage = `Error fetching app metadata: ${error.message}`;
 959 |       console.error(errorMessage);
 960 |       
 961 |       return {
 962 |         content: [
 963 |           {
 964 |             type: "text",
 965 |             text: JSON.stringify({
 966 |               error: errorMessage,
 967 |               os,
 968 |               country,
 969 |               appIds: appIds.split(',').map(id => id.trim()),
 970 |               timestamp: new Date().toISOString()
 971 |             }, null, 2)
 972 |           }
 973 |         ],
 974 |         isError: true
 975 |       };
 976 |     }
 977 |   }
 978 | );
 979 | 
 980 | // Tool: Get Top In-App Purchases
 981 | server.tool("get_top_in_app_purchases",
 982 |   "Fetches the top in-app purchases for particular iOS apps",
 983 |   {
 984 |     appIds: appIdsSchema,
 985 |     country: countrySchema
 986 |   },
 987 |   async ({ appIds, country = 'US' }) => {
 988 |     try {
 989 |       console.error(`Fetching top in-app purchases for app IDs: ${appIds}, country: ${country}`);
 990 |       
 991 |       const appIdsArray = appIds.split(',').map(id => id.trim());
 992 |       const sensorTowerService = new SensorTowerApiService();
 993 |       const inAppPurchasesData = await sensorTowerService.fetchTopInAppPurchases(appIdsArray, country);
 994 |       
 995 |       return {
 996 |         content: [
 997 |           {
 998 |             type: "text",
 999 |             text: JSON.stringify({
1000 |               country,
1001 |               appIds: appIdsArray,
1002 |               data: inAppPurchasesData
1003 |             }, null, 2)
1004 |           }
1005 |         ]
1006 |       };
1007 |     } catch (error: any) {
1008 |       const errorMessage = `Error fetching top in-app purchases: ${error.message}`;
1009 |       console.error(errorMessage);
1010 |       
1011 |       return {
1012 |         content: [
1013 |           {
1014 |             type: "text",
1015 |             text: JSON.stringify({
1016 |               error: errorMessage,
1017 |               country,
1018 |               appIds: appIds.split(',').map(id => id.trim()),
1019 |               timestamp: new Date().toISOString()
1020 |             }, null, 2)
1021 |           }
1022 |         ],
1023 |         isError: true
1024 |       };
1025 |     }
1026 |   }
1027 | );
1028 | 
1029 | // Tool: Get Compact Sales Report Estimates
1030 | server.tool("get_compact_sales_report_estimates",
1031 |   "Fetches download and revenue estimates of apps and publishers in compact format. All revenues are returned in cents.",
1032 |   {
1033 |     os: osSchema,
1034 |     startDate: dateSchema,
1035 |     endDate: dateSchema,
1036 |     appIds: appIdsOptionalSchema,
1037 |     publisherIds: publisherIdsSchema,
1038 |     unifiedAppIds: unifiedAppIdsSchema,
1039 |     unifiedPublisherIds: unifiedPublisherIdsSchema,
1040 |     categories: categoriesSchema,
1041 |     dateGranularity: dateGranularitySchema,
1042 |     dataModel: dataModelSchema
1043 |   },
1044 |   async ({ os, startDate, endDate, appIds, publisherIds, unifiedAppIds, unifiedPublisherIds, categories, dateGranularity, dataModel }) => {
1045 |     try {
1046 |       console.error(`Fetching compact sales report estimates for ${os}, start date: ${startDate}, end date: ${endDate}`);
1047 |       
1048 |       // Process string inputs into arrays
1049 |       const appIdsArray = appIds ? appIds.split(',').map(id => id.trim()) : undefined;
1050 |       const unifiedAppIdsArray = unifiedAppIds ? unifiedAppIds.split(',').map(id => id.trim()) : undefined;
1051 |       const unifiedPublisherIdsArray = unifiedPublisherIds ? unifiedPublisherIds.split(',').map(id => id.trim()) : undefined;
1052 |       const categoriesArray = categories ? categories.split(',').map(category => category.trim()) : undefined;
1053 |       
1054 |       const sensorTowerService = new SensorTowerApiService();
1055 |       const reportData = await sensorTowerService.fetchCompactSalesReportEstimates(
1056 |         os,
1057 |         startDate,
1058 |         endDate,
1059 |         appIdsArray,
1060 |         publisherIds,
1061 |         unifiedAppIdsArray,
1062 |         unifiedPublisherIdsArray,
1063 |         categoriesArray,
1064 |         dateGranularity,
1065 |         dataModel
1066 |       );
1067 |       
1068 |       return {
1069 |         content: [
1070 |           {
1071 |             type: "text",
1072 |             text: JSON.stringify({
1073 |               os,
1074 |               startDate,
1075 |               endDate,
1076 |               appIds: appIdsArray,
1077 |               publisherIds,
1078 |               unifiedAppIds: unifiedAppIdsArray,
1079 |               unifiedPublisherIds: unifiedPublisherIdsArray,
1080 |               categories: categoriesArray,
1081 |               dateGranularity,
1082 |               dataModel,
1083 |               data: reportData
1084 |             }, null, 2)
1085 |           }
1086 |         ]
1087 |       };
1088 |     } catch (error: any) {
1089 |       const errorMessage = `Error fetching compact sales report estimates: ${error.message}`;
1090 |       console.error(errorMessage);
1091 |       
1092 |       return {
1093 |         content: [
1094 |           {
1095 |             type: "text",
1096 |             text: JSON.stringify({
1097 |               error: errorMessage,
1098 |               os,
1099 |               startDate,
1100 |               endDate,
1101 |               appIds: appIds ? appIds.split(',').map(id => id.trim()) : undefined,
1102 |               publisherIds,
1103 |               unifiedAppIds: unifiedAppIds ? unifiedAppIds.split(',').map(id => id.trim()) : undefined,
1104 |               unifiedPublisherIds: unifiedPublisherIds ? unifiedPublisherIds.split(',').map(id => id.trim()) : undefined,
1105 |               categories: categories ? categories.split(',').map(category => category.trim()) : undefined,
1106 |               dateGranularity,
1107 |               dataModel,
1108 |               timestamp: new Date().toISOString()
1109 |             }, null, 2)
1110 |           }
1111 |         ],
1112 |         isError: true
1113 |       };
1114 |     }
1115 |   }
1116 | );
1117 | 
1118 | // Tool: Get Active Users
1119 | server.tool("get_active_users",
1120 |   "Fetches active user estimates of apps per country by date and time period.",
1121 |   {
1122 |     os: osSchema,
1123 |     appIds: appIdsRequiredSchema,
1124 |     timePeriod: timePeriodSchema,
1125 |     startDate: dateSchema,
1126 |     endDate: dateSchema,
1127 |     countries: countriesSchema,
1128 |     dataModel: dataModelSchema
1129 |   },
1130 |   async ({ os, appIds, timePeriod, startDate, endDate, countries, dataModel }) => {
1131 |     try {
1132 |       console.error(`Fetching active users for ${os}, app IDs: ${appIds}, time period: ${timePeriod}, start date: ${startDate}, end date: ${endDate}`);
1133 |       
1134 |       // Process string inputs into arrays
1135 |       const appIdsArray = appIds.split(',').map(id => id.trim());
1136 |       const countriesArray = countries ? countries.split(',').map(country => country.trim()) : undefined;
1137 |       
1138 |       const sensorTowerService = new SensorTowerApiService();
1139 |       const activeUsersData = await sensorTowerService.fetchActiveUsers(
1140 |         os,
1141 |         appIdsArray,
1142 |         timePeriod,
1143 |         startDate,
1144 |         endDate,
1145 |         countriesArray,
1146 |         dataModel
1147 |       );
1148 |       
1149 |       return {
1150 |         content: [
1151 |           {
1152 |             type: "text",
1153 |             text: JSON.stringify({
1154 |               os,
1155 |               appIds: appIdsArray,
1156 |               timePeriod,
1157 |               startDate,
1158 |               endDate,
1159 |               countries: countriesArray,
1160 |               dataModel,
1161 |               data: activeUsersData
1162 |             }, null, 2)
1163 |           }
1164 |         ]
1165 |       };
1166 |     } catch (error: any) {
1167 |       const errorMessage = `Error fetching active users: ${error.message}`;
1168 |       console.error(errorMessage);
1169 |       
1170 |       return {
1171 |         content: [
1172 |           {
1173 |             type: "text",
1174 |             text: JSON.stringify({
1175 |               error: errorMessage,
1176 |               os,
1177 |               appIds: appIds.split(',').map(id => id.trim()),
1178 |               timePeriod,
1179 |               startDate,
1180 |               endDate,
1181 |               countries: countries ? countries.split(',').map(country => country.trim()) : undefined,
1182 |               dataModel,
1183 |               timestamp: new Date().toISOString()
1184 |             }, null, 2)
1185 |           }
1186 |         ],
1187 |         isError: true
1188 |       };
1189 |     }
1190 |   }
1191 | );
1192 | 
1193 | // Tool: Get Category History
1194 | server.tool("get_category_history",
1195 |   "Fetches detailed category ranking history of a particular app, category, and chart type.",
1196 |   {
1197 |     os: osSchema,
1198 |     appIds: appIdsRequiredSchema,
1199 |     category: categorySchema,
1200 |     chartTypeIds: chartTypeIdsSchema,
1201 |     countries: countriesRequiredSchema,
1202 |     startDate: dateSchema.optional(),
1203 |     endDate: dateSchema.optional(),
1204 |     isHourly: isHourlySchema
1205 |   },
1206 |   async ({ os, appIds, category, chartTypeIds, countries, startDate, endDate, isHourly }) => {
1207 |     try {
1208 |       console.error(`Fetching category history for ${os}, app IDs: ${appIds}, category: ${category}, chart types: ${chartTypeIds}, countries: ${countries}`);
1209 |       
1210 |       // Process string inputs into arrays
1211 |       const appIdsArray = appIds.split(',').map(id => id.trim());
1212 |       const chartTypeIdsArray = chartTypeIds.split(',').map(id => id.trim());
1213 |       const countriesArray = countries.split(',').map(country => country.trim());
1214 |       
1215 |       const sensorTowerService = new SensorTowerApiService();
1216 |       const categoryHistoryData = await sensorTowerService.fetchCategoryHistory(
1217 |         os,
1218 |         appIdsArray,
1219 |         category,
1220 |         chartTypeIdsArray,
1221 |         countriesArray,
1222 |         startDate,
1223 |         endDate,
1224 |         isHourly
1225 |       );
1226 |       
1227 |       return {
1228 |         content: [
1229 |           {
1230 |             type: "text",
1231 |             text: JSON.stringify({
1232 |               os,
1233 |               appIds: appIdsArray,
1234 |               category,
1235 |               chartTypeIds: chartTypeIdsArray,
1236 |               countries: countriesArray,
1237 |               startDate,
1238 |               endDate,
1239 |               isHourly,
1240 |               data: categoryHistoryData
1241 |             }, null, 2)
1242 |           }
1243 |         ]
1244 |       };
1245 |     } catch (error: any) {
1246 |       const errorMessage = `Error fetching category history: ${error.message}`;
1247 |       console.error(errorMessage);
1248 |       
1249 |       return {
1250 |         content: [
1251 |           {
1252 |             type: "text",
1253 |             text: JSON.stringify({
1254 |               error: errorMessage,
1255 |               os,
1256 |               appIds: appIds.split(',').map(id => id.trim()),
1257 |               category,
1258 |               chartTypeIds: chartTypeIds.split(',').map(id => id.trim()),
1259 |               countries: countries.split(',').map(country => country.trim()),
1260 |               startDate,
1261 |               endDate,
1262 |               isHourly,
1263 |               timestamp: new Date().toISOString()
1264 |             }, null, 2)
1265 |           }
1266 |         ],
1267 |         isError: true
1268 |       };
1269 |     }
1270 |   }
1271 | );
1272 | 
1273 | // Tool: Get Category Ranking Summary
1274 | server.tool("get_category_ranking_summary",
1275 |   "Fetches today's category ranking summary of a particular app with data on chart type, category, and rank.",
1276 |   {
1277 |     os: osSchema,
1278 |     appId: appIdSchema,
1279 |     country: countryRequiredSchema
1280 |   },
1281 |   async ({ os, appId, country }) => {
1282 |     try {
1283 |       console.error(`Fetching category ranking summary for ${os}, app ID: ${appId}, country: ${country}`);
1284 |       
1285 |       const sensorTowerService = new SensorTowerApiService();
1286 |       const rankingSummaryData = await sensorTowerService.fetchCategoryRankingSummary(
1287 |         os,
1288 |         appId,
1289 |         country
1290 |       );
1291 |       
1292 |       return {
1293 |         content: [
1294 |           {
1295 |             type: "text",
1296 |             text: JSON.stringify({
1297 |               os,
1298 |               appId,
1299 |               country,
1300 |               data: rankingSummaryData
1301 |             }, null, 2)
1302 |           }
1303 |         ]
1304 |       };
1305 |     } catch (error: any) {
1306 |       const errorMessage = `Error fetching category ranking summary: ${error.message}`;
1307 |       console.error(errorMessage);
1308 |       
1309 |       return {
1310 |         content: [
1311 |           {
1312 |             type: "text",
1313 |             text: JSON.stringify({
1314 |               error: errorMessage,
1315 |               os,
1316 |               appId,
1317 |               country,
1318 |               timestamp: new Date().toISOString()
1319 |             }, null, 2)
1320 |           }
1321 |         ],
1322 |         isError: true
1323 |       };
1324 |     }
1325 |   }
1326 | );
1327 | 
1328 | // Tool: Get Network Analysis
1329 | server.tool("get_network_analysis",
1330 |   "Fetches the impressions share of voice (SOV) time series of the requested apps.",
1331 |   {
1332 |     os: osSchema,
1333 |     appIds: appIdsRequiredSchema,
1334 |     startDate: dateSchema,
1335 |     endDate: dateSchema,
1336 |     period: networkAnalysisPeriodSchema,
1337 |     networks: networksSchema,
1338 |     countries: countriesSchema
1339 |   },
1340 |   async ({ os, appIds, startDate, endDate, period, networks, countries }) => {
1341 |     try {
1342 |       console.error(`Fetching network analysis for ${os}, app IDs: ${appIds}, start date: ${startDate}, end date: ${endDate}`);
1343 |       
1344 |       // Process string inputs into arrays
1345 |       const appIdsArray = appIds.split(',').map(id => id.trim());
1346 |       const networksArray = networks ? networks.split(',').map(network => network.trim()) : undefined;
1347 |       const countriesArray = countries ? countries.split(',').map(country => country.trim()) : undefined;
1348 |       
1349 |       const sensorTowerService = new SensorTowerApiService();
1350 |       const networkAnalysisData = await sensorTowerService.fetchNetworkAnalysis(
1351 |         os,
1352 |         appIdsArray,
1353 |         startDate,
1354 |         endDate,
1355 |         period,
1356 |         networksArray,
1357 |         countriesArray
1358 |       );
1359 |       
1360 |       return {
1361 |         content: [
1362 |           {
1363 |             type: "text",
1364 |             text: JSON.stringify({
1365 |               os,
1366 |               appIds: appIdsArray,
1367 |               startDate,
1368 |               endDate,
1369 |               period,
1370 |               networks: networksArray,
1371 |               countries: countriesArray,
1372 |               data: networkAnalysisData
1373 |             }, null, 2)
1374 |           }
1375 |         ]
1376 |       };
1377 |     } catch (error: any) {
1378 |       const errorMessage = `Error fetching network analysis: ${error.message}`;
1379 |       console.error(errorMessage);
1380 |       
1381 |       return {
1382 |         content: [
1383 |           {
1384 |             type: "text",
1385 |             text: JSON.stringify({
1386 |               error: errorMessage,
1387 |               os,
1388 |               appIds: appIds.split(',').map(id => id.trim()),
1389 |               startDate,
1390 |               endDate,
1391 |               period,
1392 |               networks: networks ? networks.split(',').map(network => network.trim()) : undefined,
1393 |               countries: countries ? countries.split(',').map(country => country.trim()) : undefined,
1394 |               timestamp: new Date().toISOString()
1395 |             }, null, 2)
1396 |           }
1397 |         ],
1398 |         isError: true
1399 |       };
1400 |     }
1401 |   }
1402 | );
1403 | 
1404 | // Tool: Get Network Analysis Rank
1405 | server.tool("get_network_analysis_rank",
1406 |   "Fetches the ranks for the countries, networks and dates of the requested apps.",
1407 |   {
1408 |     os: osSchema,
1409 |     appIds: appIdsRequiredSchema,
1410 |     startDate: dateSchema,
1411 |     endDate: dateSchema,
1412 |     period: networkAnalysisPeriodSchema,
1413 |     networks: networksSchema,
1414 |     countries: countriesSchema
1415 |   },
1416 |   async ({ os, appIds, startDate, endDate, period, networks, countries }) => {
1417 |     try {
1418 |       console.error(`Fetching network analysis rank for ${os}, app IDs: ${appIds}, start date: ${startDate}, end date: ${endDate}`);
1419 |       
1420 |       // Process string inputs into arrays
1421 |       const appIdsArray = appIds.split(',').map(id => id.trim());
1422 |       const networksArray = networks ? networks.split(',').map(network => network.trim()) : undefined;
1423 |       const countriesArray = countries ? countries.split(',').map(country => country.trim()) : undefined;
1424 |       
1425 |       const sensorTowerService = new SensorTowerApiService();
1426 |       const networkAnalysisRankData = await sensorTowerService.fetchNetworkAnalysisRank(
1427 |         os,
1428 |         appIdsArray,
1429 |         startDate,
1430 |         endDate,
1431 |         period,
1432 |         networksArray,
1433 |         countriesArray
1434 |       );
1435 |       
1436 |       return {
1437 |         content: [
1438 |           {
1439 |             type: "text",
1440 |             text: JSON.stringify({
1441 |               os,
1442 |               appIds: appIdsArray,
1443 |               startDate,
1444 |               endDate,
1445 |               period,
1446 |               networks: networksArray,
1447 |               countries: countriesArray,
1448 |               data: networkAnalysisRankData
1449 |             }, null, 2)
1450 |           }
1451 |         ]
1452 |       };
1453 |     } catch (error: any) {
1454 |       const errorMessage = `Error fetching network analysis rank: ${error.message}`;
1455 |       console.error(errorMessage);
1456 |       
1457 |       return {
1458 |         content: [
1459 |           {
1460 |             type: "text",
1461 |             text: JSON.stringify({
1462 |               error: errorMessage,
1463 |               os,
1464 |               appIds: appIds.split(',').map(id => id.trim()),
1465 |               startDate,
1466 |               endDate,
1467 |               period,
1468 |               networks: networks ? networks.split(',').map(network => network.trim()) : undefined,
1469 |               countries: countries ? countries.split(',').map(country => country.trim()) : undefined,
1470 |               timestamp: new Date().toISOString()
1471 |             }, null, 2)
1472 |           }
1473 |         ],
1474 |         isError: true
1475 |       };
1476 |     }
1477 |   }
1478 | );
1479 | 
1480 | // Tool: Get Retention
1481 | server.tool("get_retention",
1482 |   "Fetches retention of apps (from day 1 to day 90), along with the baseline retention.",
1483 |   {
1484 |     os: osSchema,
1485 |     appIds: appIdsRequiredSchema,
1486 |     date_granularity: retentionDateGranularitySchema,
1487 |     start_date: dateSchema,
1488 |     end_date: dateSchema.optional(),
1489 |     country: countrySchema.optional()
1490 |   },
1491 |   async ({ os, appIds, date_granularity, start_date, end_date, country }) => {
1492 |     try {
1493 |       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}` : ''}`);
1494 |       
1495 |       // Process string inputs into arrays
1496 |       const appIdsArray = appIds.split(',').map(id => id.trim());
1497 |       
1498 |       const sensorTowerService = new SensorTowerApiService();
1499 |       const retentionData = await sensorTowerService.fetchRetention(
1500 |         os,
1501 |         appIdsArray,
1502 |         date_granularity,
1503 |         start_date,
1504 |         end_date,
1505 |         country
1506 |       );
1507 |       
1508 |       return {
1509 |         content: [
1510 |           {
1511 |             type: "text",
1512 |             text: JSON.stringify({
1513 |               os,
1514 |               appIds: appIdsArray,
1515 |               date_granularity,
1516 |               start_date,
1517 |               end_date,
1518 |               country,
1519 |               data: retentionData
1520 |             }, null, 2)
1521 |           }
1522 |         ]
1523 |       };
1524 |     } catch (error: any) {
1525 |       const errorMessage = `Error fetching retention data: ${error.message}`;
1526 |       console.error(errorMessage);
1527 |       
1528 |       return {
1529 |         content: [
1530 |           {
1531 |             type: "text",
1532 |             text: JSON.stringify({
1533 |               error: errorMessage,
1534 |               os,
1535 |               appIds: appIds.split(',').map(id => id.trim()),
1536 |               date_granularity,
1537 |               start_date,
1538 |               end_date,
1539 |               country,
1540 |               timestamp: new Date().toISOString()
1541 |             }, null, 2)
1542 |           }
1543 |         ],
1544 |         isError: true
1545 |       };
1546 |     }
1547 |   }
1548 | );
1549 | 
1550 | // Tool: Get Downloads By Sources
1551 | server.tool("get_downloads_by_sources",
1552 |   "Fetches app downloads by sources (organic, paid, and browser) with percentages and absolute values.",
1553 |   {
1554 |     os: osSchema,
1555 |     app_ids: unifiedAppIdsRequiredSchema,
1556 |     countries: countriesRequiredForDownloadsSchema,
1557 |     start_date: dateSchema,
1558 |     end_date: dateSchema,
1559 |     date_granularity: dateGranularityDownloadsSchema
1560 |   },
1561 |   async ({ os, app_ids, countries, start_date, end_date, date_granularity }) => {
1562 |     try {
1563 |       console.error(`Fetching downloads by sources for ${os}, app IDs: ${app_ids}, countries: ${countries}, start date: ${start_date}, end date: ${end_date}`);
1564 |       
1565 |       // Process string inputs into arrays
1566 |       const appIdsArray = app_ids.split(',').map(id => id.trim());
1567 |       const countriesArray = countries.split(',').map(country => country.trim());
1568 |       
1569 |       const sensorTowerService = new SensorTowerApiService();
1570 |       const downloadsBySourcesData = await sensorTowerService.fetchDownloadsBySources(
1571 |         os,
1572 |         appIdsArray,
1573 |         countriesArray,
1574 |         start_date,
1575 |         end_date,
1576 |         date_granularity
1577 |       );
1578 |       
1579 |       return {
1580 |         content: [
1581 |           {
1582 |             type: "text",
1583 |             text: JSON.stringify({
1584 |               os,
1585 |               app_ids: appIdsArray,
1586 |               countries: countriesArray,
1587 |               start_date,
1588 |               end_date,
1589 |               date_granularity,
1590 |               data: downloadsBySourcesData
1591 |             }, null, 2)
1592 |           }
1593 |         ]
1594 |       };
1595 |     } catch (error: any) {
1596 |       const errorMessage = `Error fetching downloads by sources: ${error.message}`;
1597 |       console.error(errorMessage);
1598 |       
1599 |       return {
1600 |         content: [
1601 |           {
1602 |             type: "text",
1603 |             text: JSON.stringify({
1604 |               error: errorMessage,
1605 |               os,
1606 |               app_ids: app_ids.split(',').map(id => id.trim()),
1607 |               countries: countries.split(',').map(country => country.trim()),
1608 |               start_date,
1609 |               end_date,
1610 |               date_granularity,
1611 |               timestamp: new Date().toISOString()
1612 |             }, null, 2)
1613 |           }
1614 |         ],
1615 |         isError: true
1616 |       };
1617 |     }
1618 |   }
1619 | );
1620 | 
1621 | async function runServer(): Promise<void> {
1622 |   try {
1623 |     const transport = new StdioServerTransport();
1624 |     await server.connect(transport);
1625 |     console.error("Sensor Tower Reporting MCP Server running on stdio");
1626 |   } catch (error) {
1627 |     console.error("Failed to start server:", error);
1628 |     throw error;
1629 |   }
1630 | }
1631 | 
1632 | // Graceful shutdown handling
1633 | process.on('SIGINT', () => {
1634 |   console.error('Received SIGINT, shutting down gracefully...');
1635 |   process.exit(0);
1636 | });
1637 | 
1638 | process.on('SIGTERM', () => {
1639 |   console.error('Received SIGTERM, shutting down gracefully...');
1640 |   process.exit(0);
1641 | });
1642 | 
1643 | runServer().catch((error) => {
1644 |   console.error("Fatal error running server:", error);
1645 |   process.exit(1);
1646 | });
1647 | 
```
Page 5/5FirstPrevNextLast