💻 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,4 +1,4 @@
import { atom } from 'recoil';
import { atom, selectorFamily } from 'recoil';
import { TAttachment } from 'librechat-data-provider';
import { atomWithLocalStorage } from './utils';
import { BadgeItem } from '~/common';
@ -10,6 +10,43 @@ const messageAttachmentsMap = atom<Record<string, TAttachment[] | undefined>>({
default: {},
});
/**
* Selector to get attachments for a specific conversation.
*/
const conversationAttachmentsSelector = selectorFamily<
Record<string, TAttachment[]>,
string | undefined
>({
key: 'conversationAttachments',
get:
(conversationId) =>
({ get }) => {
if (!conversationId) {
return {};
}
const attachmentsMap = get(messageAttachmentsMap);
const result: Record<string, TAttachment[]> = {};
// Filter to only include attachments for this conversation
Object.entries(attachmentsMap).forEach(([messageId, attachments]) => {
if (!attachments) {
return;
}
const relevantAttachments = attachments.filter(
(attachment) => attachment.conversationId === conversationId,
);
if (relevantAttachments.length > 0) {
result[messageId] = relevantAttachments;
}
});
return result;
},
});
const queriesEnabled = atom<boolean>({
key: 'queriesEnabled',
default: true,
@ -30,6 +67,7 @@ const chatBadges = atomWithLocalStorage<Pick<BadgeItem, 'id'>[]>('chatBadges', [
export default {
hideBannerHint,
messageAttachmentsMap,
conversationAttachmentsSelector,
queriesEnabled,
isEditingBadges,
chatBadges,