Embeddable UI
This document describes the general communication protocol for any embeddable UIs. This is implemented by the mcp-ui iframe based solution, in the context of UI over MCP.
Concepts
- Embedded iframes communicate with the parent window via
postMessage
. - The parent window can send messages to the iframe.
- The iframe can send messages to the parent window.
Communication Protocol
Message Structure
typescript
type Message = {
type: string;
messageId?: string; // optional, used for tracking the message
payload: Record<string, unknown>;
};
1
2
3
4
5
2
3
4
5
Message Types
intent
- the user has interacted with the UI and expressed an intent, and the host should act on itnotify
- the iframe already acted upon the user interaction, and is notifying the host to trigger any side effectsprompt
- the iframe asks the host to run a prompttool
- the iframe asks the host to run a tool calllink
- the iframe asks the host to navigate to a link
intent
- indicates that the user has interacted with the UI and expressed an intent, and the host should act on it
- the payload is an object with the following properties:
intent
- the intent that the user expressedparams
- the parameters to pass to the intent
Example:
typescript
window.parent.postMessage(
{
type: "intent",
payload: {
intent: "create-task",
params: {
title: "Buy groceries",
description: "Buy groceries for the week",
},
},
},
"*"
);
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
notify
- indicates that the iframe already acted upon the user interaction, and is notifying the host to trigger any side effects
- the payload is an object with the following properties:
message
- the message to notify the host with
Example:
typescript
window.parent.postMessage(
{
type: "notify",
payload: {
message: "cart-updated",
},
},
"*"
);
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
prompt
- indicates that the iframe asks the host to run a prompt
- the payload is an object with the following properties:
prompt
- the prompt to run
Example:
typescript
window.parent.postMessage(
{
type: "prompt",
payload: {
prompt: "What is the weather in Tokyo?",
},
},
"*"
);
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
tool
- indicates that the iframe asks the host to run a tool call
- the payload is an object with the following properties:
toolName
- the name of the tool to runparams
- the parameters to pass to the tool
Example:
typescript
window.parent.postMessage(
{
type: "tool",
payload: {
toolName: "get-weather",
params: {
city: "Tokyo",
},
},
},
"*"
);
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
link
- indicates that the iframe asks the host to navigate to a link
- the payload is an object with the following properties:
url
- the URL to navigate to
Example:
typescript
window.parent.postMessage(
{
type: "link",
payload: {
url: "https://www.google.com",
},
},
"*"
);
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Reserved Message Types - from the iframe to the host
ui-lifecycle-iframe-ready
- the iframe is ready to receive messagesui-size-change
- the iframe's size has changed and the host should adjust the iframe's sizeui-request-data
- the iframe sends a request to the host to request data
ui-lifecycle-iframe-ready
- indicates that the iframe is ready to receive messages
Example: See Render Data
ui-size-change
- indicates that the iframe's size has changed and the host should adjust the iframe's size
- the payload is an object with the following properties:
width
- the new width of the iframeheight
- the new height of the iframe
Example:
typescript
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
window.parent.postMessage(
{
type: "ui-size-change",
payload: {
height: entry.contentRect.height,
},
},
"*"
);
});
});
resizeObserver.observe(document.documentElement);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ui-request-data
- a message that the iframe sends to the host to request data. The message must include a
messageId
to allow the iframe to track the response. - the payload is an object with the following properties:
requestType
- the type of the requestparams
- the parameters to pass to the request
Example:
typescript
window.parent.postMessage(
{
type: "ui-request-data",
messageId: "123",
payload: {
requestType: "get-payment-methods",
params: {
// any params needed for the request
},
},
},
"*"
);
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
See also Asynchronous Data Requests with Message IDs
Reserved Message Types - from the host to the iframe
ui-lifecycle-iframe-render-data
- the host sends the iframe render dataui-message-received
- the host sends the iframe to indicate that the action has been receivedui-message-response
- the iframe sends the host to indicate that the action has been processed or failed
ui-lifecycle-iframe-render-data
- a message that the host sends to the iframe to pass any relevant render data
- the payload is an object with the following properties:
renderData
- the render data to pass to the iframe
Example: See Render Data
ui-message-received
- a message that the host sends to the iframe to indicate that the action has been received. The original messageId is passed back to the host to allow the host to track the action. This is useful for
request-data
messages, but is not limited to this type. Example: See Asynchronous Data Requests with Message IDs
ui-message-response
- a message that the iframe sends to the host to indicate that the action has been processed. The original messageId is passed back to the host to allow the host to track the action. This is useful for
request-data
messages, but is not limited to this type. - the payload is an object with the following properties:
response
- the response to the actionerror
- the error, if any, that occurred
Example: See Asynchronous Data Requests with Message IDs
Query Parameters
waitForRenderData
- a query parameter that can be passed to the iframe to indicate that the iframe should wait for the render data to be passed before sending any messages
- the value of the query parameter is a boolean
- if the query parameter is present, the iframe will wait for the render data to be passed before sending any messages
Example: See Render Data
Usage Examples
Passing Render Data to the Iframe
In the host:
typescript
iframeSrc = "https://my-embeddable-ui.com?waitForRenderData=true";
iframe = document.createElement("iframe");
iframe.src = iframeSrc; // the iframe will wait for the render data to be passed before rendering
document.body.appendChild(iframe);
window.addEventListener("message", (event) => {
if (event.data.type === "ui-lifecycle-iframe-ready") {
iframe.contentWindow.postMessage(
{
type: "ui-lifecycle-iframe-render-data",
payload: { renderData: { theme: "dark" } },
},
"*"
);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In the iframe:
typescript
// In the iframe's script
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("waitForRenderData") === "true") {
let customRenderData = null;
// The parent will send this message on load or when we notify it we're ready
window.addEventListener("message", (event) => {
// Add origin checks for security
if (event.data.type === "ui-lifecycle-iframe-render-data") {
// If the iframe has already received data, we don't need to do anything
if (customRenderData) {
return;
} else {
customRenderData = event.data.payload.renderData;
// Now you can render the UI with the received data
renderUI(renderData);
}
}
});
// We can let the parent know we're ready to receive data
window.parent.postMessage({ type: "ui-lifecycle-iframe-ready" }, "*");
} else {
// If the iframe doesn't need to wait for data, we can render the default UI immediately
renderUI();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Asynchronous Data Requests with Message IDs
In the iframe:
typescript
const requests = new Map<string, (response: any) => void>();
button.addEventListener("click", () => {
const messageId = crypto.randomUUID();
requests.set(messageId, (response) => {
// handle the response
});
window.parent.postMessage(
{
type: "ui-request-data",
messageId,
payload: {
requestType: "get-payment-methods",
params: {
// any params needed for the request
},
},
},
"*"
);
});
window.addEventListener("message", (event) => {
if (event.data.type === "ui-message-response") {
const { messageId, response, error } = event.data.payload;
if (error) {
// handle the error
} else {
const callback = requests.get(messageId);
if (callback) {
callback(response);
}
}
// clean up the request
requests.delete(messageId);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
In the host:
typescript
window.addEventListener("message", async (event) => {
if (event.data.messageId) {
iframe.contentWindow.postMessage(
{
type: "ui-message-received",
messageId: event.data.messageId,
},
"*"
);
}
if (event.data.type === "ui-request-data") {
const {
messageId,
payload: { requestType, params },
} = event.data;
if (requestType === "get-payment-methods") {
try {
const paymentMethods = await fetchPaymentMethods(params);
iframe.contentWindow.postMessage(
{
type: "ui-message-response",
messageId: messageId,
payload: { response: { paymentMethods } },
},
"*"
);
} catch (error) {
iframe.contentWindow.postMessage(
{
type: "ui-message-response",
messageId: messageId,
payload: { error },
},
"*"
);
}
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40