Guide · AWS Textract

AWS Textract in MCP Servers — document intelligence, text detection, form parsing

AWS Textract is a fully managed document intelligence service that extracts printed text, handwriting, forms, tables, and structured data from images and PDFs without requiring custom ML models. For MCP servers, Textract unlocks a class of document-understanding tools that let AI agents process invoices, contracts, ID documents, medical forms, and scanned archives — returning structured data rather than raw pixel data. This guide covers Textract's four primary APIs (DetectDocumentText, AnalyzeDocument, AnalyzeExpense, AnalyzeID), the Block model that underpins all responses, sync vs async job patterns, IAM permissions, and the most common integration mistakes that cause silent failures or empty responses.

TL;DR

Use DetectDocumentText for raw text extraction (LINE + WORD blocks). Use AnalyzeDocument with FeatureTypes for forms (KEY_VALUE_SET blocks), tables (TABLE + CELL blocks), targeted Q&A (QUERY + QUERY_RESULT blocks), and signature detection. Use AnalyzeExpense for invoices and receipts (SummaryFields + LineItemGroups). Use AnalyzeID for identity documents. Sync APIs accept inline bytes (max 10 MB base64) or S3 references; async APIs require S3. All responses share the same Block-list model navigated via Relationships arrays.

Textract's document model: everything is a Block

Every Textract response — regardless of API — returns a flat array of Block objects. Each Block has a BlockType string, a Confidence score (0–100), a Geometry object with bounding box and polygon coordinates, and a Relationships array that links it to child or value blocks. Navigation is entirely through these relationships — there is no nested tree structure returned directly.

The most important BlockTypes are:

To navigate, build an id-to-block index first, then walk Relationships:

import {
  TextractClient,
  AnalyzeDocumentCommand,
  type Block,
} from '@aws-sdk/client-textract';

const client = new TextractClient({ region: 'us-east-1' });

function indexBlocks(blocks: Block[]): Map<string, Block> {
  const map = new Map<string, Block>();
  for (const block of blocks) {
    if (block.Id) map.set(block.Id, block);
  }
  return map;
}

function getChildren(block: Block, index: Map<string, Block>): Block[] {
  const children: Block[] = [];
  for (const rel of block.Relationships ?? []) {
    if (rel.Type === 'CHILD') {
      for (const id of rel.Ids ?? []) {
        const child = index.get(id);
        if (child) children.push(child);
      }
    }
  }
  return children;
}

function blockText(block: Block, index: Map<string, Block>): string {
  if (block.BlockType === 'WORD') return block.Text ?? '';
  return getChildren(block, index)
    .filter(c => c.BlockType === 'WORD')
    .map(c => c.Text ?? '')
    .join(' ');
}

DetectDocumentText — raw text extraction

DetectDocumentText is the simplest Textract API. It returns PAGE, LINE, and WORD blocks with confidence scores. No feature types are required. Use it when you only need the plain text content of a document and do not need structured form fields, tables, or targeted queries.

import {
  TextractClient,
  DetectDocumentTextCommand,
} from '@aws-sdk/client-textract';
import { readFileSync } from 'fs';

const client = new TextractClient({ region: 'us-east-1' });

// Option 1: inline bytes (sync, max 10 MB base64-encoded)
async function detectTextInline(filePath: string): Promise<string> {
  const bytes = readFileSync(filePath);
  const resp = await client.send(new DetectDocumentTextCommand({
    Document: { Bytes: bytes },
  }));

  const lines = (resp.Blocks ?? [])
    .filter(b => b.BlockType === 'LINE')
    .map(b => b.Text ?? '');
  return lines.join('\n');
}

// Option 2: S3 reference (sync or as part of async job)
async function detectTextFromS3(bucket: string, key: string): Promise<string> {
  const resp = await client.send(new DetectDocumentTextCommand({
    Document: { S3Object: { Bucket: bucket, Name: key } },
  }));

  const lines = (resp.Blocks ?? [])
    .filter(b => b.BlockType === 'LINE')
    .map(b => b.Text ?? '');
  return lines.join('\n');
}

Sync API limitations: A single DetectDocumentText call can only process one page. For multi-page PDFs, you must use the async StartDocumentTextDetection API, which accepts S3 input and processes up to 3,000 pages per job. The sync APIs (both DetectDocumentText and AnalyzeDocument) accept JPEG, PNG, TIFF, and PDF — but PDFs are limited to one page in the sync path.

AnalyzeDocument — forms, tables, queries, signatures

AnalyzeDocument extends text detection with structured feature extraction. You request up to three FeatureTypes in a single call: FORMS, TABLES, QUERIES, and SIGNATURES. Each feature type adds its own block types to the response alongside the standard LINE/WORD blocks.

import {
  TextractClient,
  AnalyzeDocumentCommand,
  FeatureType,
} from '@aws-sdk/client-textract';

const client = new TextractClient({ region: 'us-east-1' });

