MULTI-APP TOOLKIT
Build with Google Search Console + LinkedIn Ads
- Multi-app
- One MCP session
- 13 actions
- 2 triggers
- Managed auth
MCP
One MCP session, both toolsets
One session gives your product or agent every Google Search Console and LinkedIn Ads tool at once — connect once, list tools, and call them like any single-app session.
// accessToken: mint a short-lived token with the Connect SDK — see the MCP guide
const transport = new StreamableHTTPClientTransport(
new URL("https://remote.mcp.pipedream.net/v3"),
{
requestInit: {
headers: {
Authorization: `Bearer ${accessToken}`,
"x-pd-project-id": "{project_id}",
"x-pd-environment": "production",
"x-pd-external-user-id": "{external_user_id}", // any stable ID for this user in your system
"x-pd-app-slug": "google_search_console,linkedin_ads",
},
},
},
)
const mcp = new Client({ name: "my-agent", version: "1.0.0" })
await mcp.connect(transport)
const { tools } = await mcp.listTools()
// One list, both toolsets: Google Search Console and LinkedIn Ads tools arrive
// together, each keyed by its own app's slug.
// e.g. run Compare Search Analytics:
const result = await mcp.callTool({
name: "google_search_console-compare-search-analytics",
arguments: {
siteUrl: "Property (siteUrl)",
currentStartDate: "Current Period Start Date (YYYY-MM-DD)",
},
})import { PipedreamClient } from "@pipedream/sdk"
const pd = new PipedreamClient({
projectId: process.env.PIPEDREAM_PROJECT_ID!,
clientId: process.env.PIPEDREAM_CLIENT_ID!,
clientSecret: process.env.PIPEDREAM_CLIENT_SECRET!,
projectEnvironment: "production",
})
const externalUserId = "{external_user_id}" // any stable ID for this user in your system
const [googleSearchConsoleTools, linkedinAdsTools] =
await Promise.all([
pd.components.list({ app: "google_search_console" }),
pd.components.list({ app: "linkedin_ads" }),
])
// One external user owns both connected accounts, so either app's tools
// run on their behalf with the same externalUserId.curl "https://api.pipedream.com/v1/connect/{project_id}/components?app=google_search_console" \
-H "X-PD-Environment: production" \
-H "Authorization: Bearer {access_token}"
curl "https://api.pipedream.com/v1/connect/{project_id}/components?app=linkedin_ads" \
-H "X-PD-Environment: production" \
-H "Authorization: Bearer {access_token}"
# Connect both accounts to the same external_user_id, then
# configure and invoke either app's tools on that user's behalf.ARCHITECTURE
One user, two connected accounts
Your user connects each account once, under whatever ID they already have in your product. The two stay independent — either can be revoked on its own — and your code reaches both through that one user.
Your product
external_user_id
{external_user_id}Pipedream Connect
Managed identity
Auth · tools · routing
Google Search Console
Connected account
LinkedIn Ads
Connected account
TOOLS
Google Search Console tools
The Google Search Console tools this pairing puts in reach, each one running on your user's own connected account.
Actions
-
Compare Search Analytics
actionCompare Google Search Console traffic between two date ranges for one property and return the deltas. Fetches both periods in parallel, joins the rows on their dimension keys, and reports per-row and total
clicks,impressions,ctrandpositionchange — the period-over-period arithmetic is done for you.Use for any question that compares two date ranges: month over month, quarter over quarter, year over year, "which queries gained or lost the most clicks", "did mobile grow", "did that update hurt us". Use Query Search Analytics for a single date range, for paging past 5000 rows, and for the
hourandsearchAppearancedimensions this tool does not accept. Property-level vs page-level position questions also need two Query Search Analytics calls with differentaggregationTypevalues.Returns
{ current_period, previous_period, totals, rows, row_count, has_more, truncated, note }.totalscarriescurrent,previous,deltaandpct_changefor the whole period, computed from every fetched row rather than just the returned ones. Each row is{ keys, current, previous, delta, pct_change }.Reading the deltas. A key present in only one period gets zeros for the other, so new and lost queries both show up — but its
delta.ctranddelta.positionarenull, because the missing period has no CTR or position to compare against (delta.clicksanddelta.impressionsare still real numbers).pct_changeisnullwhere the previous period was zero — not 0 and not infinity.ctris a 0-1 fraction (0.1428means 14.3%) andpositionis 1-indexed where lower is better, so a NEGATIVE position delta is an improvement; both are impression-weighted, so never re-average them across rows. Query rows always understate the real total because Google omits anonymized (rare) queries — compare with no dimensions, or bydate, for true totals.truncatedis true when either period hit the internal 5000-row cap, so rows and totals may be incomplete: narrow the range or add a filter.has_moreis a different signal — it only means the join produced more rows thanrowLimit.notewarns about anonymized queries when relevant, otherwise it isnull.Mistakes. "The previous period" means the same number of days immediately before the current period: for 2026-08-01..2026-08-28 (28 days) that is 2026-07-04..2026-07-31. "The same period last year" means both dates shifted back one year. Grouping Discover by
queryreturns a 400 — Discover has noquerydimension. Comparing a range that ends today makes the current period look artificially low, because data is final only after about 2-3 days unlessdataStateisall.filterValue(withfilterDimension/filterOperator) is applied identically to both periods and wins overadvancedDimensionFilters; the equivalent prop on Query Search Analytics is namedsubdomainFilterfor backwards compatibility.Example.
siteUrl="sc-domain:example.com",currentStartDate="2026-08-01",currentEndDate="2026-08-28",previousStartDate="2026-07-04",previousEndDate="2026-07-31",dimensions=["query"],sortBy="clicks_delta"returnstotals: { current: { clicks: 74, ... }, previous: { clicks: 68, ... }, delta: { clicks: 6, ... }, pct_change: { clicks: 0.0882, ... } }and rows such as{ keys: ["example brand"], current: { clicks: 41, ... }, previous: { clicks: 33, ... }, delta: { clicks: 8, ... } }.See the documentation
Read-onlyv0.0.1 -
Delete Sitemap
actionUnlists a sitemap from a Google Search Console property. Destructive, and not reversible by this tool.
Confirm before calling. Only call it once the user has explicitly confirmed the exact
sitemapUrl. If the ask is vague ("remove the old sitemap"), call List Sitemaps first, show the candidate paths and ask which one. If the user names a sitemap but has not confirmed the deletion, state the exactsitemapUrlyou are about to delete and ask. Silence is not consent: if you cannot obtain an explicit confirmation in this turn (for example no confirmation prompt is available), end your turn by asking in plain text and do NOT call this tool. Never delete a sitemap as a side effect of another task, and never delete-and-resubmit to "refresh" one — Submit Sitemap on the existing path already does that.What this does NOT do. It only UNLISTS the sitemap from Search Console. The pages it contained stay in Google's index and remain crawlable, and the sitemap file stays on the website. Do not offer this tool as a way to remove content from Google.
Returns
{ deleted: true, sitemapUrl }— the API body is empty, so there is nothing else to report. To prove it is gone, call List Sitemaps with NOsitemapUrland check thepathis absent; asking for the deleted path directly returns 404 "'<url>' is not a submitted or a known sitemap.", which reads as an error.Mistakes. Passing a path (
/sitemap.xml) or a guessed URL instead of apathfrom List Sitemaps returns that same 404 — and so does thehttps://www.variant when the property lists thehttp://one, because host and scheme are part of the identity. RequiressiteOwnerorsiteFullUser; asiteRestrictedUsergets 403.See the documentation
Writev0.0.1 -
Inspect URLs
actionReturns Google's index status, canonical selection and crawl state for 1-10 URLs of one Search Console property in a single call. This is the API behind the URL Inspection tool in the Search Console UI.
Use for "is this page indexed?", "when did Google last crawl it?", "does Google's canonical match the one I declared?", "why is this URL missing from search?", and batch health checks after a deploy or migration — pass every URL you care about in ONE call, not one call per URL.
Not for backlinks. Search Console's Links report has no API at all.
referringUrlshere is only a small sample of pages Google happened to discover the URL from — not a backlink profile — so when asked for backlinks, call no Search Console tool and say plainly that the links report is not available through the API. The result also carries no Core Web Vitals or page-experience data. And if the user asks to "request indexing" or force a recrawl of an ordinary page, do not run this tool (or any other) on your own initiative: explain that no API does that, OFFER this index-status check or a sitemap resubmission via Submit Sitemap, and wait for them to choose.Returns
{ results: [...], summary: { total, indexed, not_indexed, errors } }, one row per URL in the same order asinspectionUrls. Reading the rows:verdictisPASS(indexed),NEUTRAL(known but not indexed, or unknown to Google),FAILorPARTIAL. A URL Google has never seen returnsNEUTRALwith acoverageStatelike"URL is unknown to Google"— that is a valid answer, not an error.canonical_mismatchistruewhengoogleCanonicalanduserCanonicalare both present and differ,falsewhen they match,nullwhen either is missing.referringUrlsis truncated to the first 5;referring_url_countis the full count.errorisnullon success and a message when that single URL failed — one bad URL never aborts the batch. Insummary,indexedcountsPASSrows,errorscounts rows with anerror, andnot_indexedis everything else.- The raw API result is attached per row as
full_resultONLY whenincludeFullResultis true. Leave it off unless the user asks for the complete raw result: it is large and mostly rich-results and AMP detail.
Mistakes. A path such as
/aboutis not accepted — send full absolute URLs that live undersiteUrl. Each inspection takes Google roughly 5-10 seconds, so a 10-URL batch runs about 20 seconds; that is normal, not a hang. Quota is 2,000 inspections per day AND 600 per minute per property, and a quota error does not say which was hit. RequiressiteOwnerorsiteFullUser— asiteRestrictedUsergets 403, and so does a mismatchedsiteUrl(URLs outside the property, a missing trailing slash,sc-domain:versus URL-prefix confusion), so copy the identifier verbatim from List Sites.Example.
siteUrl="sc-domain:example.com",inspectionUrls=["https://www.example.com/"]->results[0]hasverdict: "PASS",coverageState: "Submitted and indexed",googleCanonical: "https://www.example.com/",userCanonical: "https://example.com/"andcanonical_mismatch: true— Google indexed the www URL even though the page declares the non-www one.See the documentation
Read-onlyv0.0.1 -
List Sitemaps
actionLists the sitemaps Google Search Console knows about for a property, with their error, warning and freshness state normalized to numbers.
Use for any sitemap question — which are submitted, which have errors or warnings, when Google last downloaded one, how many URLs it contains. Always call it before Delete Sitemap when the user has not named an exact sitemap URL, and after Submit Sitemap or Delete Sitemap to confirm the new state: in both cases leave
sitemapUrlempty and look for thepath, because a single-path lookup of a deleted or unknown sitemap returns 404.Not for whether the URLs inside a sitemap are indexed (use Inspect URLs). This tool reports what Google recorded about a sitemap; it does not fetch or parse the XML itself.
Returns
{ sitemaps: [...], count, summary: { with_errors, with_warnings, pending, never_downloaded } }.pathis the full sitemap URL and is the exact string to pass to Submit Sitemap or Delete Sitemap.warnings,errorsandsubmitted_urlsare integers here (the raw API returns strings);submitted_urlsis the sum ofcontents[].submitted.isPending: truemeans Google accepted the sitemap but has not fetched it yet. A sitemap with nolastDownloadedhas never been downloaded and is counted insummary.never_downloaded.Mistakes. Do not guess a sitemap URL — list first and use a
pathfrom the result; an unknown path returns 404 "'<url>' is not a submitted or a known sitemap."lastDownloadedcan be years old while the sitemap is still valid, so report the date rather than treating it as an error.errorsandwarningscount sitemap-parsing problems, not indexing problems.Example.
siteUrl="sc-domain:example.com"with no other input returns{ count: 3, summary: { with_errors: 1, ... }, sitemaps: [{ path: "https://www.example.com/sitemap.xml", lastDownloaded: "2018-05-06T02:44:10.000Z", errors: 1, warnings: 1, submitted_urls: 2, contents: [{ type: "web", submitted: "2", indexed: "0" }] }, ...] }— so "when was it last downloaded and how many URLs?" is 6 May 2018 and 2 URLs.See the documentation
Read-onlyv0.0.1 -
List Sites
actionLists every Google Search Console property the connected Google account can access, plus that account's email address.
Call this first on any per-site task, unless the user already gave an exact identifier such as
sc-domain:example.com: every other tool needs the identifier byte-for-byte, and it must be copied from here, never constructed. Call it again after a 403 to see what the account really has.Do NOT call it when Search Console cannot do the task at all — backlinks or the Links report, requesting indexing of an ordinary page, adding or removing property owners. Say that first, and call this only if the user then asks for something the tools do cover.
Returns
{ account_email, sites: [{ siteUrl, permissionLevel, property_type }], count }, domain properties first then alphabetical. The list is complete — the API has no pagination.property_typeis"domain"for ansc-domain:identifier (covers every subdomain and both schemes — prefer it for traffic questions unless the user names a specific prefix) and"url_prefix"otherwise (an exact scheme + host + path prefix, trailing slash included).account_emailisnullif the email could not be read; the site list still returns.Permission levels.
siteOwnerandsiteFullUsercan submit and delete sitemaps and inspect URLs (only an owner can manage users);siteRestrictedUseris read-only on reports and cannot submit sitemaps or inspect URLs (403);siteUnverifiedUserhas no data at all. Check the level here before promising a write.Mistakes. A URL-prefix identifier with the wrong scheme or subdomain, or missing its trailing slash, returns 403 "User does not have sufficient permission for site" even when the account is authorized — so copy
siteUrlverbatim. This action takes no parameters.See the documentation
Read-onlyv0.0.1 -
Query Search Analytics
actionQuery Google Search Console search analytics for one property and one date range: clicks, impressions, CTR and average position, optionally grouped by dimensions and filtered. This is the main traffic-reporting tool for a site.
Use for any single-date-range question about how a site performs in Google Search — top queries, top pages, country or device splits, daily or hourly trends, the CTR or average position of a term. Use Compare Search Analytics instead for period-over-period questions (month over month, year over year, "did the update hurt us") — it fetches both ranges and joins them for you. Use Inspect URLs for index status, canonicals and crawl state; this tool only reports traffic.
Returns the API response unchanged —
rows(each{ keys, clicks, impressions, ctr, position }, wherekeyslines up positionally withdimensions),responseAggregationTypeandmetadata— plusrow_count,has_more(the page came back full, so more rows probably exist),next_start_row(pass it back asstartRow) andreturned_totals(clicksandimpressionssummed over the RETURNED rows only).Reading the numbers.
ctris a 0-1 fraction (0.1428means 14.3%) andpositionis a 1-indexed float where LOWER is better. Both are impression-weighted, so never re-average them across rows — a plain mean is wrong.returned_totalsis not the property total, least of all when grouping byquery: Google omits anonymized (rare) queries, so the sum of query rows is materially LESS than the same range grouped bydate. For a true total, query with no dimensions or withdate. And checkhas_morebefore reporting a count or a "top N" — a truncated first page is not the whole answer.Scoping to one page or segment. Set
subdomainFilter— the filter VALUE, despite the legacy prop name — together withfilterDimensionandfilterOperator. For "page X only" that isfilterDimension: page,filterOperator: equals, value = the full URL. Selecting a matching URL-prefix property instead is NOT equivalent: a prefix property covers every page under it, so the numbers come back for the whole prefix. UseadvancedDimensionFiltersfor multi-condition filters; it is ignored wheneversubdomainFilteris set.Freshness. The most recent 2-3 days are not final and are omitted under
dataState: final(the default), so a range ending today comes back one to three rows short. When the user asks for "the last N days" and every day needs a figure, setdataState: all.Discover has no
querydimension (400 "Request for DISCOVER cannot be grouped by query"). When the user asks for Discover queries, do not stop to ask: report Discover pages instead (searchType: discover,dimensions: ["page"]) and say why.Example.
siteUrl="sc-domain:example.com",startDate="2025-09-01",endDate="2026-08-31",dimensions=["query"],rowLimit=10returns rows such as{ keys: ["example brand"], clicks: 41, impressions: 287, ctr: 0.1429, position: 2.4 }plusrow_count: 10,has_more: trueandnext_start_row: 10.There is no
fieldsparameter — rows are already minimal, sorowLimitplushas_more/next_start_rowis the payload lever. Quota: 1,200 queries per minute per site. See the documentationRead-onlyv1.1.0 -
Submit Sitemap
actionSubmits a sitemap (or resubmits one already listed) to Google Search Console for a property, then reads the stored record back. Resubmitting is idempotent — it creates no duplicate, it updates
lastSubmittedand setsisPending: true.Use for a new sitemap, or when the user wants Google to pick up new or changed pages on a site: resubmitting an existing sitemap is the SUPPORTED way to ask Google to re-read it.
There is no API to request indexing of a single ordinary page. The "Request indexing" button in the Search Console UI has no API equivalent, and the Indexing API behind Submit URL for Indexing covers only JobPosting and BroadcastEvent pages. So the two legitimate options are this tool (have Google re-read the sitemap containing the page) and Inspect URLs (check the page's current index status). Say that plainly rather than implying a page can be force-indexed — and when the user asks to request indexing or force a recrawl, OFFER those two options and wait for them to pick one; do not run either unasked.
Returns
{ submitted: true, sitemap, previous_last_submitted }. The submit call itself returns an empty body, sositemapis the record read back from Google immediately afterwards and is how you confirm it landed.previous_last_submittedis thelastSubmittedthe sitemap had before this call, ornullfor a first submission — use it to tell a fresh submission from a resubmission. Right after a submitisPendingis normallytrueandlastDownloadedstill holds the old date (or is absent): Google fetches asynchronously, usually within minutes to days. So do NOT report a sitemap as "processed" or "indexed" on the strength of a successful submit.Mistakes.
sitemapUrlmust be an absolute URL to the sitemap file (https://www.example.com/sitemap.xml) that lives under the property — not a page URL, a path (/sitemap.xml) or a property identifier. If the user has not given you a sitemap URL, ask for it rather than guessing a conventional path. Submitting is not indexing: Google may still choose not to index the URLs it finds. Do not delete and resubmit to "refresh" a sitemap — just resubmit. RequiressiteOwnerorsiteFullUser(check with List Sites); asiteRestrictedUsergets 403.See the documentation
Writev0.0.1 -
Submit URL for Indexing
actionSends a
URL_UPDATEDorURL_DELETEDnotification for one page to Google's Indexing API (indexing.googleapis.com) — a SEPARATE API from Search Console reporting.Use only for pages carrying JobPosting or BroadcastEvent (livestream
VideoObject) structured data — Google supports nothing else: a job listing that was posted, changed or filled, or a livestream page going live or ending. Default quota is 200 notifications per day per project, and the connected account must be a verified owner of the site.Do NOT use it to "request indexing" an ordinary page — no API does that. The "Request indexing" button in the Search Console UI has no API equivalent, and calling this for a normal page does not get it crawled sooner; Google ignores or rejects notifications for pages without the supported structured data. When a user asks you to request indexing, recrawl or "push" an ordinary page, say that no API can do it and offer the two real options instead: Submit Sitemap to have Google re-read the sitemap containing the page, and Inspect URLs to check its current index status and last crawl time.
Returns Google's
urlNotificationMetadata: the notifiedurlpluslatestUpdate/latestRemoveobjects carryingtypeandnotifyTime. A success means the notification was accepted, NOT that the page was crawled or indexed.Mistakes. The
siteUrlprop is the page URL to notify about, not a property identifier — the name is legacy. Pass a full canonical page URL such ashttps://www.example.com/jobs/paleobotanist; neversc-domain:example.comor a bare property prefix. SendURL_DELETEDonly after the page actually returns 404 or 410. Do not burn the 200/day quota on ordinary pages.See the documentation
Writev0.0.6
Triggers
No Google Search Console triggers are available yet.
TOOLS
LinkedIn Ads tools
The LinkedIn Ads tools this pairing puts in reach, each one running on your user's own connected account.
Actions
-
Create A Report
actionQueries the Analytics Finder to get analytics for the specified entity i.e company, account, campaign. See the docs hereRead-onlyv0.0.8 -
Create Report By Advertiser Account
actionSample query using analytics finder that gets analytics for a particular account for date range starting in a given year. See the docs hereRead-onlyv0.0.8 -
List Accounts Options
actionRetrieves available options for the Accounts field.Read-onlyv0.0.2 -
Query Analytics Finder Campaign Sample
actionSample query using analytics finder that gets analytics for a particular campaign in a date range starting in a given year. See the docs hereRead-onlyv0.0.8 -
Send Conversion Event
actionSends a conversion event to LinkedIn Ads. See the documentationWritev0.0.8
Triggers
-
New Event Registration Form Response
triggerEmit new event when a fresh response is received on the event registration form. User needs to configure the prop of the specific event. See the documentationv0.0.6 -
New Lead Gen Form Created
triggerEmit new event when a new lead is captured through a form. See the documentationv0.0.5
MULTI-APP
Works with more apps
The combinations customers connect alongside these two — nothing here is limited to a pair.
REFERENCE
Toolkit details
- x-pd-app-slug
- google_search_console,linkedin_ads
- Primary app
- Google Search Console (google_search_console)
- Second app
- LinkedIn Ads (linkedin_ads)
- Authentication
- OAuth + OAuth
- Available actions
- 13
- Available triggers
- 2