CrawlForge MCP
Basic ToolResult Handles1 credit

read_result

Read back a result that was too large to return inline. When a tool's JSON exceeds max_inline_chars, its response carries a preview, a result_handle and truncated: true; pass that handle here to slice the text, search it, page through it by line or read one JSON path — without fetching the page again or paying the tool's price a second time.

Use Cases

Find the Section You Need

Run a search for a heading or phrase, then slice from the match offset — two calls that read one section of a long page instead of the whole result.

Page Through Long Text

lines returns a window of lines with has_more, so a large markdown document is read in windows your context can hold.

Pull One Field From a Large JSON

json_path reads a single path from a stored crawl_deep, batch_scrape or deep_research result, or from the parsed body of a JSON fetch_url.

Never Re-Fetch

The stored result is the one you already paid for. Reading it costs 1 credit per call; re-running the tool costs its full price and hits the site again.

Endpoint

POST/api/v1/tools/read_result
Auth Required
1 req/s on Free plan
1 credit

Parameters

handle comes from a response with truncated: true — this tool never fetches a page, it only reads a result you already have. A handle expires 1 hour after the result was stored.
NameTypeRequiredDefaultDescription
handle
stringRequired-
The `result_handle` from a truncated response (`res_` followed by a UUID). Readable only by the account that created it, for 1 hour.
Example: res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c
operation
stringRequired-
`slice` returns a character range of the text view; `search` finds a case-insensitive literal substring (never a regex) and returns each match with context; `lines` returns a window of lines; `json_path` reads one path from the stored JSON.
Example: search
offset
numberOptional0
`slice`: first character to return. `lines`: index of the first line to return.
Example: 18240
length
numberOptional10000
`slice`: number of characters to return (default 10,000). `lines`: number of lines to return (default 200, at most 5,000).
Example: 4000
query
stringOptional-
`search` only, and required there: the literal text to look for, matched case-insensitively.
Example: rate limits
max_matches
numberOptional20
`search`: maximum matches to return, 1-100. `truncated: true` in the response means more matched than were returned.
Example: 5
path
stringOptional-
`json_path` only, and required there: dotted keys and array indexes, in dot or bracket form (`pages.0.url` or `pages[0].url`); no wildcards, filters or slices. Read from the stored result object — or from the parsed body when the stored text is JSON, such as a `fetch_url` body.
Example: pages.0.url
max_inline_chars
numberOptional40000
Largest amount of text to return inline, 1,000-10,000,000 characters; every operation caps the text it returns here. For `json_path`, a larger `value` comes back as `value: null` with a `preview`, `truncated: true` and a warning to narrow the path.
Example: 40000

Operations

Every response carries handle, tool, operation, view, view_path, total_chars and expires_at, then the fields of the operation you asked for.

slice
A character range of the text view: offset, length, text and has_more. Defaults to the first 10,000 characters.
search
Case-insensitive literal substring match, never a regex: query, matches (each with offset, length, context_offset and context — 200 characters either side), total_matches and truncated.
lines
A window of lines: first_line, line_count, total_lines, char_offset, lines and has_more. offset is the first line index, length the line count (default 200, at most 5,000).
json_path
One path from the stored result object: path, value and value_chars. Over max_inline_chars the value is replaced by value: null, a preview and truncated: true, with a warning to narrow the path.

Where a stored result lives

On the REST API a stored result is a per-account value kept for 1 hour and readable only by the account that created it. On the self-hosted MCP server the store is on your own machine under ~/.crawlforge/results/ (1-hour TTL, 200 MB LRU) and nothing is uploaded. Error responses are never stored, and batch_scrape jobs share the same store.

Request Examples

cURL

terminalBash
# 1. A scrape whose JSON exceeded max_inline_chars (default 40,000) came back
#    with a preview instead of the markdown:
#      "truncated": true,
#      "result_handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
#      "total_chars": 182406

# 2. Search the stored result for the section you need (1 credit)
curl -X POST https://crawlforge.dev/api/v1/tools/read_result \
  -H "X-API-Key: cf_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
    "operation": "search",
    "query": "rate limits",
    "max_matches": 5
  }'

# 3. Read the section at the first match offset (1 credit) — no second fetch
curl -X POST https://crawlforge.dev/api/v1/tools/read_result \
  -H "X-API-Key: cf_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
    "operation": "slice",
    "offset": 18240,
    "length": 4000
  }'

TypeScript

readResult.tsTypescript
// npm install crawlforge-sdk
import { CrawlForge } from 'crawlforge-sdk';

const client = new CrawlForge({ apiKey: process.env.CRAWLFORGE_API_KEY });

// 1. Scrape a long page. Over max_inline_chars (default 40,000 characters of
//    JSON) the markdown is replaced by a preview and a result_handle.
const page = await client.scrape({
  url: 'https://example.com/docs/api',
  formats: ['markdown'],
});

// page.data is untyped in crawlforge-sdk 0.1 — a truncated result carries the
// handle fields shown in the cURL tab instead of the markdown.
const scraped = page.data as
  | { truncated: true; result_handle: string; total_chars: number }
  | { truncated?: false; formats: { markdown: string } };

if (scraped.truncated) {
  const { result_handle, total_chars } = scraped;
  console.log('Stored ' + total_chars + ' characters as ' + result_handle);

  // 2. Search the stored markdown for the section you need (1 credit).
  const found = await client.readResult({
    handle: result_handle,
    operation: 'search',
    query: 'rate limits',
    max_matches: 5,
  });

  // result.data is untyped in crawlforge-sdk 0.1 — its shape is the Response Example below.
  const { matches } = found.data as { matches: { offset: number }[] };

  // 3. Slice from the first match offset (1 credit) — no second fetch.
  const [first] = matches;
  if (first) {
    const section = await client.readResult({
      handle: result_handle,
      operation: 'slice',
      offset: first.offset,
      length: 4000,
    });
    const { text, has_more } = section.data as { text: string; has_more: boolean };
    console.log(text);
    console.log('More after this slice:', has_more);
  }
} else {
  console.log(scraped.formats.markdown); // small enough to arrive inline
}

Python

read_result.pyPython
# pip install crawlforge
from crawlforge import CrawlForge

client = CrawlForge()  # reads CRAWLFORGE_API_KEY

# 1. Scrape a long page. Over max_inline_chars (default 40,000 characters of
#    JSON) the markdown is replaced by a preview and a result_handle.
page = client.scrape(url='https://example.com/docs/api', formats=['markdown'])

# result.data is a plain dict — its shape is the Response Example below.
if page.data.get('truncated'):
    handle = page.data['result_handle']
    print(f"Stored {page.data['total_chars']} characters as {handle}")

    # 2. Search the stored markdown for the section you need (1 credit).
    found = client.read_result(
        handle=handle,
        operation='search',
        query='rate limits',
        max_matches=5,
    )

    # 3. Slice from the first match offset (1 credit) - no second fetch.
    if found.data['matches']:
        first = found.data['matches'][0]
        section = client.read_result(
            handle=handle,
            operation='slice',
            offset=first['offset'],
            length=4000,
        )
        print(section.data['text'])
        print('More after this slice:', section.data['has_more'])
else:
    print(page.data['formats']['markdown'])  # small enough to arrive inline

Response Example

