The Integrations directory shows each tool's own mark, and only what g1t sets up end to end can be set up: cards draw the brand's icon from Simple Icons where it has one (GitHub, Linear, Jira, Sentry, Datadog, Anthropic, Gemini and some sixty more), in the brand's colour washed toward the text colour, and in the text colour itself when the brand's is black or nearly (GitHub, Notion, Vercel), so each reads in the dark theme and the light one; outgoing webhooks take the webhook mark, and the rest keep their letters, which now count letters and digits alone, so "GitHub (MCP)" is GM and no longer "G(". The search's magnifying glass no longer sits on the placeholder: the field is the input group used elsewhere, with the icon as its addon (the older Input it used dropped the padding it was given). Every connector backed only by an MCP server (GitHub (MCP), Linear (MCP), Notion, Atlassian, Sentry (MCP), Cloudflare, Vercel, Stripe, Figma and the rest) is Soon, since Customize, where they were connected, went with the hosted agents and their Set up led nowhere; they sort after what works, with Ask for this in place of Set up, and the rail cannot pin them. Available stays GitHub, webhooks, Linear, Jira, Sentry, Datadog, the alerts webhook and the AI providers for a workspace, and GitHub sign-in and MCP clients for you, as the docs' Available today already said; a test holds the catalog to that list.
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. */ | |