Guide · AWS Rekognition · Face Collections

AWS Rekognition Face Collections in MCP Servers — IndexFaces, SearchFacesByImage, CompareFaces

Rekognition face collections are persistent, indexed stores of facial feature vectors — not stored images. An MCP server uses a collection to build workflows like "is this person already in our system?", "find photos containing this individual", or "verify that two images show the same person". The key primitives are CreateCollectionCommand (provision the index), IndexFacesCommand (add a face from an image with a custom ExternalImageId), SearchFacesByImageCommand (find similar faces in the collection given a query image), and CompareFacesCommand (compare two images directly without a collection). This guide covers the full workflow from collection creation through face lifecycle management, similarity threshold design, and the difference between collection-based search and direct comparison.

TL;DR

Create a collection once with CreateCollectionCommand. Add faces with IndexFacesCommand using ExternalImageId (your own identifier — user UUID, photo ID, etc.) and QualityFilter: 'AUTO'. Search by supplying a query image to SearchFacesByImageCommand with FaceMatchThreshold: 95 for high-security or 80 for standard use. Each match returns Face.ExternalImageId (your identifier) and Similarity (0–100). For direct two-image comparison without collection overhead, use CompareFacesCommand. Delete individual faces with DeleteFacesCommand using FaceId values. Rekognition stores only the facial feature vector, not the original image — GDPR-style deletion removes the vector but not any copy of the image you stored separately.

Collection creation and configuration

import {
  RekognitionClient,
  CreateCollectionCommand,
  DescribeCollectionCommand,
  DeleteCollectionCommand,
} from '@aws-sdk/client-rekognition';

const rekognition = new RekognitionClient({ region: 'us-east-1' });

// Create once — idempotent in terms of your business logic (the API throws
// ResourceAlreadyExistsException if the collectionId already exists, so guard it)
async function ensureCollection(collectionId: string): Promise<void> {
  try {
    await rekognition.send(new CreateCollectionCommand({ CollectionId: collectionId }));
  } catch (err: any) {
    if (err.name !== 'ResourceAlreadyExistsException') throw err;
  }
}

// Inspect an existing collection
const { FaceCount, FaceModelVersion, CollectionARN } = await rekognition.send(
  new DescribeCollectionCommand({ CollectionId: 'users-collection' })
);
console.log(`${FaceCount} faces, model v${FaceModelVersion}`);
// FaceModelVersion controls detection accuracy; new collections get the latest model automatically

CollectionId is a region-scoped, account-scoped identifier — you can have multiple collections (e.g., one per product tenant, one per workspace). Collections are regional resources; a collection in us-east-1 is not accessible from eu-west-1. The face model version is pinned when the collection is created — to upgrade, create a new collection and re-index all faces.

Indexing faces with ExternalImageId

import { IndexFacesCommand } from '@aws-sdk/client-rekognition';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

const server = new McpServer({ name: 'face-tools', version: '1.0.0' });

server.tool('index_face', {
  collection_id: z.string(),
  // S3 image containing exactly one face (usually a profile photo, ID document crop, etc.)
  s3_bucket: z.string(),
  s3_key: z.string(),
  // Your identifier — stored alongside the face vector, returned on search
  external_id: z.string().max(255),
}, async ({ collection_id, s3_bucket, s3_key, external_id }) => {
  const { FaceRecords = [], UnindexedFaces = [] } = await rekognition.send(
    new IndexFacesCommand({
      CollectionId: collection_id,
      Image: { S3Object: { Bucket: s3_bucket, Name: s3_key } },
      ExternalImageId: external_id,          // your user/photo ID — stored with the vector
      MaxFaces: 1,                           // only index the largest/most prominent face
      QualityFilter: 'AUTO',                 // skip blurry, occluded, or tilted faces
      DetectionAttributes: ['DEFAULT'],      // 'DEFAULT' returns BoundingBox, Confidence, etc.
    })
  );

  if (FaceRecords.length === 0) {
    const reason = UnindexedFaces[0]?.Reasons?.join(', ') ?? 'unknown';
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({ success: false, reason, unindexed_count: UnindexedFaces.length }),
      }],
    };
  }

  const indexed = FaceRecords.map(r => ({
    face_id: r.Face?.FaceId,          // Rekognition UUID — use to delete a specific vector
    external_id: r.Face?.ExternalImageId,
    confidence: r.Face?.Confidence,
    bounding_box: r.FaceDetail?.BoundingBox,
  }));

  return {
    content: [{
      type: 'text',
      text: JSON.stringify({ success: true, indexed }),
    }],
  };
});

ExternalImageId is the bridge between Rekognition's internal FaceId UUID and your application's user/photo identity. Store the returned FaceId alongside the user record in your database — you'll need it later to delete the vector. If you store only ExternalImageId, you can still find the vector via ListFacesCommand filtering by ExternalImageId.

QualityFilter: 'AUTO' rejects low-quality faces that would degrade search accuracy. The UnindexedFaces array in the response explains which faces were skipped and why (e.g., EXCEEDS_MAX_FACES, EXTREME_POSE, LOW_BRIGHTNESS, LOW_SHARPNESS). Log these for troubleshooting — users sometimes upload passport scans that are too dark or selfies with extreme head tilt.

Searching a collection by image

import { SearchFacesByImageCommand } from '@aws-sdk/client-rekognition';

