admob-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ko.md +410 -0
- package/README.md +409 -0
- package/dist/admob/client.js +21 -0
- package/dist/admob/constants.js +86 -0
- package/dist/admob/reports.js +81 -0
- package/dist/auth/index.js +70 -0
- package/dist/auth/oauth-flow.js +123 -0
- package/dist/config.js +115 -0
- package/dist/index.js +30 -0
- package/dist/prompts.js +46 -0
- package/dist/resources.js +60 -0
- package/dist/server.js +33 -0
- package/dist/toolsets/accounts.js +33 -0
- package/dist/toolsets/adunits.js +62 -0
- package/dist/toolsets/apps.js +52 -0
- package/dist/toolsets/common.js +49 -0
- package/dist/toolsets/index.js +17 -0
- package/dist/toolsets/mediation.js +259 -0
- package/dist/toolsets/reports.js +108 -0
- package/dist/version.js +4 -0
- package/package.json +66 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { OAuth2Client } from 'googleapis-common';
|
|
6
|
+
import { requiredScopes, tokenPath } from './index.js';
|
|
7
|
+
async function readClientKeysFile(path) {
|
|
8
|
+
let raw;
|
|
9
|
+
try {
|
|
10
|
+
raw = await fs.readFile(path, 'utf8');
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
const keys = parsed.installed ?? parsed.web;
|
|
18
|
+
if (keys?.client_id && keys?.client_secret) {
|
|
19
|
+
return { clientId: keys.client_id, clientSecret: keys.client_secret };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// fall through to the error below
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`${path} is not a valid OAuth client JSON. Download it from https://console.cloud.google.com/apis/credentials (application type: Desktop app).`);
|
|
26
|
+
}
|
|
27
|
+
async function loadClientKeys(config) {
|
|
28
|
+
if (config.googleClientId && config.googleClientSecret) {
|
|
29
|
+
return { clientId: config.googleClientId, clientSecret: config.googleClientSecret };
|
|
30
|
+
}
|
|
31
|
+
const defaultPath = join(config.credentialsDir, 'oauth_client.json');
|
|
32
|
+
const path = config.oauthClientFile ?? defaultPath;
|
|
33
|
+
const keys = await readClientKeysFile(path);
|
|
34
|
+
if (keys)
|
|
35
|
+
return keys;
|
|
36
|
+
throw new Error(`No OAuth client found. Either:
|
|
37
|
+
- save your OAuth client JSON as ${defaultPath} (or pass --client-file <path>), or
|
|
38
|
+
- set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
|
|
39
|
+
Create one at https://console.cloud.google.com/apis/credentials (application type: Desktop app).`);
|
|
40
|
+
}
|
|
41
|
+
function openBrowser(url) {
|
|
42
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
43
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '""', url.replace(/&/g, '^&')] : [url];
|
|
44
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
45
|
+
// The URL is also printed to the terminal, so a failed launch is not fatal.
|
|
46
|
+
child.on('error', () => { });
|
|
47
|
+
child.unref();
|
|
48
|
+
}
|
|
49
|
+
function resultHtml(message) {
|
|
50
|
+
return `<html><body style="font-family: system-ui, sans-serif; text-align: center; padding-top: 4rem;">
|
|
51
|
+
<h2>admob-mcp-server</h2><p>${message}</p></body></html>`;
|
|
52
|
+
}
|
|
53
|
+
async function waitForAuthorizationCode(server) {
|
|
54
|
+
try {
|
|
55
|
+
return await new Promise((resolve, reject) => {
|
|
56
|
+
server.on('request', (req, res) => {
|
|
57
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
58
|
+
if (url.pathname !== '/oauth2callback') {
|
|
59
|
+
res.writeHead(404);
|
|
60
|
+
res.end();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const error = url.searchParams.get('error');
|
|
64
|
+
const code = url.searchParams.get('code');
|
|
65
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
66
|
+
if (error) {
|
|
67
|
+
res.end(resultHtml(`Authorization failed: ${error}. You can close this tab.`));
|
|
68
|
+
reject(new Error(`Authorization failed: ${error}`));
|
|
69
|
+
}
|
|
70
|
+
else if (code) {
|
|
71
|
+
res.end(resultHtml('Authentication complete. You can close this tab and return to the terminal.'));
|
|
72
|
+
resolve(code);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
res.end(resultHtml('Missing authorization code. You can close this tab.'));
|
|
76
|
+
reject(new Error('Authorization callback did not include a code'));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
server.closeAllConnections();
|
|
83
|
+
server.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function runAuthFlow(config) {
|
|
87
|
+
const keys = await loadClientKeys(config);
|
|
88
|
+
const scopes = requiredScopes(config.readOnly);
|
|
89
|
+
const server = createServer();
|
|
90
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
91
|
+
const { port } = server.address();
|
|
92
|
+
const client = new OAuth2Client({
|
|
93
|
+
clientId: keys.clientId,
|
|
94
|
+
clientSecret: keys.clientSecret,
|
|
95
|
+
redirectUri: `http://127.0.0.1:${port}/oauth2callback`,
|
|
96
|
+
});
|
|
97
|
+
// select_account forces the account chooser so the browser's default
|
|
98
|
+
// Google account is not silently reused.
|
|
99
|
+
const authUrl = client.generateAuthUrl({
|
|
100
|
+
access_type: 'offline',
|
|
101
|
+
prompt: 'consent select_account',
|
|
102
|
+
scope: scopes,
|
|
103
|
+
});
|
|
104
|
+
console.error(`Requesting scopes:\n ${scopes.join('\n ')}\n`);
|
|
105
|
+
console.error(`Opening your browser to sign in. If it does not open, visit:\n\n${authUrl}\n`);
|
|
106
|
+
openBrowser(authUrl);
|
|
107
|
+
const code = await waitForAuthorizationCode(server);
|
|
108
|
+
const { tokens } = await client.getToken(code);
|
|
109
|
+
if (!tokens.refresh_token) {
|
|
110
|
+
throw new Error('Google did not return a refresh token. Remove this app\'s previous access at https://myaccount.google.com/permissions and run "auth" again.');
|
|
111
|
+
}
|
|
112
|
+
await fs.mkdir(config.credentialsDir, { recursive: true });
|
|
113
|
+
const file = tokenPath(config);
|
|
114
|
+
const saved = {
|
|
115
|
+
type: 'authorized_user',
|
|
116
|
+
client_id: keys.clientId,
|
|
117
|
+
client_secret: keys.clientSecret,
|
|
118
|
+
refresh_token: tokens.refresh_token,
|
|
119
|
+
};
|
|
120
|
+
await fs.writeFile(file, `${JSON.stringify(saved, null, 2)}\n`, { mode: 0o600 });
|
|
121
|
+
console.error(`\nSaved credentials to ${file}`);
|
|
122
|
+
console.error('Setup complete. Add admob-mcp-server to your MCP client — see the README for examples.');
|
|
123
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const TOOLSET_NAMES = ['accounts', 'apps', 'adunits', 'reports', 'mediation'];
|
|
4
|
+
export const USAGE = `admob-mcp-server — MCP server for the Google AdMob API
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
admob-mcp-server [flags] Start the MCP server on stdio
|
|
8
|
+
admob-mcp-server auth [flags] Sign in with Google in your browser and save credentials
|
|
9
|
+
|
|
10
|
+
Flags:
|
|
11
|
+
--toolsets <names> Comma-separated toolsets to enable (default: all)
|
|
12
|
+
Available: ${TOOLSET_NAMES.join(', ')}
|
|
13
|
+
--read-only Skip write tools (create/update) and, for "auth",
|
|
14
|
+
request only the read scopes. Default: all tools
|
|
15
|
+
--account <pub-id> AdMob publisher ID (e.g. pub-1234567890123456).
|
|
16
|
+
Default: auto-discovered from the authenticated account
|
|
17
|
+
--client-file <path> OAuth client JSON file for the "auth" flow
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
-v, --version Show version
|
|
20
|
+
|
|
21
|
+
Environment variables:
|
|
22
|
+
ADMOB_ACCOUNT Same as --account
|
|
23
|
+
ADMOB_TOOLSETS Same as --toolsets
|
|
24
|
+
ADMOB_READ_ONLY Set to "true" to skip write tools
|
|
25
|
+
ADMOB_CREDENTIALS_DIR Where token.json is stored (default: ~/.admob-mcp)
|
|
26
|
+
ADMOB_OAUTH_CLIENT_FILE Same as --client-file
|
|
27
|
+
GOOGLE_CLIENT_ID OAuth client ID (with GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN,
|
|
28
|
+
GOOGLE_CLIENT_SECRET used directly without token.json)
|
|
29
|
+
GOOGLE_REFRESH_TOKEN
|
|
30
|
+
`;
|
|
31
|
+
function parseToolsets(raw) {
|
|
32
|
+
const names = raw
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((s) => s.trim())
|
|
35
|
+
.filter((s) => s.length > 0);
|
|
36
|
+
if (names.length === 0) {
|
|
37
|
+
throw new Error('--toolsets requires at least one toolset name');
|
|
38
|
+
}
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
if (!TOOLSET_NAMES.includes(name)) {
|
|
41
|
+
throw new Error(`Unknown toolset "${name}". Available toolsets: ${TOOLSET_NAMES.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
function normalizeAccountId(raw) {
|
|
47
|
+
return raw.startsWith('accounts/') ? raw.slice('accounts/'.length) : raw;
|
|
48
|
+
}
|
|
49
|
+
export function parseCli(argv, env = process.env) {
|
|
50
|
+
let command = 'serve';
|
|
51
|
+
let toolsets;
|
|
52
|
+
let readOnly = env.ADMOB_READ_ONLY === 'true' || env.ADMOB_READ_ONLY === '1';
|
|
53
|
+
let accountId = env.ADMOB_ACCOUNT;
|
|
54
|
+
let oauthClientFile = env.ADMOB_OAUTH_CLIENT_FILE;
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < argv.length) {
|
|
57
|
+
const arg = argv[i];
|
|
58
|
+
i++;
|
|
59
|
+
if (arg === undefined)
|
|
60
|
+
continue;
|
|
61
|
+
const eq = arg.indexOf('=');
|
|
62
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq);
|
|
63
|
+
const inline = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
64
|
+
const takeValue = () => {
|
|
65
|
+
if (inline !== undefined)
|
|
66
|
+
return inline;
|
|
67
|
+
const next = argv[i];
|
|
68
|
+
if (next === undefined)
|
|
69
|
+
throw new Error(`Missing value for ${flag}`);
|
|
70
|
+
i++;
|
|
71
|
+
return next;
|
|
72
|
+
};
|
|
73
|
+
switch (flag) {
|
|
74
|
+
case 'auth':
|
|
75
|
+
command = 'auth';
|
|
76
|
+
break;
|
|
77
|
+
case '--help':
|
|
78
|
+
case '-h':
|
|
79
|
+
command = 'help';
|
|
80
|
+
break;
|
|
81
|
+
case '--version':
|
|
82
|
+
case '-v':
|
|
83
|
+
command = 'version';
|
|
84
|
+
break;
|
|
85
|
+
case '--toolsets':
|
|
86
|
+
toolsets = parseToolsets(takeValue());
|
|
87
|
+
break;
|
|
88
|
+
case '--read-only':
|
|
89
|
+
readOnly = true;
|
|
90
|
+
break;
|
|
91
|
+
case '--account':
|
|
92
|
+
accountId = takeValue();
|
|
93
|
+
break;
|
|
94
|
+
case '--client-file':
|
|
95
|
+
oauthClientFile = takeValue();
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unknown argument "${flag}". Run with --help for usage.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (toolsets === undefined && env.ADMOB_TOOLSETS) {
|
|
102
|
+
toolsets = parseToolsets(env.ADMOB_TOOLSETS);
|
|
103
|
+
}
|
|
104
|
+
const config = {
|
|
105
|
+
accountId: accountId ? normalizeAccountId(accountId) : undefined,
|
|
106
|
+
readOnly,
|
|
107
|
+
toolsets: toolsets ?? TOOLSET_NAMES,
|
|
108
|
+
credentialsDir: env.ADMOB_CREDENTIALS_DIR ?? join(homedir(), '.admob-mcp'),
|
|
109
|
+
oauthClientFile,
|
|
110
|
+
googleClientId: env.GOOGLE_CLIENT_ID,
|
|
111
|
+
googleClientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
112
|
+
googleRefreshToken: env.GOOGLE_REFRESH_TOKEN,
|
|
113
|
+
};
|
|
114
|
+
return { command, config };
|
|
115
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { runAuthFlow } from './auth/oauth-flow.js';
|
|
4
|
+
import { parseCli, USAGE } from './config.js';
|
|
5
|
+
import { createServer } from './server.js';
|
|
6
|
+
import { VERSION } from './version.js';
|
|
7
|
+
async function main() {
|
|
8
|
+
const { command, config } = parseCli(process.argv.slice(2));
|
|
9
|
+
switch (command) {
|
|
10
|
+
case 'help':
|
|
11
|
+
console.log(USAGE);
|
|
12
|
+
return;
|
|
13
|
+
case 'version':
|
|
14
|
+
console.log(VERSION);
|
|
15
|
+
return;
|
|
16
|
+
case 'auth':
|
|
17
|
+
await runAuthFlow(config);
|
|
18
|
+
return;
|
|
19
|
+
case 'serve': {
|
|
20
|
+
const server = createServer(config);
|
|
21
|
+
await server.connect(new StdioServerTransport());
|
|
22
|
+
console.error(`admob-mcp-server v${VERSION} ready (toolsets: ${config.toolsets.join(', ')}; mode: ${config.readOnly ? 'read-only' : 'read-write'})`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
main().catch((err) => {
|
|
28
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const daysArg = z.string().optional().describe('Number of days to look back (default: 7)');
|
|
3
|
+
function userMessage(text) {
|
|
4
|
+
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
|
|
5
|
+
}
|
|
6
|
+
export function registerPrompts(server) {
|
|
7
|
+
server.registerPrompt('top_performing_apps', {
|
|
8
|
+
title: 'Top performing apps',
|
|
9
|
+
description: 'Rank your apps by AdMob revenue over a recent period',
|
|
10
|
+
argsSchema: { days: daysArg },
|
|
11
|
+
}, ({ days }) => {
|
|
12
|
+
const d = days ?? '7';
|
|
13
|
+
return userMessage(`Analyze my AdMob app performance for the last ${d} days.
|
|
14
|
+
|
|
15
|
+
1. Compute the date range: endDate = today, startDate = ${d} days before today (YYYY-MM-DD).
|
|
16
|
+
2. Call generate_network_report with dimensions ["APP"] and metrics ["ESTIMATED_EARNINGS", "IMPRESSIONS", "IMPRESSION_RPM", "MATCH_RATE"], sorted by ESTIMATED_EARNINGS descending.
|
|
17
|
+
3. Present a ranked table of apps: earnings (with currency), impressions, RPM, and each app's share of total revenue.
|
|
18
|
+
4. Call out any app with an unusually low match rate or RPM compared to the others.`);
|
|
19
|
+
});
|
|
20
|
+
server.registerPrompt('revenue_summary', {
|
|
21
|
+
title: 'Revenue summary',
|
|
22
|
+
description: 'Summarize AdMob revenue trends over a recent period',
|
|
23
|
+
argsSchema: { days: daysArg },
|
|
24
|
+
}, ({ days }) => {
|
|
25
|
+
const d = days ?? '7';
|
|
26
|
+
return userMessage(`Summarize my AdMob revenue for the last ${d} days.
|
|
27
|
+
|
|
28
|
+
1. Compute the date range: endDate = today, startDate = ${d} days before today (YYYY-MM-DD).
|
|
29
|
+
2. Call generate_network_report with dimensions ["DATE"] and metrics ["ESTIMATED_EARNINGS", "IMPRESSIONS", "IMPRESSION_RPM", "AD_REQUESTS", "MATCH_RATE"], sorted by DATE ascending.
|
|
30
|
+
3. Report: total earnings (with currency), average daily earnings, and the day-over-day trend.
|
|
31
|
+
4. Flag any anomalies (sudden drops or spikes in earnings, match rate, or RPM) with the dates they happened.`);
|
|
32
|
+
});
|
|
33
|
+
server.registerPrompt('compare_ad_formats', {
|
|
34
|
+
title: 'Compare ad formats',
|
|
35
|
+
description: 'Compare how each ad format (banner, interstitial, rewarded...) monetizes',
|
|
36
|
+
argsSchema: { days: daysArg },
|
|
37
|
+
}, ({ days }) => {
|
|
38
|
+
const d = days ?? '30';
|
|
39
|
+
return userMessage(`Compare the performance of my AdMob ad formats over the last ${d} days.
|
|
40
|
+
|
|
41
|
+
1. Compute the date range: endDate = today, startDate = ${d} days before today (YYYY-MM-DD).
|
|
42
|
+
2. Call generate_network_report with dimensions ["FORMAT"] and metrics ["ESTIMATED_EARNINGS", "IMPRESSIONS", "IMPRESSION_RPM", "IMPRESSION_CTR", "SHOW_RATE"].
|
|
43
|
+
3. Present a comparison table and explain which formats earn the most in total and which are most efficient (highest RPM).
|
|
44
|
+
4. Suggest one concrete change worth testing based on the numbers (e.g. formats with high RPM but low volume).`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CAMPAIGN_REPORT_DIMENSIONS, CAMPAIGN_REPORT_METRICS, MEDIATION_REPORT_DIMENSIONS, MEDIATION_REPORT_METRICS, NETWORK_REPORT_DIMENSIONS, NETWORK_REPORT_METRICS, } from './admob/constants.js';
|
|
2
|
+
const REFERENCES = [
|
|
3
|
+
{
|
|
4
|
+
name: 'network-report-spec',
|
|
5
|
+
title: 'Network report dimensions & metrics',
|
|
6
|
+
description: 'Valid dimensions and metrics for generate_network_report',
|
|
7
|
+
body: {
|
|
8
|
+
dimensions: NETWORK_REPORT_DIMENSIONS,
|
|
9
|
+
metrics: NETWORK_REPORT_METRICS,
|
|
10
|
+
notes: [
|
|
11
|
+
'ESTIMATED_EARNINGS and IMPRESSION_RPM are monetary; the server converts micros to currency units.',
|
|
12
|
+
'DATE, MONTH, and WEEK values are formatted YYYYMMDD.',
|
|
13
|
+
'Not every dimension/metric combination is valid — the API rejects invalid combinations.',
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'mediation-report-spec',
|
|
19
|
+
title: 'Mediation report dimensions & metrics',
|
|
20
|
+
description: 'Valid dimensions and metrics for generate_mediation_report',
|
|
21
|
+
body: {
|
|
22
|
+
dimensions: MEDIATION_REPORT_DIMENSIONS,
|
|
23
|
+
metrics: MEDIATION_REPORT_METRICS,
|
|
24
|
+
notes: [
|
|
25
|
+
"OBSERVED_ECPM is the third-party network's estimated average eCPM, converted from micros.",
|
|
26
|
+
'AD_SOURCE, AD_SOURCE_INSTANCE, and MEDIATION_GROUP dimensions also return display labels.',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'campaign-report-spec',
|
|
32
|
+
title: 'Campaign report dimensions & metrics',
|
|
33
|
+
description: 'Valid dimensions and metrics for generate_campaign_report',
|
|
34
|
+
body: {
|
|
35
|
+
dimensions: CAMPAIGN_REPORT_DIMENSIONS,
|
|
36
|
+
metrics: CAMPAIGN_REPORT_METRICS,
|
|
37
|
+
notes: [
|
|
38
|
+
'The date range must be within the last 30 days.',
|
|
39
|
+
'ESTIMATED_COST and AVERAGE_CPI are monetary; the server converts micros to currency units.',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
export function registerResources(server) {
|
|
45
|
+
for (const ref of REFERENCES) {
|
|
46
|
+
server.registerResource(ref.name, `admob://reference/${ref.name}`, {
|
|
47
|
+
title: ref.title,
|
|
48
|
+
description: ref.description,
|
|
49
|
+
mimeType: 'application/json',
|
|
50
|
+
}, async (uri) => ({
|
|
51
|
+
contents: [
|
|
52
|
+
{
|
|
53
|
+
uri: uri.href,
|
|
54
|
+
mimeType: 'application/json',
|
|
55
|
+
text: JSON.stringify(ref.body, null, 2),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { createAdmobClient, discoverAccountName } from './admob/client.js';
|
|
3
|
+
import { registerPrompts } from './prompts.js';
|
|
4
|
+
import { registerResources } from './resources.js';
|
|
5
|
+
import { registerToolsets } from './toolsets/index.js';
|
|
6
|
+
import { VERSION } from './version.js';
|
|
7
|
+
export function createServer(config) {
|
|
8
|
+
const server = new McpServer({ name: 'admob-mcp-server', version: VERSION });
|
|
9
|
+
// Auth and account discovery are lazy so the server starts (and lists tools)
|
|
10
|
+
// even before credentials exist; failures surface per tool call. Rejected
|
|
11
|
+
// promises are not cached so transient failures can recover.
|
|
12
|
+
let clientPromise;
|
|
13
|
+
let accountPromise;
|
|
14
|
+
const ctx = {
|
|
15
|
+
server,
|
|
16
|
+
client: () => (clientPromise ??= createAdmobClient(config).catch((err) => {
|
|
17
|
+
clientPromise = undefined;
|
|
18
|
+
throw err;
|
|
19
|
+
})),
|
|
20
|
+
accountName: () => (accountPromise ??= ctx
|
|
21
|
+
.client()
|
|
22
|
+
.then((client) => discoverAccountName(client, config.accountId))
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
accountPromise = undefined;
|
|
25
|
+
throw err;
|
|
26
|
+
})),
|
|
27
|
+
readOnly: config.readOnly,
|
|
28
|
+
};
|
|
29
|
+
registerToolsets(ctx, config.toolsets);
|
|
30
|
+
registerPrompts(server);
|
|
31
|
+
registerResources(server);
|
|
32
|
+
return server;
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { accountNameFromId, paginationShape, runTool } from './common.js';
|
|
3
|
+
export function registerAccountsToolset(ctx) {
|
|
4
|
+
ctx.server.registerTool('list_accounts', {
|
|
5
|
+
title: 'List AdMob accounts',
|
|
6
|
+
description: 'Lists the AdMob publisher accounts accessible with the current credentials. Use this to find your publisher ID (pub-XXXXXXXXXXXXXXXX).',
|
|
7
|
+
inputSchema: { ...paginationShape },
|
|
8
|
+
annotations: { readOnlyHint: true },
|
|
9
|
+
}, async (args) => runTool(async () => {
|
|
10
|
+
const client = await ctx.client();
|
|
11
|
+
const res = await client.accounts.list({
|
|
12
|
+
pageSize: args.pageSize,
|
|
13
|
+
pageToken: args.pageToken,
|
|
14
|
+
});
|
|
15
|
+
return res.data;
|
|
16
|
+
}));
|
|
17
|
+
ctx.server.registerTool('get_account', {
|
|
18
|
+
title: 'Get AdMob account',
|
|
19
|
+
description: 'Gets an AdMob publisher account: publisher ID, reporting currency, and reporting time zone. Defaults to the configured or auto-discovered account.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
accountId: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Publisher ID (e.g. pub-1234567890123456). Defaults to the active account.'),
|
|
25
|
+
},
|
|
26
|
+
annotations: { readOnlyHint: true },
|
|
27
|
+
}, async (args) => runTool(async () => {
|
|
28
|
+
const client = await ctx.client();
|
|
29
|
+
const name = args.accountId ? accountNameFromId(args.accountId) : await ctx.accountName();
|
|
30
|
+
const res = await client.accounts.get({ name });
|
|
31
|
+
return res.data;
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AD_FORMATS } from '../admob/constants.js';
|
|
3
|
+
import { paginationShape, runTool } from './common.js';
|
|
4
|
+
export function registerAdUnitsToolset(ctx) {
|
|
5
|
+
ctx.server.registerTool('list_ad_units', {
|
|
6
|
+
title: 'List AdMob ad units',
|
|
7
|
+
description: 'Lists the ad units in the AdMob account, including ad unit ID (ca-app-pub-.../...), format, and the app they belong to.',
|
|
8
|
+
inputSchema: { ...paginationShape },
|
|
9
|
+
annotations: { readOnlyHint: true },
|
|
10
|
+
}, async (args) => runTool(async () => {
|
|
11
|
+
const client = await ctx.client();
|
|
12
|
+
const res = await client.accounts.adUnits.list({
|
|
13
|
+
parent: await ctx.accountName(),
|
|
14
|
+
pageSize: args.pageSize,
|
|
15
|
+
pageToken: args.pageToken,
|
|
16
|
+
});
|
|
17
|
+
return res.data;
|
|
18
|
+
}));
|
|
19
|
+
if (ctx.readOnly)
|
|
20
|
+
return;
|
|
21
|
+
ctx.server.registerTool('create_ad_unit', {
|
|
22
|
+
title: 'Create AdMob ad unit',
|
|
23
|
+
description: 'Creates an ad unit under an app in the AdMob account. Requires the admob.monetization scope.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
appId: z
|
|
26
|
+
.string()
|
|
27
|
+
.describe('AdMob app ID the ad unit belongs to, e.g. ca-app-pub-XXXXXXXXXXXXXXXX~0123456789'),
|
|
28
|
+
displayName: z.string().describe('Display name of the ad unit (shown in the AdMob UI)'),
|
|
29
|
+
adFormat: z.enum(AD_FORMATS).describe('Ad format of the ad unit'),
|
|
30
|
+
adTypes: z
|
|
31
|
+
.array(z.enum(['RICH_MEDIA', 'VIDEO']))
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Ad media types supported by this ad unit'),
|
|
34
|
+
rewardSettings: z
|
|
35
|
+
.object({
|
|
36
|
+
unitAmount: z.number().int().positive().describe('Reward amount'),
|
|
37
|
+
unitType: z.string().describe('Reward item, e.g. "coins"'),
|
|
38
|
+
})
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Reward settings — only for REWARDED / REWARDED_INTERSTITIAL formats'),
|
|
41
|
+
},
|
|
42
|
+
annotations: { readOnlyHint: false },
|
|
43
|
+
}, async (args) => runTool(async () => {
|
|
44
|
+
const client = await ctx.client();
|
|
45
|
+
const res = await client.accounts.adUnits.create({
|
|
46
|
+
parent: await ctx.accountName(),
|
|
47
|
+
requestBody: {
|
|
48
|
+
appId: args.appId,
|
|
49
|
+
displayName: args.displayName,
|
|
50
|
+
adFormat: args.adFormat,
|
|
51
|
+
adTypes: args.adTypes,
|
|
52
|
+
rewardSettings: args.rewardSettings
|
|
53
|
+
? {
|
|
54
|
+
unitAmount: String(args.rewardSettings.unitAmount),
|
|
55
|
+
unitType: args.rewardSettings.unitType,
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
return res.data;
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PLATFORMS } from '../admob/constants.js';
|
|
3
|
+
import { paginationShape, runTool } from './common.js';
|
|
4
|
+
export function registerAppsToolset(ctx) {
|
|
5
|
+
ctx.server.registerTool('list_apps', {
|
|
6
|
+
title: 'List AdMob apps',
|
|
7
|
+
description: 'Lists the apps registered in the AdMob account, including app ID (ca-app-pub-...~...), platform, store link state, and approval state.',
|
|
8
|
+
inputSchema: { ...paginationShape },
|
|
9
|
+
annotations: { readOnlyHint: true },
|
|
10
|
+
}, async (args) => runTool(async () => {
|
|
11
|
+
const client = await ctx.client();
|
|
12
|
+
const res = await client.accounts.apps.list({
|
|
13
|
+
parent: await ctx.accountName(),
|
|
14
|
+
pageSize: args.pageSize,
|
|
15
|
+
pageToken: args.pageToken,
|
|
16
|
+
});
|
|
17
|
+
return res.data;
|
|
18
|
+
}));
|
|
19
|
+
if (ctx.readOnly)
|
|
20
|
+
return;
|
|
21
|
+
ctx.server.registerTool('create_app', {
|
|
22
|
+
title: 'Create AdMob app',
|
|
23
|
+
description: 'Creates an app in the AdMob account. Provide appStoreId to link a published store listing, or displayName to register an unpublished app manually. Requires the admob.monetization scope.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
platform: z.enum(PLATFORMS).describe('App platform'),
|
|
26
|
+
displayName: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Display name for an app that is not published on a store yet'),
|
|
30
|
+
appStoreId: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Store ID to link — Android package name (com.example.app) or iOS numeric App Store ID'),
|
|
34
|
+
},
|
|
35
|
+
annotations: { readOnlyHint: false },
|
|
36
|
+
}, async (args) => runTool(async () => {
|
|
37
|
+
if (!args.displayName && !args.appStoreId) {
|
|
38
|
+
throw new Error('Provide appStoreId (to link a store listing) or displayName (to register the app manually).');
|
|
39
|
+
}
|
|
40
|
+
const client = await ctx.client();
|
|
41
|
+
const res = await client.accounts.apps.create({
|
|
42
|
+
parent: await ctx.accountName(),
|
|
43
|
+
requestBody: {
|
|
44
|
+
platform: args.platform,
|
|
45
|
+
...(args.appStoreId
|
|
46
|
+
? { linkedAppInfo: { appStoreId: args.appStoreId } }
|
|
47
|
+
: { manualAppInfo: { displayName: args.displayName } }),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return res.data;
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AUTH_OPTIONS_HELP } from '../auth/index.js';
|
|
3
|
+
export const paginationShape = {
|
|
4
|
+
pageSize: z.number().int().positive().optional().describe('Maximum number of items to return'),
|
|
5
|
+
pageToken: z.string().optional().describe('nextPageToken from a previous response'),
|
|
6
|
+
};
|
|
7
|
+
export function accountNameFromId(id) {
|
|
8
|
+
return `accounts/${id.replace(/^accounts\//, '')}`;
|
|
9
|
+
}
|
|
10
|
+
function describeError(err) {
|
|
11
|
+
const httpError = err;
|
|
12
|
+
const status = httpError.status ?? httpError.response?.status;
|
|
13
|
+
const message = httpError.response?.data?.error?.message ?? (err instanceof Error ? err.message : String(err));
|
|
14
|
+
if (message.includes('Could not load the default credentials')) {
|
|
15
|
+
return AUTH_OPTIONS_HELP;
|
|
16
|
+
}
|
|
17
|
+
if (status === 401 || message.includes('invalid_grant')) {
|
|
18
|
+
return `Authentication failed: ${message}
|
|
19
|
+
The saved token may be expired or revoked. Re-run "npx admob-mcp-server auth".
|
|
20
|
+
Note: if your OAuth consent screen is in Testing mode, refresh tokens expire after 7 days — publish the app on the consent screen to get long-lived tokens.`;
|
|
21
|
+
}
|
|
22
|
+
if (status === 403) {
|
|
23
|
+
return `Permission denied: ${message}
|
|
24
|
+
Check that:
|
|
25
|
+
1. The AdMob API is enabled for your project: https://console.cloud.google.com/apis/library/admob.googleapis.com
|
|
26
|
+
2. You signed in with the Google account that has access to this AdMob account
|
|
27
|
+
3. The granted scopes cover this tool — write tools need the admob.monetization scope. Tokens created with "auth --read-only" cannot write; re-run "npx admob-mcp-server auth"`;
|
|
28
|
+
}
|
|
29
|
+
if (status === 429) {
|
|
30
|
+
return `AdMob API quota exceeded: ${message}
|
|
31
|
+
Retry later, or reduce the request cost (narrower date range, fewer dimensions). Quotas are per Google Cloud project: https://developers.google.com/admob/api/limits`;
|
|
32
|
+
}
|
|
33
|
+
return `AdMob API error${status ? ` (HTTP ${status})` : ''}: ${message}`;
|
|
34
|
+
}
|
|
35
|
+
export function jsonResult(data) {
|
|
36
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
37
|
+
}
|
|
38
|
+
export function errorResult(err) {
|
|
39
|
+
return { content: [{ type: 'text', text: describeError(err) }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
/** Runs a tool body and converts thrown errors into isError results the model can act on. */
|
|
42
|
+
export async function runTool(fn) {
|
|
43
|
+
try {
|
|
44
|
+
return jsonResult(await fn());
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return errorResult(err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerAccountsToolset } from './accounts.js';
|
|
2
|
+
import { registerAdUnitsToolset } from './adunits.js';
|
|
3
|
+
import { registerAppsToolset } from './apps.js';
|
|
4
|
+
import { registerMediationToolset } from './mediation.js';
|
|
5
|
+
import { registerReportsToolset } from './reports.js';
|
|
6
|
+
const REGISTRY = {
|
|
7
|
+
accounts: registerAccountsToolset,
|
|
8
|
+
apps: registerAppsToolset,
|
|
9
|
+
adunits: registerAdUnitsToolset,
|
|
10
|
+
reports: registerReportsToolset,
|
|
11
|
+
mediation: registerMediationToolset,
|
|
12
|
+
};
|
|
13
|
+
export function registerToolsets(ctx, toolsets) {
|
|
14
|
+
for (const name of toolsets) {
|
|
15
|
+
REGISTRY[name](ctx);
|
|
16
|
+
}
|
|
17
|
+
}
|