@shieldlabs-ai/mcp
OfficialClick on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@shieldlabs-ai/mcpCan you review the ShieldLabs request 02f1d973-4e5a-6b7c-8d9e-0f1a2b3c4d5e?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@shieldlabs-ai/mcp
MCP server that lets AI assistants read ShieldLabs identifications, search their history, explain Risk Scores and verify webhook signatures.
How it fits
Browser. The ShieldLabs agent runs an identification and hands your page a request ID.
Your backend. It sends the request ID with the protected action (signup, login, checkout) and reads the verdict from the History API, or receives it in a signed
identification.scoredwebhook.Decision. Your backend acts on the Risk Score, the three risk bands, the detection flags and the identifiers (for example, how many accounts one device has opened).
This server puts steps 2 and 3 in reach of an MCP client such as Claude Desktop, Claude Code, Grok, Cursor or VS Code: ask "review request 02f1d973-..." or "which accounts used this device?" and the assistant reads the answers from the History API. Every tool is read-only. New to ShieldLabs? Start free at app.shieldlabs.ai.
Related MCP server: inventory
Install
npx -y @shieldlabs-ai/mcpNode.js 20 or later. The server speaks stdio by default; MCP clients start it for you (see Quick
start). A container image is published as ghcr.io/shieldlabs-ai/shieldlabs-mcp.
Quick start
Copy the Private API Key (
sec_...) of your domain from the analytics dashboard at app.shieldlabs.ai. It reads the identifications of that one domain.Add the server to your client.
Claude Code
claude mcp add --transport stdio shieldlabs --env SHIELDLABS_API_KEY=sec_your_private_key -- npx -y @shieldlabs-ai/mcpGrok (the same local server;
~/.grok/config.tomlor.grok/config.toml)grok mcp add shieldlabs -e SHIELDLABS_API_KEY='${SHIELDLABS_API_KEY}' -- npx -y @shieldlabs-ai/mcp[mcp_servers.shieldlabs] command = "npx" args = ["-y", "@shieldlabs-ai/mcp"] env = { SHIELDLABS_API_KEY = "${SHIELDLABS_API_KEY}" }Claude Desktop (
claude_desktop_config.json), Cursor (.cursor/mcp.json) and other clients that use themcpServersformat:{ "mcpServers": { "shieldlabs": { "command": "npx", "args": ["-y", "@shieldlabs-ai/mcp"], "env": { "SHIELDLABS_API_KEY": "sec_your_private_key" } } } }VS Code (
.vscode/mcp.json), with the key requested at start-up:{ "inputs": [ { "type": "promptString", "id": "shieldlabs-api-key", "description": "ShieldLabs Private API Key", "password": true } ], "servers": { "shieldlabs": { "type": "stdio", "command": "npx", "args": ["-y", "@shieldlabs-ai/mcp"], "env": { "SHIELDLABS_API_KEY": "${input:shieldlabs-api-key}" } } } }Ask: "Review the ShieldLabs request
<request ID>", or run thereview_requestprompt.
More client setups, HTTP and Docker: examples/.
Hosted server (preview)
ShieldLabs also runs this server for you at https://mcp.shieldlabs.ai/mcp (streamable HTTP).
There is nothing to install: add the URL to your MCP client and sign in with your ShieldLabs
account when the client opens the sign-in page. The client then holds a short-lived access token
(one hour) and refreshes it by itself.
In this release the hosted server connects your account and offers one tool,
shieldlabs_check_connection, which confirms that the connection works. The tools that read
identifications arrive on the hosted server in a later release; until then, use the local server
above. Sign-in is the only way in for now: the hosted server does not accept API keys yet.
Claude: Customize > Connectors > Add custom connector, with the URL
https://mcp.shieldlabs.ai/mcp.
Claude Code: add the server, then run /mcp and choose Authenticate:
claude mcp add --transport http shieldlabs https://mcp.shieldlabs.ai/mcpCursor (.cursor/mcp.json) and VS Code (.vscode/mcp.json):
{ "mcpServers": { "shieldlabs": { "url": "https://mcp.shieldlabs.ai/mcp" } } }{ "servers": { "shieldlabs": { "type": "http", "url": "https://mcp.shieldlabs.ai/mcp" } } }See examples/hosted for client files. To run the hosted mode yourself, see
Deploy the hosted server.
Guide
Review one request
Give the assistant the request ID your backend received with a signup, login or payment.
shieldlabs_get_identification reads the verdict, and shieldlabs_explain_risk_score explains the
band, each weighted risk signal and the detection flags in plain language. The review_request
prompt runs these steps and ends with a suggested action (allow, step-up challenge, manual review
or block) and the reasons.
Scoring is asynchronous: the History row appears about 1 to 3 seconds after the browser call and
can be refined for up to about 10 seconds while follow-up checks finish.
shieldlabs_get_identification waits for a brand-new identification (up to about 10 seconds in
total) and returns the first version of the row it finds, so a verdict read in those first seconds
can still be refined: read it again (wait: false is enough) once observed_at is about 10
seconds in the past for the final state. Integrations that start the identification when the user
begins the action (for example on the first interaction with the form) find the row stored by the
time the form arrives.
A request ID that is not found means the identification is unverified, never clean: it may be too new, it may come from another domain (a Private API Key reads one domain), or it was issued while the visitor IP was over the ingest rate limit (such request IDs are never stored).
Investigate an account, a device or an IP address
shieldlabs_summarize_entity aggregates the recent identifications of one User HID, device ID,
visitor ID, cookie ID or IPv4 address: distinct devices, accounts, visitors, countries, IP
addresses and connection types (with the most frequent values), the highest and average Risk
Score, the worst band, rate-limit markers, and the detection flags and risk signals seen, with
counts. The result is labelled as computed from the returned history window, not as a ShieldLabs
verdict. It reads the 100 most recent identifications by default (max_items, at most 500; each
100 cost one History API request). shieldlabs_search_history lists the individual
identifications with paging.
Typical questions:
Question | Lookup |
How many accounts used this device? |
|
How many devices does this account use, and from which countries? |
|
Did this IP address send automated traffic or hit the rate limit? |
|
What happened, in order, on this account? |
|
The investigate_user prompt chains these lookups for one User HID. Account counts leave out the
User HID values that do not name an account: null, "anonymous", "fail", "-1" and
"unknown".
User HIDs travel as one URL path segment, so a User HID that contains / cannot be searched: the
tools refuse it with an explanation instead of returning an empty page (as they refuse . and
..). User HIDs made of URL-safe characters, such as the hex output of userHid() in the server
SDKs, avoid this.
Debug a webhook endpoint
shieldlabs_verify_webhook_signature checks a received delivery: the raw body, the
X-Shield-Signature header and the endpoint secret (or SHIELDLABS_WEBHOOK_SECRET). When the
signature does not match, it tests the usual mistakes (a re-serialized or pretty-printed body,
decoded & escapes, an added newline) and tells you which one explains the mismatch. It never
returns a secret or an expected signature.
While you rotate an endpoint's secret, give both secrets separated by commas, in the argument or in
SHIELDLABS_WEBHOOK_SECRET (for example whsec_new_secret,whsec_old_secret). The delivery is
valid when any of them matches, and matched_secret says which one did.
ShieldLabs sends one delivery per identification today, with a 1-second timeout and no retries.
Later releases add retries that resend identical bytes, so make the handler idempotent on
data.request_id and answer 2xx within 1 second. Use the History API for guaranteed reads and for
the latest state: a History row can be refined after the webhook was sent, and the webhook is not
sent again.
Plan an integration
The integrate_shieldlabs prompt loads the ShieldLabs setup skill and walks the assistant through
the browser identification, the server-side verdict, webhooks and a verification checklist. On
stdio it fetches the skill from the shieldlabs-skills release this server version was tested with
(tag v1.0.0, never a branch), with a 3-second timeout and a 200 KB limit. It uses its built-in
guide when that fails, with --offline, and always over HTTP. A remote or cached guide that
mentions unavailable tools also falls back to the built-in guide, which uses the server SDK for
verification. When exposed, shieldlabs_get_identification is offered as an additional check.
Run over HTTP
SHIELDLABS_API_KEY=sec_your_private_key SHIELDLABS_MCP_TOKEN=your_long_random_token \
npx -y @shieldlabs-ai/mcp --transport http --port 8787The endpoint is http://127.0.0.1:8787/mcp (streamable HTTP, stateless, JSON responses). Every
request needs Authorization: Bearer <SHIELDLABS_MCP_TOKEN>; when the variable is unset, the
server generates a token and prints it once to stderr. GET /health answers {"status":"ok"}
without authentication. Over HTTP the integrate_shieldlabs prompt always uses its built-in guide.
See examples/http.
Limit the tools
--tools exposes only the tools you list, with or without the shieldlabs_ prefix:
npx -y @shieldlabs-ai/mcp --tools get_identification,explain_risk_scoreThe identification resource follows shieldlabs_get_identification, and the prompts only mention
tools that are exposed.
Reference
Configuration
Variable | Required | Purpose |
| For the History API tools | Private API Key ( |
| No | Secret Key of the domain, for the Management API |
| No | Registered domain for the Management API, for example |
| No | Default endpoint signing secret ( |
| No | History API origin, default |
| No | Management API origin, default |
| For | Bearer token for the HTTP endpoint; generated and printed once when unset |
Flag | Default | Purpose |
|
| Transport |
|
|
|
|
| HTTP port |
|
| HTTP bind address |
| none | Browser origins allowed to call the HTTP endpoint; requests with any other |
| all available | Allowlist of tools |
| off (on with | Never fetch remote content (the setup prompt uses its built-in guide) |
| off | With |
|
Tools
Every tool is annotated readOnlyHint: true, destructiveHint: false, idempotentHint: true,
accepts response_format (markdown by default, or json) and returns structured content with an
output schema. Responses stay under 25,000 characters, in the text and in the structured content:
long pages are shortened with a notice and a next_offset to continue, and values longer than
1,000 characters as serialized in JSON (for example a landing URL reported by a browser) are cut
and listed in truncated_fields. Invisible and control characters in values (zero-width and
bidirectional characters, Unicode tag characters, variation selectors) are replaced by visible
\uXXXX or \u{XXXXX} escapes and listed in escaped_fields, so a value cannot hide text from
the person reading the output. A cut or escaped value differs from the stored one: do not search
by it.
Tool | Needs | Input | Returns |
| API key |
| The first stored version of the identification with its band, or an error that explains "not scored yet" |
| API key |
| Identifications newest first, |
| API key |
| Aggregates of the returned history window (see Guide) |
| Nothing (API key for |
| Band, weighted risk signals with meanings, detection flags, connection type, notes |
| Secret Key and domain | none | Remaining included identifications, masked keys, creation date (cached 60 s, one request shared by concurrent calls) |
| Nothing |
| Valid or not, checks, the event summary, and what to fix |
| Nothing |
| Current UTC time and the local time, for freshness checks |
Identifications follow the model of the ShieldLabs server SDKs (webhook field names) without the
raw payload, plus risk_band. signals[].description is always null: the History API's score
details are internal text, so this server returns the signal slug and weight, as webhooks do.
Resources
URI | Content |
| One identification as JSON (read once, without waiting) |
| JSON Schema of the normalized identification |
| JSON Schema of a webhook delivery, with the signature scheme |
| Catalog of risk signals, detection flags and connection types in plain language |
| The three bands and the 999 rate-limit marker |
Prompts
Prompt | Arguments | Purpose |
| none | Integration plan from the setup skill of the |
|
| Investigate one account: band, devices and other accounts on them, countries, flags, suggested action |
|
| Review one identification: verdict, risk signals, freshness, device and account history, suggested action |
Reading the results
Risk Score: integer 0-100. Bands: trusted 0-29, suspicious 30-59, dangerous 60-100. A value above 100 (sent as 999) is the rate-limit marker: the visitor IP was temporarily banned after too many identifications. It is not a score and not a band.
Risk signals are the weighted reasons behind a score. Weights can be negative (a late network check corrects an earlier one) and signal names are an open set. Branch on
risk_scoreanddetection_flags; never add up weights yourself.Identifiers:
device_idsurvives cleared cookies and private windows, and the all-zero device ID means no usable device signals.user_hidis"anonymous"for anonymous checks.A missing identification means unverified, never clean.
Security
Keys stay in the environment. The server never logs keys or secrets and never returns them, not even in error messages. Pass keys through the client's
envsettings, not command-line flags.Read-only. No tool changes anything in ShieldLabs. The Management API tool reads the profile only.
Profile keys. Only already-masked values (stars and at most four final characters) are retained. Unexpected values become
[redacted]before the response is shared or cached; raw profile bodies are discarded. Management error responses do not echo upstream bodies.Visitor data is data. User HIDs, landing URLs, referrers and UTM values in identifications come from visitors' browsers. Markdown output renders every API value as inert inline code, invisible and control characters are shown as visible escapes in every format, json responses carry an
untrusted_data_note, and the server instructions tell the assistant to treat these strings as data, never as instructions.A shared History API budget. The History API allows about 15 requests per second per domain, shared with your own backend, which polls it for new verdicts. This server sends at most 2 History API requests at a time and 5 per second for the whole process (every tool call and every HTTP client) and queues the rest; after a 429 every queued request waits 1 to 5 seconds (
Retry-After).Encrypted API calls. Base URL overrides must use https, except on loopback addresses.
HTTP transport: bearer token required on every request (constant-time comparison), requests with an unlisted
Originrefused,Hostchecked on loopback binds, 1 MB body limit, and a health endpoint without data. The default bind address is127.0.0.1. The server warns at start whenSHIELDLABS_MCP_TOKENhas fewer than 32 characters (useopenssl rand -hex 32).Webhook debugging reports whether a signature matches; it never returns the secret or a signature it computed.
Network access: the ShieldLabs APIs, plus one request to
raw.githubusercontent.comfor the setup skill of the pinnedshieldlabs-skillsrelease when theintegrate_shieldlabsprompt runs on stdio (skip it with--offline; never over HTTP). No telemetry.Hosted server (
--mode public): a 401 challenge before any JSON-RPC runs, and only ShieldLabs access tokens (slat_...) are accepted; any other credential is refused without being sent anywhere. The ShieldLabs API accepts such a token only on its MCP prefix and only with this server'sX-Shield-Gatewaysignature (HMAC-SHA256 over the time, a fresh nonce, the method, the path and the token hash), so a token copied out of a client is useless elsewhere. One client address can have at most 300 new tokens checked per minute. The only state shared between requests is the list of tokens accepted in the last 60 seconds, keyed by their SHA-256.
Errors and retries
Tool errors come back as MCP tool results with isError: true and a message that names the fix.
API calls use the retries of @shieldlabs-ai/node: connection errors, timeouts, 429 and 5xx are
retried with backoff; 400, 401, 402, 403 and 404 are not, and a Management API 429 is never
retried: after one, shieldlabs_get_domain_profile answers from memory until the 10-minute block
ends. History API requests wait in the shared budget described under Security. Cancelling a tool
call in the client stops its API requests, including the wait for a new verdict and requests still
in the queue.
The wait for a new verdict (shieldlabs_get_identification with wait: true, and
shieldlabs_explain_risk_score with a request_id) works on one budget of about 10 seconds:
It polls at once, then after 250 ms, 500 ms, 1 s, 1.5 s and then every 2 s. The last poll runs when the budget ends. Each poll is one request, without retries.
A 429, a 5xx, a connection error or a timeout does not end the wait. After a 429 the next poll waits the longest of the scheduled step, 1 second and
Retry-Aftercapped at 10 seconds, cut short at the end of the budget. WhenRetry-After, capped at 10 seconds, is longer than the time left, the tool reports the rate limit at once.When the budget ends, the tool reports the error of the last poll if it failed, and "not scored yet" otherwise.
400, 401, 403 and 404 end the wait at once: a wrong key or base URL does not heal.
Troubleshooting
Symptom | Fix |
Only the offline tools are listed (explain, verify, current time) |
|
"rejected the key (HTTP 401)" | Use the Private API Key ( |
"No identification ... is available yet" | The identification is new (retry in a few seconds), it belongs to another domain, or its request ID was issued while the visitor IP was over the ingest rate limit (never stored) |
"user_hid values that contain "/" cannot be searched" | The History API cannot match such a User HID. Look the account up by the |
"rate limit was reached (HTTP 429)" | The History API allows about 15 requests per second per domain, shared with your backend. Wait, then prefer |
Domain profile: 429 | The Management API allows about 15 calls per minute per IP and then blocks the IP for 10 minutes. Wait 10 minutes |
Domain profile: 401 |
|
"answered 404" |
|
"must be an https URL" at start | A base URL override uses plain http on a host other than |
"cannot be sent in an HTTP header" or "domain must be ASCII" at start | A key or the domain contains a line break, a space or a non-ASCII character. Copy the key again from the analytics dashboard; write an internationalized domain in its punycode form ( |
HTTP 401 on | Send |
Hosted server: HTTP 401 with | The sign-in expired or was revoked, or the client sent an API key, which the hosted server does not accept yet. Reconnect ShieldLabs in the client (in Claude: Customize > Connectors) |
Hosted server: HTTP 503 | ShieldLabs could not check the access token. Retry in a few seconds |
HTTP 403 on | The browser origin is not in |
Nothing happens on stdio | Logs go to stderr; stdout carries the protocol. Run |
Compatibility
Node.js 20, 22 and 24 (tested in CI).
Hosted mode: Cloudflare Workers (compatibility date 2026-08-15) or Node.js 20 or later; its Worker tests and deploys need Node.js 22 or later.
MCP TypeScript SDK 1.x: protocol revisions supported by the SDK, tools with output schemas, resources, resource templates and prompts. Transports: stdio and streamable HTTP.
History API and Management API as documented at docs.shieldlabs.ai; webhook schema version
2026-06-01.Semantic versioning: breaking changes only in a new major version.
Deploy the hosted server
The hosted mode (--mode public) serves many users from one server: every request brings the
access token of its user's sign-in, which the ShieldLabs account API checks before the server
answers. It runs as a Cloudflare Worker, or in a container with Node.js.
Path | Answer |
| MCP over streamable HTTP (stateless, JSON responses, one JSON-RPC message per request: a batch gets |
| Protected resource metadata (RFC 9728): the resource |
|
|
How a token is checked: the server sends GET /mcp/v1/ping to the account API with the token and
an X-Shield-Gateway: v1 kid=<key ID> t=<unix seconds> n=<nonce> sig=<signature> header. The
signature is the unpadded base64url HMAC-SHA256, with the secret of the gateway key, of v1, the
time, the nonce, the method, the path with its query and the lowercase hex SHA-256 of the token,
one per line. The account API accepts it for 60 seconds either way and refuses a nonce it has
seen, so every request gets 16 fresh random bytes. A 200 lets the request through and is
remembered for 60 seconds under the SHA-256 of the token; refusals are never remembered. The
shieldlabs_check_connection tool always pings again.
Variable | Default | Purpose |
| required | Origin that clients connect to, for example |
| required | Origin of the ShieldLabs account API that checks every token, for example |
|
| Authorization server named in the metadata |
| required, secret |
|
All URLs must use https, except on localhost, 127.0.0.1 and [::1]. The server never logs a
token, the gateway secret or a signature: every request writes one JSON line with the route, the
JSON-RPC method and tool name, the status, the duration, 8 hex characters of the token hash and
how the token was checked.
Cloudflare Workers
wrangler.jsonc holds two environments, each on its custom domain with the variables above and
three Rate Limiting bindings: RL_TOKEN (120 requests per minute per token), RL_ANON (60 per
minute per address for requests without an accepted token) and RL_TOKEN_CHECK (300 per minute
per address for tokens that are not in the cache, counted before the account API is asked). The
network 160.79.104.0/21 of Claude's hosted connectors, where many users share addresses, is not
limited per address.
Environment | Worker | Custom domain | Account API and issuer |
|
|
|
|
|
|
|
|
Set the gateway key once per environment, then deploy. Always name the environment; the top level
of wrangler.jsonc repeats dev, so a deploy without --env never touches production:
npx wrangler secret put MCP_GATEWAY_KEY --env dev
npx wrangler deploy --env dev
npx wrangler deploy --dry-run --env dev # bundle only, no Cloudflare account neededTo run the Worker on your machine against a local account API, put the overrides in .dev.vars
(ignored by git) and start wrangler dev:
cat > .dev.vars <<'EOF'
SHIELDLABS_PUBLIC_ORIGIN=http://localhost:8787
SHIELDLABS_PORTAL_URL=http://localhost:8090
SHIELDLABS_AUTH_ISSUER=http://localhost:8090
MCP_GATEWAY_KEY=k1:replace-with-the-k1-secret-of-your-local-api
EOF
npx wrangler dev --env dev --port 8787Container
The same handler runs on Node.js with --transport http --mode public, for example with the image:
docker run --rm -p 8787:8787 \
-e SHIELDLABS_PUBLIC_ORIGIN=https://mcp.example.com \
-e SHIELDLABS_PORTAL_URL=https://account.shieldlabs.ai \
-e MCP_GATEWAY_KEY \
ghcr.io/shieldlabs-ai/shieldlabs-mcp:<version> --transport http --mode public --host 0.0.0.0It speaks plain HTTP: put a TLS proxy in front of it, and let only that proxy reach the port. Rate limits are kept in memory per process, and request logs go to stdout, one JSON line per request.
The per-address limits use the address of each TCP connection, which behind a proxy is the
proxy's for every client. Add --trust-proxy to take the client address from CF-Connecting-IP
instead: the proxy must then set that header on every request, replacing any value the client
sent. Cloudflare sets it; with nginx, use proxy_set_header CF-Connecting-IP $remote_addr;.
Development
npm ci
npm run typecheck
npm run lint
npm test -- --coverage
npm run build
node dist/index.js --help
npm run smoke # the built server over stdio against the fake History API
npm run test:worker # the hosted mode in workerd (Node.js 22 or later)
npm run worker:check # the Worker bundle: no Node.js built-in module, size limit@shieldlabs-ai/node is bundled into dist/ at build time, so the published package depends only on
@modelcontextprotocol/sdk and zod. The published Node SDK is pinned as a development dependency;
npm ci installs it from the public registry using the lockfile. No sibling checkout, local SDK
tarball or registry token is needed. CI and release builds use the same dependency.
Fake History API
scripts/mock-history-api.mjs serves a deterministic dataset (test/mock-data/, generated by
scripts/generate-mock-data.mjs) as a fake History API and Management API:
npm run build
node scripts/mock-history-api.mjs --port 8788
# in another terminal: the MCP Inspector CLI passes environment variables with -e
npx @modelcontextprotocol/inspector --cli node dist/index.js \
-e SHIELDLABS_API_KEY=sec_evaldata-mockdata-00000001 \
-e SHIELDLABS_API_BASE_URL=http://127.0.0.1:8788 \
--method tools/listnpm run smoke does the same without extra downloads: it starts the fake API, runs the built
server over stdio with the MCP SDK client and calls its tools, resources and prompts.
See CONTRIBUTING.md. Documentation: docs.shieldlabs.ai. Support: contact@shieldlabs.ai.
License
Available Tools
6 toolsshieldlabs_current_timeCurrent timeARead-onlyIdempotent
Return the current time of the machine running this server, in UTC and optionally in one IANA time zone.
Use it to judge freshness: for example whether an identification's observed_at is older than a 5-minute window, or how long ago an account was last seen.
Args:
timezone (string, optional): IANA time zone such as "Europe/Berlin"
response_format ('markdown' | 'json'): default 'markdown'
Returns (json): { "utc": string, "unix_seconds": number, "unix_ms": number, "timezone": string | null, "local_time": string | null, "utc_offset": string | null }
| Name | Required | Description | Default |
|---|---|---|---|
| timezone | No | IANA time zone for an additional local time, for example "Europe/Berlin" or "America/New_York" | |
| response_format | No | Output format: 'markdown' (default) for readable text, 'json' for structured data | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| utc | Yes | Current time, RFC 3339 UTC with milliseconds |
| unix_ms | Yes | |
| timezone | Yes | |
| local_time | Yes | |
| utc_offset | Yes | |
| unix_seconds | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, non-destructive and openWorldHint=false, so the safety profile is covered. The description adds value beyond that by clarifying that the time is the server's machine clock in UTC (not the caller's local time) and that a caller-supplied timezone only adds a local rendering – a meaningful, non-obvious semantic.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose is front-loaded in one sentence, followed by usage rationale, then args and returns in compact list form. The Returns block is largely redundant with the existing output schema, which costs a little length, but the overall structure is clean and skimmable.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-required-parameter, read-only utility with an output schema present, the description covers purpose, usage motivation, and parameter intent. An agent has everything needed to call it correctly; the duplicated return block is harmless redundancy rather than a gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters are already documented with constraints and defaults. The description restates them (IANA zone example, default 'markdown') without adding syntax, fallback behavior for invalid zones, or offset semantics, so it sits at the baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence names a specific verb and resource ('Return the current time of the machine running this server') and scopes it precisely to UTC plus an optional IANA time zone. No sibling tool (identification, history, summarize, risk score, webhook verification) overlaps with this, so differentiation is implicit and unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives concrete when-to-use scenarios tied to sibling concepts: judging whether an identification's observed_at is older than a 5-minute window, or how long ago an account was last seen. It lacks any when-not-to-use or named alternative, but no plausible alternative exists among the siblings, so the guidance is nearly complete.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
shieldlabs_explain_risk_scoreExplain Risk ScoreARead-onlyIdempotent
Explain why an identification got its Risk Score: the band (trusted 0-29, suspicious 30-59, dangerous 60-100, or the 999 rate-limit marker), each weighted risk signal with a plain-language meaning from the catalog bundled with this server, the detection flags that are set, the connection type and caveats.
Args:
request_id (string, optional): read the identification from the History API, waiting up to about 10 s for a new one
identification (object or JSON string, optional): explain a pasted identification instead (normalized identification, webhook event or data object, or raw History API row)
response_format ('markdown' | 'json'): default 'markdown' Give exactly one of request_id and identification.
Returns (json): { "request_id": string | null, "risk_score": number, "risk_band": "trusted" | "suspicious" | "dangerous" | "rate_limited", "band_range": string, "band_meaning": string, "signals": [{ name, weight, label, meaning, reading, typical_weight, related_flag, in_catalog }], "detection_flags": [{ flag, label, scored, meaning }], "connection_type": { value, label, meaning } | null, "notes": [string], "source": "history" | "webhook" | "input", "truncated"?: true, "truncation_message"?: string }
A pasted risk_score must be an integer from 0 to 100, or the 999 rate-limit marker, and weights must be integers. Weights come from the identification itself; the catalog's typical weights can change, and signal names outside the catalog are possible. The catalog is also available as the resource shieldlabs://reference/risk-signals.
| Name | Required | Description | Default |
|---|---|---|---|
| request_id | No | Request ID of the identification to explain, read from the History API (needs SHIELDLABS_API_KEY; waits up to about 10 seconds for a new identification, like shieldlabs_get_identification). Give either request_id or identification. | |
| identification | No | An identification to explain without an API call: a normalized identification (for example the json output of shieldlabs_get_identification), a webhook event or its data object, or a raw History API row. A JSON object or its JSON text. | |
| response_format | No | Output format: 'markdown' (default) for readable text, 'json' for structured data | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| notes | Yes | |
| source | Yes | |
| signals | Yes | |
| risk_band | Yes | |
| truncated | No | Only present (true) when this server shortened the explanation |
| band_range | Yes | |
| request_id | Yes | |
| risk_score | Yes | |
| band_meaning | Yes | |
| connection_type | Yes | |
| detection_flags | Yes | |
| truncation_message | No | What was shortened: risk signals left out, or long pasted values cut |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, destructiveHint=false and openWorldHint=true. The description adds genuinely useful behavior beyond that: the ~10 s wait for a new identification, possible truncation via truncated/truncation_message, and the caveat that catalog typical weights can drift and out-of-catalog signal names are possible.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well front-loaded with the purpose first, then Args and Returns. However, the large Returns block restates the full response shape even though an output schema already exists, so a meaningful portion of the text duplicates structured data rather than earning its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema exists, so return values need no further explanation, and all three parameters are documented. The description still covers the pieces an agent needs: latency, truncation, the 999 rate-limit marker, band ranges, catalog drift caveats, and a pointer to the shieldlabs://reference/risk-signals resource.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description goes beyond the schema by adding the mutual-exclusion constraint ('exactly one of request_id and identification') and the validity rule that a pasted risk_score must be an integer 0-100 or the 999 marker with integer weights — constraints not encoded in the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Explain why an identification got its Risk Score') and enumerates exactly what is explained: band, weighted signals, detection flags, connection type, caveats. The purpose is unmistakable and functionally distinct from shieldlabs_get_identification, but no sibling is named in the description itself, so the differentiation is only implicit.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a concrete operating rule — 'Give exactly one of request_id and identification' — and explains the two modes: reading from the History API versus explaining a pasted identification. It does not state when to prefer this tool over shieldlabs_get_identification or shieldlabs_search_history, so exclusions are absent.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
shieldlabs_get_identificationGet identificationARead-onlyIdempotent
Read the verdict of one identification by its request ID from the ShieldLabs History API: Risk Score and band, weighted risk signals, the 19 detection flags, visitor, device, cookie and session IDs, User HID, public and local IP with country, connection type, OS, browser and traffic source.
Use it when you have the request ID of a protected action (signup, login, payment) and need its verdict. The browser only ever sees the request ID; the verdict is read here, server-side.
Scoring is asynchronous: the History row appears about 1 to 3 seconds after the browser call and can be refined for up to about 10 seconds while follow-up checks finish. This tool returns the first version of the row it finds. A verdict read in those first seconds can still be refined: read it again (wait=false is enough) once observed_at is about 10 seconds in the past for the final state. Integrations that start the identification when the user begins the action (for example on the first interaction with a form) find the row stored by the time the action reaches their backend.
With wait=true it polls for up to about 10 seconds in total. A 429, a 5xx or a network error does not end the wait (its error is returned only when the last poll failed); a wrong key or base URL ends it at once.
Args:
request_id (string): UUID returned by the browser agent
wait (boolean): poll until the verdict is stored, for up to about 10 s in total (default true); false reads once
response_format ('markdown' | 'json'): default 'markdown'
Returns (json): { "found": true, "identification": { request_id, visitor_id, device_id, session_id, cookie_id, user_hid, domain, public_ip: { ip, country }, local_ip: { ip, country }, connection_type, os, browser, device_type, traffic_source: { channel, referrer_domain, landing_url, click_id_type, utm_source, utm_medium, utm_campaign, utm_content, utm_term }, risk_score, risk_band, signals: [{ name, weight, description: null }], detection_flags: { vpn, proxy, tor, ... }, observed_at, source, truncated_fields?, escaped_fields? }, "untrusted_data_note": string }
Errors: an error result when no identification is stored for the request ID (treat it as unverified, never as clean), when the key is wrong (401), or when the rate limit (429) or a server or network error prevents the read.
Next steps: shieldlabs_explain_risk_score explains the score; shieldlabs_summarize_entity with the device_id or user_hid shows the history around it.
| Name | Required | Description | Default |
|---|---|---|---|
| wait | No | Wait for the verdict when the identification is not stored yet, polling for up to about 10 seconds in total (default true). Use false to read once, for example to read a refined verdict again. | |
| request_id | Yes | Request ID of one identification: the UUID the browser agent returned for the protected action, for example "02f1d973-84db-4156-a7f7-e799e6bf389b" | |
| response_format | No | Output format: 'markdown' (default) for readable text, 'json' for structured data | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| found | Yes | |
| identification | Yes | |
| untrusted_data_note | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnly/idempotent annotations, it discloses non-obvious asynchronous behavior: the row appears 1-3 s after the browser call, can be refined for ~10 s, and the tool returns the first version found, with the rule to re-read once observed_at is ~10 s old. It also documents polling semantics and that 429/5xx/network errors do not terminate the wait while a bad key does, plus that a missing identification must be treated as unverified, never clean.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The purpose and the async-timing caveats are front-loaded and each earns its place, but the verbatim Args and Returns blocks duplicate the input schema and the existing output schema, adding bulk without new information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read tool with annotations covering safety, a 100%-covered schema, and an output schema, the description covers the remaining gaps: async timing, polling and error semantics, and the correct interpretation of a missing verdict. Nothing an agent needs to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so request_id, wait and response_format are already fully documented in the schema, including the UUID pattern and defaults. The description's Args block largely restates that, though it does add the operational hint that wait=false suffices to re-read a refined verdict.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence names a specific verb (Read) and resource (the verdict of one identification by request ID) and enumerates the concrete payload fields returned. It is clearly distinguishable from siblings like shieldlabs_search_history or shieldlabs_summarize_entity, which operate on entities rather than a single request ID.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It states exactly when to use it ('when you have the request ID of a protected action ... and need its verdict') and adds the key situational fact that the browser only sees the request ID while the verdict must be read server-side. It also names alternatives in Next steps (shieldlabs_explain_risk_score, shieldlabs_summarize_entity) and gives the re-read condition.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
shieldlabs_search_historySearch historyARead-onlyIdempotent
List identifications for one identifier from the ShieldLabs History API, newest first, with paging.
Use it to see individual identifications of an account (user_hid), a device (device_id), a browser visitor (visitor_id), a cookie (cookie_id), a visit (session_id) or an IPv4 address (ip). For counts and aggregates over many identifications, prefer shieldlabs_summarize_entity.
Args:
type ('request_id' | 'user_hid' | 'device_id' | 'visitor_id' | 'ip' | 'session_id' | 'cookie_id')
value (string): UUID for the ID types, dotted IPv4 for ip, exact User HID for user_hid
limit (number): 1-100, default 20
offset (number): default 0
response_format ('markdown' | 'json'): default 'markdown'
Returns (json): { "lookup": { type, value }, "total": number, "count": number, "offset": number, "limit": number, "has_more": boolean, "next_offset": number (only when has_more), "identifications": [same shape as shieldlabs_get_identification], "untrusted_data_note": string, "truncated"?: boolean, "truncation_message"?: string }
Examples:
Every identification of one account: type="user_hid", value=""
Which accounts used this device: type="device_id", value=""
Next page: repeat with offset=next_offset
Errors: invalid values are refused before any request (UUID or IPv4 expected; IPv6 is not searchable; a User HID that contains "/" cannot be searched). 401 means a wrong key; 429 means the History API rate limit (about 15 requests per second per domain) was reached.
| Name | Required | Description | Default |
|---|---|---|---|
| type | Yes | Identifier to search by: request_id (one identification), user_hid (one of your accounts, exact and case-sensitive), device_id (one device), visitor_id (one browser visitor), ip (IPv4 only), session_id (one visit) or cookie_id (one browser profile) | |
| limit | No | Identifications per page, from 1 to 100 (default 20) | |
| value | Yes | Identifier value: a UUID for request_id, device_id, visitor_id, session_id and cookie_id; a dotted IPv4 address such as 203.0.113.24 for ip; the exact User HID string for user_hid ("anonymous" lists anonymous checks; a User HID that contains "/" cannot be searched) | |
| offset | No | Number of identifications to skip, for paging (default 0). Use next_offset from the previous page. | |
| response_format | No | Output format: 'markdown' (default) for readable text, 'json' for structured data | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Identifications in this response |
| limit | Yes | |
| total | Yes | Identifications matching the lookup |
| lookup | Yes | |
| offset | Yes | |
| has_more | Yes | |
| truncated | No | |
| next_offset | No | |
| identifications | Yes | |
| truncation_message | No | |
| untrusted_data_note | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/openWorld/destructive=false, and the description goes well beyond them: pre-request validation refusal, IPv6 not searchable, User HIDs containing '/' unsearchable, 401 = wrong key, and a concrete 429 rate limit (~15 req/s per domain). It also flags that results contain untrusted data via untrusted_data_note and possible truncation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the purpose and the routing decision, then organized into Args/Returns/Examples/Errors sections that are each useful. It is on the long side and the Args block overlaps the schema, but no section is padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Despite an output schema existing, the description adds the paging contract (next_offset, has_more) and error semantics an agent needs to drive the loop correctly. For a 5-parameter, enum-heavy search tool, nothing material is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3; the description's Args block largely restates the schema but does reinforce value formats and adds concrete examples (type="user_hid", type="device_id", reusing offset=next_offset), which clarifies how the parameters combine in practice.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence states a specific verb (list), resource (identifications for one identifier from the History API), ordering (newest first), and paging behavior. It explicitly distinguishes itself from the sibling shieldlabs_summarize_entity, so an agent can route between the two without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It names exactly when to use this tool (individual identifications of an account, device, visitor, cookie, visit or IP) and when to prefer the alternative (counts and aggregates over many identifications -> shieldlabs_summarize_entity). Worked examples cover the three main call patterns, including how to page.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
shieldlabs_summarize_entitySummarize user, device, visitor or IPARead-onlyIdempotent
Aggregate the recent History API identifications of one account (user_hid), device (device_id), browser visitor (visitor_id), IPv4 address (ip) or cookie (cookie_id).
The result is computed by this server from the returned history window (the most recent max_items identifications), and labelled as such: it is not a ShieldLabs verdict. It counts distinct devices, accounts, visitors, countries, IPs and connection types (with the top values), the highest and average Risk Score, band counts, rate-limit markers, detection flags and risk signals seen with counts, and the first and last observed time.
Use it for questions such as: how many accounts used this device, how many devices this account used, which countries an account came from, whether an IP sent automated traffic. Use shieldlabs_search_history to see the individual identifications.
Args:
type ('user_hid' | 'device_id' | 'visitor_id' | 'ip' | 'cookie_id')
value (string): UUID for the ID types, dotted IPv4 for ip, exact User HID for user_hid
max_items (number): 1-500, default 100
response_format ('markdown' | 'json'): default 'markdown'
Returns (json): { "lookup": { type, value }, "computed_from": string, "window": { identifications_analyzed, total_matching, max_items, complete }, "summary": { identifications_analyzed, first_observed_at, last_observed_at, devices: { distinct, top: [{ value, count, truncated? }] }, users, visitors, countries, ips, connection_types, anonymous_identifications, identifications_without_account, identifications_without_device_signals, risk: { max_risk_score, average_risk_score, worst_band, band_counts: { trusted, suspicious, dangerous }, rate_limit_markers }, flags_seen: [{ flag, count }], signals_seen: [{ name, count }], distinct_signals }, "untrusted_data_note": string }
The user count ignores values that do not name an account: null, "anonymous", "fail", "-1" and "unknown". The device count ignores the all-zero device ID (no usable device signals). Averages ignore the 999 rate-limit marker. The History API limit (about 15 requests per second per domain) is shared with the site's own backend: start with the default max_items and raise it only when the window is incomplete and the question needs older identifications.
| Name | Required | Description | Default |
|---|---|---|---|
| type | Yes | What to summarize: user_hid (one of your accounts), device_id (one device), visitor_id (one browser visitor), ip (one IPv4 address) or cookie_id (one browser profile) | |
| value | Yes | Identifier value: a UUID for device_id, visitor_id and cookie_id; a dotted IPv4 address such as 203.0.113.24 for ip; the exact User HID string for user_hid ("anonymous" summarizes anonymous checks; a User HID that contains "/" cannot be searched) | |
| max_items | No | Most recent identifications to analyze, from 1 to 500 (default 100). Each 100 identifications cost one History API request, from the domain's shared limit. | |
| response_format | No | Output format: 'markdown' (default) for readable text, 'json' for structured data | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| lookup | Yes | |
| window | Yes | |
| summary | Yes | |
| computed_from | Yes | |
| untrusted_data_note | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover safety (readOnly, idempotent, non-destructive, openWorld), but the description goes well beyond them: it discloses that results are server-computed from the returned window rather than a ShieldLabs verdict, documents which values are excluded from counts (null, 'anonymous', 'fail', '-1', 'unknown', all-zero device ID, 999 rate-limit marker), and warns about the shared ~15 req/s History API limit.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with purpose, then use cases, then args, then behavior; each section is dense and earns its place. The verbose Returns dump is largely redundant with the existing output schema, which is the only real conciseness cost.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 4-parameter aggregation tool with an output schema and full annotations, the description covers identification of scope, counting semantics, edge cases, rate limits and output format. An agent has everything needed to call it correctly and interpret the result.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so baseline is 3, but the description adds real meaning beyond the schema: value formats (UUID for ID types, dotted IPv4 for ip, exact User HID), the 'anonymous' special case, the '/' restriction, and the cost model for max_items (each 100 identifications = one History API request from a shared limit).
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (Aggregate) and resource (recent History API identifications) and enumerates the five entity types it operates over. It explicitly distinguishes itself from the sibling tool by directing the agent to shieldlabs_search_history for individual identifications.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives concrete when-to-use questions ('how many accounts used this device', 'whether an IP sent automated traffic') and names the alternative (shieldlabs_search_history) with the condition that selects it. It also advises starting with the default max_items and raising it only when the window is incomplete.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
shieldlabs_verify_webhook_signatureVerify webhook signatureARead-onlyIdempotent
Check a received ShieldLabs webhook delivery: does its X-Shield-Signature header match the raw body for the endpoint signing secret? For debugging a webhook endpoint. SHIELDLABS_WEBHOOK_SECRET is not set on this server, so pass secret.
The signature is "sha256=" plus the lowercase hex HMAC-SHA256 of the raw body, keyed with the full signing secret including its whsec_ prefix. While a secret is rotated, give several secrets separated by commas (in secret or in SHIELDLABS_WEBHOOK_SECRET): the delivery is valid when any of them matches, and matched_secret tells which. When the signature does not match a body of up to 64 KB, the tool tests common body changes (re-serialized JSON, unescaped characters, added newline) and says which one explains the mismatch. It never returns a secret or an expected signature.
Args:
payload (string): raw body exactly as received
signature_header (string): value of X-Shield-Signature
secret (string, optional): whsec_... signing secret of the endpoint, or several separated by commas
payload_encoding ('utf8' | 'base64'): default 'utf8'
response_format ('markdown' | 'json'): default 'markdown'
Returns (json): { "valid": boolean, "secret_source": "argument" | "environment", "secrets_checked": number, "matched_secret": number | null, "checks": [{ check, ok, detail }], "event": { event_type, schema_version, created_at, request_id, risk_score, risk_band } | null, "hint": string | null }
| Name | Required | Description | Default |
|---|---|---|---|
| secret | No | Endpoint signing secret including its whsec_ prefix, or several separated by commas while you rotate. Defaults to SHIELDLABS_WEBHOOK_SECRET from the server environment. | |
| payload | Yes | The raw request body exactly as received: the bytes that were signed. Do not pretty-print, re-serialize or trim it. | |
| response_format | No | Output format: 'markdown' (default) for readable text, 'json' for structured data | markdown |
| payload_encoding | No | 'utf8' (default) when payload is the body text; 'base64' when payload is the body bytes encoded as base64 (use it when exact bytes such as a trailing newline matter) | utf8 |
| signature_header | Yes | Value of the X-Shield-Signature header, for example "sha256=ea26857...fdd8" |
Output Schema
| Name | Required | Description |
|---|---|---|
| hint | Yes | |
| event | Yes | |
| valid | Yes | |
| checks | Yes | |
| secret_source | Yes | |
| matched_secret | Yes | Position, from 1, of the secret that matches, in the order given; null when none |
| secrets_checked | Yes | How many signing secrets were checked (several while a secret is rotated) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the read-only/idempotent/non-destructive annotations, the description discloses a security guarantee ('never returns a secret or an expected signature'), the 64 KB size boundary and the fallback diagnostic of testing common body mutations when a signature mismatches. It also surfaces the deployment-specific fact that SHIELDLABS_WEBHOOK_SECRET is unset on this server, forcing the caller to pass secret.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose and scoping come first, then the signature/rotation semantics, then args and return shape — well front-loaded and dense. It is on the long side and the Args/Returns blocks partly restate the schema and output schema, which is mild redundancy but also serves as a quick reference.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a five-parameter verification tool with an output schema present, the description covers the environment prerequisite, signing algorithm, rotation handling, mismatch diagnostics, and privacy guarantees. Nothing an agent needs in order to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds real semantic content: the signature format is 'sha256=' plus lowercase hex HMAC-SHA256 keyed with the full secret including the whsec_ prefix, and multiple secrets are comma-separated with matched_secret reporting which one hit. These are interaction details the per-parameter schema text does not fully convey.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource — check whether a received ShieldLabs webhook delivery's X-Shield-Signature header matches the raw body for the endpoint's signing secret. No sibling tool (identification, search history, summarize entity, explain risk score, current time) overlaps with signature verification, so no sibling disambiguation is required.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It states the context explicitly — 'For debugging a webhook endpoint' — and then covers the rotation scenario where multiple comma-separated secrets should be supplied. There is no explicit when-not guidance, but the tool's scope is narrow enough that no plausible alternative exists among the siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
6 tool updates
v1.0.0- First observed
shieldlabs_current_time - First observed
shieldlabs_explain_risk_score - First observed
shieldlabs_get_identification - First observed
shieldlabs_search_history - First observed
shieldlabs_summarize_entity - First observed
shieldlabs_verify_webhook_signature
TDQS
Scored across 6 tools
Each tool targets a distinct action: get one identification by request ID, list identifications for an identifier, aggregate history for an entity, explain a risk score, verify a webhook signature, and get server time. The descriptions explicitly cross-reference each other (e.g. search_history vs summarize_entity, and pointing to explain_risk_score for score explanations), leaving no real overlap.
All tools use snake_case with a consistent shieldlabs_ prefix and mostly follow a verb_noun pattern (get_identification, search_history, summarize_entity, explain_risk_score, verify_webhook_signature). The only minor deviation is current_time, which lacks a verb, but it still reads clearly as a command.
Six tools is well-scoped for a read-oriented risk/verdict API: single read, search, aggregate, score explanation, webhook verification, and a time helper. Each tool earns its place without redundancy or obvious padding.
The surface covers the read lifecycle well (single, list, aggregate, explain) plus webhook verification and time, and the risk-signal catalog is exposed as a resource. A minor gap is the absence of a tool to list recent identifications for a domain/tenant without first knowing an identifier, which some triage workflows would want.
Maintenance
Related MCP Connectors
Exposes FEDLIN's public security scanners as agent-callable tools over Streamable HTTP.
Context and control for any AI. Agent identity, scoped access, and proof across tools.
Read-only, deterministic AI triage and readiness tools implementing Sophon's published rubrics.
Read-only phone intelligence for AI voice agents — line type, risk, DNC, signed receipts.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables read-only document retrieval and SQLite exploration through tools over stdio, enforcing a safety gate before SQL execution.3MIT
- AlicenseNot gradedqualityAmaintenanceExposes three read-only tools over stdio — guarded SQL querying, SKU status lookup, and document retrieval — plus a schema resource. This lets MCP clients such as Claude Code answer stock and policy questions without any write access.MIT
- AlicenseNot gradedqualityAmaintenanceExposes fourteen read-only tools over the HEY Research public API, letting assistants look up Robinhood Chain projects, builders, shipped work, relationships and market context, with each answer tagged FACT, DERIVED or UNKNOWN and unmeasured fields omitted rather than zeroed. Runs over stdio or hosted Streamable HTTP, and gives no trade instructions.MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP clients such as Cursor and Claude Desktop to query a GRC platform over stdio or HTTP, with read-only access by default and opt-in create, update, and delete tools subject to platform authorization.3Apache 2.0