trackmcp
Back to directory

MCP server for QURL - secure link management for AI agents

4 stars TypeScriptOthers Updated Sep 1, 2026

Documentation

@layervai/qurl-mcp

npm version

> ⚠️ 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.

> A qURL MCP Server that supports both local `stdio` mode and remote `HTTP` mode for creating, managing, resolving, and sharing secure access links.

Overview

`qURL MCP` exposes qURL capabilities to MCP clients, GPTs, ChatGPT, and other remote integrations.

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
  • serving public legal pages
  • serving a configurable MP4 video playback page

Runtime Modes

ModePurposeStart CommandTypical 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

ToolDescription
`create_qurl`Create a new qURL
`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 qURL expiration
`update_qurl`Update qURL metadata or expiration
`mint_link`Mint a new access link for an existing resource
`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

Upload Tools

ToolModeDescription
`upload_file_qurl``stdio`Upload a local file and mint a qURL
`upload_file_data_qurl``stdio`/HTTPUpload base64 file content and mint a qURL
`upload_text_qurl``stdio`/HTTPUpload text content and mint a qURL

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

Connector upload and qURL minting are separate operations. 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.

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

URIDescription
`qurl://links`Current qURL list
`qurl://usage`Current quota and usage information

MCP Prompts

PromptDescription
`secure_a_service`Secure service integration prompt
`audit_links`Link audit prompt
`rotate_access`Access rotation prompt

Quick Start

1. Install Dependencies

bash
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

bash
npm run build

3. Start

Local `stdio` mode:

bash
npm run start

Remote `HTTP` mode:

bash
npm run start:http

MCP Client Example

If you want to use this server in `stdio` mode with a local MCP client:

json
{
  "mcpServers": {
    "qurl": {
      "command": "npx",
      "args": ["@layervai/qurl-mcp"],
      "env": { "QURL_API_KEY": "lv_live_xxx" }
    }
  }
}

Configuration Files

Copy the tracked examples to create local configuration files:

bash
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:

FilePurpose
`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

FieldPurpose
`maxUploadFileDataBytes`Limits decoded and local file uploads (default `10mb`)
`defaultQurlApiUrl`Base URL of the qURL backend API
`defaultQurlConnectorUrl`Base URL of the upload connector

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 variableConfig field
`MCP_MAX_UPLOAD_FILE_DATA_BYTES``maxUploadFileDataBytes`
`QURL_API_URL``defaultQurlApiUrl`
`QURL_CONNECTOR_URL``defaultQurlConnectorUrl`
`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`

`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, 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 first.

Size the configured maximum and reverse-proxy concurrency limit together.

Set `QURL_API_KEY` in the environment for `stdio` mode. 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

FieldPurpose
`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.

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

FieldPurpose
`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.

FieldPurpose
`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 variableConfig 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_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.

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.

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:

RoutePurpose
`/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 ` 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.

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—rejected

or rate-limited calls do not prove the credential valid. 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:

SettingValue
MCP Server URLYour public HTTPS URL plus `/mcp`
AuthenticationBearer token
TokenThe 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`

Public Page Checks

Also verify the legal pages and, when configured, the 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:

text
/.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:

bash
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 HTTP mode 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:

bash
docker run --rm -p 3000:3000 \
  -e MCP_HOST=0.0.0.0 \
  -e MCP_ALLOWED_HOSTS=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

CommandPurpose
`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 17.8 MB Noto Sans SC variable font for

offline multilingual glyph coverage. This intentionally increases the npm

tarball to roughly 11.4 MB and the unpacked package to roughly 18.4 MB for all

installs, including deployments that do not enable PDF workflows. Shipping the

font in-package avoids a runtime network dependency and preserves predictable

CJK rendering; operators prioritizing a smaller install can remove the asset

and accept the documented Helvetica fallback with limited CJK coverage. Its SIL

Open Font License and copyright notice are included in `assets/fonts/OFL.txt`.

License

MIT -- LayerV AI

Frequently asked questions

What is qurl-mcp?

qurl-mcp is MCP server for QURL - secure link management for AI agents

How do I install qurl-mcp?

Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.

Is qurl-mcp open source?

Yes — it is hosted on GitHub at https://github.com/layervai/qurl-mcp and has 4 stars.

Related MCP tools

Run your own MCP server? See who uses it and what to fix.

Measure it with TrackMCP