Operations

MCP server

The gateway can serve Model Context Protocol servers at POST /mcp/<name>. Each server exposes a fixed set of namespaces to an AI assistant such as Claude Desktop. The gateway generates the tools, their parameters and their descriptions from each namespace’s schema, so you write no server code and no tool definitions.

A server is read-only. Every tool call runs as the caller’s API key, through the same query and document paths as /v2.

Enable it

The capability is off by default. Enable it on the gateway and declare servers in LAYER_MCP_JSON:

LAYER_MCP_ENABLED=true
LAYER_MCP_ALLOWED_HOSTS=layer.example.com
LAYER_MCP_JSON='{
  "field-records": {
    "namespaces": [
      {
        "name": "site-photos",
        "toolName": "photos",
        "description": "Job-site photos, linked to the job they were taken on.",
        "filters": ["job_id", "photo_class", "taken_at"],
        "link": "source_url"
      },
      { "name": "invoices", "link": "source_url" }
    ]
  }
}'
VariablePurpose
LAYER_MCP_ENABLEDtrue serves the declared servers. Defaults to false.
LAYER_MCP_JSONThe servers, keyed by name. The gateway validates it at startup, and a change needs a gateway restart.
LAYER_MCP_ALLOWED_HOSTSComma-separated hostnames the gateway answers MCP requests on. Defaults to localhost,127.0.0.1,::1, so a remote URL needs its hostname here. Keep the Host header intact through your proxy.
LAYER_MCP_ALLOWED_ORIGINSExact origins, including the port, for browser-based clients, for example https://app.example.com:443. Native clients such as Claude Desktop send no Origin and need none.

Wildcards and empty allowlists are refused. A request from an unlisted host or origin is rejected.

Server fields

Each server has one to 512 namespaces. Names are exact, not globs. Set genericTools: true on the server to use generic tools even with eight or fewer namespaces. More than eight configured namespaces selects generic tools automatically. An optional host serves the same handler at /<name> on that dedicated hostname; include it in the host allowlist. The Desktop extension (0.1.3 and later) accepts either form: https://<gateway>/mcp/<name>, or https://<host>/<name> for a server’s dedicated host.

FieldDefaultPurpose
namerequiredThe namespace to read.
toolNamederived from the namespace nameThe stem of the tool names: photos gives search_photos and get_photos. Tool names must be unique within a server.
descriptionnoneWhat a row is. The model reads it when choosing a tool, so say what someone would search this data for.
filtersevery supported filterable attributeThe attributes offered as search filters. [] offers none.
wherenoneA pinned filter: an object of attribute to exact value (string, boolean or number), for example {"company":"story"}. See Pinned filters.
linknoneAn attribute holding a link to the source record. It is placed next to each hit so the model can cite it.
originalsoffServe the retained original file on demand with fetch_original. See Original files.

Tools

Tool discovery returns ttlMs: 0 and cacheScope: "private". A tool list belongs to the caller’s permissions and must not be shared across users. These cache hints let clients using MCP 2026-07-28, including current Claude Code, load the available tools.

Every server exposes list_namespaces, which takes no arguments and returns only namespaces the caller can read. Each entry includes name, toolName, description, rowCount (the upstream approximate count), searchKind, filterableAttributes (types and input schemas), groupableAttributes, summableAttributes and extremaAttributes (the fields aggregate takes), aggregateToolName, and linkAttribute. Counts can lag ingestion; discovery does not scan or fetch rows.

With up to eight configured namespaces, each readable namespace also gets three tools.

ToolWhat it does
search_<tool>Searches with a query, optional filters, a limit (default 10, at most 50) and an optional cursor for the next page.
get_<tool>Fetches one row by id.
aggregate_<tool>Counts, sums, mins and maxes, optionally grouped.

In generic mode, the server exposes just list_namespaces, search, get and aggregate. search takes namespace alongside the usual query, filters, limit and cursor; get takes namespace and id; aggregate takes namespace and the arguments below. Call list_namespaces first to choose a namespace and its filter types. Namespace-specific tool names are unavailable in this mode. Guessed namespace names still require membership in the server and a read grant. A caller with no readable namespaces gets an empty discovery list.

Filters take their types from the schema. Numbers take min and max, dates take after and before, and strings and booleans take an exact value.

