@zackbart/connecta 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SECURITY.md +25 -0
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/activity.d.ts +82 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +51 -0
- package/dist/activity.js.map +1 -0
- package/dist/auth/bearer.d.ts +10 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +47 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/clerk.d.ts +20 -0
- package/dist/auth/clerk.d.ts.map +1 -0
- package/dist/auth/clerk.js +167 -0
- package/dist/auth/clerk.js.map +1 -0
- package/dist/auth/downstream-oauth.d.ts +78 -0
- package/dist/auth/downstream-oauth.d.ts.map +1 -0
- package/dist/auth/downstream-oauth.js +180 -0
- package/dist/auth/downstream-oauth.js.map +1 -0
- package/dist/catalog.d.ts +11 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +144 -0
- package/dist/catalog.js.map +1 -0
- package/dist/connectors/api.d.ts +34 -0
- package/dist/connectors/api.d.ts.map +1 -0
- package/dist/connectors/api.js +36 -0
- package/dist/connectors/api.js.map +1 -0
- package/dist/connectors/remote-mcp.d.ts +33 -0
- package/dist/connectors/remote-mcp.d.ts.map +1 -0
- package/dist/connectors/remote-mcp.js +255 -0
- package/dist/connectors/remote-mcp.js.map +1 -0
- package/dist/credentials.d.ts +33 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +177 -0
- package/dist/credentials.js.map +1 -0
- package/dist/execute.d.ts +40 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +424 -0
- package/dist/execute.js.map +1 -0
- package/dist/executors/quickjs.d.ts +26 -0
- package/dist/executors/quickjs.d.ts.map +1 -0
- package/dist/executors/quickjs.js +267 -0
- package/dist/executors/quickjs.js.map +1 -0
- package/dist/favicon.d.ts +2 -0
- package/dist/favicon.d.ts.map +1 -0
- package/dist/favicon.js +49 -0
- package/dist/favicon.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-result.d.ts +9 -0
- package/dist/mcp-result.d.ts.map +1 -0
- package/dist/mcp-result.js +35 -0
- package/dist/mcp-result.js.map +1 -0
- package/dist/meta-tools.d.ts +100 -0
- package/dist/meta-tools.d.ts.map +1 -0
- package/dist/meta-tools.js +797 -0
- package/dist/meta-tools.js.map +1 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +57 -0
- package/dist/node.js.map +1 -0
- package/dist/registry.d.ts +78 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +299 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +543 -0
- package/dist/server.js.map +1 -0
- package/dist/skills.d.ts +8 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +60 -0
- package/dist/skills.js.map +1 -0
- package/dist/storage/file.d.ts +8 -0
- package/dist/storage/file.d.ts.map +1 -0
- package/dist/storage/file.js +53 -0
- package/dist/storage/file.js.map +1 -0
- package/dist/storage/memory.d.ts +4 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +29 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +91 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +1031 -0
- package/dist/ui.js.map +1 -0
- package/package.json +96 -0
- package/src/activity.ts +144 -0
- package/src/auth/bearer.ts +54 -0
- package/src/auth/clerk.ts +226 -0
- package/src/auth/downstream-oauth.ts +202 -0
- package/src/catalog.ts +166 -0
- package/src/connectors/api.ts +80 -0
- package/src/connectors/remote-mcp.ts +324 -0
- package/src/credentials.ts +259 -0
- package/src/execute.ts +550 -0
- package/src/executors/quickjs.ts +321 -0
- package/src/favicon.ts +53 -0
- package/src/index.ts +197 -0
- package/src/mcp-result.ts +43 -0
- package/src/meta-tools.ts +1120 -0
- package/src/node.ts +62 -0
- package/src/registry.ts +400 -0
- package/src/server.ts +758 -0
- package/src/skills.ts +63 -0
- package/src/storage/file.ts +62 -0
- package/src/storage/memory.ts +34 -0
- package/src/types.ts +254 -0
- package/src/ui.ts +1121 -0
package/dist/ui.js
ADDED
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
/** Connecta's default monochrome "C" mark. */
|
|
2
|
+
export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
3
|
+
<style>
|
|
4
|
+
.fg { fill: #000 }
|
|
5
|
+
@media (prefers-color-scheme: dark) { .fg { fill: #fff } }
|
|
6
|
+
</style>
|
|
7
|
+
<path class="fg" d="M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z"/>
|
|
8
|
+
</svg>`;
|
|
9
|
+
export function resolveBranding(branding) {
|
|
10
|
+
return {
|
|
11
|
+
productName: branding?.productName?.trim() || "Connecta",
|
|
12
|
+
...(branding?.ownerName?.trim()
|
|
13
|
+
? { ownerName: branding.ownerName.trim() }
|
|
14
|
+
: {}),
|
|
15
|
+
...(branding?.ownerUrl?.trim()
|
|
16
|
+
? { ownerUrl: branding.ownerUrl.trim() }
|
|
17
|
+
: {}),
|
|
18
|
+
description: branding?.description?.trim() ||
|
|
19
|
+
"Manage the services this Connecta instance makes available to agents.",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* True only for absolute `http:`/`https:` URLs. Downstream connectors control
|
|
24
|
+
* their `authorizationUrl`, so a hostile/misconfigured one could hand back a
|
|
25
|
+
* `javascript:` (or other) scheme; gate it before it can become an href.
|
|
26
|
+
*/
|
|
27
|
+
export function isSafeHttpUrl(url) {
|
|
28
|
+
if (typeof url !== "string")
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
const scheme = new URL(url).protocol;
|
|
32
|
+
return scheme === "http:" || scheme === "https:";
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Filter by connector identity/description or tool name/description. A
|
|
40
|
+
* connector-level match stays visible even when it currently exposes no tools
|
|
41
|
+
* (for example while authorization is required).
|
|
42
|
+
*/
|
|
43
|
+
export function filterUiConnectors(connectors, query) {
|
|
44
|
+
const q = query.trim().toLowerCase();
|
|
45
|
+
const filtered = [];
|
|
46
|
+
for (const connector of connectors) {
|
|
47
|
+
const connectorText = [
|
|
48
|
+
connector.id,
|
|
49
|
+
connector.title,
|
|
50
|
+
connector.description,
|
|
51
|
+
connector.status,
|
|
52
|
+
]
|
|
53
|
+
.join(" ")
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
const connectorMatches = Boolean(q && connectorText.includes(q));
|
|
56
|
+
const tools = connector.tools.filter((tool) => !q ||
|
|
57
|
+
connectorMatches ||
|
|
58
|
+
`${tool.name} ${tool.description ?? ""}`.toLowerCase().includes(q));
|
|
59
|
+
if (q && tools.length === 0 && !connectorMatches)
|
|
60
|
+
continue;
|
|
61
|
+
filtered.push({ connector, tools });
|
|
62
|
+
}
|
|
63
|
+
return filtered;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the read-only status payload served at `/ui/data`. Broken connectors are
|
|
67
|
+
* isolated: they surface status "error" with an empty tool list rather than
|
|
68
|
+
* failing the whole payload.
|
|
69
|
+
*/
|
|
70
|
+
export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false) {
|
|
71
|
+
const requestScope = {};
|
|
72
|
+
const connectors = await Promise.all(registry.listConnectors().map(async (c) => {
|
|
73
|
+
const status = await registry.statusFor(c.id, baseUrl, requestScope);
|
|
74
|
+
let tools = [];
|
|
75
|
+
// `status()` on an unauthenticated remote connector starts OAuth and
|
|
76
|
+
// stores its state + PKCE verifier. Probing listTools immediately
|
|
77
|
+
// afterward would start a second flow, overwrite that state, and return
|
|
78
|
+
// the now-stale first URL to the operator. Only inspect tools after
|
|
79
|
+
// status proves the connector is healthy.
|
|
80
|
+
if (status.state === "ok") {
|
|
81
|
+
try {
|
|
82
|
+
tools = (await registry.getTools(c.id, baseUrl, requestScope)).map((t) => ({
|
|
83
|
+
name: t.name,
|
|
84
|
+
address: `${c.id}.${t.name}`,
|
|
85
|
+
description: t.description,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// broken connector: reported via status "error", tools stay empty
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let credential;
|
|
93
|
+
if (c.credential && credentialVault) {
|
|
94
|
+
const credentialFields = (metadata) => c.credential?.fields?.map((field) => {
|
|
95
|
+
const fieldMetadata = metadata?.fields?.[field.name];
|
|
96
|
+
return {
|
|
97
|
+
name: field.name,
|
|
98
|
+
label: field.label,
|
|
99
|
+
...(field.description
|
|
100
|
+
? { description: field.description }
|
|
101
|
+
: {}),
|
|
102
|
+
...(field.placeholder
|
|
103
|
+
? { placeholder: field.placeholder }
|
|
104
|
+
: {}),
|
|
105
|
+
inputType: field.inputType ?? "password",
|
|
106
|
+
configured: Boolean(fieldMetadata),
|
|
107
|
+
...(fieldMetadata
|
|
108
|
+
? {
|
|
109
|
+
lastFour: fieldMetadata.lastFour,
|
|
110
|
+
updatedAt: fieldMetadata.updatedAt,
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
const metadata = await credentialVault.metadata(c.id);
|
|
117
|
+
const fields = credentialFields(metadata);
|
|
118
|
+
credential = {
|
|
119
|
+
label: c.credential.label,
|
|
120
|
+
...(c.credential.description
|
|
121
|
+
? { description: c.credential.description }
|
|
122
|
+
: {}),
|
|
123
|
+
...(c.credential.placeholder
|
|
124
|
+
? { placeholder: c.credential.placeholder }
|
|
125
|
+
: {}),
|
|
126
|
+
...(fields?.length ? { fields } : {}),
|
|
127
|
+
configured: fields?.length
|
|
128
|
+
? fields.every((field) => field.configured)
|
|
129
|
+
: Boolean(metadata),
|
|
130
|
+
removable: Boolean(metadata),
|
|
131
|
+
...(metadata
|
|
132
|
+
? {
|
|
133
|
+
lastFour: metadata.lastFour,
|
|
134
|
+
updatedAt: metadata.updatedAt,
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
137
|
+
testable: Boolean(c.testCredential || c.testCredentials),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
const fields = credentialFields();
|
|
142
|
+
credential = {
|
|
143
|
+
label: c.credential.label,
|
|
144
|
+
...(c.credential.description
|
|
145
|
+
? { description: c.credential.description }
|
|
146
|
+
: {}),
|
|
147
|
+
...(c.credential.placeholder
|
|
148
|
+
? { placeholder: c.credential.placeholder }
|
|
149
|
+
: {}),
|
|
150
|
+
...(fields?.length ? { fields } : {}),
|
|
151
|
+
configured: false,
|
|
152
|
+
removable: true,
|
|
153
|
+
testable: Boolean(c.testCredential || c.testCredentials),
|
|
154
|
+
error: "Stored credential could not be read.",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
id: c.id,
|
|
160
|
+
...(c.title ? { title: c.title } : {}),
|
|
161
|
+
description: c.description,
|
|
162
|
+
status: status.state,
|
|
163
|
+
...(status.message ? { message: status.message } : {}),
|
|
164
|
+
...(isSafeHttpUrl(status.authorizationUrl)
|
|
165
|
+
? { authorizationUrl: status.authorizationUrl }
|
|
166
|
+
: {}),
|
|
167
|
+
toolCount: tools.length,
|
|
168
|
+
tools,
|
|
169
|
+
...(credential ? { credential } : {}),
|
|
170
|
+
};
|
|
171
|
+
}));
|
|
172
|
+
return { serverInfo, connectors, activityEnabled };
|
|
173
|
+
}
|
|
174
|
+
function escapeHtmlAttr(value) {
|
|
175
|
+
return value
|
|
176
|
+
.replaceAll("&", "&")
|
|
177
|
+
.replaceAll('"', """)
|
|
178
|
+
.replaceAll("<", "<")
|
|
179
|
+
.replaceAll(">", ">");
|
|
180
|
+
}
|
|
181
|
+
function jsonForInlineScript(value) {
|
|
182
|
+
return JSON.stringify(value)
|
|
183
|
+
.replaceAll("<", "\\u003c")
|
|
184
|
+
.replaceAll(">", "\\u003e")
|
|
185
|
+
.replaceAll("&", "\\u0026");
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The `/ui` shell carries no connector data: everything comes client-side from
|
|
189
|
+
* the auth-gated `/ui/data`. A configured Clerk provider gives operators a
|
|
190
|
+
* normal sign-in flow and a short-lived session token; bearer-only deployments
|
|
191
|
+
* retain the manual-token fallback.
|
|
192
|
+
*/
|
|
193
|
+
export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding) {
|
|
194
|
+
const auth = uiAuth ?? { kind: "bearer" };
|
|
195
|
+
const brand = resolveBranding(branding);
|
|
196
|
+
const title = brand.ownerName
|
|
197
|
+
? `${brand.productName} — ${brand.ownerName}`
|
|
198
|
+
: brand.productName;
|
|
199
|
+
const owner = brand.ownerName
|
|
200
|
+
? brand.ownerUrl
|
|
201
|
+
? `<a class="brand navlink" href="${escapeHtmlAttr(brand.ownerUrl)}">${escapeHtmlAttr(brand.ownerName)}</a>`
|
|
202
|
+
: `<span class="brand">${escapeHtmlAttr(brand.ownerName)}</span>`
|
|
203
|
+
: `<span class="brand">${escapeHtmlAttr(brand.productName)}</span>`;
|
|
204
|
+
const product = brand.ownerName
|
|
205
|
+
? `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
|
|
206
|
+
: "";
|
|
207
|
+
const clerkScript = uiAuth?.kind === "clerk"
|
|
208
|
+
? `<script defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(uiAuth.publishableKey)}" src="${escapeHtmlAttr(uiAuth.frontendApiUrl)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
209
|
+
: "";
|
|
210
|
+
return `<!doctype html>
|
|
211
|
+
<html lang="en">
|
|
212
|
+
<head>
|
|
213
|
+
<meta charset="utf-8">
|
|
214
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
215
|
+
<meta name="theme-color" content="#ffffff">
|
|
216
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
217
|
+
<link rel="shortcut icon" href="/favicon.ico">
|
|
218
|
+
<title>${escapeHtmlAttr(title)}</title>
|
|
219
|
+
${clerkScript}
|
|
220
|
+
<style>
|
|
221
|
+
:root {
|
|
222
|
+
color-scheme: light;
|
|
223
|
+
--ink: #000;
|
|
224
|
+
--paper: #fff;
|
|
225
|
+
--rule: #ccc;
|
|
226
|
+
--muted: #666;
|
|
227
|
+
--shell: 70rem;
|
|
228
|
+
--pad: 1rem;
|
|
229
|
+
--gap: 1.5rem;
|
|
230
|
+
--sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
231
|
+
--mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Code", Consolas,
|
|
232
|
+
monospace;
|
|
233
|
+
}
|
|
234
|
+
* { border-radius: 0; box-sizing: border-box; }
|
|
235
|
+
html {
|
|
236
|
+
background: var(--paper);
|
|
237
|
+
color: var(--ink);
|
|
238
|
+
font-family: var(--sans);
|
|
239
|
+
font-size: 16px;
|
|
240
|
+
line-height: 1.5;
|
|
241
|
+
-webkit-font-smoothing: antialiased;
|
|
242
|
+
text-rendering: optimizeLegibility;
|
|
243
|
+
}
|
|
244
|
+
body { margin: 0; min-height: 100vh; }
|
|
245
|
+
::selection { background: var(--ink); color: var(--paper); }
|
|
246
|
+
:is(h1, h2, h3, p, ul, ol) { margin: 0; padding: 0; }
|
|
247
|
+
:is(h1, h2, h3) { font-size: inherit; font-weight: 400; }
|
|
248
|
+
:is(ul, ol) { list-style: none; }
|
|
249
|
+
a { color: inherit; }
|
|
250
|
+
button, input { font: inherit; }
|
|
251
|
+
button {
|
|
252
|
+
background: none;
|
|
253
|
+
border: 0;
|
|
254
|
+
color: inherit;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
margin: 0;
|
|
257
|
+
padding: 0;
|
|
258
|
+
text-align: left;
|
|
259
|
+
}
|
|
260
|
+
button:disabled { cursor: wait; opacity: .5; }
|
|
261
|
+
input {
|
|
262
|
+
background: var(--paper);
|
|
263
|
+
border: 1px solid var(--rule);
|
|
264
|
+
color: var(--ink);
|
|
265
|
+
min-height: 2rem;
|
|
266
|
+
padding: .2rem .5rem;
|
|
267
|
+
}
|
|
268
|
+
input:focus-visible { outline-offset: -1px; }
|
|
269
|
+
:is(a, button, input, summary):focus-visible {
|
|
270
|
+
outline: 1px solid var(--ink);
|
|
271
|
+
}
|
|
272
|
+
:is(a, button, summary):focus-visible { outline-offset: 2px; }
|
|
273
|
+
|
|
274
|
+
.shell {
|
|
275
|
+
margin: 0 auto;
|
|
276
|
+
max-width: var(--shell);
|
|
277
|
+
padding-left: var(--pad);
|
|
278
|
+
padding-right: var(--pad);
|
|
279
|
+
}
|
|
280
|
+
.pgrid {
|
|
281
|
+
column-gap: var(--gap);
|
|
282
|
+
display: grid;
|
|
283
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
284
|
+
row-gap: 1rem;
|
|
285
|
+
}
|
|
286
|
+
.pcap { grid-column: 1; }
|
|
287
|
+
.pbody { grid-column: 2 / -1; min-width: 0; }
|
|
288
|
+
.cap, .meta { color: var(--muted); font-size: .9em; }
|
|
289
|
+
.mono { font-family: var(--mono); font-size: .78rem; }
|
|
290
|
+
.hidden { display: none !important; }
|
|
291
|
+
|
|
292
|
+
.masthead {
|
|
293
|
+
align-items: start;
|
|
294
|
+
padding-bottom: var(--pad);
|
|
295
|
+
padding-top: var(--pad);
|
|
296
|
+
}
|
|
297
|
+
.brand {
|
|
298
|
+
font-weight: 500;
|
|
299
|
+
grid-column: 1;
|
|
300
|
+
text-decoration: none;
|
|
301
|
+
}
|
|
302
|
+
.mast-nav {
|
|
303
|
+
display: flex;
|
|
304
|
+
gap: var(--gap);
|
|
305
|
+
grid-column: 2 / -1;
|
|
306
|
+
justify-content: space-between;
|
|
307
|
+
min-width: 0;
|
|
308
|
+
}
|
|
309
|
+
.mast-actions {
|
|
310
|
+
display: flex;
|
|
311
|
+
gap: var(--gap);
|
|
312
|
+
justify-content: flex-end;
|
|
313
|
+
min-width: 0;
|
|
314
|
+
}
|
|
315
|
+
.navlink,
|
|
316
|
+
.linklike {
|
|
317
|
+
text-decoration: underline;
|
|
318
|
+
text-decoration-thickness: 1.5px;
|
|
319
|
+
text-underline-offset: .22em;
|
|
320
|
+
}
|
|
321
|
+
.navlink { text-decoration-color: transparent; }
|
|
322
|
+
.navlink:hover,
|
|
323
|
+
.navlink:focus-visible,
|
|
324
|
+
.navlink.active { text-decoration-color: currentColor; }
|
|
325
|
+
.linklike { text-decoration-color: currentColor; }
|
|
326
|
+
.linklike:hover,
|
|
327
|
+
.linklike:focus-visible { text-decoration-color: transparent; }
|
|
328
|
+
|
|
329
|
+
.page { padding-bottom: 5rem; }
|
|
330
|
+
.lead { margin-top: 6rem; }
|
|
331
|
+
.section,
|
|
332
|
+
.section + .section { margin-top: 3rem; }
|
|
333
|
+
.lead-copy,
|
|
334
|
+
.body-copy { max-width: 34em; }
|
|
335
|
+
.lead-copy > * + *,
|
|
336
|
+
.body-copy > * + * { margin-top: 1.5rem; }
|
|
337
|
+
.row,
|
|
338
|
+
.actions {
|
|
339
|
+
align-items: center;
|
|
340
|
+
display: flex;
|
|
341
|
+
flex-wrap: wrap;
|
|
342
|
+
gap: var(--gap);
|
|
343
|
+
}
|
|
344
|
+
.row input { flex: 1; min-width: 12rem; }
|
|
345
|
+
|
|
346
|
+
.gate-actions { margin-top: 1.5rem; }
|
|
347
|
+
#err { margin-top: 1.5rem; text-decoration: underline; }
|
|
348
|
+
|
|
349
|
+
.endpoint { border-bottom: 1px solid var(--rule); border-top: 1px solid var(--rule); }
|
|
350
|
+
.endpoint-row {
|
|
351
|
+
align-items: baseline;
|
|
352
|
+
display: flex;
|
|
353
|
+
gap: var(--gap);
|
|
354
|
+
min-width: 0;
|
|
355
|
+
padding: .75rem 0;
|
|
356
|
+
}
|
|
357
|
+
.endpoint-row code {
|
|
358
|
+
flex: 1;
|
|
359
|
+
min-width: 0;
|
|
360
|
+
overflow-x: auto;
|
|
361
|
+
white-space: nowrap;
|
|
362
|
+
}
|
|
363
|
+
.endpoint-row button { flex: none; }
|
|
364
|
+
|
|
365
|
+
.connector-tools { border-bottom: 1px solid var(--rule); }
|
|
366
|
+
.toolbar { margin-bottom: 1.5rem; }
|
|
367
|
+
.toolbar input { flex-basis: 18rem; }
|
|
368
|
+
#notice { margin-bottom: .75rem; }
|
|
369
|
+
#notice:empty { display: none; }
|
|
370
|
+
#notice:not(:empty) { text-decoration: underline; }
|
|
371
|
+
.error-notice, .msg { text-decoration: underline; }
|
|
372
|
+
.card { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
373
|
+
.connector-head {
|
|
374
|
+
display: grid;
|
|
375
|
+
gap: var(--gap);
|
|
376
|
+
grid-template-columns: minmax(0, 2fr) minmax(10rem, 1fr);
|
|
377
|
+
}
|
|
378
|
+
.connector-title {
|
|
379
|
+
align-items: baseline;
|
|
380
|
+
display: flex;
|
|
381
|
+
gap: .5rem;
|
|
382
|
+
}
|
|
383
|
+
.card h2 { overflow-wrap: anywhere; }
|
|
384
|
+
.connector-state { text-align: right; }
|
|
385
|
+
.dot {
|
|
386
|
+
border: 1px solid var(--ink);
|
|
387
|
+
display: inline-block;
|
|
388
|
+
flex: none;
|
|
389
|
+
height: .5rem;
|
|
390
|
+
width: .5rem;
|
|
391
|
+
}
|
|
392
|
+
.dot.ok { background: var(--ink); }
|
|
393
|
+
.dot.auth_required {
|
|
394
|
+
background: linear-gradient(90deg, var(--ink) 50%, var(--paper) 50%);
|
|
395
|
+
}
|
|
396
|
+
.connector-description { margin-top: .25rem; max-width: 40rem; }
|
|
397
|
+
.connector-message,
|
|
398
|
+
.connector-auth { margin-top: .75rem; }
|
|
399
|
+
|
|
400
|
+
.credential { border-top: 1px solid var(--rule); margin-top: .75rem; padding-top: .75rem; }
|
|
401
|
+
.credential-head {
|
|
402
|
+
align-items: baseline;
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-wrap: wrap;
|
|
405
|
+
gap: .25rem var(--gap);
|
|
406
|
+
justify-content: space-between;
|
|
407
|
+
}
|
|
408
|
+
.credential-copy { margin-top: .25rem; max-width: 40rem; }
|
|
409
|
+
.credential-actions { display: flex; flex-wrap: wrap; gap: var(--gap); margin-top: .75rem; }
|
|
410
|
+
.credential-form {
|
|
411
|
+
align-items: center;
|
|
412
|
+
display: flex;
|
|
413
|
+
flex-wrap: wrap;
|
|
414
|
+
gap: .75rem var(--gap);
|
|
415
|
+
margin-top: .75rem;
|
|
416
|
+
}
|
|
417
|
+
.credential-form > input { flex: 1 1 18rem; }
|
|
418
|
+
.credential-fields { display: grid; flex: 1 1 100%; gap: .75rem; }
|
|
419
|
+
.credential-field {
|
|
420
|
+
align-items: center;
|
|
421
|
+
display: grid;
|
|
422
|
+
gap: var(--gap);
|
|
423
|
+
grid-template-columns: minmax(9rem, 12rem) 1fr;
|
|
424
|
+
}
|
|
425
|
+
.credential-field input { min-width: 0; width: 100%; }
|
|
426
|
+
.danger { text-decoration-style: double; }
|
|
427
|
+
|
|
428
|
+
details { margin-top: .75rem; }
|
|
429
|
+
summary { cursor: pointer; list-style: none; width: max-content; }
|
|
430
|
+
summary::-webkit-details-marker { display: none; }
|
|
431
|
+
.tool-list { border-bottom: 1px solid var(--rule); margin-top: .5rem; }
|
|
432
|
+
.tool {
|
|
433
|
+
border-top: 1px solid var(--rule);
|
|
434
|
+
display: grid;
|
|
435
|
+
gap: .25rem var(--gap);
|
|
436
|
+
grid-template-columns: minmax(12rem, 1fr) minmax(0, 2fr);
|
|
437
|
+
padding: .5rem 0;
|
|
438
|
+
}
|
|
439
|
+
.tool code { font-family: var(--mono); font-size: .78rem; overflow-wrap: anywhere; }
|
|
440
|
+
.tool .td { color: var(--muted); font-size: .9em; }
|
|
441
|
+
.empty { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
442
|
+
|
|
443
|
+
.activity-copy { margin-bottom: 1.5rem; }
|
|
444
|
+
.activity-controls { margin-bottom: 1.5rem; }
|
|
445
|
+
.activity-controls input { flex: 1 1 18rem; }
|
|
446
|
+
#activityNotice { margin-bottom: .75rem; }
|
|
447
|
+
.activity-ledger { border-bottom: 1px solid var(--rule); }
|
|
448
|
+
.activity-item {
|
|
449
|
+
border-top: 1px solid var(--rule);
|
|
450
|
+
display: grid;
|
|
451
|
+
gap: .25rem var(--gap);
|
|
452
|
+
grid-template-columns: minmax(9rem, .85fr) minmax(12rem, 1.4fr) minmax(8rem, .9fr);
|
|
453
|
+
padding: .75rem 0;
|
|
454
|
+
}
|
|
455
|
+
.activity-time,
|
|
456
|
+
.activity-actor,
|
|
457
|
+
.activity-detail { color: var(--muted); font-size: .82rem; }
|
|
458
|
+
.activity-address { font-family: var(--mono); font-size: .78rem; overflow-wrap: anywhere; }
|
|
459
|
+
.activity-outcome { font-size: .9em; }
|
|
460
|
+
.activity-item.error .activity-outcome,
|
|
461
|
+
.activity-item.timeout .activity-outcome { text-decoration: underline; }
|
|
462
|
+
.activity-empty { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
463
|
+
.activity-more { margin-top: .75rem; }
|
|
464
|
+
|
|
465
|
+
@media (max-width: 36.99rem) {
|
|
466
|
+
.pgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
467
|
+
.pcap,
|
|
468
|
+
.pbody { grid-column: 1 / -1; }
|
|
469
|
+
.masthead .brand { grid-column: 1; }
|
|
470
|
+
.mast-nav { grid-column: 1 / -1; grid-row: 2; justify-content: flex-start; }
|
|
471
|
+
.product { display: none; }
|
|
472
|
+
.mast-actions { font-size: .875rem; gap: .5rem; white-space: nowrap; }
|
|
473
|
+
.lead { margin-top: 4rem; }
|
|
474
|
+
.section,
|
|
475
|
+
.section + .section { margin-top: 2.5rem; }
|
|
476
|
+
.connector-head,
|
|
477
|
+
.tool,
|
|
478
|
+
.activity-item { grid-template-columns: 1fr; }
|
|
479
|
+
.connector-state { text-align: left; }
|
|
480
|
+
.credential-field { align-items: start; grid-template-columns: 1fr; gap: .25rem; }
|
|
481
|
+
}
|
|
482
|
+
</style>
|
|
483
|
+
</head>
|
|
484
|
+
<body>
|
|
485
|
+
<header class="masthead shell pgrid">
|
|
486
|
+
${owner}
|
|
487
|
+
<div class="mast-nav">
|
|
488
|
+
${product}
|
|
489
|
+
<nav id="appNav" class="mast-actions hidden" aria-label="Dashboard views">
|
|
490
|
+
<button id="configTab" class="navlink active" type="button"
|
|
491
|
+
aria-pressed="true">Connections</button>
|
|
492
|
+
<button id="activityTab" class="navlink hidden" type="button"
|
|
493
|
+
aria-pressed="false">Activity</button>
|
|
494
|
+
<button id="change" class="navlink hidden" type="button">Change token</button>
|
|
495
|
+
<button id="signout" class="navlink hidden" type="button">Sign out</button>
|
|
496
|
+
</nav>
|
|
497
|
+
</div>
|
|
498
|
+
</header>
|
|
499
|
+
|
|
500
|
+
<main id="gate" class="page shell hidden">
|
|
501
|
+
<section class="lead pgrid">
|
|
502
|
+
<h1 class="pcap">Tool connections</h1>
|
|
503
|
+
<div class="pbody lead-copy">
|
|
504
|
+
<p>${escapeHtmlAttr(brand.description)}</p>
|
|
505
|
+
<p id="gateCopy" class="meta"></p>
|
|
506
|
+
<div id="tokenGate" class="row gate-actions hidden">
|
|
507
|
+
<input id="token" type="password" placeholder="Bearer token" autocomplete="off"
|
|
508
|
+
aria-label="Bearer token">
|
|
509
|
+
<button id="save" class="linklike" type="button">Open dashboard</button>
|
|
510
|
+
</div>
|
|
511
|
+
<div id="clerkGate" class="actions gate-actions hidden">
|
|
512
|
+
<button id="signin" class="linklike" type="button">Team sign in</button>
|
|
513
|
+
<button id="gateSignout" class="linklike hidden" type="button">Sign out</button>
|
|
514
|
+
</div>
|
|
515
|
+
<p id="err"></p>
|
|
516
|
+
</div>
|
|
517
|
+
</section>
|
|
518
|
+
</main>
|
|
519
|
+
|
|
520
|
+
<main id="app" class="page shell hidden">
|
|
521
|
+
<section id="configView">
|
|
522
|
+
<div class="lead pgrid">
|
|
523
|
+
<h1 class="pcap">MCP connection</h1>
|
|
524
|
+
<div class="pbody lead-copy">
|
|
525
|
+
<p>Use this endpoint to give an MCP client access to the tools below.</p>
|
|
526
|
+
<div class="endpoint">
|
|
527
|
+
<div class="endpoint-row">
|
|
528
|
+
<code id="mcpUrl" class="mono"></code>
|
|
529
|
+
<button id="copyMcpUrl" class="linklike" type="button">Copy URL</button>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
<p class="cap" id="serverInfo">Connecta status dashboard</p>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
<section class="section pgrid" aria-labelledby="connectorsHeading">
|
|
536
|
+
<h2 class="pcap" id="connectorsHeading">Connectors</h2>
|
|
537
|
+
<div class="pbody">
|
|
538
|
+
<div class="row toolbar">
|
|
539
|
+
<input id="filter" type="search" placeholder="Filter connectors or tools…"
|
|
540
|
+
aria-label="Filter connectors or tools">
|
|
541
|
+
</div>
|
|
542
|
+
<p id="notice" role="status" aria-live="polite"></p>
|
|
543
|
+
<div id="list" class="connector-tools"></div>
|
|
544
|
+
</div>
|
|
545
|
+
</section>
|
|
546
|
+
</section>
|
|
547
|
+
<section id="activityView" class="hidden">
|
|
548
|
+
<div class="lead pgrid">
|
|
549
|
+
<h1 class="pcap">Tool activity</h1>
|
|
550
|
+
<div class="pbody">
|
|
551
|
+
<p class="activity-copy" id="activitySummary">Arguments and results are never stored.</p>
|
|
552
|
+
<div class="row activity-controls">
|
|
553
|
+
<input id="activitySearch" type="search"
|
|
554
|
+
placeholder="Search user, tool, or outcome…"
|
|
555
|
+
aria-label="Search loaded activity">
|
|
556
|
+
<button id="refreshActivity" class="linklike" type="button">Refresh</button>
|
|
557
|
+
</div>
|
|
558
|
+
<p id="activityNotice" class="meta" role="status" aria-live="polite"></p>
|
|
559
|
+
<div id="activityList" class="activity-ledger"></div>
|
|
560
|
+
<button id="moreActivity" class="linklike activity-more hidden" type="button">Load older</button>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
</section>
|
|
564
|
+
</main>
|
|
565
|
+
|
|
566
|
+
<script>
|
|
567
|
+
const AUTH = ${jsonForInlineScript(auth)};
|
|
568
|
+
const MCP_URL = ${jsonForInlineScript(mcpUrl)};
|
|
569
|
+
const filterUiConnectors = ${filterUiConnectors.toString()};
|
|
570
|
+
const KEY = "connecta:token";
|
|
571
|
+
const $ = (id) => document.getElementById(id);
|
|
572
|
+
let DATA = null;
|
|
573
|
+
let ACTIVITY = [];
|
|
574
|
+
let ACTIVITY_CURSOR = null;
|
|
575
|
+
let ACTIVITY_LOADED = false;
|
|
576
|
+
$("mcpUrl").textContent = MCP_URL;
|
|
577
|
+
|
|
578
|
+
function esc(s) {
|
|
579
|
+
return String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
|
|
580
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Only http/https may become a clickable href — mirrors the server-side gate
|
|
584
|
+
// so a hostile scheme (javascript:, data:) can never be linked. Defense in depth.
|
|
585
|
+
function safeHttp(u) {
|
|
586
|
+
try {
|
|
587
|
+
const p = new URL(u).protocol;
|
|
588
|
+
return p === "http:" || p === "https:" ? u : null;
|
|
589
|
+
} catch (e) { return null; }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function formatDate(value) {
|
|
593
|
+
if (!value) return "";
|
|
594
|
+
const date = new Date(value);
|
|
595
|
+
return Number.isNaN(date.valueOf()) ? "" : date.toLocaleString();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function setNotice(message, isError) {
|
|
599
|
+
$("notice").textContent = message || "";
|
|
600
|
+
$("notice").classList.toggle("error-notice", Boolean(isError));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function sessionToken() {
|
|
604
|
+
return AUTH.kind === "clerk"
|
|
605
|
+
? await Clerk.session?.getToken()
|
|
606
|
+
: localStorage.getItem(KEY);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function showGate(msg) {
|
|
610
|
+
$("app").classList.add("hidden");
|
|
611
|
+
$("appNav").classList.add("hidden");
|
|
612
|
+
$("gate").classList.remove("hidden");
|
|
613
|
+
$("err").textContent = msg || "";
|
|
614
|
+
if (AUTH.kind === "clerk") {
|
|
615
|
+
const signedIn = Boolean(window.Clerk && Clerk.user);
|
|
616
|
+
$("gateCopy").textContent = signedIn
|
|
617
|
+
? "Signed in with Clerk, but this account cannot open the dashboard."
|
|
618
|
+
: "Sign in with Clerk to open the dashboard.";
|
|
619
|
+
$("signin").classList.toggle("hidden", signedIn);
|
|
620
|
+
$("gateSignout").classList.toggle("hidden", !signedIn);
|
|
621
|
+
} else {
|
|
622
|
+
$("gateCopy").textContent = "";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function load() {
|
|
627
|
+
let token;
|
|
628
|
+
try {
|
|
629
|
+
token = await sessionToken();
|
|
630
|
+
} catch (e) {
|
|
631
|
+
return showGate("Could not read the Clerk session: " + e.message);
|
|
632
|
+
}
|
|
633
|
+
if (!token) return showGate("");
|
|
634
|
+
let res;
|
|
635
|
+
try {
|
|
636
|
+
res = await fetch("/ui/data", { headers: { Authorization: "Bearer " + token } });
|
|
637
|
+
} catch (e) {
|
|
638
|
+
return showGate("Network error: " + e.message);
|
|
639
|
+
}
|
|
640
|
+
if (res.status === 401 || res.status === 403) {
|
|
641
|
+
if (AUTH.kind === "clerk") {
|
|
642
|
+
return showGate(
|
|
643
|
+
res.status === 403
|
|
644
|
+
? "This Clerk account is not allowed to access connecta."
|
|
645
|
+
: "Your Clerk session was not accepted. Sign out and try again."
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
localStorage.removeItem(KEY);
|
|
649
|
+
return showGate("Token rejected — enter a valid bearer token.");
|
|
650
|
+
}
|
|
651
|
+
if (!res.ok) return showGate("Error " + res.status);
|
|
652
|
+
DATA = await res.json();
|
|
653
|
+
$("gate").classList.add("hidden");
|
|
654
|
+
$("app").classList.remove("hidden");
|
|
655
|
+
$("appNav").classList.remove("hidden");
|
|
656
|
+
const si = DATA.serverInfo || {};
|
|
657
|
+
$("serverInfo").textContent = (si.name || "Connecta") + " v" + (si.version || "?");
|
|
658
|
+
$("activityTab").classList.toggle("hidden", !DATA.activityEnabled);
|
|
659
|
+
render();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function showView(view) {
|
|
663
|
+
const activity = view === "activity";
|
|
664
|
+
$("configView").classList.toggle("hidden", activity);
|
|
665
|
+
$("activityView").classList.toggle("hidden", !activity);
|
|
666
|
+
$("configTab").classList.toggle("active", !activity);
|
|
667
|
+
$("activityTab").classList.toggle("active", activity);
|
|
668
|
+
$("configTab").setAttribute("aria-pressed", String(!activity));
|
|
669
|
+
$("activityTab").setAttribute("aria-pressed", String(activity));
|
|
670
|
+
if (activity && !ACTIVITY_LOADED) loadActivity(true);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function actorLabel(actor) {
|
|
674
|
+
if (!actor || !actor.kind) return "unknown";
|
|
675
|
+
return actor.id ? actor.kind + " · " + actor.id : actor.kind;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function renderActivity() {
|
|
679
|
+
const list = $("activityList");
|
|
680
|
+
list.innerHTML = "";
|
|
681
|
+
const query = $("activitySearch").value.trim().toLowerCase();
|
|
682
|
+
const visible = ACTIVITY.filter((event) => {
|
|
683
|
+
if (!query) return true;
|
|
684
|
+
const actor = event.actor || {};
|
|
685
|
+
return [
|
|
686
|
+
event.address,
|
|
687
|
+
event.connectorId,
|
|
688
|
+
event.toolName,
|
|
689
|
+
event.source,
|
|
690
|
+
event.outcome,
|
|
691
|
+
event.errorCode,
|
|
692
|
+
actor.kind,
|
|
693
|
+
actor.id,
|
|
694
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
695
|
+
});
|
|
696
|
+
const uniqueTools = new Set(ACTIVITY.map((event) => event.address)).size;
|
|
697
|
+
$("activitySummary").textContent = ACTIVITY.length
|
|
698
|
+
? ACTIVITY.length + " loaded call" + (ACTIVITY.length === 1 ? "" : "s") +
|
|
699
|
+
" · " + uniqueTools + " tool" + (uniqueTools === 1 ? "" : "s") +
|
|
700
|
+
" · no arguments or results stored"
|
|
701
|
+
: "Arguments and results are never stored.";
|
|
702
|
+
if (visible.length === 0) {
|
|
703
|
+
list.innerHTML = '<div class="activity-empty">' +
|
|
704
|
+
(query ? "No loaded activity matches this search." :
|
|
705
|
+
"No connector tool calls recorded yet.") + "</div>";
|
|
706
|
+
}
|
|
707
|
+
for (const event of visible) {
|
|
708
|
+
const item = document.createElement("article");
|
|
709
|
+
item.className = "activity-item " + esc(event.outcome);
|
|
710
|
+
const retryCopy = event.attempts > 1 ? " · " + event.attempts + " attempts" : "";
|
|
711
|
+
const errorCopy = event.errorCode ? " · " + esc(event.errorCode) : "";
|
|
712
|
+
item.innerHTML =
|
|
713
|
+
'<div><div class="activity-time">' + esc(formatDate(event.occurredAt)) +
|
|
714
|
+
'</div><div class="activity-actor">' + esc(actorLabel(event.actor)) + '</div></div>' +
|
|
715
|
+
'<div><div class="activity-address">' + esc(event.address) +
|
|
716
|
+
'</div><div class="activity-detail">' + esc(event.source) + retryCopy +
|
|
717
|
+
errorCopy + '</div></div>' +
|
|
718
|
+
'<div><div class="activity-outcome">' + esc(event.outcome) +
|
|
719
|
+
'</div><div class="activity-detail">' + esc(event.durationMs) + ' ms</div></div>';
|
|
720
|
+
list.appendChild(item);
|
|
721
|
+
}
|
|
722
|
+
$("moreActivity").classList.toggle("hidden", !ACTIVITY_CURSOR);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function loadActivity(reset) {
|
|
726
|
+
if (!DATA?.activityEnabled) return;
|
|
727
|
+
$("activityNotice").textContent = "Loading activity…";
|
|
728
|
+
$("refreshActivity").disabled = true;
|
|
729
|
+
$("moreActivity").disabled = true;
|
|
730
|
+
try {
|
|
731
|
+
const token = await sessionToken();
|
|
732
|
+
if (!token) throw new Error("Your session has expired.");
|
|
733
|
+
const cursor = reset ? null : ACTIVITY_CURSOR;
|
|
734
|
+
const params = new URLSearchParams({ limit: "50" });
|
|
735
|
+
if (cursor) params.set("cursor", cursor);
|
|
736
|
+
const res = await fetch("/ui/activity?" + params, {
|
|
737
|
+
headers: { Authorization: "Bearer " + token },
|
|
738
|
+
});
|
|
739
|
+
let payload = {};
|
|
740
|
+
try { payload = await res.json(); } catch (e) {}
|
|
741
|
+
if (!res.ok) {
|
|
742
|
+
throw new Error(payload.error || "Activity could not be loaded (" + res.status + ").");
|
|
743
|
+
}
|
|
744
|
+
ACTIVITY = reset
|
|
745
|
+
? (payload.events || [])
|
|
746
|
+
: ACTIVITY.concat(payload.events || []);
|
|
747
|
+
ACTIVITY_CURSOR = payload.nextCursor || null;
|
|
748
|
+
ACTIVITY_LOADED = true;
|
|
749
|
+
$("activityNotice").textContent = "";
|
|
750
|
+
renderActivity();
|
|
751
|
+
} catch (e) {
|
|
752
|
+
$("activityNotice").textContent = e.message || "Activity could not be loaded.";
|
|
753
|
+
} finally {
|
|
754
|
+
$("refreshActivity").disabled = false;
|
|
755
|
+
$("moreActivity").disabled = false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function render() {
|
|
760
|
+
const q = $("filter").value.trim().toLowerCase();
|
|
761
|
+
const list = $("list");
|
|
762
|
+
list.innerHTML = "";
|
|
763
|
+
for (const filtered of filterUiConnectors(DATA.connectors, q)) {
|
|
764
|
+
const c = filtered.connector;
|
|
765
|
+
const tools = filtered.tools;
|
|
766
|
+
const el = document.createElement("div");
|
|
767
|
+
el.className = "card";
|
|
768
|
+
const status = c.status === "ok"
|
|
769
|
+
? "Connected"
|
|
770
|
+
: c.status === "auth_required"
|
|
771
|
+
? "Authorization needed"
|
|
772
|
+
: "Unavailable";
|
|
773
|
+
let head = '<div class="connector-head"><div><div class="connector-title">' +
|
|
774
|
+
'<span class="dot ' + esc(c.status) + '" aria-hidden="true"></span>' +
|
|
775
|
+
'<h2>' + esc(c.title || c.id) + "</h2></div>";
|
|
776
|
+
if (c.description) {
|
|
777
|
+
head += '<p class="connector-description meta">' + esc(c.description) + "</p>";
|
|
778
|
+
}
|
|
779
|
+
head += '</div><div class="connector-state cap">' + esc(status) + " · " +
|
|
780
|
+
c.toolCount + (c.toolCount === 1 ? " tool" : " tools") +
|
|
781
|
+
'<br><span class="mono">' + esc(c.id) + "</span></div></div>";
|
|
782
|
+
if (c.message) {
|
|
783
|
+
head += '<p class="connector-message msg">' + esc(c.message) + "</p>";
|
|
784
|
+
}
|
|
785
|
+
if (c.authorizationUrl) {
|
|
786
|
+
const safe = safeHttp(c.authorizationUrl);
|
|
787
|
+
head += safe
|
|
788
|
+
? '<p class="connector-auth"><a class="linklike" href="' + esc(safe) +
|
|
789
|
+
'" target="_blank" rel="noopener">Authorize connector →</a></p>'
|
|
790
|
+
: '<p class="connector-auth meta">Authorization URL: ' +
|
|
791
|
+
esc(c.authorizationUrl) + "</p>";
|
|
792
|
+
}
|
|
793
|
+
if (c.credential) {
|
|
794
|
+
const cred = c.credential;
|
|
795
|
+
const configured = Boolean(cred.configured);
|
|
796
|
+
const removable = configured || Boolean(cred.removable);
|
|
797
|
+
const state = configured
|
|
798
|
+
? "configured · ••••" + esc(cred.lastFour || "")
|
|
799
|
+
: "not configured";
|
|
800
|
+
const updated = configured && cred.updatedAt
|
|
801
|
+
? " · updated " + esc(formatDate(cred.updatedAt))
|
|
802
|
+
: "";
|
|
803
|
+
head += '<section class="credential" aria-label="' + esc(cred.label) + '">';
|
|
804
|
+
head += '<div class="credential-head"><span class="credential-label">' +
|
|
805
|
+
esc(cred.label) + '</span><span class="credential-state">' + state +
|
|
806
|
+
updated + "</span></div>";
|
|
807
|
+
if (cred.description) {
|
|
808
|
+
head += '<p class="credential-copy meta">' + esc(cred.description) + "</p>";
|
|
809
|
+
}
|
|
810
|
+
if (cred.error) {
|
|
811
|
+
head += '<div class="msg">' + esc(cred.error) + "</div>";
|
|
812
|
+
}
|
|
813
|
+
if (AUTH.kind === "clerk") {
|
|
814
|
+
head += '<div class="credential-actions">';
|
|
815
|
+
head += '<button class="linklike" type="button" data-credential-action="edit" data-connector="' +
|
|
816
|
+
esc(c.id) + '">' + (removable ? "Replace" : "Add credential") + "</button>";
|
|
817
|
+
if (configured && cred.testable) {
|
|
818
|
+
head += '<button class="linklike" type="button" data-credential-action="test" data-connector="' +
|
|
819
|
+
esc(c.id) + '">Test</button>';
|
|
820
|
+
}
|
|
821
|
+
if (removable) {
|
|
822
|
+
head += '<button type="button" class="linklike danger" data-credential-action="remove" data-connector="' +
|
|
823
|
+
esc(c.id) + '">Remove</button>';
|
|
824
|
+
}
|
|
825
|
+
head += "</div>";
|
|
826
|
+
head += '<div class="credential-form hidden" data-credential-form="' +
|
|
827
|
+
esc(c.id) + '">';
|
|
828
|
+
if (cred.fields && cred.fields.length) {
|
|
829
|
+
head += '<div class="credential-fields">';
|
|
830
|
+
for (const field of cred.fields) {
|
|
831
|
+
head += '<div class="credential-field"><label>' +
|
|
832
|
+
esc(field.label) + '</label><input type="' +
|
|
833
|
+
esc(field.inputType || "password") + '" data-credential-field="' +
|
|
834
|
+
esc(field.name) + '" aria-label="' + esc(field.label) +
|
|
835
|
+
'" placeholder="' + esc(field.placeholder || field.label) +
|
|
836
|
+
'" autocomplete="' +
|
|
837
|
+
(field.inputType === "password" ? "new-password" : "off") +
|
|
838
|
+
'" autocapitalize="none" spellcheck="false"></div>';
|
|
839
|
+
}
|
|
840
|
+
head += "</div>";
|
|
841
|
+
} else {
|
|
842
|
+
head += '<input type="password" data-credential-input="' +
|
|
843
|
+
esc(c.id) + '" aria-label="' + esc(cred.label) + '" placeholder="' +
|
|
844
|
+
esc(cred.placeholder || "Paste credential") +
|
|
845
|
+
'" autocomplete="new-password" autocapitalize="none" spellcheck="false">';
|
|
846
|
+
}
|
|
847
|
+
head += '<button class="linklike" type="button" data-credential-action="save" data-connector="' +
|
|
848
|
+
esc(c.id) + '">Save</button><button class="linklike" type="button" data-credential-action="cancel" data-connector="' +
|
|
849
|
+
esc(c.id) + '">Cancel</button></div>';
|
|
850
|
+
} else {
|
|
851
|
+
head += '<p class="credential-copy meta">Team sign in is required to manage this credential.</p>';
|
|
852
|
+
}
|
|
853
|
+
head += "</section>";
|
|
854
|
+
}
|
|
855
|
+
let body = "";
|
|
856
|
+
if (tools.length) {
|
|
857
|
+
body = "<details" + (q ? " open" : "") +
|
|
858
|
+
'><summary class="linklike">Show tools (' + tools.length + ")</summary>" +
|
|
859
|
+
'<div class="tool-list">';
|
|
860
|
+
for (const t of tools) {
|
|
861
|
+
body += '<div class="tool"><code>' + esc(t.address) + "</code>";
|
|
862
|
+
if (t.description) body += '<span class="td">' + esc(t.description) + "</span>";
|
|
863
|
+
body += "</div>";
|
|
864
|
+
}
|
|
865
|
+
body += "</div></details>";
|
|
866
|
+
}
|
|
867
|
+
el.innerHTML = head + body;
|
|
868
|
+
list.appendChild(el);
|
|
869
|
+
}
|
|
870
|
+
if (!list.children.length) {
|
|
871
|
+
list.innerHTML = '<p class="empty">No connectors or tools match this filter.</p>';
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function credentialRequest(connector, method, action, body) {
|
|
876
|
+
const token = await sessionToken();
|
|
877
|
+
if (!token) throw new Error("Your Clerk session has expired.");
|
|
878
|
+
const suffix = action ? "/" + action : "";
|
|
879
|
+
const res = await fetch(
|
|
880
|
+
"/ui/credentials/" + encodeURIComponent(connector) + suffix,
|
|
881
|
+
{
|
|
882
|
+
method,
|
|
883
|
+
headers: {
|
|
884
|
+
Authorization: "Bearer " + token,
|
|
885
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
886
|
+
},
|
|
887
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
888
|
+
},
|
|
889
|
+
);
|
|
890
|
+
if (res.status === 204) return null;
|
|
891
|
+
let payload = {};
|
|
892
|
+
try { payload = await res.json(); } catch (e) {}
|
|
893
|
+
if (!res.ok) throw new Error(payload.error || "Request failed (" + res.status + ").");
|
|
894
|
+
return payload;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function credentialForm(connector) {
|
|
898
|
+
return document.querySelector('[data-credential-form="' +
|
|
899
|
+
CSS.escape(connector) + '"]');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
$("list").onclick = async (event) => {
|
|
903
|
+
const button = event.target.closest("[data-credential-action]");
|
|
904
|
+
if (!button) return;
|
|
905
|
+
const connector = button.dataset.connector;
|
|
906
|
+
const action = button.dataset.credentialAction;
|
|
907
|
+
const form = credentialForm(connector);
|
|
908
|
+
|
|
909
|
+
if (action === "edit") {
|
|
910
|
+
form.classList.remove("hidden");
|
|
911
|
+
form.querySelector("input")?.focus();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (action === "cancel") {
|
|
915
|
+
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
916
|
+
form.classList.add("hidden");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (action === "remove" && !window.confirm(
|
|
920
|
+
"Remove this credential? The connector will stop authenticating until a replacement is added."
|
|
921
|
+
)) return;
|
|
922
|
+
|
|
923
|
+
setNotice("");
|
|
924
|
+
const buttons = [...document.querySelectorAll(
|
|
925
|
+
'[data-connector="' + CSS.escape(connector) + '"]'
|
|
926
|
+
)];
|
|
927
|
+
buttons.forEach((item) => { item.disabled = true; });
|
|
928
|
+
try {
|
|
929
|
+
if (action === "save") {
|
|
930
|
+
const fieldInputs = [...form.querySelectorAll("[data-credential-field]")];
|
|
931
|
+
if (fieldInputs.length) {
|
|
932
|
+
const values = {};
|
|
933
|
+
for (const input of fieldInputs) {
|
|
934
|
+
const value = input.value.trim();
|
|
935
|
+
if (!value) throw new Error("Complete every credential field before saving.");
|
|
936
|
+
values[input.dataset.credentialField] = value;
|
|
937
|
+
}
|
|
938
|
+
await credentialRequest(connector, "PUT", "", { values });
|
|
939
|
+
} else {
|
|
940
|
+
const input = form.querySelector("[data-credential-input]");
|
|
941
|
+
const value = input.value.trim();
|
|
942
|
+
if (!value) throw new Error("Paste a credential before saving.");
|
|
943
|
+
await credentialRequest(connector, "PUT", "", { value });
|
|
944
|
+
}
|
|
945
|
+
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
946
|
+
setNotice("Credential saved.");
|
|
947
|
+
await load();
|
|
948
|
+
} else if (action === "remove") {
|
|
949
|
+
await credentialRequest(connector, "DELETE");
|
|
950
|
+
setNotice("Credential removed.");
|
|
951
|
+
await load();
|
|
952
|
+
} else if (action === "test") {
|
|
953
|
+
const result = await credentialRequest(connector, "POST", "test");
|
|
954
|
+
setNotice(
|
|
955
|
+
result.message || (result.ok ? "Credential is valid." : "Credential test failed."),
|
|
956
|
+
!result.ok,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
} catch (e) {
|
|
960
|
+
setNotice(e.message || "Credential action failed.", true);
|
|
961
|
+
} finally {
|
|
962
|
+
buttons.forEach((item) => { item.disabled = false; });
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
$("save").onclick = () => {
|
|
967
|
+
const v = $("token").value.trim();
|
|
968
|
+
if (!v) return;
|
|
969
|
+
localStorage.setItem(KEY, v);
|
|
970
|
+
$("token").value = "";
|
|
971
|
+
load();
|
|
972
|
+
};
|
|
973
|
+
$("change").onclick = () => { localStorage.removeItem(KEY); showGate(""); };
|
|
974
|
+
$("copyMcpUrl").onclick = async () => {
|
|
975
|
+
const button = $("copyMcpUrl");
|
|
976
|
+
try {
|
|
977
|
+
await navigator.clipboard.writeText(MCP_URL);
|
|
978
|
+
button.textContent = "Copied";
|
|
979
|
+
} catch (e) {
|
|
980
|
+
button.textContent = "Copy failed";
|
|
981
|
+
}
|
|
982
|
+
window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
|
|
983
|
+
};
|
|
984
|
+
$("signin").onclick = () => Clerk.redirectToSignIn({
|
|
985
|
+
signInFallbackRedirectUrl: window.location.href,
|
|
986
|
+
signUpFallbackRedirectUrl: window.location.href,
|
|
987
|
+
});
|
|
988
|
+
$("gateSignout").onclick = () => Clerk.signOut({ redirectUrl: window.location.href });
|
|
989
|
+
$("signout").onclick = () => Clerk.signOut({ redirectUrl: window.location.href });
|
|
990
|
+
$("filter").oninput = () => { if (DATA) render(); };
|
|
991
|
+
$("configTab").onclick = () => showView("config");
|
|
992
|
+
$("activityTab").onclick = () => showView("activity");
|
|
993
|
+
$("refreshActivity").onclick = () => loadActivity(true);
|
|
994
|
+
$("moreActivity").onclick = () => loadActivity(false);
|
|
995
|
+
$("activitySearch").oninput = () => renderActivity();
|
|
996
|
+
|
|
997
|
+
async function init() {
|
|
998
|
+
if (AUTH.kind === "clerk") {
|
|
999
|
+
$("clerkGate").classList.remove("hidden");
|
|
1000
|
+
$("signout").classList.remove("hidden");
|
|
1001
|
+
try {
|
|
1002
|
+
if (!window.Clerk) {
|
|
1003
|
+
return showGate("Clerk could not load. Check your network and try again.");
|
|
1004
|
+
}
|
|
1005
|
+
await Clerk.load({
|
|
1006
|
+
...(AUTH.signInUrl ? { signInUrl: AUTH.signInUrl } : {}),
|
|
1007
|
+
...(AUTH.signUpUrl ? { signUpUrl: AUTH.signUpUrl } : {}),
|
|
1008
|
+
signInFallbackRedirectUrl: window.location.href,
|
|
1009
|
+
signUpFallbackRedirectUrl: window.location.href,
|
|
1010
|
+
afterSignOutUrl: window.location.href,
|
|
1011
|
+
});
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
return showGate("Clerk could not initialize: " + e.message);
|
|
1014
|
+
}
|
|
1015
|
+
} else {
|
|
1016
|
+
$("tokenGate").classList.remove("hidden");
|
|
1017
|
+
$("change").classList.remove("hidden");
|
|
1018
|
+
}
|
|
1019
|
+
await load();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (AUTH.kind === "clerk") {
|
|
1023
|
+
window.addEventListener("load", init);
|
|
1024
|
+
} else {
|
|
1025
|
+
init();
|
|
1026
|
+
}
|
|
1027
|
+
</script>
|
|
1028
|
+
</body>
|
|
1029
|
+
</html>`;
|
|
1030
|
+
}
|
|
1031
|
+
//# sourceMappingURL=ui.js.map
|