TUUI's MCP Elicitation Protocol: How User Consent Flows from Server to UI
TUUI's MCP elicitation protocol implements a secure 9-step round-trip between the Electron main process and renderer UI to capture explicit user consent before MCP servers execute privileged operations.
The Model-Context Protocol (MCP) requires explicit user authorization when servers request sensitive data or permissions. In the ai-ql/tuui repository, TUUI's MCP elicitation protocol manages this consent flow through a carefully orchestrated IPC bridge between the main process and the Vue-based renderer interface.
The 9-Step MCP Elicitation Flow
Step 1: Server Initiates ElicitRequest
The flow begins when an MCP server sends an ElicitRequest to request user permission or data. This request follows the ElicitRequestSchema defined in the protocol specification, signaling that the server requires explicit consent before proceeding.
Step 2: Main Process Handler Registration
In src/main/mcp/client.ts, the MCP client registers a request handler using setRequestHandler with the ElicitRequestSchema. When the server request arrives, this handler invokes elicitationTransferInvoke to delegate the decision to the UI layer.
// src/main/mcp/client.ts
client.setRequestHandler(ElicitRequestSchema, async (request) => {
const response = await elicitationTransferInvoke(request)
return response
})
Step 3: Creating the Response Channel
The elicitationTransferInvoke function in src/main/index.ts generates a unique response channel using uuidv4() to ensure one-time use. It registers a listener via listenOnceForRendererResponse and forwards the request to the renderer through IPC using mainWindow.webContents.send.
// src/main/index.ts
const responseChannel = `${msgElicitationTransferResultChannel}-${uuidv4()}`
listenOnceForRendererResponse(responseChannel, resolve)
mainWindow.webContents.send('msgElicitationTransferInvoke', {
request,
responseChannel,
})
Step 4: Renderer IPC Bridge Setup
On the renderer side, src/renderer/utils/index.ts exports the ElicitationTransfer object that maps the IPC channels. This bridge exposes msgElicitationTransferInvoke for receiving requests and msgElicitationTransferResult for sending responses back to the main process.
// src/renderer/utils/index.ts
export const ElicitationTransfer = {
request: Elicitation.msgElicitationTransferInvoke,
response: Elicitation.msgElicitationTransferResult,
}
Step 5: UI Modal Presentation
When the renderer receives the request, src/renderer/components/common/ElicitationCard.vue opens a persistent modal dialog. The component populates form fields based on request.params.requestedSchema, presenting the user with the specific consent request details.
<!-- src/renderer/components/common/ElicitationCard.vue -->
<script setup>
const elicitationDialog = ref(false)
const elicitationParams = ref<ElicitRequestParams | {}>({})
const elicitationChannel = ref('')
// ...dialog UI rendered in template based on requestedSchema
</script>
Step 6: User Consent Decision
The user interacts with the modal to either accept or decline the request. The acceptElicitation function constructs an ElicitResponse with action: 'accept' and the user-provided values, while declineElicitation creates a response with action: 'decline'.
// src/renderer/components/common/ElicitationCard.vue
const acceptElicitation = () => {
const response: ElicitResponse = {
action: 'accept',
content: toRaw(elicitationResults.value)
}
ElicitationTransfer.response(elicitationChannel.value, response)
}
const declineElicitation = () => {
const response: ElicitResponse = { action: 'decline' }
ElicitationTransfer.response(elicitationChannel.value, response)
}
Step 7: Returning the Response
The renderer sends the response back through the unique channel established in Step 3. The msgElicitationTransferResult method in src/renderer/utils/index.ts invokes window.mainApi.send to transmit the ElicitResponse to the main process.
// src/renderer/utils/index.ts
static async msgElicitationTransferResult(channel: string, response: ElicitResponse) {
await window.mainApi.send(channel, response)
}
Step 8: Main Process Promise Resolution
In src/main/IPCs.ts, the listenOnceForRendererResponse function receives the response via ipcMain.once on the unique channel. It resolves the promise created in Step 3, returning the user's decision to the MCP client handler.
// src/main/IPCs.ts
ipcMain.once(responseChannel, (_event, response) => {
resolve(response)
})
Step 9: Server Completion
The MCP client in src/main/mcp/client.ts returns the ElicitResponse to the requesting server. The server can now proceed with the privileged operation if the user accepted, or handle the denial appropriately if the user declined, completing the consent round-trip.
Key Implementation Files
The MCP elicitation protocol spans five critical files across the Electron main and renderer processes:
src/main/mcp/client.ts– Registers the MCP request handler withsetRequestHandlerand initiates the elicitation transfer to the UI layersrc/main/index.ts– ImplementselicitationTransferInvoke, generates UUID-based response channels, and manages the IPC forwarding to the renderersrc/main/IPCs.ts– Registers one-time listeners vialistenOnceForRendererResponseto capture the renderer's response without persistent handlerssrc/renderer/utils/index.ts– Provides theElicitationTransferIPC bridge that mapsmsgElicitationTransferInvokeandmsgElicitationTransferResultbetween processessrc/renderer/components/common/ElicitationCard.vue– Renders the consent modal dialog, populates fields fromrequestedSchema, and emits accept or decline actions
Summary
- TUUI's MCP elicitation protocol creates a secure 9-step round-trip between the MCP server and the user interface to obtain explicit consent before privileged operations execute.
- The flow uses unique one-time channels (UUID-based) to ensure responses are securely matched to their pending requests without risk of cross-talk or replay attacks.
- Five core files coordinate across the Electron main process (
client.ts,index.ts,IPCs.ts) and renderer process (utils/index.ts,ElicitationCard.vue) to bridge the Model-Context Protocol with the Vue UI. - Users interact with a modal dialog (
ElicitationCard.vue) that presents the specific permission request and captures either accept or decline actions, ensuring informed consent. - The protocol guarantees that privileged operations cannot proceed without explicit user authorization, satisfying the Model-Context Protocol's security and consent requirements.
Frequently Asked Questions
What triggers the MCP elicitation protocol in TUUI?
The protocol triggers when an MCP server sends an ElicitRequest to the client, typically when the server needs permission to access sensitive data or execute privileged operations. The request is received by the handler registered in src/main/mcp/client.ts, which immediately initiates the UI consent flow by calling elicitationTransferInvoke.
How does TUUI ensure elicitation responses match their original requests?
TUUI generates a unique UUID for each elicitation request in src/main/index.ts using uuidv4(). This UUID creates a one-time response channel (formatted as msgElicitationTransferResultChannel-${uuid}) that is registered with ipcMain.once in src/main/IPCs.ts. This ensures the response is securely matched to its pending promise without risk of cross-talk.
Can users modify the requested data before accepting an elicitation?
Yes. When the ElicitationCard.vue component renders the modal, it populates form fields based on request.params.requestedSchema. Users can review and modify these values before clicking accept. The acceptElicitation function captures these modified values using toRaw(elicitationResults.value) and includes them in the ElicitResponse content field sent back to the server.
What happens if a user declines an elicitation request?
If the user clicks decline, the declineElicitation function in ElicitationCard.vue creates an ElicitResponse with action: 'decline' and no content payload. This response travels back through the same IPC channels to the main process, where it resolves the pending promise in src/main/mcp/client.ts. The MCP client then returns this decline response to the server, which must abort the privileged operation or handle the denial appropriately.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →