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 |
```