microsoft-ads-mcp 0.1.0__tar.gz
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.
- microsoft_ads_mcp-0.1.0/.claude/settings.local.json +5 -0
- microsoft_ads_mcp-0.1.0/.env.example +45 -0
- microsoft_ads_mcp-0.1.0/.github/workflows/ci.yml +24 -0
- microsoft_ads_mcp-0.1.0/.gitignore +61 -0
- microsoft_ads_mcp-0.1.0/.python-version +1 -0
- microsoft_ads_mcp-0.1.0/AGENTS.md +4 -0
- microsoft_ads_mcp-0.1.0/CLAUDE.md +1 -0
- microsoft_ads_mcp-0.1.0/LICENSE +21 -0
- microsoft_ads_mcp-0.1.0/PKG-INFO +315 -0
- microsoft_ads_mcp-0.1.0/README.md +292 -0
- microsoft_ads_mcp-0.1.0/pyproject.toml +75 -0
- microsoft_ads_mcp-0.1.0/scripts/ci.sh +7 -0
- microsoft_ads_mcp-0.1.0/skill/microsoft-ads-optimizer/SKILL.md +212 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/__init__.py +3 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/__main__.py +18 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/__init__.py +1 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/auth.py +149 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/client.py +201 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/errors.py +82 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/config.py +79 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/domain/__init__.py +1 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/domain/entities.py +1065 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/server.py +145 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/__init__.py +59 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/account_properties.py +88 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/accounts.py +51 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/bulk.py +158 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/campaigns.py +128 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/conversions.py +481 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/criteria.py +630 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/extensions.py +331 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/geo.py +140 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/insights.py +381 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/mutations.py +735 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/negatives.py +146 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/reporting.py +252 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/url_resolution.py +93 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/__init__.py +31 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/_common.py +24 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/auth_tools.py +43 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/health.py +70 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/insight_tools.py +175 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/read_tools.py +317 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/reporting_tools.py +56 -0
- microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/write_tools.py +1071 -0
- microsoft_ads_mcp-0.1.0/tests/conftest.py +29 -0
- microsoft_ads_mcp-0.1.0/tests/test_auth.py +60 -0
- microsoft_ads_mcp-0.1.0/tests/test_client_reauth.py +156 -0
- microsoft_ads_mcp-0.1.0/tests/test_config.py +39 -0
- microsoft_ads_mcp-0.1.0/tests/test_conversions.py +530 -0
- microsoft_ads_mcp-0.1.0/tests/test_editorial_status.py +47 -0
- microsoft_ads_mcp-0.1.0/tests/test_errors.py +43 -0
- microsoft_ads_mcp-0.1.0/tests/test_extensions.py +275 -0
- microsoft_ads_mcp-0.1.0/tests/test_geo_criteria_bulk.py +562 -0
- microsoft_ads_mcp-0.1.0/tests/test_health_auth_state.py +89 -0
- microsoft_ads_mcp-0.1.0/tests/test_insights.py +485 -0
- microsoft_ads_mcp-0.1.0/tests/test_mutations.py +386 -0
- microsoft_ads_mcp-0.1.0/tests/test_negatives.py +113 -0
- microsoft_ads_mcp-0.1.0/tests/test_read_only_gating.py +87 -0
- microsoft_ads_mcp-0.1.0/tests/test_reporting_scope.py +80 -0
- microsoft_ads_mcp-0.1.0/tests/test_tool_search.py +64 -0
- microsoft_ads_mcp-0.1.0/tests/test_url_resolution.py +113 -0
- microsoft_ads_mcp-0.1.0/tests/test_url_tracking.py +233 -0
- microsoft_ads_mcp-0.1.0/tests/test_write_tool_schemas.py +68 -0
- microsoft_ads_mcp-0.1.0/ty.toml +21 -0
- microsoft_ads_mcp-0.1.0/uv.lock +1296 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Microsoft Advertising (Bing Ads) REST API credentials and server configuration.
|
|
2
|
+
# Copy to .env and fill in: cp .env.example .env
|
|
3
|
+
|
|
4
|
+
# --- Required ---
|
|
5
|
+
# Developer token from https://developers.ads.microsoft.com/
|
|
6
|
+
MICROSOFT_ADS_DEVELOPER_TOKEN=
|
|
7
|
+
# OAuth app (client) id for the account's identity provider (see IDENTITY_PROVIDER below).
|
|
8
|
+
# Microsoft: register at https://portal.azure.com with redirect URI
|
|
9
|
+
# https://login.microsoftonline.com/common/oauth2/nativeclient
|
|
10
|
+
# Google: an OAuth 2.0 "Desktop app" Client ID from https://console.cloud.google.com/apis/credentials
|
|
11
|
+
MICROSOFT_ADS_CLIENT_ID=
|
|
12
|
+
|
|
13
|
+
# Identity provider the Bing Ads account signs in with: microsoft (default) or google.
|
|
14
|
+
# Use google for accounts whose "Signs in with" is a Google account; CLIENT_ID/CLIENT_SECRET
|
|
15
|
+
# then refer to a Google Cloud OAuth client instead of an Azure app.
|
|
16
|
+
MICROSOFT_ADS_IDENTITY_PROVIDER=microsoft
|
|
17
|
+
|
|
18
|
+
# --- OAuth ---
|
|
19
|
+
# Provide a refresh token to run non-interactively (recommended for servers/agents).
|
|
20
|
+
# If omitted, use the get_auth_url / complete_auth tools once to mint and persist one.
|
|
21
|
+
MICROSOFT_ADS_REFRESH_TOKEN=
|
|
22
|
+
# Microsoft: only for web (confidential) app registrations; desktop/native apps leave blank.
|
|
23
|
+
# Google: the OAuth client secret paired with the Desktop-app Client ID above.
|
|
24
|
+
MICROSOFT_ADS_CLIENT_SECRET=
|
|
25
|
+
|
|
26
|
+
# --- Account scope ---
|
|
27
|
+
# Discovered via the search_accounts tool if left blank; set to pin a single account.
|
|
28
|
+
MICROSOFT_ADS_ACCOUNT_ID=
|
|
29
|
+
MICROSOFT_ADS_CUSTOMER_ID=
|
|
30
|
+
|
|
31
|
+
# --- Behavior ---
|
|
32
|
+
# production | sandbox
|
|
33
|
+
MICROSOFT_ADS_ENVIRONMENT=production
|
|
34
|
+
# When true, write/mutation tools are NOT registered and cannot be called. Default false.
|
|
35
|
+
READ_ONLY=false
|
|
36
|
+
# When true, the tool catalog is collapsed behind BM25 `search_tools` / `call_tool` (a few
|
|
37
|
+
# orientation tools stay pinned). Typed schemas and the READ_ONLY gate are preserved; the model
|
|
38
|
+
# discovers the rest on demand instead of loading the full catalog. Default false.
|
|
39
|
+
TOOL_SEARCH=false
|
|
40
|
+
|
|
41
|
+
# --- Transport ---
|
|
42
|
+
# stdio (default) or http
|
|
43
|
+
MCP_TRANSPORT=stdio
|
|
44
|
+
# MCP_HOST=127.0.0.1
|
|
45
|
+
# MCP_PORT=8000
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
ci:
|
|
10
|
+
runs-on: ubuntu-26.04-arm
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v7
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.14"
|
|
18
|
+
enable-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Sync dependencies
|
|
21
|
+
run: uv sync --frozen --dev
|
|
22
|
+
|
|
23
|
+
- name: Run CI checks
|
|
24
|
+
run: ./scripts/ci.sh
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
venv/
|
|
25
|
+
ENV/
|
|
26
|
+
env/
|
|
27
|
+
.venv/
|
|
28
|
+
|
|
29
|
+
# IDE
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# Environment and secrets
|
|
37
|
+
.env
|
|
38
|
+
.env.local
|
|
39
|
+
tokens.json
|
|
40
|
+
*.pem
|
|
41
|
+
*.key
|
|
42
|
+
|
|
43
|
+
# Tooling caches / lockfile-adjacent
|
|
44
|
+
.ruff_cache/
|
|
45
|
+
.ty_cache/
|
|
46
|
+
|
|
47
|
+
# Downloaded report/bulk working files
|
|
48
|
+
*.csv.tmp
|
|
49
|
+
.ms_ads_work/
|
|
50
|
+
|
|
51
|
+
# OS
|
|
52
|
+
.DS_Store
|
|
53
|
+
Thumbs.db
|
|
54
|
+
|
|
55
|
+
# Testing
|
|
56
|
+
.pytest_cache/
|
|
57
|
+
.coverage
|
|
58
|
+
htmlcov/
|
|
59
|
+
|
|
60
|
+
# MCP
|
|
61
|
+
.mcp.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
- use astral uv, ruff, ty tooling
|
|
2
|
+
- the upstream SDK (`msads`) is synchronous (requests/urllib3); tools are sync by design and
|
|
3
|
+
FastMCP runs them in a worker thread. Do not bolt async onto the SDK calls.
|
|
4
|
+
- writes are gated by `READ_ONLY`; write tools are *not registered* when it is true.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microsoft-ads-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for the Microsoft Advertising (Bing Ads) REST API, oriented toward agent-led campaign management and reporting.
|
|
5
|
+
Project-URL: Homepage, https://github.com/shinypebble/microsoft-ads-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/shinypebble/microsoft-ads-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/shinypebble/microsoft-ads-mcp/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: advertising,bing-ads,fastmcp,mcp,microsoft-advertising,model-context-protocol
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Classifier: Topic :: Office/Business
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
|
+
Requires-Dist: fastmcp<4,>=3.4.2
|
|
18
|
+
Requires-Dist: msads>=13.0.28
|
|
19
|
+
Requires-Dist: pydantic-settings>=2.14.1
|
|
20
|
+
Requires-Dist: pydantic>=2.13.4
|
|
21
|
+
Requires-Dist: python-dateutil>=2.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# microsoft-ads-mcp
|
|
25
|
+
|
|
26
|
+
[](https://github.com/shinypebble/microsoft-ads-mcp/actions/workflows/ci.yml)
|
|
27
|
+
|
|
28
|
+
An MCP server for the **Microsoft Advertising (Bing Ads) REST API**, built for agent-led
|
|
29
|
+
campaign management and reporting. It exposes a focused set of *useful-work* tools — walk the
|
|
30
|
+
campaign tree, create **and edit in place** (rename, repoint Final URLs, tracking templates,
|
|
31
|
+
status, bids), manage negative keywords, ad extensions, conversion goals/UET tags, and ZIP
|
|
32
|
+
location targeting, run the Bulk API, and pull performance reports that are actually downloaded
|
|
33
|
+
and parsed for you — rather than a 1:1 mirror of the API surface.
|
|
34
|
+
|
|
35
|
+
Built with [FastMCP](https://gofastmcp.com) and the official Microsoft
|
|
36
|
+
[`msads`](https://pypi.org/project/msads/) REST SDK (which ships OpenAPI-generated **Pydantic
|
|
37
|
+
v2** models). Managed with `uv`, linted/formatted with `ruff`, type-checked with `ty`.
|
|
38
|
+
|
|
39
|
+
## Why REST / `msads` (not the legacy SOAP `bingads` SDK)
|
|
40
|
+
|
|
41
|
+
Microsoft is retiring the SOAP API: **new features are REST-only from Oct 1, 2026**, and SOAP
|
|
42
|
+
is **fully deprecated on Jan 31, 2027** ([migration guide](https://learn.microsoft.com/en-us/advertising/guides/migrate-to-rest?view=bingads-13)).
|
|
43
|
+
The REST SDK `msads` gives typed Pydantic models, structured HTTP exceptions, and the same
|
|
44
|
+
OAuth/`ServiceClient` entry points — so this server is built on it directly.
|
|
45
|
+
|
|
46
|
+
### SDK quirks worth knowing
|
|
47
|
+
|
|
48
|
+
- **`msads` is synchronous** (requests/urllib3). Tools here are therefore plain sync
|
|
49
|
+
functions; FastMCP runs them in a worker thread, so the event loop is never blocked. We do
|
|
50
|
+
not wrap the SDK in async.
|
|
51
|
+
- **`msads` does not declare its `python-dateutil` dependency**, even though
|
|
52
|
+
`openapi_client` imports it. We pin `python-dateutil` explicitly in `pyproject.toml`.
|
|
53
|
+
- The package installs as the `bingads.*` (auth + `ServiceClient`) and `openapi_client.*`
|
|
54
|
+
(models + exceptions) import namespaces — there is no top-level `msads` module.
|
|
55
|
+
|
|
56
|
+
## REST API reference & endpoints
|
|
57
|
+
|
|
58
|
+
Pydantic models shipped inside `msads` are
|
|
59
|
+
code-generated from Microsoft's internal spec; the public surface is the per-operation
|
|
60
|
+
[Campaign Management reference](https://learn.microsoft.com/en-us/advertising/campaign-management-service/campaign-management-service-reference?view=bingads-13)
|
|
61
|
+
on Microsoft Learn (the [Python SOAP→REST migration guide](https://learn.microsoft.com/en-us/advertising/guides/python-sdk-migration-soap-to-rest?view=bingads-13)
|
|
62
|
+
is the most useful map of REST request/response shapes).
|
|
63
|
+
|
|
64
|
+
The REST service base URLs `ServiceClient` targets — set automatically from
|
|
65
|
+
`MICROSOFT_ADS_ENVIRONMENT` — are:
|
|
66
|
+
|
|
67
|
+
| Service | Production | Sandbox |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| Campaign Management | `https://campaign.api.bingads.microsoft.com` | `https://campaign.api.sandbox.bingads.microsoft.com` |
|
|
70
|
+
| Reporting | `https://reporting.api.bingads.microsoft.com` | `https://reporting.api.sandbox.bingads.microsoft.com` |
|
|
71
|
+
| Bulk | `https://bulk.api.bingads.microsoft.com` | `https://bulk.api.sandbox.bingads.microsoft.com` |
|
|
72
|
+
| Ad Insight | `https://adinsight.api.bingads.microsoft.com` | `https://adinsight.api.sandbox.bingads.microsoft.com` |
|
|
73
|
+
| Customer Mgmt / Billing | `https://clientcenter.api.bingads.microsoft.com` | `https://clientcenter.api.sandbox.bingads.microsoft.com` |
|
|
74
|
+
|
|
75
|
+
## Quickstart
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv sync # create .venv and install
|
|
79
|
+
cp .env.example .env # then set the credentials below
|
|
80
|
+
uv run python -m microsoft_ads_mcp # run over stdio (default)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
Set via environment variables or a local `.env` (see [.env.example](.env.example)):
|
|
86
|
+
|
|
87
|
+
| Variable | Required | Notes |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `MICROSOFT_ADS_DEVELOPER_TOKEN` | yes | From the developer portal |
|
|
90
|
+
| `MICROSOFT_ADS_CLIENT_ID` | yes | OAuth app (client) id — an Azure app, or a Google Cloud OAuth client when `IDENTITY_PROVIDER=google` |
|
|
91
|
+
| `MICROSOFT_ADS_IDENTITY_PROVIDER` | no | `microsoft` (default) or `google` for Google-federated accounts |
|
|
92
|
+
| `MICROSOFT_ADS_REFRESH_TOKEN` | recommended | Run non-interactively; else mint one via the auth tools |
|
|
93
|
+
| `MICROSOFT_ADS_CLIENT_SECRET` | no | Microsoft web/confidential apps, or the Google OAuth client secret |
|
|
94
|
+
| `MICROSOFT_ADS_ACCOUNT_ID` / `MICROSOFT_ADS_CUSTOMER_ID` | no | Discovered via `search_accounts` if unset |
|
|
95
|
+
| `MICROSOFT_ADS_ENVIRONMENT` | no | `production` (default) or `sandbox` |
|
|
96
|
+
| `READ_ONLY` | no | `true` registers no write tools at all (default `false`) |
|
|
97
|
+
| `TOOL_SEARCH` | no | `true` collapses the catalog behind BM25 `search_tools` / `call_tool` with a few tools pinned; typed schemas and the `READ_ONLY` gate are preserved (default `false`) |
|
|
98
|
+
|
|
99
|
+
Refresh tokens are persisted to `~/.config/microsoft-ads/tokens.json`, created with `0600`
|
|
100
|
+
permissions (owner read/write only).
|
|
101
|
+
|
|
102
|
+
## Authentication
|
|
103
|
+
|
|
104
|
+
If you have no refresh token yet, mint one once (interactive):
|
|
105
|
+
|
|
106
|
+
1. Call `get_auth_url()` → open the URL, sign in.
|
|
107
|
+
2. Copy the redirect URL and call `complete_auth(redirect_url)`.
|
|
108
|
+
3. The refresh token is saved and reused/auto-refreshed thereafter.
|
|
109
|
+
|
|
110
|
+
## MCP client configuration
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"microsoft-ads": {
|
|
116
|
+
"type": "stdio",
|
|
117
|
+
"command": "uv",
|
|
118
|
+
"args": ["run", "--directory", "${CLAUDE_PROJECT_DIR:-.}", "python", "-m", "microsoft_ads_mcp"],
|
|
119
|
+
"env": {
|
|
120
|
+
"MICROSOFT_ADS_DEVELOPER_TOKEN": "...",
|
|
121
|
+
"MICROSOFT_ADS_CLIENT_ID": "...",
|
|
122
|
+
"MICROSOFT_ADS_REFRESH_TOKEN": "...",
|
|
123
|
+
"READ_ONLY": "false"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Tools
|
|
131
|
+
|
|
132
|
+
Call `account_health` first to validate credentials and learn whether writes are enabled. It
|
|
133
|
+
returns a discriminated `auth_state` (`ok` / `no_token` / `token_expired` / `token_rejected` /
|
|
134
|
+
`dev_token_missing` / `account_inactive`) and `needs_interactive_auth`, so a client can branch
|
|
135
|
+
deterministically instead of pattern-matching an error string.
|
|
136
|
+
|
|
137
|
+
**Auth** — `get_auth_url`, `complete_auth` (one-time interactive sign-in; see below).
|
|
138
|
+
|
|
139
|
+
**Read** — `account_health`, `search_accounts`, `set_active_account` (switch which account
|
|
140
|
+
calls hit), `get_campaigns`, `get_ad_groups`, `get_keywords`, `get_ads` (includes the RSA copy:
|
|
141
|
+
headlines / descriptions / paths), `get_budgets`, `get_negative_keywords`, `get_ad_extensions`,
|
|
142
|
+
`get_conversion_goals`, `get_uet_tags`, `get_location_targets`, `get_location_intent`
|
|
143
|
+
(presence vs. search-interest targeting), `get_ad_schedules` (dayparting windows plus the
|
|
144
|
+
campaign time zone they run in), `get_device_bid_adjustments` (per-device modifiers —
|
|
145
|
+
Computers / Smartphones / Tablets), `resolve_postal_codes`
|
|
146
|
+
(ZIP → Microsoft LocationId), `bulk_download`, `get_account_url_options`. `get_campaigns` also
|
|
147
|
+
surfaces each campaign's `time_zone`, `start_date`, `languages`, `bid_strategy_type` (plus its
|
|
148
|
+
stored `max_cpc` / `target_cpa` / `target_roas` when the scheme carries them), and
|
|
149
|
+
`ad_schedule_use_searcher_time_zone`. `get_ad_groups` surfaces each ad group's `network` (ad
|
|
150
|
+
distribution: the entire Microsoft Advertising Network vs. Microsoft sites and select traffic only).
|
|
151
|
+
The hierarchy reads
|
|
152
|
+
(`get_campaigns`, `get_ad_groups`, `get_ads`, `get_keywords`) also surface each entity's URL
|
|
153
|
+
tracking — `tracking_url_template`, `final_url_suffix`, and `url_custom_parameters`. A `null`
|
|
154
|
+
template at a level usually means it inherits the **account-level** default, which
|
|
155
|
+
`get_account_url_options` returns (tracking template, Final URL suffix, and
|
|
156
|
+
`msclkid_auto_tagging_enabled` — the Microsoft Click ID that drives attribution). Confirm these
|
|
157
|
+
before activating paused campaigns rather than assuming the per-campaign blanks mean "untracked".
|
|
158
|
+
`get_ads` and `get_keywords` also surface `editorial_status` — the ad-review state (Active /
|
|
159
|
+
Inactive / ActiveLimited / Disapproved), separate from the Active/Paused `status` — so you can
|
|
160
|
+
tell whether an _Active_ ad or keyword is actually approved to serve (the first thing to check on
|
|
161
|
+
zero impressions).
|
|
162
|
+
`get_conversion_goals` reports each goal's `exclude_from_bidding` — the inverse of the UI's
|
|
163
|
+
"Include in conversions" checkbox, i.e. whether the goal feeds automated bidding (ECPC / tCPA) —
|
|
164
|
+
plus `count_type`, `conversion_window_in_minutes`, `goal_category`, and the revenue model; confirm
|
|
165
|
+
a goal is included before relying on it to steer spend.
|
|
166
|
+
|
|
167
|
+
**Reporting** — `run_performance_report` (submit → poll → download → parse, returns rows),
|
|
168
|
+
covering campaign / keyword / search-query / geographic reports. Supports a predefined
|
|
169
|
+
`date_range` or a custom `start_date`/`end_date`, and scoping to a single `campaign_id` /
|
|
170
|
+
`ad_group_id` / `account_id`.
|
|
171
|
+
|
|
172
|
+
**Keyword research** (Ad Insight / Keyword Planner; read-only, registered even in `READ_ONLY`
|
|
173
|
+
mode) — `estimate_keyword_bids` returns the estimated first-page (or mainline) bid per keyword
|
|
174
|
+
(`estimated_min_bid`) with the modeled CPC/CTR/clicks/impressions/cost it buys;
|
|
175
|
+
`get_keyword_ideas` discovers keywords from seed phrases and/or a landing-page URL with monthly
|
|
176
|
+
search volume, a suggested bid, and a competition bucket (defaults to English / United States);
|
|
177
|
+
and `get_keyword_traffic_estimates` projects weekly clicks / impressions / cost / position for
|
|
178
|
+
keywords at a given max CPC. `check_first_page_bids(ad_group_id, campaign_id)` joins an ad group's
|
|
179
|
+
live keyword bids to these estimates and flags the keywords bidding below their first-page bid (the
|
|
180
|
+
"Below first page bid" delivery state), each with its `current_bid`, `estimated_first_page_bid`,
|
|
181
|
+
and `shortfall`. Every value is a modeled estimate and may be `null` where Microsoft has no data.
|
|
182
|
+
|
|
183
|
+
**Write** (only when `READ_ONLY=false`) — new campaigns / ad groups / ads are created **PAUSED**.
|
|
184
|
+
|
|
185
|
+
- *Campaigns, ad groups, ads, keywords* — `create_campaign`, `update_campaign`,
|
|
186
|
+
`update_campaign_status`, `create_ad_group`, `update_ad_group`, `create_responsive_search_ad`,
|
|
187
|
+
`update_responsive_search_ad`, `add_keywords`, `update_keyword`, `delete_campaign`,
|
|
188
|
+
`delete_ad_group`, `delete_ad`, `delete_keyword`. Create/update at every level (campaign, ad
|
|
189
|
+
group, ad, keyword) accept `tracking_url_template`, `final_url_suffix`, and
|
|
190
|
+
`url_custom_parameters` (a `{key: value}` map, referenced in templates as `{_key}`).
|
|
191
|
+
`create_ad_group` / `update_ad_group` also accept `network` (ad distribution).
|
|
192
|
+
`create_campaign` / `update_campaign` also accept `bid_strategy_type` to set the campaign's
|
|
193
|
+
inline bid strategy (`EnhancedCpc`, `ManualCpc`, `MaxClicks`, `MaxConversions`, `TargetCpa`,
|
|
194
|
+
`MaxConversionValue`, `TargetRoas`) with optional `max_cpc` / `target_cpa` / `target_roas` —
|
|
195
|
+
e.g. `MaxClicks` + `max_cpc` is Maximize Clicks with a Maximum CPC limit (distinct from
|
|
196
|
+
`bid_strategy_id`, which applies a portfolio strategy; set one or the other).
|
|
197
|
+
- *Account-level URL options* — `set_account_url_options` sets the tracking template, Final URL
|
|
198
|
+
suffix, and `msclkid` auto-tagging once for the whole account (every campaign inherits them) —
|
|
199
|
+
the cleanest single-point lever for an account-wide tracking/rebrand change.
|
|
200
|
+
- *Negative keywords* — `add_negative_keywords`, `remove_negative_keywords` (campaign or ad-group
|
|
201
|
+
scope).
|
|
202
|
+
- *Ad extensions* — `add_call_extension`, `update_call_extension`, `add_callout_extension`,
|
|
203
|
+
`add_sitelink_extension`, `delete_ad_extension`. Call extensions accept
|
|
204
|
+
`is_call_tracking_enabled` (US/UK) to turn on Microsoft call tracking so call-from-ad
|
|
205
|
+
conversions are measured — pass it on `add_call_extension`, or flip it on an existing asset
|
|
206
|
+
with `update_call_extension`. New forwarding numbers are local (toll-free is no longer
|
|
207
|
+
provisioned). They also accept `is_call_only` (the "Show just my phone number" call-only mobile
|
|
208
|
+
format). `get_ad_extensions` surfaces the current `is_call_tracking_enabled` and `is_call_only`
|
|
209
|
+
flags.
|
|
210
|
+
- *Conversion goals / UET tags* — `create_conversion_goal` adds a goal: an `OfflineConversion`
|
|
211
|
+
goal (keyed by MSCLKID, no UET tag) or a UET-backed web goal (`Url` / `Event` / `Duration` /
|
|
212
|
+
`PagesViewedPerVisit`, which need a `tag_id`). Goals are created **active** (a goal doesn't spend;
|
|
213
|
+
a paused one silently fails to record). `update_conversion_goal` edits a goal in place: rename,
|
|
214
|
+
set `status`, and (most launch-relevant) toggle `exclude_from_bidding` — the inverse of the UI's
|
|
215
|
+
"Include in conversions" checkbox, the single switch for whether a goal feeds automated bidding
|
|
216
|
+
(ECPC / tCPA). Also sets `count_type`, `conversion_window_in_minutes`, and the revenue model
|
|
217
|
+
(`revenue_type` / `revenue_value` / `revenue_currency_code`). For phone calls there is no native
|
|
218
|
+
"calls from ads" goal: `apply_offline_conversions` is the bid-eligible path — filter the
|
|
219
|
+
call-center log yourself (e.g. calls ≥60s), then upload qualifying calls by MSCLKID against an
|
|
220
|
+
`OfflineConversion` goal whose name matches `conversion_name`. `update_uet_tag`
|
|
221
|
+
renames/redescribes a tag.
|
|
222
|
+
- *Location (ZIP/geo) targeting* — `add_location_targets`, `remove_location_targets`,
|
|
223
|
+
`set_location_intent` (presence — `PeopleIn` — vs. search-interest targeting; one criterion
|
|
224
|
+
per campaign, updated in place).
|
|
225
|
+
- *Ad scheduling (dayparting)* — `add_ad_schedules`, `remove_ad_schedules`, `replace_ad_schedule`
|
|
226
|
+
(day + time windows at 15-minute granularity; times run in the campaign time zone unless
|
|
227
|
+
`use_searcher_time_zone` is set). Windows are additive, but a same-day window may **not** overlap
|
|
228
|
+
an existing one (the API rejects it), so to change or extend a window use `replace_ad_schedule`
|
|
229
|
+
(which removes the old criterion then adds the new one — the only safe order) rather than adding
|
|
230
|
+
over it. `update_campaign` accepts `time_zone` to set the zone those schedules run in.
|
|
231
|
+
- *Device bid adjustments* — `set_device_bid_adjustment(campaign_id, device, bid_adjustment)` sets
|
|
232
|
+
a per-device modifier (-100 to 900 percent; -100 excludes the device). Microsoft calls mobile
|
|
233
|
+
**Smartphones** (there is no "Mobile"); "Computers" is desktop/laptop. Device criterions are
|
|
234
|
+
created as a set, so the first call also creates the other two at a neutral 0.
|
|
235
|
+
- *Bulk API* — `bulk_upload`.
|
|
236
|
+
|
|
237
|
+
The `update_*` tools patch in place: only the fields you pass change. Prefer them over
|
|
238
|
+
recreate-and-pause when an entity already exists.
|
|
239
|
+
|
|
240
|
+
### Tool discovery (`TOOL_SEARCH`)
|
|
241
|
+
|
|
242
|
+
With `TOOL_SEARCH=true`, the server lists only a few pinned orientation tools
|
|
243
|
+
(`account_health`, `search_accounts`, `get_campaigns`, `run_performance_report`, plus the auth
|
|
244
|
+
tools) alongside two synthetic tools: `search_tools(query)` (BM25 over names, descriptions, and
|
|
245
|
+
parameters) and `call_tool(name, arguments)`. The rest of the catalog is discovered on demand
|
|
246
|
+
instead of loaded upfront — useful as the tool count grows. Hidden tools keep their full typed
|
|
247
|
+
schemas, and because search runs through the normal pipeline, the `READ_ONLY` gate still applies:
|
|
248
|
+
write tools aren't registered in read-only mode, so they're neither listed nor discoverable. This
|
|
249
|
+
is FastMCP's stable `BM25SearchTransform` — no code execution, no sandbox.
|
|
250
|
+
|
|
251
|
+
## Architecture
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
src/microsoft_ads_mcp/
|
|
255
|
+
config.py # pydantic-settings; all env config
|
|
256
|
+
server.py # builds FastMCP, lifespan-manages the client, registers tools
|
|
257
|
+
api/
|
|
258
|
+
auth.py # OAuth flow + hardened token store
|
|
259
|
+
client.py # wraps msads ServiceClient(s); the single dispatch point
|
|
260
|
+
errors.py # translate openapi_client exceptions -> MsAdsApiError
|
|
261
|
+
domain/
|
|
262
|
+
entities.py # lean Pydantic summary/report models for tool outputs
|
|
263
|
+
services/
|
|
264
|
+
accounts.py # user/account reads (CustomerManagementService)
|
|
265
|
+
account_properties.py # account-level URL options (CampaignManagementService AccountProperties)
|
|
266
|
+
campaigns.py # hierarchy + list reads
|
|
267
|
+
mutations.py # create/update/delete for campaigns, ad groups, ads, keywords
|
|
268
|
+
negatives.py # negative-keyword add/list/remove
|
|
269
|
+
extensions.py # ad extensions (call/callout/sitelink)
|
|
270
|
+
conversions.py # conversion goals + UET tags
|
|
271
|
+
criteria.py # location (ZIP/geo) targeting via campaign criterions
|
|
272
|
+
geo.py # ZIP -> LocationId resolution (cached geo-locations file)
|
|
273
|
+
bulk.py # Bulk API upload/download (submit/poll)
|
|
274
|
+
reporting.py # submit/poll/download/parse
|
|
275
|
+
insights.py # Ad Insight keyword research (bid/idea/traffic estimates)
|
|
276
|
+
tools/
|
|
277
|
+
health.py read_tools.py write_tools.py reporting_tools.py insight_tools.py auth_tools.py # READ_ONLY-gated
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Development
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
uv run ruff check . && uv run ruff format --check .
|
|
284
|
+
uv run ty check
|
|
285
|
+
uv run pytest -q
|
|
286
|
+
# or all at once:
|
|
287
|
+
bash scripts/ci.sh
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### MCP Inspector
|
|
291
|
+
|
|
292
|
+
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a browser UI for
|
|
293
|
+
calling the server's tools by hand — the fastest way to exercise a tool while iterating
|
|
294
|
+
locally. FastMCP ships an integration that launches it (with auto-reload on file changes):
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
# Run the package as a module (-m) so its relative imports resolve; --with-editable .
|
|
298
|
+
# installs this package into the Inspector's ephemeral env.
|
|
299
|
+
uv run fastmcp dev inspector microsoft_ads_mcp -m --with-editable .
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
This prints a `http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=...` URL — open it, connect, and
|
|
303
|
+
call `account_health` first. To test the exact `python -m` entrypoint an MCP client uses,
|
|
304
|
+
run the standalone Inspector against the real command instead:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
npx @modelcontextprotocol/inspector uv run python -m microsoft_ads_mcp
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Either way, credentials load from `.env`. Write tools only appear when `READ_ONLY=false` —
|
|
311
|
+
set it in `.env`, or (for the standalone Inspector) in its env panel before connecting.
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
MIT — see [LICENSE](LICENSE).
|