💻 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

@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { Tools } from 'librechat-data-provider';
import type { TAttachment, UIResource } from 'librechat-data-provider';
import { useMessagesOperations } from '~/Providers';
import store from '~/store';
/**
* Hook to collect all UI resources in a conversation, indexed by resource ID.
* This enables cross-turn resource references in the conversation.
* Works in both main app (using React Query cache) and share view (using context messages).
*
* @param conversationId - The ID of the conversation to collect resources from
* @returns A Map of resource IDs to UIResource objects
*/
export function useConversationUIResources(
conversationId: string | undefined,
): Map<string, UIResource> {
const { getMessages } = useMessagesOperations();
const conversationAttachmentsMap = useRecoilValue(
store.conversationAttachmentsSelector(conversationId),
);
return useMemo(() => {
const map = new Map<string, UIResource>();
const collectResources = (attachments?: TAttachment[]) => {
attachments
?.filter((attachment) => attachment?.type === Tools.ui_resources)
.forEach((attachment) => {
const resources = attachment?.[Tools.ui_resources];
if (Array.isArray(resources)) {
resources.forEach((resource) => {
if (resource?.resourceId) {
map.set(resource.resourceId, resource);
}
});
}
});
};
// Collect from messages (works in both main app and share view)
getMessages()?.forEach((message) => {
collectResources(message.attachments);
});
// Collect from in-flight messages (Recoil state during streaming - only when we have a conversationId)
if (conversationId) {
Object.values(conversationAttachmentsMap).forEach(collectResources);
}
return map;
}, [conversationId, getMessages, conversationAttachmentsMap]);
}

View file

@ -33,5 +33,5 @@ export { default as useDocumentTitle } from './useDocumentTitle';
export { default as useSpeechToText } from './Input/useSpeechToText';
export { default as useTextToSpeech } from './Input/useTextToSpeech';
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
export { useResourcePermissions } from './useResourcePermissions';
export { default as useLocalizedConfig } from './useLocalizedConfig';
export { default as useResourcePermissions } from './useResourcePermissions';

View file

@ -24,3 +24,5 @@ export const useResourcePermissions = (resourceType: ResourceType, resourceId: s
permissionBits: data?.permissionBits || 0,
};
};
export default useResourcePermissions;