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>
61 lines
1.8 KiB
TypeScript
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>
|
|
);
|
|
}
|