# SEO MCP tools: all 49, with their inputs

> Every tool on the TheSerpAgent MCP server, with names and inputs read from the server: what it does, when to call it, what it costs and what comes back.

Page: https://theserpagent.ai/docs/mcp-tools

TheSerpAgent is the rank tracker your AI runs. This is every tool on its SEO MCP server, for agents and for the people connecting them: what each one does, when to call it, what it costs and what comes back.

**Server name** TheSerpAgent. Config files use the key theserpagent. It is not listed in a directory yet, so connect it by this address

**Address** https://api.theserpagent.ai/api/mcp

**Transport** Streamable HTTP, JSON replies, stateless

**Protocol versions** 2025-06-18, 2025-03-26, 2024-11-05

**Key** Authorization: Bearer KEY, created in the app under AI assistants by the account owner

**Rate limit** 120 tool calls a minute per account

**Reach** One account. Tools only, with no resources or prompts

**Depth** The top 30. A position of null means not in the top 30

## Use it when

The person wants positions tracked over time, asks what moved, compares two dates, watches competitors or Google AI Overview, needs a client report, or wants an alert when a page drops.

## Do not use it when

The person wants keyword research, backlinks, Bing, the Map Pack, positions below 30, or a one-off lookup with nothing to track.

Tools do not reach billing, login or API keys, which stay in the dashboard with CSV import and export. This is a rank tracker, and it offers no public SERP API.

## Start here

### list_projects

Read only

The tracked websites, with keyword counts and average position.

**When** First, to get the project ids most other tools need.

**Cost** Not metered.

It takes no inputs.

### What comes back

```
{
  "projects": [
    {
      "id": 1,
      "domain": "alpinerun.example",
      "name": "Alpine Run",
      "location": "New York, New York, United States",
      "language": "en",
      "device": "desktop",
      "keywords": 18,
      "has_icon": false,
      "average_position": 8.5
    }
  ]
}
```

### search_locations

Read only

Finds a country or city and returns its location code, up to 10 matches.

**When** Before create_project or update_project.

**Cost** Not metered.

Inputs of search_locations

| Input | Type and meaning |
| --- | --- |
| `query` required | `string`. Part of a country or city name, e.g. "Berlin". |

### What comes back

```
{
  "locations": [
    {
      "location_code": 1023191,
      "name": "New York, New York, United States",
      "type": "City"
    }
  ]
}
```

### list_languages

Read only

The Google languages a project can use, with their codes.

**When** Before create_project or update_project, to set a language.

**Cost** Not metered.

It takes no inputs.

### What comes back

```
{
  "languages": [
    {
      "code": "en",
      "name": "English"
    },
    {
      "code": "es",
      "name": "Spanish"
    }
  ]
}
```

### get_usage

Read only

The plan, its limits and what is left: keywords, projects and refresh checks this month and this hour.

**When** Before refreshing many keywords.

**Cost** Not metered.

It takes no inputs.

### What comes back

```
{
  "plan": "starter",
  "keywords": {
    "used": 34,
    "limit": 200
  },
  "projects": {
    "used": 4,
    "limit": 10
  },
  "refresh_checks": {
    "left_this_month": 1000,
    "monthly_limit": 1000,
    "left_this_hour": 500,
    "hourly_limit": 500
  }
}
```

## Read the record

### get_overview

Read only

A project's headline numbers: average position and the one before it, how many keywords sit in the top 3, 10 and 30, and today's biggest winners and losers. The average covers only the keywords in the top 30, which buckets.ranked counts. A loser with a null position left the top 30 from dropped_from.

**When** For the state of a project at a glance.

**Cost** Not metered.

