# Methodology, anlyon.dev

Fixed rules over what a server says its tools are. The same input always gives the same result. No model writes a score. No finding is not a safety guarantee.

- **Deletes or cancels** (`destructive`): Removes, cancels or revokes something.
- **Can't be undone** (`irreversible`): Sends, pays, publishes or merges: once done, it cannot be taken back.
- **Changes things** (`write`): Creates or changes something that can be changed back.
- **Only reads** (`read`): Looks things up and changes nothing.

## The verb in the tool name

Check id: `class-name-verb`

The first known verb in the tool name sets its class: read words such as "get", "list" and "search" give read; change words such as "create", "update" and "add" give write; money words ("refund", "charge", "payout"), send words ("send", "notify", "reply") and final words ("merge", "publish", "transfer") give irreversible; removal words ("delete", "remove", "cancel", "archive", "revoke") give destructive. "post" followed by "message" or "email" is a send. In a name that is not a read, a money or send word right after a plain verb ("create_refund") and a second verb after "and" also count. A money word followed by "method", "link" or "setting" is configuration, not a payment.

## The first verb of the description

Check id: `class-description-verb`

The first verb of the description's first sentence, after words like "Use this tool to", is read with the same word lists. It sets the class when the name has no known verb, and otherwise can only raise it.

## Wording that says a change is final

Check id: `class-final-wording`

A description that says "cannot be undone", "irreversible" or "permanently delete" raises the class to at least irreversible.

## An amount and a currency in the input

Check id: `class-money-input`

An input field named amount with a currency field beside it, at the top level or one object down (amount_money.amount), raises a tool that is not a read to irreversible, because it moves money. Fields such as unit_amount on a price or amount_off on a coupon do not count: they define a number for later.

## Recipients in the input

Check id: `class-recipient-input`

An input field named "to" or "recipients", at the top level or one object down, raises a tool that is not a read to irreversible, because what it sends cannot be taken back.

## A statement or command in the input

Check id: `class-code-input`

A top-level string input named "sql", "statement", "command", "cmd", "script", "code", "javascript" or "shell" raises a read to write, because what it runs can change anything it can reach.

## No known verb

Check id: `class-default-write`

A tool whose name and description name no known verb is counted as a write. A read has to be named, because a read is allowed by default in the permissions block.

## Annotations can only raise a class

Check id: `class-annotations`

MCP annotations are the server's own hints. destructiveHint: true raises the class to destructive, and readOnlyHint: false raises a read to a write. readOnlyHint: true and destructiveHint: false never lower a class: where they disagree with the tool's own words, the scan reports an annotation conflict instead.

## Hidden Unicode

Check id: `hidden-unicode`

Characters a person cannot see but a model reads: zero-width characters (U+200B to U+200D, U+2060 to U+2064, U+FEFF, U+180E), bidirectional controls (U+200E, U+200F, U+061C, U+202A to U+202E, U+2066 to U+2069), and tag characters (U+E0000 to U+E007F), which can spell out ASCII text. Checked in the name, titles, description and every string and property name in the input schema. Evidence shows each character as U+XXXX and, for tag characters, the text they spell.

## Text addressed to the model

Check id: `model-directed-text`

Phrases that address the model rather than describe the tool: tags such as "<IMPORTANT>" or "<SYSTEM>", "ignore previous instructions", "do not tell the user" and similar instructions to keep something from the user, and "before using this tool, read". These are the patterns of published tool-poisoning examples. Checked in the titles, description and input schema strings.

## References to secrets on disk

Check id: `sensitive-path`

Paths where keys and secrets usually live: ~/.ssh, id_rsa and other SSH key names, .env files, ~/.aws and AWS key names, credentials files and .git-credentials, MCP client configs (mcp.json, claude_desktop_config.json, ~/.cursor/), .netrc, .npmrc, .pypirc, .pgpass, .gnupg, Docker and Kubernetes configs, and /etc/passwd or /etc/shadow.

## The word "credentials"

Check id: `credentials-mention`

The word "credential" or "credentials" outside a path. Many tools mention credentials for ordinary reasons, so this is information only.

## Mentions of other tools or servers

Check id: `tool-shadowing`

A tool that names a tool this server does not list (for example "the send_email tool", "mcp_tool_send_email" or "mcp__other__send_email"), or uses wording such as "other servers", "all other tools" or "when this tool is available". A description that steers how the model uses another server's tools is how tool shadowing works. A name counts when it is written as an identifier (send_email, sendEmail) or in quotes, next to the word "tool". Mentions of the server's own tools are not reported.

## Base64 strings

Check id: `base64-blob`

A run of 40 or more base64 characters that mixes upper case, lower case and digits, or ends in = padding, outside a URL. When it decodes to readable text, the text is shown, and the finding is high when that text contains model-directed phrases.

## Long descriptions

Check id: `long-description`

A description over 2,000 characters. Long descriptions are not wrong in themselves, but they are where extra instructions are easiest to miss, so the length is shown.

## Annotations that disagree with the tool

Check id: `annotation-conflict`

Where the server's annotations disagree with its own definition: readOnlyHint: true on a tool whose name, description or input give a higher class (a tool named "delete_*", for example), destructiveHint: false on a tool whose words say it removes something, destructiveHint: true on a tool named as a read, readOnlyHint: false on a tool named as a read, and readOnlyHint and destructiveHint both true.

## Install scripts that reach the network

Check id: `install-script-network`

A "preinstall", "install" or "postinstall" script in the package.json that contains curl, wget, fetch or http. npm runs these scripts on install, before the server is started. Read from the published package manifest; the package is never run.

## Install scripts

Check id: `install-script`

A "preinstall", "install" or "postinstall" script in the package.json that does not reach the network by these patterns. Information only: npm runs it on install.

## Duplicate tool names

Check id: `duplicate-tool-name`

Two or more tools with the same name in one tools/list. A client keeps one of them, and which one depends on the client.

## The definition hash

Check id: `definition-hash`

Each scan records the sha256 of the server's normalised tool definitions (name, title, description, input schema and annotations, with keys sorted and tools sorted by name). A different hash means a tool definition changed, and the change is shown as a diff.

## The Claude Code permissions block

Check id: `claude-code-permissions`

A block for .claude/settings.json with one mcp__<server>__<tool> rule per tool: read tools in allow, write and irreversible tools in ask, destructive tools in deny. It is built from the same classes as the scan, as a starting point to review.

## What a scan is and is not

Check id: `scope`

A scan reads tool definitions only: the tools/list a server publishes and its package manifest. It never runs the package, calls a tool or signs in. Every finding is evidence with a place and an excerpt, found by fixed rules with no model in the loop, and the same definitions always give the same result. No finding is not a safety guarantee: a check only sees the patterns it looks for, and a scan is a point in time.
