MCP Tools Token Profiler and Schema Linter
Pricing
$5.00 / 1,000 server or tools list profileds
MCP Tools Token Profiler and Schema Linter
Paste an MCP tools/list or give public server URLs. Get estimated tokens per tool, the 5 heaviest tools, ranked savings with exact JSON paths, and lint cited to the MCP spec. No login.
Pricing
$5.00 / 1,000 server or tools list profileds
Rating
0.0
(0)
Developer
Mohammed Samaan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Every tool an MCP server lists is sent to the model in each request, so a heavy tools/list costs context on every call. This actor reads a tools/list result and tells you where the tokens go, what could be shortened, and which definitions break rules in the MCP specification.
Give it a pasted tools/list JSON or the URL of a public MCP server (no login). For each input you get one row with:
- estimated tokens per tool, split into name, description, input schema, output schema and the rest, plus server totals
- the 5 heaviest tools and their share of the total
- ranked savings ideas, each with the exact JSON path and the tokens that would be saved if you shortened it (computed by counting the edited text again)
- lint findings with the exact JSON path, a minimal fix, and the specification sentence the rule comes from
All token numbers are estimates from an OpenAI-style vocabulary (o200k_base or cl100k_base). They are not counts for Claude, Gemini or any other model, and they leave out any framing a provider adds around tool definitions. Use them to compare tools and servers with each other and to find the heavy spots.
Sample input
{"toolsListJson": "{\"tools\":[{\"name\":\"search docs\",\"description\":\"Search documents.\\n\\nParameters:\\n- query: the text to find\\n- tags: labels to match\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"the text to find\"},\"tags\":{\"type\":\"array\",\"description\":\"labels to match\"}},\"required\":[\"query\"]}}]}","serverUrls": ["https://docs.mcp.cloudflare.com/mcp"],"tokenizer": "o200k_base"}
Sample output
Real output of a local run for the pasted list above (shortened: the tools array, source and timestamps are left out here):
{"status": "ok","tokenizer": "o200k_base","tokenCountsAreEstimates": true,"toolCount": 1,"totals": { "toolCount": 1, "nameTokens": 2, "descriptionTokens": 19, "inputSchemaTokens": 39, "outputSchemaTokens": 0, "otherTokens": 0, "totalTokens": 60 },"heaviestTools": [ { "rank": 1, "index": 0, "name": "search docs", "totalTokens": 60, "share": 1 } ],"suggestions": [{"rank": 1,"kind": "description-repeats-parameters","path": "/tools/0/description","currentTokens": 19,"savedTokensEstimate": 16,"message": "The description restates 2 parameter(s) (query, tags) that inputSchema already describes. Remove those lines from the description."}],"lint": {"counts": { "error": 0, "warning": 1, "info": 1 },"issues": [{"ruleId": "tool-name-chars","severity": "warning","path": "/tools/0/name","message": "Name \"search docs\" contains characters outside A-Z, a-z, 0-9, \"_\", \"-\" and \".\".","fix": "Rename using only A-Z, a-z, 0-9, underscore, hyphen and dot (for example replace spaces with \"_\")."},{"ruleId": "schema-array-items-missing","severity": "info","path": "/tools/0/inputSchema/properties/tags","message": "Array without \"items\": the element type is not described.","fix": "Add \"items\", for example \"items\": { \"type\": \"string\" }."}]},"warnings": [],"error": null}
A row for a server that could not be read has "status": "error", a specific error text and an errorKind (auth, not-mcp, session, protocol, unreachable, timeout, too-large, unsafe-url, invalid-input, internal, unexpected). It is never charged, and a failure on one input never stops the other inputs.
Input
| Field | Type | What it does |
|---|---|---|
toolsListJson | text | A pasted tools/list result. Accepted: a bare array of tools, an object with a tools array, or the full JSON-RPC response. At most 10,000 tools and 8 million characters. |
serverUrls | list of URLs | Public Streamable HTTP MCP endpoints that need no login, at most 50. |
tokenizer | o200k_base or cl100k_base | Vocabulary used for the estimates. Default o200k_base. |
Give at least one of toolsListJson and serverUrls. Both may be given; each is its own row.
How a server is read
The actor never sends credentials and only sends tools/list and the handshake messages. It first tries the stateless form (specification revision 2026-07-28: tools/list with the per-request _meta fields and the MCP-Protocol-Version header). If the server answers with an error that is not a modern version error, it falls back to the handshake of revisions 2025-03-26 to 2025-11-25: initialize, notifications/initialized, then tools/list with the MCP-Session-Id the server returned, following nextCursor pages (up to 100 pages). JSON and Server-Sent Events responses are both handled, and the session is closed with DELETE when the server opened one.
Requests identify themselves with the User-Agent ApifyActor-mcp-tools-token-profiler/0.1 (+https://apify.com/leadharbor/mcp-tools-token-profiler). Responses larger than 8 MB (also after decompression) are refused.
Addresses are checked before every request and every redirect: private, loopback, link-local and other reserved addresses are refused, only ports 80 and 443 are allowed, and the connection is pinned to the checked address.
Tested live on 21 Oct 2026 against https://mcp.deepwiki.com/mcp (handshake form) and https://docs.mcp.cloudflare.com/mcp (stateless form). Servers that require a login (HTTP 401 or 403) are reported as such and are not profiled.
Lint rules and their sources
Only rules with a quotable source are implemented. Revisions read from the modelcontextprotocol/modelcontextprotocol repository on 21 Oct 2026: 2025-11-25 and 2026-07-28. JSON Schema rules come from the 2020-12 specification (MCP schemas default to that dialect).
| Rule id | Severity | Rule and source |
|---|---|---|
tool-not-object | error | Every entry of tools is a Tool object. schema ListToolsResult |
tool-name-missing | error | name: string is required (BaseMetadata). schema.ts |
tool-name-length | warning | "Tool names SHOULD be between 1 and 128 characters in length (inclusive)." Tool names |
tool-name-chars | warning | Only A-Z, a-z, 0-9, _, -, . are allowed; no spaces, commas or other special characters. Tool names |
tool-name-duplicate | warning | "Tool names SHOULD be unique within a server." Names are compared case-sensitively, as the same page says they SHOULD be considered case-sensitive. Tool names |
description-missing | info | description is optional; it is "a hint to the model". A quality hint, not a violation. schema Tool |
input-schema-missing | error | inputSchema "MUST be a valid JSON Schema object (not null)". Tool |
input-schema-root-type | error | inputSchema is declared with type: "object" at the root. schema Tool |
output-schema-root-type | warning | Revision 2025-11-25 restricts outputSchema to type: "object" at the root; 2026-07-28 allows any schema, so this only matters for clients on the older revision. schema Tool |
schema-dialect | info | Without $schema the default is 2020-12; implementations MUST support 2020-12 and only SHOULD document other dialects. JSON Schema usage |
schema-type-invalid | error | type is one of the seven JSON Schema names, or an array of unique ones. Validation 6.1.1 |
schema-enum-empty | warning | enum MUST be an array and SHOULD have at least one element. Validation 6.1.2 |
schema-enum-duplicate | warning | Elements of enum SHOULD be unique. Validation 6.1.2 |
schema-required-invalid | error | required MUST be an array of unique strings. Validation 6.5.3 |
schema-array-items-missing | info | Heuristic from JSON Schema 2020-12 (not a quotation): without items (or prefixItems) array elements are unconstrained. Valid, but gives the model no element type. Core 10.3.1.2 |
annotations-invalid | warning | annotations is an object; the four hints are booleans and title is a string. schema ToolAnnotations |
x-mcp-header-invalid | error | x-mcp-header must be a non-empty HTTP header token and sit on a string, integer or boolean parameter (not number). Revision 2026-07-28. x-mcp-header |
x-mcp-header-union-type | info | x-mcp-header "MUST only be applied to parameters with primitive types (integer, string, boolean)". The text does not say whether a union such as ["string","null"] counts, so a union is a note, not an error. Revision 2026-07-28. x-mcp-header |
schema-too-deep | warning | Not a specification rule: an analysis limit. A tool whose JSON nests deeper than 200 levels is reported and left out of the counts. JSON Schema usage |
x-mcp-header-duplicate | error | x-mcp-header values must be case-insensitively unique within an inputSchema. Revision 2026-07-28. x-mcp-header |
Not implemented, on purpose: rules about what a specific model provider accepts (for example strict-mode restrictions or dropped format keywords). I could not read those from a source I can cite here, so the actor does not guess.
Savings ideas (heuristics, not specification rules)
The thresholds are our own choices. Each saving is computed by counting the edited text again with the same tokenizer, and is labelled an estimate. Ideas can overlap, so do not add the savings of different ideas together.
| Kind | Fires when | Saving computed as |
|---|---|---|
description-repeats-parameters | the description has lines that restate parameters which the schema already describes | description tokens minus tokens after removing those lines |
large-enum | an enum costs 150 tokens or more | schema tokens minus tokens with the enum removed (validation is lost, so check first) |
long-description | a tool description is 300+ tokens or a property description 100+ | tokens minus tokens of its first paragraph (or sentence) |
redundant-schema-keyword | $schema names 2020-12, the default | sum over the listed paths |
duplicate-description | several tools share the same description of 30+ tokens | copies after the first |
deep-nesting | schemas nest more than 4 levels | none: only the subtree size is shown |
Pricing
Pay per event: $0.005 per input successfully profiled (event schema-profiled): one pasted list or one server URL. Inputs that fail (unreachable, login required, not MCP, invalid JSON, empty list, too many tools) produce an error row and are not charged. The run stops cleanly when your maximum charge limit is reached and says so in the status message and the run summary.
Worked example: 1 pasted list plus 10 server URLs where 7 answer and 3 need a login: 8 profiled inputs, 3 error rows, $0.04.
Use with AI agents / MCP
Send this input and read these fields from each dataset row:
{ "serverUrls": ["https://example.com/mcp"], "tokenizer": "o200k_base" }
| Read this | To learn |
|---|---|
status, error, errorKind | whether the server could be read and why not |
totalTokens, toolCount | estimated size of the whole tool list |
heaviestTools[] | which 5 tools to look at first |
suggestions[] | path, savedTokensEstimate, message for each ranked idea |
lint.counts, lint.issues[] | ruleId, severity, path, fix, source for each finding |
The dataset item can be fetched through the Apify API once the run finishes. RUN_SUMMARY in the key-value store has run-level counts.
Limits
- Tokens are estimates for the chosen OpenAI-style vocabulary only; no Claude or Gemini counts.
totalTokensis the sum of separately counted parts (name, description, input schema, output schema, other fields), so rows and totals add up exactly. It is not the count of the whole definition tokenized as one string.- Strings over 100,000 characters are counted by sampling 24 slices; the row says so in
warnings. Sampling error was measured at 0.05 to 2.05 percent (our run on English and JSON text of 120k to 600k characters) and 0.1 to 1.2 percent (independent review); CJK-heavy text over that size has not been independently verified. - Tools whose JSON nests deeper than 200 levels are not analysed (lint rule
schema-too-deep). - Memory: the default is 1024 MB. Profiling three 7.3 MB server responses in one run peaked at about 340 MB resident memory in a local measurement (a run with batches of two peaked at 356 to 377 MB, so limiting parallel fetches did not help; the report building dominates).
- A row lists at most 1,000 lint issues and 100 savings ideas for very large lists;
lint.counts,lint.byRuleandsuggestionCountalways hold the full numbers. - Only Streamable HTTP servers that need no login can be read by URL. Servers on stdio, the old HTTP+SSE transport, or behind OAuth or API keys cannot; paste their
tools/listinstead. - The stateless read (revision 2026-07-28) was seen working on two public servers. Other servers may differ; a failure is reported with the server's own error.
- Lint reports what the specification text says. A server can be perfectly usable and still get warnings or info findings.
FAQ
Where do I get a tools/list to paste? From your MCP client's debug output, from the MCP Inspector, or by sending {"jsonrpc":"2.0","id":1,"method":"tools/list"} to a server after initialize.
Why do rows and totals sometimes differ from my provider's token count? Different vocabularies and different framing around tools. Compare relatively, not absolutely.
Is my pasted JSON stored? It is processed in the run and only the profile is written to your dataset. Run storage follows your Apify account settings.
Can it fix my schemas? No. It points at the path and describes the minimal edit.
Open-source credits
| Package | Version | Licence | Checked from |
|---|---|---|---|
| js-tiktoken | 1.0.21 | MIT | license field in its package.json; the repository dqbd/tiktoken root LICENSE is MIT (the npm tarball ships no LICENSE file) |
| base64-js (dependency of js-tiktoken) | 1.5.1 | MIT | node_modules/base64-js/LICENSE |
| apify (Apify SDK for JavaScript) | 3.7.2 | Apache-2.0 | node_modules/apify/LICENSE.md |
| caniuse-lite (transitive, via the SDK; browser data) | 1.0.30001817 | CC-BY-4.0 | node_modules/caniuse-lite/LICENSE (Creative Commons Attribution 4.0); attribution: caniuse.com data |
| map-stream (transitive) | 0.1.0 | MIT text, no license field in its package.json | node_modules/map-stream/LICENCE (Copyright 2011 Dominic Tarr, MIT permission text) |
| pause-stream (transitive) | 0.0.11 | MIT and Apache-2.0 (dual) | node_modules/pause-stream/LICENSE ("Dual Licensed MIT and Apache 2") |
| sax (transitive) | 1.6.1 | BlueOak-1.0.0 | node_modules/sax/LICENSE.md (Blue Oak Model License 1.0.0) |
| tslib (transitive) | 2.8.1 | 0BSD | node_modules/tslib/LICENSE.txt |
| type-fest (transitive) | 2.19.0 and 4.41.0 | MIT or CC0-1.0 | license-mit and license-cc0 files in each package |
The remaining transitive packages in the image are MIT, Apache-2.0, ISC, BSD-2-Clause or BSD-3-Clause according to the license field of the lockfile; the packages above are the ones outside that list or without a plain license field.
Tests use tool lists captured on 21 Oct 2026 from the published @modelcontextprotocol/server-filesystem, server-memory, server-everything and server-sequential-thinking packages (version 2.0.0) of the modelcontextprotocol/servers repository. That repository's LICENSE states a transition from MIT to Apache-2.0 (new contributions Apache-2.0, earlier ones MIT); both permit this use with attribution.