Guide · AWS Comprehend · PII Detection
AWS Comprehend PII Detection in MCP Servers — ContainsPiiEntities, Redaction, Async Jobs
MCP servers that process user-provided text — support tickets, document uploads, natural-language queries — must guard against forwarding personally identifiable information (PII) to downstream systems, logs, or AI models without consent. AWS Comprehend provides two complementary PII APIs: ContainsPiiEntities classifies a document as containing or not containing PII (fast binary decision for pre-screening), and DetectPiiEntities returns exact character spans for each PII entity found. For batch workloads, the async StartPiiEntitiesDetectionJob processes large document sets in S3 with two output modes: ONLY_REDACTION (returns a copy of the document with PII masked) and ONLY_OFFSETS (returns span coordinates without modifying the document). The redaction mode further supports two masking strategies: REDACT (replaces each span with * characters) and REPLACE_WITH_PII_ENTITY_TYPE (replaces each span with the entity type label like [NAME]). This guide covers when to use each API, how to wire PII detection into an MCP tool handler as a pre-processing gate, and how to configure the async redaction job for compliance pipelines.
TL;DR
Use ContainsPiiEntitiesCommand as a fast pre-screen (returns Labels[] with entity type + score, no spans). Use DetectPiiEntitiesCommand to get exact character spans when you need to extract, redact, or audit specific PII instances. Both are synchronous (≤5,000 bytes). For large document batches, use StartPiiEntitiesDetectionJobCommand with Mode: 'ONLY_REDACTION' and RedactionConfig.MaskMode: 'REPLACE_WITH_PII_ENTITY_TYPE' to produce compliance-safe document copies. PII detection does not require a custom model — it is a built-in Comprehend capability billed at $0.0001 per 100 characters.
PII entity types Comprehend detects
Comprehend's PII recognizer identifies 27 entity types for English, and a subset for other supported languages. Key types relevant to MCP server workloads:
| Type | Examples detected |
|---|---|
| NAME | Personal names, first + last combinations |
| Email addresses in standard formats | |
| PHONE | Phone numbers (US and international formats) |
| ADDRESS | Street addresses, postal codes |
| SSN | US Social Security Numbers (NNN-NN-NNNN) |
| CREDIT_DEBIT_NUMBER | Payment card numbers (all major networks) |
| BANK_ACCOUNT_NUMBER | Bank account numbers |
| IP_ADDRESS | IPv4 and IPv6 addresses |
| URL | Web URLs that may identify individuals |
| USERNAME | Usernames in @mention format and similar patterns |
| PASSWORD | Credential-like strings following "password:" patterns |
| AWS_ACCESS_KEY | AWS access key IDs (AKIA... prefix) |
| AWS_SECRET_KEY | 40-character AWS secret access keys |
| DATE_TIME | Specific dates that may identify individuals |
| PASSPORT_NUMBER | US and international passport numbers |
| DRIVER_ID | US driver's license numbers |
AWS_ACCESS_KEY and AWS_SECRET_KEY detection is particularly valuable for MCP servers: users sometimes paste AWS credentials into tool arguments (a common accidental credential leak). Comprehend will flag these before they hit logs or downstream APIs.
ContainsPiiEntities: fast pre-screening
import {
ComprehendClient,
ContainsPiiEntitiesCommand,
LanguageCode,
} from '@aws-sdk/client-comprehend';
const client = new ComprehendClient({ region: 'us-east-1' });
interface PiiScreenResult {
containsPii: boolean;
detectedTypes: Array<{ type: string; score: number }>;
}
async function screenForPii(text: string): Promise<PiiScreenResult> {
const { Labels } = await client.send(new ContainsPiiEntitiesCommand({
Text: text,
LanguageCode: LanguageCode.EN,
}));
const detected = (Labels ?? []).filter(l => (l.Score ?? 0) >= 0.80);
return {
containsPii: detected.length > 0,
detectedTypes: detected.map(l => ({ type: l.Name ?? '', score: l.Score ?? 0 })),
};
}
ContainsPiiEntities returns Labels[] with entity type names and confidence scores — it does NOT return character offsets. It answers the question "does this text contain PII?" not "where exactly is the PII?" Use it as a pre-screen gate when you only need the binary decision (e.g., before forwarding tool input to a third-party API). If you need to log or redact the specific spans, call DetectPiiEntities instead.
The same 5,000 UTF-8 byte limit applies. For longer text, split on paragraph boundaries and run ContainsPiiEntities on each chunk — if any chunk returns a positive, treat the whole document as containing PII.
DetectPiiEntities: span-level extraction
import {
DetectPiiEntitiesCommand,
type PiiEntity,
} from '@aws-sdk/client-comprehend';
interface RedactedResult {
redactedText: string;
detectedEntities: Array<{ type: string; score: number; original: string }>;
}
async function detectAndRedact(text: string, threshold = 0.80): Promise<RedactedResult> {
const { Entities } = await client.send(new DetectPiiEntitiesCommand({
Text: text,
LanguageCode: LanguageCode.EN,
}));
const confident = (Entities ?? [])
.filter((e): e is Required<PiiEntity> =>
(e.Score ?? 0) >= threshold &&
e.BeginOffset !== undefined &&
e.EndOffset !== undefined
)
.sort((a, b) => a.BeginOffset - b.BeginOffset);
// Redact spans from right to left to avoid offset drift
let redacted = text;
const detected: RedactedResult['detectedEntities'] = [];
for (const entity of [...confident].reverse()) {
const original = text.slice(entity.BeginOffset, entity.EndOffset);
const mask = `[${entity.Type}]`;
redacted = redacted.slice(0, entity.BeginOffset) + mask + redacted.slice(entity.EndOffset);
detected.unshift({ type: entity.Type, score: entity.Score, original });
}
return { redactedText: redacted, detectedEntities: detected };
}
When building redacted text by replacing spans with masks, always process spans in reverse offset order (highest start offset first) to avoid corrupting the offset positions of earlier spans. After replacing span at offset 200–210, the string length changes — offsets 0–199 remain valid, but any forward replacement would now reference wrong positions. Sorting descending by BeginOffset and replacing right-to-left eliminates this bug.
Wiring PII detection into an MCP tool handler
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({ name: 'document-processor', version: '1.0.0' });
// Sensitive types that block forwarding to external APIs
const BLOCKING_TYPES = new Set([
'SSN', 'CREDIT_DEBIT_NUMBER', 'BANK_ACCOUNT_NUMBER',
'AWS_ACCESS_KEY', 'AWS_SECRET_KEY', 'PASSWORD', 'PASSPORT_NUMBER',
]);
server.tool('process_user_document', {
document_text: z.string().max(4000),
}, async ({ document_text }) => {
// Step 1: PII pre-screen (fast — returns in <200ms typically)
const screen = await screenForPii(document_text);
if (screen.containsPii) {
const blockingTypes = screen.detectedTypes
.filter(t => BLOCKING_TYPES.has(t.type))
.map(t => t.type);
if (blockingTypes.length > 0) {
// Hard block — do not process documents with credentials or financial PII
return {
content: [{
type: 'text',
text: JSON.stringify({
error: 'Document contains sensitive PII that cannot be processed',
detected_types: blockingTypes,
action: 'Please remove sensitive information before submitting',
}),
}],
};
}
// Soft block — redact lower-sensitivity PII and continue
const { redactedText, detectedEntities } = await detectAndRedact(document_text);
// Process the redacted version and note what was masked
return {
content: [{
type: 'text',
text: JSON.stringify({
processed_text: redactedText,
pii_redacted: detectedEntities.map(e => ({ type: e.type, score: e.score })),
note: 'PII was detected and redacted before processing',
}),
}],
};
}
// No PII — process normally
return {
content: [{ type: 'text', text: JSON.stringify({ processed_text: document_text }) }],
};
});
The two-call pattern (ContainsPiiEntities then DetectPiiEntities) optimizes cost: ContainsPiiEntities runs a cheaper classification pass first. If PII is not present (the common case for most tool inputs), you avoid the more expensive span-level detection call. If your workload is predominantly PII-heavy text (e.g., processing support tickets from an HR system), call DetectPiiEntities directly and skip the pre-screen.
Async PII redaction job for batch documents
import {
StartPiiEntitiesDetectionJobCommand,
DescribePiiEntitiesDetectionJobCommand,
} from '@aws-sdk/client-comprehend';
// ONLY_REDACTION mode: outputs redacted document copies to S3
const { JobId } = await client.send(new StartPiiEntitiesDetectionJobCommand({
JobName: `pii-redact-${Date.now()}`,
DataAccessRoleArn: 'arn:aws:iam::123456789012:role/ComprehendDataAccessRole',
LanguageCode: LanguageCode.EN,
Mode: 'ONLY_REDACTION',
RedactionConfig: {
// REPLACE_WITH_PII_ENTITY_TYPE: replaces PII with [NAME], [EMAIL], etc.
// REDACT: replaces PII with * characters
MaskMode: 'REPLACE_WITH_PII_ENTITY_TYPE',
// Optional: only redact specific entity types
// PiiEntityTypes: ['NAME', 'EMAIL', 'PHONE', 'SSN', 'CREDIT_DEBIT_NUMBER'],
MaskCharacter: '*', // only used when MaskMode is REDACT
},
InputDataConfig: {
S3Uri: 's3://my-bucket/raw-documents/',
InputFormat: 'ONE_DOC_PER_LINE',
},
OutputDataConfig: {
S3Uri: 's3://my-bucket/redacted-documents/',
KmsKeyId: 'arn:aws:kms:us-east-1:123456789012:key/my-key', // optional encryption
},
}));
REPLACE_WITH_PII_ENTITY_TYPE produces human-readable redacted text: "Contact John Smith at john@example.com" becomes "Contact [NAME] at [EMAIL]". This is better than asterisks for downstream text analysis because the type label tells you what was removed, preserving sentence structure.
ONLY_OFFSETS mode (the alternative to ONLY_REDACTION) does not modify documents — it outputs a JSON file per document listing the PII span coordinates. Use this when you need to audit what PII exists without altering the originals, or when you want to apply custom redaction logic that Comprehend's built-in modes do not support.
PiiEntityTypes filtering
By default, StartPiiEntitiesDetectionJob with ONLY_REDACTION redacts all 27 entity types. For use cases where only specific types need redaction — for example, a compliance requirement to redact financial identifiers but preserve names for customer service workflows — specify the PiiEntityTypes list:
RedactionConfig: {
MaskMode: 'REPLACE_WITH_PII_ENTITY_TYPE',
// Only redact financial and credential PII; preserve names, addresses
PiiEntityTypes: [
'SSN',
'CREDIT_DEBIT_NUMBER',
'BANK_ACCOUNT_NUMBER',
'BANK_ROUTING',
'AWS_ACCESS_KEY',
'AWS_SECRET_KEY',
'PASSWORD',
'PIN',
],
}
The synchronous DetectPiiEntities API returns all entity types without filtering — filter client-side by checking the Type field against your allowed list. There is no server-side PiiEntityTypes filter for the synchronous API; the type filter only applies to async jobs.
Confidence scores and false positive management
Comprehend's PII detection scores represent entity-level confidence. Common false positive patterns and mitigation strategies:
- IP_ADDRESS false positives: Software version strings like
1.2.3.4and date formats like2026.11.03can score high as IP addresses. If your documents contain these patterns frequently, excludeIP_ADDRESSfrom blocking behavior or raise its threshold to 0.95. - DATE_TIME false positives: Meeting reference numbers, ticket IDs, and order numbers often match date patterns. Apply
DATE_TIMEredaction only in context-appropriate pipelines (HR documents, medical records) rather than general technical support workflows. - NAME false positives: Product names, company names, and capitalized technical terms frequently score as NAME entities. A threshold of 0.85+ for NAME is appropriate for most technical document workloads; 0.75 is better for more varied prose.
- AWS_ACCESS_KEY / AWS_SECRET_KEY: These have very high precision — the AKIA prefix pattern and 40-character key format are distinctive. A threshold of 0.70 is appropriate for these types because false negatives (missing real credentials) are more costly than false positives.
For AliveMCP's monitoring configuration documents (which users sometimes paste into tool arguments), detecting and blocking AWS_ACCESS_KEY, AWS_SECRET_KEY, and PASSWORD at 0.70+ threshold protects against credential leakage to logs, while allowing IP addresses and dates through at lower confidence prevents over-redaction of legitimate technical content.