MCP connect
ProConsent to connect an MCP server: where it runs and who runs it, what it says it can do (its tools, by name and description), the permissions it asks for, and how to disconnect later. Nothing is ticked for the person; a permission the server doesn't describe says so instead of being hidden; a list the server hasn't given is said to be unknown, not shown as empty. Connect and Cancel look the same, so neither is the easy way out.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/mcp-connect
Brings in: button, locale, locked, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { McpConnect, type McpScope, type McpServerTool } from "@/components/mcp-connect"
const tools: McpServerTool[] = [
{ name: "search_issues", title: "Search issues", description: "Finds issues by text, label or assignee.", readOnly: true },
{ name: "create_issue", title: "Create issue", description: "Opens a new issue in a team you choose." },
{ name: "update_issue", description: "Changes an issue's status, assignee or labels.", destructive: true },
{ name: "sync_cycles" },
]
const scopes: McpScope[] = [
{ id: "issues:read", label: "Read issues", description: "Titles, descriptions, comments and labels in every team you can see.", required: true },
{ id: "issues:write", label: "Create and change issues", description: "Opens issues and edits their fields. It can't delete them." },
// The server didn't say what this one allows: the card says so rather than hiding it.
{ id: "admin.workspace" },
]
export default function Example() {
return (
<McpConnect
className="w-full max-w-xl"
server={{ name: "Tracker", url: "https://mcp.tracker.example/v1", owner: "Tracker Inc. (verified by your workspace admin)" }}
tools={tools}
scopes={scopes}
disconnect="Settings › Connections › Tracker › Disconnect. Its access stops at once; issues it already created stay."
onConnect={() => {}}
onCancel={() => {}}
/>
)
}<McpConnect>
| Prop | Type | Default | Description |
|---|---|---|---|
| serverrequired | McpServerInfo | — | The server: its name, where it runs and who runs it, as your app knows them. |
| tools | McpServerTool[] | undefined | — | The tools it lists. Leave out when it hasn't listed them; `[]` when it lists none. |
| scopes | McpScope[] | undefined | — | The permissions it asks for. Leave out when it hasn't said; `[]` when it asks for none. |
| disconnectrequired | string | — | How to disconnect later, e.g. "Settings › Connections › Linear › Disconnect. Its access stops at once." |
| acknowledgement | string | undefined | — | A sentence the person ticks themselves before connecting. Optional. |
| title | string | undefined | — | The heading. Defaults to "Connect {name}?". |
| onConnectrequired | (scopes: string[]) => void | — | Called with the ids of the permissions the person ticked, in the order given. |
| onCancelrequired | () => void | — | Called when the person cancels. |
| busy | boolean | undefined | false | Disables Connect and says "Connecting" while the connection is made. |
| error | string | undefined | — | Why the last attempt failed, said above the buttons. |
| toolLimit | number | undefined | 6 | How many tools to show before "Show all". |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under mcpConnect in <LocaleProvider strings={{ … }}>; the table is exported as mcpConnectStrings.
| Key | English |
|---|---|
| title | Connect {name}? |
| where | Where your data goes |
| remote | A server at |
| address | Full address |
| local | Runs on this computer |
| whereUnknown | Where this server runs isn’t known. |
| owner | Run by |
| ownerUnknown | Who runs it isn’t known. |
| tools | What it says it can do |
| toolCount | { one: "{count} tool", other: "{count} tools" } |
| toolsUnknown | The server hasn’t listed what it can do. |
| noTools | It lists no tools. |
| notDescribed | Not described by the server |
| saysReadOnly | Says it only reads |
| saysDestructive | Says it can change or delete |
| hintsNote | What a tool says about itself is the server’s own description; nothing has checked it. |
| showAll | Show all {count} |
| showFewer | Show fewer |
| scopes | What it asks for |
| scopesHint | Nothing is granted unless you tick it. |
| scopesUnknown | The server hasn’t said what access it asks for. |
| noScopes | It asks for no extra access. |
| needed | Needed to connect |
| disconnect | To disconnect later: |
| connect | Connect |
| connecting | Connecting |
| cancel | Cancel |
| tickNeeded | { one: "Tick the permission it needs to connect first.", other: "Tick the {count} permissions it needs to connect first." } |
| tickAcknowledgement | Tick the acknowledgement first. |
Types
- McpServerTool
- One tool the server lists, as it describes itself (MCP's `tools/list`).
{ /** The tool's name, as the agent calls it. */ name: string /** A readable name, when the server gives one. */ title?: string | undefined /** What the server says the tool does. Without one, the card says "Not described by the server". */ description?: string | undefined /** The server's own hint that the tool only reads (MCP's `readOnlyHint`). Shown as the server's claim. */ readOnly?: boolean- McpScope
- One permission (OAuth scope or the like) the server asks for.
{ /** The scope as the server names it ("repo:read"); what `onConnect` sends back. */ id: string /** A plain name for it, when your app has one. Without one, the id is shown. */ label?: string | undefined /** What it allows, in a sentence. Without one, the card says "Not described by the server". */ description?: string | undefined /** The server can't connect without it: Connect stays locked unti- McpServerInfo
- The server, as your app knows it (not as the server describes itself).
{ /** The name to show, e.g. "Linear". */ name: string /** Where a remote server is: its full URL. Its host is shown first. */ url?: string | undefined /** How a local server is started, e.g. `npx @example/mcp-server`. */ command?: string | undefined /** Who runs it, as your app knows it. Left out, the card says it isn't known. */ owner?: string | undefined }