200 OK42ms
{
"success": true,
"data": {
"handle": "res_9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
"tool": "scrape",
"operation": "search",
"view": "text",
"view_path": "markdown",
"total_chars": 182406,
"expires_at": "2026-09-05T15:42:10.000Z",
"query": "rate limits",
"matches": [
{
"offset": 18240,
"length": 11,
"context_offset": 18040,
"context": "…request. Every plan is metered per API key.\n\n## Rate limits\n\nEach key may make one request per second on the Free plan…"
},
{
"offset": 61377,
"length": 11,
"context_offset": 61177,
"context": "…returns 429 with a Retry-After header; see the rate limits table above for the per-plan ceilings…"
}
],
"total_matches": 2,
"truncated": false
},
"credits_used": 1,
"credits_remaining": 998,
"processing_time": 42
}
Field Descriptions
data.handleThe handle you passed, echoed back
data.toolThe tool that produced the stored result
data.view`text` when the operation read the text view (the markdown for scrape, the body for fetch_url); `json` for `json_path`
data.view_pathWhich field of the original result the text view is
data.total_charsSize of the stored text view, in characters
data.expires_atWhen the stored result is deleted — 1 hour after it was stored
data.matchesOne entry per match: `offset` and `length` index the text view; `context` carries up to 200 characters either side, starting at `context_offset`
data.total_matchesHow many matches exist in total; `truncated` is true when more matched than `max_matches` allowed
credits_usedCredits deducted for this request (1 per read)
credits_remainingYour remaining credit balance

Error Handling

Invalid Input (400 Bad Request)

handle is missing, operation is not one of slice, search, lines or json_path, search was sent without query, json_path without path, or max_matches falls outside 1-100 (VALIDATION_ERROR).

Path Not Found (400 Bad Request)

json_path could not resolve path (PATH_NOT_FOUND); the error names the keys available where it stopped. Credits are not deducted.

Result Not Found (404 Not Found)

Unknown or expired result handle (RESULT_NOT_FOUND) — results are kept 1 hour and are readable only by the account that created them. Credits are not deducted.

Storage Unavailable (503 Service Unavailable)

The result store could not be reached (STORAGE_UNAVAILABLE). Retry shortly; nothing is charged for a read that did not complete.

Insufficient Credits (402 Payment Required)

Your account doesn't have enough credits. Purchase more credits or upgrade your plan.

Rate Limit Exceeded (429 Too Many Requests)

You've exceeded your plan's rate limit. Wait a moment or upgrade your plan for higher limits.

Pro Tip: search, then slice at the match offset, reads one section for 2 credits. Walking the whole result in 10,000-character slices costs 1 credit per slice, and re-running the tool costs its full price plus another fetch of the site.

Credit Cost

1 credit
1 credit per request
Each read_result request costs 1 credit, whatever the operation and however much it returns. The scraping itself was already billed by the tool that stored the result.

Free Plan: 1,000 one-time credits = 1,000 requests

Hobby Plan: 5,000 credits/month = 5,000 requests ($19/mo)

Professional Plan: 50,000 credits/month = 50,000 requests ($99/mo)

Business Plan: 250,000 credits/month = 250,000 requests ($399/mo)

Related Tools

scrape
The usual source of a result_handle — max_inline_chars sets how much arrives inline (2 credits)
crawl_deep
Multi-page crawls are the largest results; json_path reads one page from the stored crawl (4 credits)
fetch_url
A JSON body over the inline limit is stored too; json_path reads its parsed body (1 credit)
get_batch_results
Pages through a batch_scrape job; the batch's stored results share the same 1-hour store (1 credit)
Ready to try read_result? Sign up for free and get 1,000 credits to start building.

Footer

CrawlForge MCP

Enterprise web scraping for AI Agents. 30 specialized MCP tools designed for modern developers building intelligent systems.

Product

  • Features
  • Playground
  • Pricing
  • Use Cases
  • Integrations
  • Alternatives
  • Changelog

Resources

  • Getting Started
  • API Reference
  • Templates
  • Guides
  • Blog
  • Glossary
  • FAQ
  • Sitemap

Developers

  • MCP Protocol
  • Claude Desktop
  • Cursor IDE
  • LangChain
  • LlamaIndex

Company

  • About
  • Contact
  • Privacy
  • Terms
  • Acceptable Use
  • Security
  • Cookies

Stay updated

Get the latest updates on new tools and features.

Built with Next.js and MCP protocol

© 2025-2026 CrawlForge. All rights reserved.