💻 feat: Deeper MCP UI integration in the Chat UI (#9669)

* 💻 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>

* 💻 refactor: Migrate MCP UI resources from index-based to ID-based referencing

- Replace index-based resource markers with stable resource IDs
- Update plugin to parse \ui{resourceId} format instead of \ui0
- Refactor components to use useMessagesOperations instead of useSubmitMessage
- Add ShareMessagesProvider for UI resources in share view
- Add useConversationUIResources hook for cross-turn resource lookups
- Update parsers to generate resource IDs from content hashes
- Update all tests to use resource IDs instead of indices
- Add sandbox permissions for iframe popups
- Remove deprecated MCP tool context instructions

---------

Co-authored-by: Pierre-Luc Godin <pierreluc.godin@shopify.com>
This commit is contained in:
Samuel Path 2025-12-11 22:02:38 +01:00 committed by Danny Avila
parent 4a0fbb07bc
commit 304bba853c
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
27 changed files with 1545 additions and 122 deletions

View file

@ -1,7 +1,12 @@
import crypto from 'node:crypto';
import { Tools } from 'librechat-data-provider';
import type { UIResource } from 'librechat-data-provider';
import type * as t from './types';
function generateResourceId(text: string): string {
return crypto.createHash('sha256').update(text).digest('hex').substring(0, 10);
}
const RECOGNIZED_PROVIDERS = new Set([
'google',
'anthropic',
@ -132,21 +137,34 @@ export function formatToolContent(
},
resource: (item) => {
if (item.resource.uri.startsWith('ui://')) {
uiResources.push(item.resource as UIResource);
}
const isUiResource = item.resource.uri.startsWith('ui://');
const resourceText: string[] = [];
const resourceText = [];
if ('text' in item.resource && item.resource.text != null && item.resource.text) {
if (isUiResource) {
const resourceTextValue = 'text' in item.resource ? item.resource.text : undefined;
const contentToHash = resourceTextValue || item.resource.uri || '';
const resourceId = generateResourceId(contentToHash);
const uiResource: UIResource = {
...item.resource,
resourceId,
};
uiResources.push(uiResource);
resourceText.push(`UI Resource ID: ${resourceId}`);
resourceText.push(`UI Resource Marker: \\ui{${resourceId}}`);
} else if ('text' in item.resource && item.resource.text != null && item.resource.text) {
resourceText.push(`Resource Text: ${item.resource.text}`);
}
if (item.resource.uri.length) {
resourceText.push(`Resource URI: ${item.resource.uri}`);
}
if (item.resource.mimeType != null && item.resource.mimeType) {
resourceText.push(`Resource MIME Type: ${item.resource.mimeType}`);
}
currentTextBlock += (currentTextBlock ? '\n\n' : '') + resourceText.join('\n');
if (resourceText.length) {
currentTextBlock += (currentTextBlock ? '\n\n' : '') + resourceText.join('\n');
}
},
};
@ -160,15 +178,34 @@ export function formatToolContent(
}
}
if (uiResources.length > 0) {
const uiInstructions = `
UI Resource Markers Available:
- Each resource above includes a stable ID and a marker hint like \`\\ui{abc123}\`
- You should usually introduce what you're showing before placing the marker
- For a single resource: \\ui{resource-id}
- For multiple resources shown separately: \\ui{resource-id-a} \\ui{resource-id-b}
- For multiple resources in a carousel: \\ui{resource-id-a,resource-id-b,resource-id-c}
- The UI will be rendered inline where you place the marker
- Format: \\ui{resource-id} or \\ui{id1,id2,id3} using the IDs provided above`;
currentTextBlock += uiInstructions;
}
if (CONTENT_ARRAY_PROVIDERS.has(provider) && currentTextBlock) {
formattedContent.push({ type: 'text', text: currentTextBlock });
}
let artifacts: t.Artifacts = undefined;
if (imageUrls.length || uiResources.length) {
if (imageUrls.length) {
artifacts = { content: imageUrls };
}
if (uiResources.length) {
artifacts = {
...(imageUrls.length && { content: imageUrls }),
...(uiResources.length && { [Tools.ui_resources]: { data: uiResources } }),
...artifacts,
[Tools.ui_resources]: { data: uiResources },
};
}