MCP server for the Microsoft Advertising (Bing Ads) REST API, oriented toward agent-led campaign management and reporting.
Project description
microsoft-ads-mcp
An MCP server for the Microsoft Advertising (Bing Ads) REST API, built for agent-led campaign management and reporting. It exposes a focused set of useful-work tools — walk the campaign tree, create and edit in place (rename, repoint Final URLs, tracking templates, status, bids), manage negative keywords, ad extensions, conversion goals/UET tags, and ZIP location targeting, run the Bulk API, and pull performance reports that are actually downloaded and parsed for you — rather than a 1:1 mirror of the API surface.
Built with FastMCP and the official Microsoft
msads REST SDK (which ships OpenAPI-generated Pydantic
v2 models). Managed with uv, linted/formatted with ruff, type-checked with ty.
Why REST / msads (not the legacy SOAP bingads SDK)
Microsoft is retiring the SOAP API: new features are REST-only from Oct 1, 2026, and SOAP
is fully deprecated on Jan 31, 2027 (migration guide).
The REST SDK msads gives typed Pydantic models, structured HTTP exceptions, and the same
OAuth/ServiceClient entry points — so this server is built on it directly.
SDK quirks worth knowing
msadsis synchronous (requests/urllib3). Tools here are therefore plain sync functions; FastMCP runs them in a worker thread, so the event loop is never blocked. We do not wrap the SDK in async.msadsdoes not declare itspython-dateutildependency, even thoughopenapi_clientimports it. We pinpython-dateutilexplicitly inpyproject.toml.- The package installs as the
bingads.*(auth +ServiceClient) andopenapi_client.*(models + exceptions) import namespaces — there is no top-levelmsadsmodule.
REST API reference & endpoints
Pydantic models shipped inside msads are
code-generated from Microsoft's internal spec; the public surface is the per-operation
Campaign Management reference
on Microsoft Learn (the Python SOAP→REST migration guide
is the most useful map of REST request/response shapes).
The REST service base URLs ServiceClient targets — set automatically from
MICROSOFT_ADS_ENVIRONMENT — are:
| Service | Production | Sandbox |
|---|---|---|
| Campaign Management | https://campaign.api.bingads.microsoft.com |
https://campaign.api.sandbox.bingads.microsoft.com |
| Reporting | https://reporting.api.bingads.microsoft.com |
https://reporting.api.sandbox.bingads.microsoft.com |
| Bulk | https://bulk.api.bingads.microsoft.com |
https://bulk.api.sandbox.bingads.microsoft.com |
| Ad Insight | https://adinsight.api.bingads.microsoft.com |
https://adinsight.api.sandbox.bingads.microsoft.com |
| Customer Mgmt / Billing | https://clientcenter.api.bingads.microsoft.com |
https://clientcenter.api.sandbox.bingads.microsoft.com |
Install
The published package runs from anywhere with uvx — no clone, no project files. Put your
credentials in ~/.config/microsoft-ads/.env (the same directory the server already persists
OAuth tokens to), then point your MCP client at uvx microsoft-ads-mcp:
mkdir -p ~/.config/microsoft-ads
cat > ~/.config/microsoft-ads/.env <<'EOF'
MICROSOFT_ADS_DEVELOPER_TOKEN=...
MICROSOFT_ADS_CLIENT_ID=...
# Optional once you've signed in; otherwise mint one via the auth tools (see Authentication):
# MICROSOFT_ADS_REFRESH_TOKEN=...
EOF
chmod 600 ~/.config/microsoft-ads/.env
This .env is read independently of the working directory, so the server works from any repo
and no secrets live in any project file — the MCP client config carries only the non-secret
operational flags (see MCP client configuration). Do the one-time
sign-in once (see Authentication); the refresh token is then persisted next
to this file and auto-refreshed.
From source (development)
uv sync # create .venv and install
cp .env.example .env # then set the credentials below
uv run python -m microsoft_ads_mcp # run over stdio (default)
Configuration
Credentials and flags load from (highest priority first) real environment variables, a
project-local .env, then the cwd-independent ~/.config/microsoft-ads/.env. See
.env.example for the full list:
| Variable | Required | Notes |
|---|---|---|
MICROSOFT_ADS_DEVELOPER_TOKEN |
yes | From the developer portal |
MICROSOFT_ADS_CLIENT_ID |
yes | OAuth app (client) id — an Azure app, or a Google Cloud OAuth client when IDENTITY_PROVIDER=google |
MICROSOFT_ADS_IDENTITY_PROVIDER |
no | microsoft (default) or google for Google-federated accounts |
MICROSOFT_ADS_REFRESH_TOKEN |
recommended | Run non-interactively; else mint one via the auth tools |
MICROSOFT_ADS_CLIENT_SECRET |
no | Microsoft web/confidential apps, or the Google OAuth client secret |
MICROSOFT_ADS_ACCOUNT_ID / MICROSOFT_ADS_CUSTOMER_ID |
no | Discovered via search_accounts if unset |
MICROSOFT_ADS_ENVIRONMENT |
no | production (default) or sandbox |
READ_ONLY |
no | true registers no write tools at all (default false) |
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) |
Refresh tokens are persisted to ~/.config/microsoft-ads/tokens.json, created with 0600
permissions (owner read/write only).
Authentication
If you have no refresh token yet, mint one once (interactive):
- Call
get_auth_url()→ open the URL, sign in. - Copy the redirect URL and call
complete_auth(redirect_url). - The refresh token is saved to
~/.config/microsoft-ads/tokens.json(mode0600) and reused/auto-refreshed thereafter — so you never need to add it to.envby hand.
MCP client configuration
Recommended — run the published package with uvx, credentials in
~/.config/microsoft-ads/.env (see Install). The config carries only the non-secret
flags:
{
"mcpServers": {
"microsoft-ads": {
"command": "uvx",
"args": ["microsoft-ads-mcp"],
"env": {
"READ_ONLY": "false",
"TOOL_SEARCH": "true"
}
}
}
}
Alternatively, run from a source checkout (development) and/or pass credentials inline instead of
via .env:
{
"mcpServers": {
"microsoft-ads": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "${CLAUDE_PROJECT_DIR:-.}", "python", "-m", "microsoft_ads_mcp"],
"env": {
"MICROSOFT_ADS_DEVELOPER_TOKEN": "...",
"MICROSOFT_ADS_CLIENT_ID": "...",
"MICROSOFT_ADS_REFRESH_TOKEN": "...",
"READ_ONLY": "false"
}
}
}
}
Tools
Call account_health first to validate credentials and learn whether writes are enabled. It
returns a discriminated auth_state (ok / no_token / token_expired / token_rejected /
dev_token_missing / account_inactive) and needs_interactive_auth, so a client can branch
deterministically instead of pattern-matching an error string.
Auth — get_auth_url, complete_auth (one-time interactive sign-in; see below).
Read — account_health, search_accounts, set_active_account (switch which account
calls hit), get_campaigns, get_ad_groups, get_keywords, get_ads (includes the RSA copy:
headlines / descriptions / paths), get_budgets, get_negative_keywords, get_ad_extensions,
get_conversion_goals, get_uet_tags, get_location_targets, get_location_intent
(presence vs. search-interest targeting), get_ad_schedules (dayparting windows plus the
campaign time zone they run in), get_device_bid_adjustments (per-device modifiers —
Computers / Smartphones / Tablets), resolve_postal_codes
(ZIP → Microsoft LocationId), bulk_download, get_account_url_options. get_campaigns also
surfaces each campaign's time_zone, start_date, languages, bid_strategy_type (plus its
stored max_cpc / target_cpa / target_roas when the scheme carries them), and
ad_schedule_use_searcher_time_zone. get_ad_groups surfaces each ad group's network (ad
distribution: the entire Microsoft Advertising Network vs. Microsoft sites and select traffic only).
The hierarchy reads
(get_campaigns, get_ad_groups, get_ads, get_keywords) also surface each entity's URL
tracking — tracking_url_template, final_url_suffix, and url_custom_parameters. A null
template at a level usually means it inherits the account-level default, which
get_account_url_options returns (tracking template, Final URL suffix, and
msclkid_auto_tagging_enabled — the Microsoft Click ID that drives attribution). Confirm these
before activating paused campaigns rather than assuming the per-campaign blanks mean "untracked".
get_ads and get_keywords also surface editorial_status — the ad-review state (Active /
Inactive / ActiveLimited / Disapproved), separate from the Active/Paused status — so you can
tell whether an Active ad or keyword is actually approved to serve (the first thing to check on
zero impressions).
get_conversion_goals reports each goal's exclude_from_bidding — the inverse of the UI's
"Include in conversions" checkbox, i.e. whether the goal feeds automated bidding (ECPC / tCPA) —
plus count_type, conversion_window_in_minutes, goal_category, and the revenue model; confirm
a goal is included before relying on it to steer spend.
Reporting — run_performance_report (submit → poll → download → parse, returns rows),
covering campaign / keyword / search-query / geographic reports. Supports a predefined
date_range or a custom start_date/end_date, and scoping to a single campaign_id /
ad_group_id / account_id.
Keyword research (Ad Insight / Keyword Planner; read-only, registered even in READ_ONLY
mode) — estimate_keyword_bids returns the estimated first-page (or mainline) bid per keyword
(estimated_min_bid) with the modeled CPC/CTR/clicks/impressions/cost it buys;
get_keyword_ideas discovers keywords from seed phrases and/or a landing-page URL with monthly
search volume, a suggested bid, and a competition bucket (defaults to English / United States);
and get_keyword_traffic_estimates projects weekly clicks / impressions / cost / position for
keywords at a given max CPC. check_first_page_bids(ad_group_id, campaign_id) joins an ad group's
live keyword bids to these estimates and flags the keywords bidding below their first-page bid (the
"Below first page bid" delivery state), each with its current_bid, estimated_first_page_bid,
and shortfall. Every value is a modeled estimate and may be null where Microsoft has no data.
Write (only when READ_ONLY=false) — new campaigns / ad groups / ads are created PAUSED.
- Campaigns, ad groups, ads, keywords —
create_campaign,update_campaign,update_campaign_status,create_ad_group,update_ad_group,create_responsive_search_ad,update_responsive_search_ad,add_keywords,update_keyword,delete_campaign,delete_ad_group,delete_ad,delete_keyword. Create/update at every level (campaign, ad group, ad, keyword) accepttracking_url_template,final_url_suffix, andurl_custom_parameters(a{key: value}map, referenced in templates as{_key}).create_ad_group/update_ad_groupalso acceptnetwork(ad distribution).create_campaign/update_campaignalso acceptbid_strategy_typeto set the campaign's inline bid strategy (EnhancedCpc,ManualCpc,MaxClicks,MaxConversions,TargetCpa,MaxConversionValue,TargetRoas) with optionalmax_cpc/target_cpa/target_roas— e.g.MaxClicks+max_cpcis Maximize Clicks with a Maximum CPC limit (distinct frombid_strategy_id, which applies a portfolio strategy; set one or the other). - Account-level URL options —
set_account_url_optionssets the tracking template, Final URL suffix, andmsclkidauto-tagging once for the whole account (every campaign inherits them) — the cleanest single-point lever for an account-wide tracking/rebrand change. - Negative keywords —
add_negative_keywords,remove_negative_keywords(campaign or ad-group scope). - Ad extensions —
add_call_extension,update_call_extension,add_callout_extension,add_sitelink_extension,update_sitelink_extension,add_structured_snippet_extension,update_structured_snippet_extension,delete_ad_extension. Call extensions acceptis_call_tracking_enabled(US/UK) to turn on Microsoft call tracking so call-from-ad conversions are measured — pass it onadd_call_extension, or flip it on an existing asset withupdate_call_extension. New forwarding numbers are local (toll-free is no longer provisioned). They also acceptis_call_only(the "Show just my phone number" call-only mobile format). Sitelinks carry the twodescription1/description2lines (set both or neither); structured snippets carry aheaderfrom Microsoft's predefined list (e.g. "Brands", "Services", "Types") plus 3-10 shortvalues. Theupdate_sitelink_extension/update_structured_snippet_extensiontools edit those in place (e.g. add descriptions to an existing sitelink), re-sending the replace-required fields for you so a partial edit is safe.delete_ad_extensionremoves any extension type by id — it deletes the account-level object itself, not just a single campaign/ad-group association.get_ad_extensionssurfaces the currentis_call_tracking_enabled/is_call_onlyflags, sitelink descriptions, and snippet header/values. - Conversion goals / UET tags —
create_conversion_goaladds a goal: anOfflineConversiongoal (keyed by MSCLKID, no UET tag) or a UET-backed web goal (Url/Event/Duration/PagesViewedPerVisit, which need atag_id). Goals are created active (a goal doesn't spend; a paused one silently fails to record).update_conversion_goaledits a goal in place: rename, setstatus, and (most launch-relevant) toggleexclude_from_bidding— the inverse of the UI's "Include in conversions" checkbox, the single switch for whether a goal feeds automated bidding (ECPC / tCPA). Also setscount_type,conversion_window_in_minutes, and the revenue model (revenue_type/revenue_value/revenue_currency_code). For phone calls there is no native "calls from ads" goal:apply_offline_conversionsis the bid-eligible path — filter the call-center log yourself (e.g. calls ≥60s), then upload qualifying calls by MSCLKID against anOfflineConversiongoal whose name matchesconversion_name.update_uet_tagrenames/redescribes a tag. - Location (ZIP/geo) targeting —
add_location_targets,remove_location_targets,set_location_intent(presence —PeopleIn— vs. search-interest targeting; one criterion per campaign, updated in place). - Ad scheduling (dayparting) —
add_ad_schedules,remove_ad_schedules,replace_ad_schedule(day + time windows at 15-minute granularity; times run in the campaign time zone unlessuse_searcher_time_zoneis set). Windows are additive, but a same-day window may not overlap an existing one (the API rejects it), so to change or extend a window usereplace_ad_schedule(which removes the old criterion then adds the new one — the only safe order) rather than adding over it.update_campaignacceptstime_zoneto set the zone those schedules run in. - Device bid adjustments —
set_device_bid_adjustment(campaign_id, device, bid_adjustment)sets a per-device modifier (-100 to 900 percent; -100 excludes the device). Microsoft calls mobile Smartphones (there is no "Mobile"); "Computers" is desktop/laptop. Device criterions are created as a set, so the first call also creates the other two at a neutral 0. - Bulk API —
bulk_upload.
The update_* tools patch in place: only the fields you pass change. Prefer them over
recreate-and-pause when an entity already exists.
Tool discovery (TOOL_SEARCH)
With TOOL_SEARCH=true, the server lists only a few pinned orientation tools
(account_health, search_accounts, get_campaigns, run_performance_report, plus the auth
tools) alongside two synthetic tools: search_tools(query) (BM25 over names, descriptions, and
parameters) and call_tool(name, arguments). The rest of the catalog is discovered on demand
instead of loaded upfront — useful as the tool count grows. Hidden tools keep their full typed
schemas, and because search runs through the normal pipeline, the READ_ONLY gate still applies:
write tools aren't registered in read-only mode, so they're neither listed nor discoverable. This
is FastMCP's stable BM25SearchTransform — no code execution, no sandbox.
Architecture
src/microsoft_ads_mcp/
config.py # pydantic-settings; all env config
server.py # builds FastMCP, lifespan-manages the client, registers tools
api/
auth.py # OAuth flow + hardened token store
client.py # wraps msads ServiceClient(s); the single dispatch point
errors.py # translate openapi_client exceptions -> MsAdsApiError
domain/
entities.py # lean Pydantic summary/report models for tool outputs
services/
accounts.py # user/account reads (CustomerManagementService)
account_properties.py # account-level URL options (CampaignManagementService AccountProperties)
campaigns.py # hierarchy + list reads
mutations.py # create/update/delete for campaigns, ad groups, ads, keywords
negatives.py # negative-keyword add/list/remove
extensions.py # ad extensions (call/callout/sitelink/structured snippet)
conversions.py # conversion goals + UET tags
criteria.py # location (ZIP/geo) targeting via campaign criterions
geo.py # ZIP -> LocationId resolution (cached geo-locations file)
bulk.py # Bulk API upload/download (submit/poll)
reporting.py # submit/poll/download/parse
insights.py # Ad Insight keyword research (bid/idea/traffic estimates)
tools/
health.py read_tools.py write_tools.py reporting_tools.py insight_tools.py auth_tools.py # READ_ONLY-gated
Development
uv run ruff check . && uv run ruff format --check .
uv run ty check
uv run pytest -q
# or all at once:
bash scripts/ci.sh
MCP Inspector
The MCP Inspector is a browser UI for calling the server's tools by hand — the fastest way to exercise a tool while iterating locally. FastMCP ships an integration that launches it (with auto-reload on file changes):
# Run the package as a module (-m) so its relative imports resolve; --with-editable .
# installs this package into the Inspector's ephemeral env.
uv run fastmcp dev inspector microsoft_ads_mcp -m --with-editable .
This prints a http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=... URL — open it, connect, and
call account_health first. To test the exact python -m entrypoint an MCP client uses,
run the standalone Inspector against the real command instead:
npx @modelcontextprotocol/inspector uv run python -m microsoft_ads_mcp
Either way, credentials load from .env. Write tools only appear when READ_ONLY=false —
set it in .env, or (for the standalone Inspector) in its env panel before connecting.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file microsoft_ads_mcp-0.4.0.tar.gz.
File metadata
- Download URL: microsoft_ads_mcp-0.4.0.tar.gz
- Upload date:
- Size: 178.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae7096c73230044dbdc1589d4b7c2b89502f0130acc2602deb55746ed6f49d75
|
|
| MD5 |
6784f936537509fc645157f787544d48
|
|
| BLAKE2b-256 |
9044e80ca5cda293c48dd606fa4f371a9cc514c8c7480c658bf9cabd6bdcdb0a
|
File details
Details for the file microsoft_ads_mcp-0.4.0-py3-none-any.whl.
File metadata
- Download URL: microsoft_ads_mcp-0.4.0-py3-none-any.whl
- Upload date:
- Size: 113.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d0beab098023c39d8abfc0ec51eea5e38ac4072eb99cee8b5708e785be03a72
|
|
| MD5 |
c7bc396bed3ae998ec7d9be721caa08e
|
|
| BLAKE2b-256 |
6942163af7362ce0b1d8c163904ecb5fc445dcd528a756409a5725910a2f360a
|