Inputs of get_overview

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "project": {
    "id": 1,
    "domain": "alpinerun.example"
  },
  "average_position": 8.47,
  "previous_average_position": 8.35,
  "buckets": {
    "top3": 3,
    "top10": 11,
    "top30": 17,
    "ranked": 17,
    "not_ranked": 1,
    "pending": 0,
    "total": 18
  },
  "winners": [
    {
      "id": 6,
      "term": "trail running poles",
      "position": 3,
      "change": 1
    }
  ],
  "losers": [
    {
      "id": 1,
      "term": "trail running shoes",
      "position": 7,
      "change": -3,
      "dropped_from": null
    }
  ]
}
```

### get_rankings

Read only

Current positions for a project's keywords, with previous and best position, monthly searches and CPC when known. Each keyword also says whether it is starred, whether Google shows an AI Overview and whether you are cited in it. A position of null means not in the top 30, or not checked yet when last_checked is also null.

**When** To read the current state, or to get keyword ids. Filter by list, search words, stars, position range or only what moved at the last check.

**Cost** Not metered.

Inputs of get_rankings

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `tag` | `string`. Only keywords in this list (tag name). |
| `search` | `string`. Only keywords containing this text. |
| `starred` | `boolean`. Only starred keywords. |
| `min_position` | `integer`. Best position to include, e.g. 4 to skip the top 3. |
| `max_position` | `integer`. Worst position to include, e.g. 10 for page one. |
| `changed_only` | `boolean`. Only keywords whose position changed at the last check. |
| `sort` | `term \| position \| best \| last_checked \| volume \| cpc` |
| `limit` | `integer`. Rows per page, default 50, max 200. |
| `page` | `integer` |

### What comes back

```
{
  "keywords": [
    {
      "id": 3,
      "keyword": "best running socks",
      "position": 1,
      "previous_position": 1,
      "best_position": 1,
      "monthly_searches": 4400,
      "cpc_usd": 0.64,
      "url": "https://alpinerun.example/guides/best-running-socks",
      "lists": [
        "socks"
      ],
      "starred": false,
      "ai_overview": true,
      "ai_cited": true,
      "last_checked": "2026-11-07"
    }
  ],
  "page": 1,
  "pages": 18,
  "total": 18
}
```

### get_keyword_history

Read only

One keyword's daily record: date, position and ranking page, up to 365 days back. A position of null means not in the top 30.

**When** To quote a past position, or to see how a keyword got where it is.

**Cost** Not metered.

Inputs of get_keyword_history

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `keyword_id` required | `integer`. Keyword id from get_rankings. |
| `days` | `7 \| 30 \| 90 \| 365`. How far back, default 30. |

### What comes back

```
{
  "keyword": "trail running shoes",
  "history": [
    {
      "date": "2026-11-06",
      "position": 4,
      "url": "https://alpinerun.example/shoes/trail"
    },
    {
      "date": "2026-11-07",
      "position": 7,
      "url": "https://alpinerun.example/shoes/trail"
    }
  ]
}
```

### get_changes

Read only

The biggest winners and losers of a project over 1, 7 or 30 days, up to 20 on each side. Change is places gained, so negative means lost. A keyword that left the top 30 is a loser with a null position and dropped_from saying where it was. One that entered the top 30 has no earlier number to compare, so it is not listed.

**When** When the person asks what moved.

**Cost** Not metered.

Inputs of get_changes

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `period` | `1d \| 7d \| 30d`. Compare with 1 day, 7 days (default) or 30 days ago. |

### What comes back

```
{
  "period": "7d",
  "winners": [
    {
      "keyword": "running shoes for wide feet",
      "position": 6,
      "change": 2
    }
  ],
  "losers": [
    {
      "keyword": "trail running shoes",
      "position": 7,
      "change": -3
    },
    {
      "keyword": "ultramarathon training plan",
      "position": null,
      "change": null,
      "dropped_from": 28
    }
  ]
}
```

### explain_change

Read only

Compares a keyword on two dates: its position, the stored top results and which sites entered or dropped out. It returns a difference, never a cause. By default it compares the latest check with the last check at a different position. Top results are kept 90 days; older dates give the position only.

**When** When the person asks what changed around a keyword.

**Cost** Not metered.

Inputs of explain_change

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `keyword_id` required | `integer`. Keyword id from get_rankings. |
| `from_date` | `string`. Earlier date, YYYY-MM-DD. Must be a day the keyword was checked. |
| `to_date` | `string`. Later date, YYYY-MM-DD. Defaults to the latest check. |
| `depth` | `integer`. How many top results to list per date, 1-30, default 10. |

### What comes back

```
{
  "keyword": "trail running shoes",
  "summary": "\"trail running shoes\" moved down 3 places: 4 on 2026-11-06 to 7 on 2026-11-07.",
  "change": -3,
  "from": {
    "date": "2026-11-06",
    "position": 4,
    "url": "https://alpinerun.example/shoes/trail",
    "top": [
      {
        "position": 1,
        "domain": "ridgeoutfitters.example",
        "title": "Trail running shoes | ridgeoutfitters.example",
        "url": "https://ridgeoutfitters.example/trail-running-shoes"
      }
    ]
  },
  "to": {
    "date": "2026-11-07",
    "position": 7,
    "url": "https://alpinerun.example/shoes/trail",
    "top": [
      {
        "position": 1,
        "domain": "ridgeoutfitters.example",
        "title": "Trail running shoes | ridgeoutfitters.example",
        "url": "https://ridgeoutfitters.example/trail-running-shoes"
      }
    ]
  },
  "entered": [
    "gearlab-reviews.example"
  ],
  "dropped": [
    "fleetfootmag.example"
  ],
  "note": null
}
```

## Projects

### create_project

Changes data

Starts tracking a website in one location, language and device. It starts with one alert rule, moved_down 5, which update_alert or delete_alert can change.

**When** When the person wants a new site tracked. Call search_locations first.

**Cost** Counts against the plan's project limit. Needs a plan or a running trial.

Inputs of create_project

| Input | Type and meaning |
| --- | --- |
| `domain` required | `string`. Bare domain such as example.com (no https://). |
| `location_code` required | `integer`. From search_locations. |
| `language_code` | `string`. Default en. |
| `device` | `desktop \| mobile`. Default desktop. |
| `name` | `string` |

### What comes back

```
{
  "project": {
    "id": 5,
    "domain": "summitbikes.example",
    "device": "mobile",
    "location_code": 1023191
  }
}
```

### update_project

Destructive

Changes a project's name, domain, location, language or device. Changing location, language or device starts a new history for every keyword.

**When** When the person changes a project. Confirm a new location, language or device first. The tool says so.

**Cost** Needs a plan or a running trial.

Inputs of update_project

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `name` | `string` |
| `domain` | `string`. Bare domain such as example.com. |
| `location_code` | `integer`. From search_locations. |
| `language_code` | `string`. From list_languages. |
| `device` | `desktop \| mobile` |

### What comes back

```
{
  "project": {
    "id": 5,
    "name": "Summit Bikes",
    "domain": "summitbikes.example",
    "location_code": 1023191,
    "language": "en",
    "device": "mobile"
  }
}
```

### set_project_icon

Changes data

Gives a project its own icon, from a public image address or base64: PNG, JPEG, WebP, GIF or ICO, up to 2 MB.

**When** When the person wants a project's tile to show its own icon.

**Cost** Needs a plan or a running trial.

Inputs of set_project_icon

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `image_url` | `string`. Public https:// address of the image. |
| `image_base64` | `string`. The image itself as base64, instead of image_url. |

### What comes back

```
{
  "project_id": 5,
  "icon": {
    "format": "png",
    "size_bytes": 237
  }
}
```

### remove_project_icon

Changes data

Removes a project's icon, so its tile goes back to the first letter of the domain.

**When** When the person wants the icon gone.

**Cost** Not metered.

Inputs of remove_project_icon

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "project_id": 5,
  "icon": null
}
```

