Adding mcp searcher
This commit is contained in:
26
README.md
26
README.md
@@ -284,11 +284,31 @@ CodeTyper has access to these built-in tools:
|
||||
Connect external MCP (Model Context Protocol) servers for extended capabilities:
|
||||
|
||||
```bash
|
||||
# In the TUI
|
||||
/mcp
|
||||
# Then add a new server
|
||||
# Browse and search available servers
|
||||
/mcp browse # Interactive browser
|
||||
/mcp search <query> # Search by keyword
|
||||
/mcp popular # Show popular servers
|
||||
/mcp categories # List all categories
|
||||
|
||||
# Install a server
|
||||
/mcp install sqlite
|
||||
/mcp install github
|
||||
|
||||
# Manage servers
|
||||
/mcp status # Show connected servers
|
||||
/mcp connect # Connect all servers
|
||||
/mcp disconnect # Disconnect all servers
|
||||
/mcp tools # List available tools
|
||||
/mcp add # Add custom server
|
||||
```
|
||||
|
||||
**MCP Browser Features:**
|
||||
- Search by name, description, or tags
|
||||
- Filter by category (database, web, AI, etc.)
|
||||
- View server details and required environment variables
|
||||
- One-click installation and connection
|
||||
- 15+ verified servers from Anthropic
|
||||
|
||||
## Extensibility
|
||||
|
||||
### Hooks System
|
||||
|
||||
@@ -51,6 +51,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Use tools from connected servers
|
||||
- `/mcp` command for server management
|
||||
- Status display in UI
|
||||
- **MCP Server Browser**: Search and discover MCP servers
|
||||
- Interactive browser with `/mcp browse`
|
||||
- Search servers with `/mcp search <query>`
|
||||
- Filter by category (database, web, AI, dev-tools, etc.)
|
||||
- View server details and required environment variables
|
||||
- One-click install with `/mcp install <id>`
|
||||
- 15+ curated verified servers from Anthropic
|
||||
- Registry integration with Smithery
|
||||
|
||||
- **Reasoning System**: Advanced agent orchestration
|
||||
- Memory selection for context optimization
|
||||
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
connectAllServers,
|
||||
disconnectAllServers,
|
||||
getAllTools,
|
||||
searchServers,
|
||||
getPopularServers,
|
||||
installServerById,
|
||||
getCategoriesWithCounts,
|
||||
} from "@services/mcp/index";
|
||||
import {
|
||||
MCP_CATEGORY_LABELS,
|
||||
MCP_CATEGORY_ICONS,
|
||||
} from "@constants/mcp-registry";
|
||||
import { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
|
||||
@@ -26,13 +34,18 @@ export const handleMCP = async (args: string[]): Promise<void> => {
|
||||
disconnect: handleDisconnect,
|
||||
tools: handleTools,
|
||||
add: handleAdd,
|
||||
search: handleSearch,
|
||||
browse: handleBrowse,
|
||||
install: handleInstall,
|
||||
popular: handlePopular,
|
||||
categories: handleCategories,
|
||||
};
|
||||
|
||||
const handler = handlers[subcommand];
|
||||
if (!handler) {
|
||||
console.log(chalk.yellow(`Unknown MCP command: ${subcommand}`));
|
||||
console.log(
|
||||
chalk.gray("Available: status, connect, disconnect, tools, add"),
|
||||
chalk.gray("Available: status, connect, disconnect, tools, add, search, browse, install, popular, categories"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -141,3 +154,137 @@ const handleTools = async (_args: string[]): Promise<void> => {
|
||||
const handleAdd = async (_args: string[]): Promise<void> => {
|
||||
appStore.setMode("mcp_add");
|
||||
};
|
||||
|
||||
/**
|
||||
* Search for MCP servers
|
||||
*/
|
||||
const handleSearch = async (args: string[]): Promise<void> => {
|
||||
const query = args.join(" ");
|
||||
|
||||
if (!query) {
|
||||
console.log(chalk.yellow("\nUsage: /mcp search <query>"));
|
||||
console.log(chalk.gray("Example: /mcp search database"));
|
||||
console.log(chalk.gray("Or use /mcp browse for interactive browser"));
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.gray(`\nSearching for "${query}"...`));
|
||||
|
||||
try {
|
||||
const result = await searchServers({ query, limit: 10 });
|
||||
|
||||
if (result.servers.length === 0) {
|
||||
console.log(chalk.yellow("\nNo servers found matching your search."));
|
||||
console.log(chalk.gray("Try /mcp popular to see popular servers"));
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold(`\nFound ${result.total} servers:\n`));
|
||||
|
||||
for (const server of result.servers) {
|
||||
const icon = MCP_CATEGORY_ICONS[server.category];
|
||||
const verified = server.verified ? chalk.green(" ✓") : "";
|
||||
console.log(`${icon} ${chalk.white(server.name)}${verified}`);
|
||||
console.log(` ${chalk.gray(server.description)}`);
|
||||
console.log(` ${chalk.cyan("Install:")} /mcp install ${server.id}`);
|
||||
console.log();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`\nSearch failed: ${error}`));
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open interactive MCP browser
|
||||
*/
|
||||
const handleBrowse = async (_args: string[]): Promise<void> => {
|
||||
appStore.setMode("mcp_browse");
|
||||
};
|
||||
|
||||
/**
|
||||
* Install an MCP server by ID
|
||||
*/
|
||||
const handleInstall = async (args: string[]): Promise<void> => {
|
||||
const serverId = args[0];
|
||||
|
||||
if (!serverId) {
|
||||
console.log(chalk.yellow("\nUsage: /mcp install <server-id>"));
|
||||
console.log(chalk.gray("Example: /mcp install sqlite"));
|
||||
console.log(chalk.gray("Use /mcp search to find server IDs"));
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.gray(`\nInstalling ${serverId}...`));
|
||||
|
||||
try {
|
||||
const result = await installServerById(serverId, { connect: true });
|
||||
|
||||
if (result.success) {
|
||||
console.log(chalk.green(`\n✓ Installed ${result.serverName}`));
|
||||
if (result.connected) {
|
||||
console.log(chalk.gray(" Server is now connected"));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red(`\n✗ Installation failed: ${result.error}`));
|
||||
}
|
||||
console.log();
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`\nInstallation failed: ${error}`));
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show popular MCP servers
|
||||
*/
|
||||
const handlePopular = async (_args: string[]): Promise<void> => {
|
||||
console.log(chalk.gray("\nFetching popular servers..."));
|
||||
|
||||
try {
|
||||
const servers = await getPopularServers(10);
|
||||
|
||||
console.log(chalk.bold("\nPopular MCP Servers:\n"));
|
||||
|
||||
for (const server of servers) {
|
||||
const icon = MCP_CATEGORY_ICONS[server.category];
|
||||
const verified = server.verified ? chalk.green(" ✓") : "";
|
||||
console.log(`${icon} ${chalk.white(server.name)}${verified}`);
|
||||
console.log(` ${chalk.gray(server.description)}`);
|
||||
console.log(` ${chalk.cyan("Install:")} /mcp install ${server.id}`);
|
||||
console.log();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`\nFailed to fetch servers: ${error}`));
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show MCP server categories
|
||||
*/
|
||||
const handleCategories = async (_args: string[]): Promise<void> => {
|
||||
console.log(chalk.gray("\nFetching categories..."));
|
||||
|
||||
try {
|
||||
const categories = await getCategoriesWithCounts();
|
||||
|
||||
console.log(chalk.bold("\nMCP Server Categories:\n"));
|
||||
|
||||
for (const { category, count } of categories) {
|
||||
const icon = MCP_CATEGORY_ICONS[category];
|
||||
const label = MCP_CATEGORY_LABELS[category];
|
||||
console.log(`${icon} ${chalk.white(label)} ${chalk.gray(`(${count} servers)`)}`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.gray("Use /mcp search <category> to filter by category"));
|
||||
console.log();
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`\nFailed to fetch categories: ${error}`));
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
368
src/constants/mcp-registry.ts
Normal file
368
src/constants/mcp-registry.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* MCP Registry Constants
|
||||
*
|
||||
* Constants for MCP server discovery and search
|
||||
*/
|
||||
|
||||
import type { MCPServerCategory, MCPRegistryServer } from "@/types/mcp-registry";
|
||||
|
||||
/**
|
||||
* Default registry sources
|
||||
*/
|
||||
export const MCP_REGISTRY_SOURCES = {
|
||||
/** Official MCP servers GitHub */
|
||||
OFFICIAL: "https://raw.githubusercontent.com/modelcontextprotocol/servers/main/README.md",
|
||||
/** Smithery registry API */
|
||||
SMITHERY: "https://registry.smithery.ai/servers",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Cache settings
|
||||
*/
|
||||
export const MCP_REGISTRY_CACHE = {
|
||||
/** Cache duration in milliseconds (1 hour) */
|
||||
DURATION_MS: 60 * 60 * 1000,
|
||||
/** Cache file name */
|
||||
FILE_NAME: "mcp-registry-cache.json",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Category display names
|
||||
*/
|
||||
export const MCP_CATEGORY_LABELS: Record<MCPServerCategory, string> = {
|
||||
database: "Database",
|
||||
filesystem: "File System",
|
||||
web: "Web & Browser",
|
||||
ai: "AI & ML",
|
||||
"dev-tools": "Developer Tools",
|
||||
productivity: "Productivity",
|
||||
communication: "Communication",
|
||||
cloud: "Cloud Services",
|
||||
security: "Security",
|
||||
other: "Other",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Category icons (emoji)
|
||||
*/
|
||||
export const MCP_CATEGORY_ICONS: Record<MCPServerCategory, string> = {
|
||||
database: "🗄️",
|
||||
filesystem: "📁",
|
||||
web: "🌐",
|
||||
ai: "🤖",
|
||||
"dev-tools": "🛠️",
|
||||
productivity: "📋",
|
||||
communication: "💬",
|
||||
cloud: "☁️",
|
||||
security: "🔒",
|
||||
other: "📦",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Search defaults
|
||||
*/
|
||||
export const MCP_SEARCH_DEFAULTS = {
|
||||
LIMIT: 20,
|
||||
SORT_BY: "popularity" as const,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Built-in curated server list
|
||||
* These are well-known, verified MCP servers
|
||||
*/
|
||||
export const MCP_CURATED_SERVERS: MCPRegistryServer[] = [
|
||||
{
|
||||
id: "filesystem",
|
||||
name: "Filesystem",
|
||||
description: "Read, write, and manage files on the local filesystem",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-filesystem",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
|
||||
category: "filesystem",
|
||||
tags: ["files", "read", "write", "directory"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 100,
|
||||
verified: true,
|
||||
installHint: "Replace /path/to/dir with the directory you want to access",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "sqlite",
|
||||
name: "SQLite",
|
||||
description: "Query and manage SQLite databases",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-sqlite",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-sqlite", "/path/to/db.sqlite"],
|
||||
category: "database",
|
||||
tags: ["sql", "database", "query", "sqlite"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 95,
|
||||
verified: true,
|
||||
installHint: "Replace /path/to/db.sqlite with your database file path",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "postgres",
|
||||
name: "PostgreSQL",
|
||||
description: "Connect to and query PostgreSQL databases",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-postgres",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-postgres"],
|
||||
category: "database",
|
||||
tags: ["sql", "database", "query", "postgres", "postgresql"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 90,
|
||||
verified: true,
|
||||
envVars: ["POSTGRES_CONNECTION_STRING"],
|
||||
installHint: "Set POSTGRES_CONNECTION_STRING environment variable",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "github",
|
||||
name: "GitHub",
|
||||
description: "Interact with GitHub repositories, issues, and pull requests",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-github",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-github"],
|
||||
category: "dev-tools",
|
||||
tags: ["github", "git", "repository", "issues", "pr"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 92,
|
||||
verified: true,
|
||||
envVars: ["GITHUB_PERSONAL_ACCESS_TOKEN"],
|
||||
installHint: "Set GITHUB_PERSONAL_ACCESS_TOKEN environment variable",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "gitlab",
|
||||
name: "GitLab",
|
||||
description: "Interact with GitLab repositories and CI/CD",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-gitlab",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-gitlab"],
|
||||
category: "dev-tools",
|
||||
tags: ["gitlab", "git", "repository", "ci", "cd"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 75,
|
||||
verified: true,
|
||||
envVars: ["GITLAB_PERSONAL_ACCESS_TOKEN", "GITLAB_API_URL"],
|
||||
installHint: "Set GITLAB_PERSONAL_ACCESS_TOKEN and optionally GITLAB_API_URL",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "slack",
|
||||
name: "Slack",
|
||||
description: "Send messages and interact with Slack workspaces",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-slack",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-slack"],
|
||||
category: "communication",
|
||||
tags: ["slack", "messaging", "chat", "team"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 80,
|
||||
verified: true,
|
||||
envVars: ["SLACK_BOT_TOKEN", "SLACK_TEAM_ID"],
|
||||
installHint: "Set SLACK_BOT_TOKEN and SLACK_TEAM_ID environment variables",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "google-drive",
|
||||
name: "Google Drive",
|
||||
description: "Access and manage files in Google Drive",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-gdrive",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-gdrive"],
|
||||
category: "cloud",
|
||||
tags: ["google", "drive", "cloud", "storage", "files"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 78,
|
||||
verified: true,
|
||||
installHint: "Requires Google OAuth credentials setup",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "memory",
|
||||
name: "Memory",
|
||||
description: "Persistent memory and knowledge graph for context",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-memory",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-memory"],
|
||||
category: "ai",
|
||||
tags: ["memory", "knowledge", "graph", "context", "persistent"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 85,
|
||||
verified: true,
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "brave-search",
|
||||
name: "Brave Search",
|
||||
description: "Search the web using Brave Search API",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-brave-search",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||
category: "web",
|
||||
tags: ["search", "web", "brave", "internet"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 82,
|
||||
verified: true,
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
installHint: "Set BRAVE_API_KEY environment variable",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "puppeteer",
|
||||
name: "Puppeteer",
|
||||
description: "Browser automation and web scraping with Puppeteer",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-puppeteer",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-puppeteer"],
|
||||
category: "web",
|
||||
tags: ["browser", "automation", "scraping", "puppeteer", "chrome"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 76,
|
||||
verified: true,
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "fetch",
|
||||
name: "Fetch",
|
||||
description: "Make HTTP requests and fetch web content",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-fetch",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-fetch"],
|
||||
category: "web",
|
||||
tags: ["http", "fetch", "api", "request", "web"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 88,
|
||||
verified: true,
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "sequential-thinking",
|
||||
name: "Sequential Thinking",
|
||||
description: "Step-by-step reasoning and problem-solving",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-sequential-thinking",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||
category: "ai",
|
||||
tags: ["thinking", "reasoning", "chain-of-thought", "problem-solving"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 70,
|
||||
verified: true,
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "everart",
|
||||
name: "EverArt",
|
||||
description: "AI image generation with EverArt",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-everart",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-everart"],
|
||||
category: "ai",
|
||||
tags: ["image", "generation", "ai", "art", "creative"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 65,
|
||||
verified: true,
|
||||
envVars: ["EVERART_API_KEY"],
|
||||
installHint: "Set EVERART_API_KEY environment variable",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "sentry",
|
||||
name: "Sentry",
|
||||
description: "Access Sentry error tracking and monitoring",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-sentry",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-sentry"],
|
||||
category: "dev-tools",
|
||||
tags: ["sentry", "errors", "monitoring", "debugging"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 72,
|
||||
verified: true,
|
||||
envVars: ["SENTRY_AUTH_TOKEN", "SENTRY_ORG"],
|
||||
installHint: "Set SENTRY_AUTH_TOKEN and SENTRY_ORG environment variables",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "aws-kb-retrieval",
|
||||
name: "AWS Knowledge Base",
|
||||
description: "Retrieve information from AWS Bedrock Knowledge Bases",
|
||||
author: "Anthropic",
|
||||
repository: "https://github.com/modelcontextprotocol/servers",
|
||||
package: "@modelcontextprotocol/server-aws-kb-retrieval",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"],
|
||||
category: "cloud",
|
||||
tags: ["aws", "bedrock", "knowledge-base", "retrieval", "rag"],
|
||||
transport: "stdio",
|
||||
version: "latest",
|
||||
popularity: 68,
|
||||
verified: true,
|
||||
envVars: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"],
|
||||
installHint: "Set AWS credentials environment variables",
|
||||
updatedAt: "2024-12-01",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Error messages
|
||||
*/
|
||||
export const MCP_REGISTRY_ERRORS = {
|
||||
FETCH_FAILED: "Failed to fetch MCP registry",
|
||||
PARSE_FAILED: "Failed to parse registry data",
|
||||
NOT_FOUND: "No servers found matching your search",
|
||||
INSTALL_FAILED: "Failed to install MCP server",
|
||||
ALREADY_INSTALLED: "Server is already installed",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Success messages
|
||||
*/
|
||||
export const MCP_REGISTRY_SUCCESS = {
|
||||
INSTALLED: "MCP server installed successfully",
|
||||
CONNECTED: "MCP server connected",
|
||||
CACHE_UPDATED: "Registry cache updated",
|
||||
} as const;
|
||||
@@ -35,3 +35,28 @@ export type {
|
||||
MCPTransportType,
|
||||
MCPManagerState,
|
||||
} from "@/types/mcp";
|
||||
|
||||
// Registry exports
|
||||
export {
|
||||
getAllServers,
|
||||
getCuratedServers,
|
||||
searchServers,
|
||||
getServerById,
|
||||
getServersByCategory,
|
||||
isServerInstalled,
|
||||
installServer,
|
||||
installServerById,
|
||||
getPopularServers,
|
||||
getVerifiedServers,
|
||||
getCategoriesWithCounts,
|
||||
refreshRegistry,
|
||||
clearRegistryCache,
|
||||
} from "@services/mcp/registry";
|
||||
|
||||
export type {
|
||||
MCPRegistryServer,
|
||||
MCPSearchResult,
|
||||
MCPSearchOptions,
|
||||
MCPInstallResult,
|
||||
MCPServerCategory,
|
||||
} from "@/types/mcp-registry";
|
||||
|
||||
457
src/services/mcp/registry.ts
Normal file
457
src/services/mcp/registry.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* MCP Registry Service
|
||||
*
|
||||
* Service for discovering, searching, and installing MCP servers
|
||||
*/
|
||||
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import type {
|
||||
MCPRegistryServer,
|
||||
MCPSearchResult,
|
||||
MCPSearchOptions,
|
||||
MCPRegistryCache,
|
||||
MCPInstallResult,
|
||||
MCPServerCategory,
|
||||
} from "@/types/mcp-registry";
|
||||
import {
|
||||
MCP_CURATED_SERVERS,
|
||||
MCP_REGISTRY_CACHE,
|
||||
MCP_REGISTRY_SOURCES,
|
||||
MCP_REGISTRY_ERRORS,
|
||||
MCP_REGISTRY_SUCCESS,
|
||||
MCP_SEARCH_DEFAULTS,
|
||||
} from "@constants/mcp-registry";
|
||||
import { addServer, connectServer, getServerInstances } from "./manager";
|
||||
|
||||
/**
|
||||
* In-memory cache for registry data
|
||||
*/
|
||||
let registryCache: MCPRegistryCache | null = null;
|
||||
|
||||
/**
|
||||
* Get cache file path
|
||||
*/
|
||||
const getCacheFilePath = (): string => {
|
||||
return join(homedir(), ".codetyper", MCP_REGISTRY_CACHE.FILE_NAME);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load cache from disk
|
||||
*/
|
||||
const loadCache = async (): Promise<MCPRegistryCache | null> => {
|
||||
try {
|
||||
const cachePath = getCacheFilePath();
|
||||
const file = Bun.file(cachePath);
|
||||
if (await file.exists()) {
|
||||
const data = await file.json();
|
||||
return data as MCPRegistryCache;
|
||||
}
|
||||
} catch {
|
||||
// Cache doesn't exist or is invalid
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save cache to disk
|
||||
*/
|
||||
const saveCache = async (cache: MCPRegistryCache): Promise<void> => {
|
||||
try {
|
||||
const cachePath = getCacheFilePath();
|
||||
await Bun.write(cachePath, JSON.stringify(cache, null, 2));
|
||||
} catch {
|
||||
// Ignore cache write errors
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if cache is valid (not expired)
|
||||
*/
|
||||
const isCacheValid = (cache: MCPRegistryCache): boolean => {
|
||||
const now = Date.now();
|
||||
return now - cache.updatedAt < MCP_REGISTRY_CACHE.DURATION_MS;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch servers from Smithery registry
|
||||
*/
|
||||
const fetchFromSmithery = async (): Promise<MCPRegistryServer[]> => {
|
||||
try {
|
||||
const response = await fetch(MCP_REGISTRY_SOURCES.SMITHERY);
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Transform Smithery format to our format
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((server: Record<string, unknown>) => ({
|
||||
id: String(server.name || server.id || ""),
|
||||
name: String(server.displayName || server.name || ""),
|
||||
description: String(server.description || ""),
|
||||
author: String(server.author || server.vendor || "Community"),
|
||||
repository: String(server.homepage || server.repository || ""),
|
||||
package: String(server.qualifiedName || server.package || ""),
|
||||
command: "npx",
|
||||
args: ["-y", String(server.qualifiedName || server.package || "")],
|
||||
category: mapCategory(String(server.category || "other")),
|
||||
tags: Array.isArray(server.tags) ? server.tags.map(String) : [],
|
||||
transport: "stdio" as const,
|
||||
version: String(server.version || "latest"),
|
||||
popularity: Number(server.downloads || server.useCount || 0),
|
||||
verified: Boolean(server.verified || server.isOfficial),
|
||||
installHint: String(server.installHint || ""),
|
||||
envVars: Array.isArray(server.environmentVariables)
|
||||
? server.environmentVariables.map(String)
|
||||
: undefined,
|
||||
updatedAt: String(server.updatedAt || new Date().toISOString()),
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Map external category to our category type
|
||||
*/
|
||||
const mapCategory = (category: string): MCPServerCategory => {
|
||||
const categoryMap: Record<string, MCPServerCategory> = {
|
||||
database: "database",
|
||||
databases: "database",
|
||||
db: "database",
|
||||
filesystem: "filesystem",
|
||||
files: "filesystem",
|
||||
file: "filesystem",
|
||||
web: "web",
|
||||
browser: "web",
|
||||
http: "web",
|
||||
ai: "ai",
|
||||
ml: "ai",
|
||||
"machine-learning": "ai",
|
||||
"dev-tools": "dev-tools",
|
||||
developer: "dev-tools",
|
||||
development: "dev-tools",
|
||||
tools: "dev-tools",
|
||||
productivity: "productivity",
|
||||
communication: "communication",
|
||||
chat: "communication",
|
||||
messaging: "communication",
|
||||
cloud: "cloud",
|
||||
aws: "cloud",
|
||||
gcp: "cloud",
|
||||
azure: "cloud",
|
||||
security: "security",
|
||||
};
|
||||
|
||||
const normalized = category.toLowerCase().trim();
|
||||
return categoryMap[normalized] || "other";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all servers (curated + external)
|
||||
*/
|
||||
export const getAllServers = async (
|
||||
forceRefresh = false
|
||||
): Promise<MCPRegistryServer[]> => {
|
||||
// Check in-memory cache first
|
||||
if (!forceRefresh && registryCache && isCacheValid(registryCache)) {
|
||||
return registryCache.servers;
|
||||
}
|
||||
|
||||
// Check disk cache
|
||||
if (!forceRefresh) {
|
||||
const diskCache = await loadCache();
|
||||
if (diskCache && isCacheValid(diskCache)) {
|
||||
registryCache = diskCache;
|
||||
return diskCache.servers;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from external sources
|
||||
const externalServers = await fetchFromSmithery();
|
||||
|
||||
// Merge curated servers with external, curated takes precedence
|
||||
const curatedIds = new Set(MCP_CURATED_SERVERS.map((s) => s.id));
|
||||
const filteredExternal = externalServers.filter((s) => !curatedIds.has(s.id));
|
||||
|
||||
const allServers = [...MCP_CURATED_SERVERS, ...filteredExternal];
|
||||
|
||||
// Update cache
|
||||
registryCache = {
|
||||
servers: allServers,
|
||||
updatedAt: Date.now(),
|
||||
source: MCP_REGISTRY_SOURCES.SMITHERY,
|
||||
};
|
||||
|
||||
await saveCache(registryCache);
|
||||
|
||||
return allServers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get curated servers only (no network)
|
||||
*/
|
||||
export const getCuratedServers = (): MCPRegistryServer[] => {
|
||||
return MCP_CURATED_SERVERS;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search for MCP servers
|
||||
*/
|
||||
export const searchServers = async (
|
||||
options: MCPSearchOptions = {}
|
||||
): Promise<MCPSearchResult> => {
|
||||
const {
|
||||
query = "",
|
||||
category,
|
||||
tags,
|
||||
verifiedOnly = false,
|
||||
sortBy = MCP_SEARCH_DEFAULTS.SORT_BY,
|
||||
limit = MCP_SEARCH_DEFAULTS.LIMIT,
|
||||
offset = 0,
|
||||
} = options;
|
||||
|
||||
const allServers = await getAllServers();
|
||||
|
||||
let filtered = allServers;
|
||||
|
||||
// Filter by query
|
||||
if (query) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
filtered = filtered.filter((server) => {
|
||||
const searchableText = [
|
||||
server.name,
|
||||
server.description,
|
||||
server.author,
|
||||
...server.tags,
|
||||
].join(" ").toLowerCase();
|
||||
|
||||
return searchableText.includes(lowerQuery);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by category
|
||||
if (category) {
|
||||
filtered = filtered.filter((server) => server.category === category);
|
||||
}
|
||||
|
||||
// Filter by tags
|
||||
if (tags && tags.length > 0) {
|
||||
filtered = filtered.filter((server) =>
|
||||
tags.some((tag) =>
|
||||
server.tags.some((serverTag) =>
|
||||
serverTag.toLowerCase().includes(tag.toLowerCase())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter verified only
|
||||
if (verifiedOnly) {
|
||||
filtered = filtered.filter((server) => server.verified);
|
||||
}
|
||||
|
||||
// Sort
|
||||
const sortFunctions: Record<string, (a: MCPRegistryServer, b: MCPRegistryServer) => number> = {
|
||||
popularity: (a, b) => b.popularity - a.popularity,
|
||||
name: (a, b) => a.name.localeCompare(b.name),
|
||||
updated: (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
};
|
||||
|
||||
filtered.sort(sortFunctions[sortBy] || sortFunctions.popularity);
|
||||
|
||||
// Paginate
|
||||
const total = filtered.length;
|
||||
const paginated = filtered.slice(offset, offset + limit);
|
||||
|
||||
return {
|
||||
servers: paginated,
|
||||
total,
|
||||
query,
|
||||
category,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get server by ID
|
||||
*/
|
||||
export const getServerById = async (
|
||||
id: string
|
||||
): Promise<MCPRegistryServer | undefined> => {
|
||||
const allServers = await getAllServers();
|
||||
return allServers.find((server) => server.id === id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get servers by category
|
||||
*/
|
||||
export const getServersByCategory = async (
|
||||
category: MCPServerCategory
|
||||
): Promise<MCPRegistryServer[]> => {
|
||||
const allServers = await getAllServers();
|
||||
return allServers.filter((server) => server.category === category);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a server is already installed
|
||||
*/
|
||||
export const isServerInstalled = (serverId: string): boolean => {
|
||||
const instances = getServerInstances();
|
||||
return instances.some((instance) =>
|
||||
instance.config.name === serverId ||
|
||||
instance.config.name.toLowerCase() === serverId.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Install an MCP server from the registry
|
||||
*/
|
||||
export const installServer = async (
|
||||
server: MCPRegistryServer,
|
||||
options: {
|
||||
global?: boolean;
|
||||
connect?: boolean;
|
||||
customArgs?: string[];
|
||||
} = {}
|
||||
): Promise<MCPInstallResult> => {
|
||||
const { global = false, connect = true, customArgs } = options;
|
||||
|
||||
// Check if already installed
|
||||
if (isServerInstalled(server.id)) {
|
||||
return {
|
||||
success: false,
|
||||
serverName: server.id,
|
||||
error: MCP_REGISTRY_ERRORS.ALREADY_INSTALLED,
|
||||
connected: false,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Add server to configuration
|
||||
await addServer(
|
||||
{
|
||||
name: server.id,
|
||||
command: server.command,
|
||||
args: customArgs || server.args,
|
||||
transport: server.transport,
|
||||
enabled: true,
|
||||
},
|
||||
global
|
||||
);
|
||||
|
||||
let connected = false;
|
||||
|
||||
// Connect if requested
|
||||
if (connect) {
|
||||
try {
|
||||
await connectServer(server.id);
|
||||
connected = true;
|
||||
} catch {
|
||||
// Server added but connection failed
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
serverName: server.id,
|
||||
connected,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
serverName: server.id,
|
||||
error: error instanceof Error ? error.message : MCP_REGISTRY_ERRORS.INSTALL_FAILED,
|
||||
connected: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Install server by ID
|
||||
*/
|
||||
export const installServerById = async (
|
||||
serverId: string,
|
||||
options: {
|
||||
global?: boolean;
|
||||
connect?: boolean;
|
||||
customArgs?: string[];
|
||||
} = {}
|
||||
): Promise<MCPInstallResult> => {
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
serverName: serverId,
|
||||
error: MCP_REGISTRY_ERRORS.NOT_FOUND,
|
||||
connected: false,
|
||||
};
|
||||
}
|
||||
|
||||
return installServer(server, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get popular servers
|
||||
*/
|
||||
export const getPopularServers = async (
|
||||
limit = 10
|
||||
): Promise<MCPRegistryServer[]> => {
|
||||
const allServers = await getAllServers();
|
||||
return allServers
|
||||
.sort((a, b) => b.popularity - a.popularity)
|
||||
.slice(0, limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get verified servers
|
||||
*/
|
||||
export const getVerifiedServers = async (): Promise<MCPRegistryServer[]> => {
|
||||
const allServers = await getAllServers();
|
||||
return allServers.filter((server) => server.verified);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all categories with counts
|
||||
*/
|
||||
export const getCategoriesWithCounts = async (): Promise<
|
||||
Array<{ category: MCPServerCategory; count: number }>
|
||||
> => {
|
||||
const allServers = await getAllServers();
|
||||
const counts = new Map<MCPServerCategory, number>();
|
||||
|
||||
for (const server of allServers) {
|
||||
const current = counts.get(server.category) || 0;
|
||||
counts.set(server.category, current + 1);
|
||||
}
|
||||
|
||||
return Array.from(counts.entries())
|
||||
.map(([category, count]) => ({ category, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh registry cache
|
||||
*/
|
||||
export const refreshRegistry = async (): Promise<void> => {
|
||||
await getAllServers(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear registry cache
|
||||
*/
|
||||
export const clearRegistryCache = async (): Promise<void> => {
|
||||
registryCache = null;
|
||||
try {
|
||||
const cachePath = getCacheFilePath();
|
||||
const file = Bun.file(cachePath);
|
||||
if (await file.exists()) {
|
||||
await Bun.write(cachePath, "");
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
AgentSelect,
|
||||
ThemeSelect,
|
||||
MCPSelect,
|
||||
MCPBrowser,
|
||||
TodoPanel,
|
||||
FilePicker,
|
||||
HomeContent,
|
||||
@@ -130,6 +131,7 @@ export function App({
|
||||
const isAgentSelectOpen = mode === "agent_select";
|
||||
const isThemeSelectOpen = mode === "theme_select";
|
||||
const isMCPSelectOpen = mode === "mcp_select";
|
||||
const isMCPBrowserOpen = mode === "mcp_browse";
|
||||
const isLearningPromptOpen = mode === "learning_prompt";
|
||||
|
||||
// Theme colors
|
||||
@@ -296,6 +298,11 @@ export function App({
|
||||
setMode("idle");
|
||||
}, [setMode]);
|
||||
|
||||
// Handle MCP browser close
|
||||
const handleMCPBrowserClose = useCallback(() => {
|
||||
setMode("idle");
|
||||
}, [setMode]);
|
||||
|
||||
// Handle file selection from picker
|
||||
const handleFileSelect = useCallback(
|
||||
(path: string): void => {
|
||||
@@ -400,6 +407,7 @@ export function App({
|
||||
isAgentSelectOpen ||
|
||||
isThemeSelectOpen ||
|
||||
isMCPSelectOpen ||
|
||||
isMCPBrowserOpen ||
|
||||
isLearningPromptOpen
|
||||
) {
|
||||
closeCommandMenu();
|
||||
@@ -732,6 +740,14 @@ export function App({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* MCP Browser Overlay */}
|
||||
{isMCPBrowserOpen && (
|
||||
<MCPBrowser
|
||||
onClose={handleMCPBrowserClose}
|
||||
isActive={isMCPBrowserOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* File Picker Overlay */}
|
||||
{filePickerOpen && (
|
||||
<FilePicker
|
||||
|
||||
600
src/tui/components/MCPBrowser.tsx
Normal file
600
src/tui/components/MCPBrowser.tsx
Normal file
@@ -0,0 +1,600 @@
|
||||
/**
|
||||
* MCPBrowser Component - Browse and search MCP servers from registry
|
||||
*
|
||||
* Allows users to discover, search, and install MCP servers
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect, useCallback } from "react";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import {
|
||||
searchServers,
|
||||
getCuratedServers,
|
||||
installServer,
|
||||
isServerInstalled,
|
||||
getCategoriesWithCounts,
|
||||
} from "@services/mcp/index";
|
||||
import type {
|
||||
MCPRegistryServer,
|
||||
MCPServerCategory,
|
||||
} from "@/types/mcp-registry";
|
||||
import {
|
||||
MCP_CATEGORY_LABELS,
|
||||
MCP_CATEGORY_ICONS,
|
||||
} from "@constants/mcp-registry";
|
||||
|
||||
interface MCPBrowserProps {
|
||||
onClose: () => void;
|
||||
onInstalled?: (serverName: string) => void;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
type BrowserMode = "browse" | "search" | "category" | "detail" | "installing";
|
||||
|
||||
interface CategoryCount {
|
||||
category: MCPServerCategory;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const MAX_VISIBLE = 8;
|
||||
|
||||
const STATUS_COLORS = {
|
||||
verified: "green",
|
||||
installed: "cyan",
|
||||
popular: "yellow",
|
||||
default: "white",
|
||||
} as const;
|
||||
|
||||
export function MCPBrowser({
|
||||
onClose,
|
||||
onInstalled,
|
||||
isActive = true,
|
||||
}: MCPBrowserProps): React.ReactElement {
|
||||
const [mode, setMode] = useState<BrowserMode>("browse");
|
||||
const [servers, setServers] = useState<MCPRegistryServer[]>([]);
|
||||
const [categories, setCategories] = useState<CategoryCount[]>([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedCategory, setSelectedCategory] = useState<MCPServerCategory | null>(null);
|
||||
const [selectedServer, setSelectedServer] = useState<MCPRegistryServer | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [messageType, setMessageType] = useState<"success" | "error" | "info">("info");
|
||||
|
||||
// Load initial data
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const curatedServers = getCuratedServers();
|
||||
setServers(curatedServers);
|
||||
const cats = await getCategoriesWithCounts();
|
||||
setCategories(cats);
|
||||
} catch {
|
||||
setServers(getCuratedServers());
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
// Search handler
|
||||
const handleSearch = useCallback(async (query: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await searchServers({
|
||||
query,
|
||||
category: selectedCategory || undefined,
|
||||
});
|
||||
setServers(result.servers);
|
||||
} catch {
|
||||
setMessage("Search failed");
|
||||
setMessageType("error");
|
||||
}
|
||||
setLoading(false);
|
||||
}, [selectedCategory]);
|
||||
|
||||
// Category filter handler
|
||||
const handleCategorySelect = useCallback(async (category: MCPServerCategory | null) => {
|
||||
setSelectedCategory(category);
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await searchServers({
|
||||
query: searchQuery,
|
||||
category: category || undefined,
|
||||
});
|
||||
setServers(result.servers);
|
||||
} catch {
|
||||
setMessage("Failed to filter");
|
||||
setMessageType("error");
|
||||
}
|
||||
setLoading(false);
|
||||
setMode("browse");
|
||||
setSelectedIndex(0);
|
||||
setScrollOffset(0);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Install handler
|
||||
const handleInstall = useCallback(async (server: MCPRegistryServer) => {
|
||||
setMode("installing");
|
||||
setMessage(`Installing ${server.name}...`);
|
||||
setMessageType("info");
|
||||
|
||||
try {
|
||||
const result = await installServer(server, { connect: true });
|
||||
if (result.success) {
|
||||
setMessage(`Installed ${server.name}${result.connected ? " and connected" : ""}`);
|
||||
setMessageType("success");
|
||||
onInstalled?.(server.id);
|
||||
} else {
|
||||
setMessage(result.error || "Installation failed");
|
||||
setMessageType("error");
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : "Installation failed");
|
||||
setMessageType("error");
|
||||
}
|
||||
|
||||
setMode("browse");
|
||||
setTimeout(() => setMessage(null), 3000);
|
||||
}, [onInstalled]);
|
||||
|
||||
// Filtered and displayed items
|
||||
const displayItems = useMemo(() => {
|
||||
if (mode === "category") {
|
||||
return categories.map((cat) => ({
|
||||
id: cat.category,
|
||||
label: `${MCP_CATEGORY_ICONS[cat.category]} ${MCP_CATEGORY_LABELS[cat.category]}`,
|
||||
count: cat.count,
|
||||
}));
|
||||
}
|
||||
return servers;
|
||||
}, [mode, servers, categories]);
|
||||
|
||||
// Visible window
|
||||
const visibleItems = useMemo(() => {
|
||||
if (mode === "category") {
|
||||
return displayItems.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
|
||||
}
|
||||
return servers.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
|
||||
}, [displayItems, servers, scrollOffset, mode]);
|
||||
|
||||
// Input handling
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (!isActive) return;
|
||||
|
||||
// Clear message on any key
|
||||
if (message && mode !== "installing") {
|
||||
setMessage(null);
|
||||
}
|
||||
|
||||
// Escape handling
|
||||
if (key.escape) {
|
||||
if (mode === "detail") {
|
||||
setMode("browse");
|
||||
setSelectedServer(null);
|
||||
} else if (mode === "search") {
|
||||
setMode("browse");
|
||||
setSearchQuery("");
|
||||
} else if (mode === "category") {
|
||||
setMode("browse");
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Installing mode - ignore input
|
||||
if (mode === "installing") return;
|
||||
|
||||
// Search mode - typing
|
||||
if (mode === "search") {
|
||||
if (key.return) {
|
||||
handleSearch(searchQuery);
|
||||
setMode("browse");
|
||||
} else if (key.backspace || key.delete) {
|
||||
setSearchQuery((prev) => prev.slice(0, -1));
|
||||
} else if (input && !key.ctrl && !key.meta) {
|
||||
setSearchQuery((prev) => prev + input);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Detail mode
|
||||
if (mode === "detail" && selectedServer) {
|
||||
if (key.return || input === "i") {
|
||||
if (!isServerInstalled(selectedServer.id)) {
|
||||
handleInstall(selectedServer);
|
||||
} else {
|
||||
setMessage("Already installed");
|
||||
setMessageType("info");
|
||||
setTimeout(() => setMessage(null), 2000);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigation
|
||||
if (key.upArrow || input === "k") {
|
||||
setSelectedIndex((prev) => {
|
||||
const newIndex = Math.max(0, prev - 1);
|
||||
if (newIndex < scrollOffset) {
|
||||
setScrollOffset(newIndex);
|
||||
}
|
||||
return newIndex;
|
||||
});
|
||||
} else if (key.downArrow || input === "j") {
|
||||
const maxIndex = (mode === "category" ? categories.length : servers.length) - 1;
|
||||
setSelectedIndex((prev) => {
|
||||
const newIndex = Math.min(maxIndex, prev + 1);
|
||||
if (newIndex >= scrollOffset + MAX_VISIBLE) {
|
||||
setScrollOffset(newIndex - MAX_VISIBLE + 1);
|
||||
}
|
||||
return newIndex;
|
||||
});
|
||||
} else if (key.return) {
|
||||
// Select item
|
||||
if (mode === "category") {
|
||||
const cat = categories[selectedIndex];
|
||||
if (cat) {
|
||||
handleCategorySelect(cat.category);
|
||||
}
|
||||
} else {
|
||||
const server = servers[selectedIndex];
|
||||
if (server) {
|
||||
setSelectedServer(server);
|
||||
setMode("detail");
|
||||
}
|
||||
}
|
||||
} else if (input === "/") {
|
||||
setMode("search");
|
||||
setSearchQuery("");
|
||||
} else if (input === "c") {
|
||||
setMode("category");
|
||||
setSelectedIndex(0);
|
||||
setScrollOffset(0);
|
||||
} else if (input === "i" && mode === "browse") {
|
||||
const server = servers[selectedIndex];
|
||||
if (server && !isServerInstalled(server.id)) {
|
||||
handleInstall(server);
|
||||
}
|
||||
} else if (input === "r") {
|
||||
loadData();
|
||||
}
|
||||
},
|
||||
{ isActive }
|
||||
);
|
||||
|
||||
// Render loading
|
||||
if (loading && servers.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="cyan"
|
||||
paddingX={1}
|
||||
width={60}
|
||||
>
|
||||
<Box justifyContent="center" marginY={1}>
|
||||
<Text color="cyan" bold>
|
||||
MCP Server Browser
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginY={1}>
|
||||
<Text color="gray">Loading servers...</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render detail view
|
||||
if (mode === "detail" && selectedServer) {
|
||||
const installed = isServerInstalled(selectedServer.id);
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="cyan"
|
||||
paddingX={1}
|
||||
width={60}
|
||||
>
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color="cyan" bold>
|
||||
{selectedServer.name}
|
||||
</Text>
|
||||
{selectedServer.verified && (
|
||||
<Text color="green"> ✓</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Text color="gray" wrap="wrap">
|
||||
{selectedServer.description}
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray">Author: </Text>
|
||||
<Text>{selectedServer.author}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text color="gray">Category: </Text>
|
||||
<Text>
|
||||
{MCP_CATEGORY_ICONS[selectedServer.category]}{" "}
|
||||
{MCP_CATEGORY_LABELS[selectedServer.category]}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text color="gray">Package: </Text>
|
||||
<Text color="yellow">{selectedServer.package}</Text>
|
||||
</Box>
|
||||
|
||||
{selectedServer.envVars && selectedServer.envVars.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color="gray">Required env vars:</Text>
|
||||
{selectedServer.envVars.map((envVar) => (
|
||||
<Text key={envVar} color="magenta">
|
||||
{" "}${envVar}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{selectedServer.installHint && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" wrap="wrap">
|
||||
Note: {selectedServer.installHint}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{message && (
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text
|
||||
color={
|
||||
messageType === "success"
|
||||
? "green"
|
||||
: messageType === "error"
|
||||
? "red"
|
||||
: "yellow"
|
||||
}
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box justifyContent="center" marginTop={1} paddingTop={1} borderStyle="single" borderTop borderBottom={false} borderLeft={false} borderRight={false}>
|
||||
{installed ? (
|
||||
<Text color="cyan">Already installed</Text>
|
||||
) : (
|
||||
<Text>
|
||||
<Text color="green">[Enter/i]</Text> Install{" "}
|
||||
<Text color="gray">[Esc]</Text> Back
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render category view
|
||||
if (mode === "category") {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="cyan"
|
||||
paddingX={1}
|
||||
width={60}
|
||||
>
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color="cyan" bold>
|
||||
Select Category
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* All categories option */}
|
||||
<Box
|
||||
paddingX={1}
|
||||
backgroundColor={selectedIndex === -1 ? "cyan" : undefined}
|
||||
>
|
||||
<Text
|
||||
color={selectedIndex === -1 ? "black" : "white"}
|
||||
bold={selectedIndex === -1}
|
||||
>
|
||||
📋 All Categories
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{visibleItems.map((item, index) => {
|
||||
const actualIndex = index + scrollOffset;
|
||||
const isSelected = actualIndex === selectedIndex;
|
||||
const catItem = item as { id: string; label: string; count: number };
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={catItem.id}
|
||||
paddingX={1}
|
||||
backgroundColor={isSelected ? "cyan" : undefined}
|
||||
>
|
||||
<Text
|
||||
color={isSelected ? "black" : "white"}
|
||||
bold={isSelected}
|
||||
>
|
||||
{catItem.label} ({catItem.count})
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Box
|
||||
justifyContent="center"
|
||||
marginTop={1}
|
||||
paddingTop={1}
|
||||
borderStyle="single"
|
||||
borderTop
|
||||
borderBottom={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
>
|
||||
<Text color="gray">
|
||||
<Text color="cyan">[↑↓]</Text> Navigate{" "}
|
||||
<Text color="cyan">[Enter]</Text> Select{" "}
|
||||
<Text color="cyan">[Esc]</Text> Back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render search mode
|
||||
if (mode === "search") {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="cyan"
|
||||
paddingX={1}
|
||||
width={60}
|
||||
>
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color="cyan" bold>
|
||||
Search MCP Servers
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box paddingX={1}>
|
||||
<Text color="gray">/ </Text>
|
||||
<Text>{searchQuery}</Text>
|
||||
<Text color="cyan">▋</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
justifyContent="center"
|
||||
marginTop={1}
|
||||
paddingTop={1}
|
||||
borderStyle="single"
|
||||
borderTop
|
||||
borderBottom={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
>
|
||||
<Text color="gray">
|
||||
<Text color="cyan">[Enter]</Text> Search{" "}
|
||||
<Text color="cyan">[Esc]</Text> Cancel
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render browse mode
|
||||
const hasMore = servers.length > scrollOffset + MAX_VISIBLE;
|
||||
const hasLess = scrollOffset > 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="cyan"
|
||||
paddingX={1}
|
||||
width={60}
|
||||
>
|
||||
<Box justifyContent="space-between" marginBottom={1}>
|
||||
<Text color="cyan" bold>
|
||||
MCP Server Browser
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
{servers.length} servers
|
||||
{selectedCategory && ` • ${MCP_CATEGORY_LABELS[selectedCategory]}`}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{hasLess && (
|
||||
<Box justifyContent="center">
|
||||
<Text color="gray">↑ more</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{visibleItems.map((server, index) => {
|
||||
const actualIndex = index + scrollOffset;
|
||||
const isSelected = actualIndex === selectedIndex;
|
||||
const installed = isServerInstalled((server as MCPRegistryServer).id);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={(server as MCPRegistryServer).id}
|
||||
paddingX={1}
|
||||
backgroundColor={isSelected ? "cyan" : undefined}
|
||||
>
|
||||
<Box width={45}>
|
||||
<Text
|
||||
color={isSelected ? "black" : installed ? "cyan" : "white"}
|
||||
bold={isSelected}
|
||||
>
|
||||
{(server as MCPRegistryServer).verified ? "✓ " : " "}
|
||||
{(server as MCPRegistryServer).name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={10} justifyContent="flex-end">
|
||||
{installed ? (
|
||||
<Text color={isSelected ? "black" : "cyan"}>installed</Text>
|
||||
) : (
|
||||
<Text color={isSelected ? "black" : "gray"}>
|
||||
{MCP_CATEGORY_ICONS[(server as MCPRegistryServer).category]}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{hasMore && (
|
||||
<Box justifyContent="center">
|
||||
<Text color="gray">↓ more</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text
|
||||
color={
|
||||
messageType === "success"
|
||||
? "green"
|
||||
: messageType === "error"
|
||||
? "red"
|
||||
: "yellow"
|
||||
}
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
justifyContent="center"
|
||||
marginTop={1}
|
||||
paddingTop={1}
|
||||
borderStyle="single"
|
||||
borderTop
|
||||
borderBottom={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
>
|
||||
<Text color="gray">
|
||||
<Text color="cyan">[/]</Text> Search{" "}
|
||||
<Text color="cyan">[c]</Text> Category{" "}
|
||||
<Text color="cyan">[i]</Text> Install{" "}
|
||||
<Text color="cyan">[Esc]</Text> Close
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default MCPBrowser;
|
||||
@@ -14,6 +14,7 @@ export { ModelSelect } from "@tui/components/ModelSelect";
|
||||
export { AgentSelect } from "@tui/components/AgentSelect";
|
||||
export { ThemeSelect } from "@tui/components/ThemeSelect";
|
||||
export { MCPSelect } from "@tui/components/MCPSelect";
|
||||
export { MCPBrowser } from "@tui/components/MCPBrowser";
|
||||
export { TodoPanel } from "@tui/components/TodoPanel";
|
||||
export { LearningModal } from "@tui/components/LearningModal";
|
||||
export { ImageAttachment, ImageAttachmentCompact } from "@tui/components/ImageAttachment";
|
||||
|
||||
139
src/types/mcp-registry.ts
Normal file
139
src/types/mcp-registry.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* MCP Registry Types
|
||||
*
|
||||
* Types for MCP server discovery and search
|
||||
*/
|
||||
|
||||
/**
|
||||
* MCP server category
|
||||
*/
|
||||
export type MCPServerCategory =
|
||||
| "database"
|
||||
| "filesystem"
|
||||
| "web"
|
||||
| "ai"
|
||||
| "dev-tools"
|
||||
| "productivity"
|
||||
| "communication"
|
||||
| "cloud"
|
||||
| "security"
|
||||
| "other";
|
||||
|
||||
/**
|
||||
* MCP server transport type for registry
|
||||
*/
|
||||
export type MCPRegistryTransport = "stdio" | "sse" | "http";
|
||||
|
||||
/**
|
||||
* MCP server entry from registry
|
||||
*/
|
||||
export interface MCPRegistryServer {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** Short description */
|
||||
description: string;
|
||||
/** Author/maintainer */
|
||||
author: string;
|
||||
/** Repository URL */
|
||||
repository: string;
|
||||
/** NPM package or command */
|
||||
package: string;
|
||||
/** Default command to run */
|
||||
command: string;
|
||||
/** Default arguments */
|
||||
args: string[];
|
||||
/** Category */
|
||||
category: MCPServerCategory;
|
||||
/** Tags for search */
|
||||
tags: string[];
|
||||
/** Transport type */
|
||||
transport: MCPRegistryTransport;
|
||||
/** Version */
|
||||
version: string;
|
||||
/** Downloads/popularity score */
|
||||
popularity: number;
|
||||
/** Whether verified by maintainers */
|
||||
verified: boolean;
|
||||
/** Installation instructions */
|
||||
installHint?: string;
|
||||
/** Environment variables needed */
|
||||
envVars?: string[];
|
||||
/** Last updated timestamp */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search result from registry
|
||||
*/
|
||||
export interface MCPSearchResult {
|
||||
/** Matching servers */
|
||||
servers: MCPRegistryServer[];
|
||||
/** Total count */
|
||||
total: number;
|
||||
/** Search query */
|
||||
query: string;
|
||||
/** Category filter applied */
|
||||
category?: MCPServerCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry source configuration
|
||||
*/
|
||||
export interface MCPRegistrySource {
|
||||
/** Source name */
|
||||
name: string;
|
||||
/** API URL or file path */
|
||||
url: string;
|
||||
/** Whether enabled */
|
||||
enabled: boolean;
|
||||
/** Last fetched timestamp */
|
||||
lastFetched?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached registry data
|
||||
*/
|
||||
export interface MCPRegistryCache {
|
||||
/** All servers from registry */
|
||||
servers: MCPRegistryServer[];
|
||||
/** Last updated timestamp */
|
||||
updatedAt: number;
|
||||
/** Source URL */
|
||||
source: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search options
|
||||
*/
|
||||
export interface MCPSearchOptions {
|
||||
/** Search query */
|
||||
query?: string;
|
||||
/** Filter by category */
|
||||
category?: MCPServerCategory;
|
||||
/** Filter by tags */
|
||||
tags?: string[];
|
||||
/** Only verified servers */
|
||||
verifiedOnly?: boolean;
|
||||
/** Sort by */
|
||||
sortBy?: "popularity" | "name" | "updated";
|
||||
/** Limit results */
|
||||
limit?: number;
|
||||
/** Offset for pagination */
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installation result
|
||||
*/
|
||||
export interface MCPInstallResult {
|
||||
/** Whether successful */
|
||||
success: boolean;
|
||||
/** Server name */
|
||||
serverName: string;
|
||||
/** Error message if failed */
|
||||
error?: string;
|
||||
/** Whether connected after install */
|
||||
connected: boolean;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export type AppMode =
|
||||
| "mode_select"
|
||||
| "mcp_select"
|
||||
| "mcp_add"
|
||||
| "mcp_browse"
|
||||
| "file_picker"
|
||||
| "provider_select"
|
||||
| "learning_prompt"
|
||||
|
||||
Reference in New Issue
Block a user