async function analyzeDocument(bucket: string, key: string) {
  const resp = await client.send(new AnalyzeDocumentCommand({
    Document: { S3Object: { Bucket: bucket, Name: key } },
    FeatureTypes: [
      FeatureType.FORMS,
      FeatureType.TABLES,
      FeatureType.SIGNATURES,
    ],
  }));
  return resp.Blocks ?? [];
}

FORMS feature: Adds KEY_VALUE_SET blocks. Each KEY block has a CHILD relationship to WORD blocks (the key text) and a VALUE relationship to the corresponding VALUE block. VALUE blocks may contain WORD children (text value) or SELECTION_ELEMENT children (checkbox state). Form extraction works best on structured forms with clear visual separation between labels and input areas.

type FormField = { key: string; value: string };

function extractFormFields(
  blocks: Block[],
  index: Map<string, Block>,
): FormField[] {
  const fields: FormField[] = [];

  for (const block of blocks) {
    if (block.BlockType !== 'KEY_VALUE_SET') continue;
    const entityTypes = block.EntityTypes ?? [];
    if (!entityTypes.includes('KEY')) continue;

    // Find VALUE block via VALUE relationship
    let valueBlock: Block | undefined;
    for (const rel of block.Relationships ?? []) {
      if (rel.Type === 'VALUE') {
        valueBlock = index.get(rel.Ids?.[0] ?? '');
        break;
      }
    }

    const key = blockText(block, index);
    let value = '';
    if (valueBlock) {
      const selChild = getChildren(valueBlock, index)
        .find(c => c.BlockType === 'SELECTION_ELEMENT');
      value = selChild
        ? selChild.SelectionStatus ?? ''
        : blockText(valueBlock, index);
    }

    fields.push({ key, value });
  }

  return fields;
}

AnalyzeExpense — invoice and receipt parsing

AnalyzeExpense is a specialized API for financial documents. Unlike the generic KEY_VALUE_SET approach, it returns semantically labeled SummaryFields (vendor name, date, total, tax, subtotal, payment terms, PO number) and structured LineItemGroups containing individual line items with quantity, unit price, item description, and amount. This is the right API for any MCP tool that processes invoices, receipts, or expense reports.

import {
  TextractClient,
  AnalyzeExpenseCommand,
} from '@aws-sdk/client-textract';

const client = new TextractClient({ region: 'us-east-1' });

interface ParsedExpense {
  summary: Record<string, string>;
  lineItems: Array<Record<string, string>>;
}

async function parseExpenseDocument(
  bucket: string,
  key: string,
): Promise<ParsedExpense> {
  const resp = await client.send(new AnalyzeExpenseCommand({
    Document: { S3Object: { Bucket: bucket, Name: key } },
  }));

  const expenseDoc = resp.ExpenseDocuments?.[0];
  if (!expenseDoc) return { summary: {}, lineItems: [] };

  // Extract summary fields
  const summary: Record<string, string> = {};
  for (const field of expenseDoc.SummaryFields ?? []) {
    const fieldType = field.Type?.Text ?? 'UNKNOWN';
    const value = field.ValueDetection?.Text ?? '';
    summary[fieldType] = value;
  }

  // Extract line items
  const lineItems: Array<Record<string, string>> = [];
  for (const group of expenseDoc.LineItemGroups ?? []) {
    for (const lineItem of group.LineItems ?? []) {
      const item: Record<string, string> = {};
      for (const field of lineItem.LineItemExpenseFields ?? []) {
        const fieldType = field.Type?.Text ?? 'UNKNOWN';
        const value = field.ValueDetection?.Text ?? '';
        item[fieldType] = value;
      }
      lineItems.push(item);
    }
  }

  return { summary, lineItems };
}

Common SummaryField type strings: VENDOR_NAME, VENDOR_ADDRESS, INVOICE_RECEIPT_DATE, INVOICE_RECEIPT_ID, TOTAL, TAX, SUBTOTAL, PAYMENT_TERMS, DUE_DATE, PO_NUMBER, RECEIVER_NAME. Common LineItemExpenseField type strings: ITEM, QUANTITY, UNIT_PRICE, EXPENSE_ROW, PRICE.

AnalyzeID — identity document extraction

AnalyzeID extracts key-value pairs from US driver's licenses and passports. It returns IdentityDocuments, each containing IdentityDocumentFields with a Type (field name like FIRST_NAME, LAST_NAME, DATE_OF_BIRTH, EXPIRATION_DATE, ID_TYPE) and a ValueDetection. AnalyzeID does not require a FeatureTypes parameter and does not support S3 references in the same way — it uses the same Document parameter accepting either Bytes or S3Object.

import {
  TextractClient,
  AnalyzeIDCommand,
} from '@aws-sdk/client-textract';

const client = new TextractClient({ region: 'us-east-1' });