### delete_project

Destructive

Deletes a project with all its keywords, history, competitors, alerts and report settings. It cannot be undone, and it needs the project's exact domain as confirm_domain.

**When** Only after confirming with the person. The tool says so.

**Cost** Not metered.

Inputs of delete_project

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `confirm_domain` required | `string`. The project's exact domain, as a safety check. |

### What comes back

```
{
  "deleted": {
    "id": 5,
    "domain": "summitbikes.example"
  }
}
```

## Keywords

### add_keywords

Changes data

Starts tracking keywords in a project, up to 500 in a call, optionally in lists. Duplicates are skipped. Read the first positions with get_rankings once the first check is back.

**When** When the person gives keywords to track.

**Cost** Counts against the plan's keyword limit. Needs a plan or a running trial.

Inputs of add_keywords

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `keywords` required | `array of string`. Up to 500 keywords. |
| `lists` | `array of string`. List (tag) names to put them in. |

### What comes back

```
{
  "added": 4,
  "skipped": 0,
  "queued": 4
}
```

### refresh_keywords

Changes data

Checks keywords now, by ids or by list, instead of waiting for the next daily check. Most fresh positions arrive within two minutes; a late one can take up to 6 hours, and a check that fails is given back. A keyword is fresh once its last_checked in get_rankings changes.

