UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

391 lines 20.4 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Box, Text, useInput } from 'ink'; import SelectInput from 'ink-select-input'; import { Tab, Tabs } from 'ink-tab'; import { useEffect, useState } from 'react'; import TextInput from '../../components/text-input.js'; import { colors } from '../../config/index.js'; import { useResponsiveTerminal } from '../../hooks/useTerminalWidth.js'; import { MCP_TEMPLATES, } from '../templates/mcp-templates.js'; export function McpStep({ onComplete, onBack, onDelete, existingServers = {}, configExists = false, }) { const { isNarrow } = useResponsiveTerminal(); const [servers, setServers] = useState(existingServers); // Update servers when existingServers prop changes useEffect(() => { setServers(existingServers); }, [existingServers]); const [mode, setMode] = useState('initial-menu'); const [selectedTemplate, setSelectedTemplate] = useState(null); const [currentFieldIndex, setCurrentFieldIndex] = useState(0); const [fieldAnswers, setFieldAnswers] = useState({}); const [currentValue, setCurrentValue] = useState(''); const [multilineBuffer, setMultilineBuffer] = useState(''); const [error, setError] = useState(null); const [inputKey, setInputKey] = useState(0); const [editingServerName, setEditingServerName] = useState(null); const [activeTab, setActiveTab] = useState('local'); const serverCount = Object.keys(servers).length; // Filter templates by category const localTemplates = MCP_TEMPLATES.filter(template => template.category === 'local'); const remoteTemplates = MCP_TEMPLATES.filter(template => template.category === 'remote'); // Initial menu options const initialOptions = [ { label: 'Add MCP servers', value: 'add' }, ...(serverCount > 0 ? [{ label: 'Edit existing servers', value: 'edit' }] : []), { label: 'Done & Save', value: 'done' }, ...(configExists && onDelete ? [{ label: 'Delete config file', value: 'delete' }] : []), ]; // Create template options for current tab const getTemplateOptions = () => { if (mode === 'tabs') { const options = []; const templates = activeTab === 'local' ? localTemplates : remoteTemplates; // Add templates for current tab templates.forEach(template => { options.push({ label: isNarrow ? `${template.name}` : `${template.name} - ${template.description}`, value: template.id, category: activeTab, }); }); // Add done option at the end options.push({ label: 'Done & Save', value: 'done', }); return options; } return []; }; const handleInitialSelect = (item) => { if (item.value === 'add') { setMode('tabs'); } else if (item.value === 'edit') { setMode('edit-selection'); } else if (item.value === 'done') { onComplete(servers); } else if (item.value === 'delete' && onDelete) { onDelete(); } }; const handleTemplateSelect = (item) => { if (item.value === 'done') { // Done adding servers onComplete(servers); return; } // Adding new server const template = MCP_TEMPLATES.find(t => t.id === item.value); if (template) { // Check if template has no fields if (template.fields.length === 0) { // Automatically build config and add server when no fields are required try { const serverConfig = template.buildConfig({}); setServers({ ...servers, [serverConfig.name]: serverConfig }); // Stay in tabs mode to allow adding more servers setMode('tabs'); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to build configuration'); } } else { // Template has fields, proceed with normal flow setEditingServerName(null); // Not editing setSelectedTemplate(template); setCurrentFieldIndex(0); setFieldAnswers({}); setCurrentValue(template.fields[0]?.default || ''); setMultilineBuffer(''); setError(null); setMode('field-input'); } } }; const handleEditSelect = (item) => { // Store the server name and show edit/delete options if (item.value.startsWith('edit-')) { const serverKey = item.value.replace('edit-', ''); setEditingServerName(serverKey); setMode('edit-or-delete'); } }; const handleEditOrDeleteChoice = (item) => { if (item.value === 'delete' && editingServerName !== null) { // Delete the server const newServers = { ...servers }; delete newServers[editingServerName]; setServers(newServers); setEditingServerName(null); // Go back to initial menu after deleting setMode('initial-menu'); return; } if (item.value === 'edit' && editingServerName !== null) { const server = servers[editingServerName]; if (server) { // Find matching template by server name or use custom const template = MCP_TEMPLATES.find(t => t.id === server.name) || MCP_TEMPLATES.find(t => t.id === editingServerName) || MCP_TEMPLATES.find(t => t.id === 'custom'); if (template) { setSelectedTemplate(template); setCurrentFieldIndex(0); // Pre-populate field answers from existing server const answers = {}; // Map server properties to field names based on template fields for (const field of template.fields) { if (field.name === 'serverName' && server.name) { answers.serverName = server.name; } else if (field.name === 'url' && server.url) { answers.url = server.url; } else if (field.name === 'command' && server.command) { answers.command = server.command; } else if (field.name === 'allowedDirs' && server.args) { // Special handling for filesystem server - extract allowed directories const packageIndex = server.args.findIndex(arg => arg.includes('@modelcontextprotocol/server-filesystem')); if (packageIndex !== -1) { const dirs = server.args.slice(packageIndex + 1); answers.allowedDirs = dirs.join(', '); } } else if (field.name === 'args' && server.args) { answers.args = server.args.join(' '); } else if (field.name === 'envVars' && server.env) { answers.envVars = Object.entries(server.env) .map(([key, value]) => `${key}=${value}`) .join('\n'); } else if (field.name === 'apiKey' && server.env) { // Try to find API key from env vars const apiKeyEntry = Object.entries(server.env).find(([key]) => key.includes('API_KEY') || key.includes('TOKEN')); if (apiKeyEntry) { answers.apiKey = apiKeyEntry[1]; } } } setFieldAnswers(answers); setCurrentValue(answers[template.fields[0]?.name] || template.fields[0]?.default || ''); setMultilineBuffer(''); setError(null); setMode('field-input'); } } } }; const handleFieldSubmit = () => { if (!selectedTemplate) return; const currentField = selectedTemplate.fields[currentFieldIndex]; if (!currentField) return; // For multiline fields, handle differently const isMultiline = currentField.name === 'envVars'; const finalValue = isMultiline ? multilineBuffer : currentValue.trim(); // Validate required fields if (currentField.required && !finalValue) { setError('This field is required'); return; } // Validate with custom validator if (currentField.validator && finalValue) { const validationError = currentField.validator(finalValue); if (validationError) { setError(validationError); return; } } // Save answer const newAnswers = { ...fieldAnswers, [currentField.name]: finalValue, }; setFieldAnswers(newAnswers); setError(null); // Move to next field or complete if (currentFieldIndex < selectedTemplate.fields.length - 1) { setCurrentFieldIndex(currentFieldIndex + 1); const nextField = selectedTemplate.fields[currentFieldIndex + 1]; setCurrentValue(newAnswers[nextField?.name] || nextField?.default || ''); setMultilineBuffer(''); } else { // Build config and add/update server try { const serverConfig = selectedTemplate.buildConfig(newAnswers); if (editingServerName !== null) { // Replace existing server (delete old, add new) const newServers = { ...servers }; delete newServers[editingServerName]; newServers[serverConfig.name] = serverConfig; setServers(newServers); } else { // Add new server setServers({ ...servers, [serverConfig.name]: serverConfig }); } // Reset for next server setSelectedTemplate(null); setCurrentFieldIndex(0); setFieldAnswers({}); setCurrentValue(''); setMultilineBuffer(''); setEditingServerName(null); setMode('tabs'); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to build configuration'); } } }; const editOptions = [ ...Object.entries(servers).map(([key, server], index) => ({ label: `${index + 1}. ${server.name}`, value: `edit-${key}`, })), ]; // Handle keyboard navigation useInput((input, key) => { // Handle Shift+Tab for going back (but not regular Tab, let Tabs component handle it) if (key.shift && key.tab) { if (mode === 'field-input') { // In field input mode, check if we can go back to previous field if (currentFieldIndex > 0) { // Go back to previous field setCurrentFieldIndex(currentFieldIndex - 1); const prevField = selectedTemplate?.fields[currentFieldIndex - 1]; setCurrentValue(fieldAnswers[prevField?.name || ''] || prevField?.default || ''); setMultilineBuffer(''); setInputKey(prev => prev + 1); // Force remount to reset cursor position setError(null); } else { // At first field, go back based on context if (editingServerName !== null) { // Was editing, go back to edit-or-delete choice setMode('edit-or-delete'); } else { // Was adding, go back to tabs setMode('tabs'); } setSelectedTemplate(null); setCurrentFieldIndex(0); setFieldAnswers({}); setCurrentValue(''); setMultilineBuffer(''); setError(null); } } else if (mode === 'edit-or-delete') { // In edit-or-delete, go back to edit selection setEditingServerName(null); setMode('edit-selection'); } else if (mode === 'edit-selection') { // In edit selection, go back to initial menu setMode('initial-menu'); } else if (mode === 'tabs') { // At tabs screen, go back to initial menu setMode('initial-menu'); } else if (mode === 'initial-menu' && onBack) { // At initial menu, call parent's onBack onBack(); } return; } if (mode === 'field-input' && selectedTemplate) { const currentField = selectedTemplate.fields[currentFieldIndex]; const isMultiline = currentField?.name === 'envVars'; if (isMultiline) { // Handle multiline input if (key.return) { // Add newline to buffer setMultilineBuffer(multilineBuffer + '\n'); } else if (key.escape) { // Submit multiline input on Escape handleFieldSubmit(); } else if (!key.ctrl && !key.meta && input) { setMultilineBuffer(multilineBuffer + input); } } else { if (key.return) { handleFieldSubmit(); } else if (key.escape) { // Go back to tabs or initial menu if (editingServerName !== null) { setMode('edit-or-delete'); } else { setMode('tabs'); } setSelectedTemplate(null); setCurrentFieldIndex(0); setFieldAnswers({}); setCurrentValue(''); setMultilineBuffer(''); setError(null); } } } }); if (mode === 'initial-menu') { return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "Configure MCP Servers" }) }), serverCount > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.success, children: [serverCount, " MCP server(s) configured:"] }), Object.values(servers).map((server, index) => (_jsxs(Text, { color: colors.secondary, children: ["\u2022 ", server.name, " (", server.transport, ")"] }, index)))] })), _jsx(SelectInput, { items: initialOptions, onSelect: (item) => handleInitialSelect(item) })] })); } if (mode === 'tabs') { const templateOptions = getTemplateOptions(); return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "Add MCP Servers:" }) }), serverCount > 0 && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.success, children: ["Added:", ' ', Object.values(servers) .map(s => s.name) .join(', ')] }) })), _jsxs(Tabs, { onChange: name => setActiveTab(name), defaultValue: activeTab, flexDirection: "row", colors: { activeTab: { color: colors.success, }, }, children: [_jsx(Tab, { name: "local", children: "Local Servers (STDIO)" }), _jsx(Tab, { name: "remote", children: "Remote Servers (HTTP/WebSocket)" })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { children: activeTab === 'local' ? 'Select a local MCP server to add:' : 'Select a remote MCP server to add:' }) }), _jsx(SelectInput, { items: templateOptions, onSelect: handleTemplateSelect }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.secondary, children: "Arrow keys: Navigate | Tab: Switch tabs" }) })] })); } if (mode === 'edit-selection') { return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "Select an MCP server to edit:" }) }), _jsx(SelectInput, { items: editOptions, onSelect: (item) => handleEditSelect(item) })] })); } if (mode === 'edit-or-delete') { const server = editingServerName !== null ? servers[editingServerName] : null; const editOrDeleteOptions = [ { label: 'Edit this server', value: 'edit' }, { label: 'Delete this server', value: 'delete' }, ]; return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.primary, children: [server?.name, " - What would you like to do?"] }) }), _jsx(SelectInput, { items: editOrDeleteOptions, onSelect: (item) => handleEditOrDeleteChoice(item) })] })); } if (mode === 'field-input' && selectedTemplate) { const currentField = selectedTemplate.fields[currentFieldIndex]; if (!currentField) return null; const isMultiline = currentField.name === 'envVars'; return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: colors.primary, children: [selectedTemplate.name, " Configuration"] }), _jsxs(Text, { dimColor: true, children: [' ', "(Field ", currentFieldIndex + 1, "/", selectedTemplate.fields.length, ")"] })] }), _jsx(Box, { children: _jsxs(Text, { children: [currentField.prompt, currentField.required && _jsx(Text, { color: colors.error, children: " *" }), currentField.default && (_jsxs(Text, { dimColor: true, children: [" [", currentField.default, "]"] })), ": ", currentField.sensitive && '****'] }) }), isMultiline ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { borderStyle: "round", borderColor: colors.secondary, paddingX: 1, children: _jsx(Text, { children: multilineBuffer || _jsx(Text, { dimColor: true, children: "(empty)" }) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.secondary, children: "Type to add lines. Press Esc when done to submit." }) })] })) : currentField.sensitive ? (_jsx(Box, { marginBottom: 1, borderStyle: "round", borderColor: colors.secondary, children: _jsx(TextInput, { value: currentValue, onChange: setCurrentValue, onSubmit: handleFieldSubmit, mask: "*" }, inputKey) })) : (_jsx(Box, { marginBottom: 1, borderStyle: "round", borderColor: colors.secondary, children: _jsx(TextInput, { value: currentValue, onChange: setCurrentValue, onSubmit: handleFieldSubmit }, inputKey) })), error && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.error, children: error }) })), _jsx(Box, { children: _jsx(Text, { color: colors.secondary, children: isMultiline ? 'Press Esc to submit' : 'Press Enter to continue' }) })] })); } return null; } //# sourceMappingURL=mcp-step.js.map