Files
mcpctl/src/cli/src/commands/console/components/resource-list.tsx
Michal a59d2237b9 feat: interactive MCP console (mcpctl console <project>)
Ink-based TUI that shows exactly what an LLM sees through MCP.
Browse tools/resources/prompts, execute them, and see raw JSON-RPC
traffic in a protocol log. Supports gated session flow with
begin_session, raw JSON-RPC input, and session reconnect.

- McpSession class wrapping HTTP transport with typed methods
- 12 React/Ink components (header, protocol-log, menu, tool/resource/prompt views, etc.)
- 21 unit tests for McpSession against a mock MCP server
- Fish + Bash completions with project name argument
- bun compile with --external react-devtools-core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:56:23 +00:00

61 lines
1.8 KiB
TypeScript

import { useState } from 'react';
import { Box, Text } from 'ink';
import { Select, Spinner } from '@inkjs/ui';
import type { McpResource, McpSession } from '../mcp-session.js';
interface ResourceListViewProps {
resources: McpResource[];
session: McpSession;
onResult: (resource: McpResource, content: string) => void;
onError: (msg: string) => void;
onBack: () => void;
}
export function ResourceListView({ resources, session, onResult, onError }: ResourceListViewProps) {
const [loading, setLoading] = useState<string | null>(null);
if (resources.length === 0) {
return <Text dimColor>No resources available.</Text>;
}
const options = resources.map((r) => ({
label: `${r.uri}${r.name ? ` (${r.name})` : ''}${r.description ? `${r.description.slice(0, 50)}` : ''}`,
value: r.uri,
}));
if (loading) {
return (
<Box gap={1}>
<Spinner label={`Reading ${loading}...`} />
</Box>
);
}
return (
<Box flexDirection="column">
<Text bold>Resources ({resources.length}):</Text>
<Box marginTop={1}>
<Select
options={options}
onChange={async (uri) => {
const resource = resources.find((r) => r.uri === uri);
if (!resource) return;
setLoading(uri);
try {
const result = await session.readResource(uri);
const content = result.contents
.map((c) => c.text ?? `[${c.mimeType ?? 'binary'}]`)
.join('\n');
onResult(resource, content);
} catch (err) {
onError(`resources/read failed: ${err instanceof Error ? err.message : String(err)}`);
} finally {
setLoading(null);
}
}}
/>
</Box>
</Box>
);
}