Skip to main content

MCP server for the Microsoft Advertising (Bing Ads) REST API, oriented toward agent-led campaign management and reporting.

Project description

microsoft-ads-mcp

CI

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

  • msads is 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.
  • msads does not declare its python-dateutil dependency, even though openapi_client imports it. We pin python-dateutil explicitly in pyproject.toml.
  • The package installs as the bingads.* (auth + ServiceClient) and openapi_client.* (models + exceptions) import namespaces — there is no top-level msads module.

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):

  1. Call get_auth_url() → open the URL, sign in.
  2. Copy the redirect URL and call complete_auth(redirect_url).
  3. The refresh token is saved to ~/.config/microsoft-ads/tokens.json (mode 0600) and reused/auto-refreshed thereafter — so you never need to add it to .env by 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.

Authget_auth_url, complete_auth (one-time interactive sign-in; see below).

Readaccount_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.

Reportingrun_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, keywordscreate_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) accept tracking_url_template, final_url_suffix, and url_custom_parameters (a {key: value} map, referenced in templates as {_key}). create_ad_group / update_ad_group also accept network (ad distribution). create_campaign / update_campaign also accept bid_strategy_type to set the campaign's inline bid strategy (EnhancedCpc, ManualCpc, MaxClicks, MaxConversions, TargetCpa, MaxConversionValue, TargetRoas) with optional max_cpc / target_cpa / target_roas — e.g. MaxClicks + max_cpc is Maximize Clicks with a Maximum CPC limit (distinct from bid_strategy_id, which applies a portfolio strategy; set one or the other).
  • Account-level URL optionsset_account_url_options sets the tracking template, Final URL suffix, and msclkid auto-tagging once for the whole account (every campaign inherits them) — the cleanest single-point lever for an account-wide tracking/rebrand change.
  • Negative keywordsadd_negative_keywords, remove_negative_keywords (campaign or ad-group scope).
  • Ad extensionsadd_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 accept is_call_tracking_enabled (US/UK) to turn on Microsoft call tracking so call-from-ad conversions are measured — pass it on add_call_extension, or flip it on an existing asset with update_call_extension. New forwarding numbers are local (toll-free is no longer provisioned). They also accept is_call_only (the "Show just my phone number" call-only mobile format). Sitelinks carry the two description1 / description2 lines (set both or neither); structured snippets carry a header from Microsoft's predefined list (e.g. "Brands", "Services", "Types") plus 3-10 short values. The update_sitelink_extension / update_structured_snippet_extension tools 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_extension removes any extension type by id — it deletes the account-level object itself, not just a single campaign/ad-group association. get_ad_extensions surfaces the current is_call_tracking_enabled / is_call_only flags, sitelink descriptions, and snippet header/values.
  • Conversion goals / UET tagscreate_conversion_goal adds a goal: an OfflineConversion goal (keyed by MSCLKID, no UET tag) or a UET-backed web goal (Url / Event / Duration / PagesViewedPerVisit, which need a tag_id). Goals are created active (a goal doesn't spend; a paused one silently fails to record). update_conversion_goal edits a goal in place: rename, set status, and (most launch-relevant) toggle exclude_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 sets count_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_conversions is the bid-eligible path — filter the call-center log yourself (e.g. calls ≥60s), then upload qualifying calls by MSCLKID against an OfflineConversion goal whose name matches conversion_name. update_uet_tag renames/redescribes a tag.
  • Location (ZIP/geo) targetingadd_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 unless use_searcher_time_zone is 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 use replace_ad_schedule (which removes the old criterion then adds the new one — the only safe order) rather than adding over it. update_campaign accepts time_zone to set the zone those schedules run in.
  • Device bid adjustmentsset_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 APIbulk_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

microsoft_ads_mcp-0.3.0.tar.gz (174.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

microsoft_ads_mcp-0.3.0-py3-none-any.whl (110.0 kB view details)

Uploaded Python 3

File details

Details for the file microsoft_ads_mcp-0.3.0.tar.gz.

File metadata

  • Download URL: microsoft_ads_mcp-0.3.0.tar.gz
  • Upload date:
  • Size: 174.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","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

Hashes for microsoft_ads_mcp-0.3.0.tar.gz
Algorithm Hash digest
SHA256 67f74ff19ca7815d83d72e56a2ce5117a83d7b14e15821017161b35fb11fbce9
MD5 d9d4109e3e246abb2dc85d2698cac7c9
BLAKE2b-256 f12dfca191bcacb5a709763d744f1843dd75a6517caa5aff35950221937e9d73

See more details on using hashes here.

File details

Details for the file microsoft_ads_mcp-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: microsoft_ads_mcp-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 110.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","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

Hashes for microsoft_ads_mcp-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3999ad143f0d4c19634674f46a1616187820595868b3e59f1fdcf12c3f3410e9
MD5 350043ca90feadc541fddd628eb91174
BLAKE2b-256 71b132b1a467da30e841ba7f891f7c4f2fb2d50fcae76fcc271b78d16b87a874

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page