Skip to main content
Glama
ekim197

Uber External Ads API MCP Server

by ekim197
index.ts29.5 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import axios, { AxiosError } from 'axios'; import { z } from 'zod'; // Validation schemas const AuthTokenSchema = z.string().min(1, 'Authorization token is required').optional(); const AdAccountIdSchema = z.string().min(1, 'Ad account ID is required'); const CampaignIdSchema = z.string().min(1, 'Campaign ID is required'); const CampaignFiltersSchema = z.object({ status: z.enum(['ACTIVE', 'PAUSED', 'DELETED']).optional(), page_limit: z.number().min(1).max(100).default(20).optional(), page_token: z.string().optional(), }).optional(); const AdGroupFiltersSchema = z.object({ status: z.enum(['ACTIVE', 'PAUSED', 'DELETED']).optional(), page_limit: z.number().min(1).max(100).default(20).optional(), page_token: z.string().optional(), }).optional(); const CampaignCreateSchema = z.object({ name: z.string().min(1, 'Campaign name is required'), status: z.enum(['ACTIVE', 'PAUSED']).default('ACTIVE'), budget_type: z.enum(['DAILY', 'LIFETIME']).default('DAILY'), budget_amount: z.number().positive('Budget amount must be positive'), start_time: z.string().optional(), end_time: z.string().optional(), objective: z.enum(['AWARENESS', 'CONSIDERATION', 'CONVERSION']).default('CONVERSION'), }); const CampaignUpdateSchema = z.object({ name: z.string().min(1).optional(), status: z.enum(['ACTIVE', 'PAUSED']).optional(), budget_amount: z.number().positive().optional(), end_time: z.string().optional(), }); const ReportRequestSchema = z.object({ account_id: z.string().min(1, 'Account ID is required'), report_type: z.enum(['AD_PERFORMANCE', 'CAMPAIGN_PERFORMANCE', 'CREATIVE_PERFORMANCE']), time_range: z.object({ start_time: z.string().min(1, 'Start time is required'), end_time: z.string().min(1, 'End time is required'), }), columns: z.array(z.string()).min(1, 'At least one column is required'), time_unit: z.enum(['SUMMARY', 'DAILY', 'WEEKLY', 'MONTHLY']), }); const ReportIdSchema = z.string().min(1, 'Report ID is required'); // Base API configuration const UBER_ADS_API_BASE_URL = 'https://api.uber.com/v1/ads'; const DEFAULT_AUTH_TOKEN = process.env.UBER_ADS_AUTH_TOKEN || ''; class UberExternalAdsAPIServer { private server: Server; constructor() { this.server = new Server( { name: 'uber-external-ads-api', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_campaigns', description: 'Get campaigns for an ad account', inputSchema: { type: 'object', properties: { auth_token: { type: 'string', description: 'Bearer token for authentication', }, ad_account_id: { type: 'string', description: 'The ad account UUID', }, filters: { type: 'object', properties: { status: { type: 'string', enum: ['ACTIVE', 'PAUSED', 'DELETED'], description: 'Filter campaigns by status', }, page_limit: { type: 'number', minimum: 1, maximum: 100, default: 20, description: 'Maximum number of campaigns to return per page', }, page_token: { type: 'string', description: 'Token for retrieving the next page of results. Use the next_page_token from previous response.', }, }, additionalProperties: false, }, }, required: ['ad_account_id'], additionalProperties: false, }, }, { name: 'get_campaign', description: 'Get details for a specific campaign', inputSchema: { type: 'object', properties: { auth_token: { type: 'string', description: 'Bearer token for authentication', }, ad_account_id: { type: 'string', description: 'The ad account UUID', }, campaign_id: { type: 'string', description: 'The campaign UUID', }, }, required: ['ad_account_id', 'campaign_id'], additionalProperties: false, }, }, { name: 'get_ad_groups', description: 'Get ad groups for a specific campaign', inputSchema: { type: 'object', properties: { auth_token: { type: 'string', description: 'Bearer token for authentication', }, ad_account_id: { type: 'string', description: 'The ad account UUID', }, campaign_id: { type: 'string', description: 'The campaign UUID to get ad groups for', }, filters: { type: 'object', properties: { status: { type: 'string', enum: ['ACTIVE', 'PAUSED', 'DELETED'], description: 'Filter ad groups by status', }, page_limit: { type: 'number', minimum: 1, maximum: 100, default: 20, description: 'Maximum number of ad groups to return per page', }, page_token: { type: 'string', description: 'Token for retrieving the next page of results. Use the next_page_token from previous response.', }, }, additionalProperties: false, }, }, required: ['ad_account_id', 'campaign_id'], additionalProperties: false, }, }, { name: 'create_campaign', description: 'Create a new campaign', inputSchema: { type: 'object', properties: { auth_token: { type: 'string', description: 'Bearer token for authentication', }, ad_account_id: { type: 'string', description: 'The ad account UUID', }, campaign_data: { type: 'object', properties: { name: { type: 'string', description: 'Campaign name', }, status: { type: 'string', enum: ['ACTIVE', 'PAUSED'], default: 'ACTIVE', description: 'Campaign status', }, budget_type: { type: 'string', enum: ['DAILY', 'LIFETIME'], default: 'DAILY', description: 'Budget type', }, budget_amount: { type: 'number', minimum: 0.01, description: 'Budget amount in USD', }, start_time: { type: 'string', description: 'Campaign start time (ISO 8601)', }, end_time: { type: 'string', description: 'Campaign end time (ISO 8601)', }, objective: { type: 'string', enum: ['AWARENESS', 'CONSIDERATION', 'CONVERSION'], default: 'CONVERSION', description: 'Campaign objective', }, }, required: ['name', 'budget_amount'], additionalProperties: false, }, }, required: ['ad_account_id', 'campaign_data'], additionalProperties: false, }, }, { name: 'update_campaign', description: 'Update an existing campaign', inputSchema: { type: 'object', properties: { auth_token: { type: 'string', description: 'Bearer token for authentication', }, ad_account_id: { type: 'string', description: 'The ad account UUID', }, campaign_id: { type: 'string', description: 'The campaign UUID', }, campaign_data: { type: 'object', properties: { name: { type: 'string', description: 'Campaign name', }, status: { type: 'string', enum: ['ACTIVE', 'PAUSED'], description: 'Campaign status', }, budget_amount: { type: 'number', minimum: 0.01, description: 'Budget amount in USD', }, end_time: { type: 'string', description: 'Campaign end time (ISO 8601)', }, }, additionalProperties: false, }, }, required: ['ad_account_id', 'campaign_id', 'campaign_data'], additionalProperties: false, }, }, { name: 'delete_campaign', description: 'Delete a campaign', inputSchema: { type: 'object', properties: { auth_token: { type: 'string', description: 'Bearer token for authentication', }, ad_account_id: { type: 'string', description: 'The ad account UUID', }, campaign_id: { type: 'string', description: 'The campaign UUID', }, }, required: ['ad_account_id', 'campaign_id'], additionalProperties: false, }, }, { name: 'generate_report', description: 'Generate a report for an ad account, poll until CSV is ready, and fetch the CSV data. Returns CSV data, download URL, and report schema (column meanings for headerless CSV).', inputSchema: { type: 'object', properties: { auth_token: { type: 'string', description: 'Bearer token for authentication', }, ad_account_id: { type: 'string', description: 'The ad account UUID', }, report_data: { type: 'object', properties: { account_id: { type: 'string', description: 'Account ID for the report', }, report_type: { type: 'string', enum: ['AD_PERFORMANCE', 'CAMPAIGN_PERFORMANCE', 'CREATIVE_PERFORMANCE'], description: 'Type of report to generate', }, time_range: { type: 'object', properties: { start_time: { type: 'string', description: 'Start time in ISO 8601 format (e.g., 2025-07-10T00:00:00Z)', }, end_time: { type: 'string', description: 'End time in ISO 8601 format (e.g., 2025-07-21T00:00:00Z)', }, }, required: ['start_time', 'end_time'], additionalProperties: false, }, columns: { type: 'array', items: { type: 'string', }, description: 'Array of columns to include in the report (e.g., ["campaign_id", "currency_code", "ad_spend", "impressions", "clicks"])', }, time_unit: { type: 'string', enum: ['SUMMARY', 'DAILY', 'WEEKLY', 'MONTHLY'], description: 'Time unit for aggregation', }, }, required: ['account_id', 'report_type', 'time_range', 'columns', 'time_unit'], additionalProperties: false, }, }, required: ['ad_account_id', 'report_data'], additionalProperties: false, }, }, ] as Tool[], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_campaigns': return await this.getCampaigns(args); case 'get_campaign': return await this.getCampaign(args); case 'get_ad_groups': return await this.getAdGroups(args); case 'create_campaign': return await this.createCampaign(args); case 'update_campaign': return await this.updateCampaign(args); case 'delete_campaign': return await this.deleteCampaign(args); case 'generate_report': return await this.generateReport(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); } private getAuthToken(providedToken?: string): string { return providedToken || DEFAULT_AUTH_TOKEN; } private async getCampaigns(args: any) { const authToken = this.getAuthToken(args.auth_token); const adAccountId = AdAccountIdSchema.parse(args.ad_account_id); const filters = CampaignFiltersSchema.parse(args.filters); const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/campaigns`; const params = new URLSearchParams(); if (filters?.status) { params.append('status', filters.status); } if (filters?.page_limit) { params.append('page_limit', filters.page_limit.toString()); } if (filters?.page_token) { params.append('page_token', filters.page_token); } try { const response = await axios.get(`${url}?${params.toString()}`, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', }, }); // Extract pagination info from response const data = response.data; const hasNextPage = data.next_page_token ? true : false; const campaignCount = data.campaigns ? data.campaigns.length : 0; let responseText = JSON.stringify(data, null, 2); // Add pagination summary at the top if (hasNextPage || filters?.page_token) { responseText = `📄 Pagination Info: - Campaigns returned: ${campaignCount} - Has next page: ${hasNextPage ? 'Yes' : 'No'} ${hasNextPage ? `- Next page token: ${data.next_page_token}` : ''} ${filters?.page_token ? `- Current page token: ${filters.page_token}` : ''} 📊 Campaign Data: ${responseText}`; } return { content: [ { type: 'text', text: responseText, }, ], }; } catch (error) { return this.handleApiError(error); } } private async getCampaign(args: any) { const authToken = this.getAuthToken(args.auth_token); const adAccountId = AdAccountIdSchema.parse(args.ad_account_id); const campaignId = CampaignIdSchema.parse(args.campaign_id); const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/campaigns/${campaignId}`; try { const response = await axios.get(url, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', }, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { return this.handleApiError(error); } } private async getAdGroups(args: any) { const authToken = this.getAuthToken(args.auth_token); const adAccountId = AdAccountIdSchema.parse(args.ad_account_id); const campaignId = CampaignIdSchema.parse(args.campaign_id); const filters = AdGroupFiltersSchema.parse(args.filters); const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/campaigns/${campaignId}/ad-groups`; const params = new URLSearchParams(); if (filters?.status) { params.append('status', filters.status); } if (filters?.page_limit) { params.append('page_limit', filters.page_limit.toString()); } if (filters?.page_token) { params.append('page_token', filters.page_token); } try { const response = await axios.get(`${url}?${params.toString()}`, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', }, }); // Extract pagination info from response const data = response.data; const hasNextPage = data.next_page_token ? true : false; const adGroupCount = data.adgroups ? data.adgroups.length : (data.ad_groups ? data.ad_groups.length : 0); let responseText = JSON.stringify(data, null, 2); // Add pagination summary at the top if (hasNextPage || filters?.page_token) { responseText = `📄 Pagination Info: - Ad groups returned: ${adGroupCount} - Has next page: ${hasNextPage ? 'Yes' : 'No'} ${hasNextPage ? `- Next page token: ${data.next_page_token}` : ''} ${filters?.page_token ? `- Current page token: ${filters.page_token}` : ''} - Campaign ID: ${campaignId} 📊 Ad Groups Data: ${responseText}`; } return { content: [ { type: 'text', text: responseText, }, ], }; } catch (error) { return this.handleApiError(error); } } private async createCampaign(args: any) { const authToken = this.getAuthToken(args.auth_token); const adAccountId = AdAccountIdSchema.parse(args.ad_account_id); const campaignData = CampaignCreateSchema.parse(args.campaign_data); const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/campaigns`; try { const response = await axios.post(url, campaignData, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { return this.handleApiError(error); } } private async updateCampaign(args: any) { const authToken = this.getAuthToken(args.auth_token); const adAccountId = AdAccountIdSchema.parse(args.ad_account_id); const campaignId = CampaignIdSchema.parse(args.campaign_id); const campaignData = CampaignUpdateSchema.parse(args.campaign_data); const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/campaigns/${campaignId}`; try { const response = await axios.patch(url, campaignData, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { return this.handleApiError(error); } } private async deleteCampaign(args: any) { const authToken = AuthTokenSchema.parse(args.auth_token); const adAccountId = AdAccountIdSchema.parse(args.ad_account_id); const campaignId = CampaignIdSchema.parse(args.campaign_id); const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/campaigns/${campaignId}`; try { const response = await axios.delete(url, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', }, }); return { content: [ { type: 'text', text: `Campaign ${campaignId} deleted successfully`, }, ], }; } catch (error) { return this.handleApiError(error); } } private async generateReport(args: any) { const authToken = this.getAuthToken(args.auth_token); const adAccountId = AdAccountIdSchema.parse(args.ad_account_id); const reportData = ReportRequestSchema.parse(args.report_data); const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/reporting/report`; try { // Step 1: Generate the report const response = await axios.post(url, reportData, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); const reportId = response.data.report_id; if (!reportId) { return { content: [ { type: 'text', text: 'Error: No report ID returned from API', }, ], isError: true, }; } // Step 2: Poll for report status until CSV is ready const pollResult = await this.pollReportStatus(authToken, adAccountId, reportId); if (pollResult.success) { // Step 3: Fetch the CSV data from the download URL const csvResult = await this.fetchCsvData(pollResult.download_url); const responseData: any = { report_id: reportId, status: 'COMPLETED', download_url: pollResult.download_url, report_schema: pollResult.report_schema, file_size: pollResult.file_size, url_expires_at: pollResult.url_expires_at, message: pollResult.message, schema_info: { description: "The CSV file has no headers. Use the report_schema array to understand column meanings.", columns: pollResult.report_schema }, full_response: pollResult.data }; if (csvResult.success) { responseData.csv_data = csvResult.data; responseData.csv_fetch_status = 'SUCCESS'; } else { responseData.csv_fetch_status = 'FAILED'; responseData.csv_fetch_error = csvResult.error; } return { content: [ { type: 'text', text: JSON.stringify(responseData, null, 2), }, ], }; } else { return { content: [ { type: 'text', text: JSON.stringify({ report_id: reportId, status: 'FAILED', message: pollResult.message, full_response: pollResult.data || null }, null, 2), }, ], isError: true, }; } } catch (error) { return this.handleApiError(error); } } private async fetchCsvData(downloadUrl: string): Promise<{success: boolean, data?: string, error?: string}> { try { const response = await axios.get(downloadUrl, { headers: { 'Accept': 'text/csv', }, timeout: 30000, // 30 second timeout for CSV download }); return { success: true, data: response.data }; } catch (error) { const errorMessage = axios.isAxiosError(error) ? `Failed to fetch CSV: HTTP ${error.response?.status} ${error.response?.statusText}` : `Failed to fetch CSV: ${error instanceof Error ? error.message : 'Unknown error'}`; return { success: false, error: errorMessage }; } } private async pollReportStatus(authToken: string, adAccountId: string, reportId: string): Promise<any> { const url = `${UBER_ADS_API_BASE_URL}/${adAccountId}/reporting/${reportId}`; const maxAttempts = 60; // 5 minutes max (60 attempts * 5 seconds) let attempts = 0; while (attempts < maxAttempts) { try { const response = await axios.get(url, { headers: { 'Authorization': `Bearer ${authToken}`, 'Accept': 'application/json', }, }); const status = response.data.status; // If report is ready and has a download URL, return it if (status === 'COMPLETED' && response.data.result?.success_result?.report_url) { return { success: true, data: response.data, download_url: response.data.result.success_result.report_url, report_schema: response.data.result.success_result.report_schema || [], file_size: response.data.result.success_result.file_size, url_expires_at: response.data.result.success_result.url_expires_at, message: `Report completed successfully. CSV available at: ${response.data.result.success_result.report_url}` }; } // If report failed, return error if (status === 'FAILED') { return { success: false, data: response.data, message: 'Report generation failed' }; } // If still processing, wait and try again if (status === 'PROCESSING' || status === 'PENDING') { attempts++; await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds continue; } // Unknown status return { success: false, data: response.data, message: `Unknown report status: ${status}` }; } catch (error) { attempts++; if (attempts >= maxAttempts) { throw error; } await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds before retry } } return { success: false, message: 'Report polling timeout after 5 minutes' }; } private handleApiError(error: unknown) { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; const status = axiosError.response?.status; const statusText = axiosError.response?.statusText; const data = axiosError.response?.data; let errorMessage = `HTTP ${status} ${statusText}`; if (data && typeof data === 'object') { errorMessage += `\n${JSON.stringify(data, null, 2)}`; } return { content: [ { type: 'text', text: `API Error: ${errorMessage}`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`, }, ], isError: true, }; } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Uber External Ads API MCP server running on stdio'); } } const server = new UberExternalAdsAPIServer(); server.run().catch(console.error);

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ekim197/HACKATHON_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server