# How TUUI Implements the MCP Sampling Protocol for LLM Tool Calls: IPC Architecture Explained

> Discover how TUUI implements MCP sampling for LLM tool calls using UUID-based IPC channels. Learn about its IPC architecture and UI component integration.

- Repository: [AIQL/tuui](https://github.com/ai-ql/tuui)
- Tags: deep-dive
- Published: 2026-02-23

---

**TUUI implements the MCP sampling protocol by registering a request handler in the main process that forwards LLM tool calls to the renderer via UUID-based IPC channels, enabling user interaction through UI components before returning responses to the model.**

TUUI leverages the Model Context Protocol (MCP) sampling protocol to bridge language model tool calls with interactive user interfaces in an Electron application. This architecture allows LLMs running in the backend to request user-driven sampling operations—such as confirming tool executions—while keeping the UI layer isolated in the renderer process. The implementation relies on dynamic IPC channels to maintain strict process separation while enabling synchronous-feeling round-trip communication between the MCP client and the user interface.

## MCP Sampling Protocol Architecture Overview

The implementation spans three distinct layers that handle the flow from LLM request to user interaction and back:

- **MCP Client (Main Process)**: Registers the `sampling` method handler and initiates IPC transfer
- **IPC Bridge (Main ↔ Renderer)**: Manages dynamic response channels using UUID-based naming
- **Renderer UI (Renderer Process)**: Receives requests, presents interfaces like [`SamplingCard.vue`](https://github.com/ai-ql/tuui/blob/main/SamplingCard.vue), and returns results

This separation ensures that heavy UI work and user interactions remain in the renderer while the MCP client maintains its state in the main process.

## Registering the Sampling Handler in the Main Process

The entry point for MCP sampling requests begins in the main process where the MCP client registers a specific handler for sampling operations.

### Setting Up the MCP Client Request Handler

In [`src/main/mcp/client.ts`](https://github.com/ai-ql/tuui/blob/main/src/main/mcp/client.ts), the application registers a request handler using the `SamplingRequestSchema` to intercept sampling calls from the LLM:

```typescript
// src/main/mcp/client.ts
client.setRequestHandler(SamplingRequestSchema, async (request) => {
  console.log('Sampling request received:\n', request)
  const response = await samplingTransferInvoke(request)   // Forward to renderer
  console.log(response)
  return response
})

```

When an MCP-enabled LLM issues a tool call requiring user sampling, the client receives a request matching `SamplingRequestSchema`. The handler immediately forwards the request to `samplingTransferInvoke`, which manages the IPC bridge to the renderer process.

## IPC Bridge: Dynamic Channel Communication

The main process implements a sophisticated IPC mechanism using dynamically generated channels to handle the request-response cycle without blocking the main thread.

### Creating UUID-Based Response Channels

The `samplingTransferInvoke` function in [`src/main/index.ts`](https://github.com/ai-ql/tuui/blob/main/src/main/index.ts) creates a unique response channel for each sampling request to prevent cross-talk between concurrent operations:

```typescript
// src/main/index.ts
export function samplingTransferInvoke(request: SamplingRequest): Promise<SamplingResponse> {
  return new Promise<SamplingResponse>((resolve) => {
    if (!mainWindow || mainWindow.isDestroyed()) {
      resolve(null)
      return
    }

    // Generate unique channel for this round-trip
    const responseChannel = `${msgSamplingTransferResultChannel}-${uuidv4()}`

    // Wait for single reply from renderer
    listenOnceForRendererResponse(responseChannel, resolve)

    // Send request with channel name to renderer
    mainWindow.webContents.send('msgSamplingTransferInvoke', {
      request,
      responseChannel
    } as IpcSamplingRequest)
  })
}

```

This implementation creates a UUID-suffixed channel name (`msgSamplingTransferResult-<uuid>`) for every sampling request, ensuring that concurrent tool calls receive responses on their dedicated channels without interference.

### One-Shot Listener Implementation

The `listenOnceForRendererResponse` helper in [`src/main/IPCs.ts`](https://github.com/ai-ql/tuui/blob/main/src/main/IPCs.ts) registers a single-use listener that automatically resolves the promise when the renderer responds:

```typescript
// src/main/IPCs.ts
export function listenOnceForRendererResponse(
  responseChannel: string,
  resolve: (_value: McpClientResponse) => void
) {
  ipcMain.once(responseChannel, (_event, response: McpClientResponse) => {
    resolve(response)  // Resolves the promise in samplingTransferInvoke
  })
}

```

Using `ipcMain.once` ensures that the listener is automatically cleaned up after receiving the response, preventing memory leaks from accumulated listeners.

## Renderer Process: Handling Sampling Requests

The renderer process receives sampling requests through a utility class that bridges the IPC gap and exposes methods for UI components to respond.

### Registering the IPC Listener

In [`src/renderer/utils/index.ts`](https://github.com/ai-ql/tuui/blob/main/src/renderer/utils/index.ts), the `Sampling` class provides static methods to handle incoming sampling requests and send responses:

```typescript
// src/renderer/utils/index.ts
class Sampling {
  static async msgSamplingTransferInvoke(callback: IpcSamplingRequestCallback) {
    return window.mainApi.on('msgSamplingTransferInvoke', callback)
  }

  // Renderer → Main: send the sampled result back
  static async msgSamplingTransferResult(channel: string, response: SamplingResponse) {
    console.log(channel, response)
    window.mainApi.send(channel, response)  // Uses the dynamic channel
  }
}

export const SamplingTransfer = {
  request: Sampling.msgSamplingTransferInvoke,
  response: Sampling.msgSamplingTransferResult
}

```

The `request` method registers a listener for incoming sampling operations, while the `response` method sends the user's decision back to the main process using the specific channel identifier provided in the original request.

### UI Implementation with SamplingCard.vue

The [`SamplingCard.vue`](https://github.com/ai-ql/tuui/blob/main/SamplingCard.vue) component in the renderer handles the actual user interaction, displaying dialogs when sampling is required:

```vue
<!-- src/renderer/components/common/SamplingCard.vue -->
<script setup lang="ts">
import { SamplingTransfer } from '@/utils'
import { ref } from 'vue'

const samplingDialog = ref(false)
const samplingParams = ref<SamplingRequestParams | {}>({})
const samplingChannel = ref('')

// Register the request handler once
SamplingTransfer.request(async (event, { request, responseChannel }) => {
  samplingParams.value = request
  samplingChannel.value = responseChannel
  samplingDialog.value = true  // Open UI dialog
})

// When user confirms the sampling:
async function confirmSampling(result) {
  await SamplingTransfer.response(samplingChannel.value, result)
  // UI closes after response
}
</script>

```

This component captures the `responseChannel` from the incoming request and preserves it until the user completes the interaction, then sends the result back through that specific channel to resolve the waiting promise in the main process.

## Security: Preload Script Configuration

The IPC bridge is securely exposed to the renderer through a preload script that limits available channels and prevents arbitrary IPC access:

```typescript
// src/preload/index.ts
contextBridge.exposeInMainWorld('mainApi', {
  on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener),
  send: (channel: string, ...args: any[]) => ipcRenderer.send(channel, ...args)
})

// Allowed channels include sampling protocols
const allowedChannels = [
  'msgSamplingTransferInvoke',
  'msgSamplingTransferResult',
  // ...
]

```

The preload script safely exposes only necessary Electron APIs to the renderer, with the dynamic response channels being permitted because the channel strings are generated and supplied by the trusted main process.

## End-to-End Flow: Complete Round-Trip Example

The complete MCP sampling protocol execution follows this sequence:

1. **LLM Request**: The language model sends a sampling request via MCP to the client
2. **Handler Activation**: [`src/main/mcp/client.ts`](https://github.com/ai-ql/tuui/blob/main/src/main/mcp/client.ts) receives the request and calls `samplingTransferInvoke`
3. **Channel Creation**: The main process generates a UUID-based response channel and registers a one-shot listener via `listenOnceForRendererResponse`
4. **Renderer Notification**: The request and channel name are sent to the renderer via `msgSamplingTransferInvoke`
5. **UI Presentation**: [`SamplingCard.vue`](https://github.com/ai-ql/tuui/blob/main/SamplingCard.vue) receives the request, stores the channel identifier, and presents the sampling dialog
6. **User Response**: After user interaction, the component calls `SamplingTransfer.response` with the stored channel and result
7. **Promise Resolution**: The main process receives the response on the UUID channel, resolves the original promise, and returns the result to the MCP client
8. **LLM Completion**: The client returns the sampling result to the LLM, completing the tool-call round-trip

This architecture ensures that the MCP sampling protocol maintains responsiveness while keeping UI concerns properly isolated in the renderer process.

## Summary

- **Dynamic Channel Management**: TUUI uses UUID-based channel names (`msgSamplingTransferResult-${uuid}`) to handle concurrent sampling requests without interference
- **Process Isolation**: The implementation maintains strict separation between the MCP client (main process) and UI components (renderer process) through Electron's IPC architecture
- **One-Shot Listeners**: The `listenOnceForRendererResponse` pattern prevents memory leaks by automatically removing IPC listeners after receiving responses
- **Type-Safe Bridge**: The `SamplingTransfer` utility in [`src/renderer/utils/index.ts`](https://github.com/ai-ql/tuui/blob/main/src/renderer/utils/index.ts) provides typed methods for handling sampling requests and responses
- **Secure Exposure**: The preload script limits IPC channel access while allowing dynamic channel communication required for the sampling protocol

## Frequently Asked Questions

### What is the MCP sampling protocol in TUUI?

The MCP sampling protocol in TUUI is an implementation of the Model Context Protocol that allows language models to request user input or confirmation during tool execution. According to the TUUI source code, this protocol intercepts sampling requests in [`src/main/mcp/client.ts`](https://github.com/ai-ql/tuui/blob/main/src/main/mcp/client.ts) using `SamplingRequestSchema`, then forwards them to the renderer process via IPC to display interactive UI components like [`SamplingCard.vue`](https://github.com/ai-ql/tuui/blob/main/SamplingCard.vue) before returning the user's response to the model.

### How does TUUI maintain process isolation during sampling?

TUUI maintains process isolation by keeping the MCP client in the main process while executing all user interface logic in the renderer process. The `samplingTransferInvoke` function in [`src/main/index.ts`](https://github.com/ai-ql/tuui/blob/main/src/main/index.ts) creates a promise-based bridge that sends requests to the renderer via `mainWindow.webContents.send()`, then waits for a response on a dynamically generated channel. This ensures that heavy UI operations never block the main process while the MCP client maintains its connection to the language model.

### What happens if the main window is destroyed during a sampling request?

If the main window is destroyed during a sampling request, the `samplingTransferInvoke` function immediately resolves the promise with `null` and returns early without creating IPC channels. As shown in the source code, the function checks `if (!mainWindow || mainWindow.isDestroyed())` before proceeding with channel creation, preventing errors from sending IPC messages to non-existent windows and gracefully handling the edge case.

### Can the sampling protocol handle multiple concurrent requests?

Yes, the sampling protocol handles multiple concurrent requests through its UUID-based channel naming strategy. Each call to `samplingTransferInvoke` generates a unique response channel using `uuidv4()`, ensuring that concurrent sampling operations from the LLM receive responses on their dedicated channels without cross-talk. The `listenOnceForRendererResponse` mechanism further ensures that each channel listener is isolated and cleaned up independently after its specific request completes.