quarryDocs

MCP server

quarry's two MCP addresses, every tool with its inputs and results, and how to pay per question inside MCP.

quarry is a Model Context Protocol server named quarry, served over Streamable HTTP. Setup for each client is in Connect your tool; this page is the reference.

Addresses and sign-in

quarry's two addresses, both on https://quarry.market:

AddressSign-inUse
/api/mcpOptionalThe free address. With no Authorization header, the caller is anonymous. With Authorization: Bearer and an API key or an OAuth token, it acts as that quarry account.
/api/agent/mcpRequiredThe sign-in address. The same tools, signed in from the first request, plus the older tool names below.
  • An invalid or revoked token gets HTTP 401 with a WWW-Authenticate challenge whose resource_metadata points to /.well-known/oauth-protected-resource/api/mcp (or …/api/agent on the sign-in address). Clients use it to start OAuth sign-in.
  • On the free address, calling an account tool without a token also gets that 401, with scope="openid offline_access", so a client can sign in when it first needs to. Claude does: added with Sign in when needed, it asks the user to connect their quarry account at that call.
  • An API key is sent as Authorization: Bearer qk_....

Tools

ToolAccessRead-onlyWhat it does
searchFreeYesFinds the datasets that answer a question, across the catalog
get_datasetFreeYesOne dataset's details, prices, and how to pay
askFree, subscription, or paid per questionNoThe passages of one dataset that answer a question, with citations
quote_accessAccountYesWhether a question on a dataset is included in your subscription
get_my_accessAccountYesYour subscriptions, what's left this month, and your latest purchases
request_subscriptionAccountNoPrepares a Stripe Checkout link for a monthly offer
x402_payment_guideFreeYesHow to pay per question with a USDC wallet

Read-only tools are also idempotent. ask isn't: without a reused request_id, each call is a new question. request_subscription isn't read-only, but calling it again returns the same Checkout link until it's paid or expires.

InputTypeLimits
querystring, requiredThe question or topic, 1 to 500 characters
categorystring, optionalOne category, up to 40 characters
limitinteger, optional1 to 10, default 5

The result is { results, available_categories, next_step }. Datasets are ranked by meaning and wording across the whole catalog, then reordered by a reranking model. Each result has:

  • username, slug, name, description (first 300 characters), category, and score (higher is better; null when only the word search found the dataset);
  • price_usdc as a string, or null when the dataset only sells subscriptions, and free (true when a question costs nothing);
  • subscription_offer (or null), documents (number of files), listed_at, content_updated_at;
  • listing_url, endpoint, and next, the ask call to make for this dataset;
  • snippet: { text, filename, page }, a free preview of the best-matching passage, at most 200 characters.

Only the top five results can carry a snippet. It's null when the seller turned previews off, a preview limit was reached, the preview took more than 4 seconds, or no passage matched. A missing preview never fails the search.

get_dataset

Inputs: username (1 to 60 characters) and slug (1 to 80). The result is the dataset contract plus:

  • how_to_pay: null for a subscription-only dataset, { free: true, summary } for a free one, or the x402 details (price, network, steps, a JavaScript example);
  • subscription_offer, when the dataset has a monthly offer;
  • your_access, the quote_access result, when you're signed in;
  • stats (total_calls, last_query_at) and next_step;
  • response: the shape of what ask returns (below), and http: the contract's request and response for the HTTP route, which differ.

ask

InputTypeLimits
username, slugstring, requiredThe dataset, as returned by search
querystring, requiredThe question, 1 to 2,000 characters
top_kinteger, optionalPassages to return, 1 to 20, default 5
request_idstring, optional1 to 128 characters: letters, digits, _, ., :, or -

ask answers the first way that applies:

  1. A free dataset answers directly, with no account or payment.
  2. Your subscription, when you're signed in and it has questions left. A reused request_id returns the saved answer for 24 hours, even once the month's questions have run out, so a retry after a timeout is never counted twice. Without a request_id, every call is a new question.
  3. An x402 payment sent with the call: see Pay per question inside MCP.
  4. Otherwise, a payment-required result (below).

A successful answer:

{
  "dataset": {
    "name": "Market outlook",
    "username": "alice",
    "slug": "market-outlook",
    "listing_url": "https://quarry.market/explore/alice/market-outlook",
    "updated_at": "2026-09-20T10:00:00Z"
  },
  "query": "Which segment grew fastest?",
  "results": [
    {
      "chunk_id": "3f6c2a9e-8d41-4b7a-9c0e-5a1d2b3c4d5e",
      "content": "Self-serve revenue grew 41% year over year, the fastest of any segment.",
      "relevance": 0.83,
      "citation": {
        "filename": "q3-outlook.pdf",
        "page_start": 12,
        "page_end": 12,
        "rows": null,
        "heading_path": ["Results", "Segments"],
        "document_updated_at": "2026-09-20T10:00:00Z",
        "listing_url": "https://quarry.market/explore/alice/market-outlook"
      }
    }
  ],
  "how_to_cite": "Cite as: <filename>, p. <page_start> (or rows <rows.start>–<rows.end>) — Market outlook (https://quarry.market/explore/alice/market-outlook)",
  "terms": "https://quarry.market/terms",
  "paid_with": "subscription"
}

Results come best first. relevance runs from 0 to 1, higher is better: the reranking model's score, or the similarity of meaning when reranking wasn't available (see Ask over HTTP). Compare it between the passages of one answer, not across datasets. chunk_id is the passage's stable id, the same one the HTTP route returns. paid_with is free, subscription, or x402. Subscription answers also carry quota (quota, used, reserved, remaining, resets_at, and ids) and replayed. An answer to an agent signed in to a quarry account (connected, or with an API key) carries purchase_url, this question's page on the account's purchases. Cite the file name, the page or rows, and the listing link. terms is the address of the terms, which say what you may do with the passages. rows is set for passages of CSV, TSV, JSON, and JSON Lines files, as { start, end }: spreadsheet rows (the header is row 1), JSON Lines lines, or JSON array elements counted from 1. It's null for other files. document_updated_at is when the file changed on quarry, not when the source was written: rely on dates stated in the passage when timing matters.

