@frockbot/plugin-settings 0.0.0 → 0.1.0
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/frockbot.json +55 -0
- package/package.json +43 -6
- package/src/backend.test.ts +397 -0
- package/src/backend.ts +262 -0
- package/src/client/BotPanel.vue +57 -0
- package/src/client/BotSettingsSurface.vue +1076 -0
- package/src/client/BotSettingsTrigger.vue +26 -0
- package/src/client/ConnectionsSurface.vue +813 -0
- package/src/client/ModelsSurface.vue +419 -0
- package/src/client/PackageAccounts.vue +330 -0
- package/src/client/PackageCatalogSurface.vue +584 -0
- package/src/client/PackageSettingsForm.vue +150 -0
- package/src/client/PackageSettingsSection.vue +66 -0
- package/src/client/PluginsSurface.vue +412 -0
- package/src/client/PluginsTrigger.vue +62 -0
- package/src/client/UserProfileTrigger.vue +223 -0
- package/src/client/UserSettingsSurface.vue +242 -0
- package/src/client/assignment-operations.ts +22 -0
- package/src/client/bot-settings.test.ts +218 -0
- package/src/client/bot-settings.ts +150 -0
- package/src/client/index.test.ts +113 -0
- package/src/client/index.ts +78 -0
- package/src/client/package-settings.test.ts +63 -0
- package/src/client/package-settings.ts +77 -0
- package/src/client/package-surfaces.test.ts +147 -0
- package/src/client/package-surfaces.ts +93 -0
- package/src/client/user-display-name.test.ts +44 -0
- package/src/client/user-display-name.ts +16 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +3 -0
- package/src/user.test.ts +1277 -0
- package/src/user.ts +1221 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +30 -0
- package/README.md +0 -3
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Connections: the accounts and services a User authorizes and may then give a
|
|
4
|
+
* Bot access to — Composio, Gmail, a remote MCP server.
|
|
5
|
+
*
|
|
6
|
+
* It holds authorization and its durable state only. Whether the Package
|
|
7
|
+
* providing a connector is enabled at all is Plugins' question, and a model
|
|
8
|
+
* provider's account belongs to Models even though it is a Connection too.
|
|
9
|
+
*/
|
|
10
|
+
import { clientSurfaceRegistryKey } from "@frockbot/client-core";
|
|
11
|
+
import { UiAnchor, UiButton } from "@frockbot/client-ui";
|
|
12
|
+
import {
|
|
13
|
+
frockBotWebDataKey,
|
|
14
|
+
type PluginCatalogItem,
|
|
15
|
+
} from "@frockbot/plugin-shell/shared";
|
|
16
|
+
import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
|
|
17
|
+
import { computed, inject, onMounted, ref } from "vue";
|
|
18
|
+
import { configurablePackages } from "./package-surfaces.js";
|
|
19
|
+
import PackageAccounts from "./PackageAccounts.vue";
|
|
20
|
+
import PackageSettingsForm from "./PackageSettingsForm.vue";
|
|
21
|
+
|
|
22
|
+
const providedSurfaces = inject(clientSurfaceRegistryKey);
|
|
23
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
24
|
+
if (!providedSurfaces || !providedWeb) {
|
|
25
|
+
throw new Error("settings client services were not provided");
|
|
26
|
+
}
|
|
27
|
+
const surfaces = providedSurfaces;
|
|
28
|
+
const web = providedWeb;
|
|
29
|
+
// Connections are User-scoped, so the row's link names no Bot.
|
|
30
|
+
const connectionsLink = settingsLinkV1({ anchor: "user-connections" });
|
|
31
|
+
|
|
32
|
+
const connectors = computed(() =>
|
|
33
|
+
configurablePackages({
|
|
34
|
+
catalog: web.value.pluginCatalog,
|
|
35
|
+
packages: web.value.userSettings?.packages ?? [],
|
|
36
|
+
home: "connections",
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The connect cards. Drawn from the Connection projection — which carries no
|
|
42
|
+
* URL — so a card can be shown by anything that can read Connections, while
|
|
43
|
+
* the redirect is authored only when the User presses *Reconnect*.
|
|
44
|
+
*/
|
|
45
|
+
const pendingAuthorizations = computed(() =>
|
|
46
|
+
(web.value.userSettings?.connections ?? []).filter(
|
|
47
|
+
(connection) =>
|
|
48
|
+
connection.pendingAuthorization !== undefined &&
|
|
49
|
+
connection.state !== "revoked",
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
const reconnectingConnectionId = ref<string>();
|
|
53
|
+
|
|
54
|
+
const apiKeyPackageId = ref<string>();
|
|
55
|
+
const apiKeyConnectionTypeId = ref<string>();
|
|
56
|
+
const apiKeyLabel = ref("");
|
|
57
|
+
const apiKey = ref("");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The MCP server form. A remote MCP server is a Connection like any other, but
|
|
61
|
+
* it needs its URL and transport as well as an optional key, so the generic
|
|
62
|
+
* API-key form cannot carry it. Both commands it can send are the ordinary
|
|
63
|
+
* Connection commands: `connection/create-api-key` when a key is given, and
|
|
64
|
+
* `connection/create` when the server is public.
|
|
65
|
+
*/
|
|
66
|
+
const MCP_PACKAGE_ID = "mcp";
|
|
67
|
+
const mcpFormOpen = ref(false);
|
|
68
|
+
const mcpLabel = ref("");
|
|
69
|
+
const mcpUrl = ref("");
|
|
70
|
+
const mcpTransport = ref<"streamable-http" | "sse">("streamable-http");
|
|
71
|
+
const mcpApiKey = ref("");
|
|
72
|
+
/**
|
|
73
|
+
* How the custom server authenticates. `oauth` is not a third field on the
|
|
74
|
+
* same form but a different command: it starts an authorization the host
|
|
75
|
+
* authors a redirect for, rather than creating a Connection from a secret the
|
|
76
|
+
* User pasted.
|
|
77
|
+
*/
|
|
78
|
+
const mcpAuthMode = ref<"none" | "key" | "oauth">("none");
|
|
79
|
+
const mcpScope = ref("");
|
|
80
|
+
const mcpClientId = ref("");
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The MCP lifecycle panel: the durable server records the User Durable Object
|
|
84
|
+
* owns, which the Connection rows cannot show — a server's `needs-auth` or
|
|
85
|
+
* `error` state, when it last handshook, what its instructions are, and the
|
|
86
|
+
* refusals this build recorded rather than performed.
|
|
87
|
+
*/
|
|
88
|
+
const editingInstructionsFor = ref<string>();
|
|
89
|
+
const instructionsDraft = ref("");
|
|
90
|
+
const restartingServerId = ref<string>();
|
|
91
|
+
const mcpServers = computed(() => web.value.mcpServers?.servers ?? []);
|
|
92
|
+
const mcpRefusals = computed(() => web.value.mcpServers?.refusals ?? []);
|
|
93
|
+
|
|
94
|
+
onMounted(() => {
|
|
95
|
+
void web.value.loadPluginCatalog();
|
|
96
|
+
void web.value.loadMcpServers();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function mcpStateLabel(state: string): string {
|
|
100
|
+
if (state === "needs-auth") return "Needs authorization";
|
|
101
|
+
if (state === "connecting") return "Connecting";
|
|
102
|
+
if (state === "error") return "Error";
|
|
103
|
+
return "Ready";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handshakeLabel(value: string): string {
|
|
107
|
+
const parsed = Date.parse(value);
|
|
108
|
+
return Number.isFinite(parsed) ? new Date(parsed).toLocaleString() : value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function beginInstructions(serverId: string, instructions: string): void {
|
|
112
|
+
editingInstructionsFor.value = serverId;
|
|
113
|
+
instructionsDraft.value = instructions;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cancelInstructions(): void {
|
|
117
|
+
editingInstructionsFor.value = undefined;
|
|
118
|
+
instructionsDraft.value = "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function saveInstructions(serverId: string): Promise<void> {
|
|
122
|
+
await web.value.setMcpInstructions(serverId, instructionsDraft.value);
|
|
123
|
+
cancelInstructions();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function restartServer(serverId: string): Promise<void> {
|
|
127
|
+
restartingServerId.value = serverId;
|
|
128
|
+
try {
|
|
129
|
+
await web.value.restartMcpServer(serverId);
|
|
130
|
+
} finally {
|
|
131
|
+
restartingServerId.value = undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* *Reconnect*: the authenticated command that mints a fresh redirect, with
|
|
137
|
+
* fresh PKCE and a fresh signed state. Nothing was stored waiting for this —
|
|
138
|
+
* the card only ever said that a decision was pending.
|
|
139
|
+
*/
|
|
140
|
+
async function reconnect(connectionId: string): Promise<void> {
|
|
141
|
+
reconnectingConnectionId.value = connectionId;
|
|
142
|
+
try {
|
|
143
|
+
const redirect = await web.value.startMcpAuthorization({ connectionId });
|
|
144
|
+
if (redirect) await web.value.openConnectionAuthorization(redirect);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
web.value.settingsError =
|
|
147
|
+
error instanceof Error ? error.message : "Could not reconnect";
|
|
148
|
+
} finally {
|
|
149
|
+
reconnectingConnectionId.value = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function connectionCount(packageId: string): number {
|
|
154
|
+
return (web.value.userSettings?.connections ?? []).filter(
|
|
155
|
+
(connection) =>
|
|
156
|
+
connection.packageId === packageId && connection.state !== "revoked",
|
|
157
|
+
).length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function beginMcpConnection(): void {
|
|
161
|
+
mcpFormOpen.value = true;
|
|
162
|
+
mcpLabel.value = "";
|
|
163
|
+
mcpUrl.value = "";
|
|
164
|
+
mcpTransport.value = "streamable-http";
|
|
165
|
+
mcpApiKey.value = "";
|
|
166
|
+
mcpAuthMode.value = "none";
|
|
167
|
+
mcpScope.value = "";
|
|
168
|
+
mcpClientId.value = "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cancelMcpConnection(): void {
|
|
172
|
+
mcpFormOpen.value = false;
|
|
173
|
+
mcpApiKey.value = "";
|
|
174
|
+
mcpAuthMode.value = "none";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function addMcpServer(): Promise<void> {
|
|
178
|
+
const settings = {
|
|
179
|
+
url: mcpUrl.value.trim(),
|
|
180
|
+
transport: mcpTransport.value,
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
if (mcpAuthMode.value === "oauth") {
|
|
184
|
+
// No secret is collected and none is stored: the server is discovered,
|
|
185
|
+
// a client is registered, and the redirect is authored by the host.
|
|
186
|
+
const redirect = await web.value.startMcpAuthorization({
|
|
187
|
+
label: mcpLabel.value,
|
|
188
|
+
settings: {
|
|
189
|
+
...settings,
|
|
190
|
+
...(mcpScope.value.trim() ? { scope: mcpScope.value.trim() } : {}),
|
|
191
|
+
...(mcpClientId.value.trim()
|
|
192
|
+
? { "client-id": mcpClientId.value.trim() }
|
|
193
|
+
: {}),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
cancelMcpConnection();
|
|
197
|
+
if (redirect) await web.value.openConnectionAuthorization(redirect);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (mcpApiKey.value) {
|
|
201
|
+
await web.value.createApiKeyConnection({
|
|
202
|
+
packageId: MCP_PACKAGE_ID,
|
|
203
|
+
connectionTypeId: "mcp-remote-key",
|
|
204
|
+
label: mcpLabel.value,
|
|
205
|
+
apiKey: mcpApiKey.value,
|
|
206
|
+
settings,
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
await web.value.createConnection({
|
|
210
|
+
packageId: MCP_PACKAGE_ID,
|
|
211
|
+
connectionTypeId: "mcp-remote",
|
|
212
|
+
label: mcpLabel.value,
|
|
213
|
+
settings,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
cancelMcpConnection();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
mcpApiKey.value = "";
|
|
219
|
+
web.value.settingsError =
|
|
220
|
+
error instanceof Error ? error.message : "Could not add the MCP server";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Start whichever authorization the card's Connection Type declares. */
|
|
225
|
+
function beginConnect(item: PluginCatalogItem): void {
|
|
226
|
+
if (item.packageId === MCP_PACKAGE_ID) {
|
|
227
|
+
beginMcpConnection();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const connectionType = item.connectionTypes[0];
|
|
231
|
+
if (!connectionType) return;
|
|
232
|
+
if (connectionType.authorizationKind === "api-key") {
|
|
233
|
+
apiKeyPackageId.value = item.packageId;
|
|
234
|
+
apiKeyConnectionTypeId.value = connectionType.id;
|
|
235
|
+
apiKeyLabel.value = item.displayName;
|
|
236
|
+
apiKey.value = "";
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
void connect(item.packageId, connectionType.id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function cancelApiKeyConnection(): void {
|
|
243
|
+
apiKey.value = "";
|
|
244
|
+
apiKeyPackageId.value = undefined;
|
|
245
|
+
apiKeyConnectionTypeId.value = undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function connectApiKey(): Promise<void> {
|
|
249
|
+
if (!apiKeyPackageId.value || !apiKeyConnectionTypeId.value) return;
|
|
250
|
+
try {
|
|
251
|
+
await web.value.createApiKeyConnection({
|
|
252
|
+
packageId: apiKeyPackageId.value,
|
|
253
|
+
connectionTypeId: apiKeyConnectionTypeId.value,
|
|
254
|
+
label: apiKeyLabel.value,
|
|
255
|
+
apiKey: apiKey.value,
|
|
256
|
+
});
|
|
257
|
+
cancelApiKeyConnection();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
apiKey.value = "";
|
|
260
|
+
web.value.settingsError =
|
|
261
|
+
error instanceof Error ? error.message : "Could not create Connection";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function connect(
|
|
266
|
+
packageId: string,
|
|
267
|
+
connectionTypeId: string,
|
|
268
|
+
): Promise<void> {
|
|
269
|
+
try {
|
|
270
|
+
const redirectUrl = await web.value.startConnection(
|
|
271
|
+
packageId,
|
|
272
|
+
connectionTypeId,
|
|
273
|
+
);
|
|
274
|
+
if (redirectUrl) {
|
|
275
|
+
await web.value.openConnectionAuthorization(redirectUrl);
|
|
276
|
+
}
|
|
277
|
+
await web.value.loadPluginCatalog();
|
|
278
|
+
} catch (error) {
|
|
279
|
+
web.value.settingsError =
|
|
280
|
+
error instanceof Error ? error.message : "Could not start Connection";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
</script>
|
|
284
|
+
|
|
285
|
+
<template>
|
|
286
|
+
<div class="connections-surface">
|
|
287
|
+
<UiAnchor
|
|
288
|
+
anchor="user-connections"
|
|
289
|
+
label="Connections"
|
|
290
|
+
:href="connectionsLink"
|
|
291
|
+
class="settings-row"
|
|
292
|
+
>
|
|
293
|
+
<p class="field-hint">
|
|
294
|
+
Accounts and services you can give your Bots access to. A Bot still
|
|
295
|
+
needs an explicit Assignment before it can use one.
|
|
296
|
+
</p>
|
|
297
|
+
|
|
298
|
+
<section
|
|
299
|
+
v-if="pendingAuthorizations.length"
|
|
300
|
+
class="connect-cards"
|
|
301
|
+
aria-label="Connections that need your authorization"
|
|
302
|
+
>
|
|
303
|
+
<article
|
|
304
|
+
v-for="connection in pendingAuthorizations"
|
|
305
|
+
:key="connection.connectionId"
|
|
306
|
+
class="connect-card"
|
|
307
|
+
>
|
|
308
|
+
<div class="connect-card-copy">
|
|
309
|
+
<strong>{{ connection.pendingAuthorization?.label }}</strong>
|
|
310
|
+
<span>
|
|
311
|
+
Needs your authorization. Only you can complete it — FrockBot
|
|
312
|
+
creates the sign-in link when you press Reconnect, and nothing is
|
|
313
|
+
stored waiting for it.
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
<UiButton
|
|
317
|
+
variant="primary"
|
|
318
|
+
:disabled="reconnectingConnectionId === connection.connectionId"
|
|
319
|
+
@click="reconnect(connection.connectionId)"
|
|
320
|
+
>
|
|
321
|
+
{{
|
|
322
|
+
reconnectingConnectionId === connection.connectionId
|
|
323
|
+
? "Opening…"
|
|
324
|
+
: "Reconnect"
|
|
325
|
+
}}
|
|
326
|
+
</UiButton>
|
|
327
|
+
</article>
|
|
328
|
+
</section>
|
|
329
|
+
|
|
330
|
+
<div class="connector-grid">
|
|
331
|
+
<article
|
|
332
|
+
v-for="item in connectors"
|
|
333
|
+
:key="item.packageId"
|
|
334
|
+
class="connector-card"
|
|
335
|
+
>
|
|
336
|
+
<div class="connector-summary">
|
|
337
|
+
<span class="connector-logo" aria-hidden="true">
|
|
338
|
+
{{ item.displayName.slice(0, 1) }}
|
|
339
|
+
</span>
|
|
340
|
+
<span class="connector-copy">
|
|
341
|
+
<strong>{{ item.displayName }}</strong>
|
|
342
|
+
<small>
|
|
343
|
+
{{
|
|
344
|
+
item.connectionTypes
|
|
345
|
+
.map((connection) => connection.displayName)
|
|
346
|
+
.join(", ")
|
|
347
|
+
}}
|
|
348
|
+
</small>
|
|
349
|
+
</span>
|
|
350
|
+
<UiButton
|
|
351
|
+
v-if="
|
|
352
|
+
connectionCount(item.packageId) === 0 ||
|
|
353
|
+
item.connectionTypes[0]?.allowMultiple
|
|
354
|
+
"
|
|
355
|
+
@click="beginConnect(item)"
|
|
356
|
+
>
|
|
357
|
+
{{
|
|
358
|
+
connectionCount(item.packageId) === 0
|
|
359
|
+
? "Connect"
|
|
360
|
+
: "Add another account"
|
|
361
|
+
}}
|
|
362
|
+
</UiButton>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<PackageAccounts :item="item" />
|
|
366
|
+
|
|
367
|
+
<div
|
|
368
|
+
v-if="
|
|
369
|
+
item.packageId === MCP_PACKAGE_ID &&
|
|
370
|
+
(mcpServers.length > 0 || mcpRefusals.length > 0)
|
|
371
|
+
"
|
|
372
|
+
class="mcp-status"
|
|
373
|
+
>
|
|
374
|
+
<div
|
|
375
|
+
v-for="server in mcpServers"
|
|
376
|
+
:key="server.serverId"
|
|
377
|
+
class="mcp-server"
|
|
378
|
+
>
|
|
379
|
+
<div class="mcp-server-head">
|
|
380
|
+
<span class="mcp-server-name">{{ server.label }}</span>
|
|
381
|
+
<span :class="['mcp-state', `mcp-state-${server.state}`]">
|
|
382
|
+
{{ mcpStateLabel(server.state) }}
|
|
383
|
+
</span>
|
|
384
|
+
</div>
|
|
385
|
+
<p class="mcp-meta">
|
|
386
|
+
{{ server.toolCount }} tools · epoch {{ server.serverEpoch }} ·
|
|
387
|
+
last handshake {{ handshakeLabel(server.lastHandshakeAt) }}
|
|
388
|
+
</p>
|
|
389
|
+
<p v-if="server.failure" class="connection-failure">
|
|
390
|
+
{{ server.failure.code }}: {{ server.failure.message }}
|
|
391
|
+
</p>
|
|
392
|
+
<p v-if="server.instructions" class="mcp-instructions">
|
|
393
|
+
{{ server.instructions }}
|
|
394
|
+
</p>
|
|
395
|
+
<div class="connector-actions">
|
|
396
|
+
<UiButton
|
|
397
|
+
@click="
|
|
398
|
+
beginInstructions(
|
|
399
|
+
server.serverId,
|
|
400
|
+
server.instructions ?? '',
|
|
401
|
+
)
|
|
402
|
+
"
|
|
403
|
+
>
|
|
404
|
+
Instructions
|
|
405
|
+
</UiButton>
|
|
406
|
+
<UiButton
|
|
407
|
+
:disabled="restartingServerId === server.serverId"
|
|
408
|
+
@click="restartServer(server.serverId)"
|
|
409
|
+
>
|
|
410
|
+
{{
|
|
411
|
+
restartingServerId === server.serverId
|
|
412
|
+
? "Restarting…"
|
|
413
|
+
: "Restart"
|
|
414
|
+
}}
|
|
415
|
+
</UiButton>
|
|
416
|
+
</div>
|
|
417
|
+
<form
|
|
418
|
+
v-if="editingInstructionsFor === server.serverId"
|
|
419
|
+
class="api-key-form"
|
|
420
|
+
@submit.prevent="saveInstructions(server.serverId)"
|
|
421
|
+
>
|
|
422
|
+
<label>
|
|
423
|
+
<span>Instructions for this server's tools</span>
|
|
424
|
+
<textarea
|
|
425
|
+
v-model="instructionsDraft"
|
|
426
|
+
maxlength="4096"
|
|
427
|
+
rows="4"
|
|
428
|
+
></textarea>
|
|
429
|
+
</label>
|
|
430
|
+
<div class="api-key-actions">
|
|
431
|
+
<UiButton @click="cancelInstructions">Cancel</UiButton>
|
|
432
|
+
<UiButton type="submit" variant="primary">Save</UiButton>
|
|
433
|
+
</div>
|
|
434
|
+
</form>
|
|
435
|
+
</div>
|
|
436
|
+
<p
|
|
437
|
+
v-for="refusal in mcpRefusals"
|
|
438
|
+
:key="refusal.refusalId"
|
|
439
|
+
class="connection-failure"
|
|
440
|
+
>
|
|
441
|
+
{{ refusal.code }}: {{ refusal.message }}
|
|
442
|
+
</p>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<form
|
|
446
|
+
v-if="mcpFormOpen && item.packageId === MCP_PACKAGE_ID"
|
|
447
|
+
class="api-key-form"
|
|
448
|
+
@submit.prevent="addMcpServer"
|
|
449
|
+
>
|
|
450
|
+
<label>
|
|
451
|
+
<span>Server name</span>
|
|
452
|
+
<input v-model="mcpLabel" maxlength="120" required />
|
|
453
|
+
</label>
|
|
454
|
+
<label>
|
|
455
|
+
<span>Server URL</span>
|
|
456
|
+
<input
|
|
457
|
+
v-model="mcpUrl"
|
|
458
|
+
type="url"
|
|
459
|
+
inputmode="url"
|
|
460
|
+
placeholder="https://example.com/mcp"
|
|
461
|
+
maxlength="2048"
|
|
462
|
+
required
|
|
463
|
+
/>
|
|
464
|
+
</label>
|
|
465
|
+
<label>
|
|
466
|
+
<span>Transport</span>
|
|
467
|
+
<select v-model="mcpTransport">
|
|
468
|
+
<option value="streamable-http">Streamable HTTP</option>
|
|
469
|
+
<option value="sse">Server-sent events</option>
|
|
470
|
+
</select>
|
|
471
|
+
</label>
|
|
472
|
+
<label>
|
|
473
|
+
<span>Authentication</span>
|
|
474
|
+
<select v-model="mcpAuthMode">
|
|
475
|
+
<option value="none">None (public server)</option>
|
|
476
|
+
<option value="key">API key</option>
|
|
477
|
+
<option value="oauth">OAuth</option>
|
|
478
|
+
</select>
|
|
479
|
+
</label>
|
|
480
|
+
<label v-if="mcpAuthMode === 'key'">
|
|
481
|
+
<span>API key</span>
|
|
482
|
+
<input
|
|
483
|
+
v-model="mcpApiKey"
|
|
484
|
+
type="password"
|
|
485
|
+
autocomplete="new-password"
|
|
486
|
+
/>
|
|
487
|
+
</label>
|
|
488
|
+
<template v-if="mcpAuthMode === 'oauth'">
|
|
489
|
+
<label>
|
|
490
|
+
<span>Scope (optional)</span>
|
|
491
|
+
<input v-model="mcpScope" maxlength="1024" autocomplete="off" />
|
|
492
|
+
</label>
|
|
493
|
+
<label>
|
|
494
|
+
<span>Client ID (optional)</span>
|
|
495
|
+
<input
|
|
496
|
+
v-model="mcpClientId"
|
|
497
|
+
maxlength="512"
|
|
498
|
+
autocomplete="off"
|
|
499
|
+
/>
|
|
500
|
+
</label>
|
|
501
|
+
<p class="field-hint">
|
|
502
|
+
Leave both empty for a server that registers clients itself,
|
|
503
|
+
which is what most do. FrockBot registers as a public client; a
|
|
504
|
+
server that issues a client secret is refused, because a secret
|
|
505
|
+
would have to live in this Connection's settings. You will be
|
|
506
|
+
sent to the server to sign in, and no token ever reaches this
|
|
507
|
+
page.
|
|
508
|
+
</p>
|
|
509
|
+
</template>
|
|
510
|
+
<div class="api-key-actions">
|
|
511
|
+
<UiButton @click="cancelMcpConnection">Cancel</UiButton>
|
|
512
|
+
<UiButton type="submit" variant="primary">
|
|
513
|
+
{{
|
|
514
|
+
mcpAuthMode === "oauth" ? "Continue to sign in" : "Add server"
|
|
515
|
+
}}
|
|
516
|
+
</UiButton>
|
|
517
|
+
</div>
|
|
518
|
+
</form>
|
|
519
|
+
|
|
520
|
+
<form
|
|
521
|
+
v-if="apiKeyPackageId === item.packageId"
|
|
522
|
+
class="api-key-form"
|
|
523
|
+
@submit.prevent="connectApiKey"
|
|
524
|
+
>
|
|
525
|
+
<label>
|
|
526
|
+
<span>Connection label</span>
|
|
527
|
+
<input v-model="apiKeyLabel" maxlength="120" required />
|
|
528
|
+
</label>
|
|
529
|
+
<label>
|
|
530
|
+
<span>API key</span>
|
|
531
|
+
<input
|
|
532
|
+
v-model="apiKey"
|
|
533
|
+
type="password"
|
|
534
|
+
autocomplete="new-password"
|
|
535
|
+
required
|
|
536
|
+
/>
|
|
537
|
+
</label>
|
|
538
|
+
<div class="api-key-actions">
|
|
539
|
+
<UiButton @click="cancelApiKeyConnection">Cancel</UiButton>
|
|
540
|
+
<UiButton type="submit" variant="primary">
|
|
541
|
+
Connect account
|
|
542
|
+
</UiButton>
|
|
543
|
+
</div>
|
|
544
|
+
</form>
|
|
545
|
+
|
|
546
|
+
<PackageSettingsForm :item="item" />
|
|
547
|
+
</article>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div v-if="connectors.length === 0" class="connections-empty">
|
|
551
|
+
<p>
|
|
552
|
+
No connectors are enabled yet. Enable one in Plugins — Composio,
|
|
553
|
+
Gmail, or a remote MCP server — and it will appear here to connect.
|
|
554
|
+
</p>
|
|
555
|
+
<UiButton type="button" @click="surfaces.open('plugins')">
|
|
556
|
+
Open Plugins
|
|
557
|
+
</UiButton>
|
|
558
|
+
</div>
|
|
559
|
+
</UiAnchor>
|
|
560
|
+
|
|
561
|
+
<p v-if="web.settingsError" class="settings-error" role="alert">
|
|
562
|
+
{{ web.settingsError }}
|
|
563
|
+
</p>
|
|
564
|
+
</div>
|
|
565
|
+
</template>
|
|
566
|
+
|
|
567
|
+
<style scoped>
|
|
568
|
+
.connections-surface {
|
|
569
|
+
display: flex;
|
|
570
|
+
flex-direction: column;
|
|
571
|
+
gap: 16px;
|
|
572
|
+
padding: 16px;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.settings-row {
|
|
576
|
+
display: flex;
|
|
577
|
+
flex-direction: column;
|
|
578
|
+
gap: 16px;
|
|
579
|
+
padding-right: var(--frock-control-sm);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.field-hint {
|
|
583
|
+
margin: 0;
|
|
584
|
+
color: var(--frock-text-muted);
|
|
585
|
+
font-size: var(--frock-text-sm);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.connect-cards {
|
|
589
|
+
display: flex;
|
|
590
|
+
flex-direction: column;
|
|
591
|
+
gap: 0.5rem;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.connect-card {
|
|
595
|
+
display: flex;
|
|
596
|
+
align-items: center;
|
|
597
|
+
justify-content: space-between;
|
|
598
|
+
gap: 1rem;
|
|
599
|
+
padding: 0.75rem 1rem;
|
|
600
|
+
border: 1px solid var(--frock-border);
|
|
601
|
+
border-radius: var(--frock-radius-card);
|
|
602
|
+
background: var(--frock-surface-raised);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.connect-card-copy {
|
|
606
|
+
display: flex;
|
|
607
|
+
flex-direction: column;
|
|
608
|
+
gap: 0.25rem;
|
|
609
|
+
color: var(--frock-text);
|
|
610
|
+
font-size: var(--frock-text-sm);
|
|
611
|
+
line-height: var(--frock-leading-normal);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.connect-card-copy span {
|
|
615
|
+
color: var(--frock-text-muted);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.connector-grid {
|
|
619
|
+
display: grid;
|
|
620
|
+
gap: 12px;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.connector-card {
|
|
624
|
+
min-width: 0;
|
|
625
|
+
padding: 8px;
|
|
626
|
+
border: 1px solid var(--frock-border);
|
|
627
|
+
border-radius: var(--frock-radius-card);
|
|
628
|
+
background: var(--frock-surface-raised);
|
|
629
|
+
box-shadow: var(--frock-shadow-card);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.connector-summary {
|
|
633
|
+
display: grid;
|
|
634
|
+
width: 100%;
|
|
635
|
+
min-width: 0;
|
|
636
|
+
grid-template-columns: 44px minmax(0, 1fr) auto;
|
|
637
|
+
align-items: center;
|
|
638
|
+
gap: 12px;
|
|
639
|
+
padding: 8px;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.connector-logo {
|
|
643
|
+
display: grid;
|
|
644
|
+
width: 44px;
|
|
645
|
+
height: 44px;
|
|
646
|
+
place-items: center;
|
|
647
|
+
border-radius: 12px;
|
|
648
|
+
color: var(--frock-action-secondary-text);
|
|
649
|
+
background: var(--frock-surface-accent);
|
|
650
|
+
font-weight: 800;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.connector-copy {
|
|
654
|
+
min-width: 0;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.connector-copy strong,
|
|
658
|
+
.connector-copy small {
|
|
659
|
+
display: block;
|
|
660
|
+
overflow: hidden;
|
|
661
|
+
white-space: nowrap;
|
|
662
|
+
text-overflow: ellipsis;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.connector-copy strong {
|
|
666
|
+
font-size: var(--frock-text-md);
|
|
667
|
+
font-weight: 600;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.connector-copy small {
|
|
671
|
+
margin-top: 4px;
|
|
672
|
+
color: var(--frock-text-muted);
|
|
673
|
+
font-size: var(--frock-text-sm);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.connector-actions {
|
|
677
|
+
display: flex;
|
|
678
|
+
flex-wrap: wrap;
|
|
679
|
+
gap: 8px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.connector-actions :deep(.ui-button) {
|
|
683
|
+
min-height: 28px;
|
|
684
|
+
padding: 0 10px;
|
|
685
|
+
font-size: var(--frock-text-sm);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.connections-empty {
|
|
689
|
+
display: flex;
|
|
690
|
+
flex-direction: column;
|
|
691
|
+
align-items: flex-start;
|
|
692
|
+
gap: 10px;
|
|
693
|
+
padding: 12px;
|
|
694
|
+
border: 1px solid var(--frock-border);
|
|
695
|
+
border-radius: var(--frock-radius-card);
|
|
696
|
+
background: var(--frock-surface-subtle);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.connections-empty p {
|
|
700
|
+
margin: 0;
|
|
701
|
+
color: var(--frock-text-muted);
|
|
702
|
+
font-size: var(--frock-text-sm);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.mcp-status {
|
|
706
|
+
display: grid;
|
|
707
|
+
gap: 12px;
|
|
708
|
+
margin: 0 8px;
|
|
709
|
+
padding: 12px 0 4px;
|
|
710
|
+
border-top: 1px solid var(--frock-border);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.mcp-server {
|
|
714
|
+
display: grid;
|
|
715
|
+
gap: 6px;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.mcp-server-head {
|
|
719
|
+
display: flex;
|
|
720
|
+
align-items: center;
|
|
721
|
+
gap: 8px;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.mcp-server-name {
|
|
725
|
+
color: var(--frock-text);
|
|
726
|
+
font-size: var(--frock-text-base);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.mcp-state {
|
|
730
|
+
padding: 1px 8px;
|
|
731
|
+
border: 1px solid var(--frock-border);
|
|
732
|
+
border-radius: 999px;
|
|
733
|
+
color: var(--frock-text-muted);
|
|
734
|
+
font-size: var(--frock-text-sm);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.mcp-state-ready {
|
|
738
|
+
color: var(--frock-text);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.mcp-state-error,
|
|
742
|
+
.mcp-state-needs-auth {
|
|
743
|
+
border-color: var(--frock-danger-text);
|
|
744
|
+
color: var(--frock-danger-text);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.mcp-meta,
|
|
748
|
+
.mcp-instructions {
|
|
749
|
+
margin: 0;
|
|
750
|
+
color: var(--frock-text-muted);
|
|
751
|
+
font-size: var(--frock-text-sm);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.api-key-form {
|
|
755
|
+
display: grid;
|
|
756
|
+
gap: 12px;
|
|
757
|
+
margin: 0 8px;
|
|
758
|
+
padding: 12px 0 8px;
|
|
759
|
+
border-top: 1px solid var(--frock-border);
|
|
760
|
+
animation: frock-rise-in var(--frock-motion-enter) both;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.api-key-form label {
|
|
764
|
+
display: grid;
|
|
765
|
+
gap: 6px;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.api-key-form span {
|
|
769
|
+
color: var(--frock-text-muted);
|
|
770
|
+
font-size: var(--frock-text-sm);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.api-key-form input,
|
|
774
|
+
.api-key-form select {
|
|
775
|
+
min-width: 0;
|
|
776
|
+
padding: 8px 11px;
|
|
777
|
+
border: 1px solid var(--frock-border);
|
|
778
|
+
border-radius: 9px;
|
|
779
|
+
background: var(--frock-surface-raised);
|
|
780
|
+
color: var(--frock-text);
|
|
781
|
+
font-size: var(--frock-text-base);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.api-key-form textarea {
|
|
785
|
+
min-width: 0;
|
|
786
|
+
padding: 8px 11px;
|
|
787
|
+
border: 1px solid var(--frock-border);
|
|
788
|
+
border-radius: 9px;
|
|
789
|
+
background: var(--frock-surface-raised);
|
|
790
|
+
color: var(--frock-text);
|
|
791
|
+
font-family: inherit;
|
|
792
|
+
font-size: var(--frock-text-base);
|
|
793
|
+
resize: vertical;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.api-key-actions {
|
|
797
|
+
display: flex;
|
|
798
|
+
justify-content: flex-end;
|
|
799
|
+
gap: 8px;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.connection-failure {
|
|
803
|
+
margin: 0;
|
|
804
|
+
color: var(--frock-danger-text);
|
|
805
|
+
font-size: var(--frock-text-sm);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.settings-error {
|
|
809
|
+
margin: 0;
|
|
810
|
+
color: var(--frock-danger-text);
|
|
811
|
+
font-size: var(--frock-text-sm);
|
|
812
|
+
}
|
|
813
|
+
</style>
|