Integrating our MCP server with Alexa

Vikas Kumar Ojha profile

Vikas Kumar Ojha

MachineLearning@Avalara Ex-Samsung

Uploaded

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_token

Now 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.

Uploaded

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. 

Uploaded

(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. 

Uploaded

In the templates tab, I chose Start from Scratch since it was custom.

5.

Uploaded

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.

Uploaded

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.

Uploaded

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}.

Uploaded


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.

Uploaded

Once done, we click Save. And then we proceed to set our endpoint url.

----

Setting up the endpoint

Uploaded

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.

Uploaded

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.

Uploaded

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. 










Show Comments