A Powerful AI Application Made with Nitro and Nuxt UI

Part of the series AI Agents and MCP Server: Teaming Up for the Agentic Web

Our agent and MCP server are ready. In this article, we will turn them into an AI application where users can send messages, receive streamed answers, and see when the agent uses a tool.

To build it, we will use Nuxt along with Nuxt UI, which includes components designed for building AI applications.

From Nitro to Nuxt

Nuxt uses Nitro as its server engine, so we can keep the server routes built in the previous articles and add a frontend around them.

This is a mechanical migration rather than the focus of the article, so this script performs it for us:

bash
# Remove Nitro dependencies
pnpm remove nitropack h3

# Install Nuxt dependencies
pnpm add nuxt vue vue-router

# Update package.json scripts
jq '.scripts = {
  "build": "nuxt build",
  "dev": "nuxt dev",
  "generate": "nuxt generate",
  "preview": "nuxt preview",
  "postinstall": "nuxt prepare"
}' package.json > tmp.json && mv tmp.json package.json

# Update tsconfig.json for Nuxt
cat > tsconfig.json <<'EOF'
{
  "files": [],
  "references": [
    { "path": "./.nuxt/tsconfig.app.json" },
    { "path": "./.nuxt/tsconfig.server.json" },
    { "path": "./.nuxt/tsconfig.shared.json" },
    { "path": "./.nuxt/tsconfig.node.json" }
  ]
}
EOF

# Create Nuxt config
cat > nuxt.config.ts <<'EOF'
import { defineNuxtConfig } from "nuxt/config"

export default defineNuxtConfig({
  runtimeConfig: {
    openAiApiKey: '',
    mcpEndpoint: '',
  },
  compatibilityDate: '2025-10-05',
})
EOF

# Remove old Nitro config
rm nitro.config.ts

# Set up basic Nuxt app structure
mkdir -p app/pages
cat > app/app.vue <<'EOF'
<template>
  <div>
    <NuxtRouteAnnouncer />
    <NuxtPage />
  </div>
</template>
EOF

cat > app/pages/index.vue <<'EOF'
<template>
  <div>
    <h1>Welcome to the AI Application</h1>
  </div>
</template>
EOF

# Update .env and .gitignore
sed -i '' 's/NITRO/NUXT/g' .env
echo ".nuxt" >> .gitignore

# Prepare Nuxt
pnpm run postinstall

Adding Nuxt UI

Installing Nuxt UI isn't part of this series, so you can just run this script:

bash
# Install Nuxt UI and Tailwind CSS
pnpm add @nuxt/ui tailwindcss

# Create the main CSS file and import styles
mkdir -p app/assets/css
cat > app/assets/css/main.css <<'EOF'
@import 'tailwindcss';
@import '@nuxt/ui';
EOF

# Update Nuxt config to enable Nuxt UI and include the CSS
cat > nuxt.config.ts <<'EOF'
import { defineNuxtConfig } from "nuxt/config"

export default defineNuxtConfig({
  modules: ['@nuxt/ui'],
  css: ['~/assets/css/main.css'],
  runtimeConfig: {
    openAiApiKey: '',
    mcpEndpoint: '',
  },
  compatibilityDate: '2025-10-05',
})
EOF

# Update the main app layout to use Nuxt UI's UApp component
cat > app/app.vue <<'EOF'
<template>
  <UApp>
    <NuxtPage />
  </UApp>
</template>
EOF

Once done, start the development server:

bash
pnpm run dev

The app should now start successfully. Before testing the chat, make sure the /api/chat route is present and your .env file contains NUXT_OPEN_AI_API_KEY and NUXT_MCP_ENDPOINT.

Building the application

Now that we have a working Nuxt application with Nuxt UI, we can build the chat interface. We will focus on the structure and behavior rather than on a perfect visual design.

Within the Nuxt documentation, there is a fully working example of a page with a chat interface that we can use. We don't need more than that for today's implementation.

Before using it, we need to install the AI SDK for Vue.

bash
pnpm add @ai-sdk/vue

We also need to install @nuxtjs/mdc to parse and render Markdown on the fly.

