Guide · Amazon Kendra · Enterprise Search · RAG · MCP Tool
Amazon Kendra for MCP Servers — Enterprise Search Index, Query API, and Tool Integration
Amazon Kendra is a managed ML-powered enterprise search service that uses deep learning models to rank results by semantic relevance rather than keyword frequency. Unlike OpenSearch or Elasticsearch — where relevance tuning is a manual discipline of field boosts and query DSL — Kendra ships pre-trained models for over 14 languages, pre-built data source connectors for SharePoint, Confluence, Salesforce, and S3, and a FAQ ingestion pipeline that surfaces direct answers alongside standard document results. For an MCP server, Kendra is the right choice when your documents live in an enterprise content system (not a raw S3 bucket), when you need explicit FAQ matching as a first-class result type, or when your organization needs managed connectors instead of a custom ingestion pipeline. This guide covers index creation with CDK, the full TypeScript QueryCommand handler mapping ResultItems to MCP tool responses, the three result types and how to format each, the minimal IAM policy, and a failure-mode reference table covering throttling, ARN format mistakes, and empty-result traps.
TL;DR
Kendra is a managed ML search service — you ingest documents via data source connectors or the BatchPutDocument API, then query with kendra:Query using @aws-sdk/client-kendra. Two index editions exist: DEVELOPER_EDITION (25,000-document limit, $810/month, single-AZ) and ENTERPRISE_EDITION (100,000+ documents, $1,008/month, HA across three AZs). The QueryCommand response returns ResultItems[], each with a Type of DOCUMENT, QUESTION_ANSWER, or ANSWER — your MCP handler should surface ANSWER and QUESTION_ANSWER results first as direct answers, followed by DOCUMENT results as supporting sources. For IAM, the minimal policy is kendra:Query on the index ARN, which takes the form arn:aws:kendra:REGION:ACCOUNT:index/INDEX_ID — the trailing index/ path segment is a frequent mistake that causes AccessDeniedException.
When to choose Kendra vs OpenSearch vs Bedrock Knowledge Bases
All three services can serve as the retrieval backend for an MCP search tool, but they occupy different positions in the search-vs-RAG spectrum. Choosing the wrong one means engineering around its grain rather than with it.
Kendra's ML relevance ranking is trained on over 750 million queries from enterprise content and understands concepts like "latest version", "how to", and question intent without any query rewriting on your part. Its pre-built connectors — SharePoint Online, Confluence Cloud and Server, ServiceNow, Salesforce, database connectors — handle authentication, change detection, and incremental sync, which eliminates a large class of custom ETL work. The tradeoff is control: you cannot tune the ranking model weights, the index structure is opaque, and query throughput defaults to 10 QPS on DEVELOPER_EDITION.
OpenSearch gives you complete control over the index mapping, scoring script, and query DSL. BM25, dense vector (kNN), hybrid lexical+semantic with reciprocal rank fusion, percolator queries, aggregations — all of it is available. The cost is operational burden: you manage shard sizing, replica counts, index lifecycle policies, and must build your own ingestion pipeline. OpenSearch is the right choice when you need custom scoring logic (e.g., boosting results based on recency or document authority scores your business assigns), faceted navigation, or query analytics that Kendra does not expose.
Bedrock Knowledge Bases provides embedding-based vector search with no index management — you point it at an S3 bucket and it handles chunking, embedding, and storage in a managed vector store (OpenSearch Serverless or Aurora PostgreSQL). It is purpose-built for embedding-based RAG with the RetrieveAndGenerate API. It has no structured connectors, no FAQ pipeline, and no keyword search fallback by default. Use it when your content is in S3 and you want the fastest path to grounded Q&A with citations.
| Capability | Amazon Kendra | OpenSearch | Bedrock Knowledge Bases |
|---|---|---|---|
| Relevance ranking | ML-based, pre-trained, no tuning required | BM25 + custom scoring scripts | Embedding cosine similarity (+ hybrid BM25 on OpenSearch Serverless) |
| FAQ / direct answer extraction | Yes — ANSWER and QUESTION_ANSWER result types | No built-in; requires separate QA model | Partial — model synthesizes answer, not extracted from FAQ corpus |
| Pre-built data source connectors | Yes — SharePoint, Confluence, Salesforce, S3, RDS, ServiceNow, and 20+ more | No; you build ingestion pipelines | S3 only (natively); other sources via custom ingestion |
| Custom scoring / boosting | Limited — AttributeFilter and DocumentRelevanceOverride only | Full control via function_score and script_score | None — vector similarity is the only ranking signal |
| Multi-language support | 14 languages, model pre-trained per language | Yes via language analyzers; model training is DIY | Depends on embedding model chosen |
| MCP use case fit | Enterprise knowledge base search, FAQ assistant, document discovery across SharePoint/Confluence | Log search, e-commerce catalog, custom scoring pipelines | Grounded Q&A over S3 document corpus, citation-heavy research assistant |
| Managed operations | Fully managed — no shard or cluster sizing | Semi-managed on OpenSearch Service; still need index lifecycle management | Fully managed — no vector store sizing |
Creating a Kendra index with CDK
The CDK L2 construct for Kendra does not exist in aws-cdk-lib as of this writing — you use the L1 CfnIndex construct from aws-cdk-lib/aws-kendra. The two edition choices have hard capacity and pricing differences that affect your MCP server's document storage ceiling from day one, so the edition decision belongs in infrastructure code, not an operational runbook.
DEVELOPER_EDITION stores up to 25,000 documents and 3 FAQs. It runs in a single Availability Zone (no HA), limits query throughput to 10 QPS, and costs approximately $810/month. It is appropriate for internal prototypes, development environments, and production deployments where the document corpus is small and traffic is low. ENTERPRISE_EDITION stores up to 100,000 documents (extendable via capacity units), runs across three AZs with automatic failover, allows up to 40 QPS (default, extendable), and costs approximately $1,008/month base. Use ENTERPRISE_EDITION for any production deployment where availability matters.
Custom DocumentAttributeDefinitions extend the built-in Kendra metadata fields with your own typed attributes. These attributes are indexed alongside document content and can be used in AttributeFilter expressions to scope query results, in Facets requests to build faceted navigation, and in DocumentRelevanceOverride to boost or demote results at query time. The four supported types are STRING_VALUE, STRING_LIST_VALUE, LONG_VALUE, and DATE_VALUE.
import * as cdk from 'aws-cdk-lib';
import * as kendra from 'aws-cdk-lib/aws-kendra';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
export class KendraIndexStack extends cdk.Stack {
public readonly indexId: string;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// S3 bucket for Kendra access logs
const accessLogBucket = new s3.Bucket(this, 'KendraAccessLogs', {
bucketName: `kendra-access-logs-${this.account}-${this.region}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.RETAIN,
lifecycleRules: [
{
id: 'expire-old-logs',
expiration: cdk.Duration.days(90),
},
],
});
// IAM role that Kendra assumes to write access logs and query CloudWatch
const kendraRole = new iam.Role(this, 'KendraIndexRole', {
assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'),
description: 'Role assumed by Amazon Kendra for index operations',
});
kendraRole.addToPolicy(new iam.PolicyStatement({
actions: [
'cloudwatch:PutMetricData',
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
'logs:DescribeLogGroups',
'logs:DescribeLogStreams',
],
resources: ['*'],
conditions: {
StringEquals: {
'cloudwatch:namespace': 'Kendra',
},
},
}));
accessLogBucket.grantWrite(kendraRole);
// The Kendra index — ENTERPRISE_EDITION for production HA
const cfnIndex = new kendra.CfnIndex(this, 'KendraIndex', {
name: 'mcp-enterprise-search',
edition: 'ENTERPRISE_EDITION',
// Use DEVELOPER_EDITION for dev/staging:
// edition: 'DEVELOPER_EDITION',
roleArn: kendraRole.roleArn,
description: 'Enterprise search index for MCP server tools',
// Server-side encryption with an AWS managed key (default)
// To use a customer-managed KMS key, add:
// serverSideEncryptionConfiguration: {
// kmsKeyId: 'arn:aws:kms:...',
// },
// Custom document attribute definitions
documentMetadataConfigurations: [
{
name: 'product_line',
type: 'STRING_VALUE',
search: {
facetable: true, // include in Facets responses
searchable: true, // text search over values
displayable: true, // return in result metadata
sortable: false,
},
},
{
name: 'document_version',
type: 'STRING_VALUE',
search: {
facetable: true,
searchable: false, // versions are for filtering, not text search
displayable: true,
sortable: true,
},
},
{
name: 'published_at',
type: 'DATE_VALUE',
search: {
facetable: false,
searchable: false,
displayable: true,
sortable: true, // allows sort-by-date in queries
},
},
{
name: 'view_count',
type: 'LONG_VALUE',
search: {
facetable: false,
searchable: false,
displayable: true,
sortable: true,
},
},
{
name: 'applicable_regions',
type: 'STRING_LIST_VALUE',
search: {
facetable: true,
searchable: false,
displayable: true,
sortable: false, // STRING_LIST_VALUE cannot be sorted
},
},
],
// S3 access log bucket reference
userContextPolicy: 'ATTRIBUTE_FILTER',
// Use ATTRIBUTE_FILTER for group-based filtering via metadata
// Use USER_TOKEN for JWT/OIDC-based user context (requires IAM Identity Center)
});
this.indexId = cfnIndex.attrId;
new cdk.CfnOutput(this, 'KendraIndexId', {
value: cfnIndex.attrId,
description: 'Kendra index ID — use in KENDRA_INDEX_ID env var',
});
new cdk.CfnOutput(this, 'KendraIndexArn', {
value: `arn:aws:kendra:${this.region}:${this.account}:index/${cfnIndex.attrId}`,
description: 'Kendra index ARN — use in IAM policies',
});
}
}
One CDK gotcha: CfnIndex does not expose a grantQuery convenience method (that exists only on the L2 construct, which is not yet available). You must construct the IAM statement manually using the index ARN output. The index ID is available immediately as cfnIndex.attrId; the full ARN must be assembled as shown in the output above. If you are creating data sources in the same stack, declare them after the index and pass cfnIndex.attrId as the indexId property — CloudFormation will automatically handle the creation order dependency.
MCP tool handler: querying Kendra
The query handler is the core of a Kendra-backed MCP tool. The QueryCommand sends a natural-language query string to the index and returns a ranked list of ResultItems. Each item carries a Type (the result classification), scored relevance metadata, and the document content — either an extracted excerpt or a full FAQ answer. Your MCP handler maps these items to MCP content objects, grouping by result type to surface direct answers before document links.
import {
KendraClient,
QueryCommand,
QueryCommandOutput,
QueryResultType,
ScoreConfidence,
type QueryResultItem,
type AttributeFilter,
} from '@aws-sdk/client-kendra';
const kendra = new KendraClient({
region: process.env.AWS_REGION ?? 'us-east-1',
});
const INDEX_ID = process.env.KENDRA_INDEX_ID!;
// ---- Types ----
interface KendraSearchResult {
type: 'direct_answer' | 'faq_answer' | 'document';
title: string;
excerpt: string;
sourceUri: string;
scoreConfidence: ScoreConfidence | undefined;
documentId: string;
}
interface McpTextContent {
type: 'text';
text: string;
}
// ---- Query function ----
async function queryKendra(
queryText: string,
pageSize = 10,
attributeFilter?: AttributeFilter,
): Promise {
const response: QueryCommandOutput = await kendra.send(new QueryCommand({
IndexId: INDEX_ID,
QueryText: queryText,
PageSize: pageSize,
PageNumber: 1,
// Optional: scope results to a subset of documents
// AttributeFilter: attributeFilter,
// Optional: request specific facets
// Facets: [{ DocumentAttributeKey: 'product_line', MaxResults: 10 }],
// Optional: sort by a custom attribute
// SortingConfiguration: {
// DocumentAttributeKey: 'published_at',
// SortOrder: 'DESC',
// },
// Pass optional attribute filter if provided
...(attributeFilter ? { AttributeFilter: attributeFilter } : {}),
}));
const items = response.ResultItems ?? [];
return items.map(mapResultItem);
}
function mapResultItem(item: QueryResultItem): KendraSearchResult {
const title =
item.DocumentTitle?.Text ??
item.DocumentId ??
'Untitled';
const sourceUri =
item.DocumentURI ?? '';
const scoreConfidence =
item.ScoreAttributes?.ScoreConfidence;
const documentId = item.DocumentId ?? '';
// Result type routing
switch (item.Type) {
case QueryResultType.ANSWER: {
// Direct answer extracted from a FAQ pair
const answerText =
item.AdditionalAttributes?.find(a => a.Key === 'AnswerText')
?.Value?.TextWithHighlightsValue?.Text ??
item.DocumentExcerpt?.Text ??
'';
return {
type: 'faq_answer',
title,
excerpt: answerText,
sourceUri,
scoreConfidence,
documentId,
};
}
case QueryResultType.QUESTION_ANSWER: {
// Extracted answer passage from a document (not an explicit FAQ)
const passageText =
item.AdditionalAttributes?.find(a => a.Key === 'AnswerText')
?.Value?.TextWithHighlightsValue?.Text ??
item.DocumentExcerpt?.Text ??
'';
return {
type: 'direct_answer',
title,
excerpt: passageText,
sourceUri,
scoreConfidence,
documentId,
};
}
case QueryResultType.DOCUMENT:
default: {
// Standard ranked document result
const excerptText = item.DocumentExcerpt?.Text ?? '';
return {
type: 'document',
title,
excerpt: excerptText,
sourceUri,
scoreConfidence,
documentId,
};
}
}
}
// ---- MCP tool response formatter ----
function formatMcpResponse(results: KendraSearchResult[]): McpTextContent[] {
if (results.length === 0) {
return [{ type: 'text', text: 'No results found.' }];
}
const directAnswers = results.filter(
r => r.type === 'faq_answer' || r.type === 'direct_answer',
);
const documents = results.filter(r => r.type === 'document');
const parts: string[] = [];
if (directAnswers.length > 0) {
parts.push('## Direct Answers\n');
for (const ans of directAnswers) {
const confidence = ans.scoreConfidence
? ` (confidence: ${ans.scoreConfidence})`
: '';
parts.push(`### ${ans.title}${confidence}`);
parts.push(ans.excerpt);
if (ans.sourceUri) {
parts.push(`_Source: ${ans.sourceUri}_`);
}
parts.push('');
}
}
if (documents.length > 0) {
parts.push('## Relevant Documents\n');
for (const doc of documents) {
const confidence = doc.scoreConfidence
? ` (confidence: ${doc.scoreConfidence})`
: '';
parts.push(`### ${doc.title}${confidence}`);
parts.push(doc.excerpt);
if (doc.sourceUri) {
parts.push(`_Source: ${doc.sourceUri}_`);
}
parts.push('');
}
}
return [{ type: 'text', text: parts.join('\n') }];
}
// ---- MCP tool entry point ----
export async function handleKendraSearchTool(args: {
query: string;
productLine?: string;
maxResults?: number;
}): Promise<{ content: McpTextContent[] }> {
const attributeFilter: AttributeFilter | undefined = args.productLine
? {
EqualsTo: {
Key: 'product_line',
Value: { StringValue: args.productLine },
},
}
: undefined;
const results = await queryKendra(
args.query,
args.maxResults ?? 10,
attributeFilter,
);
return { content: formatMcpResponse(results) };
}
The AdditionalAttributes array on each result item is where Kendra places the extracted answer text for ANSWER and QUESTION_ANSWER results — the key is "AnswerText" and the value is a TextWithHighlightsValue object. The DocumentExcerpt.Text field is the fallback for all result types: it contains the ranked passage for documents and a copy of the answer text for FAQ results. For DOCUMENT results, DocumentExcerpt.Text may contain HTML highlights wrapped in <strong> tags — strip these if you are embedding the excerpt in a plain-text MCP response.
IAM permissions for Kendra query
The minimum IAM policy for a Lambda function (or ECS task) that serves as an MCP tool backend for Kendra requires two actions: kendra:Query to execute search queries and kendra:DescribeIndex to run health checks and verify the index is in an ACTIVE state before accepting requests. A third optional action — kendra:SubmitFeedback — enables click-through relevance feedback, which improves Kendra's ranking model for your index over time and is worth including if your MCP tool can capture which result the user selected.
The Kendra index ARN format is arn:aws:kendra:REGION:ACCOUNT_ID:index/INDEX_ID. The index/ path segment before the index ID is mandatory — omitting it and specifying arn:aws:kendra:REGION:ACCOUNT_ID:INDEX_ID produces an AccessDeniedException at runtime that is not immediately obvious from the error message, since the IAM condition check fails silently on a malformed resource ARN.
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cdk from 'aws-cdk-lib';
// Minimal Kendra IAM policy as a standalone PolicyStatement
const kendraQueryPolicy = new iam.PolicyStatement({
sid: 'KendraQueryAccess',
effect: iam.Effect.ALLOW,
actions: [
'kendra:Query', // required: execute search queries
'kendra:DescribeIndex', // required: health check before serving requests
'kendra:SubmitFeedback', // optional: click relevance improvement
],
// ARN format: arn:aws:kendra:REGION:ACCOUNT:index/INDEX_ID
// The 'index/' prefix is part of the ARN — do not omit it
resources: [
`arn:aws:kendra:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:index/${process.env.KENDRA_INDEX_ID}`,
],
});
// Equivalent raw IAM policy JSON (for non-CDK deployments):
//
// {
// "Version": "2012-10-17",
// "Statement": [
// {
// "Sid": "KendraQueryAccess",
// "Effect": "Allow",
// "Action": [
// "kendra:Query",
// "kendra:DescribeIndex",
// "kendra:SubmitFeedback"
// ],
// "Resource": "arn:aws:kendra:us-east-1:123456789012:index/abcd1234-ab12-ab12-ab12-abcd12345678"
// }
// ]
// }
// Attach to a Lambda function's execution role
declare const mcpHandlerFn: lambda.Function;
declare const indexId: string;
declare const stack: cdk.Stack;
mcpHandlerFn.addToRolePolicy(new iam.PolicyStatement({
sid: 'KendraQueryAccess',
effect: iam.Effect.ALLOW,
actions: ['kendra:Query', 'kendra:DescribeIndex', 'kendra:SubmitFeedback'],
resources: [
`arn:aws:kendra:${stack.region}:${stack.account}:index/${indexId}`,
],
}));
// Health check using DescribeIndex
import { DescribeIndexCommand } from '@aws-sdk/client-kendra';
async function isIndexReady(indexId: string): Promise {
const response = await kendra.send(new DescribeIndexCommand({
Id: indexId,
}));
return response.Status === 'ACTIVE';
}
// Submit click feedback to improve Kendra ranking over time
import { SubmitFeedbackCommand, RelevanceType } from '@aws-sdk/client-kendra';
async function submitClickFeedback(
queryId: string,
resultId: string,
): Promise {
await kendra.send(new SubmitFeedbackCommand({
IndexId: INDEX_ID,
QueryId: queryId,
ClickFeedbackItems: [
{
ResultId: resultId,
ClickTime: new Date(),
},
],
}));
}
For cross-account deployments — where the MCP server Lambda lives in one AWS account and the Kendra index lives in another — Kendra does not support resource-based policies on the index. You must use IAM role assumption: the Lambda assumes a role in the Kendra account that has the kendra:Query permission, and the Kendra account role has a trust policy permitting the Lambda's execution role ARN to assume it. See the IAM role assumption guide for the full pattern. Note that this adds one sts:AssumeRole call per Lambda cold start; cache the assumed credentials in memory using the SDK's fromTemporaryCredentials provider to avoid per-request STS latency.
Result types and when each appears
The QueryCommand response can contain up to three distinct result types in a single query response. Kendra decides which types to include based on the index content and the nature of the query — a question-phrased query ("What is the return policy?") is more likely to trigger ANSWER and QUESTION_ANSWER results than a keyword query ("return policy"). An MCP tool that presents all three types with appropriate formatting gives users both a direct answer and the supporting source material in a single tool invocation.
| Result type | When Kendra returns it | Key fields | Recommended MCP formatting |
|---|---|---|---|
ANSWER |
An explicit FAQ pair in the index matches the query. The FAQ must be ingested via the CreateFaq API or a data source that maps to Kendra FAQ format (CSV or JSON). The question text must semantically match the query — Kendra uses a separate FAQ ranking model distinct from document ranking. |
AdditionalAttributes[Key="AnswerText"].Value.TextWithHighlightsValue.Text, DocumentTitle.Text, DocumentURI |
Surface at the top of the response as "Direct Answer". Include the FAQ question as a header and the answer text as the body. Mark with a note like "(from FAQ)" so users know this is an authoritative indexed answer. |
QUESTION_ANSWER |
Kendra's ML passage extraction model found a paragraph in a document that directly answers the query question. This is the "reading comprehension" capability — Kendra identifies the most answer-like passage even when no explicit FAQ exists. Appears on question-phrased queries against indexed documents. | AdditionalAttributes[Key="AnswerText"].Value.TextWithHighlightsValue.Text, DocumentTitle.Text, DocumentURI, DocumentExcerpt.Text |
Surface after ANSWER results but before DOCUMENT results. Label as "Extracted Answer" with the source document title and URI. This tells the user the answer came from a specific document passage, not an explicit FAQ. |
DOCUMENT |
A standard ranked document result. The document is semantically relevant to the query but Kendra did not extract a specific answer passage. Appears for all query types; is the most common result type. ScoreAttributes.ScoreConfidence indicates how confident Kendra is in the relevance ranking. |
DocumentTitle.Text, DocumentURI, DocumentExcerpt.Text, ScoreAttributes.ScoreConfidence |
Surface as "Relevant Sources" section after any direct answers. Include title, excerpt (truncated to ~200 characters), source URI, and score confidence. VERY_HIGH and HIGH confidence results deserve more prominent display than MEDIUM or LOW. |
The ScoreConfidence field on DOCUMENT results is an enum: VERY_HIGH, HIGH, MEDIUM, LOW, and NOT_AVAILABLE. NOT_AVAILABLE appears during the first few queries after index creation or after a large batch of new documents is ingested — Kendra's ranking model needs to process the new content before it can generate confidence scores. If your MCP tool logs search quality metrics, track the ratio of NOT_AVAILABLE results as a signal of index freshness issues. For FAQ-type results (ANSWER), the score confidence field is typically absent — FAQ matching is binary rather than graded.
Common failures
| Error / symptom | Cause | Fix |
|---|---|---|
ThrottlingException: Rate exceeded on QueryCommand |
DEVELOPER_EDITION has a default limit of 10 QPS for Query operations. ENTERPRISE_EDITION defaults to 40 QPS. These limits apply per index across all callers — an MCP server handling concurrent requests from multiple users can saturate the limit quickly during load tests or peak traffic. |
File a service quota increase request via the AWS Service Quotas console for Kendra / Number of query capacity units. Each capacity unit on ENTERPRISE_EDITION adds 0.5 QPS (and costs ~$250/month). For DEVELOPER_EDITION, the only option is a quota increase request — there are no capacity units available. Implement exponential backoff with jitter in the MCP handler: start at 500ms, cap at 5s, max 3 retries. |
AccessDeniedException despite correct IAM policy |
The IAM policy resource ARN is malformed — typically missing the index/ path segment. Kendra index ARNs are arn:aws:kendra:REGION:ACCOUNT:index/INDEX_ID. Using arn:aws:kendra:REGION:ACCOUNT:INDEX_ID (without index/) silently fails the IAM condition check. Also check that the region in the ARN matches the Kendra client's configured region — a client defaulting to us-east-1 querying an index in us-west-2 will produce this error even with a correct-looking ARN in the policy. |
Verify the ARN with: aws kendra describe-index --id INDEX_ID --query 'Id' --output text then construct the ARN as arn:aws:kendra:REGION:ACCOUNT:index/OUTPUT_ID. Use aws iam simulate-principal-policy to validate the policy before deploying. |
| Query returns zero results immediately after document upload | Kendra indexes documents asynchronously during a sync run. Documents uploaded to S3 are not searchable until the data source sync job completes — which can take anywhere from a few minutes (small batch) to several hours (large initial corpus). Querying the index while a sync is in progress returns only documents indexed in previous sync runs. | Use ListDataSourceSyncJobsCommand to check the status of the most recent sync job. Wait for Status: "SUCCEEDED" before testing queries. For the MCP tool, consider adding a get_index_status tool that surfaces the last successful sync time so users understand data freshness. |
ScoreConfidence: NOT_AVAILABLE on all results after index creation |
Kendra's ML ranking model needs time to process a newly created index or a large batch of newly ingested documents before it can generate confidence scores. The index state is ACTIVE (queries succeed), but the ranking model has not finished its initial calibration pass. This typically resolves within 30–60 minutes after the first sync job completes on a new index. |
Wait for at least one complete sync cycle and then run a few warm-up queries before treating NOT_AVAILABLE as an error signal. After warm-up, NOT_AVAILABLE on a small fraction of results is normal for documents with thin content (short titles, no body text). Log these as a data quality signal, not an infrastructure error. |
CDK deployment fails: DataSource must reference an existing index |
A CfnDataSource construct in the same CDK stack is deploying before the CfnIndex because CloudFormation detected no explicit dependency. CDK infers dependencies from token references, but if the data source's indexId was set to a hardcoded string rather than cfnIndex.attrId, the dependency is not registered and the deployment order is non-deterministic. |
Always pass cfnIndex.attrId (not a hardcoded string) as the indexId property of CfnDataSource. If you must use a hardcoded value, add an explicit dependency: cfnDataSource.addDependency(cfnIndex). After fixing, run cdk diff to verify the dependency arrow appears in the change set before deploying. |