From 19b147e291d602e14b8d04361ff465697ad68968 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Mon, 2 Mar 2026 13:59:59 -0800 Subject: [PATCH 1/4] Remove MCP Support --- README.md | 66 +------ package.json | 1 - src/cli/commands/auth.ts | 158 ----------------- src/cli/commands/config.ts | 238 -------------------------- src/cli/commands/start.ts | 45 +---- src/core/agent.ts | 4 +- src/core/router.ts | 121 +------------ src/data/mcp_servers.json | 250 --------------------------- src/shared/types.ts | 11 -- src/tools/mcp-bridge.ts | 293 -------------------------------- src/tools/mcp-sdk.ts | 10 -- src/tools/registry.ts | 25 --- src/utils/mcp-catalog-loader.ts | 51 ------ 13 files changed, 8 insertions(+), 1265 deletions(-) delete mode 100644 src/data/mcp_servers.json delete mode 100644 src/tools/mcp-bridge.ts delete mode 100644 src/tools/mcp-sdk.ts delete mode 100644 src/utils/mcp-catalog-loader.ts diff --git a/README.md b/README.md index cd499a6..f850a6c 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@

-npm version +npm version   -downloads +downloads   license   @@ -70,10 +70,6 @@ Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Kiro CLI, OpenCode, and Ollam Terminal, process manager, git, file search, HTTP client, environment variables, network diagnostics, cron jobs, and system info all callable by the LLM. -### 18 MCP Servers - -Connect GitHub, Brave Search, Puppeteer, PostgreSQL, MongoDB, Redis, Elasticsearch, AWS, GCP, Cloudflare, Vercel, Atlassian, Supabase, CircleCI, Postman, Stripe, ElevenLabs, and Kaggle as external tools via the Model Context Protocol. - ### Session Logging Per-session logs accessible from the TUI. Follow live, view by index, auto-pruned after 7 days. @@ -163,7 +159,7 @@ txtcode supports **9 LLM providers** for chat mode. Configure one or more during | **HuggingFace** | _Discovered at runtime_ | Inference Providers API | | **OpenRouter** | _Discovered at runtime_ | Unified API for 100+ models | -All providers support tool calling and LLM can invoke any built-in tool or connected MCP server. +All providers support tool calling and the LLM can invoke any built-in tool. --- @@ -201,52 +197,6 @@ The primary LLM in chat mode has access to **9 built-in tools** that it can call --- -## 📟 MCP Servers - -txtcode integrates with the **Model Context Protocol** to connect external tool servers. Configure during initial setup or later via **Configuration** → **Manage MCP Servers** in the TUI. - -### Developer Tools - -| Server | Transport | Description | -| :--------------- | :-------- | :-------------------------------------------------------- | -| **GitHub** | stdio | Repos, issues, PRs, code search, Actions | -| **Brave Search** | stdio | Web, image, video, and news search | -| **Puppeteer** | stdio | Browser automation, screenshots, form filling | -| **CircleCI** | stdio | Build logs, flaky tests, pipeline status, rerun workflows | -| **Postman** | stdio | Collections, workspaces, API specs, code generation | -| **Stripe** | stdio | Customers, payments, invoices, subscriptions, refunds | -| **ElevenLabs** | stdio | Text-to-speech, voice cloning, audio transcription | -| **Kaggle** | HTTP | Datasets, notebooks, competitions, models, benchmarks | - -### Databases - -| Server | Transport | Description | -| :---------------- | :-------- | :--------------------------------------------- | -| **PostgreSQL** | stdio | Read-only SQL queries and schema inspection | -| **MongoDB** | stdio | CRUD, indexes, vector search, Atlas management | -| **Redis** | stdio | Data structures, caching, vectors, pub/sub | -| **Elasticsearch** | stdio | Index management, search queries, cluster ops | -| **Supabase** | HTTP | Postgres, Auth, Storage, Edge Functions | - -### Cloud - -| Server | Transport | Description | -| :--------------- | :-------- | :------------------------------------------------- | -| **AWS** | stdio | S3, Lambda, EKS, CDK, CloudFormation, 60+ services | -| **Google Cloud** | HTTP | BigQuery, GKE, Compute, Storage, Firebase | -| **Cloudflare** | HTTP | Workers, R2, DNS, Zero Trust, 2500+ endpoints | -| **Vercel** | HTTP | Deployments, domains, env vars, logs | - -### Productivity - -| Server | Transport | Description | -| :------------ | :-------- | :------------------------------------------------ | -| **Atlassian** | HTTP | Jira issues, Confluence pages, Compass components | - -> **stdio** = local process, **HTTP** = remote Streamable HTTP endpoint. You can also add custom MCP servers via **Configuration** → **Manage MCP Servers**. - ---- - ## 💬 Chat Commands Send these commands in any messaging app while connected: @@ -272,7 +222,6 @@ To modify settings, select **Configuration** from the main menu. Options include - Change Messaging Platform - Change Coding CLI Type - Change AI Provider -- Manage MCP Servers (add/remove/enable/disable) - Change Project Path - View Current Config @@ -327,13 +276,4 @@ Verbose and debug output goes to the log file; the terminal shows only key statu -
-MCP server connection failures - -- **stdio servers:** ensure the required npm package is installed (e.g. `npx @modelcontextprotocol/server-github`) -- **HTTP servers:** verify the token is correct via **Configuration** → **Manage MCP Servers** -- Check **View Logs** for specific error messages - -
- --- diff --git a/package.json b/package.json index 0c010bd..f7bf27d 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "dependencies": { "@anthropic-ai/sdk": "^0.74.0", "@google/generative-ai": "^0.24.1", - "@modelcontextprotocol/sdk": "^1.27.1", "@slack/bolt": "^4.6.0", "@whiskeysockets/baileys": "^7.0.0-rc.9", "botbuilder": "^4.23.3", diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index bc838ee..d5d0fed 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -8,9 +8,7 @@ import makeWASocket, { } from "@whiskeysockets/baileys"; import chalk from "chalk"; import qrcode from "qrcode-terminal"; -import type { MCPServerEntry } from "../../shared/types"; import { setApiKey, setBotToken } from "../../utils/keychain"; -import { loadMCPServersCatalog, type MCPCatalogServer } from "../../utils/mcp-catalog-loader"; import { discoverHuggingFaceModels, discoverOpenRouterModels, @@ -571,8 +569,6 @@ export async function authCommand() { console.log(chalk.green(`✅ Configured ${configuredProviders.length} provider(s)`)); console.log(); - const mcpServerEntries = await configureMCPServers(); - const platform = await showCenteredList({ message: "Select messaging platform: (Use arrow keys)", choices: [ @@ -852,7 +848,6 @@ export async function authCommand() { authorizedUser: "", configuredAt: new Date().toISOString(), - mcpServers: mcpServerEntries, }; fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); @@ -884,14 +879,6 @@ export async function authCommand() { console.log(chalk.white(` ${label}: ${provider.provider} (${provider.model})`)); }); - if (mcpServerEntries.length > 0) { - console.log(chalk.cyan("\nMCP Servers:")); - mcpServerEntries.forEach((server) => { - const status = server.enabled ? chalk.green("enabled") : chalk.gray("disabled"); - console.log(chalk.white(` ${server.id} (${server.transport}) - ${status}`)); - }); - } - console.log( chalk.cyan("\nRun ") + chalk.bold("txtcode") + @@ -905,151 +892,6 @@ export async function authCommand() { } } -async function configureMCPServers(): Promise { - const catalog = loadMCPServersCatalog(); - if (!catalog || catalog.servers.length === 0) { - return []; - } - - console.log(chalk.cyan("MCP Servers (optional)")); - console.log(); - console.log( - chalk.gray("Connect external tools to your AI provider (GitHub, databases, cloud, etc.)"), - ); - console.log(); - - const categoryNames = catalog.categories as Record; - const serversByCategory = new Map(); - for (const server of catalog.servers) { - const cat = server.category || "other"; - if (!serversByCategory.has(cat)) { - serversByCategory.set(cat, []); - } - serversByCategory.get(cat)!.push(server); - } - - const selectedServers: MCPCatalogServer[] = []; - const selectedIds = new Set(); - - let continueSelecting = true; - while (continueSelecting) { - const choices: Array<{ name: string; value: string }> = [ - { name: "Configure later", value: "__SKIP__" }, - ]; - - if (selectedServers.length > 0) { - choices[0] = { name: `← Done (${selectedServers.length} selected)`, value: "__SKIP__" }; - } - - for (const [category, servers] of serversByCategory) { - const label = categoryNames[category] || category; - for (const server of servers) { - if (selectedIds.has(server.id)) { - continue; - } - const transportTag = server.transport === "http" ? " [remote]" : ""; - choices.push({ - name: `[${label}] ${server.name} - ${server.description}${transportTag}`, - value: server.id, - }); - } - } - - if (choices.length === 1) { - console.log(chalk.yellow("\nAll available MCP servers have been selected.\n")); - break; - } - - const selected = await showCenteredList({ - message: - selectedServers.length > 0 - ? `Add another MCP server: (Use arrow keys)` - : `Select MCP server to connect: (Use arrow keys)`, - choices, - pageSize: 10, - }); - - if (selected === "__SKIP__") { - if (selectedServers.length === 0) { - console.log(); - console.log( - chalk.gray( - "You can configure MCP servers anytime from 'txtcode config' → 'Manage MCP Servers'.", - ), - ); - console.log(); - } - continueSelecting = false; - break; - } - - const server = catalog.servers.find((s) => s.id === selected); - if (!server) { - continue; - } - - selectedIds.add(server.id); - - if (server.requiresToken) { - console.log(); - const token = await showCenteredInput({ - message: server.tokenPrompt || `Enter token for ${server.name}:`, - password: true, - validate: (input) => input.length > 0 || "Token/credential is required", - }); - await setBotToken(server.keychainKey, token); - - if (server.additionalTokens) { - for (const additional of server.additionalTokens) { - console.log(); - const additionalToken = await showCenteredInput({ - message: additional.tokenPrompt, - password: !additional.tokenPrompt.toLowerCase().includes("region"), - validate: (input) => input.length > 0 || "This field is required", - }); - await setBotToken(additional.keychainKey, additionalToken); - } - } - } - - selectedServers.push(server); - console.log(); - console.log(chalk.white(" Connected servers:")); - for (const s of selectedServers) { - console.log(chalk.green(` ✅ ${s.name}`)); - } - console.log(); - } - - if (selectedServers.length > 0) { - console.log(); - console.log(chalk.green(`✅ Configured ${selectedServers.length} MCP server(s)`)); - console.log(); - } - - return selectedServers.map((server): MCPServerEntry => { - const entry: MCPServerEntry = { - id: server.id, - transport: server.transport, - enabled: true, - }; - - if (server.transport === "stdio") { - entry.command = server.command; - entry.args = server.args ? [...server.args] : undefined; - - if (server.tokenIsArg && server.keychainKey) { - entry.args = entry.args || []; - entry.args.push(`__KEYCHAIN:${server.keychainKey}__`); - } - } else { - entry.url = server.url; - } - - return entry; - }); -} - export function loadConfig(): Record | null { if (!fs.existsSync(CONFIG_FILE)) { return null; diff --git a/src/cli/commands/config.ts b/src/cli/commands/config.ts index a11bd8b..a89d04c 100644 --- a/src/cli/commands/config.ts +++ b/src/cli/commands/config.ts @@ -2,9 +2,7 @@ import fs from "fs"; import os from "os"; import path from "path"; import chalk from "chalk"; -import type { MCPServerEntry } from "../../shared/types"; import { setBotToken } from "../../utils/keychain"; -import { loadMCPServersCatalog } from "../../utils/mcp-catalog-loader"; import { centerLog, showCenteredList, showCenteredInput, showCenteredConfirm } from "../tui"; import { loadConfig } from "./auth"; @@ -37,7 +35,6 @@ export async function configCommand() { { name: "Change Messaging Platform", value: "platform" }, { name: "Change Coding CLI Type", value: "ide" }, { name: "Change AI Provider", value: "ai" }, - { name: "Manage MCP Servers", value: "mcp" }, { name: "Change Project Path", value: "project" }, { name: "View Current Config", value: "view" }, { name: "Cancel", value: "cancel" }, @@ -61,9 +58,6 @@ export async function configCommand() { case "ai": await configureAI(existingConfig); break; - case "mcp": - await configureMCP(existingConfig); - break; case "project": await configureProject(existingConfig); break; @@ -244,16 +238,6 @@ function viewConfig(config: Record) { centerLog(chalk.white("Authorized User: ") + chalk.yellow(String(config.authorizedUser))); } - const mcpServers = (config.mcpServers || []) as MCPServerEntry[]; - if (mcpServers.length > 0) { - console.log(); - centerLog(chalk.white("MCP Servers:")); - for (const server of mcpServers) { - const status = server.enabled ? chalk.green("enabled") : chalk.red("disabled"); - centerLog(chalk.gray(` ${server.id} (${server.transport}) - ${status}`)); - } - } - centerLog( chalk.white("Configured At: ") + chalk.yellow(new Date(String(config.configuredAt)).toLocaleString()), @@ -263,228 +247,6 @@ function viewConfig(config: Record) { console.log(); } -async function configureMCP(config: Record) { - console.log(); - console.log(chalk.cyan(" MCP Server Management")); - console.log(); - - const mcpServers = ((config.mcpServers || []) as MCPServerEntry[]).slice(); - - if (mcpServers.length > 0) { - console.log(chalk.white(" Currently configured:")); - for (const server of mcpServers) { - const status = server.enabled ? chalk.green("enabled") : chalk.red("disabled"); - console.log(chalk.gray(` ${server.id} (${server.transport}) – ${status}`)); - } - console.log(); - } else { - console.log(chalk.gray(" No MCP servers configured yet.")); - console.log(); - } - - const action = await showCenteredList({ - message: "What would you like to do?", - choices: [ - { name: "Add server from catalog", value: "add" }, - { name: "Add custom server", value: "custom" }, - ...(mcpServers.length > 0 - ? [ - { name: "Enable/disable a server", value: "toggle" }, - { name: "Remove a server", value: "remove" }, - ] - : []), - { name: "Cancel", value: "cancel" }, - ], - }); - - if (action === "cancel") { - return; - } - - if (action === "add") { - const catalog = loadMCPServersCatalog(); - const existingIds = new Set(mcpServers.map((s) => s.id)); - const available = catalog.servers.filter((s) => !existingIds.has(s.id)); - - if (available.length === 0) { - console.log(); - console.log(chalk.yellow(" All catalog servers are already configured.")); - console.log(); - return; - } - - const categoryNames = catalog.categories as Record; - const choices = available.map((s) => { - const label = categoryNames[s.category] || s.category; - const tag = s.transport === "http" ? " [remote]" : ""; - return { name: `[${label}] ${s.name} - ${s.description}${tag}`, value: s.id }; - }); - - const selectedId = await showCenteredList({ - message: "Select server to add:", - choices, - pageSize: 10, - }); - - const server = catalog.servers.find((s) => s.id === selectedId); - if (!server) { - return; - } - - if (server.requiresToken) { - console.log(); - const token = await showCenteredInput({ - message: server.tokenPrompt || `Enter token for ${server.name}:`, - password: true, - }); - await setBotToken(server.keychainKey, token); - - if (server.additionalTokens) { - for (const additional of server.additionalTokens) { - console.log(); - const additionalToken = await showCenteredInput({ - message: additional.tokenPrompt, - password: !additional.tokenPrompt.toLowerCase().includes("region"), - }); - await setBotToken(additional.keychainKey, additionalToken); - } - } - } - - const entry: MCPServerEntry = { - id: server.id, - transport: server.transport, - enabled: true, - }; - - if (server.transport === "stdio") { - entry.command = server.command; - entry.args = server.args ? [...server.args] : undefined; - if (server.tokenIsArg && server.keychainKey) { - entry.args = entry.args || []; - entry.args.push(`__KEYCHAIN:${server.keychainKey}__`); - } - } else { - entry.url = server.url; - } - - mcpServers.push(entry); - config.mcpServers = mcpServers; - saveConfig(config); - - console.log(); - console.log(chalk.green(` ✅ Added ${server.name}`)); - console.log(); - } else if (action === "custom") { - console.log(); - const transport = await showCenteredList({ - message: "Transport type:", - choices: [ - { name: "stdio (local command)", value: "stdio" }, - { name: "Streamable HTTP (remote URL)", value: "http" }, - ], - }); - - const id = await showCenteredInput({ - message: "Server ID (short name, no spaces):", - }); - - if (!id.trim()) { - return; - } - - const entry: MCPServerEntry = { - id: id.trim(), - transport: transport as "stdio" | "http", - enabled: true, - }; - - if (transport === "stdio") { - const command = await showCenteredInput({ - message: "Command (e.g. npx):", - }); - const argsStr = await showCenteredInput({ - message: "Arguments (space-separated):", - }); - - entry.command = command.trim(); - entry.args = argsStr.trim() ? argsStr.trim().split(/\s+/) : undefined; - } else { - const url = await showCenteredInput({ - message: "Server URL:", - }); - entry.url = url.trim(); - } - - const hasToken = await showCenteredConfirm({ - message: "Does this server require an auth token?", - default: false, - }); - - if (hasToken) { - const token = await showCenteredInput({ - message: "Enter token:", - password: true, - }); - await setBotToken(`mcp-${id.trim()}`, token); - } - - mcpServers.push(entry); - config.mcpServers = mcpServers; - saveConfig(config); - - console.log(); - console.log(chalk.green(` ✅ Added custom server: ${id.trim()}`)); - console.log(); - } else if (action === "toggle") { - const choices = mcpServers.map((s) => ({ - name: `${s.id} - currently ${s.enabled ? "enabled" : "disabled"}`, - value: s.id, - })); - - const selectedId = await showCenteredList({ - message: "Select server to toggle:", - choices, - }); - - const server = mcpServers.find((s) => s.id === selectedId); - if (server) { - server.enabled = !server.enabled; - config.mcpServers = mcpServers; - saveConfig(config); - - console.log(); - const status = server.enabled ? "enabled" : "disabled"; - console.log(chalk.green(` ✅ ${server.id} is now ${status}`)); - console.log(); - } - } else if (action === "remove") { - const choices = mcpServers.map((s) => ({ - name: `${s.id} (${s.transport})`, - value: s.id, - })); - - const selectedId = await showCenteredList({ - message: "Select server to remove:", - choices, - }); - - const confirm = await showCenteredConfirm({ - message: `Remove ${selectedId}?`, - default: false, - }); - - if (confirm) { - config.mcpServers = mcpServers.filter((s) => s.id !== selectedId); - saveConfig(config); - - console.log(); - console.log(chalk.green(` ✅ Removed ${selectedId}`)); - console.log(); - } - } -} - function saveConfig(config: Record) { config.updatedAt = new Date().toISOString(); fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index f18081b..15f9dfd 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -7,9 +7,8 @@ import { TeamsBot } from "../../platforms/teams"; import { TelegramBot } from "../../platforms/telegram"; import { WhatsAppBot } from "../../platforms/whatsapp"; import { logger } from "../../shared/logger"; -import type { Config, MCPServerEntry } from "../../shared/types"; +import type { Config } from "../../shared/types"; import { getApiKey, getBotToken } from "../../utils/keychain"; -import { loadMCPServersCatalog } from "../../utils/mcp-catalog-loader"; import { centerLog } from "../tui"; import { loadConfig } from "./auth"; @@ -26,44 +25,6 @@ async function loadPlatformToken(name: string, keychainKey: string): Promise { - if (!mcpServers || mcpServers.length === 0) { - return; - } - - const catalog = loadMCPServersCatalog(); - const catalogMap = new Map(catalog.servers.map((s) => [s.id, s])); - - for (const server of mcpServers) { - if (!server.enabled) { - continue; - } - - const catalogEntry = catalogMap.get(server.id); - if (!catalogEntry) { - continue; - } - - if (catalogEntry.keychainKey) { - const token = await getBotToken(catalogEntry.keychainKey); - if (token) { - const envKey = `MCP_TOKEN_${server.id.toUpperCase().replace(/-/g, "_")}`; - process.env[envKey] = token; - } - } - - if (catalogEntry.additionalTokens) { - for (const additional of catalogEntry.additionalTokens) { - const token = await getBotToken(additional.keychainKey); - if (token) { - const envKey = `MCP_TOKEN_${additional.keychainKey.toUpperCase().replace(/-/g, "_")}`; - process.env[envKey] = token; - } - } - } - } -} - export async function startCommand(_options: { daemon?: boolean }) { const rawConfig = loadConfig(); @@ -135,13 +96,11 @@ export async function startCommand(_options: { daemon?: boolean }) { process.env.CLAUDE_MODEL = config.claudeModel || "sonnet"; process.env.GEMINI_MODEL = config.geminiModel || ""; - await loadMCPTokens(config.mcpServers || []); - const agent = new AgentCore(); await agent.init(); const shutdownHandler = async () => { - logger.debug("Shutting down MCP servers..."); + logger.debug("Shutting down agent..."); await agent.shutdown(); process.exit(0); }; diff --git a/src/core/agent.ts b/src/core/agent.ts index fc08d90..c9873d4 100644 --- a/src/core/agent.ts +++ b/src/core/agent.ts @@ -21,11 +21,11 @@ export class AgentCore { } async init(): Promise { - await this.router.initMCP(); + // Reserved for future initialization } async shutdown(): Promise { - await this.router.shutdownMCP(); + // Reserved for future cleanup } private loadAuthorizedUser() { diff --git a/src/core/router.ts b/src/core/router.ts index 7c6845f..b28b29d 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -15,19 +15,17 @@ import { processWithOpenAI } from "../providers/openai"; import { processWithOpenRouter } from "../providers/openrouter"; import { processWithXAI } from "../providers/xai"; import { logger } from "../shared/logger"; -import { IDEAdapter, MCPServerEntry, ModelInfo } from "../shared/types"; +import { IDEAdapter, ModelInfo } from "../shared/types"; import { CronTool } from "../tools/cron"; import { EnvTool } from "../tools/env"; import { GitTool } from "../tools/git"; import { HttpTool } from "../tools/http"; -import { MCPBridge, MCPServerConfig } from "../tools/mcp-bridge"; import { NetworkTool } from "../tools/network"; import { ProcessTool } from "../tools/process"; import { ToolRegistry } from "../tools/registry"; import { SearchTool } from "../tools/search"; import { SysinfoTool } from "../tools/sysinfo"; import { TerminalTool } from "../tools/terminal"; -import { loadMCPServersCatalog } from "../utils/mcp-catalog-loader"; import { ContextManager } from "./context-manager"; export const AVAILABLE_ADAPTERS = [ @@ -49,7 +47,6 @@ export class Router { private contextManager: ContextManager; private pendingHandoff: string | null = null; private currentAbortController: AbortController | null = null; - private mcpBridge: MCPBridge; constructor() { this.provider = process.env.AI_PROVIDER || "anthropic"; @@ -67,7 +64,6 @@ export class Router { this.toolRegistry.register(new CronTool()); this.toolRegistry.register(new SysinfoTool()); - this.mcpBridge = new MCPBridge(); this.contextManager = new ContextManager(); const ideType = process.env.IDE_TYPE || ""; @@ -77,121 +73,6 @@ export class Router { this.restoreAdapterModel(ideType); } - async initMCP(): Promise { - const mcpServers = this.loadMCPConfig(); - if (!mcpServers || mcpServers.length === 0) { - return; - } - - const catalog = loadMCPServersCatalog(); - const catalogMap = new Map(catalog.servers.map((s) => [s.id, s])); - - const results: string[] = []; - - for (const entry of mcpServers) { - if (!entry.enabled) { - continue; - } - - try { - const catalogEntry = catalogMap.get(entry.id); - const serverConfig = this.buildMCPServerConfig(entry, catalogEntry); - - const tools = await this.mcpBridge.connect(serverConfig); - this.toolRegistry.registerMCPTools(tools); - results.push(`${entry.id}: ${tools.length} tools`); - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - logger.debug(`MCP server "${entry.id}" failed to connect: ${msg}`); - } - } - - if (results.length > 0) { - logger.info(`MCP servers connected (${results.join(", ")})`); - logger.info(`Total tools: ${this.toolRegistry.getMCPToolCount()} MCP + built-in`); - } - } - - private buildMCPServerConfig( - entry: MCPServerEntry, - catalogEntry?: { - keychainKey?: string; - tokenEnvKey?: string; - additionalTokens?: Array<{ keychainKey: string; tokenEnvKey: string }>; - }, - ): MCPServerConfig { - const config: MCPServerConfig = { - id: entry.id, - name: entry.id, - transport: entry.transport, - }; - - if (entry.transport === "stdio") { - config.command = entry.command; - - const resolvedArgs = (entry.args || []).map((arg) => { - const keychainMatch = arg.match(/^__KEYCHAIN:(.+)__$/); - if (keychainMatch) { - return process.env[`MCP_TOKEN_${entry.id.toUpperCase().replace(/-/g, "_")}`] || arg; - } - return arg; - }); - config.args = resolvedArgs; - - const env: Record = { ...entry.env }; - if (catalogEntry?.tokenEnvKey) { - const envKey = `MCP_TOKEN_${entry.id.toUpperCase().replace(/-/g, "_")}`; - const token = process.env[envKey]; - if (token) { - env[catalogEntry.tokenEnvKey] = token; - } - } - if (catalogEntry?.additionalTokens) { - for (const additional of catalogEntry.additionalTokens) { - const envKey = `MCP_TOKEN_${additional.keychainKey.toUpperCase().replace(/-/g, "_")}`; - const token = process.env[envKey]; - if (token) { - env[additional.tokenEnvKey] = token; - } - } - } - config.env = env; - } else { - config.url = entry.url; - - const tokenEnvKey = `MCP_TOKEN_${entry.id.toUpperCase().replace(/-/g, "_")}`; - const token = process.env[tokenEnvKey]; - if (token) { - config.headers = { Authorization: `Bearer ${token}` }; - } - } - - return config; - } - - private loadMCPConfig(): MCPServerEntry[] | null { - try { - const fs = require("fs"); - const path = require("path"); - const os = require("os"); - const configPath = path.join(os.homedir(), ".txtcode", "config.json"); - if (!fs.existsSync(configPath)) { - return null; - } - const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); - return config.mcpServers || null; - } catch { - return null; - } - } - - async shutdownMCP(): Promise { - for (const serverId of this.mcpBridge.getConnectedServerIds()) { - this.toolRegistry.removeMCPTools(serverId); - } - await this.mcpBridge.disconnectAll(); - } - private createAdapter(ideType: string): IDEAdapter { switch (ideType) { case "claude-code": diff --git a/src/data/mcp_servers.json b/src/data/mcp_servers.json deleted file mode 100644 index f1ef127..0000000 --- a/src/data/mcp_servers.json +++ /dev/null @@ -1,250 +0,0 @@ -{ - "servers": [ - { - "id": "github", - "name": "GitHub", - "description": "Repos, issues, PRs, code search, Actions (73 tools)", - "category": "developer", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-github"], - "requiresToken": true, - "tokenPrompt": "Enter GitHub Personal Access Token:", - "tokenEnvKey": "GITHUB_PERSONAL_ACCESS_TOKEN", - "keychainKey": "mcp-github" - }, - { - "id": "brave-search", - "name": "Brave Search", - "description": "Web, image, video, and news search via Brave", - "category": "developer", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-brave-search"], - "requiresToken": true, - "tokenPrompt": "Enter Brave Search API Key:", - "tokenEnvKey": "BRAVE_API_KEY", - "keychainKey": "mcp-brave-search" - }, - { - "id": "puppeteer", - "name": "Puppeteer", - "description": "Browser automation, screenshots, form filling, JS execution", - "category": "developer", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-puppeteer"], - "requiresToken": false, - "keychainKey": "mcp-puppeteer" - }, - { - "id": "postgres", - "name": "PostgreSQL", - "description": "Read-only SQL queries and schema inspection", - "category": "database", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-postgres"], - "requiresToken": true, - "tokenPrompt": "Enter PostgreSQL connection string (e.g. postgresql://user:pass@localhost/db):", - "tokenEnvKey": "POSTGRES_CONNECTION_STRING", - "keychainKey": "mcp-postgres", - "tokenIsArg": true - }, - { - "id": "mongodb", - "name": "MongoDB", - "description": "CRUD operations, indexes, vector search, Atlas management", - "category": "database", - "transport": "stdio", - "command": "npx", - "args": ["-y", "mongodb-mcp-server"], - "requiresToken": true, - "tokenPrompt": "Enter MongoDB connection string (e.g. mongodb://localhost:27017/mydb):", - "tokenEnvKey": "MONGODB_URI", - "keychainKey": "mcp-mongodb" - }, - { - "id": "redis", - "name": "Redis", - "description": "Data structures, caching, vectors, pub/sub", - "category": "database", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@redis/mcp-server"], - "requiresToken": true, - "tokenPrompt": "Enter Redis connection URL (e.g. redis://localhost:6379):", - "tokenEnvKey": "REDIS_URL", - "keychainKey": "mcp-redis" - }, - { - "id": "elasticsearch", - "name": "Elasticsearch", - "description": "Index management, search queries, cluster operations", - "category": "database", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@elastic/mcp-server-elasticsearch"], - "requiresToken": true, - "tokenPrompt": "Enter Elasticsearch URL (e.g. https://localhost:9200):", - "tokenEnvKey": "ES_URL", - "keychainKey": "mcp-elasticsearch", - "additionalTokens": [ - { - "tokenPrompt": "Enter Elasticsearch API Key:", - "tokenEnvKey": "ES_API_KEY", - "keychainKey": "mcp-elasticsearch-apikey" - } - ] - }, - { - "id": "aws", - "name": "AWS", - "description": "S3, Lambda, EKS, CDK, CloudFormation, and 60+ AWS services", - "category": "cloud", - "transport": "stdio", - "command": "uvx", - "args": ["awslabs.core-mcp-server@latest"], - "requiresToken": true, - "tokenPrompt": "Enter AWS Access Key ID:", - "tokenEnvKey": "AWS_ACCESS_KEY_ID", - "keychainKey": "mcp-aws-access-key", - "additionalTokens": [ - { - "tokenPrompt": "Enter AWS Secret Access Key:", - "tokenEnvKey": "AWS_SECRET_ACCESS_KEY", - "keychainKey": "mcp-aws-secret-key" - }, - { - "tokenPrompt": "Enter AWS Region (e.g. us-east-1):", - "tokenEnvKey": "AWS_REGION", - "keychainKey": "mcp-aws-region" - } - ] - }, - { - "id": "gcp", - "name": "Google Cloud", - "description": "BigQuery, GKE, Compute Engine, Cloud Storage, Firebase", - "category": "cloud", - "transport": "http", - "url": "https://cloud.google.com/mcp", - "requiresToken": true, - "tokenPrompt": "Enter Google Cloud Access Token:", - "keychainKey": "mcp-gcp" - }, - { - "id": "cloudflare", - "name": "Cloudflare", - "description": "Workers, R2, DNS, Zero Trust, 2500+ API endpoints", - "category": "cloud", - "transport": "http", - "url": "https://api.cloudflare.com/mcp", - "requiresToken": true, - "tokenPrompt": "Enter Cloudflare API Token:", - "keychainKey": "mcp-cloudflare" - }, - { - "id": "vercel", - "name": "Vercel", - "description": "Deployments, domains, environment variables, logs", - "category": "cloud", - "transport": "http", - "url": "https://mcp.vercel.com", - "requiresToken": true, - "tokenPrompt": "Enter Vercel Access Token:", - "keychainKey": "mcp-vercel" - }, - { - "id": "atlassian", - "name": "Atlassian", - "description": "Jira issues, Confluence pages, Compass components", - "category": "productivity", - "transport": "http", - "url": "https://mcp.atlassian.com/v1/mcp", - "requiresToken": true, - "tokenPrompt": "Enter Atlassian API Token:", - "keychainKey": "mcp-atlassian" - }, - { - "id": "supabase", - "name": "Supabase", - "description": "Postgres, Auth, Storage, Edge Functions", - "category": "database", - "transport": "http", - "url": "https://mcp.supabase.com/mcp", - "requiresToken": true, - "tokenPrompt": "Enter Supabase Access Token:", - "keychainKey": "mcp-supabase" - }, - { - "id": "circleci", - "name": "CircleCI", - "description": "Build logs, flaky tests, pipeline status, config validation, rerun workflows", - "category": "developer", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@circleci/mcp-server-circleci@latest"], - "requiresToken": true, - "tokenPrompt": "Enter CircleCI Personal API Token:", - "tokenEnvKey": "CIRCLECI_TOKEN", - "keychainKey": "mcp-circleci" - }, - { - "id": "postman", - "name": "Postman", - "description": "Collections, workspaces, environments, API specs, monitors, code generation", - "category": "developer", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@postman/postman-mcp-server"], - "requiresToken": true, - "tokenPrompt": "Enter Postman API Key:", - "tokenEnvKey": "POSTMAN_API_KEY", - "keychainKey": "mcp-postman" - }, - { - "id": "stripe", - "name": "Stripe", - "description": "Customers, payments, invoices, subscriptions, refunds, products, pricing", - "category": "developer", - "transport": "stdio", - "command": "npx", - "args": ["-y", "@stripe/mcp", "--tools=all"], - "requiresToken": true, - "tokenPrompt": "Enter Stripe Secret Key (sk_...):", - "tokenEnvKey": "STRIPE_SECRET_KEY", - "keychainKey": "mcp-stripe" - }, - { - "id": "elevenlabs", - "name": "ElevenLabs", - "description": "Text-to-speech, voice cloning, audio transcription, sound effects", - "category": "developer", - "transport": "stdio", - "command": "uvx", - "args": ["elevenlabs-mcp"], - "requiresToken": true, - "tokenPrompt": "Enter ElevenLabs API Key:", - "tokenEnvKey": "ELEVENLABS_API_KEY", - "keychainKey": "mcp-elevenlabs" - }, - { - "id": "kaggle", - "name": "Kaggle", - "description": "Datasets, notebooks, competitions, models, benchmarks", - "category": "developer", - "transport": "http", - "url": "https://www.kaggle.com/mcp", - "requiresToken": true, - "tokenPrompt": "Enter Kaggle API Token (KGAT...):", - "keychainKey": "mcp-kaggle" - } - ], - "categories": { - "developer": "Developer Tools", - "database": "Databases", - "cloud": "Cloud & Infrastructure", - "productivity": "Productivity" - } -} diff --git a/src/shared/types.ts b/src/shared/types.ts index a1afdc6..8a3e7df 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -4,16 +4,6 @@ export interface Message { timestamp: Date; } -export interface MCPServerEntry { - id: string; - transport: "stdio" | "http"; - command?: string; - args?: string[]; - env?: Record; - url?: string; - enabled: boolean; -} - export interface Config { aiProvider: string; aiModel?: string; @@ -35,7 +25,6 @@ export interface Config { adapterModels?: { [adapterName: string]: string; }; - mcpServers?: MCPServerEntry[]; } export interface ModelInfo { diff --git a/src/tools/mcp-bridge.ts b/src/tools/mcp-bridge.ts deleted file mode 100644 index 82c9cc4..0000000 --- a/src/tools/mcp-bridge.ts +++ /dev/null @@ -1,293 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { logger } from "../shared/logger"; -import { Client, StdioClientTransport, StreamableHTTPClientTransport } from "./mcp-sdk"; -import { Tool, ToolDefinition, ToolResult, ParameterProperty, ParameterType } from "./types"; - -function getPackageVersion(): string { - try { - const packageJsonPath = path.join(__dirname, "../../package.json"); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - return packageJson.version || "0.1.0"; - } catch { - return "0.1.0"; - } -} - -interface MCPTransport { - start(): Promise; - close(): Promise; - send(message: unknown): Promise; - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: unknown) => void; -} - -interface MCPToolSchema { - inputSchema: { - type: "object"; - properties?: Record; - required?: string[]; - [key: string]: unknown; - }; - name: string; - description?: string; -} - -interface MCPClient { - connect(transport: MCPTransport): Promise; - listTools(): Promise<{ tools: MCPToolSchema[] }>; - callTool(params: { name: string; arguments: Record }): Promise<{ - content: Array<{ type: string; text?: string }>; - isError?: boolean; - }>; - close(): Promise; -} - -export interface MCPServerConfig { - id: string; - name: string; - transport: "stdio" | "http"; - command?: string; - args?: string[]; - env?: Record; - url?: string; - headers?: Record; -} - -interface MCPConnection { - client: MCPClient; - transport: MCPTransport; - tools: MCPToolAdapter[]; - config: MCPServerConfig; -} - -export class MCPBridge { - private connections: Map = new Map(); - - async connect(config: MCPServerConfig): Promise { - if (this.connections.has(config.id)) { - logger.debug(`MCP server "${config.id}" is already connected`); - return this.connections.get(config.id)!.tools; - } - - const client: MCPClient = new Client( - { name: "txtcode", version: getPackageVersion() }, - { capabilities: {} }, - ); - - let transport: MCPTransport; - - if (config.transport === "stdio") { - if (!config.command) { - throw new Error(`MCP server "${config.id}" requires a command for stdio transport`); - } - - transport = new StdioClientTransport({ - command: config.command, - args: config.args, - env: { ...process.env, ...config.env } as Record, - stderr: "pipe", - }); - } else { - if (!config.url) { - throw new Error(`MCP server "${config.id}" requires a URL for HTTP transport`); - } - - const requestInit: RequestInit = {}; - if (config.headers) { - requestInit.headers = config.headers; - } - - transport = new StreamableHTTPClientTransport(new URL(config.url), { requestInit }); - } - - await client.connect(transport); - - let toolsResult: { tools: MCPToolSchema[] }; - try { - toolsResult = await client.listTools(); - } catch (error) { - try { - await client.close(); - } catch { - // Best-effort cleanup - } - try { - await transport.close(); - } catch { - // Best-effort cleanup - } - throw error; - } - - const tools: MCPToolAdapter[] = toolsResult.tools.map( - (mcpTool) => new MCPToolAdapter(config.id, mcpTool, client), - ); - - this.connections.set(config.id, { client, transport, tools, config }); - - logger.debug(`MCP server "${config.name}" connected: ${tools.length} tool(s) discovered`); - - return tools; - } - - getTools(): MCPToolAdapter[] { - const allTools: MCPToolAdapter[] = []; - for (const conn of this.connections.values()) { - allTools.push(...conn.tools); - } - return allTools; - } - - getToolsForServer(serverId: string): MCPToolAdapter[] { - return this.connections.get(serverId)?.tools ?? []; - } - - getConnectedServerIds(): string[] { - return Array.from(this.connections.keys()); - } - - async disconnect(serverId: string): Promise { - const conn = this.connections.get(serverId); - if (!conn) { - return; - } - - try { - await conn.client.close(); - } catch { - // Best-effort client cleanup - } - try { - await conn.transport.close(); - } catch (error) { - logger.debug(`Error disconnecting MCP server "${serverId}": ${error}`); - } - - this.connections.delete(serverId); - logger.debug(`MCP server "${serverId}" disconnected`); - } - - async disconnectAll(): Promise { - const ids = Array.from(this.connections.keys()); - await Promise.allSettled(ids.map((id) => this.disconnect(id))); - } -} - -export class MCPToolAdapter implements Tool { - name: string; - description: string; - private serverId: string; - private mcpTool: MCPToolSchema; - private client: MCPClient; - - constructor(serverId: string, mcpTool: MCPToolSchema, client: MCPClient) { - this.serverId = serverId; - this.mcpTool = mcpTool; - this.name = `${serverId}_${mcpTool.name}`; - this.description = mcpTool.description || `MCP tool from ${serverId}`; - this.client = client; - } - - getDefinition(): ToolDefinition { - const mcpProps = this.mcpTool.inputSchema.properties || {}; - const mcpRequired = this.mcpTool.inputSchema.required || []; - - const properties: Record = {}; - for (const [key, schema] of Object.entries(mcpProps)) { - properties[key] = convertMCPSchemaToProperty(schema as Record); - } - - return { - name: this.name, - description: this.description, - parameters: { - type: "object", - properties, - required: mcpRequired, - }, - }; - } - - async execute(args: Record, signal?: AbortSignal): Promise { - if (signal?.aborted) { - return { toolCallId: "", output: "MCP tool execution aborted", isError: true }; - } - - try { - const result = await this.client.callTool({ - name: this.mcpTool.name, - arguments: args, - }); - - const content = result.content as Array<{ type: string; text?: string }>; - const output = content - .map((item) => { - if (item.type === "text" && item.text) { - return item.text; - } - return JSON.stringify(item); - }) - .join("\n"); - - return { - toolCallId: "", - output: output || "(no output)", - isError: result.isError === true, - metadata: { mcpServer: this.serverId, mcpTool: this.mcpTool.name }, - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - toolCallId: "", - output: `MCP tool error (${this.serverId}/${this.mcpTool.name}): ${message}`, - isError: true, - metadata: { mcpServer: this.serverId, mcpTool: this.mcpTool.name }, - }; - } - } -} - -const TYPE_MAP: Record = { - string: "string", - number: "number", - integer: "number", - boolean: "boolean", - object: "object", - array: "array", -}; - -function convertMCPSchemaToProperty(schema: Record): ParameterProperty { - const type = TYPE_MAP[String(schema.type || "string")] || "string"; - const prop: ParameterProperty = { - type, - description: (schema.description as string) || "", - }; - - if (schema.enum) { - prop.enum = schema.enum as string[]; - } - - if (schema.items && type === "array") { - const items = schema.items as Record; - prop.items = { type: TYPE_MAP[String(items.type || "string")] || "string" }; - } - - if (schema.properties && type === "object") { - const nested: Record = {}; - for (const [k, v] of Object.entries(schema.properties as Record)) { - nested[k] = convertMCPSchemaToProperty(v as Record); - } - prop.properties = nested; - if (schema.required) { - prop.required = schema.required as string[]; - } - } - - if (schema.default !== undefined) { - prop.default = schema.default; - } - - return prop; -} diff --git a/src/tools/mcp-sdk.ts b/src/tools/mcp-sdk.ts deleted file mode 100644 index 739e6bb..0000000 --- a/src/tools/mcp-sdk.ts +++ /dev/null @@ -1,10 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const sdkClient = require("@modelcontextprotocol/sdk/client"); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const sdkStdio = require("@modelcontextprotocol/sdk/client/stdio.js"); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const sdkHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js"); - -export const Client = sdkClient.Client; -export const StdioClientTransport = sdkStdio.StdioClientTransport; -export const StreamableHTTPClientTransport = sdkHttp.StreamableHTTPClientTransport; diff --git a/src/tools/registry.ts b/src/tools/registry.ts index fe2d9bd..e659a03 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -2,36 +2,11 @@ import { Tool, ToolCall, ToolDefinition, ToolResult, ParameterProperty } from ". export class ToolRegistry { private tools: Map = new Map(); - private mcpToolNames: Set = new Set(); register(tool: Tool): void { this.tools.set(tool.name, tool); } - registerMCPTools(tools: Tool[]): void { - for (const tool of tools) { - this.tools.set(tool.name, tool); - this.mcpToolNames.add(tool.name); - } - } - - removeMCPTools(prefix: string): void { - const toRemove: string[] = []; - for (const name of this.mcpToolNames) { - if (name.startsWith(prefix + "_")) { - toRemove.push(name); - } - } - for (const name of toRemove) { - this.tools.delete(name); - this.mcpToolNames.delete(name); - } - } - - getMCPToolCount(): number { - return this.mcpToolNames.size; - } - getDefinitions(): ToolDefinition[] { return Array.from(this.tools.values()).map((t) => t.getDefinition()); } diff --git a/src/utils/mcp-catalog-loader.ts b/src/utils/mcp-catalog-loader.ts deleted file mode 100644 index 69ccddd..0000000 --- a/src/utils/mcp-catalog-loader.ts +++ /dev/null @@ -1,51 +0,0 @@ -import fs from "fs"; -import path from "path"; - -export interface MCPAdditionalToken { - tokenPrompt: string; - tokenEnvKey: string; - keychainKey: string; -} - -export interface MCPCatalogServer { - id: string; - name: string; - description: string; - category: string; - transport: "stdio" | "http"; - command?: string; - args?: string[]; - url?: string; - requiresToken: boolean; - tokenPrompt?: string; - tokenEnvKey?: string; - keychainKey: string; - tokenIsArg?: boolean; - additionalTokens?: MCPAdditionalToken[]; -} - -export interface MCPServersCatalog { - servers: MCPCatalogServer[]; - categories: Record; -} - -let cachedCatalog: MCPServersCatalog | null = null; - -export function loadMCPServersCatalog(): MCPServersCatalog { - if (cachedCatalog) { - return cachedCatalog; - } - - try { - const catalogPath = path.join(__dirname, "..", "data", "mcp_servers.json"); - const data = fs.readFileSync(catalogPath, "utf-8"); - cachedCatalog = JSON.parse(data) as MCPServersCatalog; - return cachedCatalog; - } catch { - return { servers: [], categories: {} }; - } -} - -export function clearMCPCatalogCache(): void { - cachedCatalog = null; -} From 1cfe360a0cac8d30fedcec6deeb23b22147bb190 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Mon, 2 Mar 2026 14:00:52 -0800 Subject: [PATCH 2/4] lint --- src/cli/commands/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/config.ts b/src/cli/commands/config.ts index a89d04c..1d26f1f 100644 --- a/src/cli/commands/config.ts +++ b/src/cli/commands/config.ts @@ -3,7 +3,7 @@ import os from "os"; import path from "path"; import chalk from "chalk"; import { setBotToken } from "../../utils/keychain"; -import { centerLog, showCenteredList, showCenteredInput, showCenteredConfirm } from "../tui"; +import { centerLog, showCenteredList, showCenteredInput } from "../tui"; import { loadConfig } from "./auth"; const CONFIG_DIR = path.join(os.homedir(), ".txtcode"); From 122b64b44f61edcb414701cf2cf0b5140fa6dea4 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Mon, 2 Mar 2026 14:01:12 -0800 Subject: [PATCH 3/4] format --- README.md | 6 +++--- src/cli/commands/auth.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f850a6c..126dbc8 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,9 @@ docker run -it \ txtcode ``` -| Flag | Purpose | -| :--- | :------ | -| `-v $(pwd):/workspace` | Mounts your project directory into the container | +| Flag | Purpose | +| :----------------------------- | :-------------------------------------------------- | +| `-v $(pwd):/workspace` | Mounts your project directory into the container | | `-v ~/.txtcode:/root/.txtcode` | Persists config, session data, and logs across runs | > **Note:** API keys are stored securely via your OS keychain when running natively. Inside Docker, txtcode uses an encrypted file-based fallback (`TXTCODE_DOCKER=1` is set automatically). You can also pass keys as environment variables with `-e`, e.g. `-e ANTHROPIC_API_KEY=sk-...`. diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index d5d0fed..1e0197d 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -847,7 +847,6 @@ export async function authCommand() { idePort: 3000, authorizedUser: "", configuredAt: new Date().toISOString(), - }; fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); From b90c2a9d889277c5fe602ccef8aea3a86d018f93 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Mon, 2 Mar 2026 14:03:33 -0800 Subject: [PATCH 4/4] Fix tests --- test/unit/mcp-bridge.test.ts | 415 --------------------------- test/unit/mcp-catalog-loader.test.ts | 168 ----------- test/unit/mcp-registry.test.ts | 141 --------- 3 files changed, 724 deletions(-) delete mode 100644 test/unit/mcp-bridge.test.ts delete mode 100644 test/unit/mcp-catalog-loader.test.ts delete mode 100644 test/unit/mcp-registry.test.ts diff --git a/test/unit/mcp-bridge.test.ts b/test/unit/mcp-bridge.test.ts deleted file mode 100644 index 690011b..0000000 --- a/test/unit/mcp-bridge.test.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const { mockConnect, mockListTools, mockCallTool, mockClose, mockTransportClose } = vi.hoisted( - () => ({ - mockConnect: vi.fn(), - mockListTools: vi.fn(), - mockCallTool: vi.fn(), - mockClose: vi.fn(), - mockTransportClose: vi.fn(), - }), -); - -vi.mock("../../src/shared/logger", () => ({ - logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() }, -})); - -vi.mock("../../src/tools/mcp-sdk", () => ({ - Client: class { - connect = mockConnect; - listTools = mockListTools; - callTool = mockCallTool; - close = mockClose; - }, - StdioClientTransport: class { - close = mockTransportClose; - constructor(public params: unknown) {} - }, - StreamableHTTPClientTransport: class { - close = mockTransportClose; - constructor( - public url: URL, - public opts?: unknown, - ) {} - }, -})); - -import { MCPBridge, MCPToolAdapter } from "../../src/tools/mcp-bridge"; - -describe("MCPBridge", () => { - let bridge: MCPBridge; - - beforeEach(() => { - vi.clearAllMocks(); - bridge = new MCPBridge(); - - mockListTools.mockResolvedValue({ - tools: [ - { - name: "create_issue", - description: "Create a GitHub issue", - inputSchema: { - type: "object", - properties: { - title: { type: "string", description: "Issue title" }, - body: { type: "string", description: "Issue body" }, - }, - required: ["title"], - }, - }, - { - name: "list_repos", - description: "List repositories", - inputSchema: { - type: "object", - properties: { - owner: { type: "string", description: "Repository owner" }, - }, - required: ["owner"], - }, - }, - ], - }); - }); - - describe("connect", () => { - it("connects to a stdio server and discovers tools", async () => { - const tools = await bridge.connect({ - id: "github", - name: "GitHub", - transport: "stdio", - command: "npx", - args: ["-y", "@modelcontextprotocol/server-github"], - }); - - expect(mockConnect).toHaveBeenCalledOnce(); - expect(mockListTools).toHaveBeenCalledOnce(); - expect(tools).toHaveLength(2); - expect(tools[0].name).toBe("github_create_issue"); - expect(tools[1].name).toBe("github_list_repos"); - }); - - it("connects to an HTTP server", async () => { - const tools = await bridge.connect({ - id: "supabase", - name: "Supabase", - transport: "http", - url: "https://mcp.supabase.com/mcp", - headers: { Authorization: "Bearer test-token" }, - }); - - expect(mockConnect).toHaveBeenCalledOnce(); - expect(tools).toHaveLength(2); - }); - - it("throws when stdio server has no command", async () => { - await expect( - bridge.connect({ - id: "bad", - name: "Bad", - transport: "stdio", - }), - ).rejects.toThrow("requires a command"); - }); - - it("throws when HTTP server has no URL", async () => { - await expect( - bridge.connect({ - id: "bad", - name: "Bad", - transport: "http", - }), - ).rejects.toThrow("requires a URL"); - }); - - it("returns existing tools if server is already connected", async () => { - const first = await bridge.connect({ - id: "github", - name: "GitHub", - transport: "stdio", - command: "npx", - }); - - const second = await bridge.connect({ - id: "github", - name: "GitHub", - transport: "stdio", - command: "npx", - }); - - expect(mockConnect).toHaveBeenCalledOnce(); - expect(first).toBe(second); - }); - }); - - describe("getTools / getToolsForServer / getConnectedServerIds", () => { - it("returns all tools across servers", async () => { - await bridge.connect({ - id: "github", - name: "GitHub", - transport: "stdio", - command: "npx", - }); - - mockListTools.mockResolvedValueOnce({ - tools: [ - { - name: "web_search", - description: "Search the web", - inputSchema: { type: "object", properties: {}, required: [] }, - }, - ], - }); - - await bridge.connect({ - id: "brave", - name: "Brave Search", - transport: "stdio", - command: "npx", - }); - - expect(bridge.getTools()).toHaveLength(3); - expect(bridge.getToolsForServer("github")).toHaveLength(2); - expect(bridge.getToolsForServer("brave")).toHaveLength(1); - expect(bridge.getConnectedServerIds()).toEqual(["github", "brave"]); - }); - - it("returns empty array for unknown server", () => { - expect(bridge.getToolsForServer("nonexistent")).toEqual([]); - }); - }); - - describe("disconnect / disconnectAll", () => { - it("disconnects a single server", async () => { - await bridge.connect({ - id: "github", - name: "GitHub", - transport: "stdio", - command: "npx", - }); - - await bridge.disconnect("github"); - expect(mockTransportClose).toHaveBeenCalledOnce(); - expect(bridge.getConnectedServerIds()).toEqual([]); - }); - - it("handles disconnect of unknown server gracefully", async () => { - await bridge.disconnect("nonexistent"); - expect(mockTransportClose).not.toHaveBeenCalled(); - }); - - it("disconnects all servers", async () => { - await bridge.connect({ - id: "github", - name: "GitHub", - transport: "stdio", - command: "npx", - }); - - mockListTools.mockResolvedValueOnce({ tools: [] }); - await bridge.connect({ - id: "brave", - name: "Brave", - transport: "stdio", - command: "npx", - }); - - await bridge.disconnectAll(); - expect(mockTransportClose).toHaveBeenCalledTimes(2); - expect(bridge.getConnectedServerIds()).toEqual([]); - }); - }); -}); - -describe("MCPToolAdapter", () => { - const mockClient = { - connect: vi.fn(), - listTools: vi.fn(), - callTool: vi.fn(), - close: vi.fn(), - }; - - const sampleTool = { - name: "create_issue", - description: "Create a GitHub issue", - inputSchema: { - type: "object" as const, - properties: { - title: { type: "string", description: "Issue title" }, - body: { type: "string", description: "Issue body" }, - labels: { - type: "array", - description: "Labels", - items: { type: "string" }, - }, - metadata: { - type: "object", - description: "Extra metadata", - properties: { - priority: { type: "number", description: "Priority level" }, - }, - required: ["priority"], - }, - }, - required: ["title"], - }, - }; - - let adapter: MCPToolAdapter; - - beforeEach(() => { - vi.clearAllMocks(); - adapter = new MCPToolAdapter("github", sampleTool, mockClient as never); - }); - - describe("name and description", () => { - it("prefixes tool name with server ID", () => { - expect(adapter.name).toBe("github_create_issue"); - }); - - it("uses MCP tool description", () => { - expect(adapter.description).toBe("Create a GitHub issue"); - }); - - it("falls back to default description", () => { - const noDesc = new MCPToolAdapter( - "gh", - { ...sampleTool, description: undefined }, - mockClient as never, - ); - expect(noDesc.description).toBe("MCP tool from gh"); - }); - }); - - describe("getDefinition", () => { - it("produces valid ToolDefinition with correct name", () => { - const def = adapter.getDefinition(); - expect(def.name).toBe("github_create_issue"); - expect(def.description).toBe("Create a GitHub issue"); - expect(def.parameters.type).toBe("object"); - expect(def.parameters.required).toEqual(["title"]); - }); - - it("converts string properties", () => { - const def = adapter.getDefinition(); - expect(def.parameters.properties.title).toEqual({ - type: "string", - description: "Issue title", - }); - }); - - it("converts array properties with items", () => { - const def = adapter.getDefinition(); - expect(def.parameters.properties.labels).toEqual({ - type: "array", - description: "Labels", - items: { type: "string" }, - }); - }); - - it("converts nested object properties", () => { - const def = adapter.getDefinition(); - expect(def.parameters.properties.metadata.type).toBe("object"); - expect(def.parameters.properties.metadata.properties).toBeDefined(); - expect(def.parameters.properties.metadata.properties!.priority).toEqual({ - type: "number", - description: "Priority level", - }); - expect(def.parameters.properties.metadata.required).toEqual(["priority"]); - }); - - it("handles empty inputSchema properties", () => { - const empty = new MCPToolAdapter( - "test", - { - name: "noop", - inputSchema: { type: "object" }, - }, - mockClient as never, - ); - const def = empty.getDefinition(); - expect(def.parameters.properties).toEqual({}); - expect(def.parameters.required).toEqual([]); - }); - }); - - describe("execute", () => { - it("calls MCP server and returns text output", async () => { - mockClient.callTool.mockResolvedValue({ - content: [{ type: "text", text: "Issue #42 created" }], - isError: false, - }); - - const result = await adapter.execute({ title: "Bug fix" }); - expect(mockClient.callTool).toHaveBeenCalledWith({ - name: "create_issue", - arguments: { title: "Bug fix" }, - }); - expect(result.output).toBe("Issue #42 created"); - expect(result.isError).toBe(false); - expect(result.metadata).toEqual({ - mcpServer: "github", - mcpTool: "create_issue", - }); - }); - - it("joins multiple text content blocks", async () => { - mockClient.callTool.mockResolvedValue({ - content: [ - { type: "text", text: "Line 1" }, - { type: "text", text: "Line 2" }, - ], - }); - - const result = await adapter.execute({}); - expect(result.output).toBe("Line 1\nLine 2"); - }); - - it("JSON-stringifies non-text content", async () => { - mockClient.callTool.mockResolvedValue({ - content: [{ type: "image", data: "base64data", mimeType: "image/png" }], - }); - - const result = await adapter.execute({}); - expect(result.output).toContain("image"); - expect(result.output).toContain("base64data"); - }); - - it("returns (no output) for empty content", async () => { - mockClient.callTool.mockResolvedValue({ content: [] }); - - const result = await adapter.execute({}); - expect(result.output).toBe("(no output)"); - }); - - it("returns isError=true when MCP reports error", async () => { - mockClient.callTool.mockResolvedValue({ - content: [{ type: "text", text: "Not found" }], - isError: true, - }); - - const result = await adapter.execute({}); - expect(result.isError).toBe(true); - expect(result.output).toBe("Not found"); - }); - - it("handles exceptions from callTool", async () => { - mockClient.callTool.mockRejectedValue(new Error("Connection lost")); - - const result = await adapter.execute({}); - expect(result.isError).toBe(true); - expect(result.output).toContain("Connection lost"); - expect(result.output).toContain("github/create_issue"); - }); - - it("returns abort result when signal is already aborted", async () => { - const controller = new AbortController(); - controller.abort(); - - const result = await adapter.execute({}, controller.signal); - expect(result.isError).toBe(true); - expect(result.output).toBe("MCP tool execution aborted"); - expect(mockClient.callTool).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit/mcp-catalog-loader.test.ts b/test/unit/mcp-catalog-loader.test.ts deleted file mode 100644 index 3199383..0000000 --- a/test/unit/mcp-catalog-loader.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import fs from "fs"; -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { loadMCPServersCatalog, clearMCPCatalogCache } from "../../src/utils/mcp-catalog-loader"; - -vi.mock("../../src/shared/logger", () => ({ - logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() }, -})); - -describe("MCP Catalog Loader", () => { - beforeEach(() => { - clearMCPCatalogCache(); - }); - - it("loads the catalog from mcp_servers.json", () => { - const catalog = loadMCPServersCatalog(); - - expect(catalog.servers).toBeDefined(); - expect(Array.isArray(catalog.servers)).toBe(true); - expect(catalog.servers.length).toBe(18); - expect(catalog.categories).toBeDefined(); - }); - - it("returns all 18 expected servers", () => { - const catalog = loadMCPServersCatalog(); - const ids = catalog.servers.map((s) => s.id); - - expect(ids).toContain("github"); - expect(ids).toContain("brave-search"); - expect(ids).toContain("puppeteer"); - expect(ids).toContain("postgres"); - expect(ids).toContain("mongodb"); - expect(ids).toContain("redis"); - expect(ids).toContain("elasticsearch"); - expect(ids).toContain("aws"); - expect(ids).toContain("gcp"); - expect(ids).toContain("cloudflare"); - expect(ids).toContain("vercel"); - expect(ids).toContain("atlassian"); - expect(ids).toContain("supabase"); - expect(ids).toContain("circleci"); - expect(ids).toContain("postman"); - expect(ids).toContain("stripe"); - expect(ids).toContain("elevenlabs"); - expect(ids).toContain("kaggle"); - }); - - it("stdio servers have command field", () => { - const catalog = loadMCPServersCatalog(); - const stdioServers = catalog.servers.filter((s) => s.transport === "stdio"); - - expect(stdioServers.length).toBeGreaterThan(0); - for (const server of stdioServers) { - expect(server.command).toBeDefined(); - expect(typeof server.command).toBe("string"); - } - }); - - it("HTTP servers have url field", () => { - const catalog = loadMCPServersCatalog(); - const httpServers = catalog.servers.filter((s) => s.transport === "http"); - - expect(httpServers.length).toBeGreaterThan(0); - for (const server of httpServers) { - expect(server.url).toBeDefined(); - expect(typeof server.url).toBe("string"); - } - }); - - it("every server has a keychainKey", () => { - const catalog = loadMCPServersCatalog(); - - for (const server of catalog.servers) { - expect(server.keychainKey).toBeDefined(); - expect(server.keychainKey.startsWith("mcp-")).toBe(true); - } - }); - - it("every server has a category", () => { - const catalog = loadMCPServersCatalog(); - const validCategories = Object.keys(catalog.categories); - - for (const server of catalog.servers) { - expect(server.category).toBeDefined(); - expect(validCategories).toContain(server.category); - } - }); - - it("caches the catalog on subsequent calls", () => { - const readSpy = vi.spyOn(fs, "readFileSync"); - - const first = loadMCPServersCatalog(); - const second = loadMCPServersCatalog(); - - expect(first).toBe(second); - expect(readSpy).toHaveBeenCalledTimes(1); - - readSpy.mockRestore(); - }); - - it("clearMCPCatalogCache resets the cache", () => { - const readSpy = vi.spyOn(fs, "readFileSync"); - - loadMCPServersCatalog(); - clearMCPCatalogCache(); - loadMCPServersCatalog(); - - expect(readSpy).toHaveBeenCalledTimes(2); - - readSpy.mockRestore(); - }); - - it("returns empty catalog when file does not exist", () => { - clearMCPCatalogCache(); - const readSpy = vi.spyOn(fs, "readFileSync").mockImplementation(() => { - throw new Error("ENOENT"); - }); - - const catalog = loadMCPServersCatalog(); - expect(catalog.servers).toEqual([]); - expect(catalog.categories).toEqual({}); - - readSpy.mockRestore(); - }); - - describe("GitHub server entry", () => { - it("has correct configuration", () => { - const catalog = loadMCPServersCatalog(); - const github = catalog.servers.find((s) => s.id === "github"); - - expect(github).toBeDefined(); - expect(github!.transport).toBe("stdio"); - expect(github!.command).toBe("npx"); - expect(github!.args).toContain("@modelcontextprotocol/server-github"); - expect(github!.requiresToken).toBe(true); - expect(github!.tokenEnvKey).toBe("GITHUB_PERSONAL_ACCESS_TOKEN"); - expect(github!.keychainKey).toBe("mcp-github"); - expect(github!.category).toBe("developer"); - }); - }); - - describe("Supabase server entry (HTTP)", () => { - it("has correct configuration", () => { - const catalog = loadMCPServersCatalog(); - const supabase = catalog.servers.find((s) => s.id === "supabase"); - - expect(supabase).toBeDefined(); - expect(supabase!.transport).toBe("http"); - expect(supabase!.url).toContain("mcp.supabase.com"); - expect(supabase!.requiresToken).toBe(true); - expect(supabase!.keychainKey).toBe("mcp-supabase"); - }); - }); - - describe("AWS server entry (multi-token)", () => { - it("has additional tokens configured", () => { - const catalog = loadMCPServersCatalog(); - const aws = catalog.servers.find((s) => s.id === "aws"); - - expect(aws).toBeDefined(); - expect(aws!.additionalTokens).toBeDefined(); - expect(aws!.additionalTokens!.length).toBeGreaterThanOrEqual(2); - - const envKeys = aws!.additionalTokens!.map((t) => t.tokenEnvKey); - expect(envKeys).toContain("AWS_SECRET_ACCESS_KEY"); - expect(envKeys).toContain("AWS_REGION"); - }); - }); -}); diff --git a/test/unit/mcp-registry.test.ts b/test/unit/mcp-registry.test.ts deleted file mode 100644 index 8baff49..0000000 --- a/test/unit/mcp-registry.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { ToolRegistry } from "../../src/tools/registry"; -import type { Tool, ToolDefinition, ToolResult } from "../../src/tools/types"; - -function makeFakeTool(name: string): Tool { - return { - name, - description: `Tool: ${name}`, - getDefinition(): ToolDefinition { - return { - name, - description: `Tool: ${name}`, - parameters: { - type: "object", - properties: { - input: { type: "string", description: "Input" }, - }, - required: ["input"], - }, - }; - }, - async execute(_args: Record): Promise { - return { toolCallId: "", output: `executed ${name}`, isError: false }; - }, - }; -} - -describe("ToolRegistry MCP methods", () => { - let registry: ToolRegistry; - - beforeEach(() => { - registry = new ToolRegistry(); - }); - - describe("registerMCPTools", () => { - it("registers multiple MCP tools at once", () => { - const tools = [makeFakeTool("github_create_issue"), makeFakeTool("github_list_repos")]; - - registry.registerMCPTools(tools); - expect(registry.getMCPToolCount()).toBe(2); - - const defs = registry.getDefinitions(); - expect(defs.map((d) => d.name)).toContain("github_create_issue"); - expect(defs.map((d) => d.name)).toContain("github_list_repos"); - }); - - it("MCP tools are executable via execute()", async () => { - registry.registerMCPTools([makeFakeTool("brave_web_search")]); - - const result = await registry.execute("brave_web_search", { input: "test" }); - expect(result.output).toBe("executed brave_web_search"); - expect(result.isError).toBe(false); - }); - - it("MCP tools coexist with built-in tools", () => { - registry.register(makeFakeTool("terminal")); - registry.register(makeFakeTool("git")); - registry.registerMCPTools([ - makeFakeTool("github_create_issue"), - makeFakeTool("brave_web_search"), - ]); - - const defs = registry.getDefinitions(); - expect(defs).toHaveLength(4); - expect(registry.getMCPToolCount()).toBe(2); - }); - }); - - describe("removeMCPTools", () => { - it("removes tools matching the prefix", () => { - registry.registerMCPTools([ - makeFakeTool("github_create_issue"), - makeFakeTool("github_list_repos"), - makeFakeTool("brave_web_search"), - ]); - - registry.removeMCPTools("github"); - expect(registry.getMCPToolCount()).toBe(1); - - const defs = registry.getDefinitions(); - expect(defs.map((d) => d.name)).toEqual(["brave_web_search"]); - }); - - it("does not affect built-in tools", () => { - registry.register(makeFakeTool("git")); - registry.registerMCPTools([makeFakeTool("github_create_issue")]); - - registry.removeMCPTools("github"); - - const defs = registry.getDefinitions(); - expect(defs.map((d) => d.name)).toEqual(["git"]); - expect(registry.getMCPToolCount()).toBe(0); - }); - - it("no-ops when prefix matches nothing", () => { - registry.registerMCPTools([makeFakeTool("github_create_issue")]); - registry.removeMCPTools("nonexistent"); - expect(registry.getMCPToolCount()).toBe(1); - }); - }); - - describe("getMCPToolCount", () => { - it("returns 0 when no MCP tools registered", () => { - registry.register(makeFakeTool("terminal")); - expect(registry.getMCPToolCount()).toBe(0); - }); - }); - - describe("MCP tools in provider-specific formats", () => { - beforeEach(() => { - registry.registerMCPTools([makeFakeTool("github_create_issue")]); - }); - - it("formats MCP tools for Anthropic", () => { - const defs = registry.getDefinitionsForProvider("anthropic"); - expect(defs).toHaveLength(1); - const def = defs[0] as Record; - expect(def.name).toBe("github_create_issue"); - expect(def.input_schema).toBeDefined(); - }); - - it("formats MCP tools for OpenAI", () => { - const defs = registry.getDefinitionsForProvider("openai"); - expect(defs).toHaveLength(1); - const def = defs[0] as Record; - expect(def.type).toBe("function"); - const fn = def.function as Record; - expect(fn.name).toBe("github_create_issue"); - expect(fn.parameters).toBeDefined(); - }); - - it("formats MCP tools for Gemini", () => { - const defs = registry.getDefinitionsForProvider("gemini"); - expect(defs).toHaveLength(1); - const wrapper = defs[0] as Record; - const declarations = wrapper.functionDeclarations as Array>; - expect(declarations).toHaveLength(1); - expect(declarations[0].name).toBe("github_create_issue"); - }); - }); -});