TinyPDF.in logo
Tools › WebMCP

WebMCP examples: 20 live tools you can call in Chrome

10 TinyPDF.in pages register WebMCP tools, 20 in all. An AI agent running in Chrome can compare or query JSON, run SQL, read a PostgreSQL plan or mask ID numbers by calling a tool directly, without clicking through the page. You can test it yourself in a minute.

🔒 Your data still stays on your device. A tool call runs the same in-page code as the buttons. Nothing is uploaded to TinyPDF.in.

What is WebMCP?

WebMCP lets a web page tell the browser which actions it offers, as named tools, each with a description and a list of inputs. An AI agent running in the browser can then call a tool directly and get a structured answer, instead of reading the screen and clicking buttons. It is an origin trial in Chrome, reached through document.modelContext.

This page is a working demo. Every tool below is live on TinyPDF.in today, and each has an example you can paste into the console.

All WebMCP tools on this site

Open a page and its tools are available to the agent for as long as the tab stays open.

PageWhat an agent can doTools
JSON Compare & Schema ValidatorCompare two JSON documents by structure, or validate one against a JSON Schema.compare_json
validate_json_schema
JSON Query (SQL on JSON)Run a SQL SELECT over a JSON document.query_json_with_sql
Make JSON Safe for LLMsFind sensitive values in JSON and replace them with placeholders.find_sensitive_values_in_json
make_json_safe
Sensitive Info FinderScan text for emails, phone numbers, card numbers, keys and Indian IDs.find_sensitive_info_in_text
Mask Aadhaar & Indian IDsFind and mask Aadhaar, PAN, GSTIN, IFSC and UPI IDs in text.find_indian_ids
mask_indian_ids
PostgreSQL PlaygroundRun SQL in a practice PostgreSQL database and list its tables.run_postgres_sql
list_postgres_tables
SQLite PlaygroundRun SQL in an in-browser SQLite database and list its tables.run_sqlite_sql
list_sqlite_tables
DuckDB PlaygroundRun SQL in an in-browser DuckDB database and list its tables.run_duckdb_sql
list_duckdb_tables
Mongo Query PlaygroundRun Mongo shell queries on sample collections and list them.run_mongo_query
list_mongo_collections
PostgreSQL Query Performance LabExplain a query plan in plain words and compare index choices.analyze_postgres_query
compare_postgres_index_choices
compare_vector_index_choices
list_postgres_tables_and_indexes

Check if your browser supports WebMCP

To let you check your browser, this page loads two of the tools as a sample: the JSON Compare ones. Every tool, including these two, is registered on its own page.

Checking your browser…

How to test a WebMCP tool in Chrome

  1. Use Chrome 149 or later on a computer. The site is enrolled in Chrome's WebMCP origin trial, so there is nothing to switch on for tinypdf.in.
  2. Open the page of the tool you want from the table above, for example JSON Query or the PostgreSQL Playground.
  3. Open the developer console (F12, then the Console tab) and list that page's tools:
    await document.modelContext.getTools()
  4. Paste the example for the tool from the list below. Inputs are passed as JSON text, and the result comes back as JSON text. The page also shows on screen what was run.

Every tool page has the same steps and its own example under “Use this tool from an AI agent (WebMCP)”. Prefer a visual tool? Google's Model Context Tool Inspector extension for Chrome lists the tools on the current page and lets you fill in the inputs and run them.

Connect a desktop agent (Claude Code, Cursor and others)

A desktop agent cannot see a browser tab by itself. A small free bridge passes the tools from your open tab to the agent: a Chrome extension plus a helper that runs on your computer. Everything stays on your machine.

The bridge is WebMCP Bridge, an open-source project by AgentCat. It is not made by TinyPDF.in.

  1. You need Chrome 149 or later and Node.js 18 or later on the same computer as your agent.
  2. Install the WebMCP Bridge extension from the Chrome Web Store and pin it to the toolbar.
  3. Add the helper to your agent.
    Claude Code, in a terminal:
    claude mcp add --transport stdio webmcp-server -- npx webmcp-server
    Cursor, in ~/.cursor/mcp.json (other agents that use an mcpServers file take the same entry):
    {
      "mcpServers": {
        "webmcp-server": {
          "command": "npx",
          "args": ["webmcp-server"]
        }
      }
    }
    Restart the agent so it starts the helper.
  4. In Chrome, open the TinyPDF.in tool you want, for example JSON Query. Click the extension icon and choose Always On. A green dot means the tab is connected.
  5. In your agent, the page's tools now appear with the tab number in front, such as tab-42:query_json_with_sql. Ask for what you want, for example “use the TinyPDF tool to run this SQL over my JSON”.

