qURL
This MCP server lets an agent manage qURL access resources and links, including private CRID access and session control.
Publish and manage private CRID resources with consumer access policies (keys, invite_only, email_capture, review, contract).
Create, list, get, update, extend, and delete qURL resources.
Mint, resolve, revoke, and update qURL access tokens/links, including batch creation and sharing by CRID.
List active access sessions and terminate one or all sessions for a resource.
Manage invitations, grants, approvals, denials, and access revocation for private resources.
qURL MCP server for temporary access links
Give an AI agent tools to share localhost web apps, private APIs, and selected
files through expiring qURL™ by LayerV access links.
This repository implements the Model Context Protocol server for that workflow.
The @layervai/qurl-mcp package runs as a local stdio server backed by the
qURL CLI or API, or as an authenticated remote HTTP server backed by the API.
Start with the no-account local quickstart
or account-backed client configuration, then choose
the tools for your task. In local mode, publish_crid runs the installed
CLI to publish an app that is already running; share_by_crid issues its
access link. Creating a link alone does not publish an app or restrict an
existing public URL. Uploads require the account-backed tool set and use a separate connector lifecycle;
see upload tools for their supported options and limits.
Local app sharing · File sharing · Sharing questions · Private links
⚠️ Renamed from
@layerv/qurl-mcpin v0.4.0. The old package is deprecated and will not receive further updates. If you're using@layerv/qurl-mcp@0.3.x, swap the scope in your MCP client config — same binary, same API key, no other changes.
Let an AI agent share a localhost preview
Use this workflow when an agent needs to give a reviewer access to a development preview or internal API. A tool that creates a link does not start the app on your laptop.
Keep the local app running. Configure a local stdio MCP client using the no-account quickstart, with the qURL CLI installed on the same host and no
QURL_API_KEYset.Ask the agent to call
publish_cridwithapp_name(a short public name, without secrets or recipient details) and the app's root loopback URL, such ashttp://127.0.0.1:3000, and save the returned CRID. Alternatively, publish it directly with the CLI. Local tools use that machine's device identity; a CRID does not grant another device authority over the resource.Ask the agent to call
share_by_cridwith the published CRID and a requested lifetime. The following is a tool argument object, not a complete MCP message:{ "crid": "REPLACE_WITH_PUBLISHED_CRID", "ttl": "5m" }Share the full returned link through your intended private channel and report the expiry returned by the service. Do not assume the requested TTL was granted unchanged. Keep the publishing host and app available.
Check that your installed release exposes these tools. In account-backed mode,
share_by_crid instead uses the API credential and requires qurl:resolve and
resource authorization. For resources already managed through the API, see mint_link and the other
link-management tools. Tool availability and resource
identifiers matter; an uploaded-file identifier cannot be substituted into
the same workflow.
Related MCP server: unphurl-mcp
Let an AI agent share a PDF or image
For example, give a reviewer a short-lived link to a selected PDF report.
Use account-backed stdio mode (QURL_API_KEY set) or authenticated HTTP mode,
then configure the upload Connector under configuration files.
Upload tools are not listed in no-account local mode. Choose the tool for the transport:
File location | Tool | Important requirement |
A PDF or supported raster image on the local MCP server host |
| Available in stdio mode; the host process must be allowed to read the selected file. |
PDF or image bytes supplied to an HTTP or stdio MCP client |
| Supply the supported file as base64 data with the correct filename and media type. |
A file already stored by an upload tool |
| Reuse its resource ID to mint a fresh link without another upload. |
Text to share as a generated PDF |
| Uses the configured upload service, rather than publishing a local app. |
A stdio tool argument example:
{
"file_path": "/absolute/path/to/review.pdf",
"expires_in": "5m",
"one_time_use": true
}Pass these arguments to upload_file_qurl only for a file the user selected.
The tool uploads to the configured file service and returns a new access link.
Use the confirmed expiry in its response. Opening a single-use link to test it
can consume it before the recipient opens it.
Upload links cannot currently be revoked through this MCP server, and mint_link
cannot create another link for an uploaded file. See upload tools
for the supported formats, size limits, response fields, and cleanup limitations.
Choosing what an agent should share
Should the agent share an app or a deployed preview?
If the reviewer needs the current localhost app, keep it running and use the local CLI-backed tools to publish the resource and issue its link. An existing ngrok endpoint, Cloudflare application, or Vercel preview is another possible handoff, but its access controls and availability come from that separately configured service. This server exposes qURL operations; it does not create those other deployments. See the app-sharing comparison when choosing the workflow. Share the confirmed expiry and preserve the app's login requirements.
Should the agent upload the result or link to a running file service?
A selected PDF or image can use this server's account-backed upload tools. A hosted Google Drive document instead uses Drive's sharing permissions; an S3 object can use a presigned download URL. Those storage workflows are distinct from publishing a local file server, which must remain available. Choose based on where the result should live and which service the agent is authorized to use. This server's upload identifiers and revocation limits still apply; a file path, uploaded-file identifier, and local-service CRID are not interchangeable.
MCP sharing questions
Is this a public tunnel to my localhost app?
In no-account stdio mode, this server invokes the local CLI to publish and manage an already running app. API-backed and remote HTTP modes use their configured resources; a remote server cannot publish an app on your laptop through its own localhost address. Use the localhost-sharing comparison when choosing between resource access links and a public endpoint workflow.
Does every AI client support the same tools?
No. Configure the client's supported transport and check the installed server's
tool list. A remote HTTP server cannot use upload_file_qurl to read a file
from the client's laptop; use the file-data tool for a supported attachment.
Can I use an app link and an uploaded-file link interchangeably?
No. App resource management and uploaded-file link minting use different lifecycles. Follow the tool that issued the link, including its revocation limits. Your local app's own login continues to apply when the link opens it.
Overview
The server exposes qURL operations as MCP tools for clients that support the configured transport. See the LayerV MCP guide for product setup and the tables below for the capabilities in this source version.
It currently supports:
creating, reading, updating, and deleting qURLs
resolving access tokens
managing qURL tokens and sessions
uploading text or file content and generating qURLs
optionally serving LayerV public legal pages
serving a configurable MP4 video playback page
Quick start: no account needed
Share an app running on your machine, or a URL, privately from Claude
Desktop, Claude Code, Cursor, Codex, or any local MCP client. You need no
account and no API key: with no QURL_API_KEY set, the server drives the
qURL CLI
installed on your machine, and the first publish creates this machine's
identity by itself.
1. Install the qURL CLI (macOS and Linux; Node.js 22.12 or later is needed for the MCP server):
(umask 022 && mkdir -p "$HOME/.local/bin" "$HOME/.local/state") && chmod go-w "$HOME/.local" "$HOME/.local/bin" "$HOME/.local/state" && curl -fsSL --proto '=https' https://raw.githubusercontent.com/layervai/qurl-integrations/main/scripts/install.sh | INSTALL_DIR="$HOME/.local/bin" shOn Windows, download qurl.exe from the
latest release
and put its folder on PATH. The server finds qurl on PATH, and also in
~/.local/bin, /opt/homebrew/bin, and /usr/local/bin, which many MCP
clients leave off the PATH they give servers. If the CLI is missing, every
tool answers with this install command instead of failing.
2. Add the server to your MCP client, with no env block.
Claude Desktop (claude_desktop_config.json) and Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"qurl": {
"command": "npx",
"args": ["-y", "@layervai/qurl-mcp"]
}
}
}Codex (~/.codex/config.toml):
[mcp_servers.qurl]
command = "npx"
args = ["-y", "@layervai/qurl-mcp"]Claude Code:
claude mcp add qurl -- npx -y @layervai/qurl-mcp3. Ask for what you want, for example "share the app on localhost:3000 with my client". The agent publishes the app (it stays on your machine; the CLI's background service keeps the share running), and gives you a short-lived link for one person, or turns on access requests so people can ask for access.
Tools without an account
Tool | What it does | CLI command |
| Publish a local app or URL privately and return its CRID |
|
| Make a short-lived access link for a CRID this machine published |
|
| List this machine's published resources (read-only) |
|
| List, approve by code, or deny access requests; list or remove approved people; turn requests on or off |
|
| Private access policy, invitations, and consumer grants |
|
| Stop, start, or restart sharing a local app |
|
| Delete a resource permanently |
|
The tools follow the same rules as the qURL agent skill (https://layerv.ai/skills/qurl/SKILL.md):
Everything is published private. These tools have no option to publish publicly.
A pending access request is approved only with the six-digit code the person gives you. No listing shows a code, and no tool approves by name: anyone can type any name, and names come back as unverified data.
An access link is a bearer secret for one recipient. A CRID grants nothing by itself and is safe to share.
The server runs the CLI with an argument array (never a shell), with
JSON output, closed stdin, a timeout, and bounded output, and it does not
log what the CLI prints. The tools that need an account key (the qURL link,
token, session, and upload tools, and the qurl:// resources and prompts)
are not listed in this mode.
With an account key
Set QURL_API_KEY to a LayerV API key and the server uses the qURL API
directly, with the full tool set described in the feature map:
{
"mcpServers": {
"qurl": {
"command": "npx",
"args": ["-y", "@layervai/qurl-mcp"],
"env": { "QURL_API_KEY": "lv_live_xxx" }
}
}
}Remote HTTP server
The HTTP transport (qurl-mcp-http) serves remote agent runtimes. It is
account-based: every request carries the caller's qURL API key as a bearer
token, and it never runs a local CLI. See HTTP Authentication
and the LayerV MCP guide.
Private consumer access
Node.js 22.12 or later is required by the shared qURL SDK.
publish_crid creates a private URL resource with keys, invite_only,
email_capture, review, or contract access. manage_crid_access changes the
policy with its current version, sends invitations, lists grants, approves
or denies requests, and removes access. The tools return resource-scoped
receipts and omit invitation secrets and bearer links. Local apps use the
qURL CLI on the publisher machine. In local mode, app_name is required and sets and confirms
the public preview name through qurl preview set (requires a CLI with that command).
If naming fails, the tool returns an error with the published CRID and recovery
instructions. It does not delete or republish the resource. Callers must now
provide app_name; use the user's app name or a short neutral name without
secrets or recipient details. Include the returned name and browser address
in the answer even when invitation emails are sent.
For example, an assistant can turn “share this with ana@example.com, and allow no requests” into an invite_only resource and an authorized invitation. For email capture, the consumer must know that the publisher receives their verified email. A verified email proves mailbox control, not legal identity. Contract approval requires a trusted publisher backend entitlement receipt; these tools do not verify payment. Do not report connection removal as complete while session_cleanup is pending.
This feature requires the matching service and admission readers. Keep it disabled until the publisher and consumer journeys pass. The package pins the published SDK 3.0.0 and supports clean installation with dependency scripts blocked. This release requires Node.js 22.12 or later; Node.js 20 is no longer supported.
Runtime Modes
Mode | Purpose | Start Command | Typical Use Case |
| Local subprocess MCP server |
| Claude Desktop, Cursor, Codex, and other local MCP clients |
| Authenticated remote MCP server |
| Remote agent runtimes behind HTTPS |
Feature Map
qURL Management Tools
Tool | Description |
| Create a new qURL |
| Resolve an access token into a protected target URL |
| List qURL resources |
| Fetch details for a single qURL |
| Delete a qURL |
| Extend a link's expiration (needs |
| Update qURL metadata or expiration |
| Mint a new access link for an existing resource |
| Mint a temporary access link from a resource CRID |
| Create multiple qURLs in one request |
| Revoke a specific token |
| Update a specific token |
| List active access sessions |
| Terminate one or all active sessions |
share_by_crid recognizes a standalone $<CRID> value as an explicit request
to mint a temporary link for that CRID. The $ is a user-facing marker and is
removed before the CRID is sent to the qURL API. A bare CRID remains supported.
This tool requires qurl:resolve. Its optional Go-duration ttl must resolve to
a positive whole number of seconds. The service clamps the requested lifetime.
Sharing makes one request with a 30-second timeout and does not automatically
retry. Repeating the tool call can mint another link.
Resource status filters accept active, revoked, or active,revoked.
A resource remains active or revoked even when its expires_at is in the past;
individual access tokens can expire. expired is not accepted by list_qurls,
even though response parsing tolerates legacy values. The snapshot retains
upstream's stale expiration prose; its active/revoked status enum is authoritative.
The qURL SDK reuses the same Idempotency-Key and request body when retrying
POST/PATCH requests after a network failure or HTTP 429. Mutating HTTP 5xx
responses are not automatically retried. Each new MCP tool invocation is a new
operation with a new key, so repeating create_qurl, batch_create_qurls, or
mint_link can mint additional tokens. MCP does not expose a caller-supplied key.
Upload Tools
Tool | Mode | Description |
|
| Upload a local file and mint a qURL |
|
| Upload base64 file content and mint a qURL |
|
| Upload text content and mint a qURL |
|
| Mint a link for a stored upload without uploading bytes again |
upload_file_qurl is intentionally stdio-only. It can read any supported
PDF/image that the local MCP process user can access, so agents should invoke
it only for a path the user explicitly selected for sharing. Do not expose it
to untrusted prompts or autonomous agents: prompt injection could otherwise
select another readable PDF/image on the host. Run stdio under an OS account
whose filesystem access is limited to intended shareable content. HTTP mode
never registers this host-file tool.
The byte/text tools are also available in stdio so local clients can share
in-chat attachments without first materializing them at a known host path.
To recover after a successful upload but a failed mint, keep the returned
resource_id and call mint_uploaded_file_link. This makes one mint attempt
without uploading the file again. Each call can create a new live link. A timed-out
or failed prior mint may already have created one, so reconcile that result before
retrying automatically. Existing links are not cancelled. The response contains
the new link and its expiry status.
Connector upload and link minting are separate operations. The link comes from
the connector's POST /api/mint_link/:resource_id (in tunnel mode, a
per-recipient watermarked view), not from qURL mint_link, so uploaded files
cannot be re-linked with mint_link. Use mint_uploaded_file_link with the
saved resource_id instead. Upload links
support expires_in (1m-30d, converted to an absolute expiry on the MCP host's
clock, so a host clock that is off shortens or lengthens the link; a link whose returned expiry
is already past by this host's clock is returned with expires_at_already_past), one_time_use, and session_duration (whole seconds, up to 24h); access_policy and
max_sessions must be omitted or null; non-null values are rejected before the upload. If the connector ever mints more
links than requested, the result reports how many extra in
unexpected_extra_link_count and any IDs it returned in
unexpected_extra_qurl_ids (at most 10); they may be live even when this host's clock suggests expiry, so tell the user. If minting
fails after upload, the connector currently has no delete endpoint; the server logs the orphaned
resource_id for operator cleanup and returns the mint failure. Upload links
cannot yet be revoked from this server: delete_qurl on the upload's
resource_id does not stop a connector-minted link (revocation needs the
connector's /api/revoke_links), so prefer short expiries for sensitive files.
Duration inputs accept whole days or weeks (7d, 1w), or unsigned Go-unit
sequences (1h30m, 1.5h). Mixed day/week sequences (1d12h), leading signs,
leading-dot fractions, and Greek mu (μs) are rejected; micro sign (µs) is accepted.
HTTP upload attempts remain bounded by the per-IP and per-credential MCP rate
limits; stdio operators should separately constrain autonomous retry loops.
Upload validation binds the declared media type to the filename plus format
start/end markers; it is not a malware scanner or full PDF/image decoder.
For polyglot resistance, a PDF's final %%EOF marker must be followed only by
ASCII whitespace; producer output with other trailing bytes is rejected even
if a permissive PDF reader would accept it. JPEG validation checks framing and
terminal markers rather than decoding image segments. The authenticated
connector must independently decode or otherwise fully validate content before
storage when semantic media validity matters. It must also preserve the
declared safe media type and serve downloads with X-Content-Type-Options: nosniff rather than inferring an executable type.
There is intentionally no application-level path allowlist: symlinks and
time-of-check/time-of-use races make a lexical prefix check a misleading
security boundary. Use a dedicated OS account, container, or read-only mount
whose readable files are already limited to the intended sharing directory.
The final path component is opened with O_NOFOLLOW; intermediate directory
symlinks retain normal filesystem behavior under this trusted-local-user
boundary.
MCP Resources
URI | Description |
| Current qURL list |
| Current quota and usage information |
MCP Prompts
Prompt | Description |
| Secure service integration prompt |
| Link audit prompt |
| Access rotation prompt |
Quick Start
1. Install Dependencies
npm installFor a local stdio-only source install, use npm install --omit=optional; this
omits the AWS SDK. HTTP deployments using the DynamoDB credential quota must use
the ordinary install so the optional SDK is packaged.
2. Build
npm run build3. Start
Local stdio mode:
npm run startRemote HTTP mode:
npm run start:httpMCP Client Example
If you want to use this server in stdio mode with a local MCP client, use
the configuration from Quick start. Leave
out env to work without an account through the local qURL CLI, or add
"env": { "QURL_API_KEY": "lv_live_xxx" } to use the qURL API with an
account key.
Configuration Files
Copy the tracked examples to create local configuration files:
cp qurl-mcp.config.example.json qurl-mcp.config.json
cp qurl-mcp.http.example.json qurl-mcp.http.jsonThe local files are gitignored so credentials and machine-specific paths are not committed.
Their responsibilities are:
File | Purpose |
| Shared runtime config used by both |
| HTTP-only server listener and public access config |
qurl-mcp.config.json Reference
Shared Core Settings
Field | Purpose |
| Limits decoded and local file uploads (default |
| Base URL of the qURL backend API |
| Base URL of the upload connector |
| Optional exact host allowlist for connector-minted links |
Shared settings have these environment overrides. Environment values take precedence over the shared config file. The process caches resolved shared settings but automatically invalidates that cache when the file metadata or any relevant environment value changes.
Environment variable | Config field |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Set QURL_LINK_HOSTS=qurl.link,files.example.com (or linkHosts as a JSON
array) to restrict connector-minted upload links before they are returned or
emailed. Entries are exact ASCII hostnames or IPs, without schemes, ports,
paths, or wildcards. Subdomains need their own entries. The policy checks the
hostname; existing HTTPS and loopback development rules still apply. When neither source has a non-empty list,
custom-domain behavior stays unchanged. A blank environment list falls back to
any file policy; clear the file list to disable that policy. The tracked example
config uses an empty list. Configure all legitimate link hosts before enabling it. A rejected mint may already have created a live
link and stored file; the error reports that state and does not retry the upload.
QURL_API_KEY is intentionally environment-only and has no config-file field.
Prefer QURL_SMTP_PASSWORD for the SMTP secret as well. If smtp.password is
stored in the config file on a POSIX host, restrict that file to owner-only
permissions (for example, chmod 600); startup warns when group/other read bits
are present. This check is intentionally advisory so existing deployments do
not fail after an upgrade, and it is skipped on Windows because POSIX mode bits
are not available there.
Email delivery itself is fail-closed unless at least one exact
smtp.allowedRecipients entry or smtp.allowedRecipientDomains entry is
configured; startup warns when complete SMTP credentials lack that policy.
Raising maxUploadFileDataBytes also raises the HTTP JSON parser's per-request
memory ceiling to roughly 1.5 times that value (up to about 150 MB at the
100 MB maximum), before base64 decoding applies the exact byte cap. Until a
session has completed a successful downstream qURL API call (a completed upload's
link mint counts), its parser ceiling remains at the smaller 10 MB default upload
setting; clients configured for a larger first upload must validate the session
with a small qURL API call or a smaller upload first.
Size the configured maximum and reverse-proxy concurrency limit together.
Set QURL_API_KEY in the environment for stdio mode to use the qURL API.
Without it, stdio mode runs through the local qURL CLI and needs no account
(see Quick start). In HTTP mode, every
client request supplies its own qURL API key as a bearer token.
defaultQurlApiUrl and QURL_API_URL require HTTPS for non-loopback hosts
because qURL API keys and data are bearer-sent to that destination. Plain HTTP
is accepted only for literal loopback development endpoints. Upload connector
URLs follow the same HTTPS-except-loopback rule.
Loopback means 127.0.0.0/8 or ::1; wildcard bind addresses such as
0.0.0.0 and :: are intentionally not accepted as outbound HTTP targets.
Connector destinations are trusted operator configuration rather than caller
input; private addresses and DNS resolution are therefore permitted. Pin the
connector hostname in deployment DNS and do not point it at metadata services.
The caller's qURL bearer credential is forwarded to this host, so treat connector
URL and DNS control as part of the credential trust boundary.
Configure the connector service base URL, not an upload route: qurl-mcp appends
/api/upload to ordinary base paths, accepts that exact endpoint suffix, and
rejects ambiguous upload-like paths such as /upload or /api/upload/v2.
The MCP server performs bounded file-framing checks, not full media parsing;
the connector must independently revalidate uploaded content before storage or
serving, and delivery must retain nosniff behavior as the authoritative type
boundary.
API and connector base URLs that contain embedded credentials, a query string,
or a fragment are now rejected during startup. Deployments that previously used
one of those unusual URL forms must move credentials to QURL_API_KEY and keep
the configured service URL to its origin and optional path prefix.
SMTP Settings
Field | Purpose |
| SMTP server hostname |
| SMTP server port |
|
|
| SMTP login username |
| SMTP login password or app-specific code |
| Sender email address |
| Sender display name |
| Optional exact-address allowlist |
| Optional exact-domain allowlist (subdomains are not included) |
| Per-message recipient cap (default |
| Per-qURL-key attempted-recipient cap per fixed hourly window (default |
These settings are used when email delivery is requested by tools such as:
create_qurlmint_linkupload_text_qurlupload_file_qurlupload_file_data_qurl
If either recipient allowlist is configured, only an exact address or domain
match is delivered. If both are empty, the message and hourly caps still apply.
Domain entries are exact: example.com does not implicitly allow
mail.example.com; list each permitted subdomain explicitly.
Addresses and domains are normalized to lowercase NFC/IDNA ASCII form and a
trailing DNS root dot is removed before comparison and delivery.
Each recipient allowlist is limited to 1,000 configured entries. The
per-message recipient cap applies to the complete unique requested fan-out
before allowlist filtering, so blocked addresses cannot be used to submit an
oversized batch.
In HTTP mode, any caller with a valid qURL API key can request a server-side
SMTP delivery. Configure allowedRecipients or allowedRecipientDomains
before enabling SMTP on an Internet-facing HTTP deployment; empty allowlists
permit delivery to any syntactically valid address subject to the quotas.
The SMTP transport uses bounded connection/socket timeouts and is closed after
each delivery batch. Failed SMTP attempts still consume quota—including when a
transient outage results in zero delivered messages—so repeated failures cannot
bypass the abuse limit.
Each delivery request also has a 60-second aggregate deadline. Recipients not
started before that deadline are reported as skipped; provider-side queues are
the supported path for larger or slower fan-out.
Transport encryption is mandatory: smtp.secure: true uses implicit TLS,
while smtp.secure: false requires a successful STARTTLS upgrade. Port 465 is
reserved for implicit TLS and therefore requires smtp.secure: true.
Hourly quota state is maintained per server process: it resets on restart and
is not shared across replicas. Operators running multiple instances should
enforce a corresponding aggregate limit at the SMTP provider or gateway.
The in-process quota is therefore an abuse backstop, not a durable global
safety boundary; restart/scale-out fail-open behavior must be covered by that
provider-side limit.
Tracking fails closed for new principals after 10,000 principals are retained
in one process; existing principals continue to use their current buckets until
expired entries are pruned.
Restrict qURL API-key issuance and monitor new-principal quota-cap rejections:
cycling many valid keys can deliberately hold that shared table at capacity for
up to one quota window.
The quota uses a fixed one-hour window that starts with the first attempted
delivery after the prior window expires.
As with any fixed window, traffic immediately before and after a boundary can
total nearly twice the configured hourly value; use a provider-side sliding or
rolling limit when that boundary burst must be prevented across replicas.
HTTP SMTP delivery is restricted to the operator credential configured in
QURL_API_KEY. A different customer's
valid qURL key does not authorize use of the operator's SMTP account. Without
that explicit operator key, HTTP email delivery is disabled. Stdio retains its
local operator trust model. Recipient allowlists and quotas apply in both modes.
Generated qURL links are included in the plain-text email body. Restrict recipients with the SMTP allowlists and configure transport encryption at the SMTP server/provider when link confidentiality matters.
Prefer environment variables for SMTP credentials and policy:
QURL_SMTP_USERNAME, QURL_SMTP_PASSWORD, QURL_SMTP_FROM_EMAIL,
QURL_SMTP_ALLOWED_RECIPIENTS, QURL_SMTP_ALLOWED_RECIPIENT_DOMAINS,
QURL_SMTP_MAX_RECIPIENTS_PER_MESSAGE, and
QURL_SMTP_MAX_RECIPIENTS_PER_HOUR.
Public Video Page Settings
Field | Purpose |
| Title shown on the public video page |
| Public path of the video playback page |
| Absolute server path of the MP4 file |
When configured, the HTTP server additionally exposes:
a public video playback page
a streaming endpoint for the MP4 file
publicVideo.filePath is trusted operator configuration. The final component
must be a non-symlink regular .mp4 file; intermediate directory symlinks keep
normal filesystem resolution and must therefore remain under operator control.
Startup probes this optional asset and warns when it is missing, empty, or not
regular, but intentionally keeps the MCP service and /healthz available. The
video-file route still fails closed with 404 until the asset is corrected.
qurl-mcp.http.json Reference
Use qurl-mcp.http.example.json for local,
stateful development. qurl-mcp.http.stateless.example.json
shows every store and metric field required by a deployed stateless service.
Field | Purpose |
| HTTP MCP listener port |
| HTTP MCP bind address |
| Public base URL of the service |
| Host allowlist for Host header validation |
| Exact trusted reverse-proxy hop count (default |
| Request-scoped HTTP transport with no session affinity (default |
| Stateless-only POST/parser concurrency cap per process (default |
| Credential counter backend: |
| DynamoDB table used by the shared credential counter |
| CloudWatch EMF namespace for stateless saturation metrics |
| Stable CloudWatch EMF Service dimension |
| Stable CloudWatch EMF Environment dimension |
| Hard cap on live MCP sessions (default |
| Per-bearer live and initializing session cap (default |
| Cap on sessions that have not completed a downstream qURL API call (default |
| Connected-session idle eviction window (default 15 minutes) |
| Absolute session lifetime, including active SSE/tool requests (default 24 hours) |
| Absolute validation deadline for never-validated bearer sessions (default 1 minute) |
| Per-client |
| Per-client public-route request limit (default |
HTTP fields have matching environment overrides:
Environment variable | Config field |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The listener defaults to 127.0.0.1. A non-loopback host is rejected unless
allowedHosts is explicitly configured. Set trustProxyHops (or
MCP_TRUST_PROXY_HOPS) to the exact number of trusted proxy hops; leave it at
0 for direct connections so forwarded IP headers cannot spoof rate-limit keys.
The Host allowlist is limited to 1,000 entries so request-time validation stays
bounded even under pathological operator configuration.
/mcp applies the configured request allowance independently to both the
client IP and the SHA-256 digest of the authenticated bearer. The memory store
is process-local; the DynamoDB store uses an atomic fixed-window counter keyed
by credential digest and UTC minute. It never stores the bearer. As with any
fixed window, requests around a minute boundary can total nearly twice the
configured allowance. The table contract is a string partition key named
rate_key; the atomic update writes a numeric request_count counter and a
numeric expires_at TTL timestamp. Enable DynamoDB TTL on expires_at so
expired rows do not accumulate; TTL only schedules asynchronous cleanup, and
the minute in the key—not physical deletion—resets the active window. The task
role requires dynamodb:DescribeTable for startup and dynamodb:UpdateItem on
the request path. Use on-demand capacity or provision enough write capacity for
the expected fleet rate; throttling fails closed with 503 and never falls
back to memory. The client uses standard retry mode with at most two attempts,
a one-second connection timeout, and a two-second request timeout that throws;
these explicit bounds limit how long a request holds a concurrency permit
during a partial store failure. The optional AWS SDK dependency is top-level
exact-version pinned, while the committed package lock fixes its transitive
@aws-sdk/* and @smithy/* graph. Any SDK bump must update the lockfile and
keep the real-NodeHttpHandler timeout-materialization regression test green.
The dependency is loaded only when the DynamoDB store is selected, so
stdio-only consumers may install with --omit=optional. Deployed HTTP images
must include optional dependencies; startup fails before listening if the SDK
is absent or exposes an incompatible runtime surface. The client uses the
standard AWS_REGION and credential provider chain; ECS deployments
normally obtain both from the task environment and task role. Reverse-proxy
deployments must set the correct hop count or all callers behind the proxy will
share the proxy's single IP bucket. Only the DynamoDB credential quota is
fleet-wide: the IP limiter is process-local, so its effective fleet allowance
multiplies with task count and must be backed by a shared edge limit. The
managed deployment in qurl-integrations-infra PR #1305 enforces both a
per-source-IP WAF limit and a lower aggregate /mcp fleet cap, with live
headroom proof tracked in issue #1306. The credential bucket also prevents one
key from bypassing the request allowance by rotating source IPs, while
maxSessionsPerCredential prevents it from occupying the full session pool.
Each distinct bearer value retains one credential-bucket entry for the current
one-minute window. The IP limiter runs first, so token rotation from one source
cannot create entries faster than mcpRateLimitPerMinute; hostile distributed
traffic still requires the documented shared edge limit. The IP bucket is the
primary in-process control against arbitrary bearer rotation because distinct
unvalidated bearer strings necessarily occupy distinct credential buckets.
In stateful mode, budget pending-session parser memory as
maxUnvalidatedSessions times roughly
1.5 times the smaller of maxUploadFileDataBytes and 10 MB (plus about 64 KiB
per request). At the defaults, the theoretical concurrent ceiling is about
1.5 GiB. Lower maxUnvalidatedSessions and the shared edge concurrency limit
together when the deployment has a smaller memory budget.
Bearer credentials are conclusively validated by the first successful
downstream qURL API call. Until then, sessions use the smaller pending-session
cap and one-minute validation deadline, so arbitrary non-empty bearer strings
cannot occupy the full session pool for the normal 15-minute TTL. A client that
performs only MCP introspection remains pending by design; after deadline
eviction it must re-initialize before its next request. The session caps and
validation deadline are configurable for clients with longer
introspection-to-tool-call gaps. The deadline is absolute and applies regardless
of activity, including an open SSE stream or a long-running first tool call.
Validated clients that disconnect without sending DELETE /mcp retain their
bounded session slot for a 30-second reconnect grace period. A reconnect clears
that deadline; otherwise the session is reaped without waiting for the longer
idle TTL. Size maxSessions and the idle TTL for clients that remain connected
but do not perform explicit session teardown.
Validated sessions also expire at sessionAbsoluteTtlMs (24 hours by default),
even during an active SSE stream or tool request. This prevents keepalives from
pinning a global or per-credential session slot indefinitely.
The first downstream qURL operation must therefore complete before that
deadline; an unusually slow first API call may be interrupted and the client
must re-initialize. This fail-closed behavior prevents an invalid credential
from extending its pending slot with a deliberately long-running request.
At the unvalidated-session cap, a new initialization replaces the oldest idle unvalidated session, so junk handshakes cannot reserve every slot for the full validation TTL. Validated sessions and active requests are never evicted; pending initializations and asynchronous teardown remain bounded. Sustained traffic can still churn unvalidated sessions, so public deployments should use stateless mode and edge admission controls.
Accepting a non-empty bearer during MCP initialization is intentional: it keeps
protocol introspection available before the first qURL operation, while the
global session cap, per-credential session cap, pending-session cap, absolute
deadline, and request rate limit bound invalid-key slot usage. The MCP
middleware does not validate the key itself; only a successful downstream qURL
API response promotes the session.
Downstream errors, including non-2xx responses that appear authenticated, do
not promote it because an intermediary may have generated them before the qURL
API authenticated the bearer.
Promotion therefore assumes the configured HTTPS qURL API endpoint and every
trusted intermediary neither cache nor synthesize authenticated success
responses. Reverse proxies in that path must forward authorization and disable
response caching for qURL API traffic.
Consequently, any caller with a non-empty bearer can enumerate the public
tool/resource/prompt catalog and briefly hold bounded pending-session state. On
hostile networks, place non-loopback deployments behind an identity-aware proxy
that preserves the caller's qURL bearer credential for /mcp authorization.
Initialization and catalog listing return server-owned static metadata only;
they do not invoke tool/resource/prompt handlers, read host files, contact the
qURL API or connector, or send email. Handler calls rely on the configured qURL
API to authenticate the forwarded bearer before returning data or applying an
operation. The configured connector is a second credential authority: it must
authenticate the forwarded qURL bearer before accepting or storing upload bytes.
Deploying an unauthenticated connector is unsupported because it would allow an
unvalidated MCP caller to create connector-side state.
An off-by-default Auth0 OAuth integration is available for the directory-review path. It requires per-user token exchange and the RELEASE qualification gates in that guide before activation.
Stateful mode is the compatibility default and retains the existing MCP session
registry, GET SSE, explicit DELETE behavior, and process-local credential quota
charging for all three MCP methods. Stateless mode creates and closes a server
and transport for each POST, ignores mcp-session-id, and
returns JSON-RPC-shaped 405 responses for GET and DELETE. It is the required
mode behind a load balancer or autoscaling service because no request depends
on process-local affinity. The concurrency permit is acquired before JSON
parsing and released on every response/error/disconnect path. Stateless mode
uses the configured maxUploadFileDataBytes parser ceiling directly because
the pre-parse concurrency permit provides its memory-amplification bound.
Budget roughly maxConcurrentRequests times (1.5 times
maxUploadFileDataBytes plus 64 KiB) per process; the default concurrency at
the 100 MB upload ceiling is approximately 3 GiB before downstream work.
Stateless startup rejects configurations whose conservative parser budget
exceeds 4 GiB. Lower either setting further when the ECS task has a smaller
memory limit. In contrast, stateful sessions above the default ceiling must
first complete a successful downstream qURL API call.
On hostile networks, an authenticated edge request-size limit no larger than
the configured parser ceiling is a deployment requirement: the permit bounds
aggregate memory, but a non-empty bearer is not authoritatively validated until
the parsed operation reaches the downstream qURL API.
The stateless listener bounds header receipt at 15 seconds and both complete request receipt and idle socket lifetime at 120 seconds. A concurrency permit spans parsing through response completion, so stalled clients cannot retain the entire permit pool indefinitely. A tool call that produces no socket traffic for 120 seconds is intentionally aborted; integrations needing longer silent operations must move that work behind an asynchronous API rather than raising this fleet-wide retention bound.
Deployed (non-loopback) stateless mode requires the DynamoDB credential store
and all three stable metric identity fields. It emits a 30-second EMF heartbeat:
McpConcurrencyUtilization is the peak permit utilization observed during the
interval at request admission and heartbeat (including requests that start and
finish between heartbeats), while
McpConcurrencyRejected and McpRateLimitStoreErrors are snapshot-and-zero
interval deltas that include explicit zeros. Session caps and email recipient
quotas remain in-memory; the DynamoDB credential quota is fleet-wide and counts
every authenticated HTTP POST, including initialization, discovery, and tool
calls. Size that quota for the expected complete request pattern rather than
tool calls alone. The permit also spans the bounded DynamoDB increment: during
a store brownout, each admitted request may retain one permit for roughly four
seconds (two two-second attempts) before failing closed, while excess requests
receive a fast concurrency 503. The fixed-window counter increments every
attempt, including
attempts already above the credential limit; edge rate limits and DynamoDB
write/throttle alarms must therefore bound abusive write amplification.
Deployment owners must make both alarms and an over-limit write-amplification
probe hard promotion gates rather than treating them as optional observability.
The managed deployment in
qurl-integrations-infra#1305
provisions those alarms, with live proof tracked in its rollout ledger and
issue #1306 before promotion.
Direct createHttpRuntime embedders that inject a credential-store
implementation must still declare credentialRateLimitStore: "dynamodb" for
non-loopback stateless mode. The generic injection interface cannot prove a
custom backend is shared across replicas, so injection is deliberately not an
escape hatch from the deployed contract.
Metric identity fields are rejected in stateful mode so the concurrency gauge
cannot silently report a misleading zero.
Each stateless POST owns a fresh MCP server and transport so no request can
inherit another credential's handler state. Completed-response teardown is
tracked asynchronously. Admission stops when that backlog reaches
maxConcurrentRequests; requests already in flight may then finish, so the
backlog can transiently approach twice that count but remains bounded. While
the admission guard is closed, new requests fail with 503 and increment
McpConcurrencyRejected instead of growing teardown memory without bound.
That counter intentionally represents admission failure from either active
request saturation or teardown backpressure. Autoscaling must use
McpConcurrencyUtilization alone; the rejection counter remains page-worthy,
and low utilization alongside rejections identifies teardown lag.
Pooling these objects would weaken request isolation and is deliberately not a
performance optimization without measured registration pressure.
/healthz and the public video-file endpoint each use their own
publicFileRateLimitPerMinute bucket, isolated from legal/video-page traffic
and from each other. Keep load-balancer, liveness-probe, and expected video
range-request frequency below that per-source-IP allowance (300
requests/minute by default), or raise it for unusually aggressive clients.
Configuration Priority
By default, configuration is loaded from the two local JSON files above. If a
file is absent, built-in defaults and environment variables are used.
Relative config paths—including the defaults—are resolved from the process
working directory. Set the explicit path variables below when a supervisor,
npx, or an MCP host launches the server from a different directory.
The following environment variables independently override the config file paths:
QURL_MCP_CONFIGQURL_MCP_HTTP_CONFIG
QURL_MCP_HTTP_CONFIG never replaces the shared runtime config path. This keeps
listener settings from silently shadowing SMTP, connector, or API settings.
server.json and smithery.yaml describe the published stdio transport, so
they include shared upload/SMTP settings but intentionally omit HTTP-only
listener variables such as QURL_MCP_HTTP_CONFIG and MCP_MAX_SESSIONS.
Do not commit API keys, SMTP credentials, or private file-system paths.
HTTP Routes
After starting in http mode, the common routes are:
Route | Purpose |
| Main remote MCP endpoint |
| Health check endpoint |
| Public privacy policy page |
| Public terms of service page |
| Public video playback page |
| MP4 streaming endpoint |
/healthz is intentionally unauthenticated and Host-unvalidated for every
caller, exposes only { "ok": true }, and uses the configured public-route
request limit in a separate bucket so health probes cannot consume the
legal/video route allowance. A 429 from this route
means the probe source exceeded publicFileRateLimitPerMinute, not that the
application failed its liveness check; keep probe frequency below that limit.
It is registered before Host validation because ALB target probes use the task
IP and port as Host; public MCP and browser routes remain Host-validated.
HTTP Authentication
The /mcp endpoint requires Authorization: Bearer <qURL API key> on every
request. In stateful mode the bearer token is bound to the resulting MCP
session, so a session ID cannot be reused with a different credential. In
stateless mode the bearer remains request-scoped and is discarded when the
response closes.
Operator authentication boundary: initialization accepts any non-empty
bearer token and allows the public tools/resources/prompts catalog to be read
before authoritative validation by the first downstream qURL API call.
That catalog is assembled from static schemas and descriptions and does not
include bearer tokens, SMTP credentials, or other operator configuration.
Unvalidated-session caps, a short validation deadline, and request rate limits
bound that pre-validation state; the supplied token is forwarded only to the
configured qURL API and, for upload tools, the configured qURL Connector.
Introspection-only sessions therefore remain unvalidated and are closed at
unvalidatedSessionTtlMs; clients can re-initialize if they need a longer-lived
session. A session is promoted only after a successful qURL API call or a
deliverable upload-link mint through the configured connector (which forwards the
bearer to the qURL API)—rejected or rate-limited calls do not prove the credential
valid. The connector is therefore part of this trust boundary: point
QURL_CONNECTOR_URL only at a connector that authenticates the bearer. Its
upload error text (bounded to 1 KiB, control characters flattened) and the links
it mints are returned to the agent, and minted links may be emailed. Disconnected sessions
remain registered for a 30-second SSE reconnect grace period, while
maxSessions and maxSessionsPerCredential bound that allowance under churn.
Requests without an Origin header are accepted for non-browser MCP clients.
When Origin is present, it must match the origin of baseUrl; malformed or
cross-origin values are rejected on /mcp. Public health, legal, and configured
video routes do not use browser-origin state and are not gated by this check.
Configure remote MCP clients with:
Setting | Value |
MCP Server URL | Your public HTTPS URL plus |
Authentication | Bearer token |
Token | The caller's qURL API key |
If a client only supports OAuth discovery, place an OAuth-compatible gateway
in front of this server rather than exposing /mcp without authentication.
How to Verify Deployment
Service-Level Checks
Start with:
/healthz/mcp
LayerV legal documents are disabled by default. Only LayerV-operated services
should set MCP_SERVE_LAYERV_LEGAL_PAGES=true (or serveLayerVLegalPages: true
in the HTTP config). Self-hosted operators must publish their own policies.
Public Page Checks
When enabled, verify the legal pages and configured video page:
/legal/privacy/legal/termsthe configured public video page path
Domain Verification
If you plan to use OpenAI Platform, make sure the following root-level path exists:
/.well-known/openai-apps-challengeThis verification file must live under the domain root
.well-knownpath, not under/mcp.
Docker
The repository includes a Dockerfile for containerized deployment.
Example:
docker build -t qurl-mcp .
docker run -i -e QURL_API_KEY=lv_live_xxx qurl-mcpIf you deploy with Docker, make sure the container can still access the correct config files, or override the config file paths with environment variables.
Run the HTTP listener locally in Docker:
The image defaults to the stdio entry point and the HTTP server defaults to
container-local loopback. HTTP deployments must override the command and bind
to 0.0.0.0 with an explicit Host allowlist and HTTPS public origin. This
example publishes only to host loopback; put a TLS reverse proxy in front of
it for the configured https://mcp.example.com origin:
docker run --rm -p 127.0.0.1:3000:3000 \
-e MCP_HOST=0.0.0.0 \
-e MCP_BASE_URL=https://mcp.example.com \
-e MCP_ALLOWED_HOSTS=mcp.example.com,127.0.0.1,localhost \
qurl-mcp node dist/http.jsFor a single trusted production reverse proxy, set
MCP_TRUST_PROXY_HOPS=1, use the public HTTPS origin in MCP_BASE_URL, and
set MCP_ALLOWED_HOSTS to the public hostname. Do not expose the container's
listener directly when proxy trust is enabled.
Common Commands
Command | Purpose |
| Compile TypeScript |
| Run tests |
| Run enforced coverage |
| Run ESLint |
| TypeScript watch mode |
| Format source code |
| Check formatting |
| Start stdio mode |
| Start HTTP mode |
Recommended Deployment Order
Copy and update the two example config files
Set credentials through environment variables
Run
npm installRun
npm run buildRun
npm run start:httpVerify
/healthzVerify unauthenticated
/mcprequests receive401Configure the HTTPS reverse proxy
Verify an authenticated MCP initialization and the optional public pages
Third-Party Assets
Text-to-PDF generation bundles the regular-weight Noto Sans SC font as TrueType
(about 10 MB) for offline multilingual glyph coverage. The static font retains
all characters from the original variable font; PDFKit embeds only the glyphs
used by each document. No download or additional runtime dependency is needed.
Its SIL Open Font License and copyright notice are included in
assets/fonts/OFL.txt; conversion details are in assets/fonts/README.md.
License
MIT -- LayerV AI
Available Tools
16 toolsbatch_create_qurlsBatch Create qURLsA
Create up to 100 qURLs in a single request. The single-call alternative to looping create_qurl — saves round trips and returns a single envelope of per-item results. Only use this when the user explicitly wants multiple qURLs in one request. Never use it for a single URL, a single file, a single image, or a single attachment. Do not use this tool for uploaded files, image attachments, PDFs, or base64 file content — use upload_file_data_qurl (HTTP mode) or upload_file_qurl (stdio mode) instead. Not transactional: items succeed or fail independently (see succeeded/failed counts and per-item error). Use this when you need to mint many qURLs at once (e.g. provisioning a vendor list, distributing per-customer share links). Use create_qurl for a single resource. Response shape: { succeeded: number, failed: number, results: BatchItemResult[], request_id?: string }. Each results[i] carries index (matching the input position), success, plus either qurl_link + resource_id + qurl_site + expires_at (success) OR error: { code, message } (failure). Successful items may also carry branded_domain for custom-domain anchor text. Partial failure signaling: the handler sets isError: true on the tool response whenever failed > 0, so agents can branch without parsing JSON. The HTTP layer also returns 400 when every item fails — that's surfaced through the same shape (read data.results[*].error). One-shot links: like create_qurl, every qurl_link in the response is shown ONCE. Don't lose them.
| Name | Required | Description | Default |
|---|---|---|---|
| items | Yes | Array of qURL creation requests (1-100 items) |
Output Schema
| Name | Required | Description |
|---|---|---|
| failed | Yes | |
| results | Yes | |
| succeeded | Yes | |
| request_id | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations (readOnlyHint=false, destructiveHint=false, idempotentHint=false): discloses that the operation is NOT transactional, that isError is set when failed>0, that the HTTP layer returns 400 on total failure, and critically that qurl_link values are shown only ONCE. These are exactly the traits an agent needs before calling.
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 the single-call alternative are front-loaded, and the bolded subsections (Not transactional, Response shape, Partial failure signaling, One-shot links) are skimmable. It is somewhat over-stuffed and repeats response details that the output schema already covers, which keeps it short of a 5.
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 non-idempotent batch mint tool with nested item schemas and real failure modes, the description covers scope, routing, failure semantics, and the irreversible one-shot link behavior. Nothing an agent needs in order to invoke 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% and the single top-level parameter (items) is fully documented in the schema, including maxItems 100 and all nested fields. The description adds the 1-100 bound and per-item independence but no parameter-level syntax beyond that, so the baseline 3 applies.
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 (Create), resource (qURLs), and scope (up to 100 in a single request), and explicitly positions itself against `create_qurl` and the upload tools. An agent can distinguish it from all listed siblings 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?
Gives explicit when-to-use ('when the user explicitly wants multiple qURLs in one request', 'provisioning a vendor list'), when-not-to-use ('never for a single URL/file/image/attachment'), and names the correct alternatives for uploaded files and base64 content, including mode-specific tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
create_qurlCreate qURLA
Create a qURL — a policy-bound, expiring access link that gates a target URL with optional IP/geo/UA/AI-agent filters and time or session limits. When to use: minting a fresh protected access link for share-once or time-limited access (e.g. send a customer a 24-hour download link, gate a doc behind an IP allowlist, distribute a one-time-use credential to a contractor). Do NOT use this for chat-uploaded images, PDFs, screenshots, or file attachments. In HTTP MCP mode, those should go through upload_file_data_qurl; in stdio mode, use upload_file_qurl. When NOT to use: use mint_link when you already have a resource identifier and just need an additional access token under it — create_qurl identifies the resource by target URL and may return an existing same-type resource grouping. Use batch_create_qurls to create many in one round-trip. Use update_qurl to retag or extend an existing resource without minting a new one. If the user says 'give me the qURL of this image/file' and the content was uploaded in chat, this is the wrong tool. Behavior: not idempotent — calling twice produces two distinct qURL tokens, though both may share the same resource_id when the target URL groups to an existing same-type resource (this tool doesn't surface the underlying API's Idempotency-Key header). The returned qurl_link is shown ONCE in this response and is never recoverable through get_qurl or list_qurls; persist or share it immediately. A returned resource is in active status with the policy and per-token limits applied. Set expires_in explicitly for a predictable lifetime; otherwise the service uses its defaults. Read expires_at in the response. max_sessions is per minted qURL, not resource-wide; set one_time_use: false explicitly when you need max_sessions: 0 to mean unlimited visitors. Returns: { qurl_id: string (q_…), resource_id: string, qurl_link: string (shown once), branded_domain?: string, qurl_site: string, expires_at: string (RFC 3339), label?: string, type?: string }. qurl_id is the only q_… display ID an agent gets in this response — keep it if you plan a follow-up against get_qurl/update_qurl/mint_link (which accept either prefix). Example: create_qurl({ target_url: 'https://example.com/private', expires_in: '24h', one_time_use: true, access_policy: { geo_allowlist: ['US'] } }).
| Name | Required | Description | Default |
|---|---|---|---|
| type | No | Resource type for integrations allowed to mint non-url qURLs (max 64 chars). Defaults to url. | |
| label | No | Human-readable label identifying who this qURL is for (max 500 chars) | |
| expires_in | No | Duration string (e.g., "1h", "24h", "7d"; the API enforces 1m to 30d) | |
| target_url | Yes | The URL to protect with qURL | |
| max_sessions | No | Maximum concurrent sessions for this qURL token (0 = unlimited when one_time_use is explicitly false; max 1000) | |
| one_time_use | No | Whether the link can only be used once | |
| access_policy | No | Access control policy for the qURL | |
| custom_domain | No | Custom domain to assign to the auto-created resource (max 253 chars, must be registered/active/owned). | |
| session_duration | No | How long access lasts after the recipient reaches the content (e.g., "1h"). This anchors the resource-level session-duration cap when a new resource is created. |
Output Schema
| Name | Required | Description |
|---|---|---|
| type | No | Resource type echoed from the create request |
| label | No | |
| qurl_id | Yes | Display-friendly qURL ID (q_ prefix) |
| qurl_link | Yes | One-shot display access link — shown ONCE on creation, never returned again. Share immediately. |
| qurl_site | No | |
| expires_at | No | |
| resource_id | Yes | Stable resource identifier (public key or legacy r_ ID) |
| branded_domain | No | Bare branded hostname for anchor text when the resource has a usable custom domain |
| email_delivery | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations (which only declare non-readonly, non-idempotent, open-world, non-destructive). It discloses the not-idempotent nuance, that the underlying Idempotency-Key is not surfaced, that qurl_link is shown ONCE and never recoverable via get_qurl/list_qurls, the per-token vs resource-wide max_sessions scope, and the one_time_use/max_sessions:0 interaction.
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-loads the definition and uses bold section headers (When to use / Do NOT use / When NOT to use / Behavior / Returns) to make a long description navigable. It is on the verbose side and repeats the 'shown once' point, but nearly every sentence earns 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?
Given 9 params, nested access_policy, an existing output schema, and rich behavior, the description is complete: it names the return fields, explains lifetimes and session scope, routes incorrect use cases, and supplies a concrete example call. Nothing needed to invoke 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 schema carries most parameter documentation (baseline 3). The description still adds genuine semantics beyond it: expires_in default behavior ('otherwise the service uses its defaults'), max_sessions being per minted qURL rather than resource-wide, and the explicit one_time_use:false requirement for max_sessions:0 to mean unlimited.
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+resource ('Create a qURL') and immediately defines what a qURL is ('policy-bound, expiring access link that gates a target URL'), distinguishing it from siblings that resolve, extend, revoke, or batch. An agent can tell it apart from mint_link, batch_create_qurls, and update_qurl without opening any 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?
Explicitly provides when-to-use examples (24h download link, IP allowlist, one-time credential), when-not-to-use cases (chat uploads routed to upload_file_data_qurl/upload_file_qurl), and named alternatives with the condition that selects each (mint_link, batch_create_qurls, update_qurl). Nothing is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
delete_qurlDelete qURLADestructiveIdempotent
Permanently revoke a qURL — the link and every access token under it stop working immediately. This action is irreversible. Use this when you want to cut off access entirely (compromised link, departed user, end-of-engagement). It does not revoke links minted by the upload tools, which belong to the file connector; those cannot be revoked from this server. Use update_qurl instead when you only need to shorten the resource's expiration, retag, or rename, and extend_qurl to keep a link open longer — those preserve the existing access tokens. Idempotent: the API returns 404 for re-deletes, never-existed IDs, and resources owned by another API key (ownership-mismatch is collapsed into 404 server-side to avoid existence disclosure); this tool swallows all three. Branch on was_already_revoked to distinguish the no-op case from a successful revoke on this call. When the ID came from user input and ownership matters, call get_qurl first — a 200 confirms ownership; a thrown 404 is equally ambiguous on that endpoint too. Returns a confirmation payload. By default the resource is excluded from list_qurls; pass status: "revoked" to see it.
| Name | Required | Description | Default |
|---|---|---|---|
| resource_id | Yes | The resource public key, CRID, or legacy r_ ID to revoke (all tokens; q_ display IDs are not accepted). |
Output Schema
| Name | Required | Description |
|---|---|---|
| message | Yes | Human-readable confirmation message |
| revoked | Yes | |
| resource_id | Yes | |
| was_already_revoked | Yes | True when the API responded 404 (resource was already revoked or never existed). Agents that need to distinguish 'I revoked it' from 'it was already gone' should branch on this. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the destructive/idempotent annotations by stating irreversibility, immediacy of revocation, and the exact idempotency semantics (404 for re-deletes, never-existed IDs, and ownership-mismatch; all swallowed). It discloses the was_already_revoked discrimination signal and an existence-disclosure design detail an agent could not infer from annotations.
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 destructive effect and irreversibility notice before alternatives. The text is dense and long for one parameter, but each sentence carries operational weight (irreversibility, sibling routing, idempotency handling, ownership check, list visibility) rather than filler.
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 destructive, idempotent mutation with a schema-documented parameter and an output schema, the description supplies everything an agent needs: effect, reversibility, idempotency outcome branching, ownership caveat, and the post-delete visibility rule (excluded from list_qurls unless status: "revoked").
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% and the single parameter is fully documented there (public key / CRID / legacy r_ ID, q_ display IDs rejected), so the baseline is 3. The description adds meaningful provenance context, noting that when the ID comes from user input and ownership matters the agent should verify via get_qurl because a 404 is equally ambiguous there.
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 precise verb and scope: permanently revoke a qURL such that the link and every access token under it stop working immediately. It also carves out what it is not (links minted by upload tools / file connector) and names the sibling alternatives, so an agent can separate it from update_qurl, extend_qurl and token-level revocation without opening any 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?
Explicit when-to-use triggers are given (compromised link, departed user, end-of-engagement) plus explicit when-not-to-use routing to update_qurl for expiration/retag/rename and extend_qurl for keeping a link open. It further adds a prerequisite path via get_qurl when the ID came from user input and ownership matters.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
extend_qurlExtend qURL ExpirationA
Keep a qURL link open longer by pushing out the link's expiration by a relative duration. Pass a link's q_ display ID as resource_id, or a resource ID plus qurl_id; a resource with exactly one active link needs no qurl_id (unless it has more than the 100 links a read lists). Use this when the only change you need is more time on the clock. Use update_qurl_token instead to set an absolute expires_at or change the link's label, policy, or sessions. Use revoke_qurl_token or delete_qurl when you want to cut off access. Links minted by the upload tools belong to the file connector and cannot be extended from this server. Not idempotent: calling twice with the same extend_by extends the link twice; use update_qurl_token with expires_at when retries must not double-push. Requires qurl:write and qurl:read: it reads the resource before updating, to check the link and to return it, so each extend costs two API calls (three when the read does not list the link). A link cannot outlive its resource: if the resource's own expires_at is sooner, raise it with update_qurl first; the result then carries extend_warning. Link-selection problems return an error result; a rejected update throws with its HTTP status and error code. Returns the resource (same shape as get_qurl); the extended link's new expiry is in extended_link_expires_at (and qurls[].expires_at), not the resource's own expires_at.
| Name | Required | Description | Default |
|---|---|---|---|
| qurl_id | No | The link (q_ display ID) to extend. Required when the resource has more than one active link. | |
| extend_by | Yes | Duration to extend the link by (e.g., "24h", "7d"; 1m to 30d per call) | |
| resource_id | Yes | Resource ID, or a link's q_ display ID; a q_ ID selects that link, and this server reads the resource to find its parent |
Output Schema
| Name | Required | Description |
|---|---|---|
| crid | No | |
| slug | No | Immutable per-owner resource identity, when one was supplied at create time |
| tags | No | |
| qurls | No | |
| status | Yes | |
| private | No | |
| qurl_site | No | |
| created_at | Yes | |
| expires_at | Yes | |
| qurl_count | No | Number of access tokens minted for this resource |
| target_url | No | Underlying URL the qURL protects; omitted on connector-owned resources |
| description | No | |
| resource_id | Yes | Stable resource identifier (public key or legacy r_ ID) |
| custom_domain | No | |
| preserve_host | No | When true, the original Host header is preserved when proxying via the custom domain. Only meaningful when custom_domain is set; defaults to false on the API side. |
| extend_warning | No | Present when the resource's expiry cuts the extended link short; tell the user |
| consumer_access | No | |
| extended_qurl_id | No | The link that was extended |
| extended_link_expires_at | No | The extended link's new expiry; report this, not the resource's own expires_at |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations: discloses required scopes (qurl:write and qurl:read), the two-to-three API call cost, the resource-expires_at constraint plus extend_warning, error-vs-throw behavior, and the non-idempotent double-push consequence with a retry-safe alternative. This is exactly the actionable context annotations cannot carry.
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?
Dense but front-loaded, with the core mechanic first and constraints grouped afterward. It is long, and a few clauses (e.g. the 100-links parenthetical, the returned-field enumeration) could be trimmed, but each sentence carries substantive information rather than filler.
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 non-idempotent mutation with an output schema, the description covers prerequisites, side effects, retry safety, error semantics, and which return field holds the new expiry (extended_link_expires_at vs the resource's own expires_at). 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 coverage is 100%, so the baseline is 3, but the description adds real meaning: it explains that resource_id can be a q_ display ID selecting a link, that qurl_id is only needed for multi-link resources, and adds the sub-100-link nuance and extend_by's relative-duration semantics and range.
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 (extend) and resource (qURL link expiration) with the precise mechanism: pushing out expiration by a relative duration. It cleanly distinguishes itself from update_qurl_token, revoke_qurl_token, and delete_qurl by naming what each of those does instead.
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?
Explicitly says when to use it ('when the only change you need is more time on the clock'), names the alternatives for adjacent needs (absolute expiry, label/policy/sessions via update_qurl_token), and flags the exclusion (upload-tool links belong to the file connector and cannot be extended here). Nothing is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_qurlGet qURLARead-onlyIdempotent
Fetch a single qURL resource by ID and return its current state plus a bounded preview of access tokens. Use this when you have a specific resource ID (public key, CRID, or legacy r_ ID) or qURL display ID (q_ prefix) — q_ IDs are auto-resolved to their parent resource. Do not use this to create a new qURL for a freshly uploaded image, PDF, screenshot, or file attachment. For chat-uploaded file content in HTTP MCP mode, use upload_file_data_qurl instead. Use list_qurls instead when you need to discover qURLs by status, date range, or search query. Use resolve_qurl instead when you have an end-user access token (at_ prefix) and need to redeem it for the underlying URL. If the user asks for 'the qURL of this image/file', this tool is only correct when you already know the existing resource_id or qurl_id. qurls[] is an unordered preview capped by the API at 100 rows and may be omitted on list views, preview lookup failure, or redacted connector-owned resources; use qurl_count to detect that more token rows may exist. The one-shot qurl_link from creation is never returned here.
| Name | Required | Description | Default |
|---|---|---|---|
| resource_id | Yes | The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to fetch. If a q_ ID is passed, the API resolves it to the parent resource automatically. |
Output Schema
| Name | Required | Description |
|---|---|---|
| crid | No | |
| slug | No | Immutable per-owner resource identity, when one was supplied at create time |
| tags | No | |
| qurls | No | |
| status | Yes | |
| private | No | |
| qurl_site | No | |
| created_at | Yes | |
| expires_at | Yes | |
| qurl_count | No | Number of access tokens minted for this resource |
| target_url | No | Underlying URL the qURL protects; omitted on connector-owned resources |
| description | No | |
| resource_id | Yes | Stable resource identifier (public key or legacy r_ ID) |
| custom_domain | No | |
| preserve_host | No | When true, the original Host header is preserved when proxying via the custom domain. Only meaningful when custom_domain is set; defaults to false on the API side. |
| consumer_access | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover readOnly/idempotent/destructive, and the description goes well beyond them: q_ IDs auto-resolve to their parent, qurls[] is an unordered preview capped at 100 rows, may be omitted on list views or redacted connector resources, and the one-shot qurl_link is never returned. It also tells the agent to use qurl_count to detect 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?
Purpose is front-loaded and all sentences are functional rather than filler, but the paragraph is dense and mixes usage routing with output caveats, which slightly dilutes scanability. Every clause still earns 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?
Despite an existing output schema, the description proactively explains the non-obvious return semantics (unordered/capped preview, possible omission, qurl_link never returned) plus ID resolution behavior, leaving nothing an agent needs in order to call it correctly.
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% with a single required param, so the baseline is 3. The description adds genuine value beyond the schema by clarifying the accepted ID families and that q_ IDs auto-resolve to the parent resource, which the schema regex only implies.
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+resource: fetch a single qURL by ID, return current state plus bounded token preview. It explicitly differentiates from siblings (list_qurls, resolve_qurl, create_qurl, upload_file_data_qurl), so an agent can distinguish it without opening any 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?
Gives explicit when-to-use (you have a resource_id or q_ display ID), when-not (creating a qURL for a fresh upload; fetching chat-uploaded file content), and names three alternatives with the exact condition that selects each (discovery, token redemption, file content).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_qurlsList qURLsARead-onlyIdempotent
List qURL resources, paginated and optionally filtered by status, date range, or search text. When to use: discovery — finding qURLs by status (e.g. everything still active), auditing date ranges, or full-text search across descriptions and target URLs (via the q parameter). Filters AND together (e.g. status: 'active' + expires_before: '2026-05-01T00:00:00Z' returns active qURLs about to expire). When NOT to use: use get_qurl instead when you already have a specific resource ID — it returns the same per-resource shape more cheaply and includes the qurls[] per-token detail that list_qurls omits. Use resolve_qurl to actually open access to a target URL. Behavior: read-only and idempotent. An empty data[] with meta.has_more: false means no resource matched the filters (not an error). Pagination is cursor-based: when meta.has_more is true, pass meta.next_cursor as cursor on the next call to fetch the following page. Default page size is 20, configurable via limit up to 100. By default only active qURLs are returned; pass status: 'revoked' to see only revoked qURLs or 'active,revoked' to see both. A past resource expires_at does not change its status; individual access tokens can expire. status: 'expired' is not a valid resource filter. Sort defaults to created_at:desc; override with sort: 'expires_at:asc' etc. Returns: { data: QURL[], meta: { has_more: boolean, next_cursor?: string, page_size?: number, request_id?: string } } — each data[] item is the same stable resource shape returned by get_qurl minus per-token detail. Example: list_qurls({ status: "active", sort: "expires_at:asc", limit: 10 }) returns the 10 active qURLs expiring soonest.
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | Search query (searches description and target_url) | |
| sort | No | Sort field and direction as 'field:direction'. Valid fields: created_at, expires_at. Valid directions: asc, desc (default desc). Example: 'created_at:desc'. | |
| limit | No | Maximum number of qURLs to return (default: 20) | |
| cursor | No | Pagination cursor from a previous response | |
| status | No | Filter by status (comma-separated, e.g. 'active,revoked'). Defaults to 'active' when omitted; pass 'revoked' or 'active,revoked' to override. Only active and revoked are valid resource filters; expired is not accepted. | |
| created_after | No | Filter: created after this date (RFC 3339) | |
| expires_after | No | Filter: expires after this date (RFC 3339) | |
| created_before | No | Filter: created before this date (RFC 3339) | |
| expires_before | No | Filter: expires before this date (RFC 3339) |
Output Schema
| Name | Required | Description |
|---|---|---|
| data | Yes | |
| meta | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare read-only/idempotent/non-destructive, and the description adds substantial non-obvious behavior: empty results are not an error, cursor pagination via `meta.next_cursor`, default page size 20 (max 100), default status filter of `active`, and that `expired` is not a valid resource filter.
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 and cleanly partitioned into use/not-use/behavior/returns sections, so it scans well. It is long and the 'Returns' block restates the output shape that already exists as an output schema, which is mild redundancy.
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 9-parameter list tool with an output schema and full annotations, the description covers filtering semantics, pagination, defaults, sort behavior, and the no-match case. Nothing an agent needs to invoke 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; the description earns above that by explaining that filters AND together (with a worked status+expires_before example) and by clarifying status default/override semantics. Some detail (limit default, status default) still overlaps 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 ('List qURL resources') and immediately scopes it as paginated with optional filtering. It explicitly distinguishes itself from `get_qurl` and `resolve_qurl`, so an agent can route correctly without opening schemas.
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?
Contains explicit 'When to use' (discovery, auditing, full-text search) and 'When NOT to use' sections naming the preferred alternatives (`get_qurl` for known IDs, `resolve_qurl` to open access). This is the highest tier of routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_qurl_sessionsList qURL SessionsARead-onlyIdempotent
List active access sessions for a qURL resource. Use this to inspect who currently has live access before rotating, revoking, or terminating sessions. Use terminate_qurl_sessions when active sessions should be ended, and use get_qurl when you need token/resource metadata instead of active session state. Behavior: read-only and idempotent. Empty data[] means no active sessions for the resource.
| Name | Required | Description | Default |
|---|---|---|---|
| resource_id | Yes | The resource public key, CRID, or legacy r_ ID to list active sessions for. |
Output Schema
| Name | Required | Description |
|---|---|---|
| data | Yes | |
| meta | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already carry readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the bar is lowered. The description reinforces these and adds an interpretive detail: empty data[] means no active sessions. It does not elaborate on session expiration or auth requirements, but the annotations adequately cover the safety profile.
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?
Three tight sentences: primary action, usage alternatives, and a clear behavior note. Every sentence earns its place with no filler, and the most important information is front-loaded.
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 one-parameter read-only list tool with an output schema and comprehensive annotations, the description fully covers purpose, workflow, alternatives, and the meaning of an empty response. Nothing essential for correct invocation or selection 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%, with resource_id fully documented by type, pattern, and description. The description adds no parameter-specific detail, but none is required because the schema already carries the semantic load.
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 'List active access sessions for a qURL resource' – a specific verb and resource with a clear scope. It also distinguishes itself from siblings by clarifying that it shows live session state, not token/resource metadata (get_qurl) or session termination (terminate_qurl_sessions).
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?
Provides explicit when-to-use and when-not-to-use guidance: inspect sessions before rotating/revoking/terminating, use terminate_qurl_sessions when sessions should be ended, and use get_qurl when metadata is needed instead of active session state. This gives an agent concrete decision rules for tool selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
manage_crid_accessManage private CRID accessADestructive
Publisher access control. Invite takes only action, resource_id and email; omit expected_version, approval and policy. Report the exact invited email addresses without shortening them. Check every page of list_invitations before claiming nobody else was invited. If listing is denied, report the send receipts and say the list is unconfirmed; do not resend. Accepted delivery does not prove enrollment. Configure the next policy version, send or resend an email invitation, list invitations or consumer grants, approve or deny a request, approve a credential with id new, or revoke access. Read the current version before a decision; configuring the first policy on an existing private resource uses expected_version 0. For approve, put expected_version inside approval; zero is allowed only for id new. Existing requests use the listed decision id and version without credential fields. A six-digit code alone never authorizes approval on a resource with consumer_access. Ask which listed request the user wants to approve (the verified email for review); keys needs the intended public credential and contract needs a trusted entitlement receipt. Wait for that instruction. After an uncertain invite result, list invitations before retrying. Invite-only access has no consumer requests. Contract approval requires a trusted publisher entitlement receipt and expiry, not an unverified payment claim. A removal is complete only when session_cleanup is complete; retry the same removal/version while pending. Email proves mailbox control, not legal identity. Only listed fields are returned; invitation secrets and bearer links are never returned.
| Name | Required | Description | Default |
|---|---|---|---|
| id | No | ||
| No | |||
| limit | No | ||
| action | Yes | ||
| cursor | No | ||
| policy | No | ||
| approval | No | ||
| resource_id | Yes | The resource public key, CRID, or legacy r_ ID to manage access. | |
| expected_version | No | Current listed version for configure, resend, revoke_invitation, deny or revoke_grant. Omit on invites and listings; approve uses approval.expected_version. |
Output Schema
| Name | Required | Description |
|---|---|---|
| grant | No | |
| action | Yes | |
| grants | No | |
| resource | No | |
| invitation | No | |
| invitations | No | |
| next_cursor | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already flag destructive and non-idempotent behavior, and the description adds important operational context beyond them: accepted delivery does not prove enrollment, invitation secrets and bearer links are never returned, removal is complete only after session_cleanup, and denied listing should be reported as unconfirmed. These are exactly the behavioral caveats an agent needs.
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 description is dense and front-loads some invite-specific rules before laying out the broader action set, which makes it harder to scan. Most sentences carry useful operational information, but the telegraphic style and run-on sequencing reduce clarity and conciseness.
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 complex, destructive, multi-action tool with nested parameters and low schema description coverage, the description covers many critical safety and usage constraints. It is not fully complete because it does not explain all parameters (e.g., limit, cursor, policy modes) or permission requirements, but it is sufficient for careful invocation.
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 low at 22%, but the description compensates substantially by explaining action-specific fields: invite uses only action/resource_id/email, approve places expected_version inside approval, zero is allowed only for id new, and existing requests use the listed decision id and version without credential fields. It still leaves gaps around limit/cursor and several policy fields, so it is not fully complete.
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 identifies the resource as publisher access control for a private CRID and enumerates the supported actions (configure, invite, list, approve/deny, revoke). It is clear what the tool does, but it does not directly distinguish itself from siblings such as publish_crid or share_by_crid.
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 extensive action-specific guidance: invite should use only action/resource_id/email, omit expected_version/approval/policy; listing should page through results; after an uncertain invite, list before retrying; and do not resend when listing is denied. The guidance is strong but somewhat fragmented and does not explicitly compare against sibling tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mint_linkMint Access LinkA
Mint a fresh access link for an existing qURL resource — same one-shot display semantics as create_qurl.qurl_link. Use this to issue additional access links to a resource without creating a brand-new qURL (e.g. a second recipient, a replacement after the original was lost). Not for files from the upload tools: their links come from the file connector, so a link minted here on an upload's resource_id does not open the file; run the upload tool again instead. Use create_qurl instead when you want a brand-new resource with its own target_url and policy. Use update_qurl when you only want to change expiration/tags/description on the existing resource. Accepts resource public keys, CRIDs, legacy r_ IDs, and q_ display IDs. Constraints: expires_in and expires_at are mutually exclusive (handler returns an isError: true content block before any API call if both are set). If neither expiry field is specified, the API defaults to 24 hours from now. Output: the new qurl_link is shown ONCE — no subsequent call returns it. Capture qurl_id if you need to correlate future access events or update this specific token.
| Name | Required | Description | Default |
|---|---|---|---|
| label | No | Human-readable label identifying who this link is for (max 500 chars) | |
| expires_at | No | Absolute expiration timestamp (RFC 3339). Mutually exclusive with expires_in | |
| expires_in | No | Relative duration until expiration (e.g., "5m", "24h", "7d"; the API enforces 1m to 30d). Mutually exclusive with expires_at | |
| resource_id | Yes | The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to mint a new access link for. If a q_ ID is passed, the API resolves it to the parent resource automatically. | |
| max_sessions | No | Maximum concurrent sessions for this qURL token (0 = unlimited, max 1000) | |
| one_time_use | No | Whether this link can only be used once | |
| access_policy | No | Access control policy for this link | |
| session_duration | No | How long access lasts after clicking (e.g., "1h"). Rejected if it exceeds the parent resource's session-duration cap. |
Output Schema
| Name | Required | Description |
|---|---|---|
| type | No | Resource type echoed from the underlying resource |
| qurl_id | Yes | Display-friendly qURL ID (q_ prefix) for the minted token |
| qurl_link | Yes | Newly minted access link with one-shot display semantics, like create_qurl |
| expires_at | No | |
| branded_domain | No | Bare branded hostname for anchor text when the resource has a usable custom domain |
| email_delivery | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only convey the generic write/non-idempotent/open-world profile. The description adds behavior annotations cannot express: the link is displayed exactly once with no way to retrieve it later, capture qurl_id for correlation, the handler short-circuits with isError:true before any API call when both expiry fields are set, and a 24-hour default when neither is given. That is substantive beyond structured fields.
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 leads, alternatives and exclusions follow, then constraints and output notes — a sensible front-loaded order with no filler sentences. It is dense (inline code, bolded constraint/output markers) and slightly long, but nearly every clause carries operative guidance.
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?
With an output schema present the description need not enumerate return fields, yet it still supplies the critical non-obvious output behavior (one-shot link display, keep qurl_id). Combined with the sibling routing, upload caveat, and conflict/default rules, an agent has everything needed to call this correctly.
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 schema already documents resource_id ID formats, the expires_in/expires_at exclusivity, duration formats, and the access_policy nests. The description restates the ID-format acceptance and the mutual-exclusion constraint rather than adding new syntax or defaults, so it clears the baseline but does not exceed it.
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 ('Mint a fresh access link for an existing qURL resource') and immediately frames it against siblings by naming create_qurl and update_qurl with the conditions that separate them. An agent can distinguish this from every other qURL tool 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?
Explicit when-to-use (issue additional links to an existing resource for a second recipient or replacement) plus two named alternatives with their selection conditions (create_qurl for a new resource, update_qurl to change metadata). It also states when NOT to use it: links for upload-tool files come from the file connector, and a link minted on an upload's resource_id will not open the file. Nothing is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
publish_cridPublish a private CRIDA
Publish a URL with private access. Choose keys for approved credentials, invite_only for publisher email invitations with no access requests, email_capture for verified email capture with consent, review for verified email requests, or contract for a publisher backend entitlement receipt. This tool does not verify payment. Use version 1 for creation. Repeating this call creates another resource; after an uncertain result use get_qurl or list_qurls before retrying. For local apps use the qURL CLI. Public sharing uses create_qurl.
| Name | Required | Description | Default |
|---|---|---|---|
| policy | Yes | ||
| target_url | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| crid | Yes | |
| private | Yes | |
| resource_id | Yes | |
| consumer_access | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare idempotentHint=false and readOnlyHint=false, but the description adds valuable operational context: repeating the call creates another resource and the recommended recovery path via get_qurl/list_qurls. It also discloses the 'does not verify payment' caveat, though it does not describe permissions or auth requirements.
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 modes, then caveats and alternatives, with every sentence carrying information. It is dense and slightly list-like, but no sentence is wasted.
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?
An output schema exists, so return values need not be explained, and the description covers the mutation semantics, non-idempotency, and sibling routing. It stops short of describing auth/permission needs or what a CRID resource actually is, which would fully round out a write tool with a nested policy object.
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?
With 0% schema description coverage the description must carry the parameter burden, and it does explain all five mode values (keys, invite_only, email_capture, review, contract) with their access semantics. It does not define contract_id's relationship to contract mode or clarify target_url beyond what the schema implies, leaving a small gap.
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 names a specific verb and resource ('Publish a URL with private access') and explicitly distinguishes itself from the sibling create_qurl, which handles public sharing. An agent can separate publish_crid from create_qurl without opening either 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 gives explicit routing: use create_qurl for public sharing, get_qurl/list_qurls to resolve an uncertain result before retrying, and the qURL CLI for local apps. It also states the creation version (1), which is a concrete prerequisite rather than an inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
resolve_qurlResolve qURL Access TokenA
Redeem a qURL access token (the at_ prefix you pulled out of a qurl_link) to reveal the underlying URL and obtain a time-bound, IP-bound network access grant. Use this when an agent has been handed an access token and needs to fetch the protected resource — after a successful resolve, requests from access_grant.src_ip are permitted to the target_url for access_grant.expires_in seconds. Use get_qurl instead when you have a resource identifier (public key, CRID, or legacy r_ ID) or qURL display ID (q_) and want admin-side details rather than end-user redemption. Side-effects: consumes one use on one_time_use tokens, decrements max_sessions, and may trip access policies (IP/geo/UA/AI-agent denylists). idempotentHint: false because one-time-use tokens consume on each call; for non-one-time tokens within an active grant window, repeats are effectively no-ops, but the conservative annotation reflects worst-case behavior.
| Name | Required | Description | Default |
|---|---|---|---|
| access_token | Yes | The access token from a qURL link (e.g., at_k8xqp9h2sj9lx7r4a) |
Output Schema
| Name | Required | Description |
|---|---|---|
| target_url | Yes | Underlying URL revealed by the resolve |
| resource_id | Yes | |
| access_grant | Yes | Time-bound, IP-bound network access grant |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations by disclosing concrete side effects: consumes one-time-use tokens, decrements max_sessions, and may trip access policies. It also explains the idempotentHint:false annotation by distinguishing conservative worst-case behavior from the actual no-op behavior for non-one-time tokens within an active grant window.
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-organized and front-loaded: core action first, then when to use, then the alternative, then side effects, then idempotency nuance. Each sentence serves a distinct purpose and no space is wasted on repetition.
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 token-redemption tool with an output schema and annotations, the description covers the action, usage boundary, side effects, and annotation rationale. Everything an agent needs to decide to call it and interpret its behavior is present.
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% with an example value, but the description adds useful context: the token originates from a qurl_link and carries the at_ prefix. This helps agents recognize and source the parameter correctly without replacing schema documentation.
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?
Opens with a specific verb-object pair, 'Redeem a qURL access token', and states the outcome: reveal the underlying URL and obtain a time-bound, IP-bound grant. It also explicitly differentiates itself from get_qurl by token type vs resource identifier, making it unmistakable.
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?
Provides explicit when-to-use guidance ('Use this when an agent has been handed an access token and needs to fetch the protected resource') and explicit when-not-to-use guidance with the named alternative ('Use get_qurl instead when you have a resource identifier...'). No inference required.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
revoke_qurl_tokenRevoke qURL TokenADestructive
Revoke one qURL token under a resource without revoking the whole resource. Use this when a single recipient/link should stop working but sibling qURLs on the same resource_id must remain active. Use delete_qurl instead when you want to revoke the resource and every token under it. Constraints: requires the parent resource_id (public key, CRID, or legacy r_ ID) and the token display ID (q_…). Re-revoking an inactive token returns an API error rather than being treated as idempotent.
| Name | Required | Description | Default |
|---|---|---|---|
| qurl_id | Yes | The qURL display ID (q_ prefix) to revoke. | |
| resource_id | Yes | The resource public key, CRID, or legacy r_ ID to revoke a specific qURL token from. |
Output Schema
| Name | Required | Description |
|---|---|---|
| message | Yes | |
| qurl_id | Yes | |
| revoked | Yes | |
| resource_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark the tool as destructive and non-idempotent, and the description adds useful behavior beyond that: re-revoking an inactive token returns an API error rather than being idempotent. It also clarifies the operation is scoped so sibling tokens stay active, which adds context not fully carried by annotations.
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 description is front-loaded with the core action, immediately gives the use case, names the alternative tool, and ends with constraints and error behavior. Every sentence earns its place with no repetition or filler.
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 two-parameter mutation tool with an output schema, annotations, and sibling tools present, the description is complete: it explains what it does, when to choose it, when not to, required IDs, and non-idempotent behavior. 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 coverage is 100% and both params have descriptions and patterns, so the schema already does most of the work. The description adds a slight semantic benefit by labeling resource_id as the parent and tying qurl_id to the token display ID, but it does not fundamentally go beyond 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?
The description has a specific verb and resource: 'Revoke one qURL token under a resource without revoking the whole resource.' It clearly distinguishes this tool from delete_qurl by scoping the action to a single token rather than the entire resource.
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?
Explicitly states when to use: when a single recipient/link should stop working but sibling qURLs on the same resource_id must remain active. It also names the alternative, delete_qurl, and the condition for using that instead, which gives an agent a clear routing decision.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
terminate_qurl_sessionsTerminate qURL SessionsADestructive
Terminate active access sessions for a qURL resource. Use this after shortening access, rotating a link, or responding to a suspected leak when already-open sessions should end immediately. Pass session_id to terminate one active session; omit it to terminate all active sessions for the resource. Use list_qurl_sessions first when you need to inspect current sessions before taking action. Side-effects: existing access sessions are closed; qURL tokens themselves are not revoked.
| Name | Required | Description | Default |
|---|---|---|---|
| session_id | No | Specific session ID to terminate. Omit to terminate all active sessions for the resource. | |
| resource_id | Yes | The resource public key, CRID, or legacy r_ ID to terminate sessions for. |
Output Schema
| Name | Required | Description |
|---|---|---|
| message | Yes | |
| session_id | No | |
| terminated | Yes | |
| resource_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare destructiveHint=true and idempotentHint=false, so the description adds side-effect context ('existing access sessions are closed; qURL tokens themselves are not revoked'). This goes beyond annotations by specifying exactly what is destroyed and what is preserved, though it does not detail auth requirements or potential partial failures. No contradiction with annotations.
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?
Four sentences, each earning its place: purpose, use-case triggers, parameter semantics, and side-effects. Front-loaded with the core action and scoped alternatives, with no redundancy or filler.
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?
Given the tool's complexity, the schema covers both parameters, annotations cover safety traits, and an output schema exists, the description fully equips an agent to call it correctly. It includes when to use, how to scope, and side-effects, with no missing critical information.
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% and the schema already explains the optionality of session_id ('Omit to terminate all active sessions'). The description repeats this behavior without adding new information, so it provides no incremental value beyond the schema baseline of 3.
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 ('Terminate') and resource ('active access sessions for a qURL resource'), clearly distinguishing it from siblings like revoke_qurl_token by explicitly noting tokens are not revoked. It also references list_qurl_sessions for inspection, reinforcing its unique role.
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?
Explicitly states when to use it ('after shortening access, rotating a link, or responding to a suspected leak') and directs the agent to use list_qurl_sessions first for inspection. It also clarifies the omission of session_id as a deliberate all-terminate behavior, effectively covering alternatives and when-not scenarios.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
update_qurlUpdate qURLA
Update a qURL resource's expiration ceiling, tags, description, custom domain, or proxy host-header behavior. extend_by/expires_at here move the resource's own expiry, which does not keep an individual link open longer; use extend_qurl or update_qurl_token to change a link's expiry. Accepts resource public keys, CRIDs, legacy r_ IDs, and q_ display IDs for expiration, tags, and description updates (q_ is auto-resolved); custom domain and preserve_host updates require a resource identifier (public key, CRID, or legacy r_ ID) because the qURL API now serves them from PATCH /v1/resources/{id}. Constraints: extend_by and expires_at are mutually exclusive; custom_domain/preserve_host cannot be combined with expiration changes in one call; at least one update field (extend_by, expires_at, tags, description, custom_domain, preserve_host) must be set. Clearing fields: pass description: "", tags: [], or custom_domain: "" to clear those fields explicitly. Use delete_qurl when you want to revoke entirely. Errors: a malformed or out-of-range extend_by is rejected by input validation; if the input fails schema refinements (both extend_by + expires_at, or no fields set), the handler returns an isError: true content block. Both happen before any API call. Other API errors throw with the API's code/statusCode. Returns the updated resource (same shape as get_qurl).
| Name | Required | Description | Default |
|---|---|---|---|
| tags | No | Replace all tags on this resource (max 10 tags, each 1-50 chars) | |
| extend_by | No | Duration to extend by (e.g., "24h", "7d"; 1m to 30d per call; use expires_at for longer). Mutually exclusive with expires_at. | |
| expires_at | No | Absolute expiration timestamp (RFC 3339). Mutually exclusive with extend_by. | |
| description | No | Replace the resource description (max 500 chars) | |
| resource_id | Yes | The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to update. If a q_ ID is passed, the API resolves it to the parent resource automatically. | |
| custom_domain | No | Replace the custom domain bound to this resource (max 253 chars, must be registered/active/owned). Pass "" to clear. | |
| preserve_host | No | Whether to preserve the original Host header when proxying via the custom domain. Only meaningful when custom_domain is set; default false on the API side. |
Output Schema
| Name | Required | Description |
|---|---|---|
| crid | No | |
| slug | No | Immutable per-owner resource identity, when one was supplied at create time |
| tags | No | |
| qurls | No | |
| status | Yes | |
| private | No | |
| qurl_site | No | |
| created_at | Yes | |
| expires_at | Yes | |
| qurl_count | No | Number of access tokens minted for this resource |
| target_url | No | Underlying URL the qURL protects; omitted on connector-owned resources |
| description | No | |
| resource_id | Yes | Stable resource identifier (public key or legacy r_ ID) |
| custom_domain | No | |
| preserve_host | No | When true, the original Host header is preserved when proxying via the custom domain. Only meaningful when custom_domain is set; defaults to false on the API side. |
| consumer_access | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations declare non-read-only, non-destructive, non-idempotent, open-world behavior, and the description adds substantial context beyond them: explicit field-clearing semantics ("", []), which identifier types are accepted per field and why (PATCH /v1/resources/{id} for domain/preserve_host), and precise error behavior including the isError content block returned before any API call for schema refinement failures.
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 and alternatives, then constraints, clearing, and errors in labeled bold sections. It is dense but every section is actionable; minor redundancy exists where the mutual-exclusivity and at-least-one-field rules are stated both in the constraints block and echoed in the errors paragraph.
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 7-parameter mutation tool with full schema coverage, an output schema, and non-trivial annotation coverage, the description covers routing, constraints, clearing, identifier requirements, and failure modes, and points to get_qurl for the return shape. 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 description coverage is 100%, so the baseline is 3, but the description goes further by explaining clearing behavior for description/tags/custom_domain and the identifier-type restrictions for custom domain and preserve_host (q_ IDs not accepted there). It largely restates the extend_by/expires_at mutual exclusion already present in the schema, so it is not a full 5.
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 opens with a specific verb+resource ("Update a qURL resource") and enumerates the mutable fields (expiration ceiling, tags, description, custom domain, proxy host-header behavior), immediately distinguishing it from siblings like extend_qurl and update_qurl_token, which it names.
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?
Explicit routing guidance is provided: use extend_qurl or update_qurl_token to change an individual link's expiry, and use delete_qurl to revoke entirely. It also states the constraint conditions (at least one field required, mutual exclusivity, no combining domain changes with expiration changes) that determine when the tool can be used at all.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
update_qurl_tokenUpdate qURL TokenA
Update one qURL token under a resource: expiration, label, access policy, max sessions, or session duration. Use this when you need to change a specific q_… token without changing sibling tokens or resource-level metadata. Use update_qurl instead for resource-level description/tags/custom-domain changes, and use revoke_qurl_token when the token should stop working entirely. Constraints: extend_by and expires_at are mutually exclusive; at least one token update field must be set. Returns the updated token summary.
| Name | Required | Description | Default |
|---|---|---|---|
| label | No | Human-readable label for this token | |
| qurl_id | Yes | The qURL display ID (q_ prefix) to update. | |
| extend_by | No | Duration to extend this token by (e.g., "24h", "7d"; 1m to 30d per call). Mutually exclusive with expires_at. | |
| expires_at | No | Absolute token expiration timestamp (RFC 3339). Mutually exclusive with extend_by. | |
| resource_id | Yes | The resource public key, CRID, or legacy r_ ID to update a specific qURL token under. | |
| max_sessions | No | Maximum concurrent sessions for this token (0 = unlimited, max 1000) | |
| access_policy | No | Replace the access policy for this token | |
| session_duration | No | How long access lasts after clicking (e.g., "1h"; 1s to 24h). Empty string applies the parent resource cap when one is set. |
Output Schema
| Name | Required | Description |
|---|---|---|
| label | No | |
| status | Yes | Per-token status (wider than resource status — tokens may be consumed/expired independently) |
| qurl_id | Yes | |
| qurl_site | No | |
| use_count | No | |
| created_at | No | |
| expires_at | No | |
| max_sessions | No | |
| one_time_use | No | |
| access_policy | No | Access control policy snapshot for this token |
| session_duration | No | Seconds of access granted after a successful resolve |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=false, destructiveHint=false, idempotentHint=false, so the safety profile is covered. The description adds real behavioral context beyond them: the extend_by/expires_at mutual exclusion, the requirement that at least one update field be set, and the fact that the updated token summary is returned. It does not, however, disclose auth requirements, whether changes affect already-active sessions, or that access_policy is a wholesale replace (only the schema notes that).
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?
Three sentences, front-loaded: scope and updatable fields first, sibling routing second, hard constraints last. No sentence is redundant with another, and the constraints are bolded and compressed rather than prose-padded.
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 an 8-parameter mutation tool with nested objects, an output schema, and annotations covering safety hints, the description supplies what structured fields cannot: sibling routing and cross-field validation rules. Because an output schema exists, it correctly does not spend words explaining the return payload.
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 and the schema already documents each field including the extend_by/expires_at exclusivity. The description nevertheless contributes one genuine cross-parameter constraint not present in the schema — 'at least one token update field must be set' — which is exactly the kind of validation rule that prevents a failed call.
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?
Opens with a specific verb+resource ('Update one qURL token under a resource') and enumerates exactly which fields can change (expiration, label, access policy, max sessions, session duration). It explicitly names the sibling tools it is not (update_qurl, revoke_qurl_token) and the token-scoped boundary, so an agent can distinguish it from the resource-level and revoke operations 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?
States the selection condition ('change a specific q_… token without changing sibling tokens or resource-level metadata') and then routes to the two competing alternatives by name with the condition that selects each: update_qurl for description/tags/custom-domain, revoke_qurl_token when the token should stop working. Nothing is left to inference.
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.
1 tool update
v0.2.6- Changed
manage_crid_access1 field changed- added
Input schema / properties / expected_version / descriptionAdded value: +"Current listed version for configure, resend, revoke_invitation, deny or revoke_grant. Omit on invites and listings; approve uses approval.expected_version."
1 tool update
v0.2.3- Changed
manage_crid_access2 fields changed- changed
Output schema / properties / grant / properties / credential_kind / enumPrevious value: -[ - "passkey", - "device" -]New value: +[ + "passkey", + "device", + "email" +] - changed
Output schema / properties / grants / items / properties / credential_kind / enumPrevious value: -[ - "passkey", - "device" -]New value: +[ + "passkey", + "device", + "email" +]
11 tool updates
v0.2.2- Changed
batch_create_qurls1 field changed- changed
Input schema / properties / items / items / properties / expires_in / descriptionPrevious value: -"Duration string (e.g., \"1h\", \"24h\", \"7d\")"New value: +"Duration string (e.g., \"1h\", \"24h\", \"7d\"; the API enforces 1m to 30d)"
- Changed
create_qurl1 field changed- changed
Input schema / properties / expires_in / descriptionPrevious value: -"Duration string (e.g., \"1h\", \"24h\", \"7d\")"New value: +"Duration string (e.g., \"1h\", \"24h\", \"7d\"; the API enforces 1m to 30d)"
- Changed
extend_qurl11 fields changed- changed
Input schema / properties / extend_by / descriptionPrevious value: -"Duration to extend by (e.g., \"24h\", \"7d\")"New value: +"Duration to extend the link by (e.g., \"24h\", \"7d\"; 1m to 30d per call)" - added
Input schema / properties / extend_by / maxLengthAdded value: +32 - removed
Input schema / properties / extend_by / minLengthRemoved value: -1 - added
Input schema / properties / qurl_idAdded value: +{ + "description": "The link (q_ display ID) to extend. Required when the resource has more than one active link.", + "pattern": "^q_[0-9a-f]{11}$", + "type": "string" +} - changed
Input schema / properties / resource_id / descriptionPrevious value: -"The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to extend. If a q_ ID is passed, the API resolves it to the parent resource automatically."New value: +"Resource ID, or a link's q_ display ID; a q_ ID selects that link, and this server reads the resource to find its parent" - added
Output schema / properties / consumer_accessAdded value: +{ + "additionalProperties": false, + "properties": { + "contract_id": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": [ + "mode" + ], + "type": "object" +} - added
Output schema / properties / cridAdded value: +{ + "type": "string" +} - added
Output schema / properties / extend_warningAdded value: +{ + "description": "Present when the resource's expiry cuts the extended link short; tell the user", + "type": "string" +} - added
Output schema / properties / extended_link_expires_atAdded value: +{ + "description": "The extended link's new expiry; report this, not the resource's own expires_at", + "type": "string" +} - added
Output schema / properties / extended_qurl_idAdded value: +{ + "description": "The link that was extended", + "type": "string" +} - added
Output schema / properties / privateAdded value: +{ + "type": "boolean" +}
- Changed
get_qurl3 fields changed- added
Output schema / properties / consumer_accessAdded value: +{ + "additionalProperties": false, + "properties": { + "contract_id": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": [ + "mode" + ], + "type": "object" +} - added
Output schema / properties / cridAdded value: +{ + "type": "string" +} - added
Output schema / properties / privateAdded value: +{ + "type": "boolean" +}
- Changed
list_qurls3 fields changed- added
Output schema / properties / data / items / properties / consumer_accessAdded value: +{ + "additionalProperties": false, + "properties": { + "contract_id": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": [ + "mode" + ], + "type": "object" +} - added
Output schema / properties / data / items / properties / cridAdded value: +{ + "type": "string" +} - added
Output schema / properties / data / items / properties / privateAdded value: +{ + "type": "boolean" +}
- Added
manage_crid_access - Changed
mint_link1 field changed- changed
Input schema / properties / expires_in / descriptionPrevious value: -"Relative duration until expiration (e.g., \"5m\", \"24h\", \"7d\"). Mutually exclusive with expires_at"New value: +"Relative duration until expiration (e.g., \"5m\", \"24h\", \"7d\"; the API enforces 1m to 30d). Mutually exclusive with expires_at"
- Added
publish_crid - Added
share_by_crid - Changed
update_qurl6 fields changed- changed
Input schema / properties / extend_by / descriptionPrevious value: -"Duration to extend by (e.g., \"24h\", \"7d\"). Mutually exclusive with expires_at."New value: +"Duration to extend by (e.g., \"24h\", \"7d\"; 1m to 30d per call; use expires_at for longer). Mutually exclusive with expires_at." - added
Input schema / properties / extend_by / maxLengthAdded value: +32 - removed
Input schema / properties / extend_by / minLengthRemoved value: -1 - added
Output schema / properties / consumer_accessAdded value: +{ + "additionalProperties": false, + "properties": { + "contract_id": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": [ + "mode" + ], + "type": "object" +} - added
Output schema / properties / cridAdded value: +{ + "type": "string" +} - added
Output schema / properties / privateAdded value: +{ + "type": "boolean" +}
- Changed
update_qurl_token5 fields changed- changed
Input schema / properties / extend_by / descriptionPrevious value: -"Duration to extend this token by (e.g., \"24h\", \"7d\"). Mutually exclusive with expires_at."New value: +"Duration to extend this token by (e.g., \"24h\", \"7d\"; 1m to 30d per call). Mutually exclusive with expires_at." - added
Input schema / properties / extend_by / maxLengthAdded value: +32 - removed
Input schema / properties / extend_by / minLengthRemoved value: -1 - changed
Input schema / properties / session_duration / descriptionPrevious value: -"How long access lasts after clicking (e.g., \"1h\"). Empty string applies the parent resource cap when one is set."New value: +"How long access lasts after clicking (e.g., \"1h\"; 1s to 24h). Empty string applies the parent resource cap when one is set." - added
Input schema / properties / session_duration / maxLengthAdded value: +32
13 tool updates
v0.2.1- Changed
batch_create_qurls38 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - removed
Input schema / properties / items / items / additionalPropertiesRemoved value: -false - removed
Input schema / properties / items / items / properties / access_policy / additionalPropertiesRemoved value: -false - removed
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / additionalPropertiesRemoved value: -false - changed
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / descriptionPrevious value: -"AI agent categories to permit (all others blocked)"New value: +"AI agent categories to permit (all others blocked; max 20 entries, 128 chars each)" - added
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / items / maxLengthAdded value: +128 - added
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / items / minLengthAdded value: +1 - added
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / maxItemsAdded value: +20 - changed
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / descriptionPrevious value: -"AI agent categories to block (e.g., gptbot, commoncrawl)"New value: +"AI agent categories to block (e.g., gptbot, commoncrawl; max 20 entries, 128 chars each)" - added
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / items / maxLengthAdded value: +128 - added
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / items / minLengthAdded value: +1 - added
Input schema / properties / items / items / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / maxItemsAdded value: +20 - changed
Input schema / properties / items / items / properties / access_policy / properties / geo_allowlist / descriptionPrevious value: -"Allowed country codes (ISO 3166-1 alpha-2)"New value: +"Allowed country codes (ISO 3166-1 alpha-2; max 50 entries, 8 chars each)" - added
Input schema / properties / items / items / properties / access_policy / properties / geo_allowlist / items / maxLengthAdded value: +8 - added
Input schema / properties / items / items / properties / access_policy / properties / geo_allowlist / items / minLengthAdded value: +1 - added
Input schema / properties / items / items / properties / access_policy / properties / geo_allowlist / maxItemsAdded value: +50 - changed
Input schema / properties / items / items / properties / access_policy / properties / geo_denylist / descriptionPrevious value: -"Denied country codes (ISO 3166-1 alpha-2)"New value: +"Denied country codes (ISO 3166-1 alpha-2; max 50 entries, 8 chars each)" - added
Input schema / properties / items / items / properties / access_policy / properties / geo_denylist / items / maxLengthAdded value: +8 - added
Input schema / properties / items / items / properties / access_policy / properties / geo_denylist / items / minLengthAdded value: +1 - added
Input schema / properties / items / items / properties / access_policy / properties / geo_denylist / maxItemsAdded value: +50 - changed
Input schema / properties / items / items / properties / access_policy / properties / ip_allowlist / descriptionPrevious value: -"Allowed IP addresses or CIDR ranges"New value: +"Allowed IP addresses or CIDR ranges (max 100 entries, 64 chars each)" - added
Input schema / properties / items / items / properties / access_policy / properties / ip_allowlist / items / maxLengthAdded value: +64 - added
Input schema / properties / items / items / properties / access_policy / properties / ip_allowlist / items / minLengthAdded value: +1 - added
Input schema / properties / items / items / properties / access_policy / properties / ip_allowlist / maxItemsAdded value: +100 - changed
Input schema / properties / items / items / properties / access_policy / properties / ip_denylist / descriptionPrevious value: -"Denied IP addresses or CIDR ranges"New value: +"Denied IP addresses or CIDR ranges (max 100 entries, 64 chars each)" - added
Input schema / properties / items / items / properties / access_policy / properties / ip_denylist / items / maxLengthAdded value: +64 - added
Input schema / properties / items / items / properties / access_policy / properties / ip_denylist / items / minLengthAdded value: +1 - added
Input schema / properties / items / items / properties / access_policy / properties / ip_denylist / maxItemsAdded value: +100 - changed
Input schema / properties / items / items / properties / access_policy / properties / user_agent_allow_regex / descriptionPrevious value: -"Regex to allow matching user agents"New value: +"Regex to allow matching user agents (max 256 chars)" - added
Input schema / properties / items / items / properties / access_policy / properties / user_agent_allow_regex / maxLengthAdded value: +256 - changed
Input schema / properties / items / items / properties / access_policy / properties / user_agent_deny_regex / descriptionPrevious value: -"Regex to deny matching user agents"New value: +"Regex to deny matching user agents (max 256 chars)" - added
Input schema / properties / items / items / properties / access_policy / properties / user_agent_deny_regex / maxLengthAdded value: +256 - changed
Input schema / properties / items / items / properties / custom_domain / descriptionPrevious value: -"Custom domain to assign to the auto-created resource"New value: +"Custom domain to assign to the auto-created resource (max 253 chars, must be registered/active/owned)." - added
Input schema / properties / items / items / properties / custom_domain / maxLengthAdded value: +253 - changed
Input schema / properties / items / items / properties / max_sessions / descriptionPrevious value: -"Maximum concurrent sessions (0 = unlimited, max 1000)"New value: +"Maximum concurrent sessions for this qURL token (0 = unlimited when one_time_use is explicitly false; max 1000)" - changed
Input schema / properties / items / items / properties / session_duration / descriptionPrevious value: -"How long access lasts after clicking (e.g., \"1h\")"New value: +"How long access lasts after the recipient reaches the content (e.g., \"1h\"). This anchors the resource-level session-duration cap when a new resource is created." - added
Input schema / properties / items / items / properties / typeAdded value: +{ + "description": "Resource type for integrations allowed to mint non-url qURLs (max 64 chars). Defaults to url.", + "maxLength": 64, + "type": "string" +} - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "failed": { + "type": "number" + }, + "request_id": { + "type": "string" + }, + "results": { + "items": { + "description": "Per-item result. Discriminated on `success`.", + "oneOf": [ + { + "additionalProperties": {}, + "properties": { + "branded_domain": { + "description": "Bare branded hostname for anchor text when the resource has a usable custom domain", + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "index": { + "description": "Index of the corresponding item in the input `items` array", + "type": "number" + }, + "qurl_link": { + "description": "One-shot display access link — shown ONCE on creation", + "type": "string" + }, + "qurl_site": { + "type": "string" + }, + "resource_id": { + "type": "string" + }, + "success": { + "const": true, + "type": "boolean" + } + }, + "required": [ + "index", + "success", + "resource_id", + "qurl_link", + "qurl_site", + "expires_at" + ], + "type": "object" + }, + { + "additionalProperties": {}, + "properties": { + "error": { + "additionalProperties": {}, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "index": { + "description": "Index of the corresponding item in the input `items` array", + "type": "number" + }, + "success": { + "const": false, + "type": "boolean" + } + }, + "required": [ + "index", + "success", + "error" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "succeeded": { + "type": "number" + } + }, + "required": [ + "succeeded", + "failed", + "results" + ], + "type": "object" +}
- Changed
create_qurl37 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - removed
Input schema / properties / access_policy / additionalPropertiesRemoved value: -false - removed
Input schema / properties / access_policy / properties / ai_agent_policy / additionalPropertiesRemoved value: -false - changed
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / descriptionPrevious value: -"AI agent categories to permit (all others blocked)"New value: +"AI agent categories to permit (all others blocked; max 20 entries, 128 chars each)" - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / items / maxLengthAdded value: +128 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / maxItemsAdded value: +20 - changed
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / descriptionPrevious value: -"AI agent categories to block (e.g., gptbot, commoncrawl)"New value: +"AI agent categories to block (e.g., gptbot, commoncrawl; max 20 entries, 128 chars each)" - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / items / maxLengthAdded value: +128 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / maxItemsAdded value: +20 - changed
Input schema / properties / access_policy / properties / geo_allowlist / descriptionPrevious value: -"Allowed country codes (ISO 3166-1 alpha-2)"New value: +"Allowed country codes (ISO 3166-1 alpha-2; max 50 entries, 8 chars each)" - added
Input schema / properties / access_policy / properties / geo_allowlist / items / maxLengthAdded value: +8 - added
Input schema / properties / access_policy / properties / geo_allowlist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / geo_allowlist / maxItemsAdded value: +50 - changed
Input schema / properties / access_policy / properties / geo_denylist / descriptionPrevious value: -"Denied country codes (ISO 3166-1 alpha-2)"New value: +"Denied country codes (ISO 3166-1 alpha-2; max 50 entries, 8 chars each)" - added
Input schema / properties / access_policy / properties / geo_denylist / items / maxLengthAdded value: +8 - added
Input schema / properties / access_policy / properties / geo_denylist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / geo_denylist / maxItemsAdded value: +50 - changed
Input schema / properties / access_policy / properties / ip_allowlist / descriptionPrevious value: -"Allowed IP addresses or CIDR ranges"New value: +"Allowed IP addresses or CIDR ranges (max 100 entries, 64 chars each)" - added
Input schema / properties / access_policy / properties / ip_allowlist / items / maxLengthAdded value: +64 - added
Input schema / properties / access_policy / properties / ip_allowlist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ip_allowlist / maxItemsAdded value: +100 - changed
Input schema / properties / access_policy / properties / ip_denylist / descriptionPrevious value: -"Denied IP addresses or CIDR ranges"New value: +"Denied IP addresses or CIDR ranges (max 100 entries, 64 chars each)" - added
Input schema / properties / access_policy / properties / ip_denylist / items / maxLengthAdded value: +64 - added
Input schema / properties / access_policy / properties / ip_denylist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ip_denylist / maxItemsAdded value: +100 - changed
Input schema / properties / access_policy / properties / user_agent_allow_regex / descriptionPrevious value: -"Regex to allow matching user agents"New value: +"Regex to allow matching user agents (max 256 chars)" - added
Input schema / properties / access_policy / properties / user_agent_allow_regex / maxLengthAdded value: +256 - changed
Input schema / properties / access_policy / properties / user_agent_deny_regex / descriptionPrevious value: -"Regex to deny matching user agents"New value: +"Regex to deny matching user agents (max 256 chars)" - added
Input schema / properties / access_policy / properties / user_agent_deny_regex / maxLengthAdded value: +256 - changed
Input schema / properties / custom_domain / descriptionPrevious value: -"Custom domain to assign to the auto-created resource"New value: +"Custom domain to assign to the auto-created resource (max 253 chars, must be registered/active/owned)." - added
Input schema / properties / custom_domain / maxLengthAdded value: +253 - changed
Input schema / properties / max_sessions / descriptionPrevious value: -"Maximum concurrent sessions (0 = unlimited, max 1000)"New value: +"Maximum concurrent sessions for this qURL token (0 = unlimited when one_time_use is explicitly false; max 1000)" - changed
Input schema / properties / session_duration / descriptionPrevious value: -"How long access lasts after clicking (e.g., \"1h\")"New value: +"How long access lasts after the recipient reaches the content (e.g., \"1h\"). This anchors the resource-level session-duration cap when a new resource is created." - added
Input schema / properties / typeAdded value: +{ + "description": "Resource type for integrations allowed to mint non-url qURLs (max 64 chars). Defaults to url.", + "maxLength": 64, + "type": "string" +} - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "branded_domain": { + "description": "Bare branded hostname for anchor text when the resource has a usable custom domain", + "type": "string" + }, + "email_delivery": { + "additionalProperties": false, + "properties": { + "attempted": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "failed": { + "type": "number" + }, + "recipients": { + "items": { + "type": "string" + }, + "type": "array" + }, + "results": { + "items": { + "additionalProperties": false, + "properties": { + "email": { + "type": "string" + }, + "error": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "skipped": { + "type": "boolean" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "email", + "success", + "skipped" + ], + "type": "object" + }, + "type": "array" + }, + "sent": { + "type": "number" + }, + "skipped_reason": { + "type": "string" + } + }, + "required": [ + "attempted", + "enabled", + "recipients", + "sent", + "failed", + "results" + ], + "type": "object" + }, + "expires_at": { + "type": "string" + }, + "label": { + "type": "string" + }, + "qurl_id": { + "description": "Display-friendly qURL ID (q_ prefix)", + "type": "string" + }, + "qurl_link": { + "description": "One-shot display access link — shown ONCE on creation, never returned again. Share immediately.", + "type": "string" + }, + "qurl_site": { + "type": "string" + }, + "resource_id": { + "description": "Stable resource identifier (public key or legacy r_ ID)", + "type": "string" + }, + "type": { + "description": "Resource type echoed from the create request", + "type": "string" + } + }, + "required": [ + "qurl_id", + "resource_id", + "qurl_link" + ], + "type": "object" +}
- Changed
delete_qurl5 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - changed
Input schema / properties / resource_id / descriptionPrevious value: -"The resource ID (must start with r_). delete_qurl does not accept q_ (qURL display) IDs."New value: +"The resource public key, CRID, or legacy r_ ID to revoke (all tokens; q_ display IDs are not accepted)." - removed
Input schema / properties / resource_id / minLengthRemoved value: -1 - changed
Input schema / properties / resource_id / patternPrevious value: -"^r\\_"New value: +"^(r_[a-z0-9_-]{11}|(?:[A-Za-z0-9_-]{4}){27,53}|(?:[A-Za-z0-9_-]{4}){27,53}[A-Za-z0-9_-]{2}|(?:[A-Za-z0-9_-]{4}){26,52}[A-Za-z0-9_-]{3}|[a-z2-7]{47}|[a-z2-7]{60})$" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "message": { + "description": "Human-readable confirmation message", + "type": "string" + }, + "resource_id": { + "type": "string" + }, + "revoked": { + "const": true, + "type": "boolean" + }, + "was_already_revoked": { + "description": "True when the API responded 404 (resource was already revoked or never existed). Agents that need to distinguish 'I revoked it' from 'it was already gone' should branch on this.", + "type": "boolean" + } + }, + "required": [ + "resource_id", + "revoked", + "was_already_revoked", + "message" + ], + "type": "object" +}
- Changed
extend_qurl5 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - changed
Input schema / properties / resource_id / descriptionPrevious value: -"The resource ID (r_ prefix) or qURL display ID (q_ prefix) to extend. If a q_ ID is passed, the API resolves it to the parent resource automatically."New value: +"The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to extend. If a q_ ID is passed, the API resolves it to the parent resource automatically." - removed
Input schema / properties / resource_id / minLengthRemoved value: -1 - added
Input schema / properties / resource_id / patternAdded value: +"(?:^(r_[a-z0-9_-]{11}|(?:[A-Za-z0-9_-]{4}){27,53}|(?:[A-Za-z0-9_-]{4}){27,53}[A-Za-z0-9_-]{2}|(?:[A-Za-z0-9_-]{4}){26,52}[A-Za-z0-9_-]{3}|[a-z2-7]{47}|[a-z2-7]{60})$)|(?:^q_[0-9a-f]{11}$)" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "created_at": { + "type": "string" + }, + "custom_domain": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "preserve_host": { + "description": "When true, the original Host header is preserved when proxying via the custom domain. Only meaningful when custom_domain is set; defaults to false on the API side.", + "type": "boolean" + }, + "qurl_count": { + "description": "Number of access tokens minted for this resource", + "type": "number" + }, + "qurl_site": { + "type": "string" + }, + "qurls": { + "items": { + "additionalProperties": {}, + "description": "Single access token belonging to a qURL resource", + "properties": { + "access_policy": { + "additionalProperties": {}, + "description": "Access control policy snapshot for this token", + "properties": { + "ai_agent_policy": { + "additionalProperties": {}, + "properties": { + "allow_categories": { + "items": { + "type": "string" + }, + "type": "array" + }, + "block_all": { + "type": "boolean" + }, + "deny_categories": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "geo_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "geo_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "user_agent_allow_regex": { + "type": "string" + }, + "user_agent_deny_regex": { + "type": "string" + } + }, + "type": "object" + }, + "created_at": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "label": { + "type": "string" + }, + "max_sessions": { + "type": "number" + }, + "one_time_use": { + "type": "boolean" + }, + "qurl_id": { + "type": "string" + }, + "qurl_site": { + "type": "string" + }, + "session_duration": { + "description": "Seconds of access granted after a successful resolve", + "type": "number" + }, + "status": { + "default": "unknown", + "description": "Per-token status (wider than resource status — tokens may be consumed/expired independently)", + "enum": [ + "active", + "consumed", + "expired", + "revoked", + "unknown" + ], + "type": "string" + }, + "use_count": { + "type": "number" + } + }, + "required": [ + "qurl_id", + "status" + ], + "type": "object" + }, + "type": "array" + }, + "resource_id": { + "description": "Stable resource identifier (public key or legacy r_ ID)", + "type": "string" + }, + "slug": { + "description": "Immutable per-owner resource identity, when one was supplied at create time", + "type": "string" + }, + "status": { + "default": "unknown", + "enum": [ + "active", + "revoked", + "expired", + "unknown" + ], + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "target_url": { + "description": "Underlying URL the qURL protects; omitted on connector-owned resources", + "type": "string" + } + }, + "required": [ + "resource_id", + "expires_at", + "created_at", + "status" + ], + "type": "object" +}
- Changed
get_qurl5 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - changed
Input schema / properties / resource_id / descriptionPrevious value: -"The resource ID (r_ prefix) or qURL display ID (q_ prefix) to fetch. If a q_ ID is passed, the API resolves it to the parent resource automatically."New value: +"The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to fetch. If a q_ ID is passed, the API resolves it to the parent resource automatically." - removed
Input schema / properties / resource_id / minLengthRemoved value: -1 - added
Input schema / properties / resource_id / patternAdded value: +"(?:^(r_[a-z0-9_-]{11}|(?:[A-Za-z0-9_-]{4}){27,53}|(?:[A-Za-z0-9_-]{4}){27,53}[A-Za-z0-9_-]{2}|(?:[A-Za-z0-9_-]{4}){26,52}[A-Za-z0-9_-]{3}|[a-z2-7]{47}|[a-z2-7]{60})$)|(?:^q_[0-9a-f]{11}$)" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "created_at": { + "type": "string" + }, + "custom_domain": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "preserve_host": { + "description": "When true, the original Host header is preserved when proxying via the custom domain. Only meaningful when custom_domain is set; defaults to false on the API side.", + "type": "boolean" + }, + "qurl_count": { + "description": "Number of access tokens minted for this resource", + "type": "number" + }, + "qurl_site": { + "type": "string" + }, + "qurls": { + "items": { + "additionalProperties": {}, + "description": "Single access token belonging to a qURL resource", + "properties": { + "access_policy": { + "additionalProperties": {}, + "description": "Access control policy snapshot for this token", + "properties": { + "ai_agent_policy": { + "additionalProperties": {}, + "properties": { + "allow_categories": { + "items": { + "type": "string" + }, + "type": "array" + }, + "block_all": { + "type": "boolean" + }, + "deny_categories": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "geo_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "geo_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "user_agent_allow_regex": { + "type": "string" + }, + "user_agent_deny_regex": { + "type": "string" + } + }, + "type": "object" + }, + "created_at": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "label": { + "type": "string" + }, + "max_sessions": { + "type": "number" + }, + "one_time_use": { + "type": "boolean" + }, + "qurl_id": { + "type": "string" + }, + "qurl_site": { + "type": "string" + }, + "session_duration": { + "description": "Seconds of access granted after a successful resolve", + "type": "number" + }, + "status": { + "default": "unknown", + "description": "Per-token status (wider than resource status — tokens may be consumed/expired independently)", + "enum": [ + "active", + "consumed", + "expired", + "revoked", + "unknown" + ], + "type": "string" + }, + "use_count": { + "type": "number" + } + }, + "required": [ + "qurl_id", + "status" + ], + "type": "object" + }, + "type": "array" + }, + "resource_id": { + "description": "Stable resource identifier (public key or legacy r_ ID)", + "type": "string" + }, + "slug": { + "description": "Immutable per-owner resource identity, when one was supplied at create time", + "type": "string" + }, + "status": { + "default": "unknown", + "enum": [ + "active", + "revoked", + "expired", + "unknown" + ], + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "target_url": { + "description": "Underlying URL the qURL protects; omitted on connector-owned resources", + "type": "string" + } + }, + "required": [ + "resource_id", + "expires_at", + "created_at", + "status" + ], + "type": "object" +}
- Added
list_qurl_sessions - Changed
list_qurls7 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - added
Input schema / properties / created_after / patternAdded value: +"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - added
Input schema / properties / created_before / patternAdded value: +"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - added
Input schema / properties / expires_after / patternAdded value: +"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - added
Input schema / properties / expires_before / patternAdded value: +"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - changed
Input schema / properties / status / descriptionPrevious value: -"Filter by status (comma-separated, e.g., 'active,revoked')"New value: +"Filter by status (comma-separated, e.g. 'active,revoked'). Defaults to 'active' when omitted; pass 'revoked' or 'active,revoked' to override. Only active and revoked are valid resource filters; expired is not accepted." - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "data": { + "items": { + "additionalProperties": {}, + "properties": { + "created_at": { + "type": "string" + }, + "custom_domain": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "preserve_host": { + "description": "When true, the original Host header is preserved when proxying via the custom domain. Only meaningful when custom_domain is set; defaults to false on the API side.", + "type": "boolean" + }, + "qurl_count": { + "description": "Number of access tokens minted for this resource", + "type": "number" + }, + "qurl_site": { + "type": "string" + }, + "qurls": { + "items": { + "additionalProperties": {}, + "description": "Single access token belonging to a qURL resource", + "properties": { + "access_policy": { + "additionalProperties": {}, + "description": "Access control policy snapshot for this token", + "properties": { + "ai_agent_policy": { + "additionalProperties": {}, + "properties": { + "allow_categories": { + "items": { + "type": "string" + }, + "type": "array" + }, + "block_all": { + "type": "boolean" + }, + "deny_categories": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "geo_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "geo_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "user_agent_allow_regex": { + "type": "string" + }, + "user_agent_deny_regex": { + "type": "string" + } + }, + "type": "object" + }, + "created_at": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "label": { + "type": "string" + }, + "max_sessions": { + "type": "number" + }, + "one_time_use": { + "type": "boolean" + }, + "qurl_id": { + "type": "string" + }, + "qurl_site": { + "type": "string" + }, + "session_duration": { + "description": "Seconds of access granted after a successful resolve", + "type": "number" + }, + "status": { + "default": "unknown", + "description": "Per-token status (wider than resource status — tokens may be consumed/expired independently)", + "enum": [ + "active", + "consumed", + "expired", + "revoked", + "unknown" + ], + "type": "string" + }, + "use_count": { + "type": "number" + } + }, + "required": [ + "qurl_id", + "status" + ], + "type": "object" + }, + "type": "array" + }, + "resource_id": { + "description": "Stable resource identifier (public key or legacy r_ ID)", + "type": "string" + }, + "slug": { + "description": "Immutable per-owner resource identity, when one was supplied at create time", + "type": "string" + }, + "status": { + "default": "unknown", + "enum": [ + "active", + "revoked", + "expired", + "unknown" + ], + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "target_url": { + "description": "Underlying URL the qURL protects; omitted on connector-owned resources", + "type": "string" + } + }, + "required": [ + "resource_id", + "expires_at", + "created_at", + "status" + ], + "type": "object" + }, + "type": "array" + }, + "meta": { + "additionalProperties": {}, + "properties": { + "has_more": { + "description": "True if more pages are available beyond this response", + "type": "boolean" + }, + "next_cursor": { + "description": "Pass to a subsequent list_qurls call to fetch the next page", + "type": "string" + }, + "page_size": { + "type": "number" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "has_more" + ], + "type": "object" + } + }, + "required": [ + "data", + "meta" + ], + "type": "object" +}
- Changed
mint_link38 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - removed
Input schema / properties / access_policy / additionalPropertiesRemoved value: -false - removed
Input schema / properties / access_policy / properties / ai_agent_policy / additionalPropertiesRemoved value: -false - changed
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / descriptionPrevious value: -"AI agent categories to permit (all others blocked)"New value: +"AI agent categories to permit (all others blocked; max 20 entries, 128 chars each)" - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / items / maxLengthAdded value: +128 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / allow_categories / maxItemsAdded value: +20 - changed
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / descriptionPrevious value: -"AI agent categories to block (e.g., gptbot, commoncrawl)"New value: +"AI agent categories to block (e.g., gptbot, commoncrawl; max 20 entries, 128 chars each)" - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / items / maxLengthAdded value: +128 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ai_agent_policy / properties / deny_categories / maxItemsAdded value: +20 - changed
Input schema / properties / access_policy / properties / geo_allowlist / descriptionPrevious value: -"Allowed country codes (ISO 3166-1 alpha-2)"New value: +"Allowed country codes (ISO 3166-1 alpha-2; max 50 entries, 8 chars each)" - added
Input schema / properties / access_policy / properties / geo_allowlist / items / maxLengthAdded value: +8 - added
Input schema / properties / access_policy / properties / geo_allowlist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / geo_allowlist / maxItemsAdded value: +50 - changed
Input schema / properties / access_policy / properties / geo_denylist / descriptionPrevious value: -"Denied country codes (ISO 3166-1 alpha-2)"New value: +"Denied country codes (ISO 3166-1 alpha-2; max 50 entries, 8 chars each)" - added
Input schema / properties / access_policy / properties / geo_denylist / items / maxLengthAdded value: +8 - added
Input schema / properties / access_policy / properties / geo_denylist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / geo_denylist / maxItemsAdded value: +50 - changed
Input schema / properties / access_policy / properties / ip_allowlist / descriptionPrevious value: -"Allowed IP addresses or CIDR ranges"New value: +"Allowed IP addresses or CIDR ranges (max 100 entries, 64 chars each)" - added
Input schema / properties / access_policy / properties / ip_allowlist / items / maxLengthAdded value: +64 - added
Input schema / properties / access_policy / properties / ip_allowlist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ip_allowlist / maxItemsAdded value: +100 - changed
Input schema / properties / access_policy / properties / ip_denylist / descriptionPrevious value: -"Denied IP addresses or CIDR ranges"New value: +"Denied IP addresses or CIDR ranges (max 100 entries, 64 chars each)" - added
Input schema / properties / access_policy / properties / ip_denylist / items / maxLengthAdded value: +64 - added
Input schema / properties / access_policy / properties / ip_denylist / items / minLengthAdded value: +1 - added
Input schema / properties / access_policy / properties / ip_denylist / maxItemsAdded value: +100 - changed
Input schema / properties / access_policy / properties / user_agent_allow_regex / descriptionPrevious value: -"Regex to allow matching user agents"New value: +"Regex to allow matching user agents (max 256 chars)" - added
Input schema / properties / access_policy / properties / user_agent_allow_regex / maxLengthAdded value: +256 - changed
Input schema / properties / access_policy / properties / user_agent_deny_regex / descriptionPrevious value: -"Regex to deny matching user agents"New value: +"Regex to deny matching user agents (max 256 chars)" - added
Input schema / properties / access_policy / properties / user_agent_deny_regex / maxLengthAdded value: +256 - added
Input schema / properties / expires_at / patternAdded value: +"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - changed
Input schema / properties / max_sessions / descriptionPrevious value: -"Maximum concurrent sessions (0 = unlimited, max 1000)"New value: +"Maximum concurrent sessions for this qURL token (0 = unlimited, max 1000)" - changed
Input schema / properties / resource_id / descriptionPrevious value: -"The resource ID (r_ prefix) or qURL display ID (q_ prefix) to mint a new access link for. If a q_ ID is passed, the API resolves it to the parent resource automatically."New value: +"The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to mint a new access link for. If a q_ ID is passed, the API resolves it to the parent resource automatically." - removed
Input schema / properties / resource_id / minLengthRemoved value: -1 - added
Input schema / properties / resource_id / patternAdded value: +"(?:^(r_[a-z0-9_-]{11}|(?:[A-Za-z0-9_-]{4}){27,53}|(?:[A-Za-z0-9_-]{4}){27,53}[A-Za-z0-9_-]{2}|(?:[A-Za-z0-9_-]{4}){26,52}[A-Za-z0-9_-]{3}|[a-z2-7]{47}|[a-z2-7]{60})$)|(?:^q_[0-9a-f]{11}$)" - changed
Input schema / properties / session_duration / descriptionPrevious value: -"How long access lasts after clicking (e.g., \"1h\")"New value: +"How long access lasts after clicking (e.g., \"1h\"). Rejected if it exceeds the parent resource's session-duration cap." - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "branded_domain": { + "description": "Bare branded hostname for anchor text when the resource has a usable custom domain", + "type": "string" + }, + "email_delivery": { + "additionalProperties": false, + "properties": { + "attempted": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "failed": { + "type": "number" + }, + "recipients": { + "items": { + "type": "string" + }, + "type": "array" + }, + "results": { + "items": { + "additionalProperties": false, + "properties": { + "email": { + "type": "string" + }, + "error": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "skipped": { + "type": "boolean" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "email", + "success", + "skipped" + ], + "type": "object" + }, + "type": "array" + }, + "sent": { + "type": "number" + }, + "skipped_reason": { + "type": "string" + } + }, + "required": [ + "attempted", + "enabled", + "recipients", + "sent", + "failed", + "results" + ], + "type": "object" + }, + "expires_at": { + "type": "string" + }, + "qurl_id": { + "description": "Display-friendly qURL ID (q_ prefix) for the minted token", + "type": "string" + }, + "qurl_link": { + "description": "Newly minted access link with one-shot display semantics, like create_qurl", + "type": "string" + }, + "type": { + "description": "Resource type echoed from the underlying resource", + "type": "string" + } + }, + "required": [ + "qurl_id", + "qurl_link" + ], + "type": "object" +}
- Changed
resolve_qurl2 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "access_grant": { + "additionalProperties": {}, + "description": "Time-bound, IP-bound network access grant", + "properties": { + "expires_in": { + "description": "Seconds the network grant permits access from `src_ip` before re-resolution is required", + "type": "number" + }, + "granted_at": { + "type": "string" + }, + "src_ip": { + "description": "Caller IP that the access grant is bound to", + "type": "string" + } + }, + "required": [ + "expires_in", + "granted_at", + "src_ip" + ], + "type": "object" + }, + "resource_id": { + "type": "string" + }, + "target_url": { + "description": "Underlying URL revealed by the resolve", + "type": "string" + } + }, + "required": [ + "target_url", + "resource_id", + "access_grant" + ], + "type": "object" +}
- Added
revoke_qurl_token - Added
terminate_qurl_sessions - Changed
update_qurl8 fields changed- removed
Input schema / additionalPropertiesRemoved value: -false - added
Input schema / properties / custom_domainAdded value: +{ + "description": "Replace the custom domain bound to this resource (max 253 chars, must be registered/active/owned). Pass \"\" to clear.", + "maxLength": 253, + "type": "string" +} - added
Input schema / properties / expires_at / patternAdded value: +"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - added
Input schema / properties / preserve_hostAdded value: +{ + "description": "Whether to preserve the original Host header when proxying via the custom domain. Only meaningful when custom_domain is set; default false on the API side.", + "type": "boolean" +} - changed
Input schema / properties / resource_id / descriptionPrevious value: -"The resource ID (r_ prefix) or qURL display ID (q_ prefix) to update. If a q_ ID is passed, the API resolves it to the parent resource automatically."New value: +"The resource public key, CRID, legacy r_ ID, or qURL display ID (q_ prefix) to update. If a q_ ID is passed, the API resolves it to the parent resource automatically." - removed
Input schema / properties / resource_id / minLengthRemoved value: -1 - added
Input schema / properties / resource_id / patternAdded value: +"(?:^(r_[a-z0-9_-]{11}|(?:[A-Za-z0-9_-]{4}){27,53}|(?:[A-Za-z0-9_-]{4}){27,53}[A-Za-z0-9_-]{2}|(?:[A-Za-z0-9_-]{4}){26,52}[A-Za-z0-9_-]{3}|[a-z2-7]{47}|[a-z2-7]{60})$)|(?:^q_[0-9a-f]{11}$)" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": {}, + "properties": { + "created_at": { + "type": "string" + }, + "custom_domain": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "preserve_host": { + "description": "When true, the original Host header is preserved when proxying via the custom domain. Only meaningful when custom_domain is set; defaults to false on the API side.", + "type": "boolean" + }, + "qurl_count": { + "description": "Number of access tokens minted for this resource", + "type": "number" + }, + "qurl_site": { + "type": "string" + }, + "qurls": { + "items": { + "additionalProperties": {}, + "description": "Single access token belonging to a qURL resource", + "properties": { + "access_policy": { + "additionalProperties": {}, + "description": "Access control policy snapshot for this token", + "properties": { + "ai_agent_policy": { + "additionalProperties": {}, + "properties": { + "allow_categories": { + "items": { + "type": "string" + }, + "type": "array" + }, + "block_all": { + "type": "boolean" + }, + "deny_categories": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "geo_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "geo_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_allowlist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "ip_denylist": { + "items": { + "type": "string" + }, + "type": "array" + }, + "user_agent_allow_regex": { + "type": "string" + }, + "user_agent_deny_regex": { + "type": "string" + } + }, + "type": "object" + }, + "created_at": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "label": { + "type": "string" + }, + "max_sessions": { + "type": "number" + }, + "one_time_use": { + "type": "boolean" + }, + "qurl_id": { + "type": "string" + }, + "qurl_site": { + "type": "string" + }, + "session_duration": { + "description": "Seconds of access granted after a successful resolve", + "type": "number" + }, + "status": { + "default": "unknown", + "description": "Per-token status (wider than resource status — tokens may be consumed/expired independently)", + "enum": [ + "active", + "consumed", + "expired", + "revoked", + "unknown" + ], + "type": "string" + }, + "use_count": { + "type": "number" + } + }, + "required": [ + "qurl_id", + "status" + ], + "type": "object" + }, + "type": "array" + }, + "resource_id": { + "description": "Stable resource identifier (public key or legacy r_ ID)", + "type": "string" + }, + "slug": { + "description": "Immutable per-owner resource identity, when one was supplied at create time", + "type": "string" + }, + "status": { + "default": "unknown", + "enum": [ + "active", + "revoked", + "expired", + "unknown" + ], + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "target_url": { + "description": "Underlying URL the qURL protects; omitted on connector-owned resources", + "type": "string" + } + }, + "required": [ + "resource_id", + "expires_at", + "created_at", + "status" + ], + "type": "object" +}
- Added
update_qurl_token
9 tool updates
v0.2.0- First observed
batch_create_qurls - First observed
create_qurl - First observed
delete_qurl - First observed
extend_qurl - First observed
get_qurl - First observed
list_qurls - First observed
mint_link - First observed
resolve_qurl - First observed
update_qurl
TDQS
Scored across 16 tools
Most tools target a distinct resource/action, and the descriptions explicitly contrast alternatives such as create_qurl vs mint_link vs batch_create_qurls. There is still overlap among the link-minting/sharing tools and the multi-action manage_crid_access, but boundaries are documented well enough to avoid most misselection.
All tool names use snake_case with an action-first pattern, and qurl/crid/token/session suffixes are predictable. Minor variants like share_by_crid and batch_create_qurls do not create mixed conventions.
At 16 tools this is slightly above the ideal 3-15 range, but the extra tools are justified by resource lifecycle, token/session management, batch creation, and CRID access control. The set does not feel bloated for the domain.
Core qURL resource, token, session, and CRID access lifecycle coverage is strong. However, multiple descriptions route chat-uploaded files to upload_file_data_qurl/upload_file_qurl, which are absent from the tool set, creating a dead end; get_qurl also only exposes a bounded token preview rather than a full token listing.
Maintenance
Related MCP Connectors
Instant web publishing for AI agents. POST HTML, get a live URL. No account needed.
Command your AI agents: verifiable passports, credential injection, full audit, revoke in 60s.
Context and control for any AI. Agent identity, scoped access, and proof across tools.
OAuth 2.1 short-link tools for AI agents with scoped tokens, approvals, audit logs, and revocation.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceSafeLink lets AI agents hire other AI agents — and get hired — with cryptographic guarantees instead of trust. Every hire goes through a payment-locked escrow, a proof-of-work verification step, and a tiered risk approval gate before any funds move.12 npm4MIT
- AlicenseAqualityDmaintenanceURL intelligence for AI agents. One URL in, structured security and data quality signals out across 7 dimensions. 13 tools, risk score 0-100 with 23 configurable weights.1622 npm1MIT
- AlicenseAqualityDmaintenanceGenerate styled QR codes, manage dynamic short links with click analytics, and publish micro-landing pages via AI agents.1923 npm1MIT
- AlicenseNot gradedqualityDmaintenanceQrVerloz lets AI agents create QR codes instantly — no account needed, active for 90 days, and claimable at any time to make them permanent. Retarget the destination URL whenever you need, without reprinting.3MIT