mcp-connection-form.tsx6.6 KBView on GitHub 'use client';
import {
Field,
FieldRows,
FormActions,
FormError,
TextField,
TextareaField,
} from '@/components/ui/field';
import { AlertTriangle, Loader2, Search } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { canSubmitConnectionForm, type McpProbeStatus, type McpToolRow } from './mcp-tool-policy';
import { McpToolChecklist } from './mcp-tool-checklist';
export interface McpConnectionFormValues {
name: string;
serverUrl: string;
authorizationHeader: string;
instructions: string;
}
interface McpConnectionFormProps {
mode: 'add' | 'edit';
values: McpConnectionFormValues;
onChange: (patch: Partial<McpConnectionFormValues>) => void;
/** Tool discovery is part of connecting, not of editing an existing connection. */
probeStatus: McpProbeStatus;
probeError: string | null;
tools: McpToolRow[];
onProbe: () => void;
onToggleTool: (toolName: string, allowed: boolean) => void;
onToolInstructionChange: (toolName: string, instruction: string) => void;
onSubmit: () => void;
onCancel: () => void;
isSaving: boolean;
/**
* The "share with my organization" control, supplied by the caller.
*
* A slot rather than a prop pair, because the switch writes through the caller's
* mutation and this file is presentational. It sits HERE, in the add-a-server form,
* and not in the picker that lists servers: it describes the connection you are
* creating, so a picker-level toggle was a setting floating next to a list it did
* not apply to.
*/
orgShare?: React.ReactNode;
}
/**
* The add / edit form, presentational only.
*
* Tool permissions are captured here, at setup, rather than on a settings page the
* user never revisits — so the checklist sits between the credential fields and the
* Connect button, and the connection saves deny-by-default.
*/
export function McpConnectionForm({
mode,
values,
onChange,
probeStatus,
probeError,
tools,
onProbe,
onToggleTool,
onToolInstructionChange,
onSubmit,
onCancel,
isSaving,
orgShare,
}: McpConnectionFormProps) {
const isEditing = mode === 'edit';
const canSubmit = canSubmitConnectionForm({
name: values.name,
serverUrl: values.serverUrl,
probeStatus,
isSaving,
});
return (
// `bare`, and NO heading of its own. This form is always rendered INSIDE
// something that already frames and titles it — the connections dialog, an
// inspector panel. A bordered card inside a dialog is two frames around one
// thing, and an "Add MCP Connection" heading under the dialog's own header is a
// title inside a title (CLAUDE.md → UI affordances).
<FieldRows bare>
<TextField
label="Name"
placeholder="e.g. Outreach Tracker"
value={values.name}
onChange={(e) => onChange({ name: e.target.value })}
/>
<TextField
label="Server URL"
placeholder="https://mcp.notion.com/mcp"
value={values.serverUrl}
onChange={(e) => onChange({ serverUrl: e.target.value })}
/>
<TextField
label="API key / token"
optional
type="password"
placeholder="your_access_token_here"
value={values.authorizationHeader}
onChange={(e) => onChange({ authorizationHeader: e.target.value })}
hint={
isEditing
? 'Blank keeps the existing token.'
: 'Stored encrypted, sent as a Bearer header.'
}
/>
<TextareaField
label="Description"
optional
placeholder="e.g. Tracks outreach contacts. Use this to check status or mark someone as done after outreach."
value={values.instructions}
onChange={(e) => onChange({ instructions: e.target.value })}
hint="Tell the agent when and how to use this connection."
/>
{/* Sharing belongs to the ACT of adding a server — it is a property of the
connection being created, not of the picker you got here from. */}
{orgShare}
{!isEditing && (
<>
{/* The button belongs on the label's row: "Tool permissions — [Check available tools]"
is the whole instruction. The paragraph that used to sit here told the reader to do
the thing the button next to it does. */}
<Field label="Tool permissions">
<Button
type="button"
variant="outline"
size="sm"
className="h-8 cursor-pointer gap-1.5 text-xs"
onClick={onProbe}
disabled={probeStatus === 'probing' || !values.serverUrl.trim()}
>
{probeStatus === 'probing' ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Search className="h-3.5 w-3.5" />
)}
{probeStatus === 'idle' ? 'Check available tools' : 'Check again'}
</Button>
</Field>
{probeStatus === 'failed' && (
<div className="px-4 pb-4">
<FormError tone="warning" className="space-y-1">
<p className="flex items-center gap-1.5 font-medium">
<AlertTriangle className="h-3.5 w-3.5 flex-shrink-0" />
Could not reach this server
</p>
{probeError && <p className="break-words opacity-80">{probeError}</p>}
<p className="opacity-80">
You can still save this connection — no tool is enabled until you enable it, so
nothing is granted by saving. Retry the check from Permissions once the server is
up.
</p>
</FormError>
</div>
)}
{probeStatus === 'ready' && (
<div className="px-4 pb-4">
<McpToolChecklist
tools={tools}
onToggle={onToggleTool}
onInstructionChange={onToolInstructionChange}
idPrefix="mcp-add-tool"
emptyMessage="This server reported no tools."
/>
</div>
)}
</>
)}
<FormActions className="p-4 pt-2">
<Button onClick={onSubmit} disabled={!canSubmit} size="sm" className="cursor-pointer">
{isSaving
? isEditing
? 'Saving…'
: 'Connecting…'
: isEditing
? 'Save changes'
: 'Connect'}
</Button>
<Button
variant="ghost"
size="sm"
className="cursor-pointer"
onClick={onCancel}
disabled={isSaving}
>
Cancel
</Button>
</FormActions>
</FieldRows>
);
}