**When** After a page changes, or when the person asks for a new position today.

**Cost** One check per keyword within the monthly and hourly limits; a call past either is rejected whole. A search checked in the last hour reuses free. Needs a plan or a running trial.

Inputs of refresh_keywords

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `keyword_ids` | `array of integer`. Up to 1000 ids from get_rankings. |
| `list` | `string`. Refresh every keyword in this list instead. |

### What comes back

```
{
  "checks_used": 0,
  "reused_free": 2,
  "checks_left_this_month": 1000,
  "checks_left_this_hour": 500,
  "note": "Most fresh positions arrive within two minutes; a late one can take up to 6 hours, and a check that fails is given back. A keyword is fresh once its last_checked in get_rankings changes."
}
```

### rename_keyword

Destructive

Changes a keyword's wording. Google is then searched for the new wording, so its position starts again and the old history no longer shows.

**When** Only after confirming with the person. The tool says so.

**Cost** Needs a plan or a running trial.

Inputs of rename_keyword

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `keyword_id` required | `integer`. From get_rankings. |
| `term` required | `string`. The new wording. |

### What comes back

```
{
  "keyword": {
    "id": 38,
    "keyword": "gravel bikes",
    "position": null
  }
}
```

### star_keywords

Changes data

Marks keywords as starred, or takes the star off, up to 500 ids.

**When** To keep the keywords that matter easy to filter in get_rankings.

**Cost** Not metered.

Inputs of star_keywords

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `keyword_ids` required | `array of integer`. From get_rankings. |
| `starred` required | `boolean`. True to star, false to take the star off. |

### What comes back

```
{
  "changed": 2
}
```

### remove_keywords

Destructive

Stops tracking keywords and deletes their history. It needs the exact ids, 100 at most, and there is no way to remove all.

**When** Only after confirming with the person. The tool says so.

**Cost** Not metered. Frees keyword allowance.

Inputs of remove_keywords

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `keyword_ids` required | `array of integer`. Exact ids to remove (max 100). |

### What comes back

```
{
  "removed": 1
}
```

## Sort keywords into lists

### list_tags

Read only

A project's keyword lists, with how many keywords each holds.

**When** Before filtering or refreshing by list.

**Cost** Not metered.

