#
tokens: 7131/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── bun.lockb
├── Dockerfile
├── eslint.config.js
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── adjust
│   │   └── client.ts
│   └── mcp-adjust.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | 
 93 | # Gatsby files
 94 | .cache/
 95 | # Comment in the public line in if your project uses Gatsby and not Next.js
 96 | # https://nextjs.org/blog/next-9-1#public-directory-support
 97 | # public
 98 | 
 99 | # vuepress build output
100 | .vuepress/dist
101 | 
102 | # vuepress v2.x temp and cache directory
103 | .temp
104 | .cache
105 | 
106 | # Docusaurus cache and generated files
107 | .docusaurus
108 | 
109 | # Serverless directories
110 | .serverless/
111 | 
112 | # FuseBox cache
113 | .fusebox/
114 | 
115 | # DynamoDB Local files
116 | .dynamodb/
117 | 
118 | # TernJS port file
119 | .tern-port
120 | 
121 | # Stores VSCode versions used for testing VSCode extensions
122 | .vscode-test
123 | 
124 | # yarn v2
125 | .yarn/cache
126 | .yarn/unplugged
127 | .yarn/build-state.yml
128 | .yarn/install-state.gz
129 | .pnp.*
130 | 
131 | .DS_Store
132 | dist/
133 | build/
134 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/bitscorp-mcp-mcp-adjust-badge.jpg)](https://mseep.ai/app/bitscorp-mcp-mcp-adjust)
 2 | 
 3 | # Adjust MCP
 4 | [![smithery badge](https://smithery.ai/badge/@bitscorp-mcp/mcp-adjust)](https://smithery.ai/badge/@bitscorp-mcp/mcp-adjust)
 5 | 
 6 | Simple MCP server that interfaces with the Adjust API, allowing you to talk to your Adjust data from any MCP client like Cursor or Claude Desktop. Query reports, metrics, and performance data. Great for on-demand look ups like: "What's the install numbers for the Feb 1 campaign?"
 7 | 
 8 | I am adding more coverage of the Adjust API over time, let me know which tools you need or just open a PR.
 9 | 
10 | ## Installation
11 | Make sure to get your Adjust API key from your Adjust account settings.
12 | 
13 | ### Installing via Smithery
14 | 
15 | To install mcp-adjust for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@bitscorp-mcp/mcp-adjust):
16 | 
17 | ```bash
18 | npx -y @smithery/cli install @bitscorp/mcp-adjust --client claude
19 | ```
20 | 
21 | To install mcp-adjust for Cursor, go to Settings -> Cursor Settings -> Features -> MCP Servers -> + Add
22 | 
23 | Select Type: command and paste the below, using your API key from Adjust
24 | ```
25 | npx -y @smithery/cli@latest run @bitscorp/mcp-adjust --config "{\"apiKey\":\"YOUR_ADJUST_API_KEY\"}"
26 | ```
27 | 
28 | ### Clone and run locally
29 | Clone this repo
30 | Run `npm run build`
31 | Paste this command into Cursor (or whatever MCP Client)
32 | `node /ABSOLUTE/PATH/TO/mcp-adjust/build/mcp-adjust.js YOUR_ADJUST_API_KEY`
33 | 
34 | ## Examples
35 | - use adjust report revenue for the last 7 days
36 | 
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
1 | import neostandard from 'neostandard'
2 | 
3 | export default neostandard({
4 |   ignores: ['node_modules', 'dist']
5 | })
6 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "esModuleInterop": true,
 7 |     "outDir": "./build",
 8 |     "rootDir": "./src",
 9 |     "strict": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - apiKey
10 |     properties:
11 |       apiKey:
12 |         type: string
13 |         description: Adjust API Key.
14 |   commandFunction:
15 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
16 |     |-
17 |     (config) => ({
18 |       command: 'node',
19 |       args: ['build/mcp-adjust.js', config.apiKey]
20 |     })
21 |   exampleConfig:
22 |     apiKey: "123456"
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Set working directory
 5 | WORKDIR /usr/src/app
 6 | 
 7 | # Copy package manifest and tsconfig
 8 | COPY package.json package-lock.json tsconfig.json ./
 9 | 
10 | # Install dependencies; devDependencies are needed for building
11 | RUN npm install --ignore-scripts
12 | 
13 | # Copy the rest of the files
14 | COPY . .
15 | 
16 | # Build the project using tsc
17 | RUN npx tsc && \
18 |     cp build/mcp-adjust.js build/index.js && \
19 |     chmod 755 build/index.js
20 | 
21 | # Set environment variable for Adjust API key
22 | ENV ADJUST_API_KEY=123456
23 | 
24 | # Define the CMD to run the MCP server using the Adjust API key
25 | CMD ["node", "build/index.js", "${ADJUST_API_KEY}"]
26 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "mcp-adjust",
 3 | 	"version": "1.0.3",
 4 | 	"description": "Adjust Reporting MCP server",
 5 | 	"main": "mcp-adjust.js",
 6 | 	"type": "module",
 7 | 	"bin": {
 8 | 		"mcp-adjust": "./build/mcp-adjust.js"
 9 | 	},
10 | 	"scripts": {
11 | 		"build": "tsc && node -e \"require('fs').chmodSync('build/mcp-adjust.js', '755')\"",
12 | 		"lint": "eslint .",
13 | 		"lint:fix": "eslint --fix .",
14 | 		"postinstall": "npm run build",
15 | 		"dev": "npm run build && npx @modelcontextprotocol/inspector node build/mcp-adjust.js"
16 | 	},
17 | 	"repository": {
18 | 		"type": "git",
19 | 		"url": "git+https://github.com/bitscorp-mcp/mcp-adjust.git"
20 | 	},
21 | 	"license": "MIT",
22 | 	"author": "Alexandr Korsak <[email protected]> (https://bitscorp.co)",
23 | 	"contributors": [
24 | 		{
25 | 			"name": "Alexandr Korsak",
26 | 			"email": "[email protected]"
27 | 		}
28 | 	],
29 | 	"bugs": {
30 | 		"url": "https://github.com/bitscorp-mcp/mcp-adjust/issues"
31 | 	},
32 | 	"homepage": "https://github.com/bitscorp-mcp/mcp-adjust#readme",
33 | 	"dependencies": {
34 | 		"@modelcontextprotocol/sdk": "^1.6.0",
35 | 		"axios": "^1.8.3",
36 | 		"node-notifier": "^10.0.1",
37 | 		"zod": "^3.24.2"
38 | 	},
39 | 	"devDependencies": {
40 | 		"@types/node": "^22.13.5",
41 | 		"@types/node-notifier": "^8.0.5",
42 | 		"eslint": "^9.21.0",
43 | 		"neostandard": "^0.12.1",
44 | 		"typescript": "^5.7.3"
45 | 	},
46 | 	"keywords": [
47 | 		"mcp",
48 | 		"model-context-protocol",
49 | 		"ai",
50 | 		"nodejs",
51 | 		"javascript-runtime"
52 | 	],
53 | 	"engines": {
54 | 		"node": ">=22.0.0"
55 | 	}
56 | }
57 | 
```

--------------------------------------------------------------------------------
/src/adjust/client.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import axios, { AxiosInstance } from "axios";
 2 | 
 3 | interface AdjustApiConfig {
 4 |   apiKey: string;
 5 |   baseUrl?: string;
 6 | }
 7 | 
 8 | class AdjustApiClient {
 9 |   private axiosInstance: AxiosInstance;
10 | 
11 |   constructor(private config: AdjustApiConfig) {
12 |     this.axiosInstance = axios.create({
13 |       baseURL: config.baseUrl || "https://automate.adjust.com",
14 |       headers: {
15 |         "Authorization": `Bearer ${config.apiKey}`,
16 |         "Content-Type": "application/json",
17 |       },
18 |     });
19 |   }
20 | 
21 |   async fetchReports(date: string, params: Record<string, any> = {}) {
22 |     try {
23 |       // Build query parameters
24 |       const queryParams: Record<string, any> = {
25 |         ...params
26 |       };
27 | 
28 |       // If date_period is not provided, use the date parameter
29 |       if (!queryParams.date_period) {
30 |         queryParams.date_period = date;
31 |       }
32 | 
33 |       // Make the request to the reports-service endpoint
34 |       const response = await this.axiosInstance.get('/reports-service/report', {
35 |         params: queryParams
36 |       });
37 | 
38 |       return response.data;
39 |     } catch (error) {
40 |       console.error("Adjust API Error:", error);
41 |       throw error;
42 |     }
43 |   }
44 | 
45 |   // Example method to fetch a specific report with common parameters
46 |   async getStandardReport(appTokens: string[], dateRange: string, metrics: string[] = ["installs", "sessions", "revenue"]) {
47 |     return this.fetchReports(dateRange, {
48 |       app_token__in: appTokens.join(','),
49 |       date_period: dateRange,
50 |       dimensions: "app,partner_name,campaign,day",
51 |       metrics: metrics.join(','),
52 |       ad_spend_mode: "network"
53 |     });
54 |   }
55 | }
56 | 
57 | export default AdjustApiClient;
58 | 
```

--------------------------------------------------------------------------------
/src/mcp-adjust.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  3 | import AdjustApiClient from "./adjust/client.js";
  4 | import { z } from "zod";
  5 | 
  6 | // Create an MCP server
  7 | const server = new McpServer({
  8 |   name: "Adjust",
  9 |   version: "1.0.0",
 10 | });
 11 | 
 12 | const args = process.argv.slice(2);
 13 | 
 14 | if (args.length === 0) {
 15 |   console.error("Please provide a Mixpanel service account username and password and a project ID");
 16 |   process.exit(1);
 17 | }
 18 | 
 19 | const ADJUST_AUTH_TOKEN = process.env.ADJUST_AUTH_TOKEN || args[0] || "YOUR ADJUST AUTH TOKEN";
 20 | 
 21 | const client = new AdjustApiClient({
 22 |   apiKey: ADJUST_AUTH_TOKEN
 23 | });
 24 | 
 25 | server.tool("adjust-reporting", "Adjust reporting", {
 26 |   date: z.string()
 27 |     .describe("Date for the report in YYYY-MM-DD format")
 28 |     .default(new Date().toISOString().split('T')[0]),
 29 |   metrics: z.string()
 30 |     .describe("Comma-separated list of metrics to include")
 31 |     .default("installs,sessions,revenue"),
 32 |   dimensions: z.string().optional()
 33 |     .describe("Comma-separated values to group by (e.g., day,country,network). Options include: hour, day, week, month, year, quarter, os_name, device_type, app, app_token, store_id, store_type, currency, currency_code, network, campaign, campaign_network, campaign_id_network, adgroup, adgroup_network, adgroup_id_network, creative, country, country_code, region, partner_name, partner_id, channel, platform"),
 34 |   format_dates: z.boolean().optional()
 35 |     .describe("If false, date dimensions are returned in ISO format"),
 36 |   date_period: z.string().optional()
 37 |     .describe("Date period (e.g., this_month, yesterday, 2023-01-01:2023-01-31, -10d:-3d)"),
 38 |   cohort_maturity: z.enum(["immature", "mature"]).optional()
 39 |     .describe("Display values for immature or only mature cohorts"),
 40 |   utc_offset: z.string().optional()
 41 |     .describe("Timezone used in the report (e.g., +01:00)"),
 42 |   attribution_type: z.enum(["click", "impression", "all"]).optional()
 43 |     .default("click")
 44 |     .describe("Type of engagement the attribution awards"),
 45 |   attribution_source: z.enum(["first", "dynamic"]).optional()
 46 |     .default("dynamic")
 47 |     .describe("Whether in-app activity is assigned to install source or divided"),
 48 |   reattributed: z.enum(["all", "false", "true"]).optional()
 49 |     .default("all")
 50 |     .describe("Filter for reattributed users"),
 51 |   ad_spend_mode: z.enum(["adjust", "network", "mixed"]).optional()
 52 |     .describe("Determines the ad spend source applied in calculations"),
 53 |   sort: z.string().optional()
 54 |     .describe("Comma-separated list of metrics/dimensions to sort by (use - for descending)"),
 55 |   currency: z.string().optional()
 56 |     .default("USD")
 57 |     .describe("Currency used for conversion of money related metrics"),
 58 | }, async (params, extra) => {
 59 |   try {
 60 |     // Convert all params to a query parameters object
 61 |     const queryParams: Record<string, any> = {};
 62 | 
 63 |     // Add all non-undefined parameters to the query
 64 |     Object.entries(params).forEach(([key, value]) => {
 65 |       if (value !== undefined) {
 66 |         queryParams[key] = value;
 67 |       }
 68 |     });
 69 | 
 70 |     // Fetch data from Adjust using our API module
 71 |     const reportData = await client.fetchReports(params.date, queryParams);
 72 | 
 73 |     // Handle empty response
 74 |     if (!reportData || Object.keys(reportData).length === 0) {
 75 |       return {
 76 |         isError: false,
 77 |         content: [
 78 |           {
 79 |             type: "text" as const,
 80 |             text: `## Adjust Report for ${params.date}\n\nNo data available for the specified parameters.`,
 81 |           }
 82 |         ],
 83 |       };
 84 |     }
 85 | 
 86 |     // Simple analysis of the data
 87 |     const analysis = analyzeReportData(reportData);
 88 | 
 89 |     return {
 90 |       isError: false,
 91 |       content: [
 92 |         {
 93 |           type: "text" as const,
 94 |           text: `## Adjust Report for ${params.date}\n\n${analysis}\n\n\`\`\`json\n${JSON.stringify(reportData, null, 2)}\n\`\`\``,
 95 |         }
 96 |       ],
 97 |     };
 98 |   } catch (error) {
 99 |     console.error("Error fetching or analyzing Adjust data:", error);
100 | 
101 |     // Extract status code and message
102 |     let statusCode = 500;
103 |     let errorMessage = "Unknown error";
104 | 
105 |     if (error instanceof Error) {
106 |       errorMessage = error.message;
107 | 
108 |       // Check for Axios error with response
109 |       if ('response' in error && error.response && typeof error.response === 'object') {
110 |         const axiosError = error as any;
111 |         statusCode = axiosError.response.status;
112 | 
113 |         // Provide helpful messages based on status code
114 |         switch (statusCode) {
115 |           case 400:
116 |             errorMessage = "Bad request: Your query contains invalid parameters or is malformed.";
117 |             break;
118 |           case 401:
119 |             errorMessage = "Unauthorized: Please check your API credentials.";
120 |             break;
121 |           case 403:
122 |             errorMessage = "Forbidden: You don't have permission to access this data.";
123 |             break;
124 |           case 429:
125 |             errorMessage = "Too many requests: You've exceeded the rate limit (max 50 simultaneous requests).";
126 |             break;
127 |           case 503:
128 |             errorMessage = "Service unavailable: The Adjust API is currently unavailable.";
129 |             break;
130 |           case 504:
131 |             errorMessage = "Gateway timeout: The query took too long to process.";
132 |             break;
133 |           default:
134 |             errorMessage = axiosError.response.data?.message || errorMessage;
135 |         }
136 |       }
137 |     }
138 | 
139 |     return {
140 |       isError: true,
141 |       content: [
142 |         {
143 |           type: "text" as const,
144 |           text: `## Error Fetching Adjust Data\n\n**Status Code**: ${statusCode}\n\n**Error**: ${errorMessage}\n\nPlease check your parameters and try again.`,
145 |         },
146 |       ],
147 |     };
148 |   }
149 | });
150 | 
151 | // Add a new tool for standard reports with simplified parameters
152 | server.tool("adjust-standard-report", "Get a standard Adjust report with common metrics", {
153 |   app_tokens: z.string()
154 |     .describe("Comma-separated list of app tokens to include")
155 |     .default(""),
156 |   date_range: z.string()
157 |     .describe("Date range (e.g., 2023-01-01:2023-01-31, yesterday, last_7_days, this_month)")
158 |     .default("last_7_days"),
159 |   report_type: z.enum(["performance", "retention", "cohort", "revenue"])
160 |     .describe("Type of standard report to generate")
161 |     .default("performance"),
162 | }, async (params, extra) => {
163 |   try {
164 |     // Set up metrics and dimensions based on report type
165 |     let metrics: string[] = [];
166 |     let dimensions: string[] = [];
167 | 
168 |     switch (params.report_type) {
169 |       case "performance":
170 |         metrics = ["installs", "clicks", "impressions", "network_cost", "network_ecpi", "sessions"];
171 |         dimensions = ["app", "partner_name", "campaign", "day"];
172 |         break;
173 |       case "retention":
174 |         metrics = ["installs", "retention_rate_d1", "retention_rate_d7", "retention_rate_d30"];
175 |         dimensions = ["app", "partner_name", "campaign", "day"];
176 |         break;
177 |       case "cohort":
178 |         metrics = ["installs", "sessions_per_user", "revenue_per_user"];
179 |         dimensions = ["app", "partner_name", "campaign", "cohort"];
180 |         break;
181 |       case "revenue":
182 |         metrics = ["installs", "revenue", "arpu", "arpdau"];
183 |         dimensions = ["app", "partner_name", "campaign", "day"];
184 |         break;
185 |     }
186 | 
187 |     // Build query parameters
188 |     const queryParams: Record<string, any> = {
189 |       date_period: params.date_range,
190 |       metrics: metrics.join(','),
191 |       dimensions: dimensions.join(','),
192 |       ad_spend_mode: "network"
193 |     };
194 | 
195 |     // Handle app tokens
196 |     if (params.app_tokens) {
197 |       queryParams.app_token__in = params.app_tokens;
198 |     }
199 | 
200 |     // Fetch data from Adjust
201 |     const reportData = await client.fetchReports(params.date_range, queryParams);
202 | 
203 |     // Generate a report title based on the type
204 |     const reportTitle = `## Adjust ${params.report_type.charAt(0).toUpperCase() + params.report_type.slice(1)} Report`;
205 |     const dateRangeInfo = `### Date Range: ${params.date_range}`;
206 | 
207 |     // Analyze the data
208 |     const analysis = analyzeReportData(reportData);
209 | 
210 |     return {
211 |       isError: false,
212 |       content: [
213 |         {
214 |           type: "text" as const,
215 |           text: `${reportTitle}\n${dateRangeInfo}\n\n${analysis}\n\n\`\`\`json\n${JSON.stringify(reportData, null, 2)}\n\`\`\``,
216 |         }
217 |       ],
218 |     };
219 |   } catch (error) {
220 |     console.error("Error fetching standard Adjust report:", error);
221 | 
222 |     // Extract status code and message (same error handling as before)
223 |     let statusCode = 500;
224 |     let errorMessage = "Unknown error";
225 | 
226 |     if (error instanceof Error) {
227 |       errorMessage = error.message;
228 | 
229 |       if ('response' in error && error.response && typeof error.response === 'object') {
230 |         const axiosError = error as any;
231 |         statusCode = axiosError.response.status;
232 | 
233 |         // Provide helpful messages based on status code
234 |         switch (statusCode) {
235 |           case 400:
236 |             errorMessage = "Bad request: Your query contains invalid parameters or is malformed.";
237 |             break;
238 |           case 401:
239 |             errorMessage = "Unauthorized: Please check your API credentials.";
240 |             break;
241 |           case 403:
242 |             errorMessage = "Forbidden: You don't have permission to access this data.";
243 |             break;
244 |           case 429:
245 |             errorMessage = "Too many requests: You've exceeded the rate limit.";
246 |             break;
247 |           case 503:
248 |             errorMessage = "Service unavailable: The Adjust API is currently unavailable.";
249 |             break;
250 |           case 504:
251 |             errorMessage = "Gateway timeout: The query took too long to process.";
252 |             break;
253 |           default:
254 |             errorMessage = axiosError.response.data?.message || errorMessage;
255 |         }
256 |       }
257 |     }
258 | 
259 |     return {
260 |       isError: true,
261 |       content: [
262 |         {
263 |           type: "text" as const,
264 |           text: `## Error Fetching Adjust Standard Report\n\n**Status Code**: ${statusCode}\n\n**Error**: ${errorMessage}\n\nPlease check your parameters and try again.`,
265 |         },
266 |       ],
267 |     };
268 |   }
269 | });
270 | 
271 | // Helper function to analyze report data
272 | function analyzeReportData(data: any) {
273 |   let analysis = "";
274 | 
275 |   if (!data || !data.rows || data.rows.length === 0) {
276 |     return "No data available for analysis.";
277 |   }
278 | 
279 |   // Add totals summary
280 |   if (data.totals) {
281 |     analysis += "## Summary\n";
282 |     Object.entries(data.totals).forEach(([metric, value]) => {
283 |       analysis += `**Total ${metric}**: ${value}\n`;
284 |     });
285 |     analysis += "\n";
286 |   }
287 | 
288 |   // Add row analysis
289 |   analysis += "## Breakdown\n";
290 | 
291 |   // Get all metrics (non-dimension fields) from the first row
292 |   const firstRow = data.rows[0];
293 |   const metrics = Object.keys(firstRow).filter(key =>
294 |     !['attr_dependency', 'app', 'partner_name', 'campaign', 'campaign_id_network',
295 |       'campaign_network', 'adgroup', 'creative', 'country', 'os_name', 'day', 'week',
296 |       'month', 'year'].includes(key)
297 |   );
298 | 
299 |   // Analyze each row
300 |   data.rows.forEach((row: any, index: number) => {
301 |     // Create a title for this row based on available dimensions
302 |     let rowTitle = "";
303 |     if (row.campaign) rowTitle += `Campaign: ${row.campaign} `;
304 |     if (row.partner_name) rowTitle += `(${row.partner_name}) `;
305 |     if (row.app) rowTitle += `- App: ${row.app} `;
306 |     if (row.country) rowTitle += `- Country: ${row.country} `;
307 |     if (row.os_name) rowTitle += `- OS: ${row.os_name} `;
308 | 
309 |     analysis += `### ${rowTitle || `Row ${index + 1}`}\n`;
310 | 
311 |     // Add metrics for this row
312 |     metrics.forEach(metric => {
313 |       if (row[metric] !== undefined) {
314 |         analysis += `**${metric}**: ${row[metric]}\n`;
315 |       }
316 |     });
317 |     analysis += "\n";
318 |   });
319 | 
320 |   // Add warnings if any
321 |   if (data.warnings && data.warnings.length > 0) {
322 |     analysis += "## Warnings\n";
323 |     data.warnings.forEach((warning: string) => {
324 |       analysis += `- ${warning}\n`;
325 |     });
326 |   }
327 | 
328 |   return analysis;
329 | }
330 | 
331 | // Start the server
332 | async function main() {
333 |   try {
334 |     const transport = new StdioServerTransport();
335 |     await server.connect(transport);
336 | 
337 |     console.error("Adjust MCP Server running");
338 |   } catch (error) {
339 |     console.error("Error starting server:", error);
340 |     process.exit(1);
341 |   }
342 | }
343 | 
344 | main();
```