If it does not connect: a yellow dot means the helper is not running or the port differs (the default is 12315). Keep the TinyPDF.in tab open while the agent works, and switch tabs or pages to change which tools are offered.

We checked this on 6 October 2026 with Chrome 153 and the JSON Compare tools: the tools were listed and a call returned the right result. The bridge is a young project, so check its page if something has changed.

Call a tool from Python

You can also reach the tools from your own code. This script starts the bridge helper, lists the tools from your open TinyPDF.in tab and calls compare_json.

  1. Install the extension as above, and run pip install mcp.
  2. Open tinypdf.in/json-compare.html in Chrome and set the extension to Always On.
  3. Close any agent that is already running the helper, save the script as a file and run it with python your_file.py.
Show the Python script
import asyncio, json, shutil, sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

NPX = shutil.which("npx") or shutil.which("npx.cmd")
if not NPX:
    sys.exit("npx not found. Install Node.js 18 or later.")

async def main():
    server = StdioServerParameters(command=NPX, args=["-y", "webmcp-server"])
    async with stdio_client(server) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # give the extension a few seconds to hand over the tab's tools
            tools = []
            for _ in range(15):
                tools = (await session.list_tools()).tools
                if tools:
                    break
                await asyncio.sleep(1)
            if not tools:
                print("No tools found. Is the TinyPDF.in tab open and the extension dot green?")
                return

            print("Tools found:")
            for t in tools:
                print("  ", t.name)

            name = next((t.name for t in tools if t.name.endswith("compare_json")), None)
            if not name:
                print("compare_json is not in the list. Open json-compare.html in the tab.")
                return

            result = await session.call_tool(name, {
                "json_a": '{"id": 1, "price": 19.99}',
                "json_b": '{"price": "19.99", "id": 1}',
            })
            for part in result.content:
                text = getattr(part, "text", str(part))
                try:
                    print(json.dumps(json.loads(text), indent=2))
                except ValueError:
                    print(text)

asyncio.run(main())
What it prints
Tools found:
   tab-1935720870:compare_json
   tab-1935720870:validate_json_schema
{
  "ok": true,
  "identical": false,
  "counts": { "total": 1, "added": 0, "removed": 0, "value_changed": 0, "type_changed": 1 },
  "differences": [
    { "change": "type_changed", "path": "$.price",
      "a_type": "number", "a_value": "19.99", "b_type": "string", "b_value": "\"19.99\"" }
  ],
  "truncated": false
}

Please note

  • This is sample code, given as it is with no warranty. Read it before you run it.
  • It runs npx webmcp-server, which downloads and runs the bridge helper from npm on your computer. The helper, the extension and the mcp Python package are third-party software, not made or checked by TinyPDF.in.
  • The number in a tool name, such as tab-1935720870, is your tab and will be different each time. The script finds the tool by the end of its name for that reason.
  • The call runs in your open browser tab. The JSON you pass goes from the script to the helper to the tab on your own machine, and is not sent to TinyPDF.in.
  • The tools exist only while the tab is open and the extension is on for it.

WebMCP example calls for each tool

Click a page to see its tools, their inputs and an example to paste. Open that page first, then paste the example into the console. Every result is JSON text. Long lists are shortened here with “…”.

JSON Compare & Schema Validator

2 tools: compare_json, validate_json_schema

Open tinypdf.in/json-compare.html, then:

compare_json

Compare two JSON documents by structure, not as text. Key order is ignored. Returns every difference with its JSON path: values added, removed, changed, or changed in data type. Runs locally in the page; nothing is uploaded.

InputTypeMeaning
json_atext, requiredThe original JSON document, as text.
json_btext, requiredThe changed JSON document, as text.
array_order_matterstrue/falseIf true, arrays are compared by position. Default false: array items are matched by id or by content.
ignore_keystextComma-separated key names to skip at every depth, for example "updatedAt, requestId".
number_tolerancenumberNumbers closer than this are treated as equal. Default 0.
integer_vs_decimal_is_changetrue/falseIf true, 1 and 1.0 are reported as a type change. Default false.
allow_commentstrue/falseIf true, accepts // and /* */ comments and trailing commas.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'compare_json');
await document.modelContext.executeTool(tool, JSON.stringify({
  "json_a": "{\"id\":1,\"price\":19.99}",
  "json_b": "{\"price\":\"19.99\",\"id\":1}"
}));
What it returns
{
 "ok": true,
 "identical": false,
 "counts": {
  "total": 1,
  "added": 0,
  "removed": 0,
  "value_changed": 0,
  "type_changed": 1
 },
 "differences": [
  {
   "change": "type_changed",
   "path": "$.price",
   "a_type": "number",
   "a_value": "19.99",
   "b_type": "string",
   "b_value": "\"19.99\""
  }
 ],
 "truncated": false
}

