Merge branch 'worktree-agent-a11ea05ad7db44298'
6 files+270−610/6 viewed
| 25 | 25 | import { type ConnectorScope, type ConnectorView, connectorPath } from "@g1t/contracts/connectors"; | |
| 26 | 26 | ||
| 27 | 27 | import { AI_PROVIDERS } from "../lib/ai-providers"; | |
| 28 | + | import { type BrandIcon, brandIcon } from "../lib/brand-icons"; | |
| 28 | 29 | import { CONTACT } from "../lib/legal"; | |
| 29 | 30 | import { | |
| 30 | 31 | type CategoryFilter, | |
| ⋯ | |||
| 42 | 43 | import { cn } from "../lib/cn"; | |
| 43 | 44 | import { integrationsSection } from "../lib/integration-sections"; | |
| 44 | 45 | import { Badge } from "./ui/badge"; | |
| 46 | + | import { Button } from "./ui/button"; | |
| 45 | 47 | import { Hint } from "./ui/hint"; | |
| 46 | − | import { Input } from "./ui"; | |
| 48 | + | import { Input, InputAddon, InputGroup } from "./ui/input"; | |
| 47 | 49 | import { Card } from "./ui/card"; | |
| 48 | 50 | ||
| 49 | 51 | const CATEGORY_ICONS: Record<CategoryFilter, ReactNode> = { | |
| ⋯ | |||
| 71 | 73 | linear: "#8b8ff0", | |
| 72 | 74 | }; | |
| 73 | 75 | ||
| 74 | − | /** A provider's mark: its initials in its colour, or an icon for the generic ones. */ | |
| 75 | − | export function ProviderMark({ provider, size = 32 }: { provider: Provider; size?: number }) { | |
| 76 | − | const entry = AI_PROVIDERS[provider]; | |
| 77 | − | const color = entry?.color ?? PROVIDER_COLORS[provider] ?? "#a1a1aa"; | |
| 78 | − | const content = | |
| 79 | − | provider === "webhook" ? ( | |
| 80 | − | <Webhook size={size * 0.5} /> | |
| 81 | − | ) : provider.endsWith("_endpoint") ? ( | |
| 82 | − | <Bot size={size * 0.5} /> | |
| 83 | − | ) : ( | |
| 84 | − | entry?.mark || PROVIDERS[provider].label[0] | |
| 85 | − | ); | |
| 76 | + | /** | |
| 77 | + | * A mark's tile: a brand's icon, or letters or an icon, on a wash of | |
| 78 | + | * `color`. With no colour (a brand drawn in black) it is drawn in the text | |
| 79 | + | * colour on a neutral wash, so it reads in both themes. | |
| 80 | + | */ | |
| 81 | + | function MarkTile({ color, size, children }: { color: string | null; size: number; children: ReactNode }) { | |
| 82 | + | const tint = color ?? "var(--color-fg)"; | |
| 86 | 83 | return ( | |
| 87 | 84 | <span | |
| 88 | 85 | aria-hidden="true" | |
| ⋯ | |||
| 91 | 88 | width: size, | |
| 92 | 89 | height: size, | |
| 93 | 90 | borderRadius: size * 0.28, | |
| 94 | − | fontSize: size * (String(content).length > 1 ? 0.34 : 0.42), | |
| 95 | − | background: `color-mix(in srgb, ${color} 16%, transparent)`, | |
| 96 | − | // Toward the text colour, so the letters read on the tile in both themes. | |
| 97 | − | color: `color-mix(in srgb, ${color} 60%, var(--color-fg))`, | |
| 98 | − | boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${color} 30%, transparent)`, | |
| 91 | + | fontSize: size * (typeof children === "string" && children.length > 1 ? 0.34 : 0.42), | |
| 92 | + | background: `color-mix(in srgb, ${tint} ${color ? 16 : 8}%, transparent)`, | |
| 93 | + | // Toward the text colour, so the mark reads on the tile in both themes. | |
| 94 | + | color: color ? `color-mix(in srgb, ${color} 60%, var(--color-fg))` : "var(--color-fg)", | |
| 95 | + | boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${tint} ${color ? 30 : 14}%, transparent)`, | |
| 99 | 96 | }} | |
| 100 | 97 | > | |
| 101 | − | {content} | |
| 98 | + | {children} | |
| 102 | 99 | </span> | |
| 103 | 100 | ); | |
| 104 | 101 | } | |
| 105 | 102 | ||
| 103 | + | /** A brand's icon, from Simple Icons, in the tile's colour. */ | |
| 104 | + | function BrandGlyph({ icon, size }: { icon: BrandIcon; size: number }) { | |
| 105 | + | return ( | |
| 106 | + | <svg viewBox="0 0 24 24" width={size * 0.5} height={size * 0.5} fill="currentColor"> | |
| 107 | + | <path d={icon.path} /> | |
| 108 | + | </svg> | |
| 109 | + | ); | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | /** A provider's mark: its brand's icon, an icon for the generic ones, or its initials in its colour. */ | |
| 113 | + | export function ProviderMark({ provider, size = 32 }: { provider: Provider; size?: number }) { | |
| 114 | + | const brand = provider === "webhook" ? null : brandIcon(provider.replace(/_/g, "-")); | |
| 115 | + | if (brand) { | |
| 116 | + | return ( | |
| 117 | + | <MarkTile color={brand.color} size={size}> | |
| 118 | + | <BrandGlyph icon={brand} size={size} /> | |
| 119 | + | </MarkTile> | |
| 120 | + | ); | |
| 121 | + | } | |
| 122 | + | const entry = AI_PROVIDERS[provider]; | |
| 123 | + | const content = | |
| 124 | + | provider === "webhook" ? ( | |
| 125 | + | <Webhook size={size * 0.5} /> | |
| 126 | + | ) : provider.endsWith("_endpoint") ? ( | |
| 127 | + | <Bot size={size * 0.5} /> | |
| 128 | + | ) : ( | |
| 129 | + | entry?.mark || monogram(PROVIDERS[provider].label) | |
| 130 | + | ); | |
| 131 | + | return ( | |
| 132 | + | <MarkTile color={entry?.color ?? PROVIDER_COLORS[provider] ?? "#a1a1aa"} size={size}> | |
| 133 | + | {content} | |
| 134 | + | </MarkTile> | |
| 135 | + | ); | |
| 136 | + | } | |
| 137 | + | ||
| 106 | 138 | /** Providers to choose from, as compact tiles that open their form. */ | |
| 107 | 139 | export function ProviderTiles({ | |
| 108 | 140 | providers, | |
| ⋯ | |||
| 140 | 172 | ); | |
| 141 | 173 | } | |
| 142 | 174 | ||
| 143 | − | /** A connector's tile: its provider's mark, or its letters on a steady colour. No logos. */ | |
| 175 | + | /** A connector's tile: its brand's icon, its provider's mark, or its letters on a steady colour. */ | |
| 144 | 176 | export function ConnectorMark({ view, size = 36 }: { view: Pick<ConnectorView, "id" | "name" | "provider">; size?: number }) { | |
| 177 | + | const brand = brandIcon(view.id); | |
| 178 | + | if (brand) { | |
| 179 | + | return ( | |
| 180 | + | <MarkTile color={brand.color} size={size}> | |
| 181 | + | <BrandGlyph icon={brand} size={size} /> | |
| 182 | + | </MarkTile> | |
| 183 | + | ); | |
| 184 | + | } | |
| 145 | 185 | if (view.provider) return <ProviderMark provider={view.provider} size={size} />; | |
| 146 | − | const color = tileColor(view.id); | |
| 147 | − | const letters = monogram(view.name); | |
| 186 | + | // Outgoing webhooks have no brand: the same webhook mark as incoming ones. | |
| 187 | + | if (view.id === "webhooks") return <ProviderMark provider="webhook" size={size} />; | |
| 148 | 188 | return ( | |
| 149 | − | <span | |
| 150 | − | aria-hidden="true" | |
| 151 | − | className="inline-flex shrink-0 items-center justify-center font-semibold tracking-tight" | |
| 152 | − | style={{ | |
| 153 | − | width: size, | |
| 154 | − | height: size, | |
| 155 | − | borderRadius: size * 0.28, | |
| 156 | − | fontSize: size * (letters.length > 1 ? 0.34 : 0.42), | |
| 157 | − | background: `color-mix(in srgb, ${color} 16%, transparent)`, | |
| 158 | − | // Toward the text colour, so the letters read in both themes. | |
| 159 | − | color: `color-mix(in srgb, ${color} 60%, var(--color-fg))`, | |
| 160 | − | boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${color} 30%, transparent)`, | |
| 161 | − | }} | |
| 162 | − | > | |
| 163 | − | {letters} | |
| 164 | − | </span> | |
| 189 | + | <MarkTile color={tileColor(view.id)} size={size}> | |
| 190 | + | {monogram(view.name)} | |
| 191 | + | </MarkTile> | |
| 165 | 192 | ); | |
| 166 | 193 | } | |
| 167 | 194 | ||
| ⋯ | |||
| 258 | 285 | </nav> | |
| 259 | 286 | ||
| 260 | 287 | <div className="mt-5 min-w-0 md:mt-0"> | |
| 261 | − | <div className="relative"> | |
| 262 | − | <Search size={15} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" /> | |
| 288 | + | <InputGroup> | |
| 289 | + | <InputAddon className="pr-0"> | |
| 290 | + | <Search size={15} /> | |
| 291 | + | </InputAddon> | |
| 263 | 292 | <Input | |
| 264 | 293 | type="search" | |
| 265 | 294 | value={query} | |
| 266 | 295 | onChange={(event) => setQuery(event.target.value)} | |
| 267 | 296 | placeholder={category === "all" ? "Search integrations" : `Search all integrations, not just ${categoryTitle(category)}`} | |
| 268 | 297 | aria-label="Search integrations" | |
| 269 | − | className="pl-9" | |
| 270 | 298 | /> | |
| 271 | − | </div> | |
| 299 | + | </InputGroup> | |
| 272 | 300 | ||
| 273 | 301 | {empty && ( | |
| 274 | 302 | <Card asChild tone="plain" className="mt-8 border-dashed px-4 py-8 text-center text-sm text-muted"> | |
| ⋯ | |||
| 423 | 451 | <div className="mt-auto flex items-end justify-between gap-3 pt-3"> | |
| 424 | 452 | <p className="text-[0.6875rem] text-faint">{also}</p> | |
| 425 | 453 | {soon ? ( | |
| 426 | − | <a | |
| 427 | − | href={askHref({ connector: view, scope, workspace, address: CONTACT.support })} | |
| 428 | − | className="relative z-10 shrink-0 rounded-md border border-line px-2.5 py-1 text-xs font-medium text-fg/80 hover:border-line-strong hover:bg-raised hover:text-fg" | |
| 429 | − | > | |
| 430 | − | Ask for this | |
| 431 | − | </a> | |
| 454 | + | <Button asChild variant="outline" size="xs" className="relative z-10"> | |
| 455 | + | <a href={askHref({ connector: view, scope, workspace, address: CONTACT.support })}>Ask for this</a> | |
| 456 | + | </Button> | |
| 432 | 457 | ) : to ? ( | |
| 433 | 458 | <span className="flex shrink-0 items-center gap-0.5 text-xs font-medium text-accent"> | |
| 434 | 459 | {state ? (canManage ? "Manage" : "View") : canManage ? "Set up" : "View"} | |
| 1 | + | /** | |
| 2 | + | * Connectors' brand marks, from Simple Icons, by connector id | |
| 3 | + | * (@g1t/contracts/connectors). A connector without one here keeps its | |
| 4 | + | * letters. Named imports, so only these icons are bundled. | |
| 5 | + | */ | |
| 6 | + | import { | |
| 7 | + | type SimpleIcon, | |
| 8 | + | si1password, | |
| 9 | + | siAnthropic, | |
| 10 | + | siAsana, | |
| 11 | + | siAtlassian, | |
| 12 | + | siBetterstack, | |
| 13 | + | siBitbucket, | |
| 14 | + | siBox, | |
| 15 | + | siBuildkite, | |
| 16 | + | siCircleci, | |
| 17 | + | siClickup, | |
| 18 | + | siCloudflare, | |
| 19 | + | siCoda, | |
| 20 | + | siDatadog, | |
| 21 | + | siDeepseek, | |
| 22 | + | siDiscord, | |
| 23 | + | siDocker, | |
| 24 | + | siDropbox, | |
| 25 | + | siFigma, | |
| 26 | + | siFlydotio, | |
| 27 | + | siGithub, | |
| 28 | + | siGitlab, | |
| 29 | + | siGmail, | |
| 30 | + | siGooglebigquery, | |
| 31 | + | siGooglecalendar, | |
| 32 | + | siGooglecloud, | |
| 33 | + | siGoogledocs, | |
| 34 | + | siGoogledrive, | |
| 35 | + | siGooglegemini, | |
| 36 | + | siGooglemeet, | |
| 37 | + | siGrafana, | |
| 38 | + | siHubspot, | |
| 39 | + | siHuggingface, | |
| 40 | + | siIntercom, | |
| 41 | + | siJira, | |
| 42 | + | siKubernetes, | |
| 43 | + | siLinear, | |
| 44 | + | siMiro, | |
| 45 | + | siMistralai, | |
| 46 | + | siModelcontextprotocol, | |
| 47 | + | siNeon, | |
| 48 | + | siNetlify, | |
| 49 | + | siNewrelic, | |
| 50 | + | siNotion, | |
| 51 | + | siOkta, | |
| 52 | + | siOpenrouter, | |
| 53 | + | siOpsgenie, | |
| 54 | + | siPagerduty, | |
| 55 | + | siPaypal, | |
| 56 | + | siPostgresql, | |
| 57 | + | siPosthog, | |
| 58 | + | siSentry, | |
| 59 | + | siShortcut, | |
| 60 | + | siSnowflake, | |
| 61 | + | siSnyk, | |
| 62 | + | siStorybook, | |
| 63 | + | siStripe, | |
| 64 | + | siSupabase, | |
| 65 | + | siTrello, | |
| 66 | + | siVercel, | |
| 67 | + | siZapier, | |
| 68 | + | siZendesk, | |
| 69 | + | siZoom, | |
| 70 | + | } from "simple-icons"; | |
| 71 | + | ||
| 72 | + | const BRAND_ICONS: Record<string, SimpleIcon> = { | |
| 73 | + | github: siGithub, | |
| 74 | + | "github-mcp": siGithub, | |
| 75 | + | gitlab: siGitlab, | |
| 76 | + | bitbucket: siBitbucket, | |
| 77 | + | circleci: siCircleci, | |
| 78 | + | buildkite: siBuildkite, | |
| 79 | + | "docker-hub": siDocker, | |
| 80 | + | linear: siLinear, | |
| 81 | + | "linear-mcp": siLinear, | |
| 82 | + | jira: siJira, | |
| 83 | + | asana: siAsana, | |
| 84 | + | shortcut: siShortcut, | |
| 85 | + | clickup: siClickup, | |
| 86 | + | trello: siTrello, | |
| 87 | + | discord: siDiscord, | |
| 88 | + | zoom: siZoom, | |
| 89 | + | "google-meet": siGooglemeet, | |
| 90 | + | "google-calendar": siGooglecalendar, | |
| 91 | + | gmail: siGmail, | |
| 92 | + | notion: siNotion, | |
| 93 | + | atlassian: siAtlassian, | |
| 94 | + | "google-docs": siGoogledocs, | |
| 95 | + | zapier: siZapier, | |
| 96 | + | coda: siCoda, | |
| 97 | + | sentry: siSentry, | |
| 98 | + | "sentry-mcp": siSentry, | |
| 99 | + | datadog: siDatadog, | |
| 100 | + | pagerduty: siPagerduty, | |
| 101 | + | opsgenie: siOpsgenie, | |
| 102 | + | grafana: siGrafana, | |
| 103 | + | "new-relic": siNewrelic, | |
| 104 | + | "better-stack": siBetterstack, | |
| 105 | + | cloudflare: siCloudflare, | |
| 106 | + | "cloudflare-docs": siCloudflare, | |
| 107 | + | "google-cloud": siGooglecloud, | |
| 108 | + | vercel: siVercel, | |
| 109 | + | netlify: siNetlify, | |
| 110 | + | "fly-io": siFlydotio, | |
| 111 | + | kubernetes: siKubernetes, | |
| 112 | + | anthropic: siAnthropic, | |
| 113 | + | gemini: siGooglegemini, | |
| 114 | + | mistral: siMistralai, | |
| 115 | + | deepseek: siDeepseek, | |
| 116 | + | openrouter: siOpenrouter, | |
| 117 | + | mcp: siModelcontextprotocol, | |
| 118 | + | "hugging-face": siHuggingface, | |
| 119 | + | postgresql: siPostgresql, | |
| 120 | + | snowflake: siSnowflake, | |
| 121 | + | bigquery: siGooglebigquery, | |
| 122 | + | posthog: siPosthog, | |
| 123 | + | supabase: siSupabase, | |
| 124 | + | neon: siNeon, | |
| 125 | + | zendesk: siZendesk, | |
| 126 | + | intercom: siIntercom, | |
| 127 | + | hubspot: siHubspot, | |
| 128 | + | stripe: siStripe, | |
| 129 | + | paypal: siPaypal, | |
| 130 | + | figma: siFigma, | |
| 131 | + | miro: siMiro, | |
| 132 | + | storybook: siStorybook, | |
| 133 | + | okta: siOkta, | |
| 134 | + | "1password": si1password, | |
| 135 | + | snyk: siSnyk, | |
| 136 | + | "google-drive": siGoogledrive, | |
| 137 | + | dropbox: siDropbox, | |
| 138 | + | box: siBox, | |
| 139 | + | }; | |
| 140 | + | ||
| 141 | + | /** A connector's brand mark: its SVG path, and the colour to draw it in. */ | |
| 142 | + | export type BrandIcon = { path: string; title: string; color: string | null }; | |
| 143 | + | ||
| 144 | + | /** Relative luminance of a `RRGGBB` colour, 0 (black) to 1 (white). */ | |
| 145 | + | function luminance(hex: string): number { | |
| 146 | + | const [r, g, b] = [0, 2, 4].map((at) => { | |
| 147 | + | const channel = parseInt(hex.slice(at, at + 2), 16) / 255; | |
| 148 | + | return channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; | |
| 149 | + | }); | |
| 150 | + | return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!; | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | /** | |
| 154 | + | * The brand mark for connector `id`, or null. `color` is the brand's own, | |
| 155 | + | * or null when it is black or nearly (GitHub, Notion, Vercel): those are | |
| 156 | + | * drawn in the text colour, so they read on the dark theme as on the light. | |
| 157 | + | */ | |
| 158 | + | export function brandIcon(id: string): BrandIcon | null { | |
| 159 | + | const icon = BRAND_ICONS[id]; | |
| 160 | + | if (!icon) return null; | |
| 161 | + | return { path: icon.path, title: icon.title, color: luminance(icon.hex) < 0.04 ? null : `#${icon.hex}` }; | |
| 162 | + | } |
| 44 | 44 | ||
| 45 | 45 | test("search finds by name, description, tag and keyword, across every category", () => { | |
| 46 | 46 | const found = arrange(workspace, { category: "design", query: "sentry", connected: none }); | |
| 47 | − | assert.deepEqual(found.available.map((view) => view.id), ["sentry", "sentry-mcp"], "a search is not held to the category"); | |
| 47 | + | assert.deepEqual(found.available.map((view) => view.id), ["sentry"], "a search is not held to the category"); | |
| 48 | + | assert.deepEqual(found.soon.map((view) => view.id), ["sentry-mcp"], "what is coming sorts after what works"); | |
| 48 | 49 | assert.ok(matchesQuery(personal.find((view) => view.id === "google-calendar")!, "in a meeting")); | |
| 49 | 50 | assert.ok(matchesQuery(workspace.find((view) => view.id === "anthropic")!, "claude")); | |
| 50 | 51 | assert.ok(matchesQuery(workspace.find((view) => view.id === "incident-io")!, "incidentio")); | |
| ⋯ | |||
| 74 | 75 | assert.equal(monogram("Sentry"), "S"); | |
| 75 | 76 | assert.equal(monogram("incident.io"), "II"); | |
| 76 | 77 | assert.equal(monogram("1Password"), "1P"); | |
| 78 | + | assert.equal(monogram("GitHub (MCP)"), "GM", "punctuation is not a letter"); | |
| 79 | + | assert.equal(monogram("Linear (MCP)"), "LM"); | |
| 80 | + | assert.equal(monogram("Atlassian (Jira and Confluence)"), "AJ"); | |
| 77 | 81 | assert.equal(tileColor("figma"), tileColor("figma")); | |
| 78 | 82 | }); | |
| 79 | 83 | ||
| 101 | 101 | })).filter((group) => group.views.length > 0); | |
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | − | /** The letters on a connector's tile: two words' initials, or a name's capitals ("PagerDuty" → "PD"). */ | |
| 104 | + | /** | |
| 105 | + | * The letters on a connector's tile: two words' initials, or a name's | |
| 106 | + | * capitals ("PagerDuty" → "PD"). Only letters and digits count, so | |
| 107 | + | * "GitHub (MCP)" is "GM", never "G(". | |
| 108 | + | */ | |
| 105 | 109 | export function monogram(name: string): string { | |
| 106 | − | const words = name.split(/[\s.\-/]+/).filter((word) => /[A-Za-z0-9]/.test(word)); | |
| 110 | + | const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean); | |
| 107 | 111 | if (words.length >= 2) return (words[0]![0]! + words[1]![0]!).toUpperCase(); | |
| 108 | 112 | const word = words[0] ?? name; | |
| 109 | 113 | const capitals = word.slice(1).match(/[A-Z0-9]/); |
| 37 | 37 | } | |
| 38 | 38 | }); | |
| 39 | 39 | ||
| 40 | + | test("only what g1t sets up end to end is available; the docs' Available today lists the same", () => { | |
| 41 | + | const available = (scope: "workspace" | "personal") => | |
| 42 | + | connectorsFor(scope) | |
| 43 | + | .filter((view) => view.status === "available") | |
| 44 | + | .map((view) => view.id) | |
| 45 | + | .sort(); | |
| 46 | + | const models = Object.entries(PROVIDERS) | |
| 47 | + | .filter(([, provider]) => provider.kind === "models") | |
| 48 | + | .map(([id]) => id.replace(/_/g, "-")); | |
| 49 | + | assert.deepEqual(available("workspace"), ["alerts-webhook", "datadog", "github", "jira", "linear", "sentry", "webhooks", ...models].sort()); | |
| 50 | + | assert.deepEqual(available("personal"), ["github", "mcp"]); | |
| 51 | + | for (const connector of CONNECTORS) { | |
| 52 | + | if (connector.mcp) assert.equal(connector.status, "soon", `${connector.id}: nothing connects to MCP servers yet`); | |
| 53 | + | } | |
| 54 | + | }); | |
| 55 | + | ||
| 40 | 56 | test("a connector shows its personal side to you", () => { | |
| 41 | 57 | const github = CONNECTORS.find((c) => c.id === "github")!; | |
| 42 | 58 | const mine = connectorView(github, "personal")!; |
| 73 | 73 | /** More words search should find it by. */ | |
| 74 | 74 | keywords?: string[]; | |
| 75 | 75 | /** | |
| 76 | − | * The remote MCP server behind it, when the tool publishes one: g1t | |
| 77 | − | * connects to it (docs.g1t.sh/guides/customize/, "Connectors"), signs in | |
| 78 | − | * the way it asks (`oauth`: the browser; `header`: a key an owner | |
| 79 | − | * pastes; `none`), lists its tools, and gives them to agents. | |
| 76 | + | * The remote MCP server behind it, when the tool publishes one, and how | |
| 77 | + | * it signs in (`oauth`: the browser; `header`: a key an owner pastes; | |
| 78 | + | * `none`). Its connector is soon: see `mcp()` below. | |
| 80 | 79 | */ | |
| 81 | 80 | mcp?: { | |
| 82 | 81 | url: string; | |
| ⋯ | |||
| 141 | 140 | keywords: ["model", "llm", ...keywords], | |
| 142 | 141 | }; | |
| 143 | 142 | } | |
| 144 | − | ||
| 145 | − | /** Where an MCP-backed connector is connected: Customize's Connectors page, with the directory entry chosen. */ | |
| 146 | − | export const MCP_CONNECT_PATH = "/:workspace/-/customize/connectors/new?directory="; | |
| 147 | 143 | ||
| 148 | 144 | /** | |
| 149 | − | * A tool with a remote MCP server: available now, connected from Customize | |
| 150 | − | * (an owner signs in or pastes a key), its tools then given to agents. | |
| 145 | + | * A tool with a remote MCP server. Soon: Customize, where an owner | |
| 146 | + | * connected one, was retired with g1t's hosted agents, so nothing in g1t | |
| 147 | + | * connects to these servers today. The server is kept for when something | |
| 148 | + | * does; until then each person's own harness connects to it directly. | |
| 151 | 149 | */ | |
| 152 | 150 | function mcp( | |
| 153 | 151 | id: string, | |
| ⋯ | |||
| 158 | 156 | capabilities: ConnectorCapability[] = ["Agents can read", "Agents can act"], | |
| 159 | 157 | keywords: string[] = [], | |
| 160 | 158 | ): Connector { | |
| 161 | − | return { id, name, category, description, scopes: W, status: "available", href: { workspace: `${MCP_CONNECT_PATH}${id}` }, capabilities, keywords: ["mcp", ...keywords], mcp: server }; | |
| 159 | + | return { id, name, category, description, scopes: W, status: "soon", capabilities, keywords: ["mcp", ...keywords], mcp: server }; | |
| 162 | 160 | } | |
| 163 | 161 | ||
| 164 | 162 | /** A planned connector. */ | |