Search reads the live namespace schema and the gateway’s persisted embedding configuration, including declared CLIP image profiles. A vector column alone is not an embedding declaration: declare its source/model through the embedding configuration. CLIP profiles let a text query search photos in the declared embedding space without adding a full-text field.

SchemasearchKind and ranking
Full-text only, with fuzzy search enabledHybridText
Full-text only, without fuzzy searchBM25
Declared embedding onlyANN/Embed, using inline Embed
Full-text and a declared embeddingAuto, using inline Embed; without fuzzy search, lexical expansion is disabled with fuzziness: 0

When both search paths are available, MCP questions keep lexical and semantic ranking together at every query length. Short phrases such as “change order” can retrieve an embedded page whose extracted text contains only a signature; long questions retain lexical evidence for job names and document titles. Text-only and embedding-only namespaces keep their respective search paths. If the schema declares a string title, name, or filename, search checks that label on the first 50 ranked candidates. Title matches add a bounded contribution to the store’s relevance. $mcp_score is the normalized store relevance plus at most 0.25 for the fraction of query words matched in the label. Results follow that combined score, while $score and $dist keep their store values. ANN distance uses inverse-distance relevance. Ties retain the store order. Compound label words such as “floorplan” match “floor plan” in a question. Every cursor page uses the same candidate bound. If the declared label fields change, restart at page one; a previous cursor is rejected. Use the declared job filter when the answer needs a particular project. Document-type filters select stored labels, which may not cover every page about that topic.

Scope a request before ranking

query ranks document content. A job, project or company name in the question does not become an attribute filter. For a request about one project, resolve its stored filter value first, then pass that value in filters. A display name and a stored job code may differ. Search the name alone and inspect the matching records, or use aggregate to discover the declared field’s values; do not guess an identifier.

Keep the user’s question as the query when adding the constraint. For example, if discovery declares job and doc_type, and the records identify North Wing as job 24-021, a drawing request can use:

{"query":"North Wing drawing revision","filters":{"job":"24-021","doc_type":"drawing"},"limit":5}

Search returns applied_filters, the caller’s validated typed filters, and states the requested scope in its text. {} means the caller supplied no attribute constraint; server-pinned filters still apply and are not disclosed there. Query words alone are not evidence that a hit belongs to the requested project. If the scoped search has no matches, report that result rather than substituting documents from other projects. A missing result does not establish whether the source file exists upstream or has been indexed.

A document-type label is not a content guarantee. If an optional doc_type filter returns no matches, retry the question without that filter while keeping the exact job, project, company and other constraints. Report that the type filter was removed; the tool does not relax filters automatically. Inspect the returned content before calling it a match: a contract clause describing how change orders work is not an actual change order. An empty classified search alone does not establish that no relevant unclassified page is indexed. Keep an explicitly requested exact-classification constraint unless the user agrees to a broader search.

Retaining an original and ranking a document are separate. An indexed original PDF page can have a source page link before original_blob is attached. Converted DOCX, spreadsheet and Markdown sources do not gain original-PDF page links just because they rank highly. Search and get include a structured links list as well as rendered links, so clients that consume structured content receive the same source evidence. Each entry identifies the returned row and its sourceLink; pageLink is a PDF page URL or null. A converted-page basis or a non-PDF original MIME prevents a PDF page link. Legacy rows without either field need a PDF source URL.

If the schema has more than one candidate field, the canonical text field wins; otherwise the tool returns an error naming the ambiguity.

Both tools return the rows as structured content, plus a short text rendering with each hit’s link.

Aggregate

aggregate answers “how many” and “how much” without reading rows: a count, or the sum, minimum or maximum of a numeric field, optionally grouped by one or two fields, over the rows the same typed filters as search select. It uses the store’s native aggregate_by and group_by.

ArgumentMeaning
group_byOne or two fields from the namespace’s groupableAttributes (strings, booleans and integers). Omit for a single total.
aggregatesUp to four of {"fn":"count"}, {"fn":"sum","field":…}, {"fn":"min","field":…}, {"fn":"max","field":…}. Default: one count. sum takes a field from summableAttributes; min and max take one from extremaAttributes, which adds dates.
filtersThe same typed filters as search.
limitMost groups returned, 1 to 1,000, default 100.
{"name":"aggregate_documents","arguments":{"group_by":["doc_type"],
  "filters":{"company":"Story Renovations"}}}

The structured content holds groups (one object per group, with the group fields and count, sum_<field>, min_<field> or max_<field>; an ungrouped call has one object), truncated, limit and billing. The store returns groups in key order, so truncated: true means later groups were cut off, not that these are the largest: add filters or raise limit. A namespace that collapses still counts its rows (pages), not documents, and counts says rows.

min and max are not available with group_by, because the store aggregates only count and sum per group; the call fails with unsupported_aggregate. Filter to one group and call again without group_by. A float or timestamp field cannot be grouped by, since it would make a group per row.

Cost

The store bills a query at the size of the namespace it reads, whatever the filters: about 2 GB for each query against the BCC pages, and never less than 1.28 GB. So one call is bounded and cheap to reason about. Count and sum, grouped or not, are one store query. Each min or max is one more, and a call takes at most two. A call makes at most three queries, and a call that is refused (an unknown field, too many aggregates) makes none.

Each call returns the bytes the store billed, as billing.queries, billing.billable_logical_bytes_queried and billing.billable_logical_bytes_returned, ends its text with the same figure, and logs it with the server and namespace. The gateway’s billable_logical_bytes_queried metric counts them with every other query.

Paging

A search returns one page. Besides rows (or groups when the namespace collapses), the structured content carries:

FieldMeaning
totalHow many results the whole search has: rows, or groups when collapsed. Counted up to 1,000.
total_exactfalse when the search has more than 1,000 results and total is that bound.
next_cursorAn opaque string for the next page, or null on the last page.
offsetWhere this page starts in the result list.
snapshottrue when the page walk is pinned to a snapshot (see below).
applied_filtersThe caller’s validated typed filters, or {} if none were requested. Server pins still apply and are not included.
linksEach returned row’s id, sourceLink and eligible PDF pageLink. A source need not have a retained original for its page link to be present.

To read on, call the same search again with the same query and filters and cursor set to next_cursor. limit may change between pages. The text rendering ends with Showing 8-14 of 31. and the cursor, so a model that reads only the text can continue. A walk reaches at most the first 1,000 results. Every page ranks the same 1,000-result window, so approximate (vector) ranking cannot reorder results between pages; only the page’s own rows are then read.

{"name":"search_photos","arguments":{"query":"kitchen cabinets","limit":10,
  "cursor":"eyJ2IjoxLCJxIjoi..."}}

The first page records when it ran, and the cursor carries that time. Every later page excludes rows the gateway wrote after it, so rows landing during a walk neither repeat a result nor push one off a page: a walk returns each of the first page’s results once, in order, however much is written meanwhile. A fresh search, without a cursor, sees the new rows.

Two limits apply. A row overwritten after the first page carries a new write time and drops out of the later pages of that walk. And a namespace whose schema has no _hevlayer_upserted_at (rows written around the gateway) has no write time to pin to, so snapshot is false and a walk there can shift when rows are added.

A cursor belongs to the namespace, query, filters and collapse configuration it came from. Sending it with anything else fails with cursor_mismatch; a cursor that does not decode fails with invalid_cursor.

Errors

A failed call returns isError: true. The structured content holds error.code, error.message and the details that name the fix, and the text starts with the code.

error.codeWhenExtra fields
unknown_argumentAn argument name the tool does not take, such as q for query or page_size for limit.unknown, allowed, did_you_mean (the nearest accepted name, or null)
invalid_cursorcursor is not a cursor this server issued, or is out of range.reason
cursor_mismatchcursor came from a different namespace, query or filters.
invalid_argumentAny other bad value: an empty query, limit outside 1 to 50, a filter of the wrong type, a group_by or sum field the namespace does not offer.field, for an aggregate field
unsupported_aggregatemin or max with group_by, or more than two of them.
tool_failedThe store or gateway failed.

Errors are returned before any query runs, so a wrong name costs no store work.

Auth

A client connects with a Layer API key as a bearer token minted through the Pro key workflow. The key needs two entitlements:

{
  "mcp.field-records": {},
  "vectorstore.default": { "scopes": ["read"], "namespaces": ["site-photos", "invoices"] }
}

mcp.<name> lets the key call that server. It grants no data access, so the key also needs read on each namespace through its vectorstore entitlement. An admin key still needs mcp.<name> explicitly.

RequestResult
Key without mcp.<name>403
Key with mcp.<name> but no grant on a namespaceThat namespace’s tools are left out of tools/list, and calling one by name is denied.

Minted keys work in both keys and pass-through deriveFromStore inbound auth modes. Minted verification runs before upstream bearer pass-through: the caller keeps their namespace grants, and the configured store credential is used upstream. Invalid, expired or revoked minted keys are rejected without falling back to pass-through. Plain upstream bearers and declared bootstrap keys do not grant mcp.<name>. open mode skips verification and cannot authorize MCP, even when a minted token is supplied.

Give each person their own key, scoped to the namespaces they should see, and revoke it through the usual key workflow.

Transport

The server speaks MCP’s Streamable HTTP transport in stateless mode, with JSON responses. It keeps no session between requests, so any gateway replica can answer any call. You can drive it with plain HTTP:

curl -sS "$LAYER_GATEWAY_URL/mcp/field-records" \
  -H "Authorization: Bearer $LAYER_MCP_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-06-18' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"search_photos",
                 "arguments":{"query":"kitchen cabinets","filters":{"job_id":"2231"},"limit":10}}}'

Each tool call is written to search history, tagged mcp.<name>.

Connect Claude Desktop

Claude Desktop’s remote connector settings take a URL and OAuth, not a bearer token. Layer ships a Desktop Extension, hevlayer-mcp.mcpb, that asks for the server URL and your token and forwards Desktop’s calls to the gateway.

The operator gives each person three things: the extension file, the full URL (https://<gateway>/mcp/<name>, or https://<host>/<name> for a dedicated host), and their own API key, sent through a secure channel.

Each person then sets it up without a terminal:

  1. In Claude Desktop, open Settings → Extensions → Advanced settings → Install Extension… and choose hevlayer-mcp.mcpb.
  2. Paste the Server URL and Access token. The token field takes the token only, without Bearer.
  3. Save, enable the extension, and start a new chat.

On a Team or Enterprise account

On a Team or Enterprise plan, an Owner adds the extension to the organization once, and each member installs it from inside Claude Desktop. The upload holds only the extension: the server URL and token are not part of it. Each member enters their own when they install, so one upload serves the whole team and every member reads with their own key.

The Owner, once:

  1. In Claude Desktop, click your name in the lower left, then open Organization settings → Connectors → Desktop.
  2. Click Add custom extension and choose hevlayer-mcp.mcpb. It appears under Custom team extensions as hev layer search.
  3. Click … on it, then Add to team.

Each member then installs hev layer search from the organization’s extensions under Settings → Extensions, and pastes the Server URL and their own Access token as in steps 2 and 3 above.

  • The organization’s Allowlist toggle is on the same Desktop tab. Turning it on stops members installing extensions from a file and removes the ones they already installed that way. Anyone who installed hevlayer-mcp.mcpb by hand installs it again from the organization’s list.
  • Members need Claude Desktop 0.13.91 or later.
  • On Enterprise, device policy overrides these settings: isDesktopExtensionEnabled and isDesktopExtensionDirectoryEnabled must not be false.

See Anthropic’s desktop extension allowlist guide.

Operators build the extension from the reviewed source commit:

npm ci --ignore-scripts --prefix tools/mcp-desktop
npm test --prefix tools/mcp-desktop
npm run pack --prefix tools/mcp-desktop

Distribute tools/mcp-desktop/dist/hevlayer-mcp.mcpb (version 0.1.3) and its SHA-256 through a secure channel. The bundle is built in CI but is not published as a download. Claude Desktop supplies the Node runtime; users need no separate Node installation. Remote server URLs must use HTTPS; HTTP is accepted only on loopback. If the URL or token is invalid, the extension stays running and answers Desktop’s initialize request with an error that says what to fix.

Pinned filters

Set where on a namespace seed to serve one slice of a shared namespace, such as one company’s rows. The gateway ANDs the pin into every search, aggregate, get and fetch_original call on that tool, and into the internal lookups behind them, so a client cannot widen or override it:

  • A pinned attribute is not offered as a filter. A client that sends it gets unknown filter, so it cannot ask for another value.
  • get and fetch_original answer a record outside the pin exactly like a missing one (not found), even when the id is known.
  • Counts, totals and groups from aggregate cover the pinned rows only.
{"name":"pov-bcc-photos","toolName":"story_photos","where":{"company":"story"},
 "filters":["job","folder_1"],"link":"webUrl"}

The seed is rejected at startup if where is empty or holds anything but a string, boolean or number. Discovery fails, as for filters, if a pinned attribute is missing from the namespace, not filterable, or its value does not match the attribute’s type. Pinned attributes cannot also be listed in filters. A gateway that predates where rejects the seed, so roll the gateway first.

Set "pageLink": true on a namespace seed whose rows are PDF pages with an integer page attribute. Each hit then shows its source link and a second link with #page=<n> appended, so a result opens the matching page. Links that already carry a fragment, and rows without page, show the source link only.

Documents with several copies

When one document has more than one copy (a SharePoint file and the same file attached to a REST row, merged by layer-extract sourceRef.merge), its rows carry a locations attribute listing every copy. A hit and a get_* result then end with [also: clouddrive <webUrl>; rest <rest url>], and the structured result has the full list.

Original files

A selected source original can have incorrect provider MIME metadata. A retained PNG is returned whole with actual mimeType: "image/png", its provider declaredMimeType, sourceFormatMismatch: true and the original sourceLink. The attached bytes are unchanged. pages is only valid for actual PDF originals; a PNG request with pages fails explicitly. Typed catalog references use the same namespace grants, pins and source/version/hash binding as PDF references.

Search and get stay text-only. When a namespace seed sets originals and its rows point at a retained original, the server adds fetch_original_<tool> (fetch_original with namespace in generic mode). It takes the id of a row from search or get, and optionally pages.

Warehouse original retention also supplies stable original:<source-id> catalog IDs for unindexed selected originals. These use the same arguments and namespace read grant. A catalog cannot bypass a pinned filter: missing pinned attributes are denied. For an indexed row without original_blob, the gateway can lazily bind a catalog receipt by its exact content hash or source drive/item/version, without scanning or patching rows. Source drive/item/version/ETag are included in the returned sourceIdentity; unknown legacy identity remains an explicit error.

{"name":"pov-bcc-story-pages","toolName":"story_documents","link":"webUrl","pageLink":true,
 "originals":{"maxBytes":700000,"maxPages":3,"maxWidth":1100}}
FieldDefaultPurpose
maxBytes700000Largest original returned whole. Hosts cap a tool result near 1 MB and base64 adds a third.
maxPages3Page images per call (at most 8).
maxWidth1100Page image width in pixels (200 to 2000).

Without pages, the result is the whole original as an embedded resource with its actual MIME. With pages, it is those pages of the original PDF rendered by the gateway as PNG image content, which a vision model can read directly. Use page images for what OCR text misses: drawings, stamps, handwriting, colored annotations, charts. Both forms put the source link and the applicable PDF page link in the text, separately and unchanged, and structuredContent carries the metadata only (sha256, bytes, mimeType, declaredMimeType, sourceFormatMismatch, version, sourceLink, pageLink), never the bytes. Legacy entries without declared MIME or mismatch provenance return null for those optional fields.

Retention

For a tool with originals enabled, get and fetch_original read the current document metadata from the store. A cached row from before an original was attached cannot hide it, and a cached reference cannot serve an original that the current row no longer retains. get still returns metadata and text only; it does not load the original bytes. If the store cannot provide the current row, the call reports that failure instead of claiming the original is absent.

If no original is retained, fetch_original returns a tool error with the record’s source link so you can open the file there. A retained reference whose file is missing from storage returns the source link too.

The bytes are in the namespace’s private blob storage, content-addressed by SHA-256. Rows carry original_blob (blob://<namespace>/<sha256>), original_sha256 and original_bytes. Neither the bucket, the key nor a URL is exposed, and the tool accepts no location from the caller.

Two paths write the reference, with one format and one set of checks:

  • The Pipeline. A layer-extract Pipeline retains by default: every new or re-ingested document stores its original (up to maxBytes, 10 MiB) and stages the reference with its page rows. Nothing else has to run. Set extract.retainOriginals: false to opt out, or an object ({maxBytes, backfill}) to tune it. Originals over maxBytes are skipped and counted, never truncated.
  • A Function for existing rows. The layer-extract originals worker runs under a Function (groupByParent: true) and fills the reference for rows ingested earlier. It gets the bytes from a sealed manifest of cached originals when one is declared, otherwise through Layer’s clouddrive connector with the pipeline’s Warehouse mounted on the pod. Its completion patch holds only original_blob and original_bytes, so text, vectors, other metadata and the source and page links are untouched, and a row that already has the reference completes with the same patch (idempotent). The original is attached only when its hash, size and provider version, item and drive match what the row was indexed from (rows indexed before those attributes existed are matched by webUrl and proven by hashing against their content_hash); a file that changed since is refused (ambiguous) and left for the pipeline to re-ingest, and a filename never selects an object.

Authorization

Every fetch passes the same checks as a read, in order, and the bytes are not read until all of them pass:

  1. A valid key with the mcp.<name> entitlement. A missing, malformed, unknown or revoked key is refused before the tool runs.
  2. A read grant on the row’s namespace.
  3. The row itself, read through the normal document path. An id that is missing or deleted gives a not-found error.
  4. The row’s stored reference must equal the address the gateway derives from the row’s own indexed hash and the granted namespace. A reference edited to name another namespace, other bytes, a bucket or a URL is refused.
  5. The stored bytes are hashed and must match the hash the row was indexed from, and the size and PDF header must match. Otherwise the call fails with a version mismatch.

Errors are explicit: no original retained, original missing from storage, over maxBytes (with the advice to request pages and the source link), page out of range, version mismatch, and render timeout (20 seconds).

Set collapse on an MCP namespace seed to use the gateway’s ranked-query Collapse API. For example, the BCC document seed uses:

{"name":"pov-bcc-pages","toolName":"documents","link":"webUrl","collapse":{"by":"document"}}

by, expansion, and missing have the same meaning as on ranked queries. by: "document" requires a parent ID or a schema attribute marked document: true; an absent document-identity schema declaration fails the search. Rows with missing values follow the gateway’s missing policy. The gateway’s expansion default is tunable: larger windows can expose more groups at increased store work and response size, up to the 10,000-row candidate cap. limit counts distinct groups within that expanded candidate window; it does not guarantee a complete document search.

Collapsed searches return structuredContent.groups, in relevance order. Each group contains key, by, and best (its best matching row, including row ID, score, attributes and configured source link). Only that row is sent; matching continuation pages and the gateway’s duplicate top-level rows are omitted. The text content shows each best row’s excerpt and source link. structuredContent.collapse retains the gateway’s read and exhausted metadata. Paging walks groups: total counts groups and each cursor continues at the next group. Cite the best row’s source link and use its ID with get_documents for full details. get keeps its existing record response.

list_namespaces advertises the configured collapse object. Namespace seeds without collapse retain flat search responses, including photo searches. The same configuration applies when using generic search tools. Callers cannot override collapse via tool arguments; authorization and filters still run through the existing gateway paths.

Unavailable-source snapshots

fetch_original also accepts immutable inventory-snapshot original: IDs. These references retain the exact inventoried revision after authenticated source metadata returned 404 or 410. The response explicitly reports retentionBasis=inventorySnapshot, sourceAvailability=unavailable, the recorded observation status/time and deletionStatus=unknown. This is historical evidence, not a live source check. SourceLink is labelled as an inventory citation.

Whole-file resources retain their actual PDF or PNG MIME type. PDF page requests work for retained PDFs; PNG page requests are rejected. Authorization, namespace binding and configured pins apply before bytes are returned. Internal S3 and proof locations are not exposed. Existing schema 1 originals remain readable; older gateway readers fail closed on snapshot schema 2. No new tool arguments or HTTP registration route are required.

Unclassified original bytes

An explicitly opted-in source original can be retained without an established format. fetch_original returns application/octet-stream, declared MIME, formatValidation=unverified, exact SHA/size/version and its source citation. sourceFormatMismatch is unknown. This is a byte-fidelity proof, not a valid container or PDF claim. Whole-file size limits, namespace grants and pins still apply; requesting pages fails. Known corruption and error-response signatures are refused even if an opaque catalog record was saved earlier.

esc