validate_json_schema

Validate a JSON document against a JSON Schema (draft-04 to 2020-12). Returns whether it is valid and each error with its path, keyword and message. Runs locally in the page; nothing is uploaded.

InputTypeMeaning
schematext, requiredThe JSON Schema, as text.
jsontext, requiredThe JSON document to validate, as text.
check_formatstrue/falseCheck "format" values such as email, date and uri. Default true.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'validate_json_schema');
await document.modelContext.executeTool(tool, JSON.stringify({
  "schema": "{\"type\":\"object\",\"required\":[\"id\"],\"properties\":{\"id\":{\"type\":\"string\"}}}",
  "json": "{\"id\":5}"
}));
What it returns
{
 "ok": true,
 "valid": false,
 "draft": "not declared — using 2020-12 rules",
 "error_count": 1,
 "errors": [
  {
   "path": "$.id",
   "keyword": "type",
   "message": "Expected type string, got integer",
   "schema_path": "#/properties/id/type",
   "line": 2
  }
 ],
 "truncated": false
}

JSON Query (SQL on JSON)

1 tool: query_json_with_sql

Open tinypdf.in/json-query-local.html, then:

query_json_with_sql

Run a SQL SELECT query over a JSON document. An array of objects becomes a table named "data"; an object whose keys hold arrays of objects becomes one table per key. Nested values can be read with json_extract() and json_each(). Uses SQLite inside the page; nothing is uploaded.

InputTypeMeaning
jsontext, requiredThe JSON document, as text.
sqltext, requiredA SELECT (or WITH ... SELECT) query, for example: SELECT city, count(*) FROM data GROUP BY city
roottextOptional dotted path to the array to query, such as "order.items". It is then the table "data".

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'query_json_with_sql');
await document.modelContext.executeTool(tool, JSON.stringify({
  "json": "[{\"city\":\"Pune\",\"amount\":120},{\"city\":\"Delhi\",\"amount\":80},{\"city\":\"Pune\",\"amount\":60}]",
  "sql": "SELECT city, sum(amount) AS total FROM data GROUP BY city ORDER BY total DESC"
}));
What it returns
{
 "ok": true,
 "tables": [
  {
   "name": "data",
   "row_count": 3
  }
 ],
 "columns": [
  "city",
  "total"
 ],
 "row_count": 2,
 "rows": [
  [
   "Pune",
   180
  ],
  [
   "Delhi",
   80
  ]
 ]
}

Make JSON Safe for LLMs

2 tools: find_sensitive_values_in_json, make_json_safe

Open tinypdf.in/json-safe-for-llms.html, then:

find_sensitive_values_in_json

Scan a JSON document for values that look sensitive: names, emails, phone numbers, addresses, card and account numbers, government IDs, tokens and keys. Returns the JSON path and type of each one, without changing the document. Runs in the page; nothing is uploaded.

InputTypeMeaning
jsontext, requiredThe JSON document, as text.
protectiontextbasic: obvious identifiers only. standard (default): typical sensitive fields. aggressive: anything that could plausibly identify someone.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'find_sensitive_values_in_json');
await document.modelContext.executeTool(tool, JSON.stringify({
  "json": "{\"customer\":{\"name\":\"Priya Raman\",\"email\":\"priya@example.com\"},\"total\":499}"
}));
What it returns
{
 "ok": true,
 "protection": "standard",
 "count": 2,
 "found": [
  {
   "path": "$.customer.name",
   "type": "Person Name"
  },
  {
   "path": "$.customer.email",
   "type": "Email"
  }
 ]
}

make_json_safe

Replace sensitive values in a JSON document with stable placeholders such as PERSON_001 or EMAIL_001, keeping the structure, so the result can be shared or sent to a language model. Returns the safe JSON and the list of paths that were replaced. The table that maps placeholders back to the real values stays in the page for the user to download and is not returned. Runs in the page; nothing is uploaded.

