Changelog¶
All notable changes to this project are documented here. The format follows Keep a Changelog, and the project uses semantic versioning.
[Unreleased]¶
Fixed¶
- A path parameter can no longer send a catalogued operation to a different endpoint.
ProfileClient.call, which the MCP server'scall_operationandadsctl ops callboth use, put each path parameter into the path without encoding or checking it, and httpx resolves..segments before sending. Socall_operation("US", "stores:getInsightsForStoreAPI", ..., path_params={"brandEntityId": "../sp/targets?x="})sentPOST /sp/targets: a write made without a plan or the user's approval, and retried after a 5xx because the operation it started as is read-only.adsctl ops callskipped its confirmation the same way. A path parameter now raisesValueErrornaming it, before anything is sent, when it contains/,\,?,#or a control character; when it is empty or only dots and spaces, in whole or before its first;(Java servlet containers cut a segment at;); when it is already percent-encoded (%2F,%20); or when it is not a string or an integer (None, a bool, a float, bytes).call_operationandadsctl ops callrefuse it before even looking up the market's profile. Every other value is percent-encoded so Amazon reads back exactly what was given: letters, digits and-._~:@!$&'()*,=are sent as before, which keeps ids such asamzn1.ads-account.g.EXAMPLEunchanged, and%,;,+, spaces and non-ASCII letters are encoded (+because a server that decodes paths like form data reads it as a space). A value that was already percent-encoded used to be sent as it was and now raises, so pass plain values. Path placeholders that a spec leaves out of the operation's parameters must now be filled too; before, the request went out with the placeholder still in the path. Reports.status,wait,download(given an id) anddeletecheck the report id the same way.reports.delete("../../sb/keywords/123")sentDELETE /sb/keywords/123, which archives a Sponsored Brands keyword; it now raisesValueError.adsctl ops callreports a bad-por-qas a usage error (exit code 2) before it asks for confirmation or contacts Amazon: a path parameter refused as above, a pair without=or a name, or a name given twice. These used to end in a Python traceback after the confirmation and the profile lookup, and a repeated name silently kept only the last value.Reports.downloadraisesReportFailedError, naming the report, as soon as an expired download URL has no replacement. It used to fetch an empty URL instead, which the transport retried with backoff for up to two minutes before failing with httpx's missing-protocol error.- The catalog lists the path parameters of four marketing mix modeling operations,
whose spec writes
{brandGroupId}and{reportId}into their paths without declaring them, soops showsaid they took none.scripts/generate_catalog.pynow adds any placeholder a spec leaves undeclared. advertising-billing:getAdvertiserInvoices(GET /invoices) andadvertising-billing:getInvoice(GET /invoices/{invoiceId}) now sendAccept: application/json. Amazon's spec documents onlyapplication/vnd.invoices.v1.1+jsonandapplication/vnd.invoice.v1.1+json, and the live API answers both with 406 Not Acceptable and an empty body (the list on all 12 marketplaces tested), socall()andcall_operationcould not read invoices at all. The fix is acatalog.CORRECTIONSentry applied when the catalog loads, not an edit of the generatedcatalog.json, so regenerating the catalog from Amazon's specs keeps it.Operation.notesays why an operation differs from its spec,op.acceptsstill lists what the spec documents, and the coverage page marks the two operations corrected. API notes describe the 406 and the rest of what the invoice endpoints do that the spec does not say: the{"status", "payload"}wrapper,nextCursorpaging that drops the key on the last page, the open billing period listed asACCUMULATINGwith no id, andPOST /invoiceSummaries/listanswering withinvoiceSummariesat the top level where its spec nests them underaggregation.- The
mcp-*operations are no longer sent. They come from the OpenAPI files in which Amazon publishes the tools of its hosted Ads MCP server (beta), and theirPOST /mcp/<tool>paths are not routes on the Ads API hosts, which answer them with 403 "Invalid key=value pair (missing equal-sign) in Authorization header", the same as any path they do not serve. That reads like a demand for AWS request signing but is not one. 47 of the 110 passedOperation.read_only, socall_operationlet an agent spend a call on them.ProfileClient.call(),call_operationandadsctl ops callnow refuse them before any request withUnsupportedOperationError, whose message names the Ads API operation to call instead formcp-billing:list_invoicesandmcp-billing:list_invoice_summaries.call_operationchecks this before its write check, so an MCP tool that writes is not pointed at the plan tools. Operation.read_onlyno longer calls a write read-only because a read word appears inside a path segment. It matched its read words (list,query,get,search, ...) anywhere inside the last path segment, so "get" inside "targets", "budgetRules" and "targetPromotionGroups", and "search" inside "audienceResearch", made 60 Ads API writes count as reads, among themsponsored-products:CreateSponsoredProductsTargetingClauses(POST /sp/targets),sponsored-brands-v3:createTargetsandcreateNegativeTargets, the targeting creates of the DSP, media planning and unified (/adsApi/v1/create/targets) APIs, and the budget rule creates and associations for Sponsored Products, Brands and Display. Through those,call_operationon the MCP server created entities with no plan and no approval,ProfileClient.call()resent them after a 5xx or a timeout (which can create every target twice), andadsctl ops callsent them without asking. A read word now has to start the operation id or a whole path segment, and an operation id or segment starting withcreate,update,deleteorarchiveis never read-only, which also catchesrule-based-audiences:createQueryBasedAudience(POST /amc/audiences/query). Every path the transport retries as read-only (READ_ONLY_POST_SUFFIXES) now counts as read-only in the catalog too. Because the read words now match any segment, 28 reads on the unified/adsApi/v1/query/...paths, plussponsored-brands-v4:SBOptimizationRecommendationandSBTargetingGetTargetableASINCounts, become read-only and callable throughcall_operation. Three reads that were read-only only by the same substring accident now count as writes, socall_operationrefuses them andops callasks first:dsp-quick-actions:batchGetExecutionsV1,measurement:CheckDSPAudienceResearchEligibilityand the deprecatedsponsored-products-v2:createKeywordBidRecommendations. One write named like a read is still read-only:creative-assets:getUploadLocation, which creates an upload URL that expires after 15 minutes.- A Sponsored Products campaign budget given as a number (
{"campaignId": "9", "budget": 20}) is now sent as the object Amazon requires,{"budget": 20.0, "budgetType": "DAILY"}. Amazon rejects a bare number with a 400 for the whole request that names no field, andplan_updatebuilt a plan that looked right, with an object before and a number after, so the only clue was the mismatched shapes in the plan table. Budgets are now widened and validated through theBudgetmodel increate,update,plan_updateandplan_createalike, so a plan's before and after read the same way and the plan that is approved is the request that gets sent.plan_updateandplan_createrefuse a budget that is neither a number, a mapping nor aBudgetwith aValueErrornaming the field, instead of building a plan that fails part way through applying.plan_updateknows the campaign's current values and keeps its existingbudgetType, so raising a lifetime budget no longer plans a switch to daily;updateandcreateread nothing back, so a bare number there means a daily budget, and the object has to be passed explicitly to say otherwise. The budget may be given in either spelling (budgetTypeorbudget_type) or as aBudgetinstance. Sponsored Brands campaigns are untouched: they store the budget as a plain number beside a siblingbudgetType, and the widening would break them.EntitySpec.object_fieldsrecords, per entity, which fields Amazon takes only as an object.
Added¶
catalog.path_segment(operation_id, name, value)checks and encodes one path segment the wayProfileClient.calldoes, for a path built by hand forProfileClient.request.-
UnsupportedOperationError,Operation.unsupported_reason,Operation.note,Operation.ensure_sendable(),catalog.CORRECTIONS,catalog.is_hosted_mcp_tool(),catalog.search(include_unsupported=True)andadsctl ops search --include-unsupported.search_operationsrows carrynoteandunsupportedwhen they apply. -
API notes now record Amazon's keyword text constraints and the shape of its report creation throttle, so
api_notesanddocs/guide/quirks.mdcarry both. The keyword limits (80 characters; 10 space-separated parts for a positive keyword or anegativeExact, 4 for any other negative match type; the hyphen, plus and period only inside a word; and a short list of accepted symbols that excludes round brackets and the colon) are documented by Amazon but only on a JavaScript-rendered page, and appear nowhere in the OpenAPI specs, wherekeywordTextis a plain string with nomaxLengthand nopattern. A client cannot refuse a bad keyword from the spec, so an over-long one is only discovered as a per-item 207 error after the rest of the batch has been written. Report creation (POST /reporting/reports) is throttled per account over a window longer than one request's retries can span, so a bulk sync asking for many report types across several profiles has its last requests refused while the earlier ones succeed; the note says to retry after the run has stopped asking rather than inside the loop, and to widenRetryPolicyfor such a sync. sb.keywords.createandsb.negative_keywords.createdropstate. Both endpoints reject it outright, although a read returns it and an update accepts it, and a created keyword is enabled anyway.SbV3Spec.create_forbiddenholds the fields a create must not send.- Sponsored Brands ad groups and ads:
ProfileClient.sb.ad_groupsandsb.ads(v4), so a Sponsored Brands campaign can be built end to end rather than only updated. A campaign needs an ad group and an ad to serve at all, and the API accepts a campaign without either. Ads have no single create endpoint, one per format instead, sosb.adstakes anadFormatfield (defaultproductCollection, the console's Collections layout), posts to that format's endpoint and leaves the field out of the body; one call cannot mix formats because Amazon numbers per-item results by their position in the request. The MCP server'slist_entities,plan_update,plan_createandplan_archiveacceptsb_ad_groupsandsb_ads, andadsctlacceptssb-ad-groupsandsb-ads. - Sponsored Brands campaign management:
ProfileClient.sb.campaigns(v4),sb.keywordsandsb.negative_keywords(v3) with the same list, update, create, archive and change-plan interface as Sponsored Products. Plans, drift checks and rollback plans work for them, so the MCP server'slist_entities,plan_update,plan_createandplan_archiveacceptsb_campaigns,sb_keywordsandsb_negative_keywords, andadsctl list/adsctl plan updateaccept thesb-*names. Updates fill in the parent ids SB v3 requires; campaign updates carry placement premiums inbidding. - Sponsored Brands report presets:
sb_campaigns,sb_targeting,sb_search_termsandsb_purchased_products, available toReports.run,adsctl report,adsctl syncand the MCP server'srun_report. BatchErrorcarries Amazon's error family (error_type), the allowed range for bid and range errors (lower_limit,upper_limit), a plain-languagehint, andtransient.- Items rejected as throttled inside a 207 batch are sent again alone with backoff; items that hit an internal error are resent for updates and archives but never for creates.
apply_planin the MCP server reports each failed item with its id, label, what was sent, the allowed range and what to do.
Changed¶
catalog.search(),search_operationsandadsctl ops searchleave out operations this client cannot send (today the 110mcp-*tools) unless asked for them or scoped to their API withapi=. A scoped search returns them with the reason instead of an empty list. The coverage page counts 1,094 sendable operations in 202 Ads API specs, counts the 18 MCP tool specs (110 tools) apart, and marks their rows MCP tool.
[0.1.0] - 2026-09-26¶
First release.
Added¶
AmazonAdsandProfileClient: Login with Amazon token caching and refresh, profile discovery across the NA, EU and FE regions, market lookup by country code or id.- Operation catalog generated from all 220 OpenAPI specs Amazon publishes (1,204
operations), with
ProfileClient.call()to send any of them with the right method, path and versioned media types. - Typed Sponsored Products v3 resources (campaigns, ad groups, keywords, targets, ad group and campaign negatives, product ads) and portfolios: paginated listing, create, update, pause, enable and archive, with batch chunking and read-only field stripping taken from Amazon's spec.
BatchResultfor Amazon's 207 Multi-Status responses, pairing every success and error with the item sent.- Change plans: before/after review, fingerprints, drift checks, rollback plans built from what Amazon accepted, and restoring bids and states from change history.
- Suggested bids for keywords, product (ASIN) targets and auto targets, with the
keyword/ASIN split and
PAT_ASINconversion the endpoint needs, and seasonal themes. - Change history with
value_at()for what a bid or state was at a past moment. - Reporting v3 in one call: 31-day chunking, parallel queueing, polling with backoff, 425 duplicate reuse, retention warnings, and seven Sponsored Products presets verified against the live API.
- SQLite warehouse that keeps report history past Amazon's retention and re-pulls the 42 days Amazon may still restate, with a SQL view per report.
- Throttling:
Retry-Afterhonored, a cooldown shared across requests, and no retries of writes that could duplicate. adsctlcommand line tool for auth, profiles, listing, bids, history, reports, syncing, plans and any operation.amazon-ads-mcpMCP server: reads, plans, and fingerprint-gated applies, with a read-only mode.- Documentation site, API quirks page, and coverage page generated from the catalog.