> ## Documentation Index
> Fetch the complete documentation index at: https://firecrawl-feat-exchange.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Alexandria

> The knowledge library for superintelligence

<iframe
  title="An illustrative agent journey with Alexandria"
  sandbox="allow-scripts"
  style={{ width: "100%", height: "580px", border: "0", borderRadius: "12px", colorScheme: "dark" }}
  srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;padding:12px 0 10px;display:flex;flex-direction:column;
font:13px/1.85 ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
background:#26313B;color:#E4E9ED;border-radius:10px;overflow:hidden}
#controls{display:flex;align-items:center;justify-content:space-between;padding-right:14px}
#playback{min-width:46px}
button:focus-visible{outline:2px solid #FA5D19;outline-offset:3px}
.paused .live,.paused .cur{animation:none}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
#tabs{display:flex;gap:12px;justify-content:center;margin:0 14px 14px;flex:0 0 auto}
button{position:relative;font:inherit;font-size:11px;letter-spacing:.03em;cursor:pointer;
padding:0 0 7px;border:0;background:transparent;color:#7C8794;transition:color .2s;
min-width:78px}
button:hover{color:#C3CCD4}
button.on{color:#E4E9ED}
button .track{position:absolute;left:0;right:0;bottom:0;height:2px;background:#3C4854;
border-radius:2px;overflow:hidden}
button .seg{display:block;height:100%;width:0;background:#FA5D19;border-radius:2px}
button.on .seg{transition:width .6s cubic-bezier(.4,0,.2,1)}
#b{flex:1 1 auto;min-height:0;overflow:hidden;display:flex;flex-direction:column;
justify-content:flex-end;padding:0 14px;mask-image:linear-gradient(transparent,#000 8%);
transition:opacity .55s ease}
#b.fade{opacity:0}
.row,.sp,.user{flex:0 0 auto}
.row{white-space:pre-wrap}
.user{background:#323E49;margin:0 -14px;padding:1px 14px;white-space:pre-wrap;color:#E4E9ED}
.chev{color:#7C8794}
.dot{color:#E4E9ED}
.dotg{color:#7C8794}
.g{color:#94A1AD}
.d{color:#6E7A86}
.ok{color:#63C89B}
.hd{color:#E4E9ED;font-weight:600}
.mk{display:inline-block;width:3ch}
.acc{color:#FA5D19}
.ind{padding-left:20px}
.sp{height:10px}
.live{color:#FA5D19;animation:pulse 1.15s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
.cur{display:inline-block;width:7px;height:14px;background:#FA5D19;vertical-align:-2px;
animation:blink 1.1s steps(2,start) infinite}
@keyframes blink{50%{opacity:0}}
#status{flex:0 0 auto;padding:6px 14px 0;color:#FA5D19;min-height:22px}
</style></head><body>
<div id="controls"><div id="tabs"></div><button id="playback" aria-label="Pause animation">Pause</button></div>
<div id="b"></div>
<div id="status"></div>
<script>
var SCENES = [{"id": "Podcasts", "prompt": "Find podcast conversations about AI agents, then summarize what the speakers said.", "think": "I need a tool that searches what people said, not just episode titles.", "findQ": "podcast conversations about AI agents · sources: alexandria", "found": [["particle/podcasts/episodes/search", "Episode search"]], "inspect": "semantic_search or keyword_search · returns episode IDs and matching excerpts", "callQ": "provider: particle · capability: podcasts/episodes/search · semantic_search: AI agents", "calls": 1, "rows": [["Particle", "Episode IDs, titles, dates, and matching passages"]], "credits": "Execution uses the published tool price.", "done": "The matching passages give me evidence for a summary. I can follow episode IDs for more detail.", "after": "Writing the podcast brief…"}, {"id": "Finance", "prompt": "Find a source for cryptocurrency prices and prepare a market snapshot.", "think": "First I will discover the right data tool, then check its inputs and response.", "findQ": "cryptocurrency market prices · sources: alexandria", "found": [["coingecko/prices/simple", "Coin prices"]], "inspect": "Read supported identifiers and quote currencies before calling.", "callQ": "provider: coingecko · capability: prices/simple · options from the contract", "calls": 1, "rows": [["CoinGecko", "Price data for the requested coins and currencies"]], "credits": "Execution uses the published tool price.", "done": "The response gives me structured prices for the snapshot, with the source attached.", "after": "Preparing the market snapshot…"}, {"id": "Retail", "prompt": "Find Pizza Hut stores near a Canadian postal code.", "think": "I need store data for this location. I will inspect the location options first.", "findQ": "Pizza Hut Canada nearby stores · sources: alexandria", "found": [["pizzahut-ca/restaurants/stores", "Find nearby stores"]], "inspect": "postal_code · returns nearby stores and available store details", "callQ": "provider: pizzahut-ca · capability: restaurants/stores · postal_code: M5V 2T6", "calls": 1, "rows": [["Pizza Hut Canada", "Stores matching the requested location"]], "credits": "Execution uses the published tool price.", "done": "I can present nearby stores using the returned details without guessing which locations serve this area.", "after": "Preparing the nearby store list…"}];

var SPIN = ['✻','✳','✶','✻'];
var WORDS = ['Kindling','Smouldering','Crackling','Stoking','Crawling','Percolating','Simmering'];
var host = document.getElementById('b');
var tabs = document.getElementById('tabs');
var statusEl = document.getElementById('status');
var run = 0, tokens = 0, t0 = 0, statusTimer = null, word = 'Flowing';

function wait(ms){ return new Promise(function(r){ setTimeout(r, ms); }); }

var sceneIndex = 0;
function segs(){ return tabs.querySelectorAll('.seg'); }


function mark(f){
var all = segs();
for (var i = 0; i < all.length; i++){
all[i].style.width = i < sceneIndex ? '100%' : (i === sceneIndex ? (f * 100).toFixed(1) + '%' : '0%');
}
}
function el(cls, mine){
var d = document.createElement('div');
d.className = (cls || 'row');

if (mine !== undefined && run !== mine) return d;
host.appendChild(d); trim(); return d;
}
function gap(mine){ if (mine !== undefined && run !== mine) return; var d = document.createElement('div'); d.className = 'sp'; host.appendChild(d); }
function pad(s, n){ while (s.length < n) s += ' '; return s; }
function fmt(n){ return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }

function startStatus(){
clearInterval(statusTimer);
statusEl.textContent = '✻ Exploring Alexandria…';
}
function stopStatus(){ clearInterval(statusTimer); statusEl.innerHTML = ''; }

async function stream(target, segs, cps, mine){
for (var i = 0; i < segs.length; i++){
var span = document.createElement('span');
if (segs[i][1]) span.className = segs[i][1];
target.appendChild(span);
var txt = segs[i][0];
for (var j = 0; j < txt.length; j++){
  if (run !== mine) return;
  span.textContent += txt[j];
  if (j % 2 === 0) await wait(cps);
}
}
}

function toolLine(name, times, mine){
var d = el('row', mine);
var suffix = times > 1 ? ' ' + times + ' times' : '';
d.innerHTML = '<span class="live">●</span> <span class="g">Calling ' + name + suffix +
'… </span><span class="d"></span>';
return { node: d, name: name, suffix: suffix };
}
function toolDone(t){
t.node.innerHTML = '<span class="dot">●</span> <span>Called ' + t.name + t.suffix +
'</span> <span class="d"></span>';
}



function trim(){
while (host.children.length > 42) host.removeChild(host.firstChild);
}

function repeat(ch, n){ var out = ''; for (var i = 0; i < n; i++) out += ch; return out; }

async function renderTable(rows, mine){
var w = [];
rows.forEach(function(r){
r.forEach(function(cell, i){ if (!w[i] || cell.length > w[i]) w[i] = cell.length; });
});

function rule(l, m, r){
var out = l;
for (var i = 0; i < w.length; i++){
  out += repeat('─', w[i] + 2) + (i === w.length - 1 ? r : m);
}
return out;
}


function paint(target, row, cls){
function bar(){
  var b = document.createElement('span');
  b.className = 'd';
  b.textContent = '│';
  target.appendChild(b);
}
bar();
for (var i = 0; i < w.length; i++){
  var c = document.createElement('span');
  if (cls) c.className = cls;
  c.textContent = ' ' + pad(row[i], w[i]) + ' ';
  target.appendChild(c);
  bar();
}
}

var top = el('row ind d', mine); top.textContent = rule('┌', '┬', '┐');
await wait(90); if (run !== mine) return;

var head = el('row ind', mine);
paint(head, rows[0], 'hd');
await wait(130); if (run !== mine) return;

var mid = el('row ind d', mine); mid.textContent = rule('├', '┼', '┤');
await wait(110); if (run !== mine) return;

for (var i = 1; i < rows.length; i++){
var line = el('row ind', mine);
paint(line, rows[i], '');
await wait(130); if (run !== mine) return;
}

var bot = el('row ind d', mine); bot.textContent = rule('└', '┴', '┘');
await wait(200);
}

async function play(scene, mine){
word = WORDS[Math.floor(Math.random() * WORDS.length)];

var u = el('user', mine);
u.innerHTML = '<span class="chev">〉</span> ';
await stream(u, [[scene.prompt, '']], 20, mine);
await wait(950); if (run !== mine) return;
mark(0.08);
startStatus();

gap(mine);
var a = el('row', mine); a.innerHTML = '<span class="live">●</span> ';
await stream(a, [[scene.think, '']], 14, mine);
a.firstChild.className = 'dot';
await wait(900); if (run !== mine) return;


var pre = scene.pre || (scene.read ? [{ tool:'Read', args:'(' + scene.read.file + ')', result: scene.read.result }] : []);
for (var pi = 0; pi < pre.length; pi++){
gap(mine);
var pd = el('row', mine); pd.innerHTML = '<span class="live">●</span> ';
await stream(pd, [[pre[pi].tool, 't'], [pre[pi].args, 'g']], 9, mine);
await wait(600); if (run !== mine) return;
pd.firstChild.className = 'dot';
var pr2 = el('row ind g', mine);
pr2.innerHTML = '<span class="d mk">⎿</span>';
await stream(pr2, [[pre[pi].result, 'g']], 8, mine);
await wait(700); if (run !== mine) return;
}

gap(mine);
var t1 = toolLine('firecrawl.search', 1, mine);
var q1 = el('row ind g', mine);
q1.innerHTML = '<span class="d mk">⎿</span>';
await stream(q1, [['"' + scene.findQ + '"', 'd']], 11, mine);
await wait(2300); if (run !== mine) return;
toolDone(t1);
q1.innerHTML = '<span class="d mk">⎿</span><span class="g">found ' + scene.found.length + ' ' +
(scene.found.length === 1 ? 'capability' : 'capabilities') + ' · 0 credits</span>';
var fw = 0;
scene.found.forEach(function(r){ if (r[0].length > fw) fw = r[0].length; });
fw += 2;
for (var k = 0; k < scene.found.length; k++){
var fr = el('row ind g', mine); fr.innerHTML = '<span class="mk"></span>';
await stream(fr, [[pad(scene.found[k][0], fw), 'd'], [scene.found[k][1], 'd']], 8, mine);
await wait(420); if (run !== mine) return;
}
await wait(1100); if (run !== mine) return;
mark(0.42);

gap(mine);
var inspection = toolLine('Read tool contract', 1, mine);
var contract = el('row ind g', mine);
await stream(contract, [['Inputs, response, and examples included in the search result', 'd']], 8, mine);
await wait(700); if (run !== mine) return;
toolDone(inspection);
var details = el('row ind g', mine);
await stream(details, [[scene.inspect, 'g']], 8, mine);
await wait(900); if (run !== mine) return;
mark(0.6);

gap(mine);
var t2 = toolLine('firecrawl.scrape', scene.calls, mine);
var q2 = el('row ind g', mine);
q2.innerHTML = '<span class="d mk">⎿</span>';
await stream(q2, [['exchange: { ' + scene.callQ + ' }', 'd']], 11, mine);
await wait(3000); if (run !== mine) return;
toolDone(t2);

var w = 0;
scene.rows.forEach(function(r){ if (r[0].length > w) w = r[0].length; });
w += 2;
q2.innerHTML = '<span class="d mk">⎿</span>';
await stream(q2, [[pad(scene.rows[0][0], w), 'g'], [scene.rows[0][1], 'g']], 8, mine);
await wait(560); if (run !== mine) return;
for (var i = 1; i < scene.rows.length; i++){
var r = el('row ind g', mine); r.innerHTML = '<span class="mk"></span>';
await stream(r, [[pad(scene.rows[i][0], w), 'g'], [scene.rows[i][1], 'g']], 8, mine);
await wait(560); if (run !== mine) return;
}
var cr = el('row ind d', mine); cr.innerHTML = '<span class="mk"></span>';
await stream(cr, [[scene.credits, 'd']], 8, mine);
mark(0.74);
await wait(1200); if (run !== mine) return;

if (scene.table){
mark(0.84);
gap(mine);
await renderTable(scene.table, mine);
if (run !== mine) return;
await wait(900);
}

stopStatus();
gap(mine);
var d = el('row', mine); d.innerHTML = '<span class="live">●</span> ';
await stream(d, [[scene.done, '']], 5, mine);
d.firstChild.className = 'dot';
await wait(700); if (run !== mine) return;

mark(1);
var n = el('row', mine); n.innerHTML = '<span class="live">●</span> ';
await stream(n, [[scene.after, 'g']], 14, mine);
var cur = document.createElement('span'); cur.className = 'cur'; n.appendChild(cur);
await wait(1800); if (run !== mine) return;

host.className = 'fade';
await wait(650); if (run !== mine) return;
host.innerHTML = '';
host.className = '';
await wait(350);
}

function highlight(idx){
[].forEach.call(tabs.children, function(btn, i){ btn.className = i === idx ? 'on' : ''; });
}


function select(idx){
run++; stopStatus(); host.innerHTML = ''; host.className = '';
sceneIndex = idx; mark(0);
var mine = run, current = idx;
(async function(){
while (run === mine){
  highlight(current);
  sceneIndex = current;
  if (current === 0) mark(0);
  await play(SCENES[current], mine);
  if (run !== mine) return;
  current = (current + 1) % SCENES.length;
}
})();
}

SCENES.forEach(function(sc, i){
var btn = document.createElement('button');
btn.appendChild(document.createTextNode(sc.id));
var track = document.createElement('span');
track.className = 'track';
var seg = document.createElement('i');
seg.className = 'seg';
track.appendChild(seg);
btn.appendChild(track);
btn.onclick = function(){ if (paused) still(i); else select(i); };
tabs.appendChild(btn);
});
var playback = document.getElementById('playback');
var paused = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function still(idx){
run++; stopStatus(); host.innerHTML = ''; host.className = '';
sceneIndex = idx; highlight(idx); mark(1);
var scene = SCENES[idx];
el('user').textContent = '〉' + scene.prompt;
gap(); el('row').textContent = '1. Discover with firecrawl.search';
el('row ind g').textContent = scene.found[0][0];
gap(); el('row').textContent = '2. Inspect the contract';
el('row ind g').textContent = scene.inspect;
gap(); el('row').textContent = '3. Retrieve with firecrawl.scrape';
el('row ind g').textContent = scene.rows[0][1];
gap(); el('row').textContent = scene.done;
}
function labelPlayback(){
playback.textContent = paused ? 'Replay' : 'Pause';
playback.setAttribute('aria-label', paused ? 'Replay animation' : 'Pause animation');
document.body.classList.toggle('paused', paused);
}
playback.onclick = function(){
paused = !paused; labelPlayback();
if (paused){ run++; stopStatus(); } else select(sceneIndex);
};
labelPlayback();
if (paused) still(0); else select(0);
</script></body></html>`}
/>

Use Alexandria through the Firecrawl MCP server, CLI, SDKs, or API. Discover tools, inspect their inputs, and retrieve data with the same workflow.

<Steps>
  <Step title="Find the right tool">
    Describe what you need. Alexandria returns relevant tools alongside your web results.

    <CodeGroup>
      ```javascript JavaScript theme={null}
      const result = await firecrawl.search(
        "podcast conversations about AI agents",
        {
          sources: ["web", "alexandria"],
          limit: 2,
        },
      );
      ```

      ```python Python theme={null}
      result = firecrawl.search(
          "podcast conversations about AI agents",
          sources=["web", "alexandria"],
          limit=2,
      )
      ```

      ```bash cURL theme={null}
      curl https://api.firecrawl.dev/v2/search \
        -H "Authorization: Bearer $FIRECRAWL_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
        "query": "podcast conversations about AI agents",
        "sources": [
          "web",
          "alexandria"
        ],
        "limit": 2
      }'
      ```
    </CodeGroup>

    <Accordion title="Response: web pages and tools">
      Web pages are in `result.web`. Tools are in `result.tools`, ranked by relevance and returned with their input and response contracts. This shows one complete Particle tool item using its live contract and the billing branch’s generated request examples. The web result is illustrative; `limit: 2` allows up to two results. No recorded `example` is included because this tool does not publish one.

      ```json Complete tool item (illustrative search response) theme={null}
      {
        "web": [
          {
            "title": "A conversation about AI agents",
            "url": "https://example.com/podcasts/ai-agents"
          }
        ],
        "tools": [
          {
            "id": "particle/podcasts/episodes/search",
            "provider": "particle",
            "capability": "podcasts/episodes/search",
            "name": "Episode search",
            "description": "Use to find episodes that discussed something. This is the entry point: it returns ids the other podcast capabilities take.",
            "creditsCost": 15,
            "perRecord": false,
            "options": [
              {
                "name": "semantic_search",
                "type": "string",
                "about": "What was talked about, in your own words. Paraphrase-tolerant, so it finds dialogue that means this without containing these exact words."
              },
              {
                "name": "keyword_search",
                "type": "string",
                "about": "Exact tokens or phrases to match in the transcript, ranked BM25. Use when the wording itself matters."
              },
              {
                "name": "entity_id",
                "type": "string",
                "about": "Restrict to dialogue about one person or company, by entity id. Combines with either search."
              },
              {
                "name": "podcast_id",
                "type": "string",
                "about": "Restrict to one show, by podcast id."
              },
              {
                "name": "published_after",
                "type": "string",
                "about": "Earliest publication date, as YYYY-MM-DD."
              },
              {
                "name": "limit",
                "type": "number",
                "default": 25,
                "about": "Maximum records to return.",
                "max": 100
              },
              {
                "name": "cursor",
                "type": "string",
                "about": "Opaque cursor from a previous page. Omit for the first page."
              }
            ],
            "requiresOneOf": [
              [
                "semantic_search",
                "keyword_search"
              ]
            ],
            "response": {
              "about": "Matching episodes, each with the show it belongs to and the passage that matched. Several matching segments of one episode collapse to a single result, so a page can return fewer than the limit.",
              "key": "data",
              "fields": [
                {
                  "name": "id",
                  "type": "string",
                  "about": "Episode id, the input to podcasts/episodes."
                },
                {
                  "name": "title",
                  "type": "string",
                  "about": "Episode title."
                },
                {
                  "name": "podcast",
                  "type": "object",
                  "about": "The show this episode belongs to."
                },
                {
                  "name": "published_at",
                  "type": "string",
                  "about": "Publication date."
                },
                {
                  "name": "excerpt",
                  "type": "string",
                  "about": "The passage that matched the query."
                }
              ],
              "paginated": true
            },
            "examples": {
              "javascript": "const requestId = \"<unique-request-id>\";\nconst response = await fetch(\"https://api.firecrawl.dev/v2/scrape\", {\n  method: \"POST\",\n  headers: {\n    \"Authorization\": \"Bearer \" + process.env.FIRECRAWL_API_KEY,\n    \"Content-Type\": \"application/json\",\n    \"x-request-id\": requestId\n  },\n  body: JSON.stringify({\n  \"exchange\": {\n    \"provider\": \"particle\",\n    \"capability\": \"podcasts/episodes/search\",\n    \"options\": {\n      \"limit\": 25,\n      \"semantic_search\": \"<semantic_search>\"\n    }\n  }\n})\n});\nconst result = await response.json();\nif (!response.ok) throw new Error(result.error ?? \"Request failed\");",
              "python": "import os\nimport requests\n\nrequest_id = \"<unique-request-id>\"\nresponse = requests.post(\n  \"https://api.firecrawl.dev/v2/scrape\",\n  headers={\n    \"Authorization\": \"Bearer \" + os.environ[\"FIRECRAWL_API_KEY\"],\n    \"x-request-id\": request_id\n  },\n  json={\n  \"exchange\": {\n    \"provider\": \"particle\",\n    \"capability\": \"podcasts/episodes/search\",\n    \"options\": {\n      \"limit\": 25,\n      \"semantic_search\": \"<semantic_search>\"\n    }\n  }\n},\n  timeout=120\n)\nresponse.raise_for_status()\nresult = response.json()",
              "curl": "curl https://api.firecrawl.dev/v2/scrape \\\n  -H \"Authorization: Bearer $FIRECRAWL_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-request-id: <unique-request-id>\" \\\n  --data '{\n  \"exchange\": {\n    \"provider\": \"particle\",\n    \"capability\": \"podcasts/episodes/search\",\n    \"options\": {\n      \"limit\": 25,\n      \"semantic_search\": \"<semantic_search>\"\n    }\n  }\n}'"
            },
            "concept": "podcasts",
            "cohorts": [
              "podcasts"
            ],
            "matchedBy": [
              "semantic"
            ],
            "matchedUrls": []
          }
        ]
      }
      ```
    </Accordion>
  </Step>

  <Step title="Check what it needs">
    Open the top-ranked tool to see what inputs it accepts, what data it returns, and how to call it. These details are already included in the search result.

    <CodeGroup>
      ```javascript JavaScript theme={null}
      const tool = result.tools[0];

      console.log("Accepted inputs:", tool.options);
      console.log("Response structure:", tool.response);
      console.log("JavaScript request example:", tool.examples.javascript);
      ```

      ```python Python theme={null}
      tool = result.tools[0]

      print("Accepted inputs:", tool.options)
      print("Response structure:", tool.response)
      print("Python request example:", tool.examples["python"])
      ```
    </CodeGroup>

    `options` describes the inputs you can send. `response` describes the output structure, rather than actual provider data. `examples.javascript` is a request example you can adapt and run in the next step.

    `semantic_search` is an input specific to Particle's episode-search tool. It searches podcast content; Alexandria's earlier search finds which tool to use. Particle also accepts `keyword_search` for exact wording.
  </Step>

  <Step title="Get the data">
    Use `scrape` to call the tool with the inputs from its contract. Supply the provider and capability under `exchange`; no page URL is needed.

    <CodeGroup>
      ```javascript JavaScript theme={null}
      const episodes = await firecrawl.scrape({
        exchange: {
          provider: "particle",
          capability: "podcasts/episodes/search",
          options: {
            semantic_search: "AI agents",
            limit: 2,
          },
        },
      });

      console.log(episodes.exchange[0].data);
      ```

      ```python Python theme={null}
      episodes = firecrawl.scrape(
          exchange={
              "provider": "particle",
              "capability": "podcasts/episodes/search",
              "options": {
                  "semantic_search": "AI agents",
                  "limit": 2,
              },
          },
      )

      print(episodes.exchange[0].data)
      ```

      ```bash cURL theme={null}
      REQUEST_ID="$(uuidgen)"

      curl https://api.firecrawl.dev/v2/scrape \
        -H "Authorization: Bearer $FIRECRAWL_API_KEY" \
        -H "Content-Type: application/json" \
        -H "x-request-id: $REQUEST_ID" \
        -d '{
        "exchange": {
          "provider": "particle",
          "capability": "podcasts/episodes/search",
          "options": {
            "semantic_search": "AI agents",
            "limit": 2
          }
        }
      }'
      ```
    </CodeGroup>

    <Accordion title="Response: matching episodes">
      ```json SDK result (shortened, illustrative) theme={null}
      {
        "exchange": [
          {
            "provider": "particle",
            "capability": "podcasts/episodes/search",
            "creditsCost": 15,
            "data": {
              "data": [
                {
                  "id": "example-episode-001",
                  "title": "Building reliable AI agents",
                  "podcast": {
                    "id": "example-show-001",
                    "title": "The Agent Workshop"
                  },
                  "published_at": "2026-09-01",
                  "excerpt": "We let the agent discover the tools it needs, then read their contracts."
                }
              ]
            }
          }
        ],
        "creditsCost": 15
      }
      ```

      `episodes.exchange[0]` contains the first tool result and its execution price. Its `data` follows Particle's response contract. Raw `/v2/scrape` responses wrap this SDK result in `data`.
    </Accordion>
  </Step>
</Steps>

Tool discovery is free. Web search, scraping, and tool execution have their own charges; each tool shows its execution price. A batch can contain per-tool errors, so check each result before using its `data`.

The SDK generates an execution request ID and keeps it across automatic retries. To retry later, reuse `requestId` in JavaScript or `request_id` in Python with the identical request. For cURL, keep the same `$REQUEST_ID`.

<Card title="Explore Alexandria" icon="books" href="https://www.firecrawl.dev/app/alexandria">
  Browse tools and copy a request in the dashboard.
</Card>

## Use Alexandria from the CLI or MCP

Agents can discover tools, read their contracts, and retrieve data from the terminal or an MCP client. The examples below describe the preview integrations and require the matching release and team access.

<Tabs>
  <Tab title="Firecrawl CLI">
    ```bash theme={null}
    firecrawl search "podcast conversations about AI agents" --sources web,alexandria --limit 2

    firecrawl find-tools --providers particle --capabilities podcasts/episodes/search --expand options,response

    firecrawl find-tools https://podcasts.apple.com --limit 2
    ```

    Use `search` for semantic discovery and `find-tools` for contextual lookup or progressive disclosure. Pass a returned `next` request to `firecrawl find-tools --request '<json>'` to continue. Execution uses `firecrawl scrape --exchange <provider/capability> --options '<json>'`.
  </Tab>

  <Tab title="MCP">
    Connect the [Firecrawl MCP server](/mcp-server) to your agent. With Alexandria support enabled, the agent can discover relevant tools, inspect their inputs and price, and retrieve data through the same workflow.

    Try asking: “Find Particle's podcast tools, inspect the episode-search inputs and price, and show me the request before executing it.”

    `firecrawl_search` returns contracts in `data.tools`. `firecrawl_find_tools` provides the free contextual lookup: start from a URL or provider, then follow the returned requests to reveal groups, tools, and contracts.
  </Tab>
</Tabs>

## Find Tools

**Find Tools** is a free discovery tool that lets agents explore the catalogue step by step. Start with a page URL or a provider, then follow the returned `next` requests:

**Particle → Podcasts → Episode search → inputs and response**

Each step reveals just the detail the agent needs. Once it has the contract, it can call the selected tool through `scrape`. Find Tools costs 0 credits; executing the selected tool uses that tool's published price.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const catalogue = await firecrawl.findTools({
    providers: ["particle"],
    limit: 2,
  });

  // Each next request reveals another part of the catalogue.
  const next = catalogue.items[0]?.next;
  if (next) {
    const details = await firecrawl.scrape({ exchange: next });
    console.log(details.exchange[0].data);
  }
  ```

  ```python Python theme={null}
  catalogue = firecrawl.find_tools(providers=["particle"], limit=2)

  if catalogue.items and catalogue.items[0].get("next"):
      details = firecrawl.scrape(exchange=catalogue.items[0]["next"])
      print(details.exchange[0].data)
  ```
</CodeGroup>

An item's `next` reveals more detail. The catalogue's top-level `next` advances to the next page while preserving the filters. Supply `categories`, `groups`, or `capabilities` when you already know what to look for; `level` is inferred from those selectors.

For contextual matches beside web results, add `skills: true` to a Search request. Semantic and domain matches share the `tools` array; `matchedBy` and `matchedUrls` explain why each tool appeared. Search still requires a query. After scraping a page, pass its URL to Find Tools to discover relevant tools.
