Skip to main content
Glama

qURL MCP server for temporary access links

npm version

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-mcp in 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.

  1. 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_KEY set.

  2. Ask the agent to call publish_crid with app_name (a short public name, without secrets or recipient details) and the app's root loopback URL, such as http://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.

  3. Ask the agent to call share_by_crid with 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"
    }
  4. 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

upload_file_qurl

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

upload_file_data_qurl

Supply the supported file as base64 data with the correct filename and media type.

A file already stored by an upload tool

mint_uploaded_file_link

Reuse its resource ID to mint a fresh link without another upload.

Text to share as a generated PDF

upload_text_qurl

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.

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.

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" sh

On 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-mcp

3. 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_crid

Publish a local app or URL privately and return its CRID

qurl publish <url> --name <app_name> [--allow-requests] [--access <mode>]

share_by_crid

Make a short-lived access link for a CRID this machine published

qurl share <CRID> [--ttl] [--session-duration]

list_crids

List this machine's published resources (read-only)

qurl list --status active

manage_crid_requests

List, approve by code, or deny access requests; list or remove approved people; turn requests on or off

qurl requests, qurl approve, qurl deny, qurl grants

manage_crid_access

Private access policy, invitations, and consumer grants

qurl access <action> <CRID>

set_crid_sharing

Stop, start, or restart sharing a local app

qurl stop, qurl start, qurl restart

delete_crid

Delete a resource permanently

qurl delete <CRID> --yes

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 requires app_name, the public title recipients see, in both account and local modes. Account mode sends it as the resource's name at creation. It 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 4.0.1 or later on the publisher machine. In local mode, app_name names the resource at publish time. The tool confirms the name through qurl preview set. 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

stdio

Local subprocess MCP server

npm run start

Claude Desktop, Cursor, Codex, and other local MCP clients

http

Authenticated remote MCP server

npm run start:http

Remote agent runtimes behind HTTPS

Feature Map

qURL Management Tools

Tool

Description

create_qurl

Create a new qURL (requires a public name)

resolve_qurl

Resolve an access token into a protected target URL

list_qurls

List qURL resources

get_qurl

Fetch details for a single qURL

delete_qurl

Delete a qURL

extend_qurl

Extend a link's expiration (needs qurl:read too)

update_qurl

Update qURL metadata or expiration

mint_link

Mint a new access link for an existing resource

share_by_crid

Mint a temporary access link from a resource CRID

batch_create_qurls

Create multiple qURLs in one request

revoke_qurl_token

Revoke a specific token

update_qurl_token

Update a specific token

list_qurl_sessions

List active access sessions

terminate_qurl_sessions

Terminate one or all active sessions

create_qurl and each batch_create_qurls item require name: the public title recipients see on qurl.link and in link previews, which also names the passkey they save. The agent writes a short, specific title for what is shared (at most 100 characters, one line; no internal hostnames, IPs, or secrets). It is not the per-link label. When the target URL reuses an existing resource, that resource keeps its stored name. share_by_crid and mint_link only mint links for existing resources and take no name.

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_file_qurl

stdio

Upload a local file and mint a qURL

upload_file_data_qurl

stdio/HTTP

Upload base64 file content and mint a qURL

upload_text_qurl

stdio/HTTP

Upload text content and mint a qURL

mint_uploaded_file_link

stdio/HTTP

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

qurl://links

Current qURL list

qurl://usage

Current quota and usage information

MCP Prompts

Prompt

Description

secure_a_service

Secure service integration prompt

audit_links

Link audit prompt

rotate_access

Access rotation prompt

Quick Start

1. Install Dependencies

npm install

For 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 build

3. Start

Local stdio mode:

npm run start

Remote HTTP mode:

npm run start:http

MCP 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.json

The local files are gitignored so credentials and machine-specific paths are not committed.

Their responsibilities are:

File

Purpose

qurl-mcp.config.json

Shared runtime config used by both stdio and http modes

qurl-mcp.http.json

HTTP-only server listener and public access config

qurl-mcp.config.json Reference

Shared Core Settings

Field

Purpose

maxUploadFileDataBytes

Limits decoded and local file uploads (default 10mb)

defaultQurlApiUrl

Base URL of the qURL backend API

defaultQurlConnectorUrl

Base URL of the upload connector

linkHosts

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

MCP_MAX_UPLOAD_FILE_DATA_BYTES

maxUploadFileDataBytes

QURL_API_URL

defaultQurlApiUrl

QURL_CONNECTOR_URL

defaultQurlConnectorUrl

QURL_LINK_HOSTS

linkHosts

QURL_SMTP_HOST

smtp.host

QURL_SMTP_PORT

smtp.port

QURL_SMTP_SECURE

smtp.secure

QURL_SMTP_USERNAME

smtp.username

QURL_SMTP_PASSWORD

smtp.password

QURL_SMTP_FROM_EMAIL

smtp.fromEmail

QURL_SMTP_FROM_NAME

smtp.fromName

QURL_SMTP_ALLOWED_RECIPIENTS

smtp.allowedRecipients

QURL_SMTP_ALLOWED_RECIPIENT_DOMAINS

smtp.allowedRecipientDomains

QURL_SMTP_MAX_RECIPIENTS_PER_MESSAGE

smtp.maxRecipientsPerMessage

QURL_SMTP_MAX_RECIPIENTS_PER_HOUR

smtp.maxRecipientsPerHour

QURL_PUBLIC_VIDEO_FILE_PATH

publicVideo.filePath

QURL_PUBLIC_VIDEO_TITLE

publicVideo.title

QURL_PUBLIC_VIDEO_PAGE_PATH

publicVideo.pagePath

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.host

SMTP server hostname

smtp.port

SMTP server port

smtp.secure

true for implicit TLS; false for required STARTTLS

smtp.username

SMTP login username

smtp.password

SMTP login password or app-specific code

smtp.fromEmail

Sender email address

smtp.fromName

Sender display name

smtp.allowedRecipients

Optional exact-address allowlist

smtp.allowedRecipientDomains

Optional exact-domain allowlist (subdomains are not included)

smtp.maxRecipientsPerMessage

Per-message recipient cap (default 10)

smtp.maxRecipientsPerHour

Per-qURL-key attempted-recipient cap per fixed hourly window (default 100)

These settings are used when email delivery is requested by tools such as:

  • create_qurl

  • mint_link

  • upload_text_qurl

  • upload_file_qurl

  • upload_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

publicVideo.title

Title shown on the public video page

publicVideo.pagePath

Public path of the video playback page

publicVideo.filePath

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

port

HTTP MCP listener port

host

HTTP MCP bind address

baseUrl

Public base URL of the service

allowedHosts

Host allowlist for Host header validation

trustProxyHops

Exact trusted reverse-proxy hop count (default 0)

stateless

Request-scoped HTTP transport with no session affinity (default false)

maxConcurrentRequests

Stateless-only POST/parser concurrency cap per process (default 20)

credentialRateLimitStore

Credential counter backend: memory or dynamodb (default memory)

rateLimitDynamoDbTable

DynamoDB table used by the shared credential counter

metricsNamespace

CloudWatch EMF namespace for stateless saturation metrics

metricsService

Stable CloudWatch EMF Service dimension

metricsEnvironment

Stable CloudWatch EMF Environment dimension

maxSessions

Hard cap on live MCP sessions (default 1000)

maxSessionsPerCredential

Per-bearer live and initializing session cap (default 20)

maxUnvalidatedSessions

Cap on sessions that have not completed a downstream qURL API call (default 100)

sessionIdleTtlMs

Connected-session idle eviction window (default 15 minutes)

sessionAbsoluteTtlMs

Absolute session lifetime, including active SSE/tool requests (default 24 hours)

unvalidatedSessionTtlMs

Absolute validation deadline for never-validated bearer sessions (default 1 minute)

mcpRateLimitPerMinute

Per-client /mcp request limit (default 120)

publicFileRateLimitPerMinute

Per-client public-route request limit (default 300)

HTTP fields have matching environment overrides:

Environment variable

Config field

MCP_PORT

port

MCP_HOST

host

MCP_BASE_URL

baseUrl

MCP_ALLOWED_HOSTS

allowedHosts

MCP_TRUST_PROXY_HOPS

trustProxyHops

MCP_HTTP_STATELESS

stateless

MCP_MAX_CONCURRENT_REQUESTS

maxConcurrentRequests

MCP_CREDENTIAL_RATE_LIMIT_STORE

credentialRateLimitStore

MCP_SERVE_LAYERV_LEGAL_PAGES

serveLayerVLegalPages (default false)

MCP_RATE_LIMIT_DYNAMODB_TABLE

rateLimitDynamoDbTable

MCP_METRICS_NAMESPACE

metricsNamespace

MCP_METRICS_SERVICE

metricsService

MCP_METRICS_ENVIRONMENT

metricsEnvironment

MCP_MAX_SESSIONS

maxSessions

MCP_MAX_SESSIONS_PER_CREDENTIAL

maxSessionsPerCredential

MCP_MAX_UNVALIDATED_SESSIONS

maxUnvalidatedSessions

MCP_SESSION_IDLE_TTL_MS

sessionIdleTtlMs

MCP_SESSION_ABSOLUTE_TTL_MS

sessionAbsoluteTtlMs

MCP_UNVALIDATED_SESSION_TTL_MS

unvalidatedSessionTtlMs

MCP_RATE_LIMIT_PER_MINUTE

mcpRateLimitPerMinute

MCP_PUBLIC_FILE_RATE_LIMIT_PER_MINUTE

publicFileRateLimitPerMinute

MCP_MAX_UPLOAD_FILE_DATA_BYTES

maxUploadFileDataBytes (shared)

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_CONFIG

  • QURL_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

/mcp

Main remote MCP endpoint

/healthz

Health check endpoint

/legal/privacy

Public privacy policy page

/legal/terms

Public terms of service page

publicVideo.pagePath

Public video playback page

publicVideo.pagePath + /file

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 /mcp

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/terms

  • the 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-challenge

This verification file must live under the domain root .well-known path, 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-mcp

If 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.js

For 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

npm run build

Compile TypeScript

npm test

Run tests

npm run test:coverage

Run enforced coverage

npm run lint

Run ESLint

npm run dev

TypeScript watch mode

npm run format

Format source code

npm run format:check

Check formatting

npm run start

Start stdio mode

npm run start:http

Start HTTP mode

  1. Copy and update the two example config files

  2. Set credentials through environment variables

  3. Run npm install

  4. Run npm run build

  5. Run npm run start:http

  6. Verify /healthz

  7. Verify unauthenticated /mcp requests receive 401

  8. Configure the HTTPS reverse proxy

  9. 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.

Agent feedback and human review

submit_feedback sends a short report to the LayerV team in Slack. It is available in configured hosted HTTP, API-key stdio, and local CLI mode. Set QURL_MCP_FEEDBACK=off to hide and disable feedback in any mode. Self-hosted HTTP servers omit the tool until a Slack webhook is configured. Local agents need no account or Slack secret. They POST to https://mcp.connector.layerv.ai/feedback; hosted tools use the same Slack delivery function directly.

Example tool arguments:

{
  "category": "friction",
  "tool_name": "publish_crid",
  "message": "Expected a link after publishing. The error did not say how to retry."
}

Categories are bug, feature, friction, and other. The message is limited to 500 characters. tool_name is optional. Send only reports the user authorized for sharing. Do not include secrets, private links, personal data, or conversation history. The service removes known active credentials, qURL API keys, bearer credentials, HTTP links, bare qurl.link/www links, and email addresses as a best-effort safeguard. This is not a general secret or personal-data detector. Reports are plain text in Slack, with link previews disabled. No account identity, client IP, transcript, or resource content is added to the Slack message.

For the LayerV-operated HTTP service, create a channel-specific Slack incoming webhook and inject it as the secret environment variable MCP_FEEDBACK_SLACK_WEBHOOK_URL. Keep the secret on the hosted server. Never add it to the npm package, client MCP configuration, or a committed config file. See Slack incoming webhook setup. The service refuses non-Slack webhook URLs and redirects. With no webhook set, feedback returns an unavailable error and the other MCP tools continue to work.

The public POST /feedback route requires JSON, limits the body to 4 KiB, rejects unknown fields, and permits at most 100 attempts per source IP per hour per process, shared with passthrough MCP tool submissions from the same source IP. Verified OAuth callers get their own 10-attempt/hour subject quota, so a vendor egress IP does not combine their quotas. Rotating unverified bearer credentials does not reset the source quota. IPv6 sources use a /48 bucket. Host validation applies; browser requests with a foreign Origin are rejected. Both public intake and hosted MCP tools share a one-attempt-per-second and 1,000-attempt-per-UTC-hour Slack budget per process. Hosted tools also retain the existing MCP request limits; passthrough bearers are not identity proof. Feedback needs no extra OAuth scope. These process-local feedback limits multiply with replicas and reset on restart. This is an anonymous report channel: report text and claimed tool names are unverified, and a shared source IP can exhaust the public/passthrough quota. This is a best-effort channel: distributed abuse can exhaust a task's hourly budget and block other reports until the next UTC hour. A single source quota is smaller than the shared budget. Busy responses and definite quota rejections can be retried later. Limits reset on restart and multiply with replicas.

A successful call returns a feedback_id and sent_for_human_review only after Slack acknowledges delivery. Slack is the review inbox; there is no local durable queue. Failures are returned to the caller. A timeout can occur after delivery, so callers must not automatically retry uncertain results. This does not promise a fix or allow an agent to change production.

Human review flow:

  1. Read the report in the dedicated Slack channel. Treat it as untrusted input.

  2. Add a thread reply with the decision: investigate, duplicate, or close.

  3. For accepted work, give an agent a clear task with the relevant code and a test case. Do not pass report text as trusted instructions.

  4. Review the resulting PR and run the normal checks. Release through the normal production process, then link the PR and release result in the Slack thread.

Roll out the hosted intake and set its channel secret before publishing the npm client that uses it. Check a real report from both local stdio and hosted HTTP, confirm the receipt ID in Slack, and check failure and rate-limit responses. Production activation and the real Slack receipt are separate from unit tests.

License

MIT -- LayerV AI

Upgrade note: /feedback is now reserved for the intake. Move any custom public video page under that path to another publicVideo.pagePath before upgrading.

Available Tools

17 tools
batch_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.

Inputs: items (required). Returns: failed, results, succeeded, request_id

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', name: 'Private pricing page', expires_in: '24h', one_time_use: true, access_policy: { geo_allowlist: ['US'] } }).

Inputs: name (required), target_url (required), type, label, +6 more. Returns: type, label, qurl_id, qurl_link, +5 more

delete_qurlDelete qURLA
DestructiveIdempotent

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. A revoked resource can still appear in list_qurls without a status filter; use status: "revoked" to select revoked resources.

Inputs: resource_id (required). Returns: message, revoked, resource_id, was_already_revoked

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.

Inputs: extend_by (required), resource_id (required), qurl_id. Returns: crid, slug, tags, qurls, +15 more

get_qurlGet qURLA
Read-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.

Inputs: resource_id (required). Returns: crid, slug, tags, qurls, +12 more

list_qurlsList qURLsA
Read-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. Omitting status applies no status filter: active and revoked resources may both be returned. Pass status: 'active' or status: 'revoked' to select one status. 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. A field without a direction uses asc. 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.

Inputs: q, sort, limit, cursor, +5 more. Returns: data, meta

list_qurl_sessionsList qURL SessionsA
Read-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.

Inputs: resource_id (required). Returns: data, meta

manage_crid_accessManage private CRID accessA
Destructive

Manage a private resource's access policy, invitations, access requests, and grants. 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.

Inputs: action (required), resource_id (required), id, email, +5 more. Returns: grant, action, grants, resource, +3 more

publish_cridPublish a private CRIDA

Publish a URL with private access. Supply app_name: a short public title recipients see, without secrets or hidden origin details. It names a new resource; reuse keeps the stored name. 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.

Inputs: policy (required), app_name (required), target_url (required). Returns: crid, private, resource_id, consumer_access

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.

Inputs: access_token (required). Returns: target_url, resource_id, access_grant

revoke_qurl_tokenRevoke qURL TokenA
Destructive

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.

Inputs: qurl_id (required), resource_id (required). Returns: message, qurl_id, revoked, resource_id

share_by_cridShare qURL by CRIDA

Request a temporary qURL access link for a legacy resource identified by its CRID. Only for legacy resources without consumer_access; use manage_crid_access and the policy's invitation or grant flow for consumer_access resources. Use the known resource policy. If qurl:read is available, get_qurl (without the leading $ marker) can check consumer_access. A resolve-only key can share a known legacy resource without that read; if the policy is unknown, ask the publisher, and never treat a denied read as proof that it is legacy. When the user supplies a legacy resource CRID with a leading $ marker, remove the marker and use this tool. Set ttl to request a lifetime using Go duration syntax; the total must be a positive whole number of seconds. The service may adjust the lifetime. The link is minted by the qURL API and includes its expiration and single-use metadata.

Inputs: crid (required), ttl. Returns: crid, qurl, type, qurl_id, +3 more

submit_feedbackSend LayerV feedbackA

Send feedback about LayerV MCP to the LayerV team in Slack for human review. Use for bugs, missing features, or friction. Send only a brief report the user has authorized you to share. Never include credentials, private links, personal data, or conversation history. Receipt confirms delivery, not a promised fix. Do not automatically retry an uncertain delivery.

Inputs: message (required), category (required), tool_name. Returns: status, feedback_id

terminate_qurl_sessionsTerminate qURL SessionsA
Destructive

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.

Inputs: resource_id (required), session_id. Returns: message, session_id, terminated, resource_id

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).

Inputs: resource_id (required), tags, extend_by, expires_at, +3 more. Returns: crid, slug, tags, qurls, +12 more

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.

Inputs: qurl_id (required), resource_id (required), label, extend_by, +4 more. Returns: Single access token belonging to a qURL resource

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.2/5.0

    Scored across 17 tools

    Disambiguation3/5

    The set has several families of near-siblings: create_qurl / batch_create_qurls / mint_link / publish_crid / share_by_crid all produce access links, and extend_qurl / update_qurl / update_qurl_token split expiry changes across resource, link, and token levels. The descriptions aggressively cross-reference each other ('use X instead when...'), which rescues most cases, but the resource-vs-link-vs-token expiry distinction is subtle enough that misselection remains likely.

    Naming Consistency4/5

    Nearly everything follows a snake_case verb_noun pattern (create_qurl, get_qurl, list_qurls, delete_qurl, revoke_qurl_token, list_qurl_sessions). Minor deviations exist: batch_create_qurls pluralizes and prepends a modifier, share_by_crid uses a prepositional form, and submit_feedback is unrelated to the qurl naming family, but overall the pattern is predictable.

    Tool Count3/5

    At 17 tools the surface is on the heavy side for a link-minting service, and a few (share_by_crid vs publish_crid, extend_qurl vs update_qurl_token) could plausibly be consolidated. Each tool does map to a real operation (resource CRUD, token lifecycle, sessions, access policy), so nothing is clearly gratuitous, but the count sits at the borderline.

    Completeness4/5

    Coverage is strong: full resource CRUD, token mint/update/revoke/extend, session listing and termination, batch creation, end-user redemption, and access-policy/invitation flows via manage_crid_access and publish_crid. The main gap is observability — no access-log or analytics tool despite repeated references to correlating access events, plus no standalone search beyond list_qurls' q filter.

    Maintenance

    ActivityActive
    ResponsivenessMixed handling

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      SafeLink 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.
      30 npm
      4
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      URL 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.
      16
      64 npm
      1
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      Generate styled QR codes, manage dynamic short links with click analytics, and publish micro-landing pages via AI agents.
      19
      63 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      QrVerloz 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.
      3
      MIT

    Appeared in Searches