server.tool('search_similar_faces', {
  collection_id: z.string(),
  s3_bucket: z.string(),
  s3_key: z.string(),
  // Minimum similarity to include in results (0–100)
  threshold: z.number().min(0).max(100).default(80),
  max_results: z.number().int().min(1).max(4096).default(10),
}, async ({ collection_id, s3_bucket, s3_key, threshold, max_results }) => {
  const { FaceMatches = [], SearchedFaceBoundingBox, SearchedFaceConfidence } =
    await rekognition.send(new SearchFacesByImageCommand({
      CollectionId: collection_id,
      Image: { S3Object: { Bucket: s3_bucket, Name: s3_key } },
      FaceMatchThreshold: threshold,
      MaxFaces: max_results,
    }));

  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        query_face: {
          bounding_box: SearchedFaceBoundingBox,
          confidence: SearchedFaceConfidence,
        },
        matches: FaceMatches.map(m => ({
          external_id: m.Face?.ExternalImageId,
          face_id: m.Face?.FaceId,
          similarity: Math.round((m.Similarity ?? 0) * 10) / 10,
        })).sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0)),
        match_count: FaceMatches.length,
      }),
    }],
  };
});

Results are already sorted by similarity descending. SearchedFaceBoundingBox tells you which face in the query image was used for the search — if the query image has multiple people, Rekognition picks the largest face. For multi-face images where you want to search using a specific person, crop to that face before calling the API.

Similarity threshold design

Threshold Use case Trade-off
99+ High-security identity verification Very few false positives; misses valid matches from aging, lighting, angle changes
95 Standard identity verification (login, access control) Low false positive rate; recommended for most auth workflows
80 (default) Face grouping, photo de-duplication Catches the same person across varied conditions; some false positives among similar-looking people
60–70 Soft suggestions ("you might know…") High recall; expect false positives; always show similarity score to user

Similarity is a cosine-based score, not a simple Euclidean distance. A score of 95 means the two face vectors are extremely close in the high-dimensional feature space. Identical twins typically score 97–99.5. The same person across a 10-year age gap typically scores 85–95.

Direct comparison without a collection

import { CompareFacesCommand } from '@aws-sdk/client-rekognition';

server.tool('compare_two_faces', {
  source_s3_uri: z.string(),   // reference image (e.g., ID document photo)
  target_s3_uri: z.string(),   // image to verify against (e.g., selfie)
  similarity_threshold: z.number().default(95),
}, async ({ source_s3_uri, target_s3_uri, similarity_threshold }) => {
  const parseS3 = (uri: string) => {
    const url = new URL(uri);
    return { S3Object: { Bucket: url.hostname, Name: url.pathname.slice(1) } };
  };

  const { FaceMatches = [], UnmatchedFaces = [], SourceImageFace } =
    await rekognition.send(new CompareFacesCommand({
      SourceImage: parseS3(source_s3_uri),
      TargetImage: parseS3(target_s3_uri),
      SimilarityThreshold: similarity_threshold,
    }));

  const matched = FaceMatches.length > 0;
  const topMatch = FaceMatches.sort((a, b) => (b.Similarity ?? 0) - (a.Similarity ?? 0))[0];

  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        match: matched,
        similarity: topMatch?.Similarity ?? null,
        source_confidence: SourceImageFace?.Confidence,
        unmatched_faces_in_target: UnmatchedFaces.length,
      }),
    }],
  };
});

CompareFaces does not require a collection. The source image should contain exactly one face (the reference — e.g., an uploaded ID photo). The target image is compared against that reference — it may contain multiple faces; Rekognition returns all matches. UnmatchedFaces lists faces in the target that did not meet the threshold, letting you detect "someone else is also in this photo".

Collection lifecycle — listing and deletion

import {
  ListFacesCommand,
  DeleteFacesCommand,
  ListCollectionsCommand,
} from '@aws-sdk/client-rekognition';

// List all faces in a collection (paginated)
async function listAllFaces(collectionId: string) {
  const faces: any[] = [];
  let nextToken: string | undefined;

  do {
    const result = await rekognition.send(new ListFacesCommand({
      CollectionId: collectionId,
      MaxResults: 1000,
      NextToken: nextToken,
    }));
    faces.push(...(result.Faces ?? []));
    nextToken = result.NextToken;
  } while (nextToken);

  return faces; // Each face: { FaceId, ExternalImageId, Confidence, BoundingBox }
}

// Delete specific faces by FaceId (e.g., GDPR right-to-erasure request)
await rekognition.send(new DeleteFacesCommand({
  CollectionId: 'users-collection',
  FaceIds: ['face-uuid-1', 'face-uuid-2'],  // up to 1000 per call
}));

// List all collections in the region
const { CollectionIds = [] } = await rekognition.send(new ListCollectionsCommand({}));
// Paginate with MaxResults + NextToken if you have many collections

Rekognition stores only the numerical feature vector — not any image pixels. Calling DeleteFacesCommand removes the vector permanently. If your application stored a copy of the indexed image in S3, that must be deleted separately. Keep a database table mapping user_id → face_id so you can call DeleteFacesCommand without scanning the entire collection when processing erasure requests.

IAM permissions

{
  "Effect": "Allow",
  "Action": [
    "rekognition:CreateCollection",
    "rekognition:DeleteCollection",
    "rekognition:DescribeCollection",
    "rekognition:ListCollections",
    "rekognition:IndexFaces",
    "rekognition:ListFaces",
    "rekognition:DeleteFaces",
    "rekognition:SearchFacesByImage",
    "rekognition:CompareFaces"
  ],
  "Resource": "arn:aws:rekognition:us-east-1:123456789012:collection/*"
}
// Note: collection ARN format — scope to your specific collections with:
// "arn:aws:rekognition:us-east-1:123456789012:collection/users-*"

Unlike many AWS services, Rekognition collection operations support resource-level ARN scoping. Use this to give different MCP server instances access to different collections without sharing a single broad policy.