mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 19:46:58 +10:00
183 lines
26 KiB
TypeScript
183 lines
26 KiB
TypeScript
|
|
import figures from 'figures';
|
||
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||
|
|
import type { CommandResultDisplay } from '../../commands.js';
|
||
|
|
import { Box, color, Link, Text, useTheme } from '../../ink.js';
|
||
|
|
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||
|
|
import { AuthenticationCancelledError, performMCPOAuthFlow } from '../../services/mcp/auth.js';
|
||
|
|
import { capitalize } from '../../utils/stringUtils.js';
|
||
|
|
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||
|
|
import { Select } from '../CustomSelect/index.js';
|
||
|
|
import { Byline } from '../design-system/Byline.js';
|
||
|
|
import { Dialog } from '../design-system/Dialog.js';
|
||
|
|
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||
|
|
import { Spinner } from '../Spinner.js';
|
||
|
|
import type { AgentMcpServerInfo } from './types.js';
|
||
|
|
type Props = {
|
||
|
|
agentServer: AgentMcpServerInfo;
|
||
|
|
onCancel: () => void;
|
||
|
|
onComplete?: (result?: string, options?: {
|
||
|
|
display?: CommandResultDisplay;
|
||
|
|
}) => void;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Menu for agent-specific MCP servers.
|
||
|
|
* These servers are defined in agent frontmatter and only connect when the agent runs.
|
||
|
|
* For HTTP/SSE servers, this allows pre-authentication before using the agent.
|
||
|
|
*/
|
||
|
|
export function MCPAgentServerMenu({
|
||
|
|
agentServer,
|
||
|
|
onCancel,
|
||
|
|
onComplete
|
||
|
|
}: Props): React.ReactNode {
|
||
|
|
const [theme] = useTheme();
|
||
|
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
const [authorizationUrl, setAuthorizationUrl] = useState<string | null>(null);
|
||
|
|
const authAbortControllerRef = useRef<AbortController | null>(null);
|
||
|
|
|
||
|
|
// Abort OAuth flow on unmount so the callback server is closed even if a
|
||
|
|
// parent component's Esc handler navigates away before ours fires.
|
||
|
|
useEffect(() => () => authAbortControllerRef.current?.abort(), []);
|
||
|
|
|
||
|
|
// Handle ESC to cancel authentication flow
|
||
|
|
const handleEscCancel = useCallback(() => {
|
||
|
|
if (isAuthenticating) {
|
||
|
|
authAbortControllerRef.current?.abort();
|
||
|
|
authAbortControllerRef.current = null;
|
||
|
|
setIsAuthenticating(false);
|
||
|
|
setAuthorizationUrl(null);
|
||
|
|
}
|
||
|
|
}, [isAuthenticating]);
|
||
|
|
useKeybinding('confirm:no', handleEscCancel, {
|
||
|
|
context: 'Confirmation',
|
||
|
|
isActive: isAuthenticating
|
||
|
|
});
|
||
|
|
const handleAuthenticate = useCallback(async () => {
|
||
|
|
if (!agentServer.needsAuth || !agentServer.url) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
setIsAuthenticating(true);
|
||
|
|
setError(null);
|
||
|
|
const controller = new AbortController();
|
||
|
|
authAbortControllerRef.current = controller;
|
||
|
|
try {
|
||
|
|
// Create a temporary config for OAuth
|
||
|
|
const tempConfig = {
|
||
|
|
type: agentServer.transport as 'http' | 'sse',
|
||
|
|
url: agentServer.url
|
||
|
|
};
|
||
|
|
await performMCPOAuthFlow(agentServer.name, tempConfig, setAuthorizationUrl, controller.signal);
|
||
|
|
onComplete?.(`Authentication successful for ${agentServer.name}. The server will connect when the agent runs.`);
|
||
|
|
} catch (err) {
|
||
|
|
// Don't show error if it was a cancellation
|
||
|
|
if (err instanceof Error && !(err instanceof AuthenticationCancelledError)) {
|
||
|
|
setError(err.message);
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
setIsAuthenticating(false);
|
||
|
|
authAbortControllerRef.current = null;
|
||
|
|
}
|
||
|
|
}, [agentServer, onComplete]);
|
||
|
|
const capitalizedServerName = capitalize(String(agentServer.name));
|
||
|
|
if (isAuthenticating) {
|
||
|
|
return <Box flexDirection="column" gap={1} padding={1}>
|
||
|
|
<Text color="claude">Authenticating with {agentServer.name}…</Text>
|
||
|
|
<Box>
|
||
|
|
<Spinner />
|
||
|
|
<Text> A browser window will open for authentication</Text>
|
||
|
|
</Box>
|
||
|
|
{authorizationUrl && <Box flexDirection="column">
|
||
|
|
<Text dimColor>
|
||
|
|
If your browser doesn't open automatically, copy this URL
|
||
|
|
manually:
|
||
|
|
</Text>
|
||
|
|
<Link url={authorizationUrl} />
|
||
|
|
</Box>}
|
||
|
|
<Box marginLeft={3}>
|
||
|
|
<Text dimColor>
|
||
|
|
Return here after authenticating in your browser.{' '}
|
||
|
|
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
</Box>;
|
||
|
|
}
|
||
|
|
const menuOptions = [];
|
||
|
|
|
||
|
|
// Only show authenticate option for HTTP/SSE servers
|
||
|
|
if (agentServer.needsAuth) {
|
||
|
|
menuOptions.push({
|
||
|
|
label: agentServer.isAuthenticated ? 'Re-authenticate' : 'Authenticate',
|
||
|
|
value: 'auth'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
menuOptions.push({
|
||
|
|
label: 'Back',
|
||
|
|
value: 'back'
|
||
|
|
});
|
||
|
|
return <Dialog title={`${capitalizedServerName} MCP Server`} subtitle="agent-only" onCancel={onCancel} inputGuide={exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>
|
||
|
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||
|
|
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||
|
|
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||
|
|
</Byline>}>
|
||
|
|
<Box flexDirection="column" gap={0}>
|
||
|
|
<Box>
|
||
|
|
<Text bold>Type: </Text>
|
||
|
|
<Text dimColor>{agentServer.transport}</Text>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{agentServer.url && <Box>
|
||
|
|
<Text bold>URL: </Text>
|
||
|
|
<Text dimColor>{agentServer.url}</Text>
|
||
|
|
</Box>}
|
||
|
|
|
||
|
|
{agentServer.command && <Box>
|
||
|
|
<Text bold>Command: </Text>
|
||
|
|
<Text dimColor>{agentServer.command}</Text>
|
||
|
|
</Box>}
|
||
|
|
|
||
|
|
<Box>
|
||
|
|
<Text bold>Used by: </Text>
|
||
|
|
<Text dimColor>{agentServer.sourceAgents.join(', ')}</Text>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<Box marginTop={1}>
|
||
|
|
<Text bold>Status: </Text>
|
||
|
|
<Text>
|
||
|
|
{color('inactive', theme)(figures.radioOff)} not connected
|
||
|
|
(agent-only)
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{agentServer.needsAuth && <Box>
|
||
|
|
<Text bold>Auth: </Text>
|
||
|
|
{agentServer.isAuthenticated ? <Text>{color('success', theme)(figures.tick)} authenticated</Text> : <Text>
|
||
|
|
{color('warning', theme)(figures.triangleUpOutline)} may need
|
||
|
|
authentication
|
||
|
|
</Text>}
|
||
|
|
</Box>}
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<Box>
|
||
|
|
<Text dimColor>This server connects only when running the agent.</Text>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{error && <Box>
|
||
|
|
<Text color="error">Error: {error}</Text>
|
||
|
|
</Box>}
|
||
|
|
|
||
|
|
<Box>
|
||
|
|
<Select options={menuOptions} onChange={async value => {
|
||
|
|
switch (value) {
|
||
|
|
case 'auth':
|
||
|
|
await handleAuthenticate();
|
||
|
|
break;
|
||
|
|
case 'back':
|
||
|
|
onCancel();
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}} onCancel={onCancel} />
|
||
|
|
</Box>
|
||
|
|
</Dialog>;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VDYWxsYmFjayIsInVzZUVmZmVjdCIsInVzZVJlZiIsInVzZVN0YXRlIiwiQ29tbWFuZFJlc3VsdERpc3BsYXkiLCJCb3giLCJjb2xvciIsIkxpbmsiLCJUZXh0IiwidXNlVGhlbWUiLCJ1c2VLZXliaW5kaW5nIiwiQXV0aGVudGljYXRpb25DYW5jZWxsZWRFcnJvciIsInBlcmZvcm1NQ1BPQXV0aEZsb3ciLCJjYXBpdGFsaXplIiwiQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50IiwiU2VsZWN0IiwiQnlsaW5lIiwiRGlhbG9nIiwiS2V5Ym9hcmRTaG9ydGN1dEhpbnQiLCJTcGlubmVyIiwiQWdlbnRNY3BTZXJ2ZXJJbmZvIiwiUHJvcHMiLCJhZ2VudFNlcnZlciIsIm9uQ2FuY2VsIiwib25Db21wbGV0ZSIsInJlc3VsdCIsIm9wdGlvbnMiLCJkaXNwbGF5IiwiTUNQQWdlbnRTZXJ2ZXJNZW51IiwiUmVhY3ROb2RlIiwidGhlbWUiLCJpc0F1dGhlbnRpY2F0aW5nIiwic2V0SXNBdXRoZW50aWNhdGluZyIsImVycm9yIiwic2V0RXJyb3IiLCJhdXRob3JpemF0aW9uVXJsIiwic2V0QXV0aG9yaXphdGlvblVybCIsImF1dGhBYm9ydENvbnRyb2xsZXJSZWYiLCJBYm9ydENvbnRyb2xsZXIiLCJjdXJyZW50IiwiYWJvcnQiLCJoYW5kbGVFc2NDYW5jZWwiLCJjb250ZXh0IiwiaXNBY3RpdmUiLCJoYW5kbGVBdXRoZW50aWNhdGUiLCJuZWVkc0F1dGgiLCJ1cmwiLCJjb250cm9sbGVyIiwidGVtcENvbmZpZyIsInR5cGUiLCJ0cmFuc3BvcnQiLCJuYW1lIiwic2lnbmFsIiwiZXJyIiwiRXJyb3IiLCJtZXNzYWdlIiwiY2FwaXRhbGl6ZWRTZXJ2ZXJOYW1lIiwiU3RyaW5nIiwibWVudU9wdGlvbnMiLCJwdXNoIiwibGFiZWwiLCJpc0F1dGhlbnRpY2F0ZWQiLCJ2YWx1ZSIsImV4aXRTdGF0ZSIsInBlbmRpbmciLCJrZXlOYW1lIiwiY29tbWFuZCIsInNvdXJjZUFnZW50cyIsImpvaW4iLCJyYWRpb09mZiIsInRpY2siLCJ0cmlhbmdsZVVwT3V0bGluZSJdLCJzb3VyY2VzIjpbIk1DUEFnZW50U2VydmVyTWVudS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZpZ3VyZXMgZnJvbSAnZmlndXJlcydcbmltcG9ydCBSZWFjdCwgeyB1c2VDYWxsYmFjaywgdXNlRWZmZWN0LCB1c2VSZWYsIHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgdHlwZSB7IENvbW1hbmRSZXN1bHREaXNwbGF5IH0gZnJvbSAnLi4vLi4vY29tbWFuZHMuanMnXG5pbXBvcnQgeyBCb3gsIGNvbG9yLCBMaW5rLCBUZXh0LCB1c2VUaGVtZSB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IHVzZUtleWJpbmRpbmcgfSBmcm9tICcuLi8uLi9rZXliaW5kaW5ncy91c2VLZXliaW5kaW5nLmpzJ1xuaW1wb3J0IHtcbiAgQXV0aGVudGljYXRpb25DYW5jZWxsZWRFcnJvcixcbiAgcGVyZm9ybU1DUE9BdXRoRmxvdyxcbn0gZnJvbSAnLi4vLi4vc2VydmljZXMvbWNwL2F1dGguanMnXG5pbXBvcnQgeyBjYXBpdGFsaXplIH0gZnJvbSAnLi4vLi4vdXRpbHMvc3RyaW5nVXRpbHMuanMnXG5pbXBvcnQgeyBDb25maWd1cmFibGVTaG9ydGN1dEhpbnQgfSBmcm9tICcuLi9Db25maWd1cmFibGVTaG9ydGN1dEhpbnQuanMnXG5pbXBvcnQgeyBTZWxlY3QgfSBmcm9tICcuLi9DdXN0b21TZWxlY3QvaW5kZXguanMnXG5pbXBvcnQgeyBCeWxpbmUgfSBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL0J5bGluZS5qcydcbmltcG9ydCB7IERpYWxvZyB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vRGlhbG9nLmpzJ1xuaW1wb3J0IHsgS2V5Ym9hcmRTaG9ydGN1dEhpbnQgfSBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL0tleWJvYXJkU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgU3Bpbm5lciB9IGZyb20gJy4uL1NwaW5uZXIuanMnXG5pbXBvcnQgdHlwZSB7IEFnZW50TWNwU2VydmVySW5mbyB9IGZyb20gJy4vdHlwZXMuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGFnZW50U2VydmVyOiBBZ2VudE1jcFNlcnZlckluZm9cbiAgb25DYW5jZWw6ICgpID0+IHZvaWRcbiAgb25Db21wbGV0ZT86IChcbiAgICByZXN1bHQ/OiBzdHJpbmcsXG4gICAgb3B0aW9ucz86IHsgZGlzcGxheT86IENvbW1hbmRSZXN1bHREaXNwbGF5IH0sXG4gICkgPT4gdm9pZFxufVxuXG4vKipcbiAqIE1lbnUgZm9yIGFnZW50LXNwZWNpZmljIE1DUCBzZXJ2ZXJzLlxuICogVGhlc2Ugc2VydmVycyBhcmUgZGVmaW5lZCBpbiBhZ2VudCBmcm9udG1hdHRlciBhbmQgb25seSBjb25uZWN0IHdoZW4gdGhlIGFnZW50IHJ1bnMuXG4gKiBGb3IgSFRUUC9TU0Ugc2VydmVycywgdGhpcyBhbGxvd3MgcHJlLWF1dGhlbnRpY2F0aW9uIGJlZm9yZSB1c2luZyB0aGUgYWdlbnQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBNQ1BBZ2VudFNlcnZlck1lbnUoe1xuICBhZ2VudFNlcnZlcixcbiAgb25DYW5jZWwsXG4gIG9uQ29tcGxldGUsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IFt0aGVtZV0gPSB1c2VUaGVtZSgpXG4gIGNvbnN0IFtpc0F1dGhlbnRpY2F0aW5nLCBzZXRJc0F1dGhlbnRpY2F0aW5nXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCBbZXJyb3IsIHNldEVycm9yXSA9IHVzZVN0YXRlPHN0cmluZyB8IG51bGw+KG51bGwpXG4gIGNvbnN0IFthdXRob3JpemF0aW9uVXJsLCBzZXRBdXRob3JpemF0aW9uVXJsXSA9IHVzZVN0YXRlPHN0cmluZyB8IG51bGw+KG51bGwpXG4gIGNvbnN0IGF1dGhBYm9ydENvbnRyb2xsZXJSZWYgPSB1c2VSZWY8QWJvcnRDb250cm9sbGVyIHwgbnVsbD4obnVsbClcblxuICAvLyBBYm9ydCBPQXV0aCBmbG93IG9uIHVubW91bnQgc28gdGhlIGNhbGxiYWNrIHNlcnZlciBpcyBjbG9zZWQgZXZlbiBpZiBhXG4gIC8vIHBhcmVudCBjb21wb25lbnQncyBFc2MgaGFuZGxlciBuYXZpZ2F0ZXMgYXdheSBiZWZvcmUgb3VycyBmaXJlcy5cbiAgdXNlRWZmZWN0KCgpID0+ICgpID0+IGF1dGhBYm9ydENvbnRyb2xsZXJSZWYuY3VycmVudD8uYWJvcnQoKSwgW10pXG5cbiAgLy8gSGFuZGxlIEVTQyB0byBjYW5jZWwgYXV0aGVudGljYXRpb24gZmxvd1xuICBjb25zdCBoYW5kbGVFc2NDYW5jZWwgPSB1c2VDYWx
|