💻 feat: deeper MCP UI integration in the chat UI using plugins

---------

Co-authored-by: Samuel Path <samuel.path@shopify.com>
Co-authored-by: Pierre-Luc Godin <pierreluc.godin@shopify.com>
This commit is contained in:
Pierre-Luc Godin 2025-10-16 14:51:01 -04:00 committed by Dustin Healy
parent b8a149e563
commit 08103ffb22
22 changed files with 1441 additions and 54 deletions

View file

@ -335,6 +335,15 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
};
continue;
} else if (tool && mcpToolPattern.test(tool)) {
toolContextMap[tool] = `# MCP Tool \`${tool}\`:
When this tool returns UI resources (URIs starting with "ui://"):
- You should usually introduce what you're showing before the marker
- For a single resource: \\ui0
- For multiple resources shown separately: \\ui0 \\ui1
- For multiple resources in a carousel: \\ui0,1,2
- The UI will be rendered inline where you place the marker
- Format: \\ui{index} or \\ui{index1,index2,index3} where indices are 0-based, in the order they appear in the tool response`;
const [toolName, serverName] = tool.split(Constants.mcp_delimiter);
if (toolName === Constants.mcp_server) {
/** Placeholder used for UI purposes */

View file

@ -73,10 +73,10 @@ describe('createToolEndCallback', () => {
tool_call_id: 'tool123',
artifact: {
[Tools.ui_resources]: {
data: {
0: { type: 'button', label: 'Click me' },
1: { type: 'input', placeholder: 'Enter text' },
},
data: [
{ type: 'button', label: 'Click me' },
{ type: 'input', placeholder: 'Enter text' },
],
},
},
};
@ -100,10 +100,10 @@ describe('createToolEndCallback', () => {
messageId: 'run456',
toolCallId: 'tool123',
conversationId: 'thread789',
[Tools.ui_resources]: {
0: { type: 'button', label: 'Click me' },
1: { type: 'input', placeholder: 'Enter text' },
},
[Tools.ui_resources]: [
{ type: 'button', label: 'Click me' },
{ type: 'input', placeholder: 'Enter text' },
],
});
});
@ -115,9 +115,7 @@ describe('createToolEndCallback', () => {
tool_call_id: 'tool123',
artifact: {
[Tools.ui_resources]: {
data: {
0: { type: 'carousel', items: [] },
},
data: [{ type: 'carousel', items: [] }],
},
},
};
@ -136,9 +134,7 @@ describe('createToolEndCallback', () => {
messageId: 'run456',
toolCallId: 'tool123',
conversationId: 'thread789',
[Tools.ui_resources]: {
0: { type: 'carousel', items: [] },
},
[Tools.ui_resources]: [{ type: 'carousel', items: [] }],
});
});
@ -155,9 +151,7 @@ describe('createToolEndCallback', () => {
tool_call_id: 'tool123',
artifact: {
[Tools.ui_resources]: {
data: {
0: { type: 'test' },
},
data: [{ type: 'test' }],
},
},
};
@ -184,9 +178,7 @@ describe('createToolEndCallback', () => {
tool_call_id: 'tool123',
artifact: {
[Tools.ui_resources]: {
data: {
0: { type: 'chart', data: [] },
},
data: [{ type: 'chart', data: [] }],
},
[Tools.web_search]: {
results: ['result1', 'result2'],
@ -209,9 +201,7 @@ describe('createToolEndCallback', () => {
// Check ui_resources attachment
const uiResourceAttachment = results.find((r) => r?.type === Tools.ui_resources);
expect(uiResourceAttachment).toBeTruthy();
expect(uiResourceAttachment[Tools.ui_resources]).toEqual({
0: { type: 'chart', data: [] },
});
expect(uiResourceAttachment[Tools.ui_resources]).toEqual([{ type: 'chart', data: [] }]);
// Check web_search attachment
const webSearchAttachment = results.find((r) => r?.type === Tools.web_search);
@ -250,7 +240,7 @@ describe('createToolEndCallback', () => {
tool_call_id: 'tool123',
artifact: {
[Tools.ui_resources]: {
data: {},
data: [],
},
},
};
@ -268,7 +258,7 @@ describe('createToolEndCallback', () => {
messageId: 'run456',
toolCallId: 'tool123',
conversationId: 'thread789',
[Tools.ui_resources]: {},
[Tools.ui_resources]: [],
});
});