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:
| Address | Sign-in | Use |
|---|---|---|
/api/mcp | Optional | The 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/mcp | Required | The 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-Authenticatechallenge whoseresource_metadatapoints to/.well-known/oauth-protected-resource/api/mcp(or…/api/agenton 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
| Tool | Access | Read-only | What it does |
|---|---|---|---|
search | Free | Yes | Finds the datasets that answer a question, across the catalog |
get_dataset | Free | Yes | One dataset's details, prices, and how to pay |
ask | Free, subscription, or paid per question | No | The passages of one dataset that answer a question, with citations |
quote_access | Account | Yes | Whether a question on a dataset is included in your subscription |
get_my_access | Account | Yes | Your subscriptions, what's left this month, and your latest purchases |
request_subscription | Account | No | Prepares a Stripe Checkout link for a monthly offer |
x402_payment_guide | Free | Yes | How 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.
search
| Input | Type | Limits |
|---|---|---|
query | string, required | The question or topic, 1 to 500 characters |
category | string, optional | One category, up to 40 characters |
limit | integer, optional | 1 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, andscore(higher is better;nullwhen only the word search found the dataset);price_usdcas a string, ornullwhen the dataset only sells subscriptions, andfree(truewhen a question costs nothing);subscription_offer(ornull),documents(number of files),listed_at,content_updated_at;listing_url,endpoint, andnext, theaskcall 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:nullfor 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, thequote_accessresult, when you're signed in;stats(total_calls,last_query_at) andnext_step;response: the shape of whataskreturns (below), andhttp: the contract'srequestandresponsefor the HTTP route, which differ.
ask
| Input | Type | Limits |
|---|---|---|
username, slug | string, required | The dataset, as returned by search |
query | string, required | The question, 1 to 2,000 characters |
top_k | integer, optional | Passages to return, 1 to 20, default 5 |
request_id | string, optional | 1 to 128 characters: letters, digits, _, ., :, or - |
ask answers the first way that applies:
- A free dataset answers directly, with no account or payment.
- Your subscription, when you're signed in and it has questions left. A reused
request_idreturns 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 arequest_id, every call is a new question. - An x402 payment sent with the call: see Pay per question inside MCP.
- 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 x402PaymentRequiredobject (x402Version,resource, andacceptswith scheme, network, asset, amount, andpayTo);- a second text block,
{ quarry: { status, dataset_id, price_usdc, subscription_offer, subscribe_url, how_to_access, web_payment, next, terms } }.statusispayment_requiredorquota_exhausted_or_not_subscribed, andnextnames therequest_subscriptioncall when a monthly offer exists.web_paymentis 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, itsbodycarries anaccount_tokenthat 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, andaccess.latest_purchasesholds the 10 newest questions asked from the account, each withasked_at,dataset,listing_url,paid_with,amount_usdcfor a wallet payment, andpurchase_url, but never the question itself: another agent or a teammate's key may have asked it.purchases_urlis the full list.request_subscription(dataset_id,offer_id, both fromquote_access) returns{ status, subscription_id, url, next_step }.urlis 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(queryup to 200 characters,limit1 to 50, default 20) returns{ datasets };query_datasettakes theaskinputs with a requiredrequest_idand 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, andpayer. Without a quarry account the answer also carriespurchases, 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
PaymentRequiredagain, with the reason inerror, and no passages. An unfunded wallet is refused at verification:errornames 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.