Inputs of list_tags

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "lists": [
    {
      "id": 3,
      "name": "gear",
      "keywords": 7
    },
    {
      "id": 4,
      "name": "guides",
      "keywords": 3
    }
  ]
}
```

### create_tag

Changes data

Creates an empty keyword list in a project.

**When** Before sorting keywords into a new list.

**Cost** Not metered.

Inputs of create_tag

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `name` required | `string`. List name, up to 64 characters. |

### What comes back

```
{
  "list": {
    "id": 11,
    "name": "autumn"
  }
}
```

### rename_tag

Changes data

Renames a keyword list. Its keywords stay in it.

**When** When the person renames a list.

**Cost** Not metered.

Inputs of rename_tag

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `tag_id` required | `integer`. From list_tags. |
| `name` required | `string` |

### What comes back

```
{
  "list": {
    "id": 11,
    "name": "autumn sale"
  }
}
```

### tag_keywords

Changes data

Puts keywords in a list, up to 500 ids. The list is created if it does not exist.

**When** To group keywords for filtering, refreshing or alerts.

**Cost** Not metered.

Inputs of tag_keywords

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `list` required | `string`. List (tag) name. |
| `keyword_ids` required | `array of integer`. Up to 500 ids. |

### What comes back

```
{
  "keywords_affected": 2
}
```

### untag_keywords

Changes data

Takes keywords out of a list, up to 500 ids. The keywords stay tracked.

**When** When keywords no longer belong in a list.

**Cost** Not metered.

Inputs of untag_keywords

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `list` required | `string`. List (tag) name. |
| `keyword_ids` required | `array of integer`. Up to 500 ids. |

### What comes back

```
{
  "keywords_affected": 1
}
```

### delete_tag

Destructive

Deletes a keyword list. The keywords in it are not deleted.

**When** Only after confirming with the person. The tool says so.

**Cost** Not metered.

Inputs of delete_tag

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `tag_id` required | `integer`. From list_tags. |

### What comes back

```
{
  "deleted_list": "autumn sale",
  "keywords_deleted": 0
}
```

## Competitors

### list_competitors

Read only

The competitor websites a project watches, with their ids and how many the plan allows.

**When** Before adding or removing a competitor.

**Cost** Not metered.

Inputs of list_competitors

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "competitors": [
    {
      "id": 2,
      "domain": "runnersdepot.example"
    },
    {
      "id": 3,
      "domain": "summitgear.example"
    },
    {
      "id": 1,
      "domain": "trailhouse.example"
    }
  ],
  "limit_per_project": 3
}
```

### add_competitor

Changes data

Watches a competitor's website in a project. Its positions come from the daily checks already run for your keywords.

**When** When the person names a rival to watch.

**Cost** Uses no refresh checks. Counts against the plan's competitors per project. Needs a plan or a running trial.

Inputs of add_competitor

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `domain` required | `string`. Bare domain such as rival.com. |

### What comes back

```
{
  "competitor": {
    "id": 9,
    "domain": "rivalbikes.example"
  }
}
```

### get_competitor_rankings

Read only

Keyword by keyword: your position beside each competitor's latest position, and whether Google shows an AI Overview and cites you in it. A position of null means not in the top 30.

**When** When the person asks who ranks above them.

**Cost** Not metered.

Inputs of get_competitor_rankings

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `limit` | `integer`. Rows per page, default 50, max 100. |
| `page` | `integer` |

### What comes back

```
{
  "keywords": [
    {
      "id": 3,
      "keyword": "best running socks",
      "you": 1,
      "competitors": {
        "runnersdepot.example": 4,
        "summitgear.example": 12,
        "trailhouse.example": 3
      },
      "ai_overview": true,
      "ai_cited": true
    }
  ],
  "page": 1,
  "pages": 18,
  "total": 18
}
```

### get_visibility

Read only

Share of visibility for you and each competitor, per day and now, over 7 to 90 days. It is an estimate from typical click rates by position, and it measures no visits.

**When** When the person asks how they compare overall.

**Cost** Not metered.

Inputs of get_visibility

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `days` | `integer`. Days covered, 7 to 90. Default 30. |

### What comes back

```
{
  "domains": [
    {
      "domain": "alpinerun.example",
      "is_you": true,
      "competitor_id": null,
      "score": 15.5,
      "change": 0
    },
    {
      "domain": "runnersdepot.example",
      "is_you": false,
      "competitor_id": 2,
      "score": 14.8,
      "change": -0.8
    }
  ],
  "series": [
    {
      "date": "2026-11-07",
      "scores": {
        "alpinerun.example": 15.5,
        "runnersdepot.example": 14.8
      }
    }
  ]
}
```

### remove_competitor

Destructive

Stops watching a competitor and deletes its stored positions.

**When** Only after confirming with the person. The tool says so.

**Cost** Not metered. Frees a competitor place.

Inputs of remove_competitor

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `competitor_id` required | `integer`. From list_competitors. |

### What comes back

```
{
  "removed": {
    "id": 9,
    "domain": "rivalbikes.example"
  }
}
```

## Alerts

### create_alert

Changes data

Sets a rule. When a keyword crosses it, the account owner gets an email summary, at most one a day per project; a move checked after that day's summary goes in the next one. A rule watches the whole project or one list. moved_up sees a keyword in the top 30 at both checks. moved_down also fires, whatever the threshold, for one that leaves the top 30. Asking for a rule the project already has returns that rule, switched on, with created false.