bash
pnpm dlx nuxt module add @nuxtjs/mdc

Then, replace app/pages/index.vue with this complete version of the example:

vue
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue'

const input = ref('')

const chat = new Chat({
  onError(error) {
    console.error('Chat error:', error)
  }
})

function handleSubmit(e: Event) {
  e.preventDefault()
  chat.sendMessage({ text: input.value })
  input.value = ''
}
</script>

<template>
  <UDashboardPanel>
    <template #body>
      <UContainer>
        <UChatMessages :messages="chat.messages" :status="chat.status">
          <template #content="{ message }">
            <template v-for="(part, index) in message.parts" :key="index">
              <MDC
                v-if="part.type === 'text'"
                :value="part.text"
                :cache-key="`${message.id}-${index}`"
                unwrap="p"
              />

              <div v-else-if="part.type === 'reasoning'">
                {{ part.state === 'streaming' ? 'Thinking...' : 'Thinking complete' }}
              </div>

              <div v-else-if="part.type === 'dynamic-tool' && part.toolName === 'addition'">
                <template v-if="part.state === 'input-streaming'">
                  <template v-if="part.input && (part.input as { a: number; b: number }).a !== undefined && (part.input as { a: number; b: number }).b !== undefined">
                    Adding: {{ (part.input as { a: number; b: number }).a }} + {{ (part.input as { a: number; b: number }).b }}
                  </template>
                  <template v-else>
                    Adding...
                  </template>
                </template>
                <template v-else>
                  Addition complete: {{ (part.input as { a: number; b: number }).a }} + {{ (part.input as { a: number; b: number }).b }}
                </template>
              </div>
            </template>
          </template>
        </UChatMessages>
      </UContainer>
    </template>

    <template #footer>
      <UContainer>
        <UChatPrompt v-model="input" :error="chat.error" @submit="handleSubmit">
          <UChatPromptSubmit :status="chat.status" @stop="chat.stop" @reload="chat.regenerate" />
        </UChatPrompt>
      </UContainer>
    </template>
  </UDashboardPanel>
</template>

Type "What is 2 + 2?" in the input field. With the API route running and the environment variables configured, you should receive an answer from the agent.

Our AI application in action
Our AI application in action

To verify that the agent used the addition tool, open your browser's developer tools and inspect the event stream from the /api/chat request. You should see something like this:

The Event Stream from the /api/chat endpoint
The Event Stream from the /api/chat endpoint

It is the same UI message stream we saw with curl in the terminal.

Understanding the chat integration

We have built an AI application that uses our agent. Here's what connects the frontend to the backend.

First, the /api/chat response is not the raw output of the model. toUIMessageStreamResponse converts the agent's result into a stream designed for a user interface.

UIMessage is the interface that defines the structure of a message in an AI application. The frontend does not need to transform the received stream of messages because they are already in the correct format.

The AI SDK also provides the Chat class to manage the chat state. It handles the message stream, appends new messages, sends messages in the correct format with sendMessage({ text: input.value }), and manages status and error states.

You could implement the POST request, streamed response parsing, message updates, cancellation, and error handling yourself. Chat handles those details so we can focus on the experience, including showing the tools used, their status, and their input in real time.

Showing tools

Each message is composed of parts. A user message can have text and file parts, while an assistant message can have text, reasoning, tool-call, and file parts.

Under the hood, the AI SDK manages these parts while it receives chunks from the server. It assembles text chunks into a message and updates the state of tool-call parts as the agent uses them.

The #content slot in the complete example iterates through these parts. It renders Markdown text, shows when the model is reasoning, and displays the addition tool's progress and input.

Note

This approach is flexible, allowing you to easily customize it to fit your needs.

At the end, it looks like this:

The AI Application in action.

Showing the agent's progress gives users confidence that their request is being handled, especially when a tool call takes time. In the final article, we will deploy the application and MCP server to Cloudflare.

PP

Thanks for reading! My name is Estéban, and I love to write about web development and the human journey around it.

I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.

If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.

I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below, it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!

Continue readingDeploying Our AI Agent to the World Using Cloudflare

Reactions

Discussions

Add a Comment

You need to be logged in to access this feature.