Bridge AWS Bedrock to Nasuni via MCP
Connect AWS Bedrock to Nasuni AI Activate using the MCP tool-use pattern.
Introduction
30 Second Summary
Imagine asking your company's file storage a plain English question like "Find the Q4 earnings document" and getting an instant, accurate answer. That's the kind of natural-language file access that AI makes possible today.
In this project, you will build a Python CLI application that bridges AWS Bedrock to Nasuni AI Activate using the Model Context Protocol (MCP). You'll type natural language questions, and the app orchestrates the full round-trip: Bedrock decides which tool to call, the MCP client executes it against your governed file data, and Bedrock returns a final answer.
What You'll Build
You type a question about your files into the terminal, watch the app call the right Nasuni tool automatically, and get a natural language answer summarizing what it found in your governed file storage.
By the end of this project, you'll have:
- A working interactive chat loop where you ask natural language questions about your Nasuni-stored files and get AI-powered answers back.
- Full visibility into the tool-use orchestration happening behind the scenes: which MCP tools Bedrock chose, what parameters it extracted from your question, and how the result flows back.
- A transferable integration pattern for connecting any third-party MCP server to AWS Bedrock using the Converse API tool-use loop.
- Secret Mission: Add multi-turn conversation memory so Bedrock remembers previous file lookups and can build on them across questions.
Are there any prerequisites?
You'll need an AWS account with Bedrock access (Claude Sonnet 4.6 enabled), a Nasuni AI Activate endpoint URL with SSO credentials, Python 3.10+ installed, and the AWS CLI configured with credentials.
Nasuni AI Activate is currently in invite-only preview. You'll need confirmed access to an endpoint before starting this project.
Before We Start
Before you write any code, take a moment to lock in what you're building and why it matters. This PoC is about answering a specific feasibility question, so naming that question up front keeps you focused.
Set Up Your Python Project and Dependencies
To bridge AWS Bedrock to Nasuni, you need two SDKs: the MCP Python SDK (to talk to the Nasuni MCP server) and boto3 (to talk to Bedrock).
In this step you will set up a clean Python project using uv as the package manager, install both SDKs, and confirm everything is wired up before writing any bridge code.
In this step, get ready to:
- Install uv and scaffold a new Python project directory.
- Install the MCP Python SDK v2 and boto3.
- Verify both packages import and confirm your AWS credentials reach Bedrock.
Install uv and create your project directory
uv is a fast Python package manager that handles virtual environments and dependency tracking in one tool. You will use it to create an isolated project and manage your packages.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check if uv is already installed by running this command:
uv --version
✔️ I see a version number
uv is ready to use. Move on to the next action.
ⓧ Command not found
Install uv by running the appropriate command for your operating system.
macOS
- Install uv by running this command:
curl -LsSf https://astral.sh/uv/install.sh | sh
- Close and reopen your terminal so the uv command is available.
- Confirm it installed by running uv --version again.
Windows
- Install uv by running this command in PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
- Close and reopen PowerShell so the uv command is available.
- Confirm it installed by running uv --version again.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project by running this command:
uv init bedrock-nasuni-bridge
- Move into your new project directory by running:
cd bedrock-nasuni-bridge
- Confirm the project was created by running:
ls
You should see a pyproject.toml file listed in the output. This file is where uv tracks your project metadata and dependencies.
Don't see pyproject.toml?
Make sure you ran uv init bedrock-nasuni-bridge from your Desktop, not from inside an existing project folder. If a pyproject.toml already existed, uv will error instead of overwriting it.
help me troubleshoot why uv init didn't create my project files
Install the MCP Python SDK and boto3
The mcp package gives you the MCP client that connects to Nasuni. The boto3 package is the AWS SDK that lets you call the Bedrock Converse API.
- Install both packages by running this command:
uv add mcp boto3
What does this command do?
uv add does three things at once: it adds the packages to your pyproject.toml dependencies, creates a virtual environment (.venv), and installs the packages into that environment.
You will see output showing packages being resolved, downloaded, and installed. This may take 10-30 seconds the first time.
- Confirm both packages are listed in your pyproject.toml by running:
cat pyproject.toml
You should see mcp and boto3 listed under [project.dependencies].
Seeing a resolution error?
The MCP Python SDK requires Python 3.10 or higher. If you see a version conflict, check your Python version with python3 --version. You need 3.10 or above.
help me fix a uv dependency resolution error when installing mcp and boto3
Verify imports and confirm AWS credentials
Before writing any bridge code, you need to confirm two things: both packages import correctly, and your AWS credentials can reach Bedrock.
- Test that both packages import by running this command:
uv run python3 -c "from mcp.client import Client; import boto3; print('Ready')"
Before you run this, what do you expect to see if both packages installed correctly?
You should see Ready printed with no errors. This confirms the MCP Client class and boto3 are both importable.
- Now confirm your AWS credentials can reach Bedrock by running:
uv run python3 -c "import boto3; client = boto3.client('bedrock', region_name='us-east-1'); models = client.list_foundation_models(byProvider='Anthropic'); print(f'Found {len(models[\"modelSummaries\"])} Anthropic models')"
You should see a message like Found X Anthropic models where X is a number. This proves your AWS credentials are configured and Bedrock is accessible in us-east-1.
Seeing a credentials error?
If you see NoCredentialsError or InvalidClientTokenId, your AWS CLI is not configured. Run aws configure and enter your Access Key ID, Secret Access Key, and set the region to us-east-1.
If you see AccessDeniedException, your IAM user may not have Bedrock permissions. Make sure your account has the bedrock:ListFoundationModels permission enabled.
help me fix my AWS credentials so I can access Bedrock
- Open VS Code on your project. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the bedrock-nasuni-bridge folder, and click Open.
You should see pyproject.toml in the VS Code Explorer sidebar. Your project is ready for code.
Your environment is ready. Next up, you will connect to Nasuni AI Activate and discover what tools it exposes for AI-driven file access.
Connect to Nasuni AI Activate and Discover Tools
Your packages are installed and your AWS credentials are confirmed. Now you need to answer the first real question of any MCP integration: what tools does the server actually expose?
In this step, you will connect to your Nasuni AI Activate endpoint using the MCP Python SDK's OAuth authentication flow. Once connected, you will discover the available tools and test one directly to prove the round-trip works.
In this step, get ready to:
- Write an OAuth/SSO authentication provider that connects to your Nasuni AI Activate endpoint.
- Discover and print all available MCP tools with their schemas.
- Call one tool directly to confirm the connection works end-to-end.
Set up the OAuth authentication provider
Nasuni AI Activate uses SSO to authenticate access to your governed file data. The MCP Python SDK handles the full OAuth 2.1 flow (discovery, dynamic registration, authorization redirect, code exchange, and token replay) through its OAuthClientProvider class.
- Create a new file called bridge.py in your project folder by running:
touch bridge.py
- Confirm the file exists by running:
ls
You should see bridge.py listed in the output alongside your pyproject.toml.
- Open bridge.py in VS Code and add the imports and configuration by pasting this code:
import asyncio
import json
import sys
import webbrowser
import boto3
from mcp.client import Client
from mcp.client.auth import OAuthClientProvider, OAuthClientMetadata, InMemoryTokenStorage, AuthorizationCodeResult
from mcp.client.streamable_http import streamable_http_client
import httpx2
# --- Configuration ---
NASUNI_MCP_URL = "[[NASUNI_URL=\"your Nasuni AI Activate endpoint URL\"]]"
BEDROCK_MODEL_ID = "anthropic.claude-sonnet-4-6"
AWS_REGION = "us-east-1"
MAX_TOOL_RECURSIONS = 5
What does this code do?
- The imports pull in the MCP client, its OAuth authentication classes, the Streamable HTTP transport, and httpx2 (the HTTP client library that MCP SDK v2 uses under the hood).
- NASUNI_MCP_URL is your personal Nasuni AI Activate endpoint. The other constants are for Bedrock, which you will wire up in the next step.
- MAX_TOOL_RECURSIONS caps how many tool calls Bedrock can chain in a single query. This prevents infinite loops.
Now add the OAuth provider function below the configuration. This function builds the authentication flow that will open your browser for SSO login when you first connect.
- Add the OAuth setup function below the MAX_TOOL_RECURSIONS line by pasting this code:
# --- OAuth/SSO Setup ---
async def create_oauth_provider(server_url: str) -> OAuthClientProvider:
"""Create an OAuth provider for Nasuni SSO authentication."""
async def redirect_handler(authorization_url: str) -> None:
print(f"\nOpening browser for SSO login...")
webbrowser.open(authorization_url)
async def callback_handler() -> AuthorizationCodeResult:
print("Waiting for SSO callback...")
# In a real app, you'd run a local callback server here
# For the PoC, this waits for the redirect
auth_code = input("Paste the authorization code from the redirect: ")
return AuthorizationCodeResult(code=auth_code, state=None, iss=None)
return OAuthClientProvider(
server_url=server_url,
client_metadata=OAuthClientMetadata(
client_name="Bedrock-Nasuni Bridge PoC",
redirect_uris=["http://localhost:3030/callback"],
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
),
storage=InMemoryTokenStorage(),
redirect_handler=redirect_handler,
callback_handler=callback_handler,
)
What does the OAuth provider do?
- redirect_handler opens your browser to the Nasuni SSO login page when authentication is needed.
- callback_handler waits for you to paste the authorization code from the redirect URL after you sign in. In a production app, a local server would capture this automatically.
- OAuthClientProvider ties everything together: it handles token exchange, refresh, and replay on subsequent requests so you only sign in once per session.
- InMemoryTokenStorage stores your OAuth tokens in memory for the lifetime of the script. Tokens are lost when the script exits.
- Save bridge.py.
Seeing a red underline on httpx2?
Your editor might not recognize httpx2 as an installed package. It is a dependency of the mcp package and was installed automatically when you ran uv add mcp. The script will run fine. If you want to confirm, run:
uv run python3 -c "import httpx2; print(httpx2.__version__)"
You should see a version number printed. help me fix import errors in my MCP bridge script
Connect and discover available tools
With the authentication provider ready, you can now connect to Nasuni and ask it what tools are available. This is the list_tools() call that every MCP client makes first.
- Add the main function and entry point below your create_oauth_provider function by pasting this code:
async def main():
print("Bedrock-Nasuni MCP Bridge")
print("=" * 40)
# Create OAuth provider (strips /mcp path for base URL)
oauth = await create_oauth_provider(NASUNI_MCP_URL.replace("/mcp", ""))
# Connect to Nasuni with OAuth over Streamable HTTP
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
transport = streamable_http_client(NASUNI_MCP_URL, http_client=http_client)
async with Client(transport) as mcp_client:
# Discover available tools
tool_list = await mcp_client.list_tools()
mcp_tools = tool_list.tools
print(f"\nConnected to Nasuni. Available tools:")
for tool in mcp_tools:
print(f" - {tool.name}: {tool.description}")
print(f" Schema: {json.dumps(tool.inputSchema, indent=2)}")
if __name__ == "__main__":
asyncio.run(main())
What is happening in main?
- The OAuth provider is created with your Nasuni base URL (the /mcp path is stripped for OAuth discovery).
- httpx2.AsyncClient is the HTTP client that carries your OAuth credentials on every request.
- streamable_http_client creates the MCP transport layer, and Client(transport) gives you the full MCP client connected to Nasuni.
- list_tools() asks the server what tools it exposes. Each tool has a name, description, and JSON schema describing its expected inputs.
Before you run this, what do you think you will see? Will it print tool names right away, or will something else happen first?
- Save bridge.py and run the script:
uv run python3 bridge.py
What should I see?
Your browser will open to the Nasuni SSO login page. Sign in with your credentials. After authentication, you will be redirected to a URL containing an authorization code.
Copy the authorization code from the redirect URL and paste it into the terminal when prompted. The script will then print a list of available tools with their names, descriptions, and input schemas.
Connection refused or timeout?
- Double-check your NASUNI_MCP_URL value in bridge.py. It should be the full endpoint URL you received when your Nasuni AI Activate access was provisioned.
- Make sure the URL ends with /mcp (the standard MCP path). The OAuth provider strips this for discovery, so it must be present.
- If the browser opens but SSO fails, confirm you are using the same credentials that were granted access to AI Activate.
help me debug my Nasuni MCP connection
Test a direct tool call
Seeing the tool list proves discovery works. Now you need to prove the full round-trip by actually calling one of those tools with hardcoded arguments.
- Look at the tool list your script just printed. Pick a tool that looks like it lists folder contents or file metadata (the exact name depends on your Nasuni configuration).
- In bridge.py, find the last line of the for tool in mcp_tools: loop:
print(f" Schema: {json.dumps(tool.inputSchema, indent=2)}")
- Add the following test code directly below that line (still inside the async with Client block). Replace folder_contents with the actual tool name from your list if it differs:
# Temporary test - call a tool directly
result = await mcp_client.call_tool("folder_contents", {"path": "/"})
for block in result.content:
if hasattr(block, "text"):
print(f"\nTest result: {block.text[:500]}")
What does call_tool do?
call_tool() sends a request to the Nasuni MCP server to execute the named tool with the given arguments. The result comes back as a CallToolResult containing a list of content blocks. Each block with a text attribute holds part of the tool's response.
By calling a tool with hardcoded arguments (like listing the root folder), you confirm that authentication, transport, and tool execution all work before adding the complexity of Bedrock.
- Save bridge.py and run the script again:
uv run python3 bridge.py
After authenticating (you may need to paste the code again), you should see the tool list followed by the test result showing file or folder data from your Nasuni storage.
Seeing a tool not found error?
- The tool name in your call_tool() must match exactly what appeared in the tool list output. Tool names are case-sensitive.
- Check the input schema for the tool you chose. If it expects different parameters than {"path": "/"}, adjust your arguments to match the schema.
- If you see a permissions error, your SSO account may not have access to the root path. Try a path you know you have access to.
help me fix my Nasuni tool call error
You have confirmed the full round-trip works. Now remove the temporary test code so your script is clean for the next step.
- In bridge.py, delete the four lines you just added (the comment and the three lines below it starting with result = await mcp_client.call_tool).
- Save bridge.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved bridge.py with the temporary test code removed.
ⓧ I'd like to double check the full code
Your bridge.py should contain these sections in order. Scroll up and confirm each is present:
- Imports block (asyncio, json, sys, webbrowser, boto3, mcp imports, httpx2).
- Configuration section with NASUNI_MCP_URL, BEDROCK_MODEL_ID, AWS_REGION, and MAX_TOOL_RECURSIONS.
- The create_oauth_provider function with redirect_handler and callback_handler nested inside.
- The main function that creates the OAuth provider, connects via httpx2 and Streamable HTTP, calls list_tools(), and prints each tool.
- The if __name__ == "__main__": entry point.
- No temporary test code remaining (the call_tool lines should be removed).
You have connected to Nasuni AI Activate, discovered its available tools, and proved the round-trip works with a direct tool call. Next up, you will teach Bedrock about these tools so it can decide when to call them based on your natural language questions.
Wire Up Bedrock Converse API with Tool Definitions
You can now connect to Nasuni and see exactly which tools are available.
The problem is that Bedrock has no idea those tools exist. Before the model can decide to call a tool, it needs the full definition: name, description, and input schema. You'll translate the MCP tool schemas into Bedrock's toolConfig format so the model can reason about when to use each one.
In this step, get ready to:
- Convert MCP tool schemas into Bedrock's toolSpec format.
- Send a user query to Bedrock with tool definitions attached.
- Detect when Bedrock responds with a tool-use request.
Convert MCP tool schemas to Bedrock format
Bedrock's Converse API expects tool definitions in a specific structure called toolSpec. Each tool needs a name, description, and a JSON input schema. The MCP tools you discovered already have all three fields. You just need a function that reshapes them.
- In bridge.py, add these two new functions between the end of create_oauth_provider and the async def main() block:
# --- MCP Tool Schema Conversion ---
def mcp_tools_to_bedrock_format(mcp_tools: list) -> list:
"""Convert MCP tool definitions to Bedrock toolConfig format."""
bedrock_tools = []
for tool in mcp_tools:
bedrock_tools.append({
"toolSpec": {
"name": tool.name,
"description": tool.description or "",
"inputSchema": {
"json": tool.inputSchema
},
}
})
return bedrock_tools
# --- Bedrock Interaction ---
def send_to_bedrock(bedrock_client, messages: list, tools: list) -> dict:
"""Send a conversation to Bedrock with tool definitions."""
response = bedrock_client.converse(
modelId=BEDROCK_MODEL_ID,
messages=messages,
toolConfig={"tools": tools},
)
return response
What do these functions do?
You added two helper functions that bridge the gap between MCP and Bedrock:
- mcp_tools_to_bedrock_format loops through the MCP tools you discovered and reshapes each into Bedrock's toolSpec structure. The model uses the description and inputSchema to decide when a tool is relevant and what arguments to pass.
- send_to_bedrock wraps the Converse API call. It sends the full conversation plus the tool definitions in one request. Bedrock reads both to decide whether to answer directly or request a tool call.
Send a test query and detect tool-use requests
Now you'll wire these functions into your main() function. You need a Bedrock runtime client, the converted tool list, and a test query to trigger a tool-use response.
- In bridge.py, find this line inside your main() function:
oauth = await create_oauth_provider(NASUNI_MCP_URL.replace("/mcp", ""))
- Add this line directly below it to create the Bedrock runtime client:
bedrock_client = boto3.client("bedrock-runtime", region_name=AWS_REGION)
- Next, find and delete this line from the tool listing loop (the schema print is no longer needed):
print(f" Schema: {json.dumps(tool.inputSchema, indent=2)}")
- Finally, replace the rest of the code after the tool listing for loop (everything from the loop's end to the bottom of main()) with this test query block:
bedrock_tools = mcp_tools_to_bedrock_format(mcp_tools)
# Test query
query = "What files are available?"
messages = [{"role": "user", "content": [{"text": query}]}]
response = send_to_bedrock(bedrock_client, messages, bedrock_tools)
print(f"\nStop reason: {response['stopReason']}")
print(f"Response: {json.dumps(response['output']['message'], indent=2)}")
What does this code do?
- boto3.client("bedrock-runtime", ...) creates a client for the Bedrock Runtime service. This is the service that actually runs model inference.
- mcp_tools_to_bedrock_format(mcp_tools) converts the Nasuni tools you discovered into the format Bedrock expects.
- The messages list holds the conversation in Bedrock's format. Each message has a role and content array.
- The printed stopReason tells you whether Bedrock answered directly (end_turn) or wants to call a tool (tool_use).
- Save bridge.py.
Before you run this, think about what you expect to see. You asked "What files are available?" and gave Bedrock a list of Nasuni file tools. Do you think Bedrock will answer directly, or ask to call a tool?
- Run the script:
python3 bridge.py
After authenticating with SSO, you should see a stopReason of tool_use. The response JSON contains a toolUse block with the tool name Bedrock chose, a toolUseId, and the input parameters it extracted from your question.
Why does Bedrock request a tool call?
Bedrock sees your question ("What files are available?") and compares it against the tool descriptions you provided. It recognizes that a Nasuni file-listing tool is the best way to answer, so it returns a tool_use stop reason instead of guessing an answer.
The toolUseId is a unique identifier that links the request to the result you'll send back later. Bedrock is essentially saying: "I need this tool's output before I can answer you."
Seeing an error instead of tool_use?
- If you see an access denied error, confirm your AWS CLI credentials have Bedrock access and that Claude Sonnet 4.6 is enabled in your us-east-1 region.
- If you see a validation error about toolConfig, double check that mcp_tools_to_bedrock_format returns a list of objects with toolSpec at the top level.
- If the SSO login fails before reaching Bedrock, check that your Nasuni AI Activate endpoint URL is correct in NASUNI_MCP_URL.
Help me debug my Bedrock Converse API tool-use response
✔️ Awesome, I've got everything!
Great. Double check you've saved bridge.py and confirmed the tool_use stop reason in your terminal output.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these sections is present in your bridge.py, in this order:
- Imports (asyncio, json, sys, webbrowser, boto3, MCP imports, httpx2).
- Configuration section (NASUNI_MCP_URL, BEDROCK_MODEL_ID, AWS_REGION, MAX_TOOL_RECURSIONS).
- OAuth/SSO Setup section (create_oauth_provider function).
- MCP Tool Schema Conversion section (mcp_tools_to_bedrock_format function).
- Bedrock Interaction section (send_to_bedrock function).
- main() function with bedrock_client creation, tool conversion, and the test query block.
- if __name__ == "__main__": entry point at the bottom.
Bedrock is now asking for a tool call, but nobody is executing it. Next up, you'll close the loop by routing that request to Nasuni and feeding the result back to Bedrock for a final answer.
Complete the Tool-Use Loop
Right now your script asks Bedrock a question, and Bedrock says "I'd like to call this Nasuni tool with these parameters." But nobody actually executes that tool call. The conversation stalls.
In this step you will close the loop. You will route Bedrock's tool requests to Nasuni via MCP, capture the response, feed it back to Bedrock in the format it expects, and let Bedrock produce a final natural language answer.
In this step, get ready to:
- Build an orchestration function that loops until Bedrock has a final answer.
- Execute MCP tool calls and format the results for Bedrock.
- Test the full round-trip with a real file query.
Create the orchestration function
The tool-use pattern is a conversation loop. You send a message, Bedrock responds, and you check whether it wants to call a tool or is done talking. If it is done, you extract the text. If it wants a tool, you execute it and send the result back.
- In bridge.py, find the send_to_bedrock function. Directly below it, add a new section comment and the first part of the run_query function by pasting this code:
# --- Main Orchestration ---
async def run_query(mcp_client: Client, bedrock_client, query: str, tools: list) -> str:
"""Run a single query through the Bedrock-Nasuni bridge."""
messages = [{"role": "user", "content": [{"text": query}]}]
recursion_count = 0
while recursion_count < MAX_TOOL_RECURSIONS:
# Send to Bedrock
response = send_to_bedrock(bedrock_client, messages, tools)
stop_reason = response["stopReason"]
assistant_message = response["output"]["message"]
# Add assistant response to conversation
messages.append(assistant_message)
if stop_reason == "end_turn":
# Final response - extract text
final_text = ""
for block in assistant_message["content"]:
if "text" in block:
final_text += block["text"]
return final_text
What does this code do?
This is the top half of your orchestration function. It wraps a user query into Bedrock's message format, then enters a loop.
- Each iteration sends the current conversation to Bedrock and checks the stopReason field.
- If stopReason is end_turn, Bedrock is done. The function extracts the text blocks and returns the answer.
- MAX_TOOL_RECURSIONS prevents infinite loops if the model keeps requesting tools without settling on a final answer.
Execute tool calls and format results
When Bedrock returns stopReason: "tool_use", its response contains one or more toolUse blocks. Each block names a Nasuni tool and provides the arguments Bedrock extracted from your question. You need to call each tool, collect the results, and send them back.
- Directly below the return final_text line you just added, paste the tool execution branch:
elif stop_reason == "tool_use":
# Process tool calls
tool_results = []
for block in assistant_message["content"]:
if "toolUse" in block:
tool_use = block["toolUse"]
tool_name = tool_use["name"]
tool_input = tool_use["input"]
tool_use_id = tool_use["toolUseId"]
print(f" [Tool Call] {tool_name}({json.dumps(tool_input)})")
# Call the tool on Nasuni via MCP
result = await mcp_client.call_tool(tool_name, tool_input)
# Extract text content from the result
result_text = ""
for content_block in result.content:
if hasattr(content_block, "text"):
result_text += content_block.text
print(f" [Tool Result] {result_text[:200]}...")
tool_results.append({
"toolResult": {
"toolUseId": tool_use_id,
"content": [{"text": result_text}],
}
})
What does this code do?
- It iterates through every toolUse block in Bedrock's response. A single response can request multiple tool calls.
- For each tool, it extracts the name, input arguments, and toolUseId (Bedrock uses this ID to match results back to requests).
- client.call_tool(tool_name, tool_input) sends the request to Nasuni over MCP. The result comes back as content blocks.
- Each result is wrapped in the exact toolResult format Bedrock expects: the matching toolUseId plus the text content.
- Now paste the final lines of the function directly below the closing }) of tool_results.append:
# Add tool results as a user message
messages.append({"role": "user", "content": tool_results})
recursion_count += 1
else:
return f"Unexpected stop reason: {stop_reason}"
return "Maximum tool recursions reached."
Why send tool results as a user message?
Bedrock's Converse API models the conversation as alternating user and assistant messages. After the assistant requests a tool, the next message must come from the "user" role carrying the tool results. This lets the model inspect the data and either request another tool or produce its final answer.
Test the complete round-trip
The run_query function is ready. Now update main() to use it instead of the raw Bedrock call you had before.
- In the main() function at the bottom of bridge.py, find this block of code:
# Test query
query = "What files are available?"
messages = [{"role": "user", "content": [{"text": query}]}]
response = send_to_bedrock(bedrock_client, messages, bedrock_tools)
print(f"\nStop reason: {response['stopReason']}")
print(f"Response: {json.dumps(response['output']['message'], indent=2)}")
- Replace that entire block with this:
# Test the full loop
answer = await run_query(mcp_client, bedrock_client, "What files are in the root folder?", bedrock_tools)
print(f"\nAnswer: {answer}")
What changed?
Instead of manually building messages and printing raw JSON, you now call run_query which handles the entire orchestration. It sends your question to Bedrock, executes any tool calls against Nasuni, feeds results back, and returns the final natural language answer.
- Save bridge.py.
Before you run this, what do you expect to see? Will Bedrock respond immediately, or will there be intermediate output showing tool calls?
- Run the script:
python3 bridge.py
After authenticating with SSO, you should see the full loop in action. Bedrock requests a tool call (printed with the [Tool Call] prefix), Nasuni returns file data (printed with [Tool Result]), and then Bedrock produces a natural language answer summarizing what it found.
Seeing an authentication error?
- If the SSO login fails, confirm your Nasuni AI Activate endpoint URL is correct in the NASUNI_MCP_URL variable.
- If Bedrock returns an access error, verify your AWS credentials are configured and that Claude Sonnet 4.6 is enabled in your Bedrock console for the us-east-1 region.
- If you see "Maximum tool recursions reached" instead of an answer, the tool may be returning empty results. Check that your Nasuni account has files in the root folder.
Help me debug my Bedrock-Nasuni bridge tool-use loop
✔️ Awesome, I've got everything!
Great. Double check you have saved bridge.py and that your output shows the complete loop.
ⓧ I'd like to double check the full code
Scroll up through your file and confirm each section is present in this order:
- The imports and configuration block at the top.
- The create_oauth_provider function.
- The mcp_tools_to_bedrock_format function.
- The send_to_bedrock function.
- The # --- Main Orchestration --- comment followed by the run_query function containing the while loop with end_turn, tool_use, and else branches.
- The main() function calling run_query with the query "What files are in the root folder?".
You have proven the core integration pattern works end-to-end. Bedrock reasons about your question, picks the right Nasuni tool, and uses real file data to give you an answer. Next up, you will wrap this in an interactive chat loop so you can explore your file data freely.
Build the Interactive Chat Loop
Your bridge now handles the full round-trip: Bedrock picks a tool, Nasuni executes it, and Bedrock produces a natural language answer.
A single hardcoded query proves the pattern works, but it does not let you explore boundaries. In this step, you will replace that one-shot test with an interactive chat loop so you can rapidly fire different questions at your file data and see what the integration can handle.
In this step, get ready to:
- Replace the hardcoded test query with an interactive input loop.
- Observe the built-in orchestration logging that traces each tool call.
- Verify the chat loop handles errors without crashing.
Replace the test query with an interactive chat loop
Right now, main() runs a single hardcoded question and exits. You need a while True loop that keeps prompting until the user types exit.
- In bridge.py, scroll to the main() function and add this comment directly above async def main()::
# --- Main Entry Point ---
- Inside main(), find the test block at the bottom of the function:
# Test the full loop
answer = await run_query(mcp_client, bedrock_client, "What files are in the root folder?", bedrock_tools)
print(f"\nAnswer: {answer}")
- Delete those three lines and replace them with the following interactive chat loop:
print("\nAsk questions about your files (type 'exit' to quit):\n")
while True:
query = input("You: ").strip()
if query.lower() in ("exit", "quit"):
print("Goodbye!")
break
if not query:
continue
try:
answer = await run_query(mcp_client, bedrock_client, query, bedrock_tools)
print(f"\nAssistant: {answer}\n")
except Exception as e:
print(f"\nError: {e}\n")
What does this code do?
- input("You: ").strip() shows a prompt and waits for the learner to type a question.
- The if query.lower() in ("exit", "quit") check lets the user break out of the loop cleanly.
- The try/except wrapper catches any error (authentication timeout, missing tool, network failure) and prints it without crashing the loop.
- Each answer prints with the Assistant: prefix so you can visually distinguish your input from the model's response.
- Save bridge.py.
Seeing an indentation error?
The while True: block must sit at the same indentation level as the bedrock_tools = ... line above it (three levels of indent inside the two async with blocks).
If you see IndentationError, make sure the entire while loop is indented with exactly 12 spaces (or 3 tabs) to match the surrounding code.
Help me fix an indentation error in my bridge.py chat loop
Observe the orchestration logging
You do not need to add any new logging code. The run_query function you built in the previous step already prints two lines for every tool call:
- [Tool Call] shows the tool name and the parameters Bedrock extracted from your question.
- [Tool Result] shows the first 200 characters of what Nasuni returned.
These will appear inline between your You: prompt and the Assistant: response every time Bedrock decides to call a tool.
Why is visible logging important for a PoC?
When a query returns an unexpected answer, the logging lets you pinpoint where things went wrong. Did Bedrock pick the wrong tool? Did it pass bad parameters? Did Nasuni return empty data?
This transparency turns your PoC into a diagnostic tool for understanding what your integration can and cannot handle.
Test the full chat interface with multiple queries
Before you run the app, think about what you expect to happen. Do you think Bedrock will call a tool for every question, or will some questions be answered without one?
- Run your bridge by executing this command:
uv run bridge.py
- Complete the SSO login when your browser opens.
- Once you see the You: prompt, try asking three different questions in sequence. For example:
You: What files are in the root folder?
You: Show me the contents of the Accounting folder
You: What is the most recently modified file?
For each question, you should see the [Tool Call] and [Tool Result] logging appear, followed by a natural language Assistant: response summarizing what Bedrock found.
- Type exit and press Enter to quit.
You should see Goodbye! printed and the script exits cleanly.
Getting an authentication timeout?
If the SSO session expires mid-conversation, the try/except block catches the error and prints it. You can continue typing queries. The OAuth provider will attempt a token refresh on the next tool call.
If refreshes keep failing, stop the script with Ctrl+C and restart it to trigger a fresh SSO login.
Help me debug an authentication timeout in my Bedrock-Nasuni bridge
✔️ Awesome, I've got everything!
Double check you have saved bridge.py and confirmed the chat loop responds to at least 3 different queries.
ⓧ I'd like to double check the full code
Your bridge.py has grown across multiple steps. Scroll through and confirm these sections are present in order:
- The imports block (asyncio, json, sys, webbrowser, boto3, mcp imports, httpx2).
- The configuration section with NASUNI_MCP_URL, BEDROCK_MODEL_ID, AWS_REGION, and MAX_TOOL_RECURSIONS.
- The create_oauth_provider function.
- The mcp_tools_to_bedrock_format function.
- The send_to_bedrock function.
- The run_query function with the full tool-use loop, including [Tool Call] and [Tool Result] print statements.
- The # --- Main Entry Point --- comment followed by async def main() containing the interactive while True chat loop.
- The if __name__ == "__main__": block at the very end.
Secret mission
Add Multi-Turn Conversation Memory
Right now, every question starts from scratch. Real file exploration is iterative: you find a folder, drill into it, then ask about a specific document. Add conversation memory so Bedrock can reference previous answers when making tool calls.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with Bedrock API calls that stay within free-tier volume for PoC usage, so there are no ongoing costs.
Resources you used:
- Local Python project directory containing bridge.py and installed packages (mcp, boto3).
- uv package manager (installed globally on your machine).
Keep everything running
No action needed. Choose this if you are still actively experimenting with the Bedrock-Nasuni bridge or plan to extend it further.
- Your project folder stays on your Desktop, ready to run whenever you need it.
- uv remains installed globally. It is useful for future Python projects too.
- Bedrock API usage only occurs when you actively run the script. No charges accrue while the app is idle.
Pause - I'll come back to this later
Nothing is running persistently, so there are no processes to shut down or services to pause.
- Your project folder and installed packages remain in place for when you return.
- No background services or daemons are running from this project.
Delete - I don't want to use this again
Remove all project files and optionally uninstall uv if you do not plan to use it for other Python projects.
- Delete the project folder on your Desktop (the one containing bridge.py) by running this command:
macOS
cd ~/Desktop
rm -rf [[PROJECT_DIR="your project folder name"]]
Windows
cd ~\Desktop
Remove-Item -Recurse -Force [[PROJECT_DIR="your project folder name"]]
- Uninstall uv by running:
uv self uninstall
What about my AWS credentials?
Your AWS CLI credentials were configured before this project and are used by other tools. This project did not create any persistent AWS resources (no Lambda functions, no S3 buckets, no IAM roles). Leave your AWS configuration in place.
Nice Work!
Nice Work!
You just built a working Python CLI that bridges AWS Bedrock to Nasuni AI Activate via the Model Context Protocol. You can now ask natural language questions about your governed file data and watch the full orchestration loop in action.
You've learned how to:
- Connect to a remote MCP server with SSO authentication over Streamable HTTP transport and dynamically discover its available tools at runtime.
- Translate MCP tool definitions into Bedrock's toolConfig format so the model can reason about when to call each tool.
- Build the full tool-use orchestration loop: send a query to Bedrock, route tool calls to Nasuni via MCP, feed results back, and produce a final natural language answer.
- Secret Mission: Added multi-turn conversation memory so Bedrock can reference previous file lookups and build on them across turns.
Ready to quiz yourself?