**When** When the person wants to hear about a drop or a gain.

**Cost** Not metered.

Inputs of create_alert

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `type` required | `moved_up \| moved_down \| entered_top3 \| left_top3 \| entered_top10 \| left_top10 \| dropped_out` |
| `threshold` | `integer`. Places moved, 1-29. Only for moved_up and moved_down. |
| `list` | `string`. Only watch keywords in this list. |

### What comes back

```
{
  "alert": {
    "id": 7,
    "rule": "Moved down 3+ places",
    "list": "Spring"
  },
  "created": true
}
```

### list_alerts

Read only

A project's alert rules, with their ids and whether each is on.

**When** Before adding, changing or deleting a rule.

**Cost** Not metered.

Inputs of list_alerts

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "alerts": [
    {
      "id": 1,
      "type": "moved_down",
      "threshold": 3,
      "enabled": true,
      "rule": "Moved down 3+ places",
      "list": null
    },
    {
      "id": 2,
      "type": "entered_top3",
      "threshold": null,
      "enabled": true,
      "rule": "Entered the top 3",
      "list": null
    }
  ]
}
```

### update_alert

Changes data

Turns an alert rule on or off, or changes how many places it watches for.

**When** When the person wants a rule paused or changed.

**Cost** Not metered.

Inputs of update_alert

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `alert_id` required | `integer`. From list_alerts. |
| `enabled` | `boolean` |
| `threshold` | `integer`. Places moved, 1-29. Only for moved_up and moved_down. |

### What comes back

```
{
  "alert": {
    "id": 7,
    "type": "moved_down",
    "threshold": 5,
    "enabled": true,
    "rule": "Moved down 5+ places",
    "list": "spring"
  }
}
```

### get_alert_history

Read only

The alerts a project has fired, newest first: date, keyword, the rule and the move, 50 to a page. A position of null means not in the top 30.

**When** When the person asks what fired.

**Cost** Not metered.

Inputs of get_alert_history

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `page` | `integer` |

### What comes back

```
{
  "events": [
    {
      "date": "2026-11-07T06:00:00+00:00",
      "keyword": "trail running shoes",
      "keyword_id": 1,
      "rule": "Moved down 2+ places",
      "from": 4,
      "to": 7
    }
  ],
  "page": 1,
  "pages": 1,
  "total": 9
}
```

### delete_alert

Destructive

Deletes an alert rule.

**When** Only after confirming with the person, or turn it off with update_alert instead. The tool says so.

**Cost** Not metered.

Inputs of delete_alert

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `alert_id` required | `integer`. From list_alerts. |

### What comes back

```
{
  "deleted": 7
}
```

### list_channels

Read only

The Slack channels and webhooks that receive a project's daily alert summary, with their status. The full addresses stay hidden, because they can hold a secret.

**When** Before adding or testing a channel.

**Cost** Not metered.

Inputs of list_channels

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "channels": [
    {
      "id": 1,
      "type": "slack",
      "host": "hooks.slack.com",
      "status": "active",
      "last_error": null,
      "last_success_at": "2026-11-07T09:00:00+00:00"
    },
    {
      "id": 2,
      "type": "webhook",
      "host": "hooks.larkfield.example",
      "status": "active",
      "last_error": null,
      "last_success_at": "2026-11-07T09:00:00+00:00"
    }
  ]
}
```

### add_channel

Changes data

Sends a project's daily alert summary to a Slack incoming webhook or to any public https address. A webhook's signing secret is shown once, in the reply.

**When** When the person wants alerts in Slack or in their own system.

**Cost** Needs a plan or a running trial.

Inputs of add_channel

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `type` required | `slack \| webhook` |
| `url` required | `string`. The Slack webhook address, or your own https:// address. |

### What comes back

```
{
  "channel": {
    "id": 3,
    "type": "slack",
    "host": "hooks.slack.com",
    "status": "active",
    "last_error": null,
    "last_success_at": null
  }
}
```

### test_channel

Changes data

Sends a test message to a Slack channel or webhook and says whether it arrived.

