AWS Bedrock · 2026-09-17 · Bedrock arc
AWS Bedrock for MCP Servers: Converse API, Agents, Knowledge Base, Guardrails, and InvokeModel — Five Production Patterns
AWS Bedrock is not a single API — it is a family of services that can be combined in different ways depending on what your MCP tools need to do. The Converse API is the right default for calling foundation models because it normalizes request format across model families. Bedrock Agents adds managed orchestration for multi-step tasks. Knowledge Base delivers a fully managed RAG pipeline. Guardrails enforces content policy at both the model boundary and on arbitrary text. InvokeModel reaches model-specific parameters that Converse normalizes away. Each layer has production failure modes that are non-obvious from the documentation: the Agents API is streaming-only with no non-streaming variant; Knowledge Base's RetrieveAndGenerate accepts a shorter model list than the full Bedrock catalog; Guardrail DRAFT versions mutate silently when you edit config; and InvokeModel streaming chunks do not align with JSON boundaries. Five patterns address these failure modes concretely.
TL;DR
- Converse API: use cross-region inference profile IDs (
us.anthropic.claude-3-5-sonnet-20241022-v2:0) not on-demand IDs in production. CheckstopReasonbefore reading content —max_tokensmeans the response is truncated. CatchThrottlingExceptionwith exponential backoff starting at 1 s, cap at 60 s, add jitter. - Bedrock Agents:
InvokeAgentalways returns a streaming event iterator — drain until you see achunkevent withbytes. Never use the literal stringDRAFTas an alias ID in production. Pass MCP session IDs throughsessionState.sessionAttributes(string values only). - Knowledge Base: use
@aws-sdk/client-bedrock-agent-runtimefor bothRetrieveCommandandRetrieveAndGenerateCommand. Always extractcitations[].retrievedReferences[].location.s3Location.uri. Choose hierarchical chunking for documents longer than 5 pages. - Guardrails: check
response.stopReason === 'guardrail_intervened'before accessingresponse.output.message— that field has an empty content array on blocked responses. Pin to a numericguardrailVersion—DRAFTchanges silently on config edits. UseApplyGuardrailCommandto validate tool arguments that never touch a model. - InvokeModel: default to Converse. Use
InvokeModelCommandonly for Claude extended thinking (budget_tokens), Titan embedding dimension control, or Llama raw prompt format. Buffer streaming chunks and attempt JSON parse after each — chunks do not align with JSON object boundaries.
Pattern 1 — Converse API: the right default for MCP tool handlers calling Bedrock
The Bedrock SDK has two entry points for text generation: InvokeModel, which sends a raw model-specific byte body, and Converse, which uses a unified message format across all supported models. For MCP tools, Converse is almost always the right starting point because it eliminates per-model serialization code and provides built-in toolConfig support for structured tool use.
The first production decision in a Converse-based MCP tool is which model ID format to use. Bedrock exposes two formats that behave differently under load. On-demand model IDs like anthropic.claude-3-5-sonnet-20241022-v2:0 route to the model in a single region and are bounded by that region's on-demand quota — when the quota is exhausted, you get ThrottlingException with no automatic failover. Cross-region inference profile IDs add a two-letter geography prefix: us.anthropic.claude-3-5-sonnet-20241022-v2:0 (US), eu.anthropic.claude-3-5-haiku-20241022-v1:0 (EU). These route across all enabled regions in the geography, giving access to combined quota at the cost of 5–50 ms of routing overhead. For most MCP servers the combined quota headroom is worth the small latency premium.
The second production decision is InferenceConfiguration values. These are not uniform across model families — sharing the same config across Claude, Titan, and Llama causes silent misbehavior. Claude accepts topP and temperature together. Titan Premier ignores topP when temperature is 0. Command R+ clamps temperature to 0.9 without error. Llama silently uses its default temperature when the value exceeds its valid range. Always validate InferenceConfiguration values per model family before passing them to Converse.
Streaming via ConverseStreamCommand returns a typed async iterable of discriminated union events: messageStart, contentBlockStart, contentBlockDelta, and messageStop. Each event must be checked for its union member before accessing fields — TypeScript will not narrow the type automatically without a discriminant check. The messageStop event carries stopReason, which is the critical signal: end_turn means the model finished naturally, tool_use means a ToolUseBlock was emitted, max_tokens means the output was truncated, and stop_sequence means a stop string matched. Silently ignoring max_tokens is the most common source of truncated agent responses.
ThrottlingException requires exponential backoff with jitter. The minimum retry interval is 1 s; the maximum cap is 60 s. Start at 1 s, double on each retry, add ±20% jitter to prevent synchronized retries from multiple MCP server instances hitting the quota simultaneously. After 5 retries, return an error to the MCP client — do not retry indefinitely because quota exhaustion rarely resolves in under 30 seconds.
import {
BedrockRuntimeClient,
ConverseCommand,
ThrottlingException,
} from '@aws-sdk/client-bedrock-runtime';
const client = new BedrockRuntimeClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
// Cross-region inference profile: combined quota across US regions
const MODEL_ID = 'us.anthropic.claude-3-5-sonnet-20241022-v2:0';
async function converseWithRetry(userMessage: string, maxRetries = 5): Promise<string> {
let delay = 1000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await client.send(new ConverseCommand({
modelId: MODEL_ID,
messages: [{ role: 'user', content: [{ text: userMessage }] }],
inferenceConfig: { maxTokens: 2048, temperature: 0.3 },
}));
// Always check stopReason before reading content
if (response.stopReason === 'max_tokens') {
throw new Error('Response truncated at maxTokens — increase limit or handle partial response');
}
const text = response.output?.message?.content
?.filter(b => 'text' in b)
.map(b => (b as { text: string }).text)
.join('') ?? '';
return text;
} catch (err) {
if (err instanceof ThrottlingException && attempt < maxRetries) {
const jitter = delay * 0.2 * (Math.random() * 2 - 1);
await new Promise(r => setTimeout(r, delay + jitter));
delay = Math.min(delay * 2, 60_000);
continue;
}
throw err;
}
}
throw new Error('Max retries exceeded');
}
See the detailed guide: MCP Server Bedrock Converse API — model IDs, InferenceConfiguration, streaming, throttling.
Pattern 2 — Bedrock Agents: MCP servers as thin streaming adapters
AWS Bedrock Agents runs an agentic loop internally — it decides which Lambda-backed action groups to call, in which order, and loops until the model produces a final response. From the MCP client's perspective, none of this orchestration is visible: the client calls a single MCP tool and receives a single result. The MCP server's role is minimal: receive the tool call, map arguments to InvokeAgentCommand parameters, drain the response event stream, and return the final answer.
The most consequential production detail about Bedrock Agents is that InvokeAgent is always streaming — there is no non-streaming variant of this API. The response is always an async event iterator. You must drain the iterator to completion to receive the final agent response, which arrives as a chunk event with a bytes field. Events before the final chunk carry trace data and intermediate steps; the final answer is only in the chunk.bytes field decoded as UTF-8.
The second production detail is alias IDs. Bedrock Agents have a DRAFT version (the working copy you edit) and published versions with stable aliases. DRAFT is appropriate for testing but must never be used in production because it reflects the current edit state of the agent configuration — any change to the agent (prompt update, action group change, knowledge base attachment) immediately affects all DRAFT invocations. Production invocations must use a published alias ID, which looks like ABCDEF1234 and is created via CreateAgentAliasCommand or the Bedrock console. Published aliases are pinned to a specific agent version and are stable across configuration changes.
MCP session context passes through sessionState.sessionAttributes. This field accepts a Record<string, string> — only string values, no nested objects. The Bedrock Agent receives these attributes in the Lambda action group event as event.sessionAttributes. Use this to pass the MCP session ID, user identifier, or tenant context that your action group Lambda needs to make authorization decisions or look up per-session state.
When an agent produces unexpected output or loops without terminating, set enableTrace: true on the invocation and log the OrchestrationTrace events from the stream. The orchestrationTrace.observation.finalResponse field in the last trace event shows what the model decided to return and why. The rationale field in orchestration trace events shows the model's reasoning at each step, which reveals whether the agent is looping due to a failing action group or a mismatched prompt.
import {
BedrockAgentRuntimeClient,
InvokeAgentCommand,
} from '@aws-sdk/client-bedrock-agent-runtime';
const agentClient = new BedrockAgentRuntimeClient({
region: process.env.AWS_REGION ?? 'us-east-1',
});
async function invokeBedrockAgent(
agentId: string,
agentAliasId: string, // never 'DRAFT' in production
sessionId: string,
userMessage: string,
mcpSessionAttributes?: Record<string, string>,
): Promise<string> {
const response = await agentClient.send(new InvokeAgentCommand({
agentId,
agentAliasId,
sessionId,
inputText: userMessage,
enableTrace: process.env.NODE_ENV !== 'production',
sessionState: mcpSessionAttributes
? { sessionAttributes: mcpSessionAttributes }
: undefined,
}));
// InvokeAgent ALWAYS returns a streaming iterator — drain it
let finalAnswer = '';
for await (const event of response.completion ?? []) {
if (event.chunk?.bytes) {
// Final answer arrives here
finalAnswer += new TextDecoder().decode(event.chunk.bytes);
}
// event.trace contains OrchestrationTrace for debugging (when enableTrace:true)
}
if (!finalAnswer) {
throw new Error('Bedrock Agent returned no content — check trace events for loop/failure');
}
return finalAnswer;
}
Action group Lambda functions follow a strict request/response schema enforced by Bedrock. The event contains event.apiPath (the action group operation name), event.requestBody.content['application/json'].properties (the input parameters as an array of {name, value} objects), and event.sessionAttributes (the string map from sessionState). The response must return { response: { actionGroup, apiPath, httpStatusCode, responseBody: { 'application/json': { body: JSON.stringify(result) } } } } — the body must be a JSON string, not a JSON object.
See the detailed guide: MCP Server Bedrock Agents — action groups, session state, trace events, agent aliases.
Pattern 3 — Knowledge Base: managed RAG in a single MCP tool
Bedrock Knowledge Base is a fully managed RAG pipeline: you point it at an S3 bucket, it chunks and embeds the documents, stores vectors in a managed vector store (OpenSearch Serverless or Aurora PostgreSQL with pgvector), and exposes two APIs — Retrieve for pure vector search and RetrieveAndGenerate for retrieval plus synthesis. For an MCP tool that needs to answer questions grounded in a document corpus, Knowledge Base replaces the embedding lookup, vector search, context assembly, and generation steps you would otherwise implement yourself.
Both APIs live in @aws-sdk/client-bedrock-agent-runtime, not in @aws-sdk/client-bedrock-runtime. This is the first production surprise: the SDK package name suggests these are agent-runtime APIs, but they are the correct package for Knowledge Base operations regardless of whether you are using Bedrock Agents.
The distinction between the two APIs determines where RAG logic lives. Retrieve runs only the retrieval step and returns chunks with similarity scores — use this when you need to compose retrieved chunks with other data before synthesis, or when you need a model not supported by RetrieveAndGenerate. RetrieveAndGenerate runs the full pipeline — retrieval plus model synthesis — and returns a generated answer with structured citations. The supported model ARN list for RetrieveAndGenerate is shorter than the full Bedrock catalog and differs by region: anthropic.claude-3-sonnet-20240229-v1:0, anthropic.claude-3-haiku-20240307-v1:0, and amazon.titan-text-premier-v1:0 are consistently available. Cross-region inference profile IDs are not accepted by RetrieveAndGenerate — you must use the full model ARN.
Citation extraction requires traversing a nested structure. The RetrieveAndGenerate response contains a citations array where each entry corresponds to a segment of the generated text. Each citation has a retrievedReferences array of source chunks, and each reference has a location.s3Location.uri field with the S3 URI of the source document. Always extract and return these URIs in the MCP tool response — omitting sources makes the tool's answers unverifiable and breaks trust in the grounded output.
The chunking strategy is set at index time and cannot be changed without re-syncing all data sources. Three strategies exist: fixed-size chunking splits documents at a token count boundary (default 300 tokens, 20% overlap) — fast to sync, appropriate for short uniform documents like product descriptions or FAQs. Semantic chunking uses an embedding model to find natural semantic boundaries — better recall on conversational or narrative documents, slower sync. Hierarchical chunking creates parent chunks (1,500 tokens) linked to child chunks (300 tokens) — the child chunk is used for retrieval (narrow context window), the parent chunk is returned to the model (broader context) — best for technical documents longer than 5 pages where section context matters for correct interpretation. Choose hierarchical chunking for technical documentation corpora; it consistently outperforms fixed-size on heterogeneous document sets.
For multi-tenant or multi-product knowledge bases, RetrievalFilter expressions dramatically improve precision by restricting retrieval to documents matching attribute conditions. Filters use equals, notEquals, greaterThan, lessThan, in, notIn, andAll, and orAll operators against metadata attributes. Attributes must be defined in .metadata.json sidecar files stored alongside each S3 object. A sidecar for docs/guide.pdf is stored as docs/guide.pdf.metadata.json and contains {"metadataAttributes": {"tenant_id": "acme", "product": "pro", "language": "en"}}.
import {
BedrockAgentRuntimeClient,
RetrieveAndGenerateCommand,
type RetrievedReference,
} from '@aws-sdk/client-bedrock-agent-runtime';
const kbClient = new BedrockAgentRuntimeClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
const KB_ID = process.env.BEDROCK_KB_ID!;
// Full model ARN required — cross-region profile IDs not accepted here
const MODEL_ARN = 'arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0';
interface Citation {
generatedText: string;
sources: Array<{ content: string; s3Uri: string }>;
}
async function groundedSearch(
query: string,
tenantId: string, // filter to this tenant's documents
sessionId?: string, // for multi-turn continuity
): Promise<{ answer: string; citations: Citation[]; sessionId: string }> {
const response = await kbClient.send(new RetrieveAndGenerateCommand({
input: { text: query },
retrieveAndGenerateConfiguration: {
type: 'KNOWLEDGE_BASE',
knowledgeBaseConfiguration: {
knowledgeBaseId: KB_ID,
modelArn: MODEL_ARN,
retrievalConfiguration: {
vectorSearchConfiguration: {
numberOfResults: 5,
overrideSearchType: 'HYBRID',
filter: {
equals: { key: 'tenant_id', value: tenantId },
},
},
},
},
},
sessionId, // omit to start a new session, pass to continue
}));
const citations: Citation[] = (response.citations ?? []).map(c => ({
generatedText: c.generatedResponsePart?.textResponsePart?.text ?? '',
sources: (c.retrievedReferences ?? []).map((ref: RetrievedReference) => ({
content: ref.content?.text ?? '',
s3Uri: ref.location?.s3Location?.uri ?? '',
})),
}));
return {
answer: response.output?.text ?? '',
citations,
sessionId: response.sessionId ?? '',
};
}
See the detailed guide: MCP Server Bedrock Knowledge Base — retrieveAndGenerate, citations, chunking, S3 sources.
Pattern 4 — Guardrails: content policy enforcement at two layers
Bedrock Guardrails enforce content policies — topic denial, harmful content filtering, PII redaction, and custom word blocklists — at the API level rather than in application code. For MCP servers, the critical architectural decision is which of two integration points to use: the Converse API attachment, which covers model input and output; or the standalone ApplyGuardrail API, which covers arbitrary text including tool arguments, retrieved RAG chunks, and third-party API responses that never touch a foundation model.
The Converse integration attaches a guardrailConfig object to a ConverseCommand request. When the guardrail fires on the input, the model is never invoked and you save the inference cost. When it fires on the output, the response is blocked before it leaves Bedrock infrastructure. The response carries stopReason: 'guardrail_intervened' instead of 'end_turn'. This is the critical production detail: when a guardrail intervenes, the output.message.content array is empty. If your tool handler accesses response.output?.message?.content[0]?.text without checking stopReason first, it will silently return an empty string to the MCP client with no indication that a policy violation occurred.
The ApplyGuardrail integration is more powerful but requires an additional API call. Use it to validate user-supplied tool arguments before any processing (preventing prompt injection through tool inputs), to screen retrieved RAG chunks before they are included in a model prompt (preventing policy-violating content from reaching the model through the knowledge base), and to validate third-party API responses before returning them to the MCP client. This creates a complete content policy perimeter around the MCP tool's execution path, not just the model call.
For defense-in-depth, use both: ApplyGuardrailCommand with source: 'INPUT' to pre-validate user input at the tool boundary, then guardrailConfig on the ConverseCommand to cover the model's output. This prevents a user from crafting input that passes initial validation but elicits a policy-violating model response through indirect instruction.
Guardrail versioning has a production gotcha. The DRAFT version reflects the current edit state of the guardrail configuration. Any change to the guardrail — adding a blocked topic, updating a PII redaction policy, modifying a word list — immediately alters DRAFT behavior for all inflight requests. In production, always pin to a numeric version created via CreateGuardrailVersionCommand. Numeric versions are immutable snapshots that only change behavior when you explicitly deploy a new version.
import {
BedrockRuntimeClient,
ConverseCommand,
ApplyGuardrailCommand,
} from '@aws-sdk/client-bedrock-runtime';
const client = new BedrockRuntimeClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
const GUARDRAIL_ID = process.env.BEDROCK_GUARDRAIL_ID!;
const GUARDRAIL_VERSION = process.env.BEDROCK_GUARDRAIL_VERSION ?? '1'; // never 'DRAFT' in prod
// Step 1: pre-validate user input before any processing
async function validateInput(userText: string): Promise<{ safe: boolean; reason?: string }> {
const result = await client.send(new ApplyGuardrailCommand({
guardrailIdentifier: GUARDRAIL_ID,
guardrailVersion: GUARDRAIL_VERSION,
source: 'INPUT',
content: [{ text: { text: userText } }],
}));
if (result.action === 'GUARDRAIL_INTERVENED') {
// Parse which policy triggered for audit logging
const assessment = result.assessments?.[0];
let reason = 'content_policy';
if (assessment?.topicPolicy?.topics?.some(t => t.action === 'BLOCKED')) {
reason = 'topic_policy';
} else if (assessment?.sensitiveInformationPolicy?.piiEntities?.length) {
reason = 'pii_policy';
} else if (assessment?.wordPolicy?.customWords?.length) {
reason = 'word_policy';
}
return { safe: false, reason };
}
return { safe: true };
}
// Step 2: call model with guardrail attached to output
async function converseGuarded(modelId: string, userMessage: string) {
const response = await client.send(new ConverseCommand({
modelId,
messages: [{ role: 'user', content: [{ text: userMessage }] }],
guardrailConfig: {
guardrailIdentifier: GUARDRAIL_ID,
guardrailVersion: GUARDRAIL_VERSION,
trace: 'ENABLED',
},
inferenceConfig: { maxTokens: 1024, temperature: 0.7 },
}));
// MUST check stopReason — output.message.content is EMPTY when guardrail fires
if (response.stopReason === 'guardrail_intervened') {
return { blocked: true, text: 'Response blocked by content policy.' };
}
const text = response.output?.message?.content
?.filter(b => 'text' in b)
.map(b => (b as { text: string }).text)
.join('') ?? '';
return { blocked: false, text };
}
For multimodal MCP tools that pass images as tool arguments, set applyGuardrailsToImages: true in the guardrail configuration in the Bedrock console. This enables visual content moderation via the ImageBlock content type in ApplyGuardrail requests, covering NSFW content, violence, and other image-based policy categories.
See the detailed guide: MCP Server Bedrock Guardrails — content filtering, BLOCKED responses, image moderation.
Pattern 5 — InvokeModel vs Converse: when to drop to the lower level
The Converse API is the right default for MCP tool handlers that call foundation models — it eliminates per-model serialization code, provides typed streaming events, and enables tool use across model families without model-specific prompt engineering. But three capability categories are only accessible through InvokeModelCommand: Claude's extended thinking, Titan embedding dimension control, and Llama's raw prompt format.
Claude extended thinking (budget_tokens) is not exposed in Converse's InferenceConfiguration. Extended thinking requires sending a thinking object in the Claude-specific request body with a budget_tokens value between 1,024 and 32,000. When using extended thinking, the response includes a thinking content block that contains the model's internal reasoning — this block must be filtered before returning the response to the MCP client, and it must be included in the messages history if the conversation continues across turns (otherwise follow-up quality degrades significantly).
Titan embedding models accept an embeddingConfig.outputEmbeddingLength parameter that controls the dimensionality of the output vector — useful for reducing storage in vector databases at the cost of some recall quality. This parameter is not available in Converse.
Llama raw prompt format uses model-specific special tokens (<|begin_of_text|>, <|start_header_id|>, <|end_header_id|>, <|eot_id|>) that Converse's normalization layer strips or transforms. When you need exact control over Llama's prompt structure — for example, when loading a fine-tuned Llama model that expects a specific token sequence — InvokeModel with the raw format is required.
The critical production detail for InvokeModelWithResponseStreamCommand is that streaming chunks do not align with JSON object boundaries. The PayloadPart events carry raw bytes that must be decoded and buffered — a single JSON object in the response can arrive split across multiple chunks, and a single chunk can contain the end of one JSON object and the start of another. The correct pattern is to append decoded bytes to a buffer and attempt JSON.parse(buffer) after each append, catching parse errors as incomplete-chunk signals.
import {
BedrockRuntimeClient,
InvokeModelCommand,
InvokeModelWithResponseStreamCommand,
} from '@aws-sdk/client-bedrock-runtime';
const client = new BedrockRuntimeClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
// Claude extended thinking via InvokeModel (not available in Converse)
async function claudeExtendedThinking(prompt: string, budgetTokens = 8000): Promise<{
thinking: string;
answer: string;
}> {
const requestBody = {
anthropic_version: 'bedrock-2023-05-31',
max_tokens: budgetTokens + 2048, // max_tokens must exceed budget_tokens
thinking: { type: 'enabled', budget_tokens: budgetTokens },
messages: [{ role: 'user', content: prompt }],
};
const response = await client.send(new InvokeModelCommand({
modelId: 'anthropic.claude-3-5-sonnet-20241022-v2:0', // on-demand ID for InvokeModel
contentType: 'application/json',
accept: 'application/json',
body: new TextEncoder().encode(JSON.stringify(requestBody)),
}));
const result = JSON.parse(new TextDecoder().decode(response.body));
// Filter thinking blocks — must be excluded from MCP client response
// but INCLUDED in messages history for multi-turn quality
const thinkingText = result.content
.filter((b: { type: string }) => b.type === 'thinking')
.map((b: { thinking: string }) => b.thinking)
.join('');
const answerText = result.content
.filter((b: { type: string }) => b.type === 'text')
.map((b: { text: string }) => b.text)
.join('');
return { thinking: thinkingText, answer: answerText };
}
// Streaming InvokeModel — buffer chunks, attempt JSON parse after each
async function invokeModelStreaming(modelId: string, requestBody: object): Promise<unknown> {
const response = await client.send(new InvokeModelWithResponseStreamCommand({
modelId,
contentType: 'application/json',
accept: 'application/json',
body: new TextEncoder().encode(JSON.stringify(requestBody)),
}));
const decoder = new TextDecoder();
let buffer = '';
for await (const event of response.body ?? []) {
if (event.chunk?.bytes) {
buffer += decoder.decode(event.chunk.bytes, { stream: true });
try {
return JSON.parse(buffer); // succeeds when complete JSON object accumulated
} catch {
// Incomplete chunk — continue accumulating
}
}
}
return JSON.parse(buffer); // final parse attempt after stream ends
}
Pin BedrockRuntimeClient instances at module scope — instantiating the client per request adds 50–150 ms of TCP connection setup latency. The AWS SDK reuses HTTP connections across requests when the client is a singleton; per-request instantiation bypasses this connection pooling.
See the detailed guide: MCP Server Bedrock InvokeModel vs Converse — payload formats, streaming, model families.
Failure modes reference
| Service | Symptom | Root cause | Fix |
|---|---|---|---|
| Converse API | Empty string returned from tool handler | stopReason: 'max_tokens' — response truncated, code silently joined empty content |
Check stopReason before reading content; increase maxTokens or handle partial response |
| Converse API | Hard 429 under load with no failover | On-demand model ID — bounded to single-region quota | Switch to cross-region inference profile ID (us. prefix) |
| Converse API | Silent wrong temperature behavior on Titan | Titan Premier ignores topP when temperature is 0 |
Validate InferenceConfiguration per model family; do not share config across families |
| Converse streaming | Tool use event never handled | stopReason: 'tool_use' not checked; handler only looks for 'end_turn' |
Dispatch on all stopReason values; return tool results in next turn for 'tool_use' |
| Bedrock Agents | ValidationException on alias ID |
Passing literal string 'DRAFT' for production invocations |
Use published alias ID (alphanumeric, 10 chars); create with CreateAgentAliasCommand |
| Bedrock Agents | Agent returns empty answer | Stream not fully drained — handler reads first event only | Drain full async iterator; final answer is in chunk.bytes event not first event |
| Bedrock Agents | Action group receives wrong session context | sessionState.sessionAttributes passed non-string values (silently dropped) |
Serialize all values to strings before setting sessionAttributes |
| Bedrock Agents | Agent loops without terminating | Action group Lambda returns malformed response body (object instead of JSON string) | Return responseBody: { 'application/json': { body: JSON.stringify(result) } } — body must be a string |
| Knowledge Base | ResourceNotFoundException on RetrieveAndGenerate |
Used cross-region profile ID for modelArn — not supported by this API |
Use full model ARN: arn:aws:bedrock:REGION::foundation-model/MODEL_ID |
| Knowledge Base | Poor retrieval quality | Fixed-size chunking on long technical documents — section context lost at chunk boundary | Switch to hierarchical chunking; re-sync all data sources after chunking strategy change |
| Knowledge Base | RetrievalFilter returns no results |
Attribute not present in .metadata.json sidecar or sidecar not co-located with S3 object |
Ensure docs/guide.pdf.metadata.json exists at same S3 prefix as docs/guide.pdf; re-sync data source |
| Guardrails | Empty string returned when guardrail fires | output.message.content accessed without checking stopReason === 'guardrail_intervened' |
Always check stopReason first; return user-facing policy message when guardrail intervened |
| Guardrails | Blocking behavior changes unexpectedly | Using DRAFT version — config edit immediately alters all live invocations |
Create published version with CreateGuardrailVersionCommand; pin to numeric version in production |
| InvokeModel | JSON parse error on streaming response | Parsing each chunk independently — chunks do not align with JSON object boundaries | Accumulate bytes in buffer; attempt JSON.parse(buffer) after each append; catch errors as incomplete-chunk signal |
| InvokeModel | 50–150 ms added latency per tool call | BedrockRuntimeClient instantiated per request — no connection reuse |
Pin client at module scope; AWS SDK reuses HTTP connections when client is a singleton |
| InvokeModel (Claude) | Extended thinking follow-up quality degrades | Thinking blocks filtered from history before next turn | Include thinking blocks in messages history for multi-turn; only filter from MCP client response |
Composing Bedrock services in a single MCP tool
The five patterns are composable. An MCP tool that answers questions from a private document corpus with content policy enforcement combines Knowledge Base (for retrieval), Guardrails (at input and output), and Converse (for synthesis with a model not available in RetrieveAndGenerate) — all within a single tool handler. The sequence is: validate user input with ApplyGuardrailCommand (input gate); call RetrieveCommand to get source chunks (bypassing RetrieveAndGenerate's limited model list); validate retrieved chunks with ApplyGuardrailCommand (prevents policy-violating content entering the model prompt); assemble a prompt with chunks and call ConverseCommand with guardrailConfig (output gate); extract citations from the retrieved references and return with the model's answer.
The same composability applies to Bedrock Agents for agentic workflows: Agents internally calls Knowledge Base data sources and Lambda action groups, so attaching a guardrail to the Agent invocation via the Bedrock console applies content policy across all the agent's sub-calls without requiring individual guardrail calls in each action group Lambda.
For monitoring, MCP server health checks for Bedrock-backed tools should verify two layers: that the Bedrock client can reach the service (a lightweight ConverseCommand with maxTokens: 1 on a fast model like Haiku), and that the downstream resources (knowledge base, agent alias, guardrail version) still exist and are active. A tool that passes the API health check but references a deleted guardrail version will fail at first real invocation.
For cost observability, log response.usage.inputTokens and response.usage.outputTokens from every Converse response alongside the modelId used. Bedrock pricing differs by model family and varies by 10–30× across the catalog — the same workload costs dramatically different amounts on Claude Sonnet vs Claude Haiku vs Titan. See MCP server LLM cost tracking for token accounting patterns that aggregate costs per tool invocation.
Related guides
- MCP Server Bedrock Converse API — model IDs, InferenceConfiguration, streaming, throttling
- MCP Server Bedrock Agents — action groups, session state, trace events, agent aliases
- MCP Server Bedrock Knowledge Base — retrieveAndGenerate, citations, chunking, S3 sources
- MCP Server Bedrock Guardrails — content filtering, BLOCKED responses, image moderation
- MCP Server Bedrock InvokeModel vs Converse — payload formats, streaming, model families
- MCP Server AWS Bedrock — getting started with foundation models
- MCP Server Health Check — monitoring, probes, uptime
- MCP Server Error Handling — timeouts, retries, fallbacks
- AWS Lambda for MCP Servers — five production patterns
- AWS DynamoDB for MCP Servers — five production patterns