async function parseIdentityDocument(
  bucket: string,
  key: string,
): Promise<Record<string, string>> {
  const resp = await client.send(new AnalyzeIDCommand({
    DocumentPages: [{ S3Object: { Bucket: bucket, Name: key } }],
  }));

  const result: Record<string, string> = {};
  const idDoc = resp.IdentityDocuments?.[0];
  for (const field of idDoc?.IdentityDocumentFields ?? []) {
    const key = field.Type?.Text ?? 'UNKNOWN';
    const value = field.ValueDetection?.Text ?? '';
    result[key] = value;
  }
  return result;
}

Note: AnalyzeID uses DocumentPages (an array) instead of Document (singular), which is a common source of TypeScript type errors when switching between Textract APIs in the same file.

IAM permissions for Textract MCP tools

Textract requires an IAM policy on the calling principal. For sync APIs, the minimum permission set is:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "textract:DetectDocumentText",
        "textract:AnalyzeDocument",
        "textract:AnalyzeExpense",
        "textract:AnalyzeID"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::your-document-bucket/*"
    }
  ]
}

For async jobs, add textract:StartDocumentAnalysis, textract:GetDocumentAnalysis, textract:StartDocumentTextDetection, and textract:GetDocumentTextDetection. If you use SNS notifications for job completion, also add sns:Publish on the SNS topic and ensure the Textract service principal (textract.amazonaws.com) has the sts:AssumeRole permission in the trust policy of the IAM role passed to NotificationChannel.RoleArn.

Textract does not support VPC endpoints for data-plane APIs — requests must go over the public HTTPS endpoint. Ensure that Lambda functions or ECS tasks running Textract MCP tools have internet access (NAT Gateway or VPC endpoint for S3 is sufficient for the S3 GetObject, but the Textract API call itself must reach the public endpoint).

Sync vs async: when to use each

Scenario API path Max pages Input source
Single-page JPEG or PNG DetectDocumentText / AnalyzeDocument 1 Bytes or S3
Single-page PDF DetectDocumentText / AnalyzeDocument 1 Bytes or S3
Multi-page PDF (>1 page) StartDocumentAnalysis / GetDocumentAnalysis 3,000 S3 only
Invoice or receipt (1–2 pages) AnalyzeExpense 10 (async: 500) Bytes or S3
ID document (1 page) AnalyzeID 2 (front + back) Bytes or S3
Large document archive StartDocumentTextDetection / GetDocumentTextDetection 3,000 S3 only

The async job submission APIs return a JobId immediately. You then poll GetDocumentAnalysis or use an SNS notification to know when the job completes. Job status values are IN_PROGRESS, SUCCEEDED, FAILED, and PARTIAL_SUCCESS. See the Textract async document analysis guide for the full SNS + polling MCP tool pattern.

MCP tool design patterns for Textract

Textract integrates naturally into MCP server tools. The most common patterns are:

Pattern 1 — Extract-and-summarize: Tool receives an S3 URI, calls DetectDocumentText, assembles the LINE text into a string, and passes it to a language model for summarization or answering questions about the content. Simple and effective for plain text documents.

Pattern 2 — Structured field extraction: Tool receives an S3 URI and a list of field names, calls AnalyzeDocument with FORMS or QUERIES, and returns a JSON object with the extracted field values. The QUERIES feature is more reliable for targeted extraction than FORMS when form layout is irregular. See the Textract Queries API guide for implementation details.

Pattern 3 — Invoice processing pipeline: Tool receives an S3 URI, calls AnalyzeExpense, and returns a normalized invoice object (vendor, date, total, line items). Downstream tools can post to accounting systems or trigger approval workflows. See the Textract AnalyzeExpense guide for full field coverage.

Pattern 4 — Table extraction to CSV: Tool calls AnalyzeDocument with TABLES, navigates the TABLE → CELL → WORD block graph, and serializes cells to a two-dimensional array or CSV string. Useful for extracting data tables from PDF reports. See the Textract table extraction guide for CELL traversal and merged-cell handling.

Pricing and rate limits

Textract pricing is per page processed. Approximate rates (US East 1): DetectDocumentText $0.0015/page, AnalyzeDocument FORMS/TABLES $0.015/page, AnalyzeDocument QUERIES $0.01/page/query-up-to-15-queries, AnalyzeExpense $0.01/page, AnalyzeID $0.01/page. Prices drop with volume tiers.

Default service quotas: 5 concurrent async jobs per account per region, 250 synchronous transactions per second (DetectDocumentText), 100 TPS (AnalyzeDocument). Request quota increases via the Service Quotas console before building high-throughput MCP tools. For batch workloads, the async API is more quota-efficient since jobs are not counted against the synchronous TPS limit once submitted.

A common MCP server mistake is calling AnalyzeDocument with all four feature types on every request. If you only need tables, pass only FeatureTypes: [FeatureType.TABLES] — you pay per feature type used, and unnecessary feature types add latency without benefit.