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.
Files changed (66) hide show
  1. microsoft_ads_mcp-0.1.0/.claude/settings.local.json +5 -0
  2. microsoft_ads_mcp-0.1.0/.env.example +45 -0
  3. microsoft_ads_mcp-0.1.0/.github/workflows/ci.yml +24 -0
  4. microsoft_ads_mcp-0.1.0/.gitignore +61 -0
  5. microsoft_ads_mcp-0.1.0/.python-version +1 -0
  6. microsoft_ads_mcp-0.1.0/AGENTS.md +4 -0
  7. microsoft_ads_mcp-0.1.0/CLAUDE.md +1 -0
  8. microsoft_ads_mcp-0.1.0/LICENSE +21 -0
  9. microsoft_ads_mcp-0.1.0/PKG-INFO +315 -0
  10. microsoft_ads_mcp-0.1.0/README.md +292 -0
  11. microsoft_ads_mcp-0.1.0/pyproject.toml +75 -0
  12. microsoft_ads_mcp-0.1.0/scripts/ci.sh +7 -0
  13. microsoft_ads_mcp-0.1.0/skill/microsoft-ads-optimizer/SKILL.md +212 -0
  14. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/__init__.py +3 -0
  15. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/__main__.py +18 -0
  16. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/__init__.py +1 -0
  17. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/auth.py +149 -0
  18. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/client.py +201 -0
  19. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/api/errors.py +82 -0
  20. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/config.py +79 -0
  21. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/domain/__init__.py +1 -0
  22. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/domain/entities.py +1065 -0
  23. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/server.py +145 -0
  24. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/__init__.py +59 -0
  25. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/account_properties.py +88 -0
  26. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/accounts.py +51 -0
  27. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/bulk.py +158 -0
  28. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/campaigns.py +128 -0
  29. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/conversions.py +481 -0
  30. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/criteria.py +630 -0
  31. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/extensions.py +331 -0
  32. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/geo.py +140 -0
  33. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/insights.py +381 -0
  34. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/mutations.py +735 -0
  35. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/negatives.py +146 -0
  36. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/reporting.py +252 -0
  37. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/services/url_resolution.py +93 -0
  38. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/__init__.py +31 -0
  39. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/_common.py +24 -0
  40. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/auth_tools.py +43 -0
  41. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/health.py +70 -0
  42. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/insight_tools.py +175 -0
  43. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/read_tools.py +317 -0
  44. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/reporting_tools.py +56 -0
  45. microsoft_ads_mcp-0.1.0/src/microsoft_ads_mcp/tools/write_tools.py +1071 -0
  46. microsoft_ads_mcp-0.1.0/tests/conftest.py +29 -0
  47. microsoft_ads_mcp-0.1.0/tests/test_auth.py +60 -0
  48. microsoft_ads_mcp-0.1.0/tests/test_client_reauth.py +156 -0
  49. microsoft_ads_mcp-0.1.0/tests/test_config.py +39 -0
  50. microsoft_ads_mcp-0.1.0/tests/test_conversions.py +530 -0
  51. microsoft_ads_mcp-0.1.0/tests/test_editorial_status.py +47 -0
  52. microsoft_ads_mcp-0.1.0/tests/test_errors.py +43 -0
  53. microsoft_ads_mcp-0.1.0/tests/test_extensions.py +275 -0
  54. microsoft_ads_mcp-0.1.0/tests/test_geo_criteria_bulk.py +562 -0
  55. microsoft_ads_mcp-0.1.0/tests/test_health_auth_state.py +89 -0
  56. microsoft_ads_mcp-0.1.0/tests/test_insights.py +485 -0
  57. microsoft_ads_mcp-0.1.0/tests/test_mutations.py +386 -0
  58. microsoft_ads_mcp-0.1.0/tests/test_negatives.py +113 -0
  59. microsoft_ads_mcp-0.1.0/tests/test_read_only_gating.py +87 -0
  60. microsoft_ads_mcp-0.1.0/tests/test_reporting_scope.py +80 -0
  61. microsoft_ads_mcp-0.1.0/tests/test_tool_search.py +64 -0
  62. microsoft_ads_mcp-0.1.0/tests/test_url_resolution.py +113 -0
  63. microsoft_ads_mcp-0.1.0/tests/test_url_tracking.py +233 -0
  64. microsoft_ads_mcp-0.1.0/tests/test_write_tool_schemas.py +68 -0
  65. microsoft_ads_mcp-0.1.0/ty.toml +21 -0
  66. microsoft_ads_mcp-0.1.0/uv.lock +1296 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "enabledMcpjsonServers": [
3
+ "microsoft-ads"
4
+ ]
5
+ }
@@ -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
+ [![CI](https://github.com/shinypebble/microsoft-ads-mcp/actions/workflows/ci.yml/badge.svg)](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).