@bibobonking/opensol 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -1
- package/README.md +2 -2
- package/desktop/src-tauri/Cargo.toml +1 -1
- package/dist/assets/index-CeN54A2N.css +1 -0
- package/dist/assets/index-fhIO9a7_.js +318 -0
- package/dist/index.html +4 -4
- package/index.html +2 -2
- package/package.json +1 -1
- package/server/README.md +2 -2
- package/server/daemon.mjs +76 -1
- package/server/lib/mcp-oauth.mjs +219 -0
- package/server/lib/mcp-oauth.test.mjs +175 -0
- package/server/lib/mcp.mjs +404 -37
- package/src/App.tsx +2 -0
- package/src/components/CommandPalette.tsx +14 -1
- package/src/components/Logo.tsx +1 -1
- package/src/components/McpPanel.tsx +687 -0
- package/src/components/ModelDeck.tsx +12 -3
- package/src/components/SettingsModal.tsx +43 -5
- package/src/components/Sidebar.tsx +2 -1
- package/src/components/Splash.tsx +1 -1
- package/src/data/providers.ts +1 -1
- package/src/lib/daemon.ts +10 -1
- package/src/lib/mcp-config.test.mjs +47 -0
- package/src/lib/mcp-config.ts +203 -0
- package/src/store/mcp.ts +28 -1
- package/src/store/settings.ts +1 -1
- package/src/store/ui.ts +4 -0
- package/src/types.ts +14 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-DdI1_iPD.css +0 -1
- package/dist/assets/index-egG5Bgfo.js +0 -312
- package/src/components/McpTab.tsx +0 -337
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { ChevronDown, Plug, Plus, RefreshCw, Trash2, Unplug, Wrench } from 'lucide-react'
|
|
3
|
-
import type { McpServer } from '../types'
|
|
4
|
-
import { useMcp } from '../store/mcp'
|
|
5
|
-
import { useUi } from '../store/ui'
|
|
6
|
-
import { cn } from '../lib/utils'
|
|
7
|
-
import { Segmented, Spinner, Switch } from './ui'
|
|
8
|
-
import type { McpServerInput } from '../lib/daemon'
|
|
9
|
-
|
|
10
|
-
const field =
|
|
11
|
-
'h-9 w-full rounded-xl border border-line bg-panel px-3 text-[12px] text-ink outline-none transition-all placeholder:text-faint focus:border-accent/50 focus:shadow-[0_0_0_3px_rgb(var(--accent)/0.10)]'
|
|
12
|
-
|
|
13
|
-
const PRESETS: Array<{ label: string; draft: Draft }> = [
|
|
14
|
-
{ label: 'Filesystem', draft: { name: 'Filesystem', transport: 'stdio', command: 'npx', args: '-y @modelcontextprotocol/server-filesystem .', env: '', url: '', headers: '' } },
|
|
15
|
-
{ label: 'Memory', draft: { name: 'Memory', transport: 'stdio', command: 'npx', args: '-y @modelcontextprotocol/server-memory', env: '', url: '', headers: '' } },
|
|
16
|
-
{ label: 'Fetch', draft: { name: 'Fetch', transport: 'stdio', command: 'uvx', args: 'mcp-server-fetch', env: '', url: '', headers: '' } },
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
interface Draft {
|
|
20
|
-
name: string
|
|
21
|
-
transport: 'stdio' | 'http'
|
|
22
|
-
command: string
|
|
23
|
-
args: string
|
|
24
|
-
env: string
|
|
25
|
-
url: string
|
|
26
|
-
headers: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const EMPTY: Draft = { name: '', transport: 'stdio', command: '', args: '', env: '', url: '', headers: '' }
|
|
30
|
-
|
|
31
|
-
/** Shell-like split that keeps quoted segments together. */
|
|
32
|
-
function splitArgs(value: string): string[] {
|
|
33
|
-
const out: string[] = []
|
|
34
|
-
const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g
|
|
35
|
-
let match: RegExpExecArray | null
|
|
36
|
-
while ((match = pattern.exec(value))) out.push(match[1] ?? match[2] ?? match[3])
|
|
37
|
-
return out
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parsePairs(value: string, separator: string): Record<string, string> {
|
|
41
|
-
const out: Record<string, string> = {}
|
|
42
|
-
for (const line of value.split('\n')) {
|
|
43
|
-
const index = line.indexOf(separator)
|
|
44
|
-
if (index < 1) continue
|
|
45
|
-
out[line.slice(0, index).trim()] = line.slice(index + 1).trim()
|
|
46
|
-
}
|
|
47
|
-
return out
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function toInput(server: McpServer): McpServerInput {
|
|
51
|
-
return {
|
|
52
|
-
id: server.id,
|
|
53
|
-
name: server.name,
|
|
54
|
-
transport: server.transport,
|
|
55
|
-
enabled: server.enabled,
|
|
56
|
-
approval: server.approval,
|
|
57
|
-
disabledTools: server.disabledTools,
|
|
58
|
-
command: server.command,
|
|
59
|
-
args: server.args,
|
|
60
|
-
env: server.env,
|
|
61
|
-
url: server.url,
|
|
62
|
-
headers: server.headers,
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function StatusDot({ status }: { status: McpServer['status'] }) {
|
|
67
|
-
return (
|
|
68
|
-
<span
|
|
69
|
-
className={cn(
|
|
70
|
-
'h-2 w-2 shrink-0 rounded-full',
|
|
71
|
-
status === 'connected' && 'bg-success shadow-[0_0_0_3px_rgb(var(--success)/0.18)]',
|
|
72
|
-
status === 'connecting' && 'animate-breathe bg-warning',
|
|
73
|
-
status === 'error' && 'bg-danger',
|
|
74
|
-
(status === 'disconnected' || status === 'disabled') && 'bg-faint/60',
|
|
75
|
-
)}
|
|
76
|
-
/>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function McpTab() {
|
|
81
|
-
const servers = useMcp((state) => state.servers)
|
|
82
|
-
const loaded = useMcp((state) => state.loaded)
|
|
83
|
-
const loading = useMcp((state) => state.loading)
|
|
84
|
-
const error = useMcp((state) => state.error)
|
|
85
|
-
const load = useMcp((state) => state.load)
|
|
86
|
-
const save = useMcp((state) => state.save)
|
|
87
|
-
const act = useMcp((state) => state.act)
|
|
88
|
-
const notify = useUi((state) => state.notify)
|
|
89
|
-
const askConfirm = useUi((state) => state.askConfirm)
|
|
90
|
-
const [draft, setDraft] = useState<Draft | null>(null)
|
|
91
|
-
const [open, setOpen] = useState<string | null>(null)
|
|
92
|
-
const [busy, setBusy] = useState(false)
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
void load()
|
|
96
|
-
}, [load])
|
|
97
|
-
|
|
98
|
-
const persist = async (next: McpServerInput[], message?: string) => {
|
|
99
|
-
setBusy(true)
|
|
100
|
-
try {
|
|
101
|
-
await save(next)
|
|
102
|
-
if (message) notify(message, 'success')
|
|
103
|
-
} catch (failure) {
|
|
104
|
-
notify(failure instanceof Error ? failure.message : 'Could not save MCP servers.', 'error')
|
|
105
|
-
} finally {
|
|
106
|
-
setBusy(false)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const addServer = async () => {
|
|
111
|
-
if (!draft || !draft.name.trim()) return
|
|
112
|
-
const entry: McpServerInput =
|
|
113
|
-
draft.transport === 'stdio'
|
|
114
|
-
? { name: draft.name.trim(), transport: 'stdio', command: draft.command.trim(), args: splitArgs(draft.args), env: parsePairs(draft.env, '=') }
|
|
115
|
-
: { name: draft.name.trim(), transport: 'http', url: draft.url.trim(), headers: parsePairs(draft.headers, ':') }
|
|
116
|
-
await persist([...servers.map(toInput), { ...entry, enabled: true, approval: 'ask' }], `Added ${entry.name}. Connecting…`)
|
|
117
|
-
setDraft(null)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const patch = (id: string, change: Partial<McpServerInput>) =>
|
|
121
|
-
persist(servers.map((server) => (server.id === id ? { ...toInput(server), ...change } : toInput(server))))
|
|
122
|
-
|
|
123
|
-
const remove = async (server: McpServer) => {
|
|
124
|
-
const ok = await askConfirm({
|
|
125
|
-
title: `Remove ${server.name}?`,
|
|
126
|
-
body: 'The server is disconnected and its tools disappear from the agent. Nothing on disk is deleted.',
|
|
127
|
-
confirmLabel: 'Remove server',
|
|
128
|
-
tone: 'danger',
|
|
129
|
-
})
|
|
130
|
-
if (ok) await persist(servers.filter((item) => item.id !== server.id).map(toInput), `Removed ${server.name}.`)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<div className="space-y-5 px-5 py-6 sm:px-8">
|
|
135
|
-
<div className="settings-card">
|
|
136
|
-
<div className="flex items-start gap-3">
|
|
137
|
-
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-accent-soft text-accent">
|
|
138
|
-
<Plug size={16} />
|
|
139
|
-
</span>
|
|
140
|
-
<div className="min-w-0 flex-1">
|
|
141
|
-
<p className="settings-label">Model Context Protocol</p>
|
|
142
|
-
<p className="settings-hint">
|
|
143
|
-
Connect MCP servers and their tools show up for the agent as <span className="font-mono">mcp__server__tool</span>. Local
|
|
144
|
-
(stdio) servers run as a process on this computer with your permissions, so only add servers you trust.
|
|
145
|
-
</p>
|
|
146
|
-
</div>
|
|
147
|
-
{!draft && (
|
|
148
|
-
<button
|
|
149
|
-
type="button"
|
|
150
|
-
onClick={() => setDraft({ ...EMPTY })}
|
|
151
|
-
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-full bg-ink px-3.5 text-[11.5px] font-medium text-panel transition-transform active:scale-95"
|
|
152
|
-
>
|
|
153
|
-
<Plus size={13} /> Add server
|
|
154
|
-
</button>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
{draft && (
|
|
160
|
-
<div className="settings-card animate-scalein space-y-3">
|
|
161
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
162
|
-
<p className="settings-label mr-auto">New MCP server</p>
|
|
163
|
-
{PRESETS.map((preset) => (
|
|
164
|
-
<button key={preset.label} type="button" className="chip h-7 rounded-full px-3 text-[11px]" onClick={() => setDraft({ ...preset.draft })}>
|
|
165
|
-
{preset.label}
|
|
166
|
-
</button>
|
|
167
|
-
))}
|
|
168
|
-
</div>
|
|
169
|
-
<div className="grid gap-3 sm:grid-cols-[1fr_auto]">
|
|
170
|
-
<input className={field} placeholder="Name (e.g. GitHub)" value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} />
|
|
171
|
-
<Segmented
|
|
172
|
-
options={[
|
|
173
|
-
{ value: 'stdio', label: 'Local · stdio' },
|
|
174
|
-
{ value: 'http', label: 'Remote · HTTP' },
|
|
175
|
-
]}
|
|
176
|
-
value={draft.transport}
|
|
177
|
-
onChange={(value) => setDraft({ ...draft, transport: value })}
|
|
178
|
-
size="md"
|
|
179
|
-
/>
|
|
180
|
-
</div>
|
|
181
|
-
{draft.transport === 'stdio' ? (
|
|
182
|
-
<>
|
|
183
|
-
<div className="grid gap-3 sm:grid-cols-[160px_1fr]">
|
|
184
|
-
<input className={cn(field, 'font-mono')} placeholder="command" value={draft.command} onChange={(e) => setDraft({ ...draft, command: e.target.value })} />
|
|
185
|
-
<input className={cn(field, 'font-mono')} placeholder="arguments" value={draft.args} onChange={(e) => setDraft({ ...draft, args: e.target.value })} />
|
|
186
|
-
</div>
|
|
187
|
-
<textarea
|
|
188
|
-
className={cn(field, 'h-20 resize-none py-2 font-mono')}
|
|
189
|
-
placeholder={'Environment, one per line\nAPI_KEY=…'}
|
|
190
|
-
value={draft.env}
|
|
191
|
-
onChange={(e) => setDraft({ ...draft, env: e.target.value })}
|
|
192
|
-
/>
|
|
193
|
-
</>
|
|
194
|
-
) : (
|
|
195
|
-
<>
|
|
196
|
-
<input className={cn(field, 'font-mono')} placeholder="https://example.com/mcp" value={draft.url} onChange={(e) => setDraft({ ...draft, url: e.target.value })} />
|
|
197
|
-
<textarea
|
|
198
|
-
className={cn(field, 'h-20 resize-none py-2 font-mono')}
|
|
199
|
-
placeholder={'Headers, one per line\nAuthorization: Bearer …'}
|
|
200
|
-
value={draft.headers}
|
|
201
|
-
onChange={(e) => setDraft({ ...draft, headers: e.target.value })}
|
|
202
|
-
/>
|
|
203
|
-
</>
|
|
204
|
-
)}
|
|
205
|
-
<div className="flex justify-end gap-2">
|
|
206
|
-
<button type="button" className="chip h-8 rounded-full px-4" onClick={() => setDraft(null)}>
|
|
207
|
-
Cancel
|
|
208
|
-
</button>
|
|
209
|
-
<button
|
|
210
|
-
type="button"
|
|
211
|
-
disabled={busy || !draft.name.trim() || (draft.transport === 'stdio' ? !draft.command.trim() : !draft.url.trim())}
|
|
212
|
-
onClick={() => void addServer()}
|
|
213
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-full bg-accent px-4 text-[11.5px] font-medium text-on-accent transition-transform active:scale-95 disabled:opacity-40"
|
|
214
|
-
>
|
|
215
|
-
{busy && <Spinner size={11} />} Save and connect
|
|
216
|
-
</button>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
|
|
221
|
-
{error && <p className="rounded-xl border border-danger/25 bg-danger/[0.05] px-3 py-2 text-[11.5px] text-danger">{error}</p>}
|
|
222
|
-
{!loaded && loading && (
|
|
223
|
-
<p className="flex items-center gap-2 text-[12px] text-faint">
|
|
224
|
-
<Spinner size={12} /> Loading servers…
|
|
225
|
-
</p>
|
|
226
|
-
)}
|
|
227
|
-
{loaded && servers.length === 0 && !draft && (
|
|
228
|
-
<p className="rounded-2xl border border-dashed border-line px-5 py-10 text-center text-[12px] text-muted">
|
|
229
|
-
No MCP servers yet. Add one to give the agent new tools.
|
|
230
|
-
</p>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
{servers.map((server, index) => {
|
|
234
|
-
const expanded = open === server.id
|
|
235
|
-
return (
|
|
236
|
-
<div key={server.id} style={{ animationDelay: `${index * 50}ms` }} className="settings-card animate-rise !p-0">
|
|
237
|
-
<div className="flex items-center gap-3 px-4 py-3.5">
|
|
238
|
-
<StatusDot status={server.status} />
|
|
239
|
-
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setOpen(expanded ? null : server.id)}>
|
|
240
|
-
<p className="flex items-center gap-2 text-[13px] font-medium text-ink">
|
|
241
|
-
<span className="truncate">{server.name}</span>
|
|
242
|
-
<span className="rounded-full bg-raised px-2 py-px font-mono text-[9.5px] text-faint">{server.transport}</span>
|
|
243
|
-
</p>
|
|
244
|
-
<p className="mt-0.5 truncate text-[11px] text-muted">
|
|
245
|
-
{server.status === 'connected'
|
|
246
|
-
? `${server.tools.length} tool${server.tools.length === 1 ? '' : 's'}${server.serverInfo?.name ? ` · ${server.serverInfo.name} ${server.serverInfo.version ?? ''}` : ''}`
|
|
247
|
-
: server.status === 'connecting'
|
|
248
|
-
? 'Connecting…'
|
|
249
|
-
: server.status === 'error'
|
|
250
|
-
? server.error || 'Connection failed'
|
|
251
|
-
: server.status === 'disabled'
|
|
252
|
-
? 'Turned off'
|
|
253
|
-
: 'Not connected'}
|
|
254
|
-
</p>
|
|
255
|
-
</button>
|
|
256
|
-
{server.status === 'connected' || server.status === 'connecting' ? (
|
|
257
|
-
<>
|
|
258
|
-
<button type="button" className="icon-button h-8 w-8" title="Reload tools" aria-label="Reload tools" onClick={() => void act(server.id, 'refresh')}>
|
|
259
|
-
<RefreshCw size={13} className={cn(server.status === 'connecting' && 'animate-spin')} />
|
|
260
|
-
</button>
|
|
261
|
-
<button type="button" className="icon-button h-8 w-8" title="Disconnect" aria-label="Disconnect" onClick={() => void act(server.id, 'disconnect')}>
|
|
262
|
-
<Unplug size={13} />
|
|
263
|
-
</button>
|
|
264
|
-
</>
|
|
265
|
-
) : (
|
|
266
|
-
<button
|
|
267
|
-
type="button"
|
|
268
|
-
className="chip h-8 rounded-full px-3.5 text-[11.5px]"
|
|
269
|
-
disabled={!server.enabled}
|
|
270
|
-
onClick={() => void act(server.id, 'connect')}
|
|
271
|
-
>
|
|
272
|
-
Connect
|
|
273
|
-
</button>
|
|
274
|
-
)}
|
|
275
|
-
<Switch checked={server.enabled} onChange={(enabled) => void patch(server.id, { enabled })} label={`Enable ${server.name}`} />
|
|
276
|
-
<button type="button" className="icon-button h-8 w-8" aria-label="Expand" onClick={() => setOpen(expanded ? null : server.id)}>
|
|
277
|
-
<ChevronDown size={14} className={cn('transition-transform duration-300', expanded && 'rotate-180')} />
|
|
278
|
-
</button>
|
|
279
|
-
</div>
|
|
280
|
-
{expanded && (
|
|
281
|
-
<div className="animate-fade space-y-3 border-t border-line px-4 py-3.5">
|
|
282
|
-
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
283
|
-
<p className="font-mono text-[10.5px] text-faint">
|
|
284
|
-
{server.transport === 'stdio' ? `${server.command} ${(server.args ?? []).join(' ')}` : server.url}
|
|
285
|
-
</p>
|
|
286
|
-
<div className="flex items-center gap-2">
|
|
287
|
-
<span className="text-[11px] text-muted">Tool calls</span>
|
|
288
|
-
<Segmented
|
|
289
|
-
options={[
|
|
290
|
-
{ value: 'ask', label: 'Ask first' },
|
|
291
|
-
{ value: 'auto', label: 'Auto-run' },
|
|
292
|
-
]}
|
|
293
|
-
value={server.approval}
|
|
294
|
-
onChange={(approval) => void patch(server.id, { approval })}
|
|
295
|
-
/>
|
|
296
|
-
</div>
|
|
297
|
-
</div>
|
|
298
|
-
{server.status === 'error' && server.error && <p className="text-[11.5px] text-danger">{server.error}</p>}
|
|
299
|
-
{server.tools.length > 0 && (
|
|
300
|
-
<ul className="divide-y divide-line rounded-xl border border-line">
|
|
301
|
-
{server.tools.map((tool) => {
|
|
302
|
-
const off = server.disabledTools.includes(tool.name)
|
|
303
|
-
return (
|
|
304
|
-
<li key={tool.name} className="flex items-center gap-3 px-3 py-2">
|
|
305
|
-
<Wrench size={12} className={cn('shrink-0', off ? 'text-faint' : 'text-accent')} />
|
|
306
|
-
<div className="min-w-0 flex-1">
|
|
307
|
-
<p className={cn('truncate font-mono text-[11.5px]', off ? 'text-faint line-through' : 'text-ink')}>{tool.name}</p>
|
|
308
|
-
{tool.description && <p className="truncate text-[10.5px] text-muted">{tool.description}</p>}
|
|
309
|
-
</div>
|
|
310
|
-
{tool.readOnly && <span className="rounded-full bg-success/10 px-1.5 py-px text-[9px] text-success">read-only</span>}
|
|
311
|
-
<Switch
|
|
312
|
-
checked={!off}
|
|
313
|
-
label={`Enable ${tool.name}`}
|
|
314
|
-
onChange={(on) =>
|
|
315
|
-
void patch(server.id, {
|
|
316
|
-
disabledTools: on ? server.disabledTools.filter((n) => n !== tool.name) : [...server.disabledTools, tool.name],
|
|
317
|
-
})
|
|
318
|
-
}
|
|
319
|
-
/>
|
|
320
|
-
</li>
|
|
321
|
-
)
|
|
322
|
-
})}
|
|
323
|
-
</ul>
|
|
324
|
-
)}
|
|
325
|
-
<div className="flex justify-end">
|
|
326
|
-
<button type="button" onClick={() => void remove(server)} className="inline-flex items-center gap-1.5 text-[11.5px] font-medium text-faint transition-colors hover:text-danger">
|
|
327
|
-
<Trash2 size={12} /> Remove server
|
|
328
|
-
</button>
|
|
329
|
-
</div>
|
|
330
|
-
</div>
|
|
331
|
-
)}
|
|
332
|
-
</div>
|
|
333
|
-
)
|
|
334
|
-
})}
|
|
335
|
-
</div>
|
|
336
|
-
)
|
|
337
|
-
}
|