Skip to content

Commit 64263b2

Browse files
committed
Add support for mcp resources
1 parent 1079b74 commit 64263b2

24 files changed

+1804
-1112
lines changed

browser/data-browser/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@dnd-kit/utilities": "^3.2.2",
1515
"@emoji-mart/react": "^1.1.1",
1616
"@emotion/is-prop-valid": "^1.3.1",
17-
"@modelcontextprotocol/sdk": "^1.6.1",
17+
"@modelcontextprotocol/sdk": "^1.11.4",
1818
"@openrouter/ai-sdk-provider": "^0.4.3",
1919
"@radix-ui/react-popover": "^1.1.2",
2020
"@radix-ui/react-scroll-area": "^1.2.0",
@@ -30,7 +30,7 @@
3030
"@tiptap/starter-kit": "^2.11.7",
3131
"@tiptap/suggestion": "^2.11.7",
3232
"@tomic/react": "workspace:*",
33-
"ai": "^4.1.61",
33+
"ai": "^4.3.16",
3434
"emoji-mart": "^5.6.0",
3535
"polished": "^4.3.1",
3636
"prismjs": "^1.29.0",

browser/data-browser/src/Providers.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import CrashPage from './views/CrashPage';
1818
import { AppSettingsContextProvider } from './helpers/AppSettings';
1919
import { NavStateProvider } from './components/NavState';
2020
import { Toaster } from './components/Toaster';
21+
import { McpServersProvider } from './components/AI/MCP/useMcpServers';
2122

2223
// Setup bugsnag for error handling, but only if there's an API key
2324
const ErrBoundary = window.bugsnagApiKey
@@ -39,37 +40,39 @@ export const Providers: React.FC<React.PropsWithChildren> = ({ children }) => {
3940
return (
4041
<NavStateProvider>
4142
<AppSettingsContextProvider>
42-
<ControlLockProvider>
43-
<HotKeysWrapper>
44-
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
45-
<ThemeWrapper>
46-
<GlobalStyle />
47-
<ErrBoundary FallbackComponent={CrashPage}>
48-
{/* Default form validation provider. Does not do anything on its own but will make sure useValidation works without context*/}
49-
<FormValidationContextProvider
50-
onValidationChange={() => undefined}
51-
>
52-
<Toaster />
53-
<MetaSetter />
54-
<DropdownContainer>
55-
<DialogGlobalContextProvider>
56-
<PopoverContainer>
57-
<DropdownContainer>
58-
<NewResourceUIProvider>
59-
<SkipNav />
60-
<NavWrapper>{children}</NavWrapper>
61-
</NewResourceUIProvider>
62-
</DropdownContainer>
63-
</PopoverContainer>
64-
<NetworkIndicator />
65-
</DialogGlobalContextProvider>
66-
</DropdownContainer>
67-
</FormValidationContextProvider>
68-
</ErrBoundary>
69-
</ThemeWrapper>
70-
</StyleSheetManager>
71-
</HotKeysWrapper>
72-
</ControlLockProvider>
43+
<McpServersProvider>
44+
<ControlLockProvider>
45+
<HotKeysWrapper>
46+
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
47+
<ThemeWrapper>
48+
<GlobalStyle />
49+
<ErrBoundary FallbackComponent={CrashPage}>
50+
{/* Default form validation provider. Does not do anything on its own but will make sure useValidation works without context*/}
51+
<FormValidationContextProvider
52+
onValidationChange={() => undefined}
53+
>
54+
<Toaster />
55+
<MetaSetter />
56+
<DropdownContainer>
57+
<DialogGlobalContextProvider>
58+
<PopoverContainer>
59+
<DropdownContainer>
60+
<NewResourceUIProvider>
61+
<SkipNav />
62+
<NavWrapper>{children}</NavWrapper>
63+
</NewResourceUIProvider>
64+
</DropdownContainer>
65+
</PopoverContainer>
66+
<NetworkIndicator />
67+
</DialogGlobalContextProvider>
68+
</DropdownContainer>
69+
</FormValidationContextProvider>
70+
</ErrBoundary>
71+
</ThemeWrapper>
72+
</StyleSheetManager>
73+
</HotKeysWrapper>
74+
</ControlLockProvider>
75+
</McpServersProvider>
7376
</AppSettingsContextProvider>
7477
</NavStateProvider>
7578
);

browser/data-browser/src/chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput.tsx

Lines changed: 136 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,36 @@ import { useStore } from '@tomic/react';
1212
import { useSettings } from '../../../helpers/AppSettings';
1313
import type { Node } from '@tiptap/pm/model';
1414
import Placeholder from '@tiptap/extension-placeholder';
15+
import { useMcpServers } from '../../../components/AI/MCP/useMcpServers';
16+
import type {
17+
AtomicResourceSuggestion,
18+
MCPResourceSuggestion,
19+
MentionItem,
20+
} from './types';
21+
import { Row } from '../../../components/Row';
22+
import {
23+
IconButton,
24+
IconButtonVariant,
25+
} from '../../../components/IconButton/IconButton';
26+
import { FaArrowRight } from 'react-icons/fa6';
27+
28+
const createAttribute = (propName: string, dataName: string) => {
29+
return {
30+
[propName]: {
31+
default: null,
32+
parseHTML: (element: HTMLElement) => element.getAttribute(dataName),
33+
renderHTML: (attributes: Record<string, unknown>) => {
34+
if (!attributes[propName]) {
35+
return {};
36+
}
37+
38+
return {
39+
[dataName]: attributes[propName],
40+
};
41+
},
42+
},
43+
};
44+
};
1545

1646
// Modify the Mention extension to allow serializing to markdown.
1747
const SerializableMention = Mention.extend({
@@ -27,76 +57,95 @@ const SerializableMention = Mention.extend({
2757
},
2858
};
2959
},
60+
addAttributes() {
61+
return {
62+
...createAttribute('type', 'data-type'),
63+
...createAttribute('serverId', 'data-server-id'),
64+
...createAttribute('mimeType', 'data-mime-type'),
65+
...createAttribute('id', 'data-id'),
66+
...createAttribute('label', 'data-label'),
67+
...createAttribute('isA', 'data-is-a'),
68+
};
69+
},
3070
});
3171

3272
interface AsyncAIChatInputProps {
33-
onMentionUpdate: (mentions: string[]) => void;
73+
onMentionUpdate: (mentions: MentionItem[]) => void;
3474
onChange: (markdown: string) => void;
3575
onSubmit: () => void;
76+
hasFiles: boolean;
3677
}
3778

38-
const AsyncAIChatInput: React.FC<AsyncAIChatInputProps> = ({
39-
onMentionUpdate,
40-
onChange,
41-
onSubmit,
42-
}) => {
79+
const AsyncAIChatInput: React.FC<
80+
React.PropsWithChildren<AsyncAIChatInputProps>
81+
> = ({ onMentionUpdate, onChange, onSubmit, children, hasFiles }) => {
4382
const store = useStore();
44-
const { drive } = useSettings();
83+
const { drive, mcpServers } = useSettings();
4584
const [markdown, setMarkdown] = useState('');
4685
const markdownRef = useRef(markdown);
4786
const onSubmitRef = useRef(onSubmit);
48-
49-
const editor = useEditor({
50-
extensions: [
51-
Markdown.configure({
52-
html: true,
53-
}),
54-
StarterKit.extend({
55-
addKeyboardShortcuts() {
56-
return {
57-
Enter: () => {
58-
// Check if the cursor is in a code block, if so allow the user to press enter.
59-
// Pressing shift + enter will exit the code block.
60-
if ('language' in this.editor.getAttributes('codeBlock')) {
61-
return false;
62-
}
63-
64-
// The content has to be read from a ref because this callback is not updated often leading to stale content.
65-
onSubmitRef.current();
66-
setMarkdown('');
67-
this.editor.commands.clearContent();
68-
69-
return true;
70-
},
71-
};
72-
},
73-
}).configure({
74-
blockquote: false,
75-
bulletList: false,
76-
orderedList: false,
77-
// paragraph: false,
78-
heading: false,
79-
listItem: false,
80-
horizontalRule: false,
81-
bold: false,
82-
strike: false,
83-
italic: false,
84-
}),
85-
SerializableMention.configure({
86-
HTMLAttributes: {
87-
class: 'ai-chat-mention',
88-
},
89-
suggestion: searchSuggestionBuilder(store, drive),
90-
renderText({ options, node }) {
91-
return `${options.suggestion.char}${node.attrs.title}`;
92-
},
93-
}),
94-
Placeholder.configure({
95-
placeholder: 'Ask me anything...',
96-
}),
97-
],
98-
autofocus: true,
99-
});
87+
const { serversWithResources, searchResourcesOfServer } = useMcpServers();
88+
89+
const editor = useEditor(
90+
{
91+
extensions: [
92+
Markdown.configure({
93+
html: true,
94+
}),
95+
StarterKit.extend({
96+
addKeyboardShortcuts() {
97+
return {
98+
Enter: () => {
99+
// Check if the cursor is in a code block, if so allow the user to press enter.
100+
// Pressing shift + enter will exit the code block.
101+
if ('language' in this.editor.getAttributes('codeBlock')) {
102+
return false;
103+
}
104+
105+
// The content has to be read from a ref because this callback is not updated often leading to stale content.
106+
onSubmitRef.current();
107+
setMarkdown('');
108+
this.editor.commands.clearContent();
109+
110+
return true;
111+
},
112+
};
113+
},
114+
}).configure({
115+
blockquote: false,
116+
bulletList: false,
117+
orderedList: false,
118+
heading: false,
119+
listItem: false,
120+
horizontalRule: false,
121+
bold: false,
122+
strike: false,
123+
italic: false,
124+
}),
125+
SerializableMention.configure({
126+
HTMLAttributes: {
127+
class: 'ai-chat-mention',
128+
},
129+
suggestion: searchSuggestionBuilder(
130+
store,
131+
drive,
132+
mcpServers.filter(server =>
133+
serversWithResources.includes(server.id),
134+
),
135+
searchResourcesOfServer,
136+
),
137+
renderText({ options, node }) {
138+
return `${options.suggestion.char}bla${node.attrs.title}`;
139+
},
140+
}),
141+
Placeholder.configure({
142+
placeholder: 'Ask me anything...',
143+
}),
144+
],
145+
autofocus: true,
146+
},
147+
[serversWithResources, searchResourcesOfServer],
148+
);
100149

101150
const handleChange = (value: string) => {
102151
setMarkdown(value);
@@ -108,7 +157,7 @@ const AsyncAIChatInput: React.FC<AsyncAIChatInputProps> = ({
108157
}
109158

110159
const mentions = digForMentions(editor.getJSON());
111-
onMentionUpdate(Array.from(new Set(mentions)));
160+
onMentionUpdate(mentions);
112161
};
113162

114163
useEffect(() => {
@@ -117,12 +166,29 @@ const AsyncAIChatInput: React.FC<AsyncAIChatInputProps> = ({
117166
}, [markdown, onSubmit]);
118167

119168
return (
120-
<EditorWrapper hideEditor={false}>
121-
<TiptapContextProvider editor={editor}>
122-
<EditorContent editor={editor} />
123-
<EditorEvents onChange={handleChange} />
124-
</TiptapContextProvider>
125-
</EditorWrapper>
169+
<>
170+
<EditorWrapper hideEditor={false}>
171+
<TiptapContextProvider editor={editor}>
172+
<EditorContent editor={editor} />
173+
<EditorEvents onChange={handleChange} />
174+
</TiptapContextProvider>
175+
</EditorWrapper>
176+
<Row justify='space-between'>
177+
{children}
178+
<IconButton
179+
disabled={markdown.length === 0 && !hasFiles}
180+
onClick={() => {
181+
onSubmit();
182+
setMarkdown('');
183+
editor?.commands.clearContent();
184+
}}
185+
title='Send'
186+
variant={IconButtonVariant.Fill}
187+
>
188+
<FaArrowRight />
189+
</IconButton>
190+
</Row>
191+
</>
126192
);
127193
};
128194

@@ -141,9 +207,11 @@ const EditorWrapper = styled(EditorWrapperBase)`
141207
}
142208
`;
143209

144-
function digForMentions(data: JSONContent): string[] {
210+
function digForMentions(
211+
data: JSONContent,
212+
): Array<MCPResourceSuggestion | AtomicResourceSuggestion> {
145213
if (data.type === 'mention') {
146-
return [data.attrs!.id];
214+
return [data.attrs as MCPResourceSuggestion | AtomicResourceSuggestion];
147215
}
148216

149217
if (data.content) {

0 commit comments

Comments
 (0)