When no passage answers the question, ask returns isError: true with the error NO_MATCH, on every path: no x402 payment settles (the same signed payment can be sent again with another question to the dataset), and no subscription question is used. Rephrase the question, or try another dataset.

When ask can't answer without payment, it returns isError: true with:

  • structuredContent: the x402 PaymentRequired object (x402Version, resource, and accepts with scheme, network, asset, amount, and payTo);
  • a second text block, { quarry: { status, dataset_id, price_usdc, subscription_offer, subscribe_url, how_to_access, web_payment, next, terms } }. status is payment_required or quota_exhausted_or_not_subscribed, and next names the request_subscription call when a monthly offer exists. web_payment is the request (method, url, body) for a wallet that pays web requests but can't sign inside an MCP call, such as Coinbase's: see Give your assistant a wallet. From an agent signed in to a quarry account, its body carries an account_token that lists the purchase on that account: see Ask over HTTP.

A dataset that only sells subscriptions returns the error SUBSCRIPTION_REQUIRED instead, with the same quarry object in the same text block and no structuredContent.

Account tools

These need a signed-in account or an API key (see Addresses and sign-in).

  • quote_access (username, slug) returns { dataset_id, dataset, status, access, subscription_offer, x402 }, without using a question. The statuses are listed in the Subscription API.
  • get_my_access (no input) returns { subscriptions, latest_purchases, purchases_url }. Each subscription has its status, offer, dataset, renewal state, Checkout link if payment is pending, and access. latest_purchases holds the 10 newest questions asked from the account, each with asked_at, dataset, listing_url, paid_with, amount_usdc for a wallet payment, and purchase_url, but never the question itself: another agent or a teammate's key may have asked it. purchases_url is the full list.
  • request_subscription (dataset_id, offer_id, both from quote_access) returns { status, subscription_id, url, next_step }. url is a Stripe Checkout link for the person to review and pay; nothing is charged until they do. Clients that support it are asked to open the link.

x402_payment_guide

Takes no input and returns a Markdown guide to paying per question with a USDC wallet, over MCP and HTTP.

Older tool names

The sign-in address also serves the tools of quarry's first MCP server:

  • search_datasets (query up to 200 characters, limit 1 to 50, default 20) returns { datasets };
  • query_dataset takes the ask inputs with a required request_id and returns the raw HTTP result, not citations. It only uses subscriptions.

Pay per question inside MCP

quarry follows the x402 MCP transport. Call ask without payment, sign one of the accepts entries of the returned PaymentRequired with your wallet, and call ask again with the identical arguments and the payment in _meta["x402/payment"]:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { x402Client } from "@x402/core/client";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";

const mcp = new Client({ name: "my-agent", version: "1.0.0" });
await mcp.connect(new StreamableHTTPClientTransport(new URL("https://quarry.market/api/mcp")));

const payer = new x402Client();
registerExactEvmScheme(payer, { signer: privateKeyToAccount(process.env.AGENT_PRIVATE_KEY) });

const args = { username: "alice", slug: "market-outlook", query: "Which segment grew fastest?" };
const unpaid = await mcp.callTool({ name: "ask", arguments: args });
if (unpaid.isError && unpaid.structuredContent?.accepts) {
  const payment = await payer.createPaymentPayload(unpaid.structuredContent);
  const paid = await mcp.callTool({ name: "ask", arguments: args, _meta: { "x402/payment": payment } });
  if (paid.isError) console.log("Not paid:", paid.structuredContent?.error ?? paid.content[0].text);
  else console.log(JSON.parse(paid.content[0].text), paid._meta?.["x402/payment-response"]);
} else {
  // Free dataset, subscription-only dataset, or an error: print what came back.
  console.log(unpaid.content.map((c) => c.text).join("\n"));
}
await mcp.close();

Install the packages with npm install @modelcontextprotocol/sdk @x402/core @x402/evm viem and run the file as an ES module, for example node agent.mjs. AGENT_PRIVATE_KEY is the private key of a wallet holding USDC on the network in accepts. The x402 client refuses to sign more than $1 per payment by default; for a pricier dataset, call payer.setSpendControls({ maxAmountPerPayment: "$5" }) with your own cap.

What comes back:

  • Paid: the passages, with paid_with: "x402". _meta["x402/payment-response"] holds the settlement: success, transaction (the transaction hash), network, and payer. Without a quarry account the answer also carries purchases, a sentence on where questions asked from an account are listed; with one, purchase_url.
  • Refused: a payment that doesn't match, fails verification, or is refused at settlement returns PaymentRequired again, with the reason in error, and no passages. An unfunded wallet is refused at verification: error names the balance or a reverted transfer.
  • In flight: a signed payment already being used by another call returns PAYMENT_IN_FLIGHT. Wait for that call's result.
  • Not charged: you pay only when the search succeeds. A failed search returns SEARCH_FAILED, and the payment stays unspent.

For HTTP instead of MCP, see Paying with x402.

Errors and limits

A tool that fails returns isError: true and a text block with { "error": { "code", "message", "details" } } (details is optional). Check isError as well as the HTTP status: a failed tool call still answers HTTP 200. Every code is in Error codes, and rate limits are in Limits.

Arguments outside the limits above never reach the tool. The result is still isError: true, but its text is plain, starting with MCP error -32602, not JSON: check for it before parsing.

On this page

View as Markdown