MCP server
HTMLvault exposes a hosted Model Context Protocol (MCP) server so AI agents can publish and manage secure links directly — no curl, no glue code. The endpoint is Streamable HTTP at https://htmlvault.dev/api/mcp and is available to any Pro or Enterprise account with an API key.
| Endpoint | https://htmlvault.dev/api/mcp |
| Transport | Streamable HTTP (JSON-RPC 2.0 over a single POST) |
| Auth | Authorization: Bearer <key> — Pro or Enterprise API key |
| Tools | list_links · get_link · get_analytics · create_link · update_link · delete_link · scan_html |
Quick start
Generate an API key on the Integrations screen, then connect your client. Replace htmlvault_YOUR_KEY with your full key in every snippet below.
Claude Code
Run once in your terminal — HTMLvault is then available in every Claude Code session.
claude mcp add --transport http htmlvault https://htmlvault.dev/api/mcp \ --header "Authorization: Bearer htmlvault_YOUR_KEY"
Claude Desktop / Claude.ai (custom connector)
Add a custom connector pointing at the endpoint, or use the mcp-remote bridge in claude_desktop_config.json:
claude_desktop_config.json
{
"mcpServers": {
"htmlvault": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://htmlvault.dev/api/mcp",
"--header", "Authorization: Bearer htmlvault_YOUR_KEY"]
}
}
}Cursor
Add to ~/.cursor/mcp.json (or the project-level .cursor/mcp.json):
~/.cursor/mcp.json
{
"mcpServers": {
"htmlvault": {
"url": "https://htmlvault.dev/api/mcp",
"headers": { "Authorization": "Bearer htmlvault_YOUR_KEY" }
}
}
}Anthropic API (MCP connector)
Pass the endpoint in the mcp_servers array of a Messages API request (requires the MCP connector beta header):
{
"model": "claude-sonnet-4-6",
"mcp_servers": [
{
"type": "url",
"url": "https://htmlvault.dev/api/mcp",
"name": "htmlvault",
"authorization_token": "htmlvault_YOUR_KEY"
}
],
"messages": [{ "role": "user", "content": "Vault this report and give me the link." }]
}ChatGPT
ChatGPT connects to HTMLvault through a Custom GPT Action over the REST API rather than MCP. The import-ready OpenAPI schema and step-by-step setup live on the Integrations screen.
Authentication
Every request carries a bearer token in the Authorization header. Keys are created and revoked on the Integrations screen and are shown once at generation time.
Authorization: Bearer htmlvault_YOUR_KEY
Personal vs. org keys
Personal keys require a Pro or Enterprise plan and act in the key owner’s own workspace.
Org keys (Enterprise) act on behalf of the organization owner’sworkspace — every tool call is attributed to the owner’s account, and links belonging to individual members (under their personal accounts) are notvisible through an org key. If you list or fetch links with an org key you see the owner’s links, not the whole organization’s.
Auth errors
| Field | Type | Description |
|---|---|---|
missing_key | 401 | No Authorization header (or empty bearer). Message: “No API key provided. Include the header: Authorization: Bearer <key>. Create a key at https://htmlvault.dev/connect” |
invalid_key | 401 | Key is not recognized or has been revoked. Message: “API key not recognized or has been revoked. Manage your keys at https://htmlvault.dev/connect” |
plan_required | 403 | Key is valid but the account is on the Free plan. Message: “MCP access requires a Pro or Enterprise plan. Upgrade at https://htmlvault.dev/pricing” |
The 401 cases share a shape ({ "error": "...", "message": "..." }); the free-plan case is a distinct 403so a client can tell “wrong key” from “upgrade needed.”
Tool reference
Seven tools. Read-only tools (list_links, get_link, get_analytics, scan_html) are annotated readOnlyHint: true; delete_link is annotated destructiveHint: true. Every tool returns its JSON result inside the MCP content envelope as a text block.
list_links
List your links, newest first, with cursor pagination.
| Field | Type | Description |
|---|---|---|
limit | integer | Results per page. Default 20, maximum 100. |
cursor | string | Opaque pagination token. Pass next_cursor from a previous response to fetch the next page. |
created_after | string | ISO 8601 timestamp — return only links created strictly after this time. |
created_before | string | ISO 8601 timestamp — return only links created strictly before this time. |
result
{
"links": [
{
"slug": "a1b2c3d4e5f6g7h8",
"title": "Q3 Proposal",
"created_at": "2026-06-10T18:22:04.511Z",
"expires_at": null,
"view_count": 42,
"is_public": true,
"url": "https://htmlvault.io/a1b2c3d4e5f6g7h8"
}
],
"next_cursor": "MjAyNi0wNi0xMFQxODoyMjowNC41MTFafGExYjJjM2Q0",
"has_more": true
}When has_more is true, call again with cursor set to next_cursor. When it is false, next_cursor is null and you have the last page.
get_link
Fetch one link’s metadata and full stored HTML.
| Field | Type | Description |
|---|---|---|
slugrequired | string | The link slug. |
result
{
"slug": "a1b2c3d4e5f6g7h8",
"title": "Q3 Proposal",
"html_content": "<!doctype html>…",
"created_at": "2026-06-10T18:22:04.511Z",
"expires_at": null,
"view_count": 42,
"is_public": true,
"url": "https://htmlvault.io/a1b2c3d4e5f6g7h8"
}Returns { "error": "Link not found" } if the slug is not owned by the caller.
get_analytics
View metrics for one link — totals, engagement, and breakdowns.
| Field | Type | Description |
|---|---|---|
slugrequired | string | The link slug. |
period | "7d" | "30d" | "90d" | "all" | Time window for the metrics. Default 30d. |
result
{
"slug": "a1b2c3d4e5f6g7h8",
"period": "30d",
"totals": { "views": 318, "unique_views": 204, "repeat_views": 114, "bot_views": 27 },
"engagement": { "avg_time_on_page_seconds": 47, "avg_scroll_depth_pct": 68 },
"breakdowns": {
"countries": [{ "value": "United States", "views": 190 }],
"devices": [{ "value": "desktop", "views": 240 }],
"channels": [{ "value": "clay-icp-ceos", "views": 35 }],
"referrers": [{ "value": "https://mail.google.com/", "views": 60 }]
}
}Automated traffic is excluded from views, engagement, and every breakdown; bot hits are counted separately as bot_views so the number is visible without polluting the human totals. Metric definitions match the dashboard exactly. Returns { "error": "Link not found" } if the slug is not owned by the caller.
create_link
Publish HTML as a secure, shareable link. Content is checked against Google Safe Browsing and your account’s PII scan policy before it goes live.
| Field | Type | Description |
|---|---|---|
htmlrequired | string | Complete HTML to publish. 5 MB limit. |
titlerequired | string | Short descriptive title shown in your dashboard. |
expiresIn | "1h" | "24h" | "3d" | "7d" | "30d" | "never" | Expiry window. Omit to use your account default. |
tracking | "none" | Set to none to publish with no tracking pixels or default tracking code. Omit to apply your tracking defaults. |
domain | string | Hostname of one of your active custom domains to serve from. Omit to inherit your account default domain. |
result
{
"slug": "k9m2p4q7r1s8t3v6",
"url": "https://htmlvault.io/k9m2p4q7r1s8t3v6",
"title": "Q3 Proposal"
}If your account’s PII policy is set to warn (rather than block), the link still publishes and the response includes a warnings array describing the categories found. See Errors for the block cases. A successful MCP create_link fires the link.created webhook.
update_link
Replace the HTML of an existing link in place — the slug and public URL are unchanged, so a link you have already shared updates live. Re-runs the same Safe Browsing and PII policy gates as publishing.
| Field | Type | Description |
|---|---|---|
slugrequired | string | The link slug to update. |
htmlrequired | string | New HTML content. |
result
{
"slug": "k9m2p4q7r1s8t3v6",
"url": "https://htmlvault.io/k9m2p4q7r1s8t3v6",
"updated_at": "2026-06-13T15:04:11.882Z"
}Returns { "error": "Link not found or not owned" } if the slug is not yours. Fires the link.updated webhook on success.
delete_link
Delete one of your links.
| Field | Type | Description |
|---|---|---|
slugrequired | string | The link slug to delete. |
Deletion is immediate and irreversible. The link row is hard-deleted and all of its view records are removed with it (cascade). It does not pass through the retention window — there is no recovery period. A link under a legal hold cannot be deleted.
result
{ "deleted": "k9m2p4q7r1s8t3v6" }A link under legal hold returns { "error": "legal_hold", "message": "This link is under a legal hold and cannot be deleted." }. A slug you don’t own (or already deleted) returns { "error": "Link not found or already deleted" }.
scan_html
Run the regex PII & secret scanner against HTML without publishing — useful for an agent to check content before calling create_link.
| Field | Type | Description |
|---|---|---|
htmlrequired | string | HTML content to scan. |
result
{
"hasSensitiveData": true,
"findings": [
{ "type": "api_key", "label": "API Key", "count": 1 },
{ "type": "email", "label": "Email Address", "count": 3 }
]
}This is the zero-token regex scanner only — it reports findings but never blocks. The publish gates on create_link / update_link are what enforce your policy.
Workflow recipes
Publish straight from a chat
Ask the agent to generate an HTML artifact (a proposal, dashboard, or one-pager), then “vault this.” The agent calls create_link with the HTML and a short title and returns the URL. Because the publish gates run server-side, anything that trips your PII policy or Safe Browsing is caught before the link exists.
Living dashboards
Publish a status page once with create_link, share the URL, then have the agent refresh it on a schedule with update_link. The slug never changes, so recipients always see current data at the same link — and get_analyticstells you who’s actually opening it.
Agent pipelines with notifications
When an automated pipeline creates links via MCP, subscribe your stack to the matching webhook events — link.created to log the new URL, link.viewed to react to engagement. The agent publishes; your systems are notified out-of-band without polling.
Limits & errors
Rate limit
Requests are rate-limited per API key to 100 requests per minute (fixed one-minute window). Over the limit returns 429 with a Retry-After header:
429 Too Many Requests
{ "error": "rate_limited", "message": "Too many requests. Please slow down and retry after the window resets." }The exact ceiling may be tuned over time; clients should honor Retry-After rather than hard-code the value.
Error catalog
Tool-level errors are returned as a JSON object in the content envelope (transport status 200); auth and rate-limit errors are returned at the HTTP layer with the status shown.
| Field | Type | Description |
|---|---|---|
pii_detected | tool result | PII policy is set to block and the content matched. Shape: { "error": "pii_detected", "findings": [...] }. From create_link / update_link. |
unsafe_content | tool result | Content links to a known-malicious site (Safe Browsing). Shape: { "error": "unsafe_content", "message": "...", "threatTypes": [...] }. |
unknown_domain | tool result | The domain passed to create_link isn’t an active domain on your account. |
legal_hold | tool result | From delete_link on a held link. Shape: { "error": "legal_hold", "message": "..." }. |
rate_limited | 429 | Per-key rate limit exceeded; see above. |
missing_key / invalid_key | 401 | No bearer header / unrecognized or revoked key. |
plan_required | 403 | Valid key but Free plan. |
Subscribing to events instead of polling? See the Webhooks reference.