> ## Documentation Index
> Fetch the complete documentation index at: https://docs.periphery.exposed/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools and prompts

> Every tool and prompt the Periphery MCP server offers, what each one returns, and the errors your assistant can receive.

The [MCP server](https://docs.periphery.exposed/mcp/overview.md) offers five tools and three prompts. Every tool only reads: nothing your assistant does through it changes anything in Periphery. Results are JSON, sent both as text and as structured content, so any client can use them.

## Things to ask

You don't call the tools yourself; your assistant picks them. A few requests that work well:

* "What has Periphery indexed for acme.com? Group the apps by whether they look like ours, a partner's, impersonation, or unrelated."
* "Which apps tied to acme.com expose a verified credential? Rank them by severity and tell me what to rotate first."
* "Find copies of acme-login.vercel.app by its favicon, and tell me which ones look like phishing."
* "What appeared for acme.com this week?"
* "How many searches do I have left this month?"

## search\_apps

Runs a query and returns one page of up to 50 hits. Each call uses one search from your monthly allowance.

| Input | Type | Description |
| - | - | - |
| `query` | string, required | The query in the console's [search syntax](https://docs.periphery.exposed/search/query-syntax.md): domains, free text and [filters](https://docs.periphery.exposed/search/filters.md). Up to 256 characters and 10 terms. Values separated by commas match any of them, except in `domain:` and `has:`, where they must all match. |
| `page` | integer | The page, from 1 (the default) to 20,000. Each page is another search. |

```json theme={null}
{
  "query": "acme.com has:creds",
  "interpretedAs": "has:creds domain:acme.com",
  "page": 1,
  "total": 1,
  "hasMore": false,
  "hits": [
    {
      "hostname": "acme-dashboard.vercel.app",
      "title": "Acme dashboard",
      "status": 200,
      "provider": "vercel",
      "firstSeen": "2026-08-02T10:15:40Z",
      "lastSeen": "2026-09-20T14:03:11Z",
      "faviconHash": -247388890,
      "emails": ["ops@acme.com"],
      "verifiedDetectors": ["Stripe"]
    }
  ],
  "searchesLeft": 2958,
  "consoleUrl": "https://console.periphery.exposed/search?q=acme.com+has%3Acreds"
}
```

* `interpretedAs` is the query as Periphery read it, when that differs from what was sent: a bare domain runs as `domain:`.
* `total` counts every match across pages and stops at 1,000,000, where `totalCapped` is `true`.
* A hit lists up to 10 email addresses; `emailCount` gives the total when there are more, and [`get_app`](#get_app) has them all.
* When the query matched the page's text (free text, or a domain the page mentions), each hit carries a `snippet` of that text, with the matched words in `**bold**` and secrets masked as in [`get_app`](#get_app). A matched word with something masked in it isn't bold.
* If Periphery can't read an app's credentials at that moment, a hit with findings comes without its title and snippet rather than with them unmasked.
* `searchesLeft` is omitted when your plan has no monthly limit.

A query that is empty or too long, or a page past 20,000, is refused before it runs and uses no search. So is a call your client gives up on while it waits for the account's turn: nothing is counted for it. A page past the end of the results is not refused: it returns no hits and uses a search, so your assistant stops when `hasMore` is `false`.

## get\_app

Returns the full record of one app. It uses no search.

| Input | Type | Description |
| - | - | - |
| `hostname` | string, required | The app's hostname, as `search_apps` returned it. |

The record has the same fields as [`GET /v1/apps/{hostname}`](https://docs.periphery.exposed/api-reference/apps/get-an-app), plus the console link, without the favicon URL and without the page-source link:

```json theme={null}
{
  "hostname": "acme-dashboard.vercel.app",
  "title": "Acme dashboard",
  "status": 200,
  "provider": "vercel",
  "firstSeen": "2026-08-02T10:15:40Z",
  "lastSeen": "2026-09-20T14:03:11Z",
  "faviconHash": -247388890,
  "text": "Acme dashboard Sign in Forgot your password? Contact ops@acme.com",
  "hasVerifiedCreds": true,
  "emailCount": 1,
  "emails": ["ops@acme.com"],
  "verifiedDetectors": ["Stripe"],
  "credentials": [
    {"detector": "Stripe", "verified": true, "masked": "sk_live_••••••••0000"}
  ],
  "screenshotUrl": "https://s3.gra.io.cloud.ovh.net/periphery/screenshots/acme-dashboard.vercel.app.png?X-Amz-Signature=…",
  "consoleUrl": "https://console.periphery.exposed/app/acme-dashboard.vercel.app"
}
```

`text` is the visible text of the last snapshot, up to 2 KB. `credentials` lists every finding, verified or not, masked as on the [app page](https://docs.periphery.exposed/apps/emails-and-credentials.md). The screenshot link is signed and valid for one hour.

Over MCP, what your assistant reads goes to its AI provider, so the text of a page, its title and search snippets included, is masked before it leaves:

* every credential Periphery found on the page, wherever it shows: printed across lines, cut off where the stored text or a snippet ends, or with invisible characters such as zero-width spaces inside it;
* a value of 8 or more characters labelled as a secret, such as `API_KEY=…`, `API Key: …`, `"password": "…"`, `Bearer …` or `Authorization: Basic …`; a quoted value counts up to its closing quote, spaces included, or to the end of the text when the quote was cut off;
* any other long string that looks random rather than like a word or a number, such as the secret half of an AWS key pair, whose finding holds only the key ID, even when it's printed right against that ID.

A masked value keeps a few characters at each end, like `sk_live_••••••••0000` in the record above, or none when only a piece of it was there or it holds spaces. Lower-case hostnames of three labels or more stay readable, deployment hashes in them included, such as `dashboard-6f3b9a2c.vercel.app`; a random-looking label in a hostname written in capitals, with fewer labels, or cut off can be masked. A title is cut to 512 characters. There is no link to the raw page source, which can hold keys in full, and no screenshot of a page that prints a credential Periphery found (see [`get_app_screenshot`](#get_app_screenshot)). Text the console shows you is not masked. A secret in a form no detector knows and that doesn't look random, a short random key such as a 16-character authenticator seed, or a labelled one shorter than 8 characters can still be in the text.

## get\_app\_screenshot

Returns the screenshot of an app's last snapshot, 1280 × 800, as an image the assistant can look at: useful to judge what an app is, whose branding it carries, or whether it imitates a login page. It uses no search.

| Input | Type | Description |
| - | - | - |
| `hostname` | string, required | The app's hostname, as `search_apps` returned it. |

The image comes with a one-line caption: the hostname, the snapshot time and the console link.

The screenshot shows the page as any visitor sees it, and an image goes to your assistant's AI provider like text does. So when the page's text prints a credential Periphery found, the tool sends no image, and `get_app` no `screenshotUrl`: the call returns an error naming the record page, where you can see the screenshot yourself. A key Periphery didn't find can still show in a screenshot.

## get\_account

Shows the connected account: its plan, whether the plan includes MCP, the searches used and left this month, when the allowance resets (`resetsAt`, the 1st of next month, UTC) and the rate limit. It works on every plan, so on Free your assistant can explain why the other tools refuse.

## list\_key\_types

Lists the credential detectors with verified findings in the index, the most common first, each with the filter to use, such as `creds:openai`, so your assistant can filter by detector without spending a search on a misspelled name. It uses no search.

## Prompts

Clients that support MCP prompts, such as Claude, list these as ready-made requests. Each fills in a short investigation plan, which your assistant then follows with the tools.

| Prompt | Argument | What it asks for |
| - | - | - |
| Investigate a domain | `domain` | Find every app tied to the domain, classify each one (run by the organisation, third party, impersonation, unrelated) with evidence and confidence, and end with a table of actions. |
| Triage exposed credentials | `domain` | Find the apps tied to the domain with verified credentials, rank them by severity, and order the response: rotate at the issuer, remove from the deployment, review the issuer's logs. |
| Find clones of an app | `hostname` | Search the app's favicon hash, compare the candidates on independent signals, and report the suspicious ones first. |

They follow the [response playbooks](https://docs.periphery.exposed/workflows/respond-to-findings.md): a match is a lead, not proof, and a surfaced credential is never used or tested.

## Errors

Errors come back as tool results your assistant can read and act on, rather than as failed calls:

| Message | Cause | What happens next |
| - | - | - |
| Periphery's MCP server is part of the Plus plan… | The account is on Free. | Upgrade, then ask again. `get_account` still works. |
| The monthly search limit is reached… | The month's searches are used up. | `get_app` and `get_app_screenshot` still work; searches return on the 1st, UTC. |
| query too long: up to 10 terms… | The query has more than 10 terms. | The assistant shortens it; no search was used. |
| validating "arguments": … | The arguments break the tool's input schema: a query over 256 characters, a page below 1 or past 20,000, an unknown argument. The client usually catches these before sending. | The assistant fixes the call; no search was used. |
| No live app has the hostname… | The hostname was never indexed, has no completed snapshot yet, or no longer exists. | The assistant checks the hostname against its search results. |
| rate limit: this account makes one request per second… | More than eight calls of one account waiting at once, or one that waited more than a few seconds. | Wait a few seconds, then try again. |
| Periphery is busy, try again in a moment | Too many searches are running across Periphery. | Retry after a moment. |
| Search took too long, narrow it with more words or a filter | The query ran past its time limit. | Narrow the query rather than retrying it as is. |

Some answers come at the HTTP level, before any tool runs:

| Status | Cause |
| - | - |
| `401` | The request has no valid token. This is how a client knows to start the sign-in. |
| `429` rate limit: too many requests for this account | The account sent more than 100 requests in a second. `Retry-After` says when to try again. Not every client retries on its own. |
| `429` rate limit: too many invalid tokens from this address | The address sent a flood of tokens that don't work. A token issued or used in the last two hours still gets through; others wait for `Retry-After`. |
| `503` | Periphery is unavailable. Try again shortly. |

JSON-RPC batches are not supported: send one request per call.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.