# Nullprint (pypi · nullprint-mcp)

Create, launch, check and drive Nullprint multi-profile browser profiles from any MCP agent.

- Trust score: 55/100 (low)
- Change this week: −11
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-10-23

## Components

- npm · `nullprint-mcp`: 58/100, [markdown](https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp.md), [page](https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp)
- pypi · `nullprint-mcp`: 55/100 (this document), [markdown](https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp-2.md), [page](https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp-2)

## Channel facts

- Registry: `pypi`
- Package: `nullprint-mcp`
- Version: `0.1.5`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-10-23.

- **Supply Chain Security**: 50/100
  - Malware scan not yet available for this package.
  - No known CVEs affecting this package version or its production dependencies.
  - Runs hatchling.build at install time, a recognised build step with no custom scripting around it.
  - 0 of 28 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 45/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 0 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 68/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 3957 tokens (~208/item across 19 items; 19 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 30/100
  - Stability observed for 9 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 71/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 0% of tool parameters carry a description.
  - Structured output schemas are declared (11% of tools); any adoption earns full credit.
- **Tool Safety**: 75/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - 0 of 2 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "profile_delete" implies "delete" and declares no destructiveHint at all, which the MCP spec reads as destructive by default.
  - An AI judge read all 20 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### How do I install the Nullprint MCP server?

Nullprint runs locally as a PyPI package, launched with uvx nullprint-mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add nullprintai-nullprint-mcp -- uvx nullprint-mcp
```

### Cursor

```json
{
  "mcpServers": {
    "nullprintai-nullprint-mcp": {
      "command": "uvx",
      "args": [
        "nullprint-mcp"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "nullprintai-nullprint-mcp": {
      "command": "uvx",
      "args": [
        "nullprint-mcp"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add nullprintai-nullprint-mcp -- uvx nullprint-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "nullprintai-nullprint-mcp": {
      "type": "local",
      "command": [
        "uvx",
        "nullprint-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add nullprintai-nullprint-mcp --command uvx --arg nullprint-mcp
```

### Hermes

```yaml
mcp_servers:
  nullprintai-nullprint-mcp:
    command: "uvx"
    args: ["nullprint-mcp"]
```

### Netclaw

```json
{
  "McpServers": {
    "nullprintai-nullprint-mcp": {
      "Transport": "stdio",
      "Command": "uvx",
      "Arguments": [
        "nullprint-mcp"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add nullprintai-nullprint-mcp -t stdio -c uvx -a nullprint-mcp
```

### Other

```json
{
  "mcpServers": {
    "nullprintai-nullprint-mcp": {
      "command": "uvx",
      "args": [
        "nullprint-mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-10-23 (score 55, −11)

- [security regression] Malware scan: pass → unverified
- [functional regression] Schema quality: 3384 → 3957
- [functional improvement] Stability: unverified → 0.30
- [functional] Package version: 0.1.2 → 0.1.5

### 2026-10-14 (score 66, +42)

- [security improvement] Injection markers: unverified → pass
- [security improvement] Malware scan: unverified → pass
- [security] First check of Judged manipulation: pass
- [security] Stability: Stability not yet verified: not enough scan history yet (needs a 30-day window).
- [functional improvement] MCP protocol: unverified → pass
- [functional improvement] Tool coverage: unverified → 100
- [functional] First check of Tool coverage: 0
- [functional] First check of Schema quality: fail
- [functional] First check of Tool coverage: 12
- [functional] First check of Schema quality: excellent
- [functional] First check of Schema quality: fail
- [functional] First check of Destructive annotations: 0

### 2026-10-13 (score 24, −15)

- [security regression] Malware scan: pass → unverified

### 2026-10-12 (score 39)

First indexed and scored.

## MCP tools (19)

### `profiles_list` (~104 tokens)

List every identity in this installation, with its live status.

Each entry carries: name, os, engine (which browser core backs it),
created, proxy (the exit server only — never credentials), status
("running" when a browser is open for it, otherwise "idle"), and meta
(the local ledger: group, tags, note, last_launched).

Call this first to find the exact name to pass to profile_start /
profile_stop; names are case-sensitive.

Output parameters:

- `result` (array)

### `profile_start` (~372 tokens)

Open a browser session for an identity and return the endpoint to drive it.

The identity keeps its frozen fingerprint, its exit proxy, and its logged-in
cookies — starting it does not re-randomize anything. Starting an identity
that already has a session open is a no-op that returns the existing one.

Args:
    name: identity name, exactly as profiles_list reports it.
    headless: run without a visible window. Default False (headed), which
        is what sites expect and what you want for anything login-shaped.

Returns the session record: name, opened_at, idle_seconds, url (the page it
is on), plus cdp_endpoint — a ws:// DevTools address you can attach your own
automation to. It is only published for Chrome-core identities that have
external debugging turned on in the app (off by default; Edit -> Advanced ->
External debugging, needs a second verification, then reopen the identity).
Otherwise it is None, cdp_unavailable gives the daemon's reason code
(external_debug_off, external_debug_not_granted, not_supported_for_engine,
starting) and cdp_note says what to do — the session commands work either way.

Prefer the daemon's own commands over attaching to cdp_endpoint. A session
driven from outside bypasses Nullprint's consistency guards, so an outside
tool can set a viewport, locale, timezone, or header that contradicts the
identity's frozen fingerprint and make it detectable. Attach at your own
risk, and change nothing the identity did not already claim.

Fails with a readable message if the identity does not exist (404), if too
many sessions are already warm (429), or if the daemon is not running.

Input parameters:

- `headless` (boolean)
- `name` (string, required)

### `profile_stop` (~224 tokens)

Stop an identity's browser, however it was started.

Cookies and login state live in the identity's own storage and survive the
stop; whatever was typed into an open form does not. Stop identities you are
done with — a running identity holds a warm browser and keeps its login
state exposed.

Two things can be running for one identity and this closes either: the warm
session profile_start opens (the daemon drives it), and a window the desktop
app launched on its own. The session is closed first; if there is none, the
identity-level stop is used instead, which also terminates a launched window
and is a no-op on an identity that is already idle.

Args:
    name: identity name, exactly as profiles_list reports it.

Returns {name, stopped, closed}: `stopped` is False when the identity was
already idle, and `closed` lists what was actually shut down.

Fails with a readable message if the identity does not exist, or if the
daemon is not running.

Input parameters:

- `name` (string, required)

### `profile_create` (~463 tokens)

Create a new identity. Its fingerprint is generated and frozen on the spot.

You do not choose the fingerprint — the point of the product is that every
identity gets one self-consistent, frozen set of traits that stays
byte-identical across restarts. Do not create identities speculatively: they
count against the installation's quota.

Args:
    name: the new identity's name. Must be unique — creating a name that
        already exists fails rather than overwriting it.
    engine: which browser core backs the identity. Leave unset to use this
        installation's default; an unknown value is rejected with the list
        of accepted ones. The `engine` field in profiles_list shows what
        existing identities use.
    proxy: exit proxy URL, e.g. "http://user:pass@host:port" or
        "socks5://host:port" — any vendor, bring your own. Omit it and the
        identity runs on a direct connection. Test a URL with proxy_check
        before binding it here.
    group, tags, note: the local ledger — which account, email, or purpose
        this identity is bound to. Free text, searchable in the app, and
        returned under `meta` by profiles_list.
    urls: startup tabs — pages opened automatically when the identity starts.
    force_dark: personal display preference, OFF by default. True inverts
        web pages that have no dark theme of their own (Google Ads and the
        like) to dark; sites may notice the changed colors, so use it only
        on identities the user manages for themselves, never on accounts
        being built up. Leave unset to keep pages exactly as the sites
        render them. Chrome-engine identities only.

Returns {name, os, created}. Fails with a readable message if the name is
taken (409), if the quota is reached or the installation is unauthorized
(403), or if an argument is rejected (422).

Input parameters:

- `engine`
- `force_dark`
- `group`
- `name` (string, required)
- `note`
- `proxy`
- `tags`
- `urls`

### `profile_settings` (~164 tokens)

Change a display setting of an existing identity. Only the settings you
pass are changed; everything else in its launch settings stays as it is.

Args:
    name: identity name, exactly as profiles_list reports it.
    force_dark: True = force dark web pages (inverts sites that have no dark
        theme; the identity's color scheme then reads as dark too). False =
        off, pages render exactly as the sites intend (the default). Sites
        may notice forced dark colors — reserve it for identities the user
        manages for themselves. Chrome-engine identities only. Takes effect
        the next time the identity starts.

Returns {name, launch} with the launch settings now stored.

Input parameters:

- `force_dark`
- `name` (string, required)

### `profile_checkup` (~291 tokens)

Run the consistency checkup on an identity — is its fingerprint coherent?

This is the check competitors cannot make: every trait has one correct value
given the identity's OS, browser core, and exit IP, so each item gets a
verdict rather than just being displayed. Run it after changing an
identity's proxy, and before trusting an identity with anything that matters.

Args:
    name: identity name, exactly as profiles_list reports it.
    live: False (default) derives everything from the stored record — fast,
        no side effects. True additionally STARTS THE BROWSER to measure the
        real TLS/JA3 fingerprint and User-Agent against the browser core's
        baseline. That takes tens of seconds and opens a window, so use it
        when you actually need measured proof, not for a routine look.

Returns {name, engine, live, summary, checks}. `summary` is "pass", "warn",
or "fail" — the worst verdict among the checks. Each entry in `checks` has
an id, a label, a verdict ("pass"/"warn"/"fail"/"skip"), and a detail
string explaining it. "skip" means the check needs live=True, or does not
apply (a direct-connection identity has no exit geography to align).

Input parameters:

- `live` (boolean)
- `name` (string, required)

### `proxy_check` (~220 tokens)

Test a proxy URL and report where it actually comes out. No identity needed.

Use it before binding a proxy to an identity with profile_create: a dead or
slow exit found here costs nothing, the same exit found halfway through a
signup costs the account.

Args:
    url: the proxy URL to test, e.g. "http://user:pass@host:port" or
        "socks5://host:port".

Returns {ok: true, exit_ip, country, latency_ms} when the proxy works, or
{ok: false, error} when it does not. A dead proxy is an ANSWER, not a
failure — check the `ok` field rather than assuming success. Note that
\`country` can be null even on a working proxy if the exit IP cannot be
geolocated.

This tests a bare URL. To test the proxy an identity is already bound to,
the daemon's per-identity check is a separate endpoint not yet exposed here.

Input parameters:

- `url` (string, required)

### `profile_delete` (~241 tokens)

Move an identity to the recycle bin. Recoverable — this is not erasure.

The identity's browser storage, cookies and logged-in sessions go to the
recycle bin intact and profile_restore brings them back unchanged. It stops
counting against the installation's quota while it sits there, and it no
longer appears in profiles_list.

A running identity is refused (409) rather than killed under itself — stop
it with profile_stop first, then delete. Deleting a name that is already in
the recycle bin replaces the older copy, so the older one is lost.

There is deliberately NO tool here for permanent deletion. Emptying the
recycle bin destroys logged-in accounts with no way back, so it is a manual
action in the desktop app only — a person does it, looking at what they are
about to lose. Do not look for a way around this; tell the human instead.

Args:
    name: identity name, exactly as profiles_list reports it.

Fails with a readable message if the identity does not exist (404), or if it
is still running (409).

Input parameters:

- `name` (string, required)

### `trash_list` (~123 tokens)

List the identities in the recycle bin — what is still recoverable.

Read-only. This is how you find out what can be restored: deleted
identities do not appear in profiles_list, so without this you could only
restore a name you already knew. Pair it with profile_restore.

Each entry carries name, engine, os, created, and deleted_at. Newest
deletion first. An empty list means the recycle bin is empty.

Nothing here is gone yet — everything listed still has its cookies and
login state and comes back intact via profile_restore.

Output parameters:

- `result` (array)

### `profile_restore` (~130 tokens)

Bring an identity back from the recycle bin, exactly as it was.

Its frozen fingerprint, proxy binding, cookies and login state all come
back — restoring is not re-creating, so nothing is regenerated and no
logged-in account is lost.

Args:
    name: the trashed identity's name.

Returns {name, engine, created}. Fails with a readable message if nothing by
that name is in the recycle bin (404), or if a live identity has taken the
name since (409) — rename or delete that one first, then restore.

Input parameters:

- `name` (string, required)

### `session_goto` (~134 tokens)

Navigate a session's current page to a URL.

Open the identity's session with profile_start first — this drives an
already-open session, it does not open one itself.

Args:
    name: identity name, exactly as profiles_list reports it.
    url: the URL to navigate to, e.g. "https://example.com/login".

Returns {url}: the page's URL after navigation. Fails with a readable
message if the session is not open (409 — call profile_start first) or
the identity does not exist (404).

Input parameters:

- `name` (string, required)
- `url` (string, required)

### `session_snapshot` (~134 tokens)

Take an accessibility-tree snapshot of the session's current page.

This is the primary way to see what is on a page before acting on it —
read it to find the roles/names/selectors to pass to session_click_selector
or session_fill_selector. Requires a session opened with profile_start first.

Args:
    name: identity name, exactly as profiles_list reports it.

Returns {snapshot}: the page's accessibility tree as text. Fails with a
readable message if the session is not open (409 — call profile_start
first) or the identity does not exist (404).

Input parameters:

- `name` (string, required)

### `session_click_selector` (~163 tokens)

Click an element on the session's current page, located by a CSS selector.

Requires a session opened with profile_start first. `selector` is a CSS
selector (e.g. "button#submit", "a.nav-link") — not an XPath, not a role/name
pair (use session_snapshot to find one).

Args:
    name: identity name, exactly as profiles_list reports it.
    selector: CSS selector for the element to click.

Returns {ok: true}. Fails with a readable message if the session is not
open (409 — call profile_start first), the identity does not exist (404),
or the selector never matches / the click times out (502).

Input parameters:

- `name` (string, required)
- `selector` (string, required)

### `session_fill_selector` (~182 tokens)

Fill a text field on the session's current page, located by a CSS selector.

Uses Playwright's fill(), which drives React/Vue-controlled inputs correctly
(setting the DOM value directly does not). Requires a session opened with
profile_start first. `selector` is a CSS selector, not an XPath.

Args:
    name: identity name, exactly as profiles_list reports it.
    selector: CSS selector for the input to fill.
    text: the text to put in the field, replacing whatever was there.

Returns {ok: true}. Fails with a readable message if the session is not
open (409 — call profile_start first), the identity does not exist (404),
or the selector never matches / the fill times out (502).

Input parameters:

- `name` (string, required)
- `selector` (string, required)
- `text` (string, required)

### `session_press` (~132 tokens)

Press a keyboard key on the session's current page (whatever is focused).

Requires a session opened with profile_start first. Use Playwright key
names, e.g. "Enter", "Tab", "Escape", "Control+A".

Args:
    name: identity name, exactly as profiles_list reports it.
    key: the key (or chord) to press.

Returns {ok: true}. Fails with a readable message if the session is not
open (409 — call profile_start first) or the identity does not exist (404).

Input parameters:

- `key` (string, required)
- `name` (string, required)

### `session_eval` (~234 tokens)

Evaluate a JavaScript expression on the session's current page.

Requires a session opened with profile_start first. A page-JS error is
data to read, not a tool failure: the daemon returns {"ok": false,
"error", "error_type"} instead of raising when the expression throws.

Args:
    name: identity name, exactly as profiles_list reports it.
    expr: the JavaScript expression to evaluate in the page, e.g.
        "document.title" or "document.querySelectorAll('a').length".

Returns the {ok, result} envelope: {"ok": true, "result": <value>} on
success. `result` is whatever the expression evaluates to as JSON —
string, number, boolean, object, array, or null — not necessarily a
string; do not assume its type. Fails with a readable message (raises,
rather than the {ok: false} envelope above) if the session is not open
(409 — call profile_start first) or the identity does not exist (404).

Input parameters:

- `expr` (string, required)
- `name` (string, required)

### `session_tabs` (~130 tokens)

List the session's open tabs, with the currently active one marked.

Requires a session opened with profile_start first. Use this to find the
index to pass to session_switch_tab.

Args:
    name: identity name, exactly as profiles_list reports it.

Returns {tabs, active}: `tabs` is a list of {index, url}, `active` is the
index of the tab currently in front. Fails with a readable message if the
session is not open (409 — call profile_start first) or the identity does
not exist (404).

Input parameters:

- `name` (string, required)

### `session_switch_tab` (~157 tokens)

Bring one of the session's open tabs to the front and make it the active one.

Requires a session opened with profile_start first. Get `index` from
session_tabs — indices are only valid as of that snapshot, a tab opened or
closed since shifts them.

Args:
    name: identity name, exactly as profiles_list reports it.
    index: the tab's index, from session_tabs.

Returns {index, url} for the now-active tab. Fails with a readable message
if the session is not open (409 — call profile_start first), the identity
does not exist (404), or the index is out of range (422).

Input parameters:

- `index` (integer, required)
- `name` (string, required)

### `session_dialog` (~251 tokens)

Answer the JavaScript dialog (alert, confirm, prompt, beforeunload) open on the session's page.

Dialogs are left open, as in a normal browser: while one is open the page
is blocked and the other session_* tools fail with 409 "a JavaScript
\<type> is open on the page (<message>)". Call this to answer it, then
retry. A beforeunload that your own session_goto (or click) causes is
accepted automatically and never shows up here.

Args:
    name: identity name, exactly as profiles_list reports it.
    action: "accept" (OK / Leave) or "dismiss" (Cancel / Stay).
    prompt_text: for a prompt() dialog, the text to enter before
        accepting; omit it to accept the prompt's default value.

Returns {ok, action, type, message} for the dialog that was answered.
Fails with a readable message if no dialog is open (409), the session
is not open (409 — call profile_start first) or the identity does not
exist (404).

Input parameters:

- `action` (string, required)
- `name` (string, required)
- `prompt_text`

## Diagnostics

Captured diagnostic sections: Provenance, Install scripts, Dependencies. The full working is on the page: https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp-2#diagnostics

## Score history

- 2026-10-23: 55
- 2026-10-22: 66
- 2026-10-21: 66
- 2026-10-20: 66
- 2026-10-19: 66
- 2026-10-18: 66
- 2026-10-17: 66
- 2026-10-16: 66
- 2026-10-15: 66
- 2026-10-14: 66
- 2026-10-13: 24
- 2026-10-12: 39

## Common questions

### What is the Nullprint MCP server?

Nullprint is an MCP server listed in the public MCP registry as io.github.nullprintai/nullprint-mcp. Create, launch, check and drive Nullprint multi-profile browser profiles from any MCP agent. This page covers its PyPI package (nullprint-mcp).

### Is the Nullprint MCP server safe to use?

Nullprint scores 55 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 23 October 2026. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the Nullprint MCP server expose?

Nullprint exposes 19 tools: profiles_list, profile_start, profile_stop, profile_create, profile_settings, and 14 more. Their descriptions and schemas cost roughly 3,849 tokens of context every time the server is loaded.

### Is the Nullprint MCP server still maintained?

Nullprint is still listed as active in the MCP registry. We last reached this channel on 23 October 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the Nullprint MCP server under?

Nullprint declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- PyPI project: https://pypi.org/project/nullprint-mcp/
- Socket report: https://socket.dev/pypi/package/nullprint-mcp
- Repository: https://github.com/nullprintai/nullprint-mcp
- Website: https://nullprint.ai/docs/mcp/
- Changelog RSS feed: https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp-2.xml
- Changelog JSON feed: https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp-2.json
- HTML version of this page: https://verifymcp.io/servers/nullprintai-nullprint-mcp/nullprint-mcp-2
