Vikas Kumar Ojha
MachineLearning@Avalara Ex-Samsung
Before diving into this blog, I highly recommend reading my previous post, where I explain how an MCP client and server work and walk through an implementation from scratch. That background will make the concepts discussed here much easier to follow.
Before we begin, I want to clarify that I am not affiliated with Amazon in any way. The views expressed in this blog are entirely my own and should not be interpreted as a product review.
Alexa is a capable voice assistant—its microphone quality and hardware are impressive, but it often struggles when faced with questions that require deeper reasoning or complex decision-making. While Alexa Skills allow you to extend its capabilities—such as integrating with external platforms or performing tasks like paying bills, there are limits to what’s available out of the box. But what if you want functionality that doesn’t exist yet, or you simply want Alexa to be more intelligent? That’s exactly what we’ll explore in this blog.
Alexa Skills
Alexa Skills are like apps for Alexa.
Just as you install apps on your phone to give it new abilities, you enable skills to teach Alexa how to do new things. By default, Alexa can answer basic questions, set alarms, or play music. But if you want Alexa to do something specific, like check order status, control custom software, or talk to your own backend, you need a skill.
In this blog, we will integrate the MCP into my previous blog with Alexa Skills and use it on Alexa.
Running the MCP server on a GPU
In the previous blog, I mentioned that I was running the MCP server on an Ubuntu VM on my PC. However, my main host OS is Windows 11, and setting up dependencies on Windows is often complicated. To reduce this complexity, I used an Ubuntu system for development and then created Docker configurations to run it on Windows. Later, I learned that Alexa Skills require the backend to return a response within 8 seconds. The easiest way to achieve this was to run the model on a GPU. Since I have an NVIDIA RTX 4090, let’s go ahead and make the necessary modifications to run the model on the GPU.
Modifying the Dockerfile
To utilize the GPU for inference, we need to build llama.cpp with CUDA support. Choosing Docker in my previous blog helps us here by avoiding the complicated steps of setting up CUDA and build tools on Windows. So, let’s go ahead and make the required changes to our Dockerfile.
# ================================
# GPU-enabled llama.cpp + FastAPI
# ================================
FROM nvidia/cuda:12.1.1-devel-ubuntu22.04
# Avoid tzdata prompts
ENV DEBIAN_FRONTEND=noninteractive
# ----------------
# System packages
# ----------------
RUN apt-get update && apt-get install -y --no-install-recommends \
python3.10 \
python3-pip \
python3-dev \
build-essential \
git \
cmake \
pkg-config \
libopenblas-dev \
libgomp1 \
libcurl4-openssl-dev \
wget \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Make python3.10 default
RUN ln -s /usr/bin/python3.10 /usr/bin/python
# ----------------
# CUDA build flags for llama.cpp
# ----------------
ENV CMAKE_ARGS="-DLLAMA_CUDA=on -DGGML_CUDA_NO_VMM=1"
ENV FORCE_CMAKE=1
ENV LLAMA_CUDA=1
ENV LLAMA_CUDA_FORCE_MMQ=1
ENV LLAMA_CUDA_FORCE_CUBLAS=1
# Python runtime flags
ENV PIP_NO_CACHE_DIR=1
ENV PYTHONUNBUFFERED=1
# ----------------
# Create app user
# ----------------
RUN useradd --create-home appuser
WORKDIR /home/appuser/app
# ----------------
# Install Python deps (as root)
# ----------------
COPY requirements.txt .
RUN python -m pip install --upgrade pip setuptools wheel \
&& python -m pip install --no-cache-dir -r requirements.txt
RUN pip install torch==2.2.2+cu121 torchvision==0.17.2+cu121 torchaudio==2.2.2+cu121 \
--index-url https://download.pytorch.org/whl/cu121
# Sanity check (fail fast if MCP deps are broken)
RUN python -c "from llama_index.tools.mcp import BasicMCPClient, McpToolSpec; print('llama_index.tools.mcp import OK')"
# ----------------
# Copy app code
# ----------------
COPY --chown=appuser:appuser . .
USER appuser
# ----------------
# Runtime config
# ----------------
EXPOSE 3000
ENV MODEL_PATH=/home/appuser/app/gorilla-openfunctions-v2-GGUF/gorilla-openfunctions-v2-q4_K_M.gguf
ENV MCP_SERVER_URLS="http://mcp_calculator:8000/sse,http://mcp_converter:8000/sse"
# ----------------
# Start FastAPI
# ----------------
CMD ["python","-m","uvicorn","llm_client:app","--host","0.0.0.0","--port","3000","--workers","1"]
However, if you remember from the previous blog, we were launching all the associated Docker containers for the MCP client and server using Docker Compose. Therefore, we also need to enable the NVIDIA runtime there; otherwise, it can lead to NVIDIA driver-related issues, which I encountered during my own experimentation. Let’s go ahead and modify the code that generates the updated configuration.
#!/usr/bin/env python3
"""
Generate docker-compose.generated.yml by inspecting MCP Dockerfiles.
- MCP internal port inferred from EXPOSE / ENV PORT
- Host ports auto-assigned
- NVIDIA enabled only for fastapi_app (Windows Docker Desktop compatible)
"""
import re
import yaml
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MCP_DIR = ROOT / "own_MCP_servers"
OUT = ROOT / "docker-compose.generated.yml"
FASTAPI_SERVICE = "fastapi_app"
BASE_HOST_PORT = 8000
def parse_dockerfile_ports(dockerfile_path: Path):
"""
Parse EXPOSE and ENV PORT from a Dockerfile.
Defaults to 8000 if not found.
"""
expose_port = None
env_port = None
content = dockerfile_path.read_text()
expose_match = re.search(r"EXPOSE\s+(\d+)", content)
if expose_match:
expose_port = int(expose_match.group(1))
env_match = re.search(r"ENV\s+PORT\s*=\s*(\d+)", content)
if env_match:
env_port = int(env_match.group(1))
return env_port or expose_port or 8000
def discover_mcp_services():
"""
Discover MCP services and infer their internal ports.
"""
services = []
for d in sorted(MCP_DIR.iterdir()):
dockerfile = d / "Dockerfile"
if d.is_dir() and dockerfile.exists():
port = parse_dockerfile_ports(dockerfile)
services.append((d.name, port))
return services
def build_compose():
services = {}
# -------- FastAPI (GPU-enabled) --------
services[FASTAPI_SERVICE] = {
"build": {
"context": str(ROOT),
"dockerfile": "Dockerfile",
},
"container_name": "fastapi_mcp_app",
"runtime": "nvidia",
"environment": [
"MODEL_PATH=/models/gorilla-openfunctions-v2-q4_K_M.gguf",
"NVIDIA_VISIBLE_DEVICES=all",
],
"ports": ["3000:3000"],
"restart": "unless-stopped",
"volumes": [
"./gorilla-openfunctions-v2-GGUF:/models:ro",
"./mcp_tools_lancedb:/home/appuser/app/mcp_tools_lancedb",
".:/home/appuser/app:ro",
],
"depends_on": [],
}
# -------- MCP services --------
mcp_urls = []
host_port = BASE_HOST_PORT
for name, internal_port in discover_mcp_services():
svc_name = f"mcp_{name}"
services[svc_name] = {
"build": {
"context": f"./own_MCP_servers/{name}",
"dockerfile": "Dockerfile",
},
"container_name": svc_name,
"environment": [
"HOST=0.0.0.0",
f"PORT={internal_port}",
],
"ports": [f"{host_port}:{internal_port}"],
"restart": "unless-stopped",
"volumes": [
f"./own_MCP_servers/{name}/app:/app:ro",
],
}
mcp_urls.append(f"http://{svc_name}:{internal_port}/sse")
services[FASTAPI_SERVICE]["depends_on"].append(svc_name)
host_port += 1
# Inject MCP_SERVER_URLS
services[FASTAPI_SERVICE]["environment"].append(
"MCP_SERVER_URLS=" + ",".join(mcp_urls)
)
return {
"version": "3.8",
"services": services,
}
def main(run_up=True):
compose = build_compose()
with open(OUT, "w") as f:
yaml.safe_dump(compose, f, sort_keys=False)
print(f"Generated {OUT}")
if run_up:
subprocess.check_call(
["docker", "compose", "-f", str(OUT), "up", "-d", "--build"]
)
if __name__ == "__main__":
main()
The above code will generate a Docker Compose configuration that includes the additional runtime: nvidia parameter for the container named fastapi_mcp_app. This instructs Docker to run the container using the NVIDIA Container Runtime instead of the default Docker runtime. Since this container hosts our LLM model, this configuration ensures that inference and prediction are executed on the GPU.
On my Ubuntu VM, I allocated 8 CPU cores out of the 14 available on my system. With this setup, inference took around 30–60 seconds. Switching to the GPU reduced the inference time significantly, bringing it down to about 6 seconds, which is within acceptable limits for Alexa. That said, 6 seconds is still relatively long, and we’ll address further optimizations in a future blog. For now, let’s move ahead and integrate our MCP server with Alexa.
Adding alexa endpoint for handling requests from Alexa
Alexa uses something called intents to communicate with its backend services. When a user requests an action, Alexa maps the user’s utterance to an intent. This intent, along with any extracted parameters (slots), is then sent to the backend, where it is handled accordingly.
Intents are conceptually similar to calling a function with parameters. With that in mind, let’s modify the llm_client.py file from our previous blog to include support for this endpoint.
@app.post("/query")
async def run_query(req: QueryRequest):
try:
result = await execute_query_pipeline(req.query, k=req.k)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/alexa")
async def alexa(req: Request):
body = await req.json()
req_type = body["request"]["type"]
if req_type == "LaunchRequest":
return {
"version": "1.0",
"response": {
"outputSpeech": {
"type": "PlainText",
"text": "Hi, you can ask me anything."
},
"shouldEndSession": False
}
}
if req_type == "IntentRequest":
intent = body["request"]["intent"]
slots = intent.get("slots", {})
query = (
slots.get("query", {})
.get("value")
)
if not query:
return {
"version": "1.0",
"response": {
"outputSpeech": {
"type": "PlainText",
"text": "What would you like me to help you with?"
},
"shouldEndSession": False
}
}
print(f"query={query}")
try:
result = await execute_query_pipeline(query, k=4)
final_text = result.get("final_text") or "Sorry, I could not process that request."
except Exception as e:
final_text = "Something went wrong while processing your request."
return {
"version": "1.0",
"response": {
"outputSpeech": {
"type": "PlainText",
"text": final_text
},
"shouldEndSession": True
}
}
The LaunchRequest is triggered when a skill is launched, while an IntentRequest is used when the user invokes a specific command within the skill.
Integrating the MCP with Alexa
Well, building a cool MCP is useless if it can’t be used hassle-free, and what could be more hassle-free than simply using our friend Alexa to do the same?
As mentioned in the previous sections, Alexa provides Skills that allow additional functionality to be added. I wondered whether these skills could be developed by individuals, and with a bit of research, I discovered the Alexa Developer Console, which allows users to develop and publish their own skills.
Requirements for developing Alexa skills
1. A valid Amazon Account. Here, the Echodot device might not be needed for development, and the Alexa app on the phone will suffice, I suppose. For testing on echo dot make sure same account is used on echo dot device as well.
2. A system or VPS where the backend for the skill can be hosted, and the endpoint should have a valid TLS certificate.
I didn’t want to use a VPS since I only have one, and it is currently being used to host idealpodium.com. So, I decided to research alternatives and found that even URLs issued by ngrok would work. Another reason for pursuing this approach was to enable more people to try it.
Developing the Alexa skill
For that, we will follow a step-by-step process and develop the skill and use it on our Echo Dot device.
Starting the MCP server
As mentioned above, either ngrok or any TLS-verified endpoint will work with Alexa, so let’s go ahead and launch our server instance. To run this, I’m assuming that Docker and Python are already set up on your PC.
Clone the git repo
[email protected]:VikasOjha666/Alexa_MCP.git
Launching the server
Make sure you download the model weights from here and place them in the base path of the cloned code. After placing, we need to make sure that the MODEL_PATH param in the llm_client.py file points to the same location. After doing that, run the command below
python3 scripts/generate_compose_and_up.py
If you’re running this for the first time, you’ll need to run the above script. It will generate the docker-compose.yml file and then run the docker-compose command to build and launch the instance.
If you already have a docker-compose file, you can simply run docker-compose to launch the instance. This will host the model at http://localhost:3000.
Tunneling the MCP client with ngrok
The ngrok URLs, as mentioned earlier, are already TLS-certified, so they can be used as Alexa skill endpoints. However, in the free version of ngrok, the URL changes each time we restart the tunnel, which means it must be updated in the Alexa skill whenever we want to test.
So, let’s go ahead and tunnel localhost using ngrok.
ngrok http 3000
The above command should be run only after the auth token is set up for your ngrok account. Usually, it can be configured using the following command.
ngrok config add-authtoken your_ngrok_tokenNow we will get some url like https://27e420b843d0.ngrok-free.app, which can be utilised in Alexa skills.
Creating the Alexa skill
We’ll create a skill but won’t publish it to the store; instead, we’ll test it only on our own account. To do that, we need to follow the steps below:
1. Go to https://developer.amazon.com/alexa/console/ask
2. Log in to your Amazon account if required. After navigating to the above url you would see an option called Create Skill.
You’ll see something like the image above. Enter the skill name and the primary locale. The locale is especially important if you want to use the skill on an Echo Dot device. In my case, the device locale was English (US), so I kept the default setting.
I named my skill “kaya”, and after that, I clicked the button on the page.
3.
(i) The next tab is Experience, Model, and Hosting Service, where the Skill Builder provides predefined words and formats that can be used to invoke a skill. Since I was building a dummy skill, I chose Other.
(ii) Similarly, under Choose a model in the same tab, I selected Custom.
(iii) Finally, under Hosting Services in the same tab, I selected Alexa-hosted (Python)
Keeping the hosting region as the default, I clicked next.
4.
In the templates tab, I chose Start from Scratch since it was custom.
5.
The review tab is just to review our choice. After you are sure about your choices, you can hit Create skill to create the skill.
Configuring the skill to call the endpoint and be used with Alexa
Let's configure the skill we created above to call our endpoint.
1. Adding the Invocation Name
We need a name to call our skill. In my case, I have given kaya bot.
2. Adding Intents
Intents act as a mapping that Alexa uses to determine what action to perform based on what we say. The same intent is then passed to the backend endpoint for processing, so let’s go ahead and define it.
For doing that we go to Interaction Model ---> Intents and click Add Intents.
Then there would be an option to create a custom Intent. We select that and add an intent named QueryIntent.
Since in my case I have already created its throwing error that same name can't be used.
3. Adding Sample Utterances
After you create the Intent, it will have an option to add sample utterances. They are the ways how a user might use the skill. In my case, I have added tell me {query}, kaya bot {query}, and do {query}.
So once the skill opens, I could execute an action by saying tell me what is sum of numbers 2 and 3.
4. Adding Intent Slots
Intent slots are similar to function parameters. They define what data is passed for a given intent. In this case, the parameter will be AMAZON.SearchQuery.
Once done, we click Save. And then we proceed to set our endpoint url.
Setting up the endpoint
In the Endpoint tab, we set the URL for the Default Region, North America, Europe, and India (since I reside in India), and then click Save.
For each URL, there is also a declaration stating that the subdomain belongs to a domain with a certificate issued by a trusted authority.
Build the skill
Now we go to the custom tab and hit save and then Build.
Our skill is now built and is ready for testing.
Testing the skill
To test the skill, we need to go to the test tab, and we need to set the Skill testing to development from off.
Once done, we can start testing our skill, so let's enable and do some tests.
Testing on the Echo Dot Device
To test the skill on an Echo Dot device, we need to open the Alexa app on our phone. In Skills & Games, go to Your Skills, then navigate to the Dev section, where you will find your skill. If it is disabled, enable it and restart Alexa. That’s it — we’re ready to go.
Now, let’s hear Alexa in action below.
Reference and Further Readings
Congrats, we have learnt together an interesting skill. If you like what you read, please consider sharing this blog with your network. It would help me motivate to write more blogs like this.
Comments
No comments yet. Be the first to comment!