Skip to content

Commit 65ee74c

Browse files
committed
Editable MCP server settings #951
1 parent 8fac3a1 commit 65ee74c

File tree

7 files changed

+258
-121
lines changed

7 files changed

+258
-121
lines changed

browser/data-browser/package.json

Lines changed: 1 addition & 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.11.4",
17+
"@modelcontextprotocol/sdk": "^1.13.3",
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",
@@ -62,7 +62,6 @@
6262
"@types/prismjs": "^1.26.5",
6363
"@types/react": "^19.0.0",
6464
"@types/react-dom": "^19.0.0",
65-
"@types/react-pdf": "^7.0.0",
6665
"@types/react-window": "^1.8.8",
6766
"@vitejs/plugin-react": "^4.3.4",
6867
"babel-plugin-react-compiler": "19.1.0-rc.2",

browser/data-browser/src/components/AI/AISettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { Column, Row } from '../Row';
33
import { Checkbox, CheckboxLabel } from '../forms/Checkbox';
44
import { InputStyled, InputWrapper } from '../forms/InputStyles';
5-
import { MCPServersManager } from '../MCPServersManager';
5+
import { MCPServersManager } from './MCP/MCPServersManager';
66
import styled from 'styled-components';
77
import { transition } from '../../helpers/transition';
88
import { useSettings } from '../../helpers/AppSettings';

browser/data-browser/src/components/MCPServersManager.tsx renamed to browser/data-browser/src/components/AI/MCP/MCPServersManager.tsx

Lines changed: 21 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useState } from 'react';
22
import { styled } from 'styled-components';
3-
import { FaPlus, FaTrash } from 'react-icons/fa6';
4-
import { Column, Row } from './Row';
5-
import { InputStyled, InputWrapper } from './forms/InputStyles';
6-
import { IconButton, IconButtonVariant } from './IconButton/IconButton';
7-
import type { MCPServer } from './AI/types';
8-
import { BasicSelect } from './forms/BasicSelect';
3+
import { FaPlus } from 'react-icons/fa6';
4+
import { Column, Row } from '../../Row';
5+
import { InputStyled, InputWrapper } from '../../forms/InputStyles';
6+
import { IconButton, IconButtonVariant } from '../../IconButton/IconButton';
7+
import type { MCPServer } from '../types';
8+
import { BasicSelect } from '../../forms/BasicSelect';
9+
import { ServerItem } from './ServerItem';
910

1011
interface MCPServersManagerProps {
1112
servers: MCPServer[];
@@ -40,10 +41,12 @@ export const MCPServersManager: React.FC<MCPServersManagerProps> = ({
4041
setNewServerTransport('http');
4142
};
4243

43-
const removeMcpServer = (index: number) => {
44-
const updatedServers = [...servers];
45-
updatedServers.splice(index, 1);
46-
setServers(updatedServers);
44+
const onServerUpdated = (updated: MCPServer) => {
45+
setServers(servers.map(s => (s.id === updated.id ? updated : s)));
46+
};
47+
48+
const onRemoveServer = (id: string) => {
49+
setServers(servers.filter(s => s.id !== id));
4750
};
4851

4952
return (
@@ -52,21 +55,14 @@ export const MCPServersManager: React.FC<MCPServersManagerProps> = ({
5255
{servers.length === 0 ? (
5356
<EmptyMessage>No MCP servers configured</EmptyMessage>
5457
) : (
55-
servers.map((server, index) => (
56-
<ServerItem key={index}>
57-
<ServerDetails>
58-
<ServerName>{server.name}</ServerName>
59-
<ServerUrl>{server.url}</ServerUrl>
60-
<TransportType>Transport: {server.transport}</TransportType>
61-
</ServerDetails>
62-
<IconButton
63-
color='alert'
64-
onClick={() => removeMcpServer(index)}
65-
title='Remove server'
66-
>
67-
<FaTrash />
68-
</IconButton>
69-
</ServerItem>
58+
servers.map(server => (
59+
<ServerItem
60+
key={server.id}
61+
server={server}
62+
onServerUpdated={onServerUpdated}
63+
onRemove={() => onRemoveServer(server.id)}
64+
disabled={false}
65+
/>
7066
))
7167
)}
7268
</ServerList>
@@ -134,36 +130,6 @@ const ServerList = styled.div`
134130
margin-bottom: 1rem;
135131
`;
136132

137-
const ServerItem = styled.div`
138-
display: flex;
139-
justify-content: space-between;
140-
align-items: center;
141-
padding: 0.75rem;
142-
border-radius: 4px;
143-
background-color: ${p => p.theme.colors.bg1};
144-
border: 1px solid ${p => p.theme.colors.bg2};
145-
`;
146-
147-
const ServerDetails = styled.div`
148-
display: flex;
149-
flex-direction: column;
150-
gap: 0.25rem;
151-
`;
152-
153-
const ServerName = styled.div`
154-
font-weight: bold;
155-
`;
156-
157-
const ServerUrl = styled.div`
158-
font-size: 0.9em;
159-
color: ${p => p.theme.colors.textLight};
160-
`;
161-
162-
const TransportType = styled.div`
163-
font-size: 0.8em;
164-
color: ${p => p.theme.colors.textLight || '#888'};
165-
`;
166-
167133
const EmptyMessage = styled.div`
168134
padding: ${p => p.theme.size()};
169135
text-align: center;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { styled } from 'styled-components';
3+
import { FaCheck, FaPencil, FaTrash, FaXmark } from 'react-icons/fa6';
4+
import { Row } from '../../Row';
5+
import { InputStyled, InputWrapper } from '../../forms/InputStyles';
6+
import { IconButton, IconButtonVariant } from '../../IconButton/IconButton';
7+
import { BasicSelect } from '../../forms/BasicSelect';
8+
import type { MCPServer } from '../types';
9+
10+
export interface ServerItemProps {
11+
server: MCPServer;
12+
onServerUpdated: (updated: MCPServer) => void;
13+
onRemove: () => void;
14+
disabled: boolean;
15+
}
16+
17+
export const ServerItem: React.FC<ServerItemProps> = ({
18+
server,
19+
onServerUpdated,
20+
onRemove,
21+
disabled,
22+
}) => {
23+
const [isEditing, setIsEditing] = useState(false);
24+
const [editServer, setEditServer] = useState<MCPServer>(server);
25+
26+
// Keep local edit state in sync if server prop changes (e.g. after save)
27+
28+
const startEdit = () => {
29+
setEditServer(server);
30+
setIsEditing(true);
31+
};
32+
33+
const cancelEdit = () => {
34+
setEditServer(server);
35+
setIsEditing(false);
36+
};
37+
38+
const saveEdit = () => {
39+
if (!editServer.name || !editServer.url || !editServer.transport) return;
40+
onServerUpdated(editServer);
41+
setIsEditing(false);
42+
};
43+
44+
useEffect(() => {
45+
setEditServer(server);
46+
}, [server]);
47+
48+
return (
49+
<ServerItemRoot>
50+
{isEditing ? (
51+
<ServerDetails
52+
as='form'
53+
onSubmit={e => {
54+
e.preventDefault();
55+
saveEdit();
56+
}}
57+
>
58+
<InputWrapper>
59+
<InputStyled
60+
type='text'
61+
value={editServer.name}
62+
onChange={e =>
63+
setEditServer(s => ({ ...s, name: e.target.value }))
64+
}
65+
placeholder='Server name'
66+
required
67+
/>
68+
</InputWrapper>
69+
<InputWrapper>
70+
<InputStyled
71+
type='url'
72+
value={editServer.url}
73+
onChange={e =>
74+
setEditServer(s => ({ ...s, url: e.target.value }))
75+
}
76+
placeholder='Server URL'
77+
required
78+
/>
79+
</InputWrapper>
80+
<StyledSelect
81+
value={editServer.transport}
82+
onChange={e =>
83+
setEditServer(s => ({
84+
...s,
85+
transport: e.target.value as 'http' | 'sse',
86+
}))
87+
}
88+
title='Select transport type'
89+
>
90+
<option value='http'>HTTP</option>
91+
<option value='sse'>SSE</option>
92+
</StyledSelect>
93+
<Row gap='0.5rem'>
94+
<IconButton
95+
variant={IconButtonVariant.Fill}
96+
color='main'
97+
type='submit'
98+
title='Save changes'
99+
disabled={
100+
!editServer.name || !editServer.url || !editServer.transport
101+
}
102+
>
103+
<FaCheck />
104+
</IconButton>
105+
<IconButton
106+
color='textLight'
107+
onClick={cancelEdit}
108+
title='Cancel edit'
109+
type='button'
110+
>
111+
<FaXmark />
112+
</IconButton>
113+
</Row>
114+
</ServerDetails>
115+
) : (
116+
<ServerDetails>
117+
<ServerName>{server.name}</ServerName>
118+
<ServerUrl>{server.url}</ServerUrl>
119+
<TransportType>Transport: {server.transport}</TransportType>
120+
</ServerDetails>
121+
)}
122+
<Row gap='0.25rem'>
123+
<IconButton
124+
color='main'
125+
onClick={startEdit}
126+
title='Edit server'
127+
disabled={disabled || isEditing}
128+
>
129+
<FaPencil />
130+
</IconButton>
131+
<IconButton
132+
color='textLight'
133+
onClick={onRemove}
134+
title='Remove server'
135+
disabled={disabled}
136+
>
137+
<FaTrash />
138+
</IconButton>
139+
</Row>
140+
</ServerItemRoot>
141+
);
142+
};
143+
144+
const ServerItemRoot = styled.div`
145+
display: flex;
146+
justify-content: space-between;
147+
align-items: center;
148+
padding: 0.75rem;
149+
border-radius: 4px;
150+
/* background-color: ${p => p.theme.colors.bg1}; */
151+
border: 1px solid ${p => p.theme.colors.bg2};
152+
`;
153+
154+
const ServerDetails = styled.div`
155+
display: flex;
156+
flex-direction: column;
157+
gap: 0.25rem;
158+
width: 100%;
159+
`;
160+
161+
const ServerName = styled.div`
162+
font-weight: bold;
163+
`;
164+
165+
const ServerUrl = styled.div`
166+
font-size: 0.9em;
167+
color: ${p => p.theme.colors.textLight};
168+
`;
169+
170+
const TransportType = styled.div`
171+
font-size: 0.8em;
172+
color: ${p => p.theme.colors.textLight || '#888'};
173+
`;
174+
175+
const StyledSelect = styled(BasicSelect)`
176+
select {
177+
min-width: 7ch;
178+
margin-left: 0.5rem;
179+
}
180+
`;

browser/data-browser/src/components/AI/useGenerativeData.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ import {
77
import { useSettings } from '../../helpers/AppSettings';
88
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
99

10-
const generateTitleSystemPrompt = `You are part of a well oiled machine that responds to user input.
10+
const generateTitleSystemPrompt = (
11+
conversation: string,
12+
) => `You are part of a well oiled machine that responds to user input.
1113
It is your job to think of a short title that fits the given conversation.
1214
The user will provide a JSON object containing the conversation.
1315
1416
ALWAYS USE THE SAME LANGUAGE AS THE USER!
15-
ONLY RESPOND WITH JUST THE TITLE, NOTHING ELSE! NO FORMATTING OR EXTRA TEXT!`;
17+
ONLY RESPOND WITH JUST THE TITLE, NOTHING ELSE! NO FORMATTING OR EXTRA TEXT!
18+
19+
Here follows the conversation as a JSON object:
20+
\`\`\`json
21+
${conversation}
22+
\`\`\`
23+
`;
1624

1725
export const useGenerativeData = () => {
1826
const { openRouterApiKey } = useSettings() as { openRouterApiKey?: string };
@@ -34,8 +42,8 @@ export const useGenerativeData = () => {
3442

3543
const { text } = await generateText({
3644
model: openrouter('google/gemma-3-4b-it:free'),
37-
system: generateTitleSystemPrompt,
38-
prompt: convoString,
45+
// Google/gemma-3-4b-it:free doesn't support system prompts so we have to do it this way
46+
prompt: generateTitleSystemPrompt(convoString),
3947
});
4048

4149
const cleaned = text.trim();

browser/data-browser/src/components/IconButton/IconButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ const MagicIconButton = styled(IconButtonBase)<ButtonStyleProps>`
226226
inset: 0;
227227
opacity: 0;
228228
z-index: -2;
229+
will-change: filter;
229230
background: radial-gradient(ellipse at top right, #365ccd, transparent),
230231
radial-gradient(
231232
ellipse at bottom left,
@@ -245,6 +246,7 @@ const MagicIconButton = styled(IconButtonBase)<ButtonStyleProps>`
245246
position: absolute;
246247
inset: var(--border-width);
247248
opacity: var(--bg-opacity);
249+
will-change: transform;
248250
border-radius: calc(${p => p.theme.radius} - var(--border-width));
249251
background: ${p => p.theme.colors.bg};
250252
z-index: -1;

0 commit comments

Comments
 (0)