InputTypeMeaning
jsontext, requiredThe JSON document, as text.
protectiontextbasic: obvious identifiers only. standard (default): typical sensitive fields. aggressive: anything that could plausibly identify someone.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'make_json_safe');
await document.modelContext.executeTool(tool, JSON.stringify({
  "json": "{\"customer\":{\"name\":\"Priya Raman\",\"email\":\"priya@example.com\"},\"total\":499}"
}));
What it returns
{
 "ok": true,
 "protection": "standard",
 "replaced": 2,
 "safe_json": "{\"customer\":{\"name\":\"PERSON_NAME_001\",\"email\":\"EMAIL_001\"},\"total\":499}",
 "changed": [
  {
   "path": "$.customer.name",
   "type": "Person Name"
  },
  {
   "path": "$.customer.email",
   "type": "Email"
  }
 ]
}

Sensitive Info Finder

1 tool: find_sensitive_info_in_text

Open tinypdf.in/pdf-pii.html, then: These tools work on text you pass in; an agent cannot read a PDF or photo.

find_sensitive_info_in_text

Scan text for values that are usually private: email addresses, phone numbers, US SSNs, card numbers (checked with the Luhn test), IP addresses, IBAN-style account numbers, long API-key-like strings, and Indian IDs (Aadhaar, PAN, GSTIN, IFSC, UPI). Returns each finding with its type, line and a short context. It is pattern matching, so treat it as a first pass, not a guarantee. Runs in the page; nothing is uploaded.

InputTypeMeaning
texttext, requiredThe text to scan.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'find_sensitive_info_in_text');
await document.modelContext.executeTool(tool, JSON.stringify({
  "text": "Mail asha@example.org, card 4111 1111 1111 1111"
}));
What it returns
{
 "ok": true,
 "count": 2,
 "by_type": {
  "Email": 1,
  "Card Number": 1
 },
 "found": [
  {
   "type": "email",
   "label": "Email",
   "line": 1,
   "value": "asha@example.org",
   "context": "Mail asha@example.org, card 4111 1111 111…"
  },
  {
   "type": "card",
   "label": "Card Number",
   "line": 1,
   "value": "4111 1111 1111 1111",
   "context": "…a@example.org, card 4111 1111 1111 1111"
  }
 ]
}

Mask Aadhaar & Indian IDs

2 tools: find_indian_ids, mask_indian_ids

Open tinypdf.in/mask-aadhaar.html, then: These tools work on text you pass in; an agent cannot read a PDF or photo.

find_indian_ids

Find Indian ID numbers in text: Aadhaar (and the 16-digit Aadhaar VID), PAN, GSTIN, IFSC codes and UPI IDs. Aadhaar and GSTIN are verified with their check digit, so ordinary 12-digit numbers are not reported. Returns each ID with its line and a masked form. Runs in the page; nothing is uploaded.

InputTypeMeaning
texttext, requiredThe text to scan.
typeslistWhich IDs to look for. Default: all of them.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'find_indian_ids');
await document.modelContext.executeTool(tool, JSON.stringify({
  "text": "Aadhaar 2345 6789 0124, IFSC HDFC0001234"
}));
What it returns
{
 "ok": true,
 "count": 2,
 "found": [
  {
   "type": "aadhaar",
   "label": "Aadhaar",
   "line": 1,
   "value": "2345 6789 0124",
   "masked": "XXXX XXXX 0124",
   "check_digit_valid": true
  },
  {
   "type": "ifsc",
   "label": "IFSC",
   "line": 1,
   "value": "HDFC0001234",
   "masked": "XXXXXXXXXXX"
  }
 ]
}

mask_indian_ids

Return the text with Indian ID numbers masked. Aadhaar keeps its last 4 digits (XXXX XXXX 1234), as recommended when sharing a copy; PAN, GSTIN and IFSC are fully masked; the name part of a UPI ID is masked. Runs in the page; nothing is uploaded.

InputTypeMeaning
texttext, requiredThe text to mask.
typeslistWhich IDs to look for. Default: all of them.
mask_whole_aadhaartrue/falseHide all 12 Aadhaar digits instead of keeping the last 4. Default false.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'mask_indian_ids');
await document.modelContext.executeTool(tool, JSON.stringify({
  "text": "Aadhaar 2345 6789 0124, IFSC HDFC0001234"
}));
What it returns
{
 "ok": true,
 "count": 2,
 "masked_text": "Aadhaar XXXX XXXX 0124, IFSC XXXXXXXXXXX",
 "masked": [
  {
   "type": "aadhaar",
   "line": 1,
   "masked": "XXXX XXXX 0124"
  },
  {
   "type": "ifsc",
   "line": 1,
   "masked": "XXXXXXXXXXX"
  }
 ]
}

PostgreSQL Playground

