toolsets-menu.tsโข14.2 kB
/**
* Toolsets menu implementation for interactive navigation
*/
import inquirer from "inquirer";
import { execSync } from "child_process";
import { theme } from "../../../utils/theme.js";
import { output } from "../../../utils/output.js";
import { ToolsetInfo, ToolDetail } from "../show.js";
import { ViewType, MenuChoice, InteractiveOptions } from "./types.js";
/**
* Format toolset choice for inquirer list
*/
function formatToolsetChoice(toolset: ToolsetInfo): MenuChoice {
let name = `๐งฐ ${theme.primary(toolset.name)}`;
// Add auto-generated indicator
if (toolset.autoGenerated) {
name += ` ${theme.warning("[auto]")}`;
}
// Add tool count
name += ` ${theme.muted(`(${toolset.toolCount} tool${toolset.toolCount !== 1 ? "s" : ""})`)}`;
// Add description or apps using it
if (toolset.description) {
const shortDesc =
toolset.description.length > 40
? toolset.description.substring(0, 40) + "..."
: toolset.description;
name += `\n ${theme.muted(shortDesc)}`;
} else if (toolset.apps && toolset.apps.length > 0) {
name += `\n ${theme.muted(`Used by: ${toolset.apps.join(", ")}`)}`;
}
return {
name,
value: toolset,
short: toolset.name,
};
}
/**
* Display the toolsets list and handle selection
*/
export async function showToolsetsList(
toolsets: ToolsetInfo[],
options: InteractiveOptions
): Promise<{
action: string;
nextView?: ViewType;
data?: unknown;
itemName?: string;
}> {
// Clear screen and show header
console.clear();
output.displayHeader(`๐งฐ Toolsets (${toolsets.length} total)`);
output.displaySpaceBuffer(1);
// Show summary
const inUse = toolsets.filter((ts) => ts.apps && ts.apps.length > 0).length;
const autoGenerated = toolsets.filter((ts) => ts.autoGenerated).length;
output.info(
`Total: ${theme.value(toolsets.length.toString())} | In Use: ${theme.value(inUse.toString())} | Auto-generated: ${theme.value(autoGenerated.toString())}`
);
output.displaySpaceBuffer(1);
if (toolsets.length === 0) {
output.warn("No toolsets configured.");
output.info("Toolsets help organize MCP tools by application context.");
output.displaySpaceBuffer(2);
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "What would you like to do?",
choices: [
{
name: "[Back]",
value: "back",
},
],
},
]);
return { action };
}
// Create choices
const choices: MenuChoice[] = [];
// Sort toolsets: manual first, then auto-generated
const manualToolsets = toolsets.filter((ts) => !ts.autoGenerated);
const autoToolsets = toolsets.filter((ts) => ts.autoGenerated);
if (manualToolsets.length > 0) {
choices.push(new inquirer.Separator("โโ Manual Toolsets โโ") as any);
for (const toolset of manualToolsets) {
choices.push(formatToolsetChoice(toolset));
}
}
if (autoToolsets.length > 0) {
choices.push(
new inquirer.Separator("โโ Auto-generated Toolsets โโ") as any
);
for (const toolset of autoToolsets) {
choices.push(formatToolsetChoice(toolset));
}
}
choices.push(new inquirer.Separator() as any, {
name: "[Back]",
value: { action: "back" },
});
// Show menu
const { selection } = await inquirer.prompt([
{
type: "list",
name: "selection",
message: "Select a toolset for details:",
choices,
pageSize: Math.min(30, toolsets.length + 8), // Increased page size
},
]);
// Handle selection
if (selection.action === "back") {
return { action: "back" };
} else {
// Toolset selected
return {
action: "navigate",
nextView: ViewType.TOOLSET_DETAIL,
data: selection,
itemName: selection.name,
};
}
}
/**
* Copy text to clipboard (cross-platform)
*/
function copyToClipboard(text: string): boolean {
try {
const platform = process.platform;
if (platform === "darwin") {
execSync("pbcopy", { input: text });
} else if (platform === "win32") {
execSync("clip", { input: text });
} else {
// Linux
try {
execSync("xclip -selection clipboard", { input: text });
} catch {
// Try xsel if xclip is not available
execSync("xsel --clipboard --input", { input: text });
}
}
return true;
} catch {
return false;
}
}
/**
* Display toolset detail view with enhanced tool listing
*/
export async function showToolsetDetail(
toolset: ToolsetInfo,
_allToolsets: ToolsetInfo[]
): Promise<{
action: string;
nextView?: ViewType;
data?: unknown;
itemName?: string;
}> {
// Clear screen and show header
console.clear();
output.displayHeader(`Toolset: ${toolset.name}`);
output.displaySpaceBuffer(1);
// Display toolset details
if (toolset.description) {
output.info(`Description: ${theme.muted(toolset.description)}`);
}
output.info(`Tools: ${theme.value(toolset.toolCount.toString())}`);
if (toolset.autoGenerated) {
output.info(`Type: ${theme.warning("Auto-generated")}`);
} else {
output.info(`Type: ${theme.success("Manual")}`);
}
output.displaySpaceBuffer(1);
// Display tool details if available
if (toolset.serverGroups && Object.keys(toolset.serverGroups).length > 0) {
output.displaySubHeader("๐ง Tool Details:");
output.displaySpaceBuffer(1);
const serverEntries = Object.entries(toolset.serverGroups);
serverEntries.forEach(([serverName, tools], serverIndex) => {
const isLastServer = serverIndex === serverEntries.length - 1;
const serverPrefix = isLastServer ? "โโโ" : "โโโ";
const toolPrefix = isLastServer ? " " : "โ ";
output.info(
`${serverPrefix} ${theme.primary(serverName.charAt(0).toUpperCase() + serverName.slice(1))} Operations (${tools.length} tool${tools.length !== 1 ? "s" : ""}) - from '${theme.warning(serverName)}' server`
);
tools.forEach((tool, toolIndex) => {
const isLastTool = toolIndex === tools.length - 1;
const currentToolPrefix = isLastTool ? "โโโ" : "โโโ";
let toolDisplay = `${toolPrefix}${currentToolPrefix} ${theme.value(tool.toolName)}`;
if (tool.description) {
toolDisplay += ` - ${theme.muted(tool.description)}`;
}
output.info(toolDisplay);
});
if (!isLastServer) {
output.info("โ");
}
});
} else if (toolset.toolDetails && toolset.toolDetails.length > 0) {
// Fallback display if serverGroups not available
output.displaySubHeader("๐ง Tools:");
output.displaySpaceBuffer(1);
toolset.toolDetails.forEach((tool, index) => {
const isLast = index === toolset.toolDetails!.length - 1;
const prefix = isLast ? "โโโ" : "โโโ";
let toolDisplay = `${prefix} ${theme.value(tool.toolName)} (${theme.warning(tool.serverName)})`;
if (tool.description) {
toolDisplay += ` - ${theme.muted(tool.description)}`;
}
output.info(toolDisplay);
});
}
// Display applications using this toolset
if (toolset.apps && toolset.apps.length > 0) {
output.displaySpaceBuffer(1);
output.displaySubHeader("๐ฑ Used by applications:");
for (const app of toolset.apps) {
output.info(` โข ${theme.primary(app)}`);
}
}
output.displaySpaceBuffer(2);
// Show helpful information
output.displayHelpContext(
"โน๏ธ Toolsets organize MCP tools into logical groups."
);
output.displayHelpContext(
" They can be created manually or auto-generated from app configurations."
);
output.displaySpaceBuffer(2);
// Create action choices
const choices: MenuChoice[] = [];
// Add tool navigation if tools are available
if (toolset.toolDetails && toolset.toolDetails.length > 0) {
choices.push({
name: "๐ View Tool Details",
value: { action: "view_tools" },
});
}
choices.push(
{
name: "๐ Copy Tool List",
value: { action: "copy_tools" },
},
new inquirer.Separator() as any,
{
name: "[Back]",
value: { action: "back" },
}
);
// Show actions menu
const { selection } = await inquirer.prompt([
{
type: "list",
name: "selection",
message: "Actions:",
choices,
pageSize: 10,
},
]);
// Handle actions
if (selection.action === "copy_tools") {
if (toolset.toolDetails && toolset.toolDetails.length > 0) {
const toolList = toolset.toolDetails
.map((tool) => tool.namespacedName)
.join("\n");
if (copyToClipboard(toolList)) {
output.success("โ
Tool list copied to clipboard!");
} else {
output.error("โ Failed to copy to clipboard");
}
} else {
output.warn("No tools to copy");
}
await new Promise((resolve) => setTimeout(resolve, 1500));
return { action: "stay" };
} else if (selection.action === "view_tools") {
// Navigate to tool selection menu
return {
action: "navigate",
nextView: ViewType.TOOL_DETAIL,
data: toolset,
itemName: "Tool Details",
};
}
return { action: selection.action };
}
/**
* Display individual tool detail view
*/
export async function showToolDetail(
toolset: ToolsetInfo,
_allToolsets: ToolsetInfo[]
): Promise<{
action: string;
nextView?: ViewType;
data?: unknown;
itemName?: string;
}> {
// Clear screen and show header
console.clear();
output.displayHeader(`Tools in ${toolset.name}`);
output.displaySpaceBuffer(1);
if (!toolset.toolDetails || toolset.toolDetails.length === 0) {
output.warn("No tool details available for this toolset.");
output.displaySpaceBuffer(2);
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "Actions:",
choices: [
{
name: "[Back]",
value: "back",
},
],
},
]);
return { action };
}
// Group tools by server for better organization
const serverGroups = toolset.serverGroups || {};
const choices: MenuChoice[] = [];
// Add tools grouped by server
Object.entries(serverGroups).forEach(([serverName, tools]) => {
choices.push(
new inquirer.Separator(
`โโ ${serverName.toUpperCase()} Server (${tools.length} tools) โโ`
) as any
);
tools.forEach((tool) => {
let name = `๐ง ${theme.primary(tool.toolName)}`;
if (tool.description) {
const shortDesc =
tool.description.length > 50
? tool.description.substring(0, 50) + "..."
: tool.description;
name += `\n ${theme.muted(shortDesc)}`;
}
name += `\n ${theme.warning(`Server: ${tool.serverName} | ID: ${tool.namespacedName}`)}`;
choices.push({
name,
value: tool,
short: tool.toolName,
});
});
choices.push(new inquirer.Separator() as any);
});
// Add navigation options
choices.push({
name: "[Back to Toolset]",
value: { action: "back" },
});
// Show tool selection menu
const { selection } = await inquirer.prompt([
{
type: "list",
name: "selection",
message: "Select a tool for detailed information:",
choices,
pageSize: Math.min(25, choices.length),
},
]);
// Handle selection
if (selection.action === "back") {
return { action: "back" };
} else {
// Show individual tool details
return await showIndividualToolDetail(selection as ToolDetail, toolset);
}
}
/**
* Display individual tool information
*/
async function showIndividualToolDetail(
tool: ToolDetail,
_toolset: ToolsetInfo
): Promise<{
action: string;
nextView?: ViewType;
data?: unknown;
itemName?: string;
}> {
// Clear screen and show header
console.clear();
output.displayHeader(`Tool: ${tool.toolName}`);
output.displaySpaceBuffer(1);
// Display tool details
output.info(`Server: ${theme.primary(tool.serverName)} (stdio)`);
output.info(`Full Name: ${theme.value(tool.namespacedName)}`);
if (tool.description) {
output.info(`Description: ${theme.muted(tool.description)}`);
}
if (tool.parameters && tool.parameters.length > 0) {
output.info(`Parameters: ${theme.warning(tool.parameters.join(", "))}`);
}
output.displaySpaceBuffer(1);
// Show usage examples
output.displaySubHeader("๐ Usage Information:");
output.info(
`โข This tool is provided by the '${theme.warning(tool.serverName)}' MCP server`
);
output.info(
`โข It can be called using the namespaced name: ${theme.value(tool.namespacedName)}`
);
if (tool.description) {
output.info(`โข Purpose: ${theme.muted(tool.description)}`);
}
output.displaySpaceBuffer(2);
// Create action choices
const choices: MenuChoice[] = [
{
name: "๐ Copy Tool Name",
value: { action: "copy_name" },
},
{
name: "๐ View Server Details",
value: { action: "view_server" },
},
new inquirer.Separator() as any,
{
name: "[Back to Tool List]",
value: { action: "back" },
},
];
// Show actions menu
const { selection } = await inquirer.prompt([
{
type: "list",
name: "selection",
message: "Actions:",
choices,
pageSize: 10,
},
]);
// Handle actions
if (selection.action === "copy_name") {
if (copyToClipboard(tool.namespacedName)) {
output.success(`โ
Copied '${tool.namespacedName}' to clipboard!`);
} else {
output.error("โ Failed to copy to clipboard");
}
await new Promise((resolve) => setTimeout(resolve, 1500));
return { action: "stay" };
} else if (selection.action === "view_server") {
// Navigate to server details - this would need to be implemented
// For now, just show a message
output.info(
`Navigate to server '${tool.serverName}' details would go here.`
);
await new Promise((resolve) => setTimeout(resolve, 2000));
return { action: "stay" };
}
return { action: selection.action };
}