# 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:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store
dist/
build/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
[](https://mseep.ai/app/bitscorp-mcp-mcp-adjust)
# Adjust MCP
[](https://smithery.ai/badge/@bitscorp-mcp/mcp-adjust)
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?"
I am adding more coverage of the Adjust API over time, let me know which tools you need or just open a PR.
## Installation
Make sure to get your Adjust API key from your Adjust account settings.
### Installing via Smithery
To install mcp-adjust for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@bitscorp-mcp/mcp-adjust):
```bash
npx -y @smithery/cli install @bitscorp/mcp-adjust --client claude
```
To install mcp-adjust for Cursor, go to Settings -> Cursor Settings -> Features -> MCP Servers -> + Add
Select Type: command and paste the below, using your API key from Adjust
```
npx -y @smithery/cli@latest run @bitscorp/mcp-adjust --config "{\"apiKey\":\"YOUR_ADJUST_API_KEY\"}"
```
### Clone and run locally
Clone this repo
Run `npm run build`
Paste this command into Cursor (or whatever MCP Client)
`node /ABSOLUTE/PATH/TO/mcp-adjust/build/mcp-adjust.js YOUR_ADJUST_API_KEY`
## Examples
- use adjust report revenue for the last 7 days
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import neostandard from 'neostandard'
export default neostandard({
ignores: ['node_modules', 'dist']
})
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"esModuleInterop": true,
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- apiKey
properties:
apiKey:
type: string
description: Adjust API Key.
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({
command: 'node',
args: ['build/mcp-adjust.js', config.apiKey]
})
exampleConfig:
apiKey: "123456"
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine
# Set working directory
WORKDIR /usr/src/app
# Copy package manifest and tsconfig
COPY package.json package-lock.json tsconfig.json ./
# Install dependencies; devDependencies are needed for building
RUN npm install --ignore-scripts
# Copy the rest of the files
COPY . .
# Build the project using tsc
RUN npx tsc && \
cp build/mcp-adjust.js build/index.js && \
chmod 755 build/index.js
# Set environment variable for Adjust API key
ENV ADJUST_API_KEY=123456
# Define the CMD to run the MCP server using the Adjust API key
CMD ["node", "build/index.js", "${ADJUST_API_KEY}"]
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-adjust",
"version": "1.0.3",
"description": "Adjust Reporting MCP server",
"main": "mcp-adjust.js",
"type": "module",
"bin": {
"mcp-adjust": "./build/mcp-adjust.js"
},
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/mcp-adjust.js', '755')\"",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"postinstall": "npm run build",
"dev": "npm run build && npx @modelcontextprotocol/inspector node build/mcp-adjust.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bitscorp-mcp/mcp-adjust.git"
},
"license": "MIT",
"author": "Alexandr Korsak <[email protected]> (https://bitscorp.co)",
"contributors": [
{
"name": "Alexandr Korsak",
"email": "[email protected]"
}
],
"bugs": {
"url": "https://github.com/bitscorp-mcp/mcp-adjust/issues"
},
"homepage": "https://github.com/bitscorp-mcp/mcp-adjust#readme",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"axios": "^1.8.3",
"node-notifier": "^10.0.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^22.13.5",
"@types/node-notifier": "^8.0.5",
"eslint": "^9.21.0",
"neostandard": "^0.12.1",
"typescript": "^5.7.3"
},
"keywords": [
"mcp",
"model-context-protocol",
"ai",
"nodejs",
"javascript-runtime"
],
"engines": {
"node": ">=22.0.0"
}
}
```
--------------------------------------------------------------------------------
/src/adjust/client.ts:
--------------------------------------------------------------------------------
```typescript
import axios, { AxiosInstance } from "axios";
interface AdjustApiConfig {
apiKey: string;
baseUrl?: string;
}
class AdjustApiClient {
private axiosInstance: AxiosInstance;
constructor(private config: AdjustApiConfig) {
this.axiosInstance = axios.create({
baseURL: config.baseUrl || "https://automate.adjust.com",
headers: {
"Authorization": `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
},
});
}
async fetchReports(date: string, params: Record<string, any> = {}) {
try {
// Build query parameters
const queryParams: Record<string, any> = {
...params
};
// If date_period is not provided, use the date parameter
if (!queryParams.date_period) {
queryParams.date_period = date;
}
// Make the request to the reports-service endpoint
const response = await this.axiosInstance.get('/reports-service/report', {
params: queryParams
});
return response.data;
} catch (error) {
console.error("Adjust API Error:", error);
throw error;
}
}
// Example method to fetch a specific report with common parameters
async getStandardReport(appTokens: string[], dateRange: string, metrics: string[] = ["installs", "sessions", "revenue"]) {
return this.fetchReports(dateRange, {
app_token__in: appTokens.join(','),
date_period: dateRange,
dimensions: "app,partner_name,campaign,day",
metrics: metrics.join(','),
ad_spend_mode: "network"
});
}
}
export default AdjustApiClient;
```
--------------------------------------------------------------------------------
/src/mcp-adjust.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import AdjustApiClient from "./adjust/client.js";
import { z } from "zod";
// Create an MCP server
const server = new McpServer({
name: "Adjust",
version: "1.0.0",
});
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Please provide a Mixpanel service account username and password and a project ID");
process.exit(1);
}
const ADJUST_AUTH_TOKEN = process.env.ADJUST_AUTH_TOKEN || args[0] || "YOUR ADJUST AUTH TOKEN";
const client = new AdjustApiClient({
apiKey: ADJUST_AUTH_TOKEN
});
server.tool("adjust-reporting", "Adjust reporting", {
date: z.string()
.describe("Date for the report in YYYY-MM-DD format")
.default(new Date().toISOString().split('T')[0]),
metrics: z.string()
.describe("Comma-separated list of metrics to include")
.default("installs,sessions,revenue"),
dimensions: z.string().optional()
.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"),
format_dates: z.boolean().optional()
.describe("If false, date dimensions are returned in ISO format"),
date_period: z.string().optional()
.describe("Date period (e.g., this_month, yesterday, 2023-01-01:2023-01-31, -10d:-3d)"),
cohort_maturity: z.enum(["immature", "mature"]).optional()
.describe("Display values for immature or only mature cohorts"),
utc_offset: z.string().optional()
.describe("Timezone used in the report (e.g., +01:00)"),
attribution_type: z.enum(["click", "impression", "all"]).optional()
.default("click")
.describe("Type of engagement the attribution awards"),
attribution_source: z.enum(["first", "dynamic"]).optional()
.default("dynamic")
.describe("Whether in-app activity is assigned to install source or divided"),
reattributed: z.enum(["all", "false", "true"]).optional()
.default("all")
.describe("Filter for reattributed users"),
ad_spend_mode: z.enum(["adjust", "network", "mixed"]).optional()
.describe("Determines the ad spend source applied in calculations"),
sort: z.string().optional()
.describe("Comma-separated list of metrics/dimensions to sort by (use - for descending)"),
currency: z.string().optional()
.default("USD")
.describe("Currency used for conversion of money related metrics"),
}, async (params, extra) => {
try {
// Convert all params to a query parameters object
const queryParams: Record<string, any> = {};
// Add all non-undefined parameters to the query
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams[key] = value;
}
});
// Fetch data from Adjust using our API module
const reportData = await client.fetchReports(params.date, queryParams);
// Handle empty response
if (!reportData || Object.keys(reportData).length === 0) {
return {
isError: false,
content: [
{
type: "text" as const,
text: `## Adjust Report for ${params.date}\n\nNo data available for the specified parameters.`,
}
],
};
}
// Simple analysis of the data
const analysis = analyzeReportData(reportData);
return {
isError: false,
content: [
{
type: "text" as const,
text: `## Adjust Report for ${params.date}\n\n${analysis}\n\n\`\`\`json\n${JSON.stringify(reportData, null, 2)}\n\`\`\``,
}
],
};
} catch (error) {
console.error("Error fetching or analyzing Adjust data:", error);
// Extract status code and message
let statusCode = 500;
let errorMessage = "Unknown error";
if (error instanceof Error) {
errorMessage = error.message;
// Check for Axios error with response
if ('response' in error && error.response && typeof error.response === 'object') {
const axiosError = error as any;
statusCode = axiosError.response.status;
// Provide helpful messages based on status code
switch (statusCode) {
case 400:
errorMessage = "Bad request: Your query contains invalid parameters or is malformed.";
break;
case 401:
errorMessage = "Unauthorized: Please check your API credentials.";
break;
case 403:
errorMessage = "Forbidden: You don't have permission to access this data.";
break;
case 429:
errorMessage = "Too many requests: You've exceeded the rate limit (max 50 simultaneous requests).";
break;
case 503:
errorMessage = "Service unavailable: The Adjust API is currently unavailable.";
break;
case 504:
errorMessage = "Gateway timeout: The query took too long to process.";
break;
default:
errorMessage = axiosError.response.data?.message || errorMessage;
}
}
}
return {
isError: true,
content: [
{
type: "text" as const,
text: `## Error Fetching Adjust Data\n\n**Status Code**: ${statusCode}\n\n**Error**: ${errorMessage}\n\nPlease check your parameters and try again.`,
},
],
};
}
});
// Add a new tool for standard reports with simplified parameters
server.tool("adjust-standard-report", "Get a standard Adjust report with common metrics", {
app_tokens: z.string()
.describe("Comma-separated list of app tokens to include")
.default(""),
date_range: z.string()
.describe("Date range (e.g., 2023-01-01:2023-01-31, yesterday, last_7_days, this_month)")
.default("last_7_days"),
report_type: z.enum(["performance", "retention", "cohort", "revenue"])
.describe("Type of standard report to generate")
.default("performance"),
}, async (params, extra) => {
try {
// Set up metrics and dimensions based on report type
let metrics: string[] = [];
let dimensions: string[] = [];
switch (params.report_type) {
case "performance":
metrics = ["installs", "clicks", "impressions", "network_cost", "network_ecpi", "sessions"];
dimensions = ["app", "partner_name", "campaign", "day"];
break;
case "retention":
metrics = ["installs", "retention_rate_d1", "retention_rate_d7", "retention_rate_d30"];
dimensions = ["app", "partner_name", "campaign", "day"];
break;
case "cohort":
metrics = ["installs", "sessions_per_user", "revenue_per_user"];
dimensions = ["app", "partner_name", "campaign", "cohort"];
break;
case "revenue":
metrics = ["installs", "revenue", "arpu", "arpdau"];
dimensions = ["app", "partner_name", "campaign", "day"];
break;
}
// Build query parameters
const queryParams: Record<string, any> = {
date_period: params.date_range,
metrics: metrics.join(','),
dimensions: dimensions.join(','),
ad_spend_mode: "network"
};
// Handle app tokens
if (params.app_tokens) {
queryParams.app_token__in = params.app_tokens;
}
// Fetch data from Adjust
const reportData = await client.fetchReports(params.date_range, queryParams);
// Generate a report title based on the type
const reportTitle = `## Adjust ${params.report_type.charAt(0).toUpperCase() + params.report_type.slice(1)} Report`;
const dateRangeInfo = `### Date Range: ${params.date_range}`;
// Analyze the data
const analysis = analyzeReportData(reportData);
return {
isError: false,
content: [
{
type: "text" as const,
text: `${reportTitle}\n${dateRangeInfo}\n\n${analysis}\n\n\`\`\`json\n${JSON.stringify(reportData, null, 2)}\n\`\`\``,
}
],
};
} catch (error) {
console.error("Error fetching standard Adjust report:", error);
// Extract status code and message (same error handling as before)
let statusCode = 500;
let errorMessage = "Unknown error";
if (error instanceof Error) {
errorMessage = error.message;
if ('response' in error && error.response && typeof error.response === 'object') {
const axiosError = error as any;
statusCode = axiosError.response.status;
// Provide helpful messages based on status code
switch (statusCode) {
case 400:
errorMessage = "Bad request: Your query contains invalid parameters or is malformed.";
break;
case 401:
errorMessage = "Unauthorized: Please check your API credentials.";
break;
case 403:
errorMessage = "Forbidden: You don't have permission to access this data.";
break;
case 429:
errorMessage = "Too many requests: You've exceeded the rate limit.";
break;
case 503:
errorMessage = "Service unavailable: The Adjust API is currently unavailable.";
break;
case 504:
errorMessage = "Gateway timeout: The query took too long to process.";
break;
default:
errorMessage = axiosError.response.data?.message || errorMessage;
}
}
}
return {
isError: true,
content: [
{
type: "text" as const,
text: `## Error Fetching Adjust Standard Report\n\n**Status Code**: ${statusCode}\n\n**Error**: ${errorMessage}\n\nPlease check your parameters and try again.`,
},
],
};
}
});
// Helper function to analyze report data
function analyzeReportData(data: any) {
let analysis = "";
if (!data || !data.rows || data.rows.length === 0) {
return "No data available for analysis.";
}
// Add totals summary
if (data.totals) {
analysis += "## Summary\n";
Object.entries(data.totals).forEach(([metric, value]) => {
analysis += `**Total ${metric}**: ${value}\n`;
});
analysis += "\n";
}
// Add row analysis
analysis += "## Breakdown\n";
// Get all metrics (non-dimension fields) from the first row
const firstRow = data.rows[0];
const metrics = Object.keys(firstRow).filter(key =>
!['attr_dependency', 'app', 'partner_name', 'campaign', 'campaign_id_network',
'campaign_network', 'adgroup', 'creative', 'country', 'os_name', 'day', 'week',
'month', 'year'].includes(key)
);
// Analyze each row
data.rows.forEach((row: any, index: number) => {
// Create a title for this row based on available dimensions
let rowTitle = "";
if (row.campaign) rowTitle += `Campaign: ${row.campaign} `;
if (row.partner_name) rowTitle += `(${row.partner_name}) `;
if (row.app) rowTitle += `- App: ${row.app} `;
if (row.country) rowTitle += `- Country: ${row.country} `;
if (row.os_name) rowTitle += `- OS: ${row.os_name} `;
analysis += `### ${rowTitle || `Row ${index + 1}`}\n`;
// Add metrics for this row
metrics.forEach(metric => {
if (row[metric] !== undefined) {
analysis += `**${metric}**: ${row[metric]}\n`;
}
});
analysis += "\n";
});
// Add warnings if any
if (data.warnings && data.warnings.length > 0) {
analysis += "## Warnings\n";
data.warnings.forEach((warning: string) => {
analysis += `- ${warning}\n`;
});
}
return analysis;
}
// Start the server
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Adjust MCP Server running");
} catch (error) {
console.error("Error starting server:", error);
process.exit(1);
}
}
main();
```