2 tools: run_postgres_sql, list_postgres_tables

Open tinypdf.in/postgresql-playground.html, then:

run_postgres_sql

Run one or more SQL statements in a PostgreSQL database that lives in this browser tab. Sample tables are ready: customers, products, orders and order_items. Returns the columns and up to 200 rows for each statement. Changes stay in the tab and reset on reload; nothing is uploaded.

InputTypeMeaning
sqltext, requiredSQL to run. Several statements may be separated by semicolons.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'run_postgres_sql');
await document.modelContext.executeTool(tool, JSON.stringify({
  "sql": "SELECT city, count(*) AS customers FROM customers GROUP BY city ORDER BY customers DESC LIMIT 3"
}));
What it returns
{
 "ok": true,
 "results": [
  {
   "statement": "SELECT city, count(*) AS customers FROM customers GROUP BY city ORDER BY customers DESC LIMIT 3",
   "columns": [
    "city",
    "customers"
   ],
   "row_count": 3,
   "rows": [
    [
     "Pune",
     2
    ],
    [
     "Mumbai",
     2
    ],
    "…"
   ],
   "ms": 6.4
  }
 ]
}

list_postgres_tables

List the tables and views in the in-browser PostgreSQL practice database, with their columns, types and row counts. Call this before writing a query.

No inputs.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'list_postgres_tables');
await document.modelContext.executeTool(tool, JSON.stringify({}));
What it returns
{
 "ok": true,
 "tables": [
  {
   "name": "customers",
   "kind": "table",
   "row_count": 8,
   "columns": [
    {
     "name": "id",
     "type": "INTEGER"
    },
    {
     "name": "name",
     "type": "TEXT"
    },
    "…"
   ]
  },
  {
   "name": "products",
   "kind": "table",
   "row_count": 8,
   "columns": [
    {
     "name": "id",
     "type": "INTEGER"
    },
    {
     "name": "name",
     "type": "TEXT"
    },
    "…"
   ]
  },
  "…"
 ]
}

SQLite Playground

2 tools: run_sqlite_sql, list_sqlite_tables

Open tinypdf.in/sqlite-playground.html, then:

run_sqlite_sql

Run one or more SQL statements in a SQLite database that lives in this browser tab. Sample tables are ready: customers, products, orders and order_items. Returns the columns and up to 200 rows for each statement. Changes stay in the tab and reset on reload; nothing is uploaded.

InputTypeMeaning
sqltext, requiredSQL to run. Several statements may be separated by semicolons.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'run_sqlite_sql');
await document.modelContext.executeTool(tool, JSON.stringify({
  "sql": "SELECT city, count(*) AS customers FROM customers GROUP BY city ORDER BY customers DESC LIMIT 3"
}));
What it returns
{
 "ok": true,
 "results": [
  {
   "statement": "SELECT city, count(*) AS customers FROM customers GROUP BY city ORDER BY customers DESC LIMIT 3",
   "columns": [
    "city",
    "customers"
   ],
   "row_count": 3,
   "rows": [
    [
     "Pune",
     2
    ],
    [
     "Mumbai",
     2
    ],
    "…"
   ],
   "ms": 11.9
  }
 ]
}

list_sqlite_tables

List the tables and views in the in-browser SQLite practice database, with their columns, types and row counts. Call this before writing a query.

No inputs.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'list_sqlite_tables');
await document.modelContext.executeTool(tool, JSON.stringify({}));
What it returns
{
 "ok": true,
 "tables": [
  {
   "name": "customers",
   "kind": "table",
   "row_count": 8,
   "columns": [
    {
     "name": "id",
     "type": "integer pk"
    },
    {
     "name": "name",
     "type": "text"
    },
    "…"
   ]
  },
  {
   "name": "order_items",
   "kind": "table",
   "row_count": 21,
   "columns": [
    {
     "name": "order_id",
     "type": "integer pk"
    },
    {
     "name": "product_id",
     "type": "integer pk"
    },
    "…"
   ]
  },
  "…"
 ]
}

DuckDB Playground

2 tools: run_duckdb_sql, list_duckdb_tables

Open tinypdf.in/duckdb-playground.html, then:

run_duckdb_sql

Run one or more SQL statements in a DuckDB database that lives in this browser tab. Sample tables are ready: customers, products, orders and order_items. Returns the columns and up to 200 rows for each statement. Changes stay in the tab and reset on reload; nothing is uploaded.