**When** After adding a channel.

**Cost** Needs a plan or a running trial.

Inputs of test_channel

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `channel_id` required | `integer`. From list_channels. |

### What comes back

```
{
  "ok": true,
  "error": null
}
```

### delete_channel

Destructive

Stops sending a project's alerts to a Slack channel or webhook.

**When** Only after confirming with the person. The tool says so.

**Cost** Not metered.

Inputs of delete_channel

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `channel_id` required | `integer`. From list_channels. |

### What comes back

```
{
  "deleted": 3
}
```

## Reports

### generate_report

Read only

Makes the branded PDF report of a project over 7 or 30 days, 30 by default: overview, winners and losers, trend and every keyword. It returns a download link that works for one hour without signing in.

**When** When the person needs a client report.

**Cost** Not metered.

Inputs of generate_report

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `days` | `7 \| 30`. Period covered, 7 or 30 days. Default 30. |

### What comes back

```
{
  "url": "https://api.theserpagent.ai/api/reports/1/download?days=30&expires=…&signature=…",
  "expires_in_minutes": 60,
  "filename": "report-alpinerun.example-2026-11-07.pdf"
}
```

### get_report_branding

Read only

What a project's PDF report carries: company name, colour, whether it has a logo and whether our name is hidden, plus where replies to its scheduled emails go.

**When** Before making or changing a client's report.

**Cost** Not metered.

Inputs of get_report_branding

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "company_name": "Larkfield SEO",
  "color": "#1d4ed8",
  "reply_to": "reports@larkfield.example",
  "hide_branding": true,
  "has_logo": true
}
```

### set_report_branding

Changes data

Puts a company name and colour on a project's PDF report, and chooses whether our name is hidden. The company name also goes on the From line of its scheduled emails, and replies go to reply_to; the sending address stays ours. Only the fields sent change, and null clears one.

**When** When the person wants the report under their own name.

**Cost** Not metered.

Inputs of set_report_branding

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `company_name` | `string or null`. Up to 80 characters. |
| `color` | `string or null`. Hex colour such as #0f766e. |
| `reply_to` | `string or null`. Where replies to scheduled report emails go. Without one, a reply reaches our address and not the agency. |
| `hide_branding` | `boolean`. True hides "Powered by TheSerpAgent". |

### What comes back

```
{
  "company_name": "Larkfield SEO",
  "color": "#1d4ed8",
  "reply_to": "reports@larkfield.example",
  "hide_branding": true,
  "has_logo": false
}
```

### set_report_logo

Changes data

Puts a logo on a project's PDF report, from a public image address or base64: PNG or JPEG, up to 512 KB.

**When** When the person gives a logo for the report.

**Cost** Not metered.

Inputs of set_report_logo

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `image_url` | `string`. Public https:// address of the image. |
| `image_base64` | `string`. The image itself as base64, instead of image_url. |

### What comes back

```
{
  "company_name": "Larkfield SEO",
  "color": "#1d4ed8",
  "hide_branding": true,
  "has_logo": true
}
```

### remove_report_logo

Changes data

Takes the logo off a project's PDF report.

**When** When the person wants the logo gone.

**Cost** Not metered.

Inputs of remove_report_logo

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "company_name": "Larkfield SEO",
  "color": "#1d4ed8",
  "hide_branding": true,
  "has_logo": false
}
```

### list_report_schedules

Read only

The PDF reports a project emails on a schedule: how often, to whom and when the next one goes out.

**When** Before adding or removing a schedule.

**Cost** Not metered.

Inputs of list_report_schedules

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "schedules": [
    {
      "id": 1,
      "frequency": "weekly",
      "recipients": [
        "marketing@alpinerun.example"
      ],
      "next_run_at": "2026-11-09T07:00:00+00:00"
    }
  ]
}
```

### create_report_schedule

Changes data

Emails a project's PDF report daily, weekly or monthly to up to 5 people, at 07:00 UTC. A project can hold up to 5 schedules.

**When** When the person wants a report sent on its own. It writes to the addresses given, so confirm them first. The tool says so.

**Cost** Needs a plan or a running trial.

Inputs of create_report_schedule

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `frequency` required | `daily \| weekly \| monthly` |
| `recipients` required | `array of string`. Up to 5 email addresses. |

### What comes back

```
{
  "schedule": {
    "id": 2,
    "frequency": "weekly",
    "recipients": [
      "marketing@summitbikes.example"
    ],
    "next_run_at": "2026-11-09T07:00:00+00:00"
  }
}
```

### delete_report_schedule

Destructive

Stops emailing a scheduled report.

**When** Only after confirming with the person. The tool says so.

**Cost** Not metered.

Inputs of delete_report_schedule

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |
| `schedule_id` required | `integer`. From list_report_schedules. |

### What comes back

```
{
  "deleted": 2
}
```

## Summaries

### get_summary

Read only

The latest plain-English summary of a project, and whether it is from today. Summaries are written automatically from the ranking figures, not by AI.

**When** Before writing a new one.

**Cost** Not metered.

Inputs of get_summary

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "summary": {
    "text": "17 of your 18 tracked keywords rank in the top 30, averaging position 8.47, worse than 8.35 before. 3 are in the top 3 and 11 in the top 10. Biggest gain: \"trail running poles\", up 1 to position 3. Biggest drop: \"trail running shoes\", down 3 to position 7. Your share of visibility is 15.5%; trailhouse.example leads with 17.1%. Google shows an AI Overview for 9 of your keywords and cites you in 4.",
    "source": "template",
    "date": "2026-11-07"
  },
  "up_to_date": true
}
```

### write_summary

Changes data

Writes today's plain-English summary of a project, automatically from its ranking figures. No AI writes it on our side, so for a fuller write-up read get_overview and get_changes and write it yourself in the conversation; that text does not go into PDF reports. A summary already written today comes back as it is.

**When** When the person wants the state of a project in words.

**Cost** No allowance. Needs a plan or a running trial.

Inputs of write_summary

| Input | Type and meaning |
| --- | --- |
| `project_id` required | `integer`. Project id from list_projects. |

### What comes back

```
{
  "summary": {
    "text": "17 of your 18 tracked keywords rank in the top 30, averaging position 8.47, worse than 8.35 before. 3 are in the top 3 and 11 in the top 10. Biggest gain: \"trail running poles\", up 1 to position 3. Biggest drop: \"trail running shoes\", down 3 to position 7. Your share of visibility is 15.5%; trailhouse.example leads with 17.1%. Google shows an AI Overview for 9 of your keywords and cites you in 4.",
    "source": "template",
    "date": "2026-11-07"
  },
  "cached": false
}
```

## Where this list comes from

The names and inputs are read from the server's own tool list. Each answer is the tool's real output from a test copy of the server, on a fictional agency's data, with long lists shortened. The tool hints are the server's: `readOnlyHint` and `destructiveHint`.

Start here list_projects search_locations list_languages get_usage

Read the record get_overview get_rankings get_keyword_history get_changes explain_change

Projects create_project update_project set_project_icon remove_project_icon delete_project

Keywords add_keywords refresh_keywords rename_keyword star_keywords remove_keywords

Sort keywords into lists list_tags create_tag rename_tag tag_keywords untag_keywords delete_tag

Competitors list_competitors add_competitor get_competitor_rankings get_visibility remove_competitor

Alerts create_alert list_alerts update_alert get_alert_history delete_alert list_channels add_channel test_channel delete_channel

Reports generate_report get_report_branding set_report_branding set_report_logo remove_report_logo list_report_schedules create_report_schedule delete_report_schedule

Summaries get_summary write_summary

## Read next

- [Connect Claude Code: One command, then what to ask](https://theserpagent.ai/docs/connect-ai/claude)
- [Connect Cursor: One config block, then what to ask](https://theserpagent.ai/docs/connect-ai/cursor)
- [What your AI can do: The record, the tools, the costs and the limits](https://theserpagent.ai/ai-agents)

## Stop checking rankings one by one

Add your site and keywords, and we start checking them right away.

[Start free trial](https://app.theserpagent.ai/signup)

14 days free with a card on file. Nothing is charged until they end.
