Hermitage Fine Art β MCP
Content and auction-platform tools for office agents. Nothing to install: point an MCP client at the endpoint below.
π΄ Production β writes are live
Endpoint: https://mcp.admin.hermitagefineart.net/mcp
Tools (48)
statusreadEnvironment, database hosts, S3 buckets, row counts of the key collections, and how many documents still carry page content in the pre-segments `blocks` array rather than in `segments`, and whether this server can publish the site (`publishing`). Call this first to see whether writes would hit production.
content_describereadLive field map of a collection: field names, types, which are localized and which are relations, and the allowed values of a select (a lot's `status` with its names and what each does on the site). Use it before writing so field names and shapes are right. It cannot describe page content, which is a json field: `pages`, `pages-auctions` and `sales-categories` show `segments` (what the site renders) and `blocks` (the pre-segments array, kept but never rendered) β read those with content_segments_read instead.
content_listreadList rows of a collection with optional filters. Each filter is 'field=value' (equals) or 'field~=value' (contains); filter by the field's real name β a lot's number is `number`, not `lotNumber`. Rows carry long descriptions, so ask for `fields` unless you need whole documents.
content_getreadFetch a single document. locale 'all' returns every locale side by side: a locale that is simply missing from the answer has no translation at all (the page then falls back to English), which is different from an empty string β that one blanks the public page. On a page it returns the raw content json too; `blocks` there is the pre-segments array and is not what the site renders, so read a page's content with content_segments_read.
content_global_readreadGlobals are singletons with no id (e.g. the site global holding the notice bar). 'list' shows the known slugs, 'describe' its fields, 'get' its values β with locale 'all' to see which translations exist.
content_inspectreadFull profile of one lot across both databases: lot, object, attachments, private-sales item and the auction's site pages-auctions record β the one that carries the auction page's content and meta, flagged MISSING when absent. The fastest way to ground a change before making it; to read or edit that page's content, follow up with content_segments_read.
content_segments_componentsreadThe vocabulary of page content: which components exist and, for one of them, its props β types, which are required, which are translated, which take their values from other documents. Read this before adding or filling a segment; component and prop names are not guessable.
content_segments_readreadPage content lives in `segments`. This shows the document's segments in order β component, a one-line summary and anything that fails validation β and says whether the page renders from segments or from the pre-segments `blocks` array. A document that has segments NEVER renders its blocks, so a leftover blocks list is dead weight, not content. Ask for one segment to see its stored props and which languages each translated field has.
content_object_idsreadResolve a batch of lot references to their catalogs object ids (objects have no slug, so bulk edits need the uuids). Give either several 'auction/lot' refs, or one auction slug plus lot numbers.
content_audit_sales_itemsreadFind private-sales rows whose lot is missing or whose lot has no object β those render broken on the site.
content_audit_mastersreadCheck that the canonical S3 master exists for each media row of an owner type. Legacy masters stored under their old '<id>.WxH.jpg' name read as missing here.
content_verify_attachmentsreadRe-check an owner's attachments and their S3 masters. Run it in a fresh call after an upload: a large image can land in S3 while its rows roll back.
content_mark_soldwriteSet objects.sold on the lots' objects. This is the ONE sold flag: auction, lot and private-sales pages all read it, so a piece marked sold shows as sold everywhere. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_mark_resalewriteSet or clear objects.resale on the lots' objects β the resale right levied on the piece. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_mark_temporarywriteSet or clear objects.temporary on the lots' objects β the β temporary-import status. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_mark_vatwriteSet or clear objects.vat on the lots' objects β VAT applies to the piece. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_import_soldwriteFetch an ended auction's results from Bidspirit and set objects.sold on every lot that sold. Matches the Bidspirit auction by day and name unless bidspirit_id is given; scraping takes about a minute. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_edit_textwriteWrite one localized scalar field, or a batch of them. An empty string is REFUSED: empty is not the same as absent β it blanks the public page, while a missing locale falls back to English. Use clear to empty a field deliberately. Only text fields: page content is json, and writing a string into it would leave the page unable to read its own content β use content_segments_set. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_global_setwriteWrite a field of a global (singleton). The site-wide notice bar is one of these: it is the `notice` field of the `site` global, not a document you can look up β that is where to put an announcement and where to remove one. A localized field REQUIRES a locale; without one Payload writes English under every language. Taking a notice down means clearing it in ALL FOUR locales. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_createwriteCreate a row from a JSON object. Creating a catalogs auction also creates the site pages-auctions record it needs β without that record the auction's lots do not render. A lot's `status` takes a name β available, disabled, withdrawn, no-images β or its code; content_describe catalogs lots says what each does on the site. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_updatewriteUpdate a row from a JSON object of changed fields. Localized fields need a locale. Page content (`segments`) is not edited this way β use content_segments_set and content_segments_edit, which keep segment ids and check the result; a whole-array rewrite here is refused when it would break a segment. A lot's `status` takes a name β available, disabled, withdrawn, no-images β or its code; content_describe catalogs lots says what each does on the site. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_deletewriteDelete one or many rows. Nothing cascades: the plan lists rows that point at the deleted one (an auction's lots, its site page) and they are left behind β delete them too if that is what you mean. Deleting an owner collection (objects, auctions, pagesβ¦) on production is gated behind confirm. Deleting an attachment removes the join row only β the S3 bytes are content-addressed and may be shared. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_clone_productwriteCopy a lot's object, its attachments (same content-addressed media, no re-upload), the lot itself, and optionally a private-sales item. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_segments_editwriteStructural edits to a page's content: add a segment (from the components in content_segments_components), remove one, move it, or switch it off without deleting it. A new segment starts empty, so fill it with content_segments_set before it can render β an incomplete segment is skipped at build time and its block simply does not appear on the page. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_segments_setwriteWrite one value inside a segment, addressed by the dotted path of the prop ("title", "paragraphs.0.text", "slides.1.image_wide"); row-add and row-rm grow and shrink a list first. A translated prop is stored per language, so locale picks which one you are writing β and clearing a value removes that language rather than blanking it, which is what makes the fallback to English work. A write that would leave the segment unrenderable is refused, with the reason. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_customize_category_blockwriteThe pre-segments way to give one private-sales category its own opening text: clone the shared private-sales/root block to private-sales/<category> and repoint that category, leaving the root text untouched. It only applies to a category that still renders from blocks β a converted one opens with the private-sales-intro segment, whose text lives in the component, and the tool refuses with the segment commands to use instead. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_ensure_auction_pagewriteEvery auction needs a site pages-auctions record keyed by its id, or its lots do not render on the built site. The record is created with the auction-legal segment, which is what carries the temporary-import, resale and VAT notes. Idempotent: it does nothing when the record exists. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_sales_itemwriteAdd a lot to a private-sales category, or move an item to the top. Private-sales items have no sort field β they are ordered newest first, so "pin" recreates the row. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_set_platformswriteFetch upcoming auction links from the external platforms (about a minute of scraping), keep those whose date matches the auction, and merge them into auctions.platforms by host β links on other hosts are kept. The buyer's premium per platform comes from PLATFORMS_FEE_*; a real run refuses when a matched platform has no fee configured. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_highlightwriteThe "auction highlight" cards (image, title, realized price) shown on /auctions-highlights/. Add fills title, lot number and the first gallery image itself β you supply the price. Order is create order, newest first: the position field is never read. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_uploadwriteAttach images (or PDFs and office files) to a record. When the file sits on the person's own computer or phone, get an upload_token from content_upload_link first β a file attached to this conversation cannot be passed to a tool. Images go through the convert service and end up as a jpeg master. Re-running is idempotent, which is how a flaky large upload is healed β always verify afterwards with content_verify_attachments in a separate call. gallery is the ordered role: without sort the files are numbered 0,1,2β¦ in the order given, and the piece's main image is simply gallery sort 0 β there is no separate cover role. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_catalog_pdfwriteAttach a catalog PDF to an auction: the original becomes the attachment's master, and a compressed copy is published in the CDN bucket under the master's content key β the address the site links the catalog from. The same copy also goes to files/auctions/<slug>/<name>.pdf, which the auction's field points at. A previous catalog attachment of the same role is replaced once the new one is in place. When the PDF is on the person's own machine, get an upload_token from content_upload_link first β a file attached to this conversation cannot be passed to a tool. The target bucket is whatever S3_CDN_BUCKET names for this environment β the public one in production, the local store in development β and it is a DIFFERENT bucket from image masters. Changes reach the public site only after `site_publish`; offer it once the person has finished editing, with the `changes` from the `publish:` line this tool ends its report with.
content_s3writeList, copy, move or delete objects in the buckets. Keys are sharded by the media id: objects/aa/bb/cc/<id>.jpg β pass a bare id to ls and it is expanded for you. Deleting bytes is not undoable and a master may be shared by several records (keys are content hashes) β list first.
content_onedrive_lsreadList a OneDrive/SharePoint folder (read-only). Use it to find the path to feed content_upload or content_catalog_pdf. Lot photos live under 'PHOTO BY LOT NUMBER/<AUCTION>' and are named <lotNumber>_<index>.
content_import_photoswriteImport lot gallery photos for a whole auction from the photographers' OneDrive folder in one go β instead of a content_upload per lot. Photos are named "<lot>_<n>" (12_1.jpg, 7bis_2.jpg); each goes to its lot's gallery in the order of <n>. First call with dry_run: true. That reads the folder (no downloads) and returns a plan: how many lots and photos, which lots already have a gallery (left alone unless replace), lots in the auction without photos, files that match no lot, files in a format the store does not keep (tif β the person must convert them). Show the person that plan in plain words. Once they agree, call again with the plan_id and dry_run: false. The import runs in the background, one photo at a time, and survives restarts; the call waits up to ~40 seconds and returns its state. Follow it with jobs_status. A plan not started within an hour expires β make a new one. The result is one JSON document: `plan_id` and `summary` for a plan; for a started import the job record (`state`, `counts`, `failed_items`, `result`, `publish`) and `next` β what to do now.
content_import_lotswriteCreate an auction's lots from its lot list (.xlsx or .xls): one object (title and description per language, the β temporary-import flag) and one lot (number, estimates) per row. Only an auction that has no lots yet is imported into. The file comes either from the person's computer β get an upload_token from content_upload_link with purpose lots_xlsx first β or from OneDrive (onedrive_path; content_onedrive_ls finds it). First call with dry_run: true. That reads the file and returns a plan: the sheet and how its columns map to fields (fields in guessed_by_position were taken by column position, not by a header β confirm them), columns left unmapped, how many lots, three sample lots, how many lots have a title and a description in each language, temporary lots, errors and warnings by sheet row. When a column is mapped wrong, call the dry run again with `mapping` (and `sheet` for another sheet); with an upload_token the same link serves every dry run until the import starts. Show the person that plan in plain words. A plan with `blocking` reasons cannot start: tell the person what to fix. Once they agree, call again with the plan_id and dry_run: false. The import runs in the background and survives restarts; follow it with jobs_status. A plan not started within an hour expires β make a new one. A language without a description is left unwritten, so a description can be added later. When `pages_auction` is false the auction has no site page record and its lots will not render: offer content_ensure_auction_page. The result is one JSON document: `plan_id` and `summary` for a plan; for a started import the job record (`state`, `counts`, `failed_items`, `result`, `publish`) and `next` β what to do now.
jobs_statusreadReport on a background job (a photo import, a PDF compressionβ¦). While it is queued or running, the call waits up to ~40 seconds for it to move; call it again with the same job until it is done or failed β no pause is needed in between. You may also stop, tell the person it is running, and check later. Tell the person the outcome in plain words: how many items were done, which failed and why. A finished job that wrote content carries `publish` β the changes to pass to site_publish once the person has finished their edits. The result is one JSON document: `state` (planned, queued, running, cancelling, done, failed, cancelled, expired), `step`, `counts` per item state, `summary` (the approved plan), `result`, `failed_items`, `error` (the job itself stopped), `next` β what to do now.
jobs_listreadThe recent background jobs, newest first β use it when a job id is lost. By default the ones waiting or running plus the last finished few. The result is one JSON document with `jobs`.
jobs_cancelwriteStop a planned, queued or running job. A running one stops after the item in hand; what was done stays done (nothing is rolled back) and the rest is skipped. Preview with dry_run: true and tell the person how much has been done already. The result is one JSON document: the job record.
jobs_retrywriteQueue a new job that redoes only the failed items of a finished job, with the same settings. Preview with dry_run: true (lists the failed items). The result is one JSON document: the new job record and `next`.
content_upload_linkUse this when the file is on the person's own computer or phone β which is the usual case, and the only way to get it here. A file attached to this conversation CANNOT be passed to a tool: ask for nothing, just hand over the link this returns. The link opens a page where they drop the file; it carries the destination, so they cannot send it to the wrong place. Then, in order: write the `url` in your own reply so the person can click it, and tell them to sign in if asked, drop the file and come back; call content_upload, content_catalog_pdf or content_import_lots with the token and dry_run: true β that call waits a few seconds for the file and shows you the plan, and if nothing has arrived yet you simply call it again; show the person the plan, and write only once they agree. The result is one JSON document: `url` (for the person), `token`, `label`, `max_files`, `accepted_types`, `expires_at`, and `next` β the exact call to make once the address is in your reply.
platforms_fetch_upcomingreadLinks to our upcoming (non-live) auctions on Bidspirit, Invaluable, Drouot, the-saleroom and Interencheres, with their dates normalised. Feed these to content_set_platforms. Drives a headless browser, so expect it to take about a minute. The result is one JSON document: `auctions` β { platform, url, date (ISO 8601) }.
platforms_list_resultsreadList the ended auctions in the Bidspirit admin β the ids feed platforms_fetch_results and content_import_sold. Drives a headless browser, so expect it to take about a minute. The result is one JSON document: `auctions` β { id, date, name, lots, sold, total }; `lots` and `sold` are null when unknown.
platforms_fetch_resultsreadSold lots with their hammer prices for one Bidspirit auction. Unsold lots are omitted, and lot numbers keep their suffix. Drives a headless browser, so expect it to take about a minute. The result is one JSON document: `lots` β { lotNumber, hammerPrice }.
platforms_compare_pricesreadCompare per-lot estimates between our site and the other platforms, flagging the lots that differ. The hermitagefineart URL is the canonical source and must be included. Drives a headless browser, so expect it to take about a minute. The result is one JSON document: `platforms`, `totalLots`, `differences`, and `lots` β one row per lot with lotNumber, title, hasDifferences and one entry per platform: { estimateLow, estimateHigh } (each null when the platform shows none), or null when the lot is missing there.
platforms_compare_inforeadCompare titles, prices and images per lot between our site and the other platforms, reporting the mismatches. The hermitagefineart URL is the canonical source and must be included. Drives a headless browser, so expect it to take about a minute. The result is one JSON document: `platforms`, `totalLots`, `issueCount`, and `issues` β { lotNumber, title, issues, platforms } per lot with a mismatch, where `platforms` has one entry per platform: { title, estimateLow, estimateHigh, hasImage } (each estimate null when the platform shows none, titleMismatch: true when its title differs), or null when the lot is missing there.
site_publishwritePublish the public site (hermitagefineart.com) from the live databases. Works only on the production server. Nothing written with the other tools shows on the site until a publish runs. A publish takes the database as it is when the build reads it β every edit made since the last publish, other people's included. Run it once, when the person has finished the edits they want live, not after each write. It takes 3β6 minutes. Every publish rebuilds every page β home, news, text pages, private sales, every auction's own page with its lot list β EXCEPT the individual lot pages of auctions that have already taken place. So list in `changes` everything you changed: the server works out which of those pages need rebuilding and says in the preview which entries are not needed. Write tools end their report with a `publish:` line naming the entries to pass. A publish adds and updates pages; it never takes one down. A deleted item's own page, every page of a disabled auction, and the old address after a slug change stay on the site (the item drops out of lists). Tell the person that taking a page down needs a developer to run a full build. Call with dry_run: true first and show the person the preview. Publish only after they agree, with the arguments the preview's `next` names. Then follow the run with site_publish_status. The result is one JSON document. `action`: preview, started or joined. Preview: `would` β start, join (a queued run that has not read the database yet and covers the request) or wait (behind a run in progress, `ahead`); `last_success` β the last publish that finished well and when it read the database. Every result: `changes` β a verdict on each entry (rebuild, not_needed, not_found, auction_disabled, invalid) with a note; `lots` / `auctions` β what the build is asked to render on top of the ordinary set; `next` β what to do now. `url` is the CI page, for developers; the person cannot open it.
site_publish_statusreadReport on a site publish: queued, running, done or failed. While it is queued or running, the call waits up to ~40 seconds for the state to change; call it again with the same run until it reports done or failed β no pause is needed in between. You may also stop, tell the person it is running, and check later. Without run: lists the publishes queued or running now and the last finished one. Use it when you have lost the run number. An edit is on the site once a publish that finished as done read the database after the edit was made (db_read_at). Pages are cached for up to a minute: if the person still sees the old version, a reload a minute later shows the new one. A publish that stays queued for more than 10 minutes with nothing running ahead of it is stuck: stop calling and tell the person a developer should check the build machine. Tell the person the outcome in plain words. On failure, pass on the errors in plain words, not the raw log. The result is one JSON document: a run record, or `runs` β a list of them β without run. A record: `state` (queued, running, done, failed; `cancelled` when someone stopped it); `elapsed_s` β time in the queue while queued, otherwise since it started; `db_read_at` β when the build read the database; `log_unavailable` β the log could not be read this time, so db_read_at is unknown; `warnings` / `errors` from the build; `log` β the lines asked for; `next` β what to do now, absent once it is done. `url` is the CI page, for developers; the person cannot open it.