InputTypeMeaning
sqltext, requiredSQL to run. Several statements may be separated by semicolons.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'run_duckdb_sql');
await document.modelContext.executeTool(tool, JSON.stringify({
  "sql": "SELECT 42 AS answer"
}));
What it returns
{
 "ok": true,
 "results": [
  {
   "statement": "SELECT 42 AS answer",
   "columns": [
    "answer"
   ],
   "row_count": 1,
   "rows": [
    [
     42
    ]
   ],
   "ms": 6
  }
 ]
}

list_duckdb_tables

List the tables and views in the in-browser DuckDB practice database, with their columns, types and row counts. Call this before writing a query.

No inputs.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'list_duckdb_tables');
await document.modelContext.executeTool(tool, JSON.stringify({}));
What it returns
{
 "ok": true,
 "tables": [
  {
   "name": "customers",
   "kind": "table",
   "row_count": 8,
   "columns": [
    {
     "name": "id",
     "type": "integer"
    },
    {
     "name": "name",
     "type": "varchar"
    },
    "…"
   ]
  },
  {
   "name": "products",
   "kind": "table",
   "row_count": 8,
   "columns": [
    {
     "name": "id",
     "type": "integer"
    },
    {
     "name": "name",
     "type": "varchar"
    },
    "…"
   ]
  },
  "…"
 ]
}

Mongo Query Playground

2 tools: run_mongo_query, list_mongo_collections

Open tinypdf.in/mongo-playground.html, then:

run_mongo_query

Run MongoDB shell statements (find, aggregate, insertOne, updateMany and so on) against sample collections in an in-browser practice database. Sample collections are ready: customers, products and orders. Returns the documents as JSON text. Data lives only in this browser tab and resets on reload; nothing is uploaded.

InputTypeMeaning
querytext, requiredMongo shell statements, for example db.orders.find({ status: "shipped" }).limit(5)

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'run_mongo_query');
await document.modelContext.executeTool(tool, JSON.stringify({
  "query": "db.customers.find({}).limit(2)"
}));
What it returns
{
 "ok": true,
 "results": [
  {
   "statement": "db.customers.find({}).limit(2)",
   "result": "[\n  {\n    _id: 1,\n    name: 'Asha Rao',\n    city: 'Pune',\n    email: 'asha@example.com',\n    joined: ISODate('2025-01-14T00:00:00.000Z')\n  },\n  {\n    _id: 2,\n  …",
   "count": 2,
   "ms": 0.4
  }
 ]
}

list_mongo_collections

List the collections in the in-browser MongoDB-style shell practice database, with their fields and row counts. Call this before writing a query.

No inputs.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'list_mongo_collections');
await document.modelContext.executeTool(tool, JSON.stringify({}));
What it returns
{
 "ok": true,
 "collections": [
  {
   "name": "customers",
   "kind": "collection",
   "row_count": 8,
   "columns": [
    {
     "name": "_id",
     "type": "int"
    },
    {
     "name": "name",
     "type": "string"
    },
    "…"
   ]
  },
  {
   "name": "orders",
   "kind": "collection",
   "row_count": 12,
   "columns": [
    {
     "name": "_id",
     "type": "int"
    },
    {
     "name": "customerId",
     "type": "int"
    },
    "…"
   ]
  },
  "…"
 ]
}

PostgreSQL Query Performance Lab

4 tools: analyze_postgres_query, compare_postgres_index_choices, compare_vector_index_choices, list_postgres_tables_and_indexes

Open tinypdf.in/postgresql-query-performance.html, then:

analyze_postgres_query

Run a SQL query in an in-browser PostgreSQL practice database with EXPLAIN ANALYZE and return how it ran: a one-line verdict, query time, rows read, rows returned, pages touched, each plan step in plain words, and suggested indexes. CREATE INDEX, DROP INDEX and ALTER TABLE statements may be placed before the query to change indexes first. INSERT, UPDATE and DELETE are measured and rolled back. The practice database has two tables: orders (id, customer_id, status, amount, created_at, note, tags text[]) and customers (id, name, email, city, signed_up). It runs in this browser tab; nothing is uploaded.

InputTypeMeaning
sqltext, requiredSQL to run. Several statements may be separated by semicolons.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'analyze_postgres_query');
await document.modelContext.executeTool(tool, JSON.stringify({
  "sql": "SELECT * FROM orders WHERE customer_id = 742"
}));
What it returns
{
 "ok": true,
 "results": [
  {
   "statement": "SELECT * FROM orders WHERE customer_id = 742",
   "verdict": "Full table scan: read 50,000 rows of orders to return 10.",
   "query_ms": 2.246,
   "rows_read": 50000,
   "rows_returned": 10,
   "pages_touched": 650,
   "steps": [
    {
     "depth": 0,
     "step": "Full table scan · orders",
     "detail": "Read all 50,000 rows and threw away 49,990 that did not match (customer_id = 742).",
     "ms": 2.233,
     "share_of_time_pct": 100,
     "rows_out": 10,
     "rows_read": 50000,
     "slow": true
    }
   ],
   "suggestions": [
    {
     "why": "Postgres read 50,000 rows of orders to keep 10. An index on customer_id lets it go straight to them.",
     "sql": "CREATE INDEX idx_orders_customer_id ON orders (customer_id);"
    }
   ]
  }
 ]
}

compare_postgres_index_choices

For one SELECT query, try several candidate indexes one at a time (B-tree, hash, BRIN, GIN, multi-column in both orders, partial, expression, primary key) in an in-browser PostgreSQL practice database and return, for each, whether PostgreSQL used it, the query time, rows read, pages touched, index size and build time. Nothing is kept: every candidate is removed after it is measured. The practice database has two tables: orders (id, customer_id, status, amount, created_at, note, tags text[]) and customers (id, name, email, city, signed_up). It runs in this browser tab; nothing is uploaded.

InputTypeMeaning
sqltext, requiredOne SELECT query.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'compare_postgres_index_choices');
await document.modelContext.executeTool(tool, JSON.stringify({
  "sql": "SELECT * FROM orders WHERE customer_id = 742"
}));
What it returns
{
 "ok": true,
 "fastest": "Hash (customer_id)",
 "choices": [
  {
   "choice": "No index",
   "query_ms": 2.141,
   "rows_read": 50000,
   "pages_touched": 650
  },
  {
   "choice": "B-tree (customer_id)",
   "sql": "CREATE INDEX idx_orders_customer_id ON orders (customer_id);",
   "used_by_query": true,
   "query_ms": 0.036,
   "rows_read": 10,
   "pages_touched": 12,
   "index_size_bytes": 491520,
   "build_ms": 24.5
  },
  "…"
 ]
}

compare_vector_index_choices

For one pgvector nearest-neighbour query (ORDER BY embedding <=> / <-> / <#> a vector, LIMIT k) on the lab's synthetic vector table items (384-dimension vectors, question vectors in the table queries), try an exact scan, HNSW and IVFFlat indexes with different settings and operator classes, and return for each the query time, whether the index was used, recall against the exact answer, index size and build time, plus a sweep of hnsw.ef_search and ivfflat.probes. Nothing is kept. The practice database has two tables: orders (id, customer_id, status, amount, created_at, note, tags text[]) and customers (id, name, email, city, signed_up). It runs in this browser tab; nothing is uploaded.

InputTypeMeaning
sqltext, requiredOne SELECT with ORDER BY a vector distance and LIMIT. Example: SELECT id FROM items ORDER BY embedding <=> (SELECT embedding FROM queries WHERE id = 1) LIMIT 10

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'compare_vector_index_choices');
await document.modelContext.executeTool(tool, JSON.stringify({
  "sql": "SELECT id FROM items ORDER BY embedding <=> (SELECT embedding FROM queries WHERE id = 1) LIMIT 10"
}));
What it returns
{
 "ok": true,
 "vectors": 5000,
 "fastest_with_90pct_recall": "HNSW, smaller graph (m 8, ef_construction 32)",
 "choices": [
  {
   "choice": "No index",
   "query_ms": 5.293,
   "recall": 1,
   "rows_returned": 10
  },
  {
   "choice": "HNSW (m 16, ef_construction 64)",
   "sql": "CREATE INDEX cand_hnsw ON items USING hnsw (embedding vector_cosine_ops)",
   "used_by_query": true,
   "query_ms": 1.24,
   "recall": 0.9,
   "rows_returned": 10,
   "index_size_bytes": 10248192,
   "build_ms": 2455.4
  },
  "…"
 ],
 "speed_vs_recall": [
  {
   "index": "HNSW (m 16, ef_construction 64)",
   "setting": "hnsw.ef_search = 10",
   "index_used": true,
   "query_ms": 0.556,
   "recall": 0.8,
   "rows_returned": 10
  },
  {
   "index": "HNSW (m 16, ef_construction 64)",
   "setting": "hnsw.ef_search = 20",
   "index_used": true,
   "query_ms": 0.716,
   "recall": 0.9,
   "rows_returned": 10
  },
  "…"
 ]
}

list_postgres_tables_and_indexes

List the tables in the in-browser PostgreSQL practice database with their columns, row counts, sizes, and the indexes and primary keys that exist right now.

No inputs.

Example:

const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'list_postgres_tables_and_indexes');
await document.modelContext.executeTool(tool, JSON.stringify({}));
What it returns
{
 "ok": true,
 "tables": [
  {
   "name": "orders",
   "row_count": 50000,
   "size_bytes": 5365760,
   "columns": [
    {
     "name": "id",
     "type": "integer"
    },
    {
     "name": "customer_id",
     "type": "integer"
    },
    "…"
   ],
   "indexes": []
  },
  {
   "name": "customers",
   "row_count": 5000,
   "size_bytes": 458752,
   "columns": [
    {
     "name": "id",
     "type": "integer"
    },
    {
     "name": "name",
     "type": "text"
    },
    "…"
   ],
   "indexes": []
  }
 ]
}

How a page registers a WebMCP tool

A tool is a name, a description, an input schema and a function. This is the whole pattern:

document.modelContext.registerTool({
  name: 'add_numbers',
  description: 'Add two numbers and return the sum.',
  inputSchema: {
    type: 'object',
    properties: { a: { type: 'number' }, b: { type: 'number' } },
    required: ['a', 'b']
  },
  execute: async ({ a, b }) => JSON.stringify({ sum: a + b })
});

The page adds a WebMCP origin-trial token, then calls document.modelContext.registerTool() once for each tool with a name, a description, an input schema and a function. On TinyPDF.in each function passes the inputs to the code that already powers that page and returns the result as JSON text. In browsers without WebMCP the registration is skipped and the page works as before.

WebMCP limits today

  • WebMCP is an origin trial in Chrome, not a finished standard. Details may change, and the trial currently ends on 30 March 2027.
  • The tools exist only while the page is open in a tab. There is no server or API behind them.
  • Results are capped (for example 200 rows from a query) so that a reply stays small enough for an agent to read.
  • The PDF and image tools need a file you choose yourself, and the AI text tools use a server, so they are not exposed.

WebMCP questions

What is WebMCP?

WebMCP lets a web page tell the browser which actions it offers, as named tools with a description and a list of inputs. An AI agent running in the browser can then call a tool directly instead of clicking through the page. It is being tested in Chrome as an origin trial and is not yet a web standard.

Is WebMCP the same as MCP?

No. MCP (Model Context Protocol) connects an AI agent to tools that run on a server or on your computer. WebMCP is for web pages: the page itself offers tools to an agent that is running inside the browser, using the page that is already open.

What is document.modelContext?

It is the browser object a page uses for WebMCP. A page calls document.modelContext.registerTool() to offer a tool. In the console you can call getTools() to list the tools on the current page and executeTool() to run one.

How do I test WebMCP without an AI agent?

Open a page that registers tools in Chrome 149 or later, open the developer console, list the tools with document.modelContext.getTools(), and run one with executeTool(). This page has a working example for 20 tools, and a button that runs four of them for you.

Which TinyPDF.in tools support it?

Every tool that works on text and runs fully in the browser: JSON Compare, JSON Query, Make JSON Safe for LLMs, Sensitive Info Finder, Mask Aadhaar, the PostgreSQL, SQLite, DuckDB and Mongo playgrounds, and the PostgreSQL Query Performance Lab. The full list is on this page.

Why are the PDF and image tools not included?

They need a file that you choose from your device, which an agent cannot hand to a page. Tools that send data to a server, such as the AI text detector, are left out on purpose so that an agent cannot run them without you.

Is my data sent to a server when an agent calls a tool?

No. Each tool runs the same in-page code as the buttons on that page. The data stays in your browser tab. What the agent does with the result is up to the agent.

Which browsers work?

Chrome 149 or later on desktop, while the origin trial runs (currently until 30 March 2027). In other browsers the page works as usual and simply does not register any tools.

Do I need an account or an API key?

No. There is nothing to sign up for and no key. A page's tools exist only while that page is open in your tab.

Can I call these tools from Python?

Yes. With the bridge extension on and a TinyPDF.in tool page open, a short script using the mcp Python package can list the page's tools and call them. This page has a working script under “Connect a desktop agent”.

Can a desktop agent such as Claude Code or Cursor call these tools?

Yes, through a bridge. A desktop agent cannot see a browser tab by itself, so a small browser extension and a local helper pass the tools from your open tab to the agent. The steps are on this page under “Connect a desktop agent”.

I get "Failed to parse input arguments". What is wrong?

Pass the arguments as JSON text, not as an object: wrap them in JSON.stringify(...), as in the example on this page.