@hachej/boring-mcp 0.0.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.
@@ -0,0 +1,638 @@
1
+ "use client";
2
+
3
+ // src/front/index.tsx
4
+ import { useEffect, useState } from "react";
5
+ import { definePlugin } from "@hachej/boring-workspace/plugin";
6
+
7
+ // src/shared/index.ts
8
+ var BORING_MCP_PLUGIN_ID = "boring-mcp";
9
+ var BORING_MCP_SOURCES_TAB_PANEL_ID = "boring-mcp.sources.tab";
10
+ var BORING_MCP_SOURCES_PANEL_ID = "boring-mcp.sources.panel";
11
+ var MCP_ERROR_CODES = {
12
+ SOURCE_NOT_FOUND: "MCP_SOURCE_NOT_FOUND",
13
+ SOURCE_FORBIDDEN: "MCP_SOURCE_FORBIDDEN",
14
+ SOURCE_UNAVAILABLE: "MCP_SOURCE_UNAVAILABLE",
15
+ PROVIDER_CONFIG_INVALID: "MCP_PROVIDER_CONFIG_INVALID",
16
+ PROVIDER_TIMEOUT: "MCP_PROVIDER_TIMEOUT",
17
+ PROVIDER_ERROR: "MCP_PROVIDER_ERROR",
18
+ TOOL_NOT_FOUND: "MCP_TOOL_NOT_FOUND",
19
+ TOOL_NOT_ALLOWED: "MCP_TOOL_NOT_ALLOWED",
20
+ PROVIDER_TOOL_DRIFT: "MCP_PROVIDER_TOOL_DRIFT",
21
+ RESOURCE_LIMIT_EXCEEDED: "MCP_RESOURCE_LIMIT_EXCEEDED",
22
+ SECRET_LEAK_GUARD: "MCP_SECRET_LEAK_GUARD",
23
+ INPUT_INVALID: "MCP_INPUT_INVALID",
24
+ RESOURCE_URI_INVALID: "MCP_RESOURCE_URI_INVALID"
25
+ };
26
+ function toMcpSourceDto(source) {
27
+ return {
28
+ id: source.id,
29
+ provider: source.provider,
30
+ displayName: source.displayName,
31
+ status: source.status,
32
+ ownerKind: source.ownerKind,
33
+ credentialProvider: source.credentialProvider,
34
+ scopes: source.scopes,
35
+ providerAccountLabel: source.providerAccountLabel,
36
+ lastVerifiedAt: source.lastVerifiedAt,
37
+ createdAt: source.createdAt,
38
+ updatedAt: source.updatedAt
39
+ };
40
+ }
41
+ var McpError = class extends Error {
42
+ code;
43
+ details;
44
+ constructor(code, message, details) {
45
+ super(message);
46
+ this.name = "McpError";
47
+ this.code = code;
48
+ this.details = details;
49
+ }
50
+ };
51
+ var NOTION_MCP_TEMPLATE = {
52
+ id: "notion",
53
+ displayName: "Notion",
54
+ readOnlyDefault: true,
55
+ allowedTools: ["NOTION_SEARCH_NOTION_PAGE", "NOTION_GET_PAGE_MARKDOWN", "NOTION_RETRIEVE_PAGE"],
56
+ deniedTools: ["create_*", "update_*", "delete_*", "publish_*", "admin_*"],
57
+ allowedResourceUriPrefixes: ["notion:", "notion://"]
58
+ };
59
+ var AIRTABLE_MCP_TEMPLATE = {
60
+ id: "airtable",
61
+ displayName: "Airtable",
62
+ readOnlyDefault: true,
63
+ allowedTools: ["ping", "list_bases", "list_workspaces", "list_tables_for_base", "get_table_schema", "search_records"],
64
+ deniedTools: ["create_*", "update_*", "delete_*", "publish_*", "admin_*"],
65
+ allowedResourceUriPrefixes: ["airtable:", "airtable://"]
66
+ };
67
+ var DEFAULT_MCP_PROVIDER_TEMPLATES = [NOTION_MCP_TEMPLATE, AIRTABLE_MCP_TEMPLATE];
68
+ function getMcpProviderTemplate(provider, templates = DEFAULT_MCP_PROVIDER_TEMPLATES) {
69
+ return templates.find((template) => template.id === provider);
70
+ }
71
+ var MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
72
+ function validateMcpToolName(toolName) {
73
+ if (!MCP_TOOL_NAME_PATTERN.test(toolName)) {
74
+ throw new McpError(MCP_ERROR_CODES.INPUT_INVALID, "Invalid MCP tool name");
75
+ }
76
+ }
77
+ function wildcardMatch(pattern, value) {
78
+ if (!pattern.includes("*")) return pattern === value;
79
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
80
+ return new RegExp(`^${escaped}$`, "i").test(value);
81
+ }
82
+ function classifyMcpTool(template, toolName) {
83
+ validateMcpToolName(toolName);
84
+ if (template.deniedTools.some((pattern) => wildcardMatch(pattern, toolName))) {
85
+ return { allowed: false, risk: "write", reason: "Tool matches a denied write/admin pattern" };
86
+ }
87
+ if (template.allowedTools.some((pattern) => wildcardMatch(pattern, toolName))) {
88
+ return { allowed: true, risk: "read", reason: "Tool is on the read-only allowlist" };
89
+ }
90
+ return { allowed: false, risk: "unknown", reason: "Tool is not on the read-only allowlist" };
91
+ }
92
+ function classifyMcpTools(template, tools) {
93
+ return tools.map((tool) => ({ ...tool, decision: classifyMcpTool(template, tool.name) }));
94
+ }
95
+ function assertMcpToolAllowed(template, toolName) {
96
+ const decision = classifyMcpTool(template, toolName);
97
+ if (!decision.allowed) throw new McpError(MCP_ERROR_CODES.TOOL_NOT_ALLOWED, decision.reason);
98
+ }
99
+ var REDACTION = "[REDACTED_MCP_SECRET]";
100
+ var SECRET_KEY_PATTERN = /(api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|oauth[_-]?token|authorization|cookie|client[_-]?secret|session[_-]?headers?|mcp[_-]?session|x-composio-mcp-session)/i;
101
+ var SECRET_VALUE_PATTERN = /(Bearer\s+[A-Za-z0-9._~+\/-]{12,}|sk-[A-Za-z0-9_-]{12,}|(?:x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|oauth[_-]?token|code|client[_-]?secret|session[_-]?headers?|mcp[_-]?session|x-composio-mcp-session)\s*[:=]\s*[^\s,&,}]+)/gi;
102
+ function redactMcpSecrets(value) {
103
+ if (typeof value === "string") return value.replace(SECRET_VALUE_PATTERN, REDACTION);
104
+ if (Array.isArray(value)) return value.map(redactMcpSecrets);
105
+ if (!value || typeof value !== "object") return value;
106
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, SECRET_KEY_PATTERN.test(key) ? REDACTION : redactMcpSecrets(nested)]));
107
+ }
108
+ function containsMcpSecret(value) {
109
+ const redacted = redactMcpSecrets(value);
110
+ return JSON.stringify(redacted) !== JSON.stringify(value);
111
+ }
112
+ function hasMcpCanaryText(value, canaries) {
113
+ return canaries.some((canary) => canary.trim() && value.includes(canary));
114
+ }
115
+ function containsMcpCanary(value, canaries) {
116
+ if (typeof value === "string") return hasMcpCanaryText(value, canaries);
117
+ if (Array.isArray(value)) return value.some((item) => containsMcpCanary(item, canaries));
118
+ if (!value || typeof value !== "object") return false;
119
+ return Object.entries(value).some(([key, nested]) => hasMcpCanaryText(key, canaries) || containsMcpCanary(nested, canaries));
120
+ }
121
+ function containsMcpSecretOrCanary(value, canaries) {
122
+ return containsMcpSecret(value) || containsMcpCanary(value, canaries);
123
+ }
124
+ function doctorMcpSource(source, templates = DEFAULT_MCP_PROVIDER_TEMPLATES) {
125
+ const issues = [];
126
+ if (!getMcpProviderTemplate(source.provider, templates)) {
127
+ issues.push({ level: "error", code: MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, message: "Unknown MCP provider template" });
128
+ }
129
+ if (source.status !== "connected") {
130
+ issues.push({ level: "warning", code: MCP_ERROR_CODES.SOURCE_UNAVAILABLE, message: "MCP source is not connected" });
131
+ }
132
+ return { ok: issues.every((issue) => issue.level !== "error"), sourceId: source.id, issues };
133
+ }
134
+ var McpAccessFacade = class {
135
+ constructor(params) {
136
+ this.params = params;
137
+ }
138
+ params;
139
+ async listSources(actor) {
140
+ return (await this.params.store.listSources(actor)).filter((source) => this.canAccessSource(actor, source));
141
+ }
142
+ async probeSource(actor, sourceId) {
143
+ const source = await this.requireAccessibleSource(actor, sourceId);
144
+ this.requireConnectedSource(source);
145
+ const template = this.requireTemplate(source);
146
+ const [tools, resources] = await Promise.all([
147
+ this.params.transport.listTools(source),
148
+ this.params.transport.listResources(source)
149
+ ]);
150
+ return {
151
+ sourceId: source.id,
152
+ provider: source.provider,
153
+ tools: classifyMcpTools(template, tools),
154
+ resources
155
+ };
156
+ }
157
+ async requireAccessibleSource(actor, sourceId) {
158
+ const source = await this.params.store.getSource(sourceId);
159
+ if (!source || source.workspaceId !== actor.workspaceId) {
160
+ throw new McpError(MCP_ERROR_CODES.SOURCE_NOT_FOUND, "MCP source not found");
161
+ }
162
+ if (!this.canAccessSource(actor, source)) throw new McpError(MCP_ERROR_CODES.SOURCE_NOT_FOUND, "MCP source not found");
163
+ return source;
164
+ }
165
+ canAccessSource(actor, source) {
166
+ if (source.workspaceId !== actor.workspaceId) return false;
167
+ return this.params.accessPolicy?.canAccessSource(actor, source) ?? (source.ownerKind === "user" && source.userId === actor.userId);
168
+ }
169
+ requireConnectedSource(source) {
170
+ if (source.status !== "connected") throw new McpError(MCP_ERROR_CODES.SOURCE_UNAVAILABLE, "MCP source is not connected");
171
+ }
172
+ requireTemplate(source) {
173
+ const template = getMcpProviderTemplate(source.provider, this.params.templates);
174
+ if (!template) throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Unknown MCP provider");
175
+ return template;
176
+ }
177
+ };
178
+
179
+ // src/front/index.tsx
180
+ import { jsx, jsxs } from "react/jsx-runtime";
181
+ function cx(...classes) {
182
+ return classes.filter(Boolean).join(" ");
183
+ }
184
+ function resolveProviders(options) {
185
+ const providers = options.providers ?? DEFAULT_MCP_PROVIDER_TEMPLATES;
186
+ const enabled = new Set(options.enabledProviderIds ?? providers.map((provider) => provider.id));
187
+ return providers.filter((provider) => enabled.has(provider.id));
188
+ }
189
+ function findSourceStatus(provider, statuses = []) {
190
+ const matches = statuses.filter((status) => status.source.provider === provider.id);
191
+ return matches.find((status) => status.source.status === "connected") ?? matches.find((status) => status.source.status !== "revoked") ?? matches[0];
192
+ }
193
+ function providerSetupState(provider, options) {
194
+ return options.providerSetup?.find((setup) => setup.providerId === provider.id);
195
+ }
196
+ function actionErrorMessage(error) {
197
+ return error instanceof Error && error.message ? error.message : "MCP action failed. Please try again.";
198
+ }
199
+ function upsertSourceStatus(statuses, next) {
200
+ if (!next) return [...statuses];
201
+ const index = statuses.findIndex((status) => status.source.id === next.source.id);
202
+ if (index === -1) return [...statuses, next];
203
+ return statuses.map((status, itemIndex) => itemIndex === index ? next : status);
204
+ }
205
+ function resolveSourceApiWorkspaceId(sourceApi) {
206
+ const workspaceId = sourceApi.workspaceId ?? sourceApi.resolveWorkspaceId?.();
207
+ if (!workspaceId) throw new Error("Open a workspace before connecting MCP.");
208
+ return workspaceId;
209
+ }
210
+ function sourceApiUrl(sourceApi, path) {
211
+ const base = sourceApi.baseUrl?.replace(/\/$/, "") ?? "";
212
+ return `${base}${path}`;
213
+ }
214
+ async function readSourceApiJson(response) {
215
+ const payload = await response.json().catch(() => void 0);
216
+ if (!response.ok) {
217
+ const message = payload && typeof payload === "object" && "message" in payload && typeof payload.message === "string" ? payload.message : "MCP API request failed.";
218
+ throw new Error(message);
219
+ }
220
+ return payload;
221
+ }
222
+ async function sourceApiRequest(sourceApi, path, body) {
223
+ const workspaceId = resolveSourceApiWorkspaceId(sourceApi);
224
+ const response = await fetch(sourceApiUrl(sourceApi, path), {
225
+ method: body ? "POST" : "GET",
226
+ credentials: "same-origin",
227
+ headers: {
228
+ "content-type": "application/json",
229
+ "x-boring-workspace-id": workspaceId
230
+ },
231
+ body: body ? JSON.stringify(body) : void 0
232
+ });
233
+ return readSourceApiJson(response);
234
+ }
235
+ function defaultOpenConnectUrl(url) {
236
+ if (typeof window === "undefined") return;
237
+ window.open(url, "_blank", "noopener,noreferrer");
238
+ }
239
+ function openPendingConnectWindow(sourceApi) {
240
+ if (sourceApi.openConnectUrl || typeof window === "undefined") return void 0;
241
+ const popup = window.open("about:blank", "_blank");
242
+ if (!popup) throw new Error("Popup blocked. Allow popups for this site and try connecting again.");
243
+ try {
244
+ popup.opener = null;
245
+ } catch {
246
+ }
247
+ return popup;
248
+ }
249
+ function navigatePendingConnectWindow(popup, url) {
250
+ if (!popup) {
251
+ if (url) defaultOpenConnectUrl(url);
252
+ return;
253
+ }
254
+ if (!url) {
255
+ popup.close();
256
+ return;
257
+ }
258
+ popup.location.href = url;
259
+ }
260
+ function createBrowserSourceActions(sourceApi) {
261
+ return {
262
+ async onConnect(providerId) {
263
+ const popup = openPendingConnectWindow(sourceApi);
264
+ try {
265
+ const result = await sourceApiRequest(sourceApi, "/api/v1/boring-mcp/connect", { provider: providerId });
266
+ if (sourceApi.openConnectUrl && result.connectUrl) sourceApi.openConnectUrl(result.connectUrl);
267
+ else navigatePendingConnectWindow(popup, result.connectUrl);
268
+ return result.status;
269
+ } catch (error) {
270
+ popup?.close();
271
+ throw error;
272
+ }
273
+ },
274
+ async onRefreshStatus(sourceId) {
275
+ const result = await sourceApiRequest(sourceApi, "/api/v1/boring-mcp/refresh", { sourceId });
276
+ return result.status;
277
+ },
278
+ async onDisconnect(sourceId) {
279
+ const result = await sourceApiRequest(sourceApi, "/api/v1/boring-mcp/disconnect", { sourceId });
280
+ return result.status;
281
+ },
282
+ async onListTools(sourceId, _providerId, refresh) {
283
+ const result = await sourceApiRequest(sourceApi, "/api/v1/boring-mcp/tools", { sourceId, refresh: refresh === true });
284
+ return result.tools;
285
+ }
286
+ };
287
+ }
288
+ function statusLabel(status) {
289
+ switch (status) {
290
+ case "connected":
291
+ return "Connected";
292
+ case "expired":
293
+ return "Expired";
294
+ case "error":
295
+ return "Needs attention";
296
+ case "revoked":
297
+ return "Disconnected";
298
+ case "unconfigured":
299
+ return "Not connected";
300
+ default:
301
+ return "Not connected";
302
+ }
303
+ }
304
+ function statusBadgeClass(status) {
305
+ switch (status) {
306
+ case "connected":
307
+ return "border-accent/40 bg-accent/10 text-foreground";
308
+ case "expired":
309
+ case "error":
310
+ return "border-amber-500/35 bg-amber-500/10 text-foreground";
311
+ default:
312
+ return "border-border/70 bg-muted/50 text-muted-foreground";
313
+ }
314
+ }
315
+ function McpIcon({ className }) {
316
+ return /* @__PURE__ */ jsxs("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
317
+ /* @__PURE__ */ jsx("path", { d: "M7 8h10" }),
318
+ /* @__PURE__ */ jsx("path", { d: "M7 12h10" }),
319
+ /* @__PURE__ */ jsx("path", { d: "M7 16h6" }),
320
+ /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" })
321
+ ] });
322
+ }
323
+ function RefreshIcon({ className }) {
324
+ return /* @__PURE__ */ jsxs("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
325
+ /* @__PURE__ */ jsx("path", { d: "M20 11a8.1 8.1 0 0 0-15.5-2M4 5v4h4" }),
326
+ /* @__PURE__ */ jsx("path", { d: "M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" })
327
+ ] });
328
+ }
329
+ function ChevronIcon({ open }) {
330
+ return /* @__PURE__ */ jsx("svg", { className: cx("h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform", open && "rotate-90"), viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) });
331
+ }
332
+ function XIcon() {
333
+ return /* @__PURE__ */ jsxs("svg", { className: "size-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
334
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
335
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
336
+ ] });
337
+ }
338
+ function ActionButton({ children, disabled, tone = "secondary", onClick, title }) {
339
+ return /* @__PURE__ */ jsx(
340
+ "button",
341
+ {
342
+ type: "button",
343
+ disabled,
344
+ title,
345
+ className: cx(
346
+ "rounded-lg border px-2.5 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
347
+ tone === "primary" && "border-accent/60 bg-accent text-accent-foreground hover:bg-accent/90",
348
+ tone === "danger" && "border-destructive/30 bg-card/70 text-muted-foreground hover:border-destructive/50 hover:text-destructive",
349
+ tone === "secondary" && "border-border/70 bg-card/70 text-muted-foreground hover:border-border hover:text-foreground"
350
+ ),
351
+ onClick: (event) => {
352
+ event.stopPropagation();
353
+ onClick?.();
354
+ },
355
+ children
356
+ }
357
+ );
358
+ }
359
+ function ProviderRow({ provider, providerSetup, connectionUnavailableMessage, sourceStatus, actions, pending, expanded, tools, toolsPending, toolsError, runAction, onToggle, onLoadTools }) {
360
+ const source = sourceStatus?.source;
361
+ const setupEnabled = providerSetup?.enabled ?? true;
362
+ const status = source?.status;
363
+ const sourceId = source?.id;
364
+ const connectLabel = status === "expired" || status === "error" ? "Reconnect" : "Connect";
365
+ const showConnect = status !== "connected";
366
+ const connectBlockedByStatus = Boolean(sourceStatus && !sourceStatus.connectable);
367
+ const connectUnavailable = !setupEnabled || !actions.onConnect || connectBlockedByStatus;
368
+ const connectDisabled = connectUnavailable || pending === `${provider.id}:connect`;
369
+ const refreshDisabled = !sourceId || !actions.onRefreshStatus || pending === `${provider.id}:refresh`;
370
+ const disconnectDisabled = !sourceId || !sourceStatus?.canDisconnect || !actions.onDisconnect || pending === `${provider.id}:disconnect`;
371
+ const unavailableMessage = connectBlockedByStatus ? "This MCP cannot start a new connection in its current state. Refresh status or disconnect first." : providerSetup?.message ?? connectionUnavailableMessage ?? "Ask an admin to wire this app's MCP backend.";
372
+ const fallbackTools = provider.allowedTools.map((toolName) => ({
373
+ sourceId: sourceId ?? `template:${provider.id}`,
374
+ provider: provider.id,
375
+ toolName,
376
+ displayName: toolName.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()),
377
+ summary: "Available after this MCP is connected.",
378
+ inputSchema: {},
379
+ risk: "read",
380
+ enabled: status === "connected",
381
+ blockedReasons: status === "connected" ? [] : ["Connect this MCP to discover the live tool schema."],
382
+ schemaHash: "template",
383
+ nativeRef: { provider: provider.id, action: toolName }
384
+ }));
385
+ const visibleTools = tools ?? fallbackTools;
386
+ return /* @__PURE__ */ jsxs("li", { className: "rounded-xl border border-border/60 bg-card/70 px-3 py-2.5 transition-colors hover:border-border hover:bg-muted/50", children: [
387
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
388
+ /* @__PURE__ */ jsxs(
389
+ "button",
390
+ {
391
+ type: "button",
392
+ className: "flex min-w-0 flex-1 items-start gap-2 text-left",
393
+ "aria-expanded": expanded,
394
+ "aria-label": `${provider.displayName} MCP`,
395
+ onClick: () => onToggle(provider, sourceStatus),
396
+ children: [
397
+ /* @__PURE__ */ jsx(ChevronIcon, { open: expanded }),
398
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
399
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [
400
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: provider.displayName }),
401
+ /* @__PURE__ */ jsx("span", { className: cx("rounded border px-1.5 py-0.5 text-[10px] font-medium", statusBadgeClass(status)), children: statusLabel(status) })
402
+ ] }),
403
+ /* @__PURE__ */ jsx("div", { className: "mt-1 truncate text-xs text-muted-foreground", children: source?.providerAccountLabel ? `Account: ${source.providerAccountLabel}` : source ? "Account connected through server-owned MCP credentials" : "No account connected yet." }),
404
+ source?.lastVerifiedAt ? /* @__PURE__ */ jsxs("div", { className: "mt-0.5 truncate text-[11px] text-muted-foreground/80", children: [
405
+ "Last verified: ",
406
+ source.lastVerifiedAt
407
+ ] }) : null
408
+ ] })
409
+ ]
410
+ }
411
+ ),
412
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap justify-end gap-1.5", children: [
413
+ showConnect ? /* @__PURE__ */ jsx(
414
+ ActionButton,
415
+ {
416
+ tone: "primary",
417
+ disabled: connectDisabled,
418
+ title: connectUnavailable && showConnect ? unavailableMessage : void 0,
419
+ onClick: () => actions.onConnect && runAction(`${provider.id}:connect`, () => actions.onConnect?.(provider.id)),
420
+ children: !setupEnabled || !actions.onConnect ? "Admin setup required" : connectLabel
421
+ }
422
+ ) : null,
423
+ sourceId ? /* @__PURE__ */ jsx(
424
+ ActionButton,
425
+ {
426
+ disabled: refreshDisabled,
427
+ onClick: () => actions.onRefreshStatus && runAction(`${provider.id}:refresh`, () => actions.onRefreshStatus?.(sourceId, provider.id)),
428
+ children: "Refresh status"
429
+ }
430
+ ) : null,
431
+ sourceId && sourceStatus?.canDisconnect ? /* @__PURE__ */ jsx(
432
+ ActionButton,
433
+ {
434
+ tone: "danger",
435
+ disabled: disconnectDisabled,
436
+ onClick: () => actions.onDisconnect && runAction(`${provider.id}:disconnect`, () => actions.onDisconnect?.(sourceId, provider.id)),
437
+ children: "Disconnect"
438
+ }
439
+ ) : null
440
+ ] })
441
+ ] }),
442
+ expanded ? /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-border/50 pt-3", children: [
443
+ connectUnavailable && showConnect ? /* @__PURE__ */ jsx("div", { className: "mb-3 text-xs text-muted-foreground", children: unavailableMessage }) : null,
444
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
445
+ /* @__PURE__ */ jsxs("div", { children: [
446
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-foreground", children: "Tools" }),
447
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: status === "connected" ? "Live tool catalog exposed through governed read-only MCP." : "Tools that become available after connecting this MCP." })
448
+ ] }),
449
+ sourceId && status === "connected" && actions.onListTools ? /* @__PURE__ */ jsxs(
450
+ "button",
451
+ {
452
+ type: "button",
453
+ className: "inline-flex items-center gap-1 rounded-lg border border-border/70 bg-card/70 px-2 py-1 text-[11px] font-medium text-muted-foreground hover:border-border hover:text-foreground disabled:opacity-50",
454
+ disabled: toolsPending,
455
+ onClick: (event) => {
456
+ event.stopPropagation();
457
+ onLoadTools(sourceId, provider.id, true);
458
+ },
459
+ children: [
460
+ /* @__PURE__ */ jsx(RefreshIcon, { className: cx("h-3 w-3", toolsPending && "animate-spin") }),
461
+ "Refresh tools"
462
+ ]
463
+ }
464
+ ) : null
465
+ ] }),
466
+ toolsError ? /* @__PURE__ */ jsx("div", { role: "alert", className: "mb-2 rounded-lg border border-destructive/30 bg-destructive/8 px-3 py-2 text-xs text-destructive", children: toolsError }) : null,
467
+ toolsPending && !tools ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground", children: "Loading tools\u2026" }) : visibleTools.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground", children: "No tools returned for this MCP yet." }) : /* @__PURE__ */ jsx("ul", { role: "list", className: "grid gap-1.5", children: visibleTools.map((tool) => /* @__PURE__ */ jsx("li", { className: "rounded-lg border border-border/60 bg-background/60 px-2.5 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
468
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
469
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs font-medium text-foreground", children: tool.toolName }),
470
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 line-clamp-2 text-[11px] leading-4 text-muted-foreground", children: tool.summary || tool.description || "MCP tool" })
471
+ ] }),
472
+ /* @__PURE__ */ jsx("span", { className: cx("shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium", tool.enabled ? "bg-accent/10 text-foreground" : "bg-muted text-muted-foreground"), children: tool.enabled ? "enabled" : "blocked" })
473
+ ] }) }, `${tool.sourceId}:${tool.toolName}`)) })
474
+ ] }) : null
475
+ ] });
476
+ }
477
+ function BoringMcpSourcesPanel({ options }) {
478
+ const providers = resolveProviders(options);
479
+ const sourceApi = options.sourceApi?.enabled ? options.sourceApi : void 0;
480
+ const browserActions = sourceApi ? createBrowserSourceActions(sourceApi) : {};
481
+ const actions = { ...browserActions, ...options.sourceActions ?? {} };
482
+ const [sourceStatuses, setSourceStatuses] = useState(options.sourceStatuses ?? []);
483
+ const [pending, setPending] = useState();
484
+ const [actionError, setActionError] = useState();
485
+ const [expandedProviderId, setExpandedProviderId] = useState();
486
+ const [toolsBySourceId, setToolsBySourceId] = useState({});
487
+ const [toolsPendingSourceId, setToolsPendingSourceId] = useState();
488
+ const [toolsErrors, setToolsErrors] = useState({});
489
+ useEffect(() => {
490
+ setSourceStatuses(options.sourceStatuses ?? []);
491
+ }, [options.sourceStatuses]);
492
+ useEffect(() => {
493
+ if (!sourceApi) return;
494
+ let cancelled = false;
495
+ void (async () => {
496
+ try {
497
+ const result = await sourceApiRequest(sourceApi, "/api/v1/boring-mcp/sources");
498
+ if (!cancelled) setSourceStatuses(result.sourceStatuses);
499
+ } catch (error) {
500
+ if (!cancelled) setActionError(actionErrorMessage(error));
501
+ }
502
+ })();
503
+ return () => {
504
+ cancelled = true;
505
+ };
506
+ }, [sourceApi?.enabled, sourceApi?.baseUrl, sourceApi?.workspaceId, sourceApi?.resolveWorkspaceId]);
507
+ const loadTools = (sourceId, providerId, refresh = false) => {
508
+ if (!actions.onListTools) return;
509
+ if (!refresh && toolsBySourceId[sourceId]) return;
510
+ setToolsPendingSourceId(sourceId);
511
+ setToolsErrors((current) => ({ ...current, [sourceId]: "" }));
512
+ void (async () => {
513
+ try {
514
+ const tools = await actions.onListTools?.(sourceId, providerId, refresh);
515
+ setToolsBySourceId((current) => ({ ...current, [sourceId]: tools ?? [] }));
516
+ } catch (error) {
517
+ setToolsErrors((current) => ({ ...current, [sourceId]: actionErrorMessage(error) }));
518
+ } finally {
519
+ setToolsPendingSourceId((current) => current === sourceId ? void 0 : current);
520
+ }
521
+ })();
522
+ };
523
+ const runAction = (key, action) => {
524
+ setPending(key);
525
+ setActionError(void 0);
526
+ void (async () => {
527
+ try {
528
+ const result = await action();
529
+ setSourceStatuses((current) => upsertSourceStatus(current, result));
530
+ } catch (error) {
531
+ setActionError(actionErrorMessage(error));
532
+ } finally {
533
+ setPending((current) => current === key ? void 0 : current);
534
+ }
535
+ })();
536
+ };
537
+ const toggleProvider = (provider, sourceStatus) => {
538
+ const next = expandedProviderId === provider.id ? void 0 : provider.id;
539
+ setExpandedProviderId(next);
540
+ if (next && sourceStatus?.source.status === "connected" && sourceStatus.source.id) {
541
+ actions.onViewTools?.(sourceStatus.source.id, provider.id);
542
+ loadTools(sourceStatus.source.id, provider.id, false);
543
+ }
544
+ };
545
+ return /* @__PURE__ */ jsxs("div", { className: "boring-scrollbar-discreet min-h-0 flex-1 overflow-y-auto p-4", children: [
546
+ actionError ? /* @__PURE__ */ jsx("div", { role: "alert", "aria-live": "polite", className: "mb-4 rounded-lg border border-destructive/30 bg-destructive/8 px-3 py-2 text-sm text-destructive", children: actionError }) : null,
547
+ providers.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex h-full min-h-[180px] items-center justify-center text-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsxs("div", { children: [
548
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-foreground/80", children: "No MCP providers enabled" }),
549
+ /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-xs", children: "Enable a provider template to connect MCP tools." })
550
+ ] }) }) : /* @__PURE__ */ jsx("ul", { role: "list", className: "grid gap-2", children: providers.map((provider) => {
551
+ const sourceStatus = findSourceStatus(provider, sourceStatuses);
552
+ const sourceId = sourceStatus?.source.id;
553
+ return /* @__PURE__ */ jsx(
554
+ ProviderRow,
555
+ {
556
+ provider,
557
+ providerSetup: providerSetupState(provider, options),
558
+ connectionUnavailableMessage: options.connectionUnavailableMessage,
559
+ sourceStatus,
560
+ actions,
561
+ pending,
562
+ expanded: expandedProviderId === provider.id,
563
+ tools: sourceId ? toolsBySourceId[sourceId] : void 0,
564
+ toolsPending: sourceId ? toolsPendingSourceId === sourceId : false,
565
+ toolsError: sourceId ? toolsErrors[sourceId] : void 0,
566
+ runAction,
567
+ onToggle: toggleProvider,
568
+ onLoadTools: loadTools
569
+ },
570
+ provider.id
571
+ );
572
+ }) })
573
+ ] });
574
+ }
575
+ function BoringMcpSourcesOverlay({ options = {}, onClose, headerInsetStart = false, headerInsetEnd = false }) {
576
+ return /* @__PURE__ */ jsxs("div", { "data-boring-workspace-part": "boring-mcp-sources-overlay", className: "flex h-full min-h-0 flex-col bg-background", children: [
577
+ /* @__PURE__ */ jsxs("header", { className: cx(
578
+ "flex h-12 shrink-0 items-center justify-between border-b border-border/60",
579
+ headerInsetStart ? "pl-12" : "pl-4",
580
+ headerInsetEnd ? "pr-16" : "pr-4"
581
+ ), children: [
582
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
583
+ /* @__PURE__ */ jsx("span", { className: "grid size-7 place-items-center rounded-lg bg-foreground/[0.06] text-muted-foreground", children: /* @__PURE__ */ jsx(McpIcon, { className: "h-4 w-4" }) }),
584
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
585
+ /* @__PURE__ */ jsx("h2", { className: "truncate text-sm font-semibold tracking-tight text-foreground", children: options.tabTitle ?? options.label ?? "MCP" }),
586
+ /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: "Connected MCP providers and governed tools" })
587
+ ] })
588
+ ] }),
589
+ onClose ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-0.5", children: /* @__PURE__ */ jsx(
590
+ "button",
591
+ {
592
+ type: "button",
593
+ onClick: onClose,
594
+ "aria-label": "Close MCP",
595
+ title: "Close",
596
+ className: "inline-flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
597
+ children: /* @__PURE__ */ jsx(XIcon, {})
598
+ }
599
+ ) }) : null
600
+ ] }),
601
+ /* @__PURE__ */ jsx(BoringMcpSourcesPanel, { options })
602
+ ] });
603
+ }
604
+ function createBoringMcpPlugin(options = {}) {
605
+ return definePlugin({
606
+ id: BORING_MCP_PLUGIN_ID,
607
+ label: options.label ?? "MCP"
608
+ });
609
+ }
610
+ var boringMcpPlugin = createBoringMcpPlugin();
611
+ var front_default = boringMcpPlugin;
612
+ export {
613
+ AIRTABLE_MCP_TEMPLATE,
614
+ BORING_MCP_PLUGIN_ID,
615
+ BORING_MCP_SOURCES_PANEL_ID,
616
+ BORING_MCP_SOURCES_TAB_PANEL_ID,
617
+ BoringMcpSourcesOverlay,
618
+ BoringMcpSourcesPanel,
619
+ DEFAULT_MCP_PROVIDER_TEMPLATES,
620
+ MCP_ERROR_CODES,
621
+ MCP_TOOL_NAME_PATTERN,
622
+ McpAccessFacade,
623
+ McpError,
624
+ NOTION_MCP_TEMPLATE,
625
+ assertMcpToolAllowed,
626
+ classifyMcpTool,
627
+ classifyMcpTools,
628
+ containsMcpCanary,
629
+ containsMcpSecret,
630
+ containsMcpSecretOrCanary,
631
+ createBoringMcpPlugin,
632
+ front_default as default,
633
+ doctorMcpSource,
634
+ getMcpProviderTemplate,
635
+ redactMcpSecrets,
636
+ toMcpSourceDto,
637
+ validateMcpToolName
638
+ };