@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types ā generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
2,578 lines ⢠107 kB
JavaScript
/**
* MCP CLI Commands for NeuroLink
* Implements comprehensive MCP server management commands
* Part of Phase 4.2 - MCP CLI Commands
*
* Enhanced in Phase 8.39.0 with:
* - neurolink mcp servers - List all MCP servers
* - neurolink mcp tools - List all available tools
* - neurolink mcp discover - Enhanced tool discovery
* - neurolink mcp create-server - Scaffold a new MCP server
* - neurolink mcp annotate - Add/update tool annotations
*/
import { createExternalServerInfo } from "../../utils/mcpDefaults.js";
import { NeuroLink } from "../../neurolink.js";
import { logger } from "../../utils/logger.js";
import chalk from "chalk";
import ora from "ora";
import fs from "fs";
import path from "path";
import { getAnnotationSummary, validateAnnotations, mergeAnnotations, inferAnnotations, } from "../../mcp/toolAnnotations.js";
import { withTimeout, ErrorFactory } from "../../utils/errorHandling.js";
// Using MCPCommandArgs from types/cli.ts
/**
* Response interface for MCP status information returned from the NeuroLink SDK.
* This interface represents the raw status data that gets converted to CLI-friendly format.
*
* @interface MCPStatusResponse
* @since 7.6.1
*
* @example
* ```typescript
* const status: MCPStatusResponse = {
* autoDiscoveredServers: [
* {
* name: "filesystem",
* id: "fs-server-001",
* status: "connected",
* source: "claude-desktop"
* }
* ],
* mcpInitialized: true,
* totalServers: 3,
* availableServers: 2
* };
* ```
*/
/**
* Popular MCP servers registry
*/
const POPULAR_MCP_SERVERS = {
filesystem: {
command: "npx",
args: [
"-y",
"@modelcontextprotocol/server-filesystem",
"/path/to/allowed/files",
],
transport: "stdio",
description: "File system operations (read, write, create, list directories)",
},
github: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: { GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}" },
transport: "stdio",
description: "GitHub repository management and file operations",
},
postgres: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-postgres"],
env: { DATABASE_URL: "${DATABASE_URL}" },
transport: "stdio",
description: "PostgreSQL database query and management",
},
sqlite: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-sqlite", "/path/to/database.db"],
transport: "stdio",
description: "SQLite database operations and queries",
},
brave: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-brave-search"],
env: { BRAVE_API_KEY: "${BRAVE_API_KEY}" },
transport: "stdio",
description: "Brave Search API for web search capabilities",
},
puppeteer: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-puppeteer"],
transport: "stdio",
description: "Web scraping and browser automation",
},
git: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-git"],
transport: "stdio",
description: "Git repository operations and version control",
},
memory: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-memory"],
transport: "stdio",
description: "Persistent memory and knowledge storage",
},
bitbucket: {
command: "npx",
args: ["-y", "@nexus2520/bitbucket-mcp-server"],
env: {
BITBUCKET_USERNAME: "${BITBUCKET_USERNAME}",
BITBUCKET_TOKEN: "${BITBUCKET_TOKEN}",
BITBUCKET_BASE_URL: "${BITBUCKET_BASE_URL}",
},
transport: "stdio",
description: "Bitbucket repository management and development workflows",
},
};
const MCP_STATUS_TIMEOUT_MS = 30_000;
/** Where `externalServerManager` looks for manually configured servers. */
const MCP_CONFIG_FILENAME = ".mcp-config.json";
/**
* Explain an empty server list in terms of what the user's directory actually
* looks like.
*
* "Found 0 MCP servers" plus two generic tips reads as an error with no cause:
* the common case is simply that this project has no `.mcp-config.json`, and
* saying so turns a dead end into a next step. When the file *is* present, the
* cause is different ā it parsed but declared nothing usable ā so the advice
* has to be different too, otherwise we'd send the user to create a file they
* already have.
*/
function explainEmptyServerList() {
const configPath = path.join(process.cwd(), MCP_CONFIG_FILENAME);
const configExists = fs.existsSync(configPath);
const lines = [];
if (configExists) {
lines.push(chalk.blue(`š Found ${MCP_CONFIG_FILENAME}, but it declares no usable servers.`), chalk.blue(` Check the "mcpServers" object in ${configPath} ā each entry needs a "command" (stdio) or "url" (http/sse/websocket).`));
}
else {
lines.push(chalk.blue(`š No ${MCP_CONFIG_FILENAME} in this directory (${process.cwd()}).`), chalk.blue(" That file is where NeuroLink reads manually configured servers from."));
}
// Neither `install` nor `add` persists anything: both route through
// NeuroLink.addInMemoryMCPServer(), which registers into the in-process tool
// registry and is gone when the process exits. Nothing in the CLI writes
// MCP_CONFIG_FILENAME. Saying otherwise sends the user looking for a file
// that was never created ā so the persistent path is spelled out as the
// manual edit it actually is.
lines.push("", chalk.blue("Next steps:"), chalk.blue(configExists
? ` ⢠Add an entry to the "mcpServers" object in ${MCP_CONFIG_FILENAME} ā editing that file is the only way to configure a server permanently.`
: ` ⢠Create ${MCP_CONFIG_FILENAME} here with an "mcpServers" object ā each entry needs a "command" (stdio) or "url" (http/sse/websocket). Writing that file is the only way to configure a server permanently.`), chalk.blue(" ⢠neurolink mcp install <server> register a popular server for the current run only"), chalk.blue(" ⢠neurolink mcp discover discover tools from servers already reachable"), chalk.blue(" ⢠neurolink mcp --help all MCP subcommands"));
return lines.join("\n");
}
/**
* MCP CLI command factory
*/
export class MCPCommandFactory {
/**
* Create the main MCP command with subcommands
*/
static createMCPCommands() {
return {
command: "mcp <subcommand>",
describe: "Manage Model Context Protocol (MCP) servers",
builder: (yargs) => {
return yargs
.command("list", "List configured MCP servers with status", (yargs) => this.buildListOptions(yargs), (argv) => this.executeList(argv))
.command("servers", "List all MCP servers with detailed status", (yargs) => this.buildServersOptions(yargs), (argv) => this.executeServers(argv))
.command("tools", "List all available tools across MCP servers", (yargs) => this.buildToolsOptions(yargs), (argv) => this.executeTools(argv))
.command("discover", "Discover tools from MCP servers with annotations", (yargs) => this.buildDiscoverToolsOptions(yargs), (argv) => this.executeDiscoverTools(argv))
.command("create-server <name>", "Create a new MCP server scaffold", (yargs) => this.buildCreateServerOptions(yargs), (argv) => this.executeCreateServer(argv))
.command("annotate", "Add or update annotations for MCP tools", (yargs) => this.buildAnnotateOptions(yargs), (argv) => this.executeAnnotate(argv))
.command("install <server>", "Install popular MCP servers", (yargs) => this.buildInstallOptions(yargs), (argv) => this.executeInstall(argv))
.command("add <name> <command>", "Add custom MCP server configuration", (yargs) => this.buildAddOptions(yargs), (argv) => this.executeAdd(argv))
.command("test [server]", "Test connectivity to MCP servers", (yargs) => this.buildTestOptions(yargs), (argv) => this.executeTest(argv))
.command("exec <server> <tool>", "Execute tools from MCP servers", (yargs) => this.buildExecOptions(yargs), (argv) => this.executeExec(argv))
.command("remove <server>", "Remove MCP server configuration", (yargs) => this.buildRemoveOptions(yargs), (argv) => this.executeRemove(argv))
.command(this.createRegistryCommand())
.option("format", {
choices: ["table", "json", "compact"],
default: "table",
description: "Output format",
})
.option("output", {
type: "string",
description: "Save output to file",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.option("debug", {
type: "boolean",
default: false,
description: "Enable debug output",
})
.demandCommand(1, "Please specify an MCP subcommand")
.help();
},
handler: () => {
// No-op handler as subcommands handle everything
},
};
}
/**
* Create discover command (top-level command)
*/
static createDiscoverCommand() {
return {
command: "discover",
describe: "Auto-discover MCP servers from various sources",
builder: (yargs) => {
return yargs
.option("auto-install", {
type: "boolean",
default: false,
description: "Automatically install discovered servers",
})
.option("source", {
choices: ["claude-desktop", "vscode", "all"],
default: "all",
description: "Source to discover servers from",
})
.option("format", {
choices: ["table", "json", "compact"],
default: "table",
description: "Output format",
})
.option("quiet", {
type: "boolean",
alias: "q",
default: false,
description: "Suppress non-essential output",
})
.example("neurolink mcp discover", "Discover MCP servers from all sources")
.example("neurolink mcp discover --source claude-desktop", "Discover from Claude Desktop only")
.example("neurolink mcp discover --auto-install", "Discover and auto-install servers");
},
handler: async (argv) => await MCPCommandFactory.executeDiscover(argv),
};
}
/**
* Build options for list command
*/
static buildListOptions(yargs) {
return yargs
.option("status", {
type: "boolean",
default: false,
description: "Check server connection status",
})
.option("detailed", {
type: "boolean",
default: false,
description: "Show detailed server information",
})
.example("neurolink mcp list", "List all configured MCP servers")
.example("neurolink mcp list --status", "List servers with connection status")
.example("neurolink mcp list --detailed", "Show detailed server information");
}
/**
* Build options for install command
*/
static buildInstallOptions(yargs) {
return yargs
.positional("server", {
type: "string",
description: "Server name to install from popular registry",
choices: Object.keys(POPULAR_MCP_SERVERS),
demandOption: true,
})
.option("transport", {
choices: ["stdio", "sse", "websocket"],
default: "stdio",
description: "Transport type for MCP communication",
})
.option("args", {
type: "array",
description: "Additional arguments for the server command",
})
.option("env", {
type: "string",
description: "Environment variables as JSON string",
})
.example("neurolink mcp install filesystem", "Install filesystem MCP server")
.example("neurolink mcp install github", "Install GitHub MCP server")
.example("neurolink mcp install postgres", "Install PostgreSQL MCP server");
}
/**
* Build options for add command
*/
static buildAddOptions(yargs) {
return yargs
.positional("name", {
type: "string",
description: "Name for the custom MCP server",
demandOption: true,
})
.positional("command", {
type: "string",
description: "Command to execute the MCP server",
demandOption: true,
})
.option("transport", {
choices: ["stdio", "sse", "websocket"],
default: "stdio",
description: "Transport type for MCP communication",
})
.option("args", {
type: "array",
description: "Arguments for the server command",
})
.option("env", {
type: "string",
description: "Environment variables as JSON string",
})
.example("neurolink mcp add my-server node", "Add custom Node.js MCP server")
.example("neurolink mcp add api-server python", "Add custom Python MCP server");
}
/**
* Build options for test command
*/
static buildTestOptions(yargs) {
return yargs
.positional("server", {
type: "string",
description: "Server name to test (optional - tests all if not specified)",
})
.option("timeout", {
type: "number",
default: 10000,
description: "Test timeout in milliseconds",
})
.example("neurolink mcp test", "Test all configured servers")
.example("neurolink mcp test filesystem", "Test specific server")
.example("neurolink mcp test --timeout 5000", "Test with custom timeout");
}
/**
* Build options for exec command
*/
static buildExecOptions(yargs) {
return yargs
.positional("server", {
type: "string",
description: "MCP server name",
demandOption: true,
})
.positional("tool", {
type: "string",
description: "Tool name to execute",
demandOption: true,
})
.option("params", {
type: "string",
description: "Tool parameters as JSON string",
})
.example("neurolink mcp exec filesystem read_file", "Execute read_file tool")
.example("neurolink mcp exec github list_repos", "Execute GitHub list_repos tool");
}
/**
* Build options for remove command
*/
static buildRemoveOptions(yargs) {
return yargs
.positional("server", {
type: "string",
description: "Server name to remove",
demandOption: true,
})
.option("force", {
type: "boolean",
default: false,
description: "Force removal without confirmation",
})
.example("neurolink mcp remove filesystem", "Remove filesystem server")
.example("neurolink mcp remove old-server --force", "Force remove without confirmation");
}
/**
* Execute list command
*/
static async executeList(argv) {
try {
const spinner = argv.quiet ? null : ora("Loading MCP servers...").start();
// Get configured servers from NeuroLink
const sdk = new NeuroLink();
const mcpStatus = await sdk.getMCPStatus();
const allServers = await sdk.listMCPServers();
if (spinner) {
spinner.succeed(`Found ${allServers.length} MCP servers`);
}
if (allServers.length === 0) {
logger.always(chalk.yellow("No MCP servers configured."));
logger.always(explainEmptyServerList());
return;
}
// Format and display results
if (argv.format === "json") {
logger.always(JSON.stringify(mcpStatus, null, 2));
}
else if (argv.format === "compact") {
const allServers = await sdk.listMCPServers();
allServers.forEach((server) => {
const status = server.status === "connected" ? chalk.green("ā") : chalk.red("ā");
logger.always(`${status} ${server.name} - ${server.description || "No description"}`);
});
}
else {
// Table format
logger.always(chalk.bold("\nš§ MCP Servers:\n"));
const allServers = await sdk.listMCPServers();
for (const server of allServers) {
const status = server.status === "connected"
? chalk.green("CONNECTED")
: chalk.red("DISCONNECTED");
logger.always(`${chalk.cyan(server.name)} ${status}`);
logger.always(` Command: ${server.command || "Unknown"}`);
logger.always(` Tools: ${server.tools?.length || 0} available`);
if (argv.detailed && server.tools) {
server.tools.forEach((tool) => {
logger.always(` ⢠${tool.name}: ${tool.description}`);
});
}
if (server.error) {
logger.always(` ${chalk.red("Error:")} ${server.error}`);
}
logger.always();
}
}
}
catch (_error) {
logger.error(chalk.red(`ā List command failed: ${_error.message}`));
process.exit(1);
}
}
/**
* Execute install command
*/
static async executeInstall(argv) {
try {
const serverName = argv.server;
if (!serverName) {
logger.error(chalk.red("ā Server name is required"));
process.exit(1);
}
const serverConfig = POPULAR_MCP_SERVERS[serverName];
if (!serverConfig) {
logger.error(chalk.red(`ā Unknown server: ${serverName}`));
logger.always(chalk.blue("Available servers:"));
Object.keys(POPULAR_MCP_SERVERS).forEach((name) => {
logger.always(` ⢠${name}: ${POPULAR_MCP_SERVERS[name].description}`);
});
process.exit(1);
}
const spinner = argv.quiet
? null
: ora(`Installing ${serverName} MCP server...`).start();
// Parse environment variables if provided
let env = serverConfig.env;
if (argv.env) {
try {
const parsedEnv = JSON.parse(argv.env);
env = { ...env, ...parsedEnv };
}
catch (error) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red("ā Invalid JSON in env parameter"));
logger.error(chalk.red(`Error details: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
const serverInfo = createExternalServerInfo({
...serverConfig,
id: serverName,
name: serverName,
env,
});
// Add server to NeuroLink - direct usage, zero transformations!
const sdk = new NeuroLink();
await sdk.addInMemoryMCPServer(serverName, serverInfo);
if (spinner) {
spinner.succeed(chalk.green(`ā
Successfully installed ${serverName} MCP server`));
}
// Display configuration info
logger.always(chalk.bold("\nš Server Configuration:"));
logger.always(`Name: ${serverInfo.name}`);
logger.always(`Command: ${serverInfo.command}`);
if (serverInfo.args?.length) {
logger.always(`Args: ${serverInfo.args.join(" ")}`);
}
if (serverInfo.env) {
logger.always(`Environment: ${Object.keys(serverInfo.env).length} variables`);
}
logger.always(`Transport: ${serverInfo.transport}`);
logger.always(`Description: ${serverInfo.description}`);
// Test connection
logger.always(chalk.blue("\nš Testing connection..."));
try {
const rawStatus = await sdk.getMCPStatus();
const status = rawStatus;
const installedServer = status.externalMCPServers?.find((s) => s.name === serverName);
if (installedServer?.status === "connected") {
logger.always(chalk.green("ā
Server connected successfully"));
if (installedServer.tools?.length) {
logger.always(`š ļø Available tools: ${installedServer.tools.length}`);
}
}
else {
logger.always(chalk.yellow("ā ļø Server installed but not connected"));
if (installedServer?.error) {
logger.always(chalk.red(`Error: ${installedServer.error}`));
}
}
}
catch (testError) {
logger.always(chalk.yellow("ā ļø Could not test connection"));
logger.debug(`Test connection _error: ${testError instanceof Error ? testError.message : String(testError)}`);
}
}
catch (_error) {
logger.error(chalk.red(`ā Install command failed: ${_error.message}`));
process.exit(1);
}
}
/**
* Execute add command
*/
static async executeAdd(argv) {
try {
const name = argv.name;
if (!name) {
logger.error(chalk.red("ā Server name is required"));
process.exit(1);
}
const command = argv.command;
if (!command) {
logger.error(chalk.red("ā Command is required"));
process.exit(1);
}
const spinner = argv.quiet
? null
: ora(`Adding custom MCP server: ${name}...`).start();
// Parse environment variables if provided
let env;
if (argv.env) {
try {
env = JSON.parse(argv.env);
}
catch (error) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red("ā Invalid JSON in env parameter"));
logger.error(chalk.red(`Error details: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
const serverInfo = createExternalServerInfo({
id: name,
name,
command,
args: argv.args,
env,
transport: argv.transport || "stdio",
description: command,
});
// Add server to NeuroLink using MCPServerInfo directly
const sdk = new NeuroLink();
await sdk.addInMemoryMCPServer(name, serverInfo);
if (spinner) {
spinner.succeed(chalk.green(`ā
Successfully added ${name} MCP server`));
}
// Display configuration
logger.always(chalk.bold("\nš Server Configuration:"));
logger.always(`Name: ${serverInfo.name}`);
logger.always(`Command: ${serverInfo.command}`);
if (serverInfo.args?.length) {
logger.always(`Args: ${serverInfo.args.join(" ")}`);
}
if (serverInfo.env) {
logger.always(`Environment: ${Object.keys(serverInfo.env).length} variables`);
}
logger.always(`Transport: ${serverInfo.transport}`);
}
catch (_error) {
logger.error(chalk.red(`ā Add command failed: ${_error.message}`));
process.exit(1);
}
}
/**
* Execute test command
*/
static async executeTest(argv) {
try {
const targetServer = argv.server;
const spinner = argv.quiet
? null
: ora("Testing MCP server connections...").start();
const sdk = new NeuroLink();
await this.getMCPStatusWithTimeout(sdk, Number(argv.timeout) || MCP_STATUS_TIMEOUT_MS);
let serversToTest = await sdk.listMCPServers();
if (targetServer) {
serversToTest = serversToTest.filter((s) => s.name === targetServer);
if (serversToTest.length === 0) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red(`ā Server not found: ${targetServer}`));
process.exit(1);
}
}
if (spinner) {
spinner.succeed(`Testing ${serversToTest.length} servers`);
}
// Display test results
logger.always(chalk.bold("\nš§Ŗ MCP Server Test Results:\n"));
for (const server of serversToTest) {
const status = server.status === "connected"
? chalk.green("ā
CONNECTED")
: chalk.red("ā DISCONNECTED");
logger.always(`${server.name}: ${status}`);
if (server.status === "connected") {
logger.always(` Tools: ${server.tools?.length || 0} available`);
if (server.tools?.length) {
server.tools.slice(0, 3).forEach((tool) => {
logger.always(` ⢠${tool.name}`);
});
if (server.tools.length > 3) {
logger.always(` ... and ${server.tools.length - 3} more`);
}
}
}
else {
if (server.error) {
logger.always(` ${chalk.red("Error:")} ${server.error}`);
}
logger.always(chalk.yellow(" š” Try: neurolink mcp remove && neurolink mcp install"));
}
logger.always();
}
// Summary
const connected = serversToTest.filter((s) => s.status === "connected").length;
const total = serversToTest.length;
if (connected === total) {
logger.always(chalk.green(`š All ${total} servers connected successfully`));
}
else {
logger.always(chalk.yellow(`ā ļø ${connected}/${total} servers connected`));
}
}
catch (_error) {
logger.error(chalk.red(`ā Test command failed: ${_error.message}`));
process.exit(1);
}
}
/**
* Execute exec command
*/
static async executeExec(argv) {
try {
const serverName = argv.server;
if (!serverName) {
logger.error(chalk.red("ā Server name is required"));
process.exit(1);
}
const toolName = argv.tool;
if (!toolName) {
logger.error(chalk.red("ā Tool name is required"));
process.exit(1);
}
const spinner = argv.quiet
? null
: ora(`Executing ${toolName} on ${serverName}...`).start();
// Parse parameters if provided
let params = {};
if (argv.params) {
try {
params = JSON.parse(argv.params);
}
catch (error) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red("ā Invalid JSON in params parameter"));
logger.error(chalk.red(`Error details: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
const sdk = new NeuroLink();
let exitCode = 0;
try {
await this.getMCPStatusWithTimeout(sdk);
// Check if server exists and is connected
const allServers = await sdk.listMCPServers();
const server = allServers.find((s) => s.name === serverName);
if (!server) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red(`ā Server not found: ${serverName}`));
exitCode = 1;
}
else if (server.status !== "connected") {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red(`ā Server not connected: ${serverName}`));
logger.always(chalk.yellow("š” Try: neurolink mcp test " + serverName));
exitCode = 1;
}
else {
// Check if tool exists
const tool = server.tools?.find((t) => t.name === toolName);
if (!tool) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red(`ā Tool not found: ${toolName}`));
if (server.tools?.length) {
logger.always(chalk.blue("Available tools:"));
server.tools.forEach((t) => {
logger.always(` ⢠${t.name}: ${t.description}`);
});
}
exitCode = 1;
}
else {
exitCode = await this.runExecTool(toolName, serverName, params, argv, spinner);
}
}
}
finally {
// Always shut down MCP connections to prevent lingering child processes
await sdk.shutdown().catch(() => undefined);
}
// Flush stdout/stderr before exit so buffered output (spinner lines,
// result text) is not truncated by process.exit(). process.exit() is
// still required because MCP stdio connections and health-check timers
// keep the Node.js event loop alive even after sdk.shutdown().
await new Promise((resolve) => {
process.stdout.write("", () => {
process.stderr.write("", () => resolve());
});
});
process.exit(exitCode);
}
catch (_error) {
logger.error(chalk.red(`ā Exec command failed: ${_error.message}`));
process.exit(1);
}
}
/**
* Run the actual MCP tool execution and format/write the output.
* Extracted to keep executeExec nesting within ESLint max-depth limit.
* Returns 0 on success, 1 on failure.
*/
static async runExecTool(toolName, serverName, params, argv, spinner) {
const WARN_BYTES = 50 * 1024; // 50 KB
try {
const { toolRegistry } = await import("../../mcp/toolRegistry.js");
const executionResult = await withTimeout(toolRegistry.executeTool(toolName, params, {
sessionId: `cli-${Date.now()}`,
userId: process.env.USER || "cli-user",
config: { domainType: "cli-execution", customData: { serverName } },
}), MCP_STATUS_TIMEOUT_MS, ErrorFactory.toolTimeout(toolName, MCP_STATUS_TIMEOUT_MS));
const result = {
tool: toolName,
server: serverName,
params,
result: executionResult,
success: true,
timestamp: new Date().toISOString(),
};
if (spinner) {
spinner.succeed(chalk.green("ā
Tool executed successfully"));
}
const resultJson = JSON.stringify(result, null, 2);
const resultBytes = Buffer.byteLength(resultJson, "utf-8");
const outputFile = argv.output;
if (outputFile) {
await fs.promises.writeFile(outputFile, resultJson, "utf-8");
logger.always(chalk.green(`ā
Result saved to: ${chalk.cyan(outputFile)}`));
logger.always(` Size: ${resultBytes >= 1024 ? `${(resultBytes / 1024).toFixed(1)} KB` : `${resultBytes} B`}`);
}
else if (argv.format === "json") {
if (resultBytes > WARN_BYTES) {
logger.warn(chalk.yellow(`ā Large result (${(resultBytes / 1024).toFixed(1)} KB). ` +
`Use --output <file> to save to disk instead.`));
}
logger.always(resultJson);
}
else {
if (resultBytes > WARN_BYTES) {
logger.warn(chalk.yellow(`ā Large result (${(resultBytes / 1024).toFixed(1)} KB). ` +
`Use --output <file> to save to disk instead.`));
}
logger.always(chalk.green("š§ Tool Execution Results:"));
logger.always(` Tool: ${chalk.cyan(toolName)}`);
logger.always(` Server: ${chalk.cyan(serverName)}`);
logger.always(` Result: ${JSON.stringify(executionResult, null, 2)}`);
logger.always(` Timestamp: ${result.timestamp}`);
}
return 0;
}
catch (toolError) {
const errorMessage = toolError instanceof Error ? toolError.message : String(toolError);
if (spinner) {
spinner.fail(chalk.red("ā Tool execution failed"));
}
const result = {
tool: toolName,
server: serverName,
params,
_error: errorMessage,
success: false,
timestamp: new Date().toISOString(),
};
if (argv.format === "json") {
logger.always(JSON.stringify(result, null, 2));
}
else {
logger.error(chalk.red("š§ Tool Execution Failed:"));
logger.error(` Tool: ${chalk.cyan(toolName)}`);
logger.error(` Server: ${chalk.cyan(serverName)}`);
logger.error(` Error: ${chalk.red(errorMessage)}`);
}
return 1;
}
}
/**
* Execute remove command
*/
static async executeRemove(argv) {
try {
const serverName = argv.server;
if (!serverName) {
logger.error(chalk.red("ā Server name is required"));
process.exit(1);
}
const sdk = new NeuroLink();
await this.getMCPStatusWithTimeout(sdk);
const allServers = await sdk.listMCPServers();
const server = allServers.find((s) => s.name === serverName);
if (!server) {
logger.error(chalk.red(`ā Server not found: ${serverName}`));
process.exit(1);
}
// Confirmation unless forced
if (!argv.force) {
logger.always(chalk.yellow(`ā ļø This will remove the MCP server: ${serverName}`));
logger.always("Use --force flag to confirm removal");
process.exit(1);
}
const spinner = argv.quiet
? null
: ora(`Removing MCP server: ${serverName}...`).start();
// Remove server using the NeuroLink MCP tool registry
try {
const { toolRegistry } = await import("../../mcp/toolRegistry.js");
const removed = toolRegistry.unregisterServer(serverName);
if (!removed) {
throw new Error(`Failed to remove server ${serverName} from registry`);
}
if (spinner) {
spinner.succeed(chalk.green(`ā
Server ${serverName} removed successfully`));
}
logger.always(chalk.green(`šļø Successfully removed MCP server: ${serverName}`));
logger.always(chalk.gray(" All associated tools have been unregistered"));
}
catch (removalError) {
const errorMessage = removalError instanceof Error
? removalError.message
: String(removalError);
if (spinner) {
spinner.fail(chalk.red(`ā Failed to remove server ${serverName}`));
}
logger.error(chalk.red(`ā Server removal failed: ${errorMessage}`));
process.exit(1);
}
}
catch (_error) {
logger.error(chalk.red(`ā Remove command failed: ${_error.message}`));
process.exit(1);
}
}
/**
* Execute discover command
*/
static async executeDiscover(argv) {
try {
const spinner = argv.quiet
? null
: ora("Discovering MCP servers...").start();
const discovered = [];
// Discover from Claude Desktop
if (argv.source === "claude-desktop" || argv.source === "all") {
const claudeServers = await this.discoverFromClaudeDesktop();
discovered.push(...claudeServers);
}
// Discover from VS Code
if (argv.source === "vscode" || argv.source === "all") {
const vscodeServers = await this.discoverFromVSCode();
discovered.push(...vscodeServers);
}
if (spinner) {
spinner.succeed(`Discovered ${discovered.length} MCP servers`);
}
if (discovered.length === 0) {
logger.always(chalk.yellow("No MCP servers discovered."));
logger.always(chalk.blue("š” Try installing popular servers: neurolink mcp install filesystem"));
return;
}
// Display discovered servers
if (argv.format === "json") {
logger.always(JSON.stringify(discovered, null, 2));
}
else {
logger.always(chalk.bold("\nš Discovered MCP Servers:\n"));
discovered.forEach((server) => {
logger.always(`${chalk.cyan(server.name)}`);
logger.always(` Command: ${server.command}`);
logger.always(` Source: ${server.description || "Unknown"}`);
logger.always(` Status: ${server.status}`);
logger.always();
});
}
// Auto-install if requested
if (argv.autoInstall && discovered.length > 0) {
logger.always(chalk.blue("š Auto-installing discovered servers..."));
const sdk = new NeuroLink();
for (const server of discovered) {
try {
// Use discovered MCPServerInfo directly - zero conversions!
await sdk.addInMemoryMCPServer(server.name, server);
logger.always(chalk.green(`ā
Installed ${server.name}`));
}
catch (_error) {
logger.always(chalk.red(`ā Failed to install ${server.name}: ${_error.message}`));
}
}
}
}
catch (_error) {
logger.error(chalk.red(`ā Discover command failed: ${_error.message}`));
process.exit(1);
}
}
/**
* Discover servers from Claude Desktop configuration
*/
static async discoverFromClaudeDesktop() {
const servers = [];
try {
// Common Claude Desktop config paths
const possiblePaths = [
path.join(process.env.HOME || "", "Library", "Application Support", "Claude", "claude_desktop_config.json"),
path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json"),
path.join(process.env.HOME || "", ".config", "claude", "claude_desktop_config.json"),
];
for (const configPath of possiblePaths) {
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (config.mcpServers) {
Object.entries(config.mcpServers).forEach(([name, serverConfig]) => {
const typedConfig = serverConfig;
// SMART DEFAULTS: Use utility to eliminate manual MCPServerInfo creation
servers.push(createExternalServerInfo({
...typedConfig,
id: name,
name,
description: "Discovered from Claude Desktop",
}));
});
}
break; // Found config file, stop searching
}
}
}
catch (error) {
// Log discovery errors for debugging but don't fail
logger.debug(`Claude Desktop discovery error: ${error instanceof Error ? error.message : String(error)}`);
}
return servers;
}
/**
* Discover servers from VS Code configuration
*/
static async discoverFromVSCode() {
const servers = [];
try {
// VS Code settings paths
const possiblePaths = [
path.join(process.env.HOME || "", "Library", "Application Support", "Code", "User", "settings.json"),
path.join(process.env.APPDATA || "", "Code", "User", "settings.json"),
path.join(process.env.HOME || "", ".config", "Code", "User", "settings.json"),
];
for (const settingsPath of possiblePaths) {
if (fs.existsSync(settingsPath)) {
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
// Look for MCP-related extensions or configurations
if (settings["mcp.servers"]) {
Object.entries(settings["mcp.servers"]).forEach(([name, serverConfig]) => {
const config = serverConfig;
servers.push(createExternalServerInfo({
...config,
id: name,
name,
description: "Discovered from VS Code",
}));
});
}
break;
}
}
}
catch (error) {
// Log discovery errors for debugging but don't fail
logger.debug(`VS Code discovery error: ${error instanceof Error ? error.message : String(error)}`);
}
return servers;
}
// ========================================
// NEW MCP ENHANCEMENT COMMANDS (Phase 8.39.0)
// ========================================
/**
* Build options for servers command
*/
static buildServersOptions(yargs) {
return yargs
.option("status", {
choices: ["all", "connected", "disconnected", "failed"],
default: "all",
description: "Filter servers by status",
})
.option("category", {
type: "string",
description: "Filter servers by category",
})
.option("detailed", {
type: "boolean",
default: false,
description: "Show detailed server information including metrics",
})
.example("neurolink mcp servers", "List all MCP servers")
.example("neurolink mcp servers --status connected", "List only connected servers")
.example("neurolink mcp servers --detailed", "Show detailed metrics for each server");
}
/**
* Build options for tools command
*/
static buildToolsOptions(yargs) {
return yargs
.option("server", {
type: "string",
description: "Filter tools by server ID",
})
.option("category", {
type: "string",
description: "Filter tools by category",
})
.option("tag", {
type: "array",
description: "Filter tools by tags",
})
.option("safety", {
choices: ["safe", "moderate", "dangerous"],
description: "Filter tools by safety level",
})
.option("annotations", {
type: "boolean",
default: false,
description: "Show tool annotations",
})
.option("search", {
type: "string",
description: "Search tools by name or description",
})
.example("neurolink mcp tools", "List all available tools")
.example("neurolink mcp tools --server filesystem", "List tools from specific server")
.example("neurolink mcp tools --safety safe", "List only read-only safe tools")
.example("neurolink mcp tools --annotations", "Show tools with their annotations");
}
/**
* Build options for discover tools command
*/
static buildDiscoverToolsOptions(yargs) {
return yargs
.option("server", {
type: "string",
description: "Specific server to discover tools from",
})
.option("infer-annotations", {
type: "boolean",
default: true,
description: "Automatically infer tool annotations",
})
.option("verbose", {
type: "boolean",
alias: "v",
default: false,
description: "Show verbose discovery output",
})
.example("neurolink mcp discover", "Discover tools from all servers")
.example("neurolink mcp discover --server github", "Discover tools from specific server");
}
/**
* Build options for create-server command
*/
static buildCreateServerOptions(yargs) {
return yargs
.positional("name", {
type: "string",
description: "Name for the new MCP server",
demandOption: true,
})
.option("template", {
choices: ["basic", "typescript", "python"],
default: "typescript",
description: "Server template to use",
})
.option("output", {
type: "string",
alias: "o",
description: "Output directory (defaults to current directory)",
})
.option("tools", {
type: "array",
description: "Initial tools to include in the server",
})
.option("description", {
type: "string",
alias: "d",
description: "Server description",
})
.example("neurolink mcp create-server my-server", "Create a new TypeScript MCP server")
.example("neurolink mcp create-server my-server --template python", "Create a Python MCP server")
.example("neurolink mcp create-server my-server --tools readFile writeFile", "Create server with initial tools");
}
/**
* Execute servers command
*/
static async executeServers(argv) {
try {
const spinner = argv.quiet ? null : ora("Loading MCP servers...").start();
const sdk = new NeuroLink();
let servers = await sdk.listMCPServers();
// Filter by status
if (argv.status && argv.status !== "all") {
servers = servers.filter((s) => s.status === argv.status);
}
// Filter by category
if (argv.category) {
servers = servers.filter((s) => s.metadata?.category === argv.category);
}
if (spinner) {
spinner.succeed(`Found ${servers.length} MCP servers`);
}
if (servers.length === 0) {
logger.always(chalk.yellow("No MCP servers match the criteria."));
return;
}
// Output formatting
if (argv.format === "json") {
logger.always(JSON.stringify(servers, null, 2));
return;
}
logger.always(chalk.bold("\nš” MCP Servers:\n"));
for (const server of servers) {
const statusIcon = server.status === "connected"
? chalk.green("ā")
: server.status === "failed"
? chalk.red("ā")
: chalk.yellow("ā");
const statusText = server.status === "connected"
? chalk.green(server.status.toUpperCase())
: server.status === "failed"
? chalk.red(server.status.toUpperCase())
: chalk.yellow(server.status.toUpperCase());
logger.always(`${statusIcon} ${chalk.cyan.bold(server.name)} [${statusText}]`);
logger.always(` ID: ${server.id}`);
logger.always(` Transport: ${server.transport}`);
logger.always(` Description: ${server.description || "No description"}`);
logger.always(` Tools: ${server.tools?.length || 0} available`);
if (argv.detailed) {
if (server.command) {
logger.always(` Command: ${server.command}`);
}
if (server.url) {
logger.always(` URL: ${server.url}`);
}
if (server.metadata) {
logger.always(` Category: ${server.metadata.category || "uncategorized"}`);
if (server.metadata.version) {
logger.always(` Version: ${server.metadata.version}`);
}
if (server.metadata.uptime) {
logger.always(` Uptime: ${Math.round(server.metadata.uptime / 1000)}s`);
}
}
if (server.error) {
logger.always(` ${chalk.red("Error:")} ${server.error}`);
}
}
logger.always();
}
// Summary
const connected = servers.filter((s) => s.status === "connected").length;
const totalTools = servers.reduce((sum, s) => sum + (s.tools?.length || 0), 0);
logger.always(chalk.gray("---"));
logger.always(`${chalk.bold("Summary:")} ${connected}/${servers.length} servers connected, ${totalTools} total tools`);
}
catch (error) {
logger.error(chalk.red(`Servers command failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Execute tools command
*/
static async executeTools(argv) {
try {
const spinner = argv.quiet ? null : ora("Loading MCP tools...").start();
const sdk = new NeuroLink();
const servers = await sdk.listMCPServers();
let tools = [];
const { inferAnnotations } = await import("../../mcp/toolAnnotations.js");
for (const server of servers) {
if (server.status !== "connected") {
continue;
}
for (const tool of server.tools || []) {
const annotations = tool.annotations ??
inferAnnotations({
name: tool.name,
description: tool.description,
});
tools.push({
name: tool.name,
description: tool.description,
serverId: server.id,
serverName: server.name,
inputSchema: tool.inputSchema,
category: this.inferToolCategory(tool.name, tool.description),
annotations,
});
}
}
// Apply filters
if (argv.server) {
tools = tools.filter((t) => t.serverId === argv.server || t.serverName === argv.server);
}
if (argv.category) {
tools = tools.filter((t) => t.category === argv.category);
}
if (argv.search) {
const searchTerm = String(argv.search).toLowerCase();
tools = tools.filter((t) => t.name.toLowerCase().includes(searchTerm) ||
t.description.toLowerCase().includes(searchTerm));
}
if (argv.safety) {
tools = tools.filter((t) => {
switch (argv.safety) {
case "safe":
return t.annotations?.readOnlyHint === true;
case "dangerous":
return t.annotations?.destructiveHint === true;
case "moderate":
return (!t.annotations?.readOnlyHint && !t.annotations?.destructiveHint);
default:
return true;
}
});
}
if (spinner) {
spinner.succeed(`Found ${tools.length} tools`);
}
if (tools.length === 0) {
logger.always(chalk.yellow("No tools match the criteria."));
return;
}
// Output formatting
if (argv.format === "json") {
logger.always(JSON.stringify(tools, null, 2));
return;
}
logger.always(chalk.bold("\nš§ MCP Tools:\n"));
// Group tools by server for better readability
const toolsByServer = new Map();
for (const tool of tools) {
const key = tool.serverName;
if (!toolsByServer.has(key)) {
toolsByServer.set(key, []);
}
const list = toolsByServer.get(key);
if (list) {
list.push(tool);
}
}
for (const [serverName, serverTools] of toolsByServer) {
logger.always(chalk.cyan.bold(`${serverName}`) +
chalk.gray(` (${serverTools.length} tools)`));
for (const tool of serverTools) {
// Safety indicator
let safetyIcon = "ā";
if (tool.annotations?.destructiveHint) {
safetyIcon = chalk.red("ā ");
}
else if (tool.annotations?.readOnlyHint) {
safetyIcon = chalk.green("ā");
}
else if (tool.annotations?.idempotentHint) {
safetyIcon = chalk.blue("ā");
}
logger.always(` ${safetyIcon} ${chalk.white.bold(tool.name)}`);
logger.always(` ${chalk.gray(tool.description)}`);
if (argv.annotations && tool.annotations) {
const annotationStr = getAnnotationSummary(tool.annotations);
if (annotationStr !== "[no annotations]") {
logger.always(` ${chalk.dim(annotationStr)}`);
}
}
}
logger.always();
}
// Summary
const safeCount = tools.filter((t) => t.annotations?.readOnlyHint).length;
const dangerousCount = tools.filter((t) => t.annotations?.destructiveHint).length;
logger.always(chalk.gray("---"));
logger.always(`${chalk.bold("Summary:")} ${tools.length} tools (${chalk.green(`${safeCount} safe`)}, ${chalk.red(`${dangerousCount} destructive`)})`);
}
catch (error) {
logger.error(chalk.red(`Tools command failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Execute discover tools command
*/
static async executeDiscoverTools(argv) {
try {
const spinner = argv.quiet
? null
: ora("Discovering MCP tools...").start();
const sdk = new NeuroLink();
const servers = await sdk.listMCPServers();
// Filter to specific server if requested
let targetServers = servers.filter((s) => s.status === "connected");
if (argv.server) {
targetServers = targetServers.filter((s) => s.id === argv.server || s.name === argv.server);
if (targetServers.length === 0) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red(`Server not found or not connected: ${argv.server}`));
process.exit(1);
}
}
if (spinner) {
spinner.text = `Discovering tools from ${targetServers.length} servers...`;
}
const results = [];
for (const server of targetServers) {
const { inferAnnotations } = await import("../../mcp/toolAnnotations.js");
const toolsWithAnnotations = (server.tools || []).map((tool) => {
const existing = tool
.annotations;
const annotations = existing ??
(argv.inferAnnotations !== false
? inferAnnotations({
name: tool.name,
description: tool.description,
})
: {});
return {
name: tool.name,
description: tool.description,
annotations,
};
});
results.push({
serverId: server.id,
serverName: server.name,
toolCount: toolsWithAnnotations.length,
tools: toolsWithAnnotations,
});
if (argv.verbose) {
logger.always(chalk.dim(`Discovered ${toolsWithAnnotations.length} tools from ${server.name}`));
}
}
if (spinner) {
const totalTools = results.reduce((sum, r) => sum + r.toolCount, 0);
spinner.succeed(`Discovered ${totalTools} tools from ${results.length} servers`);
}
// Output formatting
if (argv.format === "json") {
logger.always(JSON.stringify(results, null, 2));
return;
}
logger.always(chalk.bold("\nš Tool Discovery Results:\n"));
for (const result of results) {
logger.always(chalk.cyan.bold(result.serverName) +
chalk.gray(` [${result.serverId}]`));
logger.always(` Tools discovered: ${result.toolCount}`);
if (argv.verbose && result.tools.length > 0) {
for (const tool of result.tools) {
const annotationStr = getAnnotationSummary(tool.annotations);
logger.always(` ⢠${tool.name}${annotationStr !== "[no annotations]"
? ` ${chalk.dim(annotationStr)}`
: ""}`);
}
}
logger.always();
}
// Summary by annotations
const allTools = results.flatMap((r) => r.tools);
const safeCount = allTools.filter((t) => t.annotations.readOnlyHint).length;
const destructiveCount = allTools.filter((t) => t.annotations.destructiveHint).length;
const idempotentCount = allTools.filter((t) => t.annotations.idempotentHint).length;
logger.always(chalk.gray("---"));
logger.always(chalk.bold("Annotation Summary:"));
logger.always(` ${chalk.green("Read-only (safe):")} ${safeCount}`);
logger.always(` ${chalk.red("Destructive:")} ${destructiveCount}`);
logger.always(` ${chalk.blue("Idempotent:")} ${idempotentCount}`);
}
catch (error) {
logger.error(chalk.red(`Discover command failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Execute create-server command
*/
static async executeCreateServer(argv) {
try {
const serverName = argv.name;
if (!serverName) {
logger.error(chalk.red("Server name is required"));
process.exit(1);
}
const template = argv.template || "typescript";
const outputDir = argv.output || process.cwd();
const description = argv.description || `Custom MCP server: ${serverName}`;
const initialTools = argv.tools || [];
const spinner = argv.quiet
? null
: ora(`Creating MCP server: ${serverName}...`).start();
// Create output directory
const serverDir = path.join(outputDir, serverName);
if (fs.existsSync(serverDir)) {
if (spinner) {
spinner.fail();
}
logger.error(chalk.red(`Directory already exists: ${serverDir}`));
process.exit(1);
}
fs.mkdirSync(serverDir, { recursive: true });
// Generate server files based on template
if (template === "typescript") {
await this.generateTypeScriptServer(serverDir, serverName, description, initialTools);
}
else if (template === "python") {
await this.generatePythonServer(serverDir, serverName, description, initialTools);
}
else {
await this.generateBasicServer(serverDir, serverName, description, initialTools);
}
if (spinner) {
spinner.succeed(chalk.green(`Successfully created MCP server: ${serverName}`));
}
logger.always(chalk.bold("\nš Server created at:") + ` ${serverDir}`);
logger.always();
logger.always(chalk.bold("Next steps:"));
logger.always(` 1. cd ${serverDir}`);
if (template === "typescript") {
logger.always(" 2. npm install");
logger.always(" 3. npm run build");
logger.always(" 4. npm start");
}
else if (template === "python") {
logger.always(" 2. pip install -r requirements.txt");
logger.always(" 3. python server.py");
}
else {
logger.always(" 2. node server.js");
}
logger.always();
logger.always(chalk.blue("š” Add to NeuroLink: neurolink mcp add " +
serverName +
" node " +
path.join(serverDir, "dist", "index.js")));
}
catch (error) {
logger.error(chalk.red(`Create server command failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Generate TypeScript MCP server
*/
static async generateTypeScriptServer(serverDir, serverName, description, tools) {
// package.json
const packageJson = {
name: serverName,
version: "1.0.0",
description,
type: "module",
main: "dist/index.js",
scripts: {
build: "tsc",
start: "node dist/index.js",
dev: "tsc --watch",
},
dependencies: {
"@modelcontextprotocol/sdk": "^1.0.0",
},
devDependencies: {
"@types/node": "^20.0.0",
typescript: "^5.0.0",
},
};
fs.writeFileSync(path.join(serverDir, "package.json"), JSON.stringify(packageJson, null, 2));
// tsconfig.json
const tsConfig = {
compilerOptions: {
target: "ES2022",
module: "NodeNext",
moduleResolution: "NodeNext",
outDir: "./dist",
rootDir: "./src",
strict: true,
esModuleInterop: true,
skipLibCheck: true,
declaration: true,
},
include: ["src/**/*"],
};
fs.writeFileSync(path.join(serverDir, "tsconfig.json"), JSON.stringify(tsConfig, null, 2));
// Create src directory
fs.mkdirSync(path.join(serverDir, "src"), { recursive: true });
// Generate tool definitions
const toolDefs = tools.length > 0
? `
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
${tools
.map((toolName) => ` { name: "${toolName}", description: "Add a description for ${toolName}", inputSchema: { type: "object", properties: {}, required: [] } },`)
.join("\n")}
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
${tools
.map((toolName) => ` case "${toolName}":
// Implement ${toolName} tool logic here
return {
content: [{ type: "text", text: "${toolName} executed successfully" }],
};`)
.join("\n")}
default:
throw new Error(\`Unknown tool: \${request.params.name}\`);
}
});
`
: `
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "hello",
description: "A simple hello tool",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name to greet" },
},
required: ["name"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "hello") {
const args = request.params.arguments as { name?: string };
return {
content: [{
type: "text",
text: \`Hello, \${args.name || "World"}!\`,
}],
};
}
throw new Error(\`Unknown tool: \${request.params.name}\`);
});
`;
// Main server file
const mainFile = `/**
* ${serverName} - MCP Server
* ${description}
*
* Generated by NeuroLink CLI
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{
name: "${serverName}",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
${toolDefs}
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("${serverName} MCP server running on stdio");
}
main().catch(console.error);
`;
fs.writeFileSync(path.join(serverDir, "src", "index.ts"), mainFile);
// README.md
const readme = `# ${serverName}
${description}
## Installation
\`\`\`bash
npm install
npm run build
\`\`\`
## Usage
Add to your NeuroLink configuration:
\`\`\`bash
neurolink mcp add ${serverName} node ${path.join(serverDir, "dist", "index.js")}
\`\`\`
## Development
\`\`\`bash
npm run dev # Watch mode
npm run build # Build for production
\`\`\`
## Tools
${tools.length > 0 ? tools.map((t) => `- **${t}**: add a description`).join("\n") : "- **hello**: A simple hello tool"}
`;
fs.writeFileSync(path.join(serverDir, "README.md"), readme);
}
/**
* Generate Python MCP server
*/
static async generatePythonServer(serverDir, serverName, description, tools) {
// requirements.txt
const requirements = `mcp>=1.0.0
`;
fs.writeFileSync(path.join(serverDir, "requirements.txt"), requirements);
// Generate tool definitions - Python MCP SDK expects a single @server.call_tool() handler
// that dispatches on the tool name
const toolDefs = tools.length > 0
? (() => {
const branches = tools
.map((toolName, index) => {
const _safeName = toolName.replace(/-/g, "_");
const keyword = index === 0 ? "if" : "elif";
return ` ${keyword} name == "${toolName}":
# Implement ${toolName} tool logic here
return [TextContent(type="text", text="${toolName} executed successfully")]`;
})
.join("\n");
return `
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool calls."""
${branches}
else:
raise ValueError(f"Unknown tool: {name}")
`;
})()
: `
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool calls."""
if name == "hello":
name_arg = arguments.get("name", "World")
return [TextContent(type="text", text=f"Hello, {name_arg}!")]
else:
raise ValueError(f"Unknown tool: {name}")
`;
const toolListDefs = tools.length > 0
? tools
.map((toolName) => `
Tool(
name="${toolName}",
description="Add a description for ${toolName}",
inputSchema={
"type": "object",
"properties": {},
"required": [],
},
),`)
.join("\n")
: `
Tool(
name="hello",
description="A simple hello tool",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name to greet"},
},
"required": ["name"],
},
),`;
// Main server file
const mainFile = `"""
${serverName} - MCP Server
${description}
Generated by NeuroLink CLI
"""
import asyncio
from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions, Server
from mcp.types import (
Resource,
Tool,
TextContent,
)
server = Server("${serverName}")
@server.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools."""
return [${toolListDefs}
]
${toolDefs}
async def main():
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="${serverName}",
server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
if __name__ == "__main__":
asyncio.run(main())
`;
fs.writeFileSync(path.join(serverDir, "server.py"), mainFile);
// README.md
const readme = `# ${serverName}
${description}
## Installation
\`\`\`bash
pip install -r requirements.txt
\`\`\`
## Usage
Add to your NeuroLink configuration:
\`\`\`bash
neurolink mcp add ${serverName} python ${path.join(serverDir, "server.py")}
\`\`\`
## Tools
${tools.length > 0 ? tools.map((t) => `- **${t}**: add a description`).join("\n") : "- **hello**: A simple hello tool"}
`;
fs.writeFileSync(path.join(serverDir, "README.md"), readme);
}
/**
* Generate basic JavaScript MCP server
*/
static async generateBasicServer(serverDir, serverName, description, tools) {
// package.json
const packageJson = {
name: serverName,
version: "1.0.0",
description,
type: "module",
main: "server.js",
scripts: {
start: "node server.js",
},
dependencies: {
"@modelcontextprotocol/sdk": "^1.0.0",
},
};
fs.writeFileSync(path.join(serverDir, "package.json"), JSON.stringify(packageJson, null, 2));
// Generate tool handler
const toolHandler = tools.length > 0
? `const handlers = {
${tools.map((t) => `"${t}": async (args) => ({ type: "text", text: "${t} executed" })`).join(",\n ")}
};
if (handlers[request.params.name]) {
return { content: [await handlers[request.params.name](request.params.arguments)] };
}`
: `if (request.params.name === "hello") {
const args = request.params.arguments || {};
return {
content: [{
type: "text",
text: \`Hello, \${args.name || "World"}!\`,
}],
};
}`;
const toolList = tools.length > 0
? tools.map((t) => `{ name: "${t}", description: "Add a description", inputSchema: { type: "object", properties: {} } }`)
: [
`{ name: "hello", description: "A simple hello tool", inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } }`,
];
// Main server file
const mainFile = `/**
* ${serverName} - MCP Server
* ${description}
*
* Generated by NeuroLink CLI
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "${serverName}", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler({ method: "tools/list" }, async () => ({
tools: [
${toolList.join(",\n ")}
],
}));
server.setRequestHandler({ method: "tools/call" }, async (request) => {
${toolHandler}
throw new Error(\`Unknown tool: \${request.params.name}\`);
});
const transport = new StdioServerTransport();
server.connect(transport).then(() => {
console.error("${serverName} MCP server running on stdio");
}).catch(console.error);
`;
fs.writeFileSync(path.join(serverDir, "server.js"), mainFile);
// README.md
const readme = `# ${serverName}
${description}
## Installation
\`\`\`bash
npm install
\`\`\`
## Usage
\`\`\`bash
neurolink mcp add ${serverName} node ${path.join(serverDir, "server.js")}
\`\`\`
## Tools
${tools.length > 0 ? tools.map((t) => `- **${t}**: add a description`).join("\n") : "- **hello**: A simple hello tool"}
`;
fs.writeFileSync(path.join(serverDir, "README.md"), readme);
}
/**
* Infer tool category from name and description
*/
static inferToolCategory(name, description) {
const combined = (name + " " + description).toLowerCase();
if (combined.includes("git") || combined.includes("commit")) {
return "version-control";
}
if (combined.includes("file") ||
combined.includes("read") ||
combined.includes("write") ||
combined.includes("directory")) {
return "file-system";
}
if (combined.includes("api") ||
combined.includes("http") ||
combined.includes("request") ||
combined.includes("fetch")) {
return "api";
}
if (combined.includes("database") ||
combined.includes("query") ||
combined.includes("sql")) {
return "database";
}
if (combined.includes("search") ||
combined.includes("find") ||
combined.includes("grep")) {
return "search";
}
if (combined.includes("auth") ||
combined.includes("login") ||
combined.includes("token")) {
return "authentication";
}
return "general";
}
// ========================================
// REGISTRY COMMANDS (Phase 8.39.0)
// ========================================
/**
* Create the registry subcommand
*/
static createRegistryCommand() {
return {
command: "registry <action>",
describe: "Browse and search the MCP server registry",
builder: (yargs) => {
return yargs
.command("search [query]", "Search for MCP servers in the registry", (yargs) => yargs
.positional("query", {
type: "string",
description: "Search query",
})
.option("category", {
type: "string",
alias: "c",
description: "Filter by category",
})
.option("tag", {
type: "array",
alias: "t",
description: "Filter by tags",
})
.option("verified", {
type: "boolean",
default: false,
description: "Show only verified servers",
})
.option("limit", {
type: "number",
default: 10,
description: "Maximum results to show",
}), (argv) => this.executeRegistrySearch(argv))
.command("list", "List all available servers in the registry", (yargs) => yargs
.option("category", {
type: "string",
description: "Filter by category",
})
.option("limit", {
type: "number",
default: 25,
description: "Maximum results",
}), (argv) => this.executeRegistryList(argv))
.command("info <server>", "Get detailed information about a server", (yargs) => yargs.positional("server", {
type: "string",
description: "Server ID to get info for",
demandOption: true,
}), (argv) => this.executeRegistryInfo(argv))
.command("categories", "List all server categories", () => { }, () => this.executeRegistryCategories())
.command("popular", "Show popular MCP servers", (yargs) => yargs.option("limit", {
type: "number",
default: 10,
description: "Number of servers to show",
}), (argv) => this.executeRegistryPopular(argv))
.demandCommand(1, "Please specify a registry action")
.help();
},
handler: () => { },
};
}
/**
* Execute registry search
*/
static async executeRegistrySearch(argv) {
try {
const { globalMCPRegistryClient } = await import("../../mcp/mcpRegistryClient.js");
const spinner = argv.quiet ? null : ora("Searching registry...").start();
const results = await withTimeout(globalMCPRegistryClient.search({
query: argv.query,
categories: argv.category ? [argv.category] : undefined,
tags: argv.tag,
verifiedOnly: argv.verified,
limit: argv.limit ?? 10,
}), 30_000, ErrorFactory.toolTimeout("registrySearch", 30_000));
if (spinner) {
spinner.succeed(`Found ${results.totalCount} servers`);
}
if (results.entries.length === 0) {
logger.always(chalk.yellow("No servers found matching your criteria."));
return;
}
if (argv.format === "json") {
logger.always(JSON.stringify(results.entries, null, 2));
return;
}
logger.always(chalk.bold("\nš¦ MCP Server Registry Results:\n"));
for (const entry of results.entries) {
const verified = entry.verified ? chalk.green(" ā") : "";
logger.always(`${chalk.cyan.bold(entry.name)}${verified} (${entry.id})`);
logger.always(` ${chalk.gray(entry.description)}`);
logger.always(` Version: ${entry.version} | Categories: ${entry.categories?.join(", ") ?? "none"}`);
if (entry.tags?.length) {
logger.always(` Tags: ${entry.tags.join(", ")}`);
}
logger.always();
}
if (results.hasMore) {
logger.always(chalk.dim(`Showing ${results.entries.length} of ${results.totalCount} results. Use --limit to see more.`));
}
}
catch (error) {
logger.error(chalk.red(`Registry search failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Execute registry list
*/
static async executeRegistryList(argv) {
try {
const { globalMCPRegistryClient } = await import("../../mcp/mcpRegistryClient.js");
const spinner = argv.quiet ? null : ora("Loading registry...").start();
const results = await withTimeout(globalMCPRegistryClient.search({
categories: argv.category ? [argv.category] : undefined,
limit: argv.limit ?? 25,
sortBy: "name",
sortDirection: "asc",
}), 30_000, ErrorFactory.toolTimeout("registrySearch", 30_000));
if (spinner) {
spinner.succeed(`Found ${results.totalCount} servers`);
}
if (argv.format === "json") {
logger.always(JSON.stringify(results.entries, null, 2));
return;
}
logger.always(chalk.bold("\nš¦ Available MCP Servers:\n"));
// Group by category
const byCategory = new Map();
for (const entry of results.entries) {
const category = entry.categories?.[0] ?? "uncategorized";
if (!byCategory.has(category)) {
byCategory.set(category, []);
}
const catList = byCategory.get(category);
if (catList) {
catList.push(entry);
}
}
for (const [category, entries] of byCategory) {
logger.always(chalk.yellow.bold(`\n${category.toUpperCase()}`));
for (const entry of entries) {
const verified = entry.verified ? chalk.green("ā") : " ";
logger.always(` ${verified} ${chalk.cyan(entry.id.padEnd(20))} ${entry.description.slice(0, 50)}${entry.description.length > 50 ? "..." : ""}`);
}
}
}
catch (error) {
logger.error(chalk.red(`Registry list failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Execute registry info
*/
static async executeRegistryInfo(argv) {
try {
const { globalMCPRegistryClient } = await import("../../mcp/mcpRegistryClient.js");
const serverId = argv.server;
const entry = await withTimeout(globalMCPRegistryClient.getEntry(serverId), 30_000, ErrorFactory.toolTimeout("registryGetEntry", 30_000));
if (!entry) {
logger.error(chalk.red(`Server not found: ${serverId}`));
process.exit(1);
}
if (argv.format === "json") {
logger.always(JSON.stringify(entry, null, 2));
return;
}
logger.always(chalk.bold(`\nš¦ ${entry.name}`));
logger.always(chalk.gray(`ID: ${entry.id}`));
if (entry.verified) {
logger.always(chalk.green("ā Verified"));
}
logger.always();
logger.always(entry.description);
logger.always();
logger.always(chalk.bold("Details:"));
logger.always(` Version: ${entry.version}`);
if (entry.author) {
logger.always(` Author: ${entry.author}`);
}
if (entry.license) {
logger.always(` License: ${entry.license}`);
}
if (entry.homepage) {
logger.always(` Homepage: ${entry.homepage}`);
}
if (entry.repository) {
logger.always(` Repository: ${entry.repository}`);
}
if (entry.categories?.length) {
logger.always(`\n${chalk.bold("Categories:")} ${entry.categories.join(", ")}`);
}
if (entry.tags?.length) {
logger.always(`${chalk.bold("Tags:")} ${entry.tags.join(", ")}`);
}
if (entry.transports?.length) {
logger.always(`${chalk.bold("Transports:")} ${entry.transports.join(", ")}`);
}
if (entry.tools?.length) {
logger.always(`\n${chalk.bold("Tools:")} (${entry.tools.length})`);
for (const tool of entry.tools.slice(0, 10)) {
logger.always(` ⢠${tool}`);
}
if (entry.tools.length > 10) {
logger.always(` ... and ${entry.tools.length - 10} more`);
}
}
if (entry.requiredEnvVars?.length) {
logger.always(`\n${chalk.bold("Required Environment Variables:")}`);
for (const envVar of entry.requiredEnvVars) {
const isSet = process.env[envVar] ? chalk.green("ā") : chalk.red("ā");
logger.always(` ${isSet} ${envVar}`);
}
}
// Installation command
logger.always(`\n${chalk.bold("Installation:")}`);
if (entry.npmPackage) {
logger.always(chalk.cyan(` neurolink mcp install ${entry.id}`));
logger.always(chalk.dim(` or: npx -y ${entry.npmPackage}`));
}
else if (entry.command) {
logger.always(chalk.cyan(` ${entry.command} ${entry.args?.join(" ") ?? ""}`));
}
}
catch (error) {
logger.error(chalk.red(`Registry info failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Execute registry categories
*/
static async executeRegistryCategories() {
try {
const { globalMCPRegistryClient } = await import("../../mcp/mcpRegistryClient.js");
const categories = await withTimeout(globalMCPRegistryClient.getCategories(), 30_000, ErrorFactory.toolTimeout("registryGetCategories", 30_000));
logger.always(chalk.bold("\nš Available Categories:\n"));
for (const category of categories) {
const entries = await withTimeout(globalMCPRegistryClient.getByCategory(category), 30_000, ErrorFactory.toolTimeout("registryGetByCategory", 30_000));
logger.always(` ${chalk.cyan(category.padEnd(20))} (${entries.length} servers)`);
}
}
catch (error) {
logger.error(chalk.red(`Registry categories failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Execute registry popular
*/
static async executeRegistryPopular(argv) {
try {
const { globalMCPRegistryClient } = await import("../../mcp/mcpRegistryClient.js");
const spinner = argv.quiet
? null
: ora("Loading popular servers...").start();
const entries = await withTimeout(globalMCPRegistryClient.getPopularServers(argv.limit ?? 10), 30_000, ErrorFactory.toolTimeout("registryGetPopular", 30_000));
if (spinner) {
spinner.succeed(`Top ${entries.length} popular servers`);
}
if (argv.format === "json") {
logger.always(JSON.stringify(entries, null, 2));
return;
}
logger.always(chalk.bold("\nš Popular MCP Servers:\n"));
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const rank = chalk.yellow(`#${i + 1}`);
const verified = entry.verified ? chalk.green(" ā") : "";
logger.always(`${rank} ${chalk.cyan.bold(entry.name)}${verified}`);
logger.always(` ${chalk.gray(entry.description)}`);
logger.always(` ${chalk.dim(`Install: neurolink mcp install ${entry.id}`)}`);
logger.always();
}
}
catch (error) {
logger.error(chalk.red(`Registry popular failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
/**
* Build options for annotate command
*/
static buildAnnotateOptions(yargs) {
return yargs
.option("tool", {
type: "string",
alias: "t",
description: "Tool name to annotate",
})
.option("server", {
type: "string",
alias: "s",
description: "Server ID containing the tool",
})
.option("annotations", {
type: "string",
alias: "a",
description: "Annotations as JSON string",
})
.option("read-only", {
type: "boolean",
description: "Set readOnlyHint annotation",
})
.option("destructive", {
type: "boolean",
description: "Set destructiveHint annotation",
})
.option("idempotent", {
type: "boolean",
description: "Set idempotentHint annotation",
})
.option("requires-confirmation", {
type: "boolean",
description: "Set requiresConfirmation annotation",
})
.option("tags", {
type: "array",
description: "Tags for the tool",
})
.option("estimated-duration", {
type: "number",
description: "Estimated execution duration in ms",
})
.option("rate-limit", {
type: "number",
description: "Rate limit hint (calls per minute)",
})
.option("cost", {
type: "number",
description: "Cost hint (arbitrary units)",
})
.option("complexity", {
choices: ["simple", "medium", "complex"],
description: "Complexity level",
})
.option("security-level", {
choices: ["public", "internal", "restricted"],
description: "Security classification",
})
.option("audit", {
type: "boolean",
description: "Whether tool execution should be audited",
})
.option("infer", {
type: "boolean",
description: "Infer annotations from tool name/description",
})
.option("list", {
type: "boolean",
description: "List all tools with their current annotations",
})
.option("validate", {
type: "boolean",
description: "Validate annotations without applying",
})
.example("neurolink mcp annotate --tool readFile --read-only", "Mark readFile as read-only")
.example("neurolink mcp annotate --tool deleteFile --destructive --requires-confirmation", "Mark deleteFile as destructive requiring confirmation")
.example('neurolink mcp annotate --tool myTool --annotations \'{"readOnlyHint": true, "tags": ["data"]}\'', "Set custom annotations via JSON")
.example("neurolink mcp annotate --list", "List all tools with annotations")
.example("neurolink mcp annotate --tool myTool --infer", "Infer annotations from tool name/description");
}
/**
* Execute annotate command
*/
static async executeAnnotate(argv) {
try {
const sdk = new NeuroLink();
const servers = await sdk.listMCPServers();
if (argv.list) {
await this.listToolAnnotations(servers, argv);
return;
}
const toolName = argv.tool;
if (!toolName) {
logger.error(chalk.red("Tool name is required. Use --tool <name> or --list to see all tools."));
process.exit(1);
}
// `--infer` standalone path: when the user passes `--tool <name> --infer`
// and no server scope, emit the heuristic annotations inferred purely
// from the tool name/description. This makes `--infer` useful in any
// environment (no MCP servers required) and matches user intent ā
// "show me what NeuroLink would infer for a tool with this name".
const serverId = argv.server;
if (argv.infer &&
!serverId &&
servers.filter((s) => s.status === "connected").length === 0) {
const inferred = inferAnnotations({
name: toolName,
description: "",
});
const errors = validateAnnotations(inferred);
if (errors.length > 0) {
logger.error(chalk.red("Inferred annotations validation errors:"));
for (const error of errors) {
logger.error(chalk.red(` - ${error}`));
}
process.exit(1);
}
if (argv.format === "json") {
logger.always(JSON.stringify({ tool: toolName, inferred: true, annotations: inferred }, null, 2));
}
else {
logger.always(chalk.bold(`\nInferred annotations for ${chalk.cyan(toolName)}:`));
logger.always(JSON.stringify(inferred, null, 2));
}
return;
}
const foundTool = this.findToolForAnnotation(servers, toolName, serverId);
if (foundTool === "ambiguous") {
logger.error(chalk.red(`Tool '${toolName}' exists on multiple servers. Use --server <id> to specify which server to annotate.`));
logger.always(chalk.yellow("Use 'neurolink mcp annotate --list' to see available tools and their server IDs."));
process.exit(1);
}
if (!foundTool) {
logger.error(chalk.red(`Tool '${toolName}' not found.${serverId ? ` Server: ${serverId}` : ""}`));
logger.always(chalk.yellow("Use 'neurolink mcp annotate --list' to see available tools."));
process.exit(1);
}
const annotations = this.buildAnnotationsFromArgs(argv, foundTool);
const errors = validateAnnotations(annotations);
if (errors.length > 0) {
logger.error(chalk.red("Annotation validation errors:"));
for (const error of errors) {
logger.error(chalk.red(` - ${error}`));
}
process.exit(1);
}
// Validate only mode
if (argv.validate) {
logger.always(chalk.green("Annotations are valid:"));
logger.always(JSON.stringify(annotations, null, 2));
return;
}
this.printAnnotationUpdate(foundTool, annotations);
}
catch (error) {
logger.error(chalk.red(`Annotate command failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
static async getMCPStatusWithTimeout(sdk, timeoutMs = MCP_STATUS_TIMEOUT_MS) {
return withTimeout(sdk.getMCPStatus(), timeoutMs, ErrorFactory.toolTimeout("mcpStatus", timeoutMs));
}
static async listToolAnnotations(servers, argv) {
const spinner = argv.quiet
? null
: ora("Loading tool annotations...").start();
const allTools = this.collectToolAnnotations(servers);
if (spinner) {
spinner.succeed(`Found ${allTools.length} tools`);
}
if (argv.format === "json") {
logger.always(JSON.stringify(allTools, null, 2));
return;
}
logger.always(chalk.bold("\n Tool Annotations:\n"));
const byServer = allTools.reduce((acc, tool) => {
if (!acc[tool.serverId]) {
acc[tool.serverId] = {
serverName: tool.serverName,
tools: [],
};
}
acc[tool.serverId].tools.push(tool);
return acc;
}, {});
for (const [serverId, { serverName, tools }] of Object.entries(byServer)) {
logger.always(chalk.cyan.bold(`\n${serverName} (${serverId}):`));
for (const tool of tools) {
logger.always(` ${chalk.yellow(tool.toolName)} ${chalk.gray(getAnnotationSummary(tool.annotations))}`);
if (argv.detailed) {
logger.always(` ${chalk.gray(tool.description)}`);
}
}
}
logger.always();
}
static collectToolAnnotations(servers) {
const allTools = [];
for (const server of servers) {
if (server.status !== "connected") {
continue;
}
for (const tool of server.tools || []) {
const existing = tool
.annotations;
allTools.push({
serverName: server.name,
serverId: server.id,
toolName: tool.name,
description: tool.description,
annotations: existing ??
inferAnnotations({
name: tool.name,
description: tool.description,
}),
});
}
}
return allTools;
}
static findToolForAnnotation(servers, toolName, serverId) {
const matches = [];
for (const server of servers) {
if (serverId && server.id !== serverId) {
continue;
}
for (const tool of server.tools || []) {
if (tool.name === toolName) {
matches.push({
name: tool.name,
description: tool.description,
serverId: server.id,
serverName: server.name,
});
}
}
}
if (matches.length === 0) {
return null;
}
if (matches.length > 1) {
return "ambiguous";
}
return matches[0] ?? null;
}
static buildAnnotationsFromArgs(argv, foundTool) {
let annotations = {};
if (argv.annotations) {
try {
annotations = {
...annotations,
...JSON.parse(argv.annotations),
};
}
catch {
logger.error(chalk.red("Invalid JSON in --annotations"));
process.exit(1);
}
}
if (argv["read-only"] !== undefined) {
annotations.readOnlyHint = argv["read-only"];
}
if (argv.destructive !== undefined) {
annotations.destructiveHint = argv.destructive;
}
if (argv.idempotent !== undefined) {
annotations.idempotentHint = argv.idempotent;
}
if (argv["requires-confirmation"] !== undefined) {
annotations.requiresConfirmation = argv["requires-confirmation"];
}
if (argv.tags) {
annotations.tags = argv.tags;
}
if (argv["estimated-duration"] !== undefined) {
annotations.estimatedDuration = argv["estimated-duration"];
}
if (argv["rate-limit"] !== undefined) {
annotations.rateLimitHint = argv["rate-limit"];
}
if (argv.cost !== undefined) {
annotations.costHint = argv.cost;
}
if (argv.complexity) {
annotations.complexity = argv.complexity;
}
if (argv["security-level"]) {
annotations.securityLevel = argv["security-level"];
}
if (argv.audit !== undefined) {
annotations.auditRequired = argv.audit;
}
if (argv.infer) {
annotations = mergeAnnotations(inferAnnotations({
name: foundTool.name,
description: foundTool.description,
}), annotations);
}
return annotations;
}
static printAnnotationUpdate(foundTool, annotations) {
logger.always(chalk.bold("\n Tool Annotation Update:\n"));
logger.always(` Server: ${chalk.cyan(foundTool.serverName)} (${foundTool.serverId})`);
logger.always(` Tool: ${chalk.yellow(foundTool.name)}`);
logger.always(` Description: ${chalk.gray(foundTool.description)}`);
logger.always();
logger.always(chalk.bold(" Annotations:"));
if (annotations.readOnlyHint !== undefined) {
logger.always(` readOnlyHint: ${annotations.readOnlyHint ? chalk.green("true") : chalk.red("false")}`);
}
if (annotations.destructiveHint !== undefined) {
logger.always(` destructiveHint: ${annotations.destructiveHint ? chalk.red("true") : chalk.green("false")}`);
}
if (annotations.idempotentHint !== undefined) {
logger.always(` idempotentHint: ${annotations.idempotentHint ? chalk.green("true") : chalk.gray("false")}`);
}
if (annotations.requiresConfirmation !== undefined) {
logger.always(` requiresConfirmation: ${annotations.requiresConfirmation ? chalk.yellow("true") : chalk.gray("false")}`);
}
if (annotations.tags?.length) {
logger.always(` tags: ${chalk.blue(annotations.tags.join(", "))}`);
}
if (annotations.estimatedDuration !== undefined) {
logger.always(` estimatedDuration: ${annotations.estimatedDuration}ms`);
}
if (annotations.rateLimitHint !== undefined) {
logger.always(` rateLimitHint: ${annotations.rateLimitHint} calls/min`);
}
if (annotations.costHint !== undefined) {
logger.always(` costHint: ${annotations.costHint}`);
}
if (annotations.complexity) {
logger.always(` complexity: ${chalk.cyan(annotations.complexity)}`);
}
if (annotations.securityLevel) {
const secColor = annotations.securityLevel === "restricted"
? chalk.red
: annotations.securityLevel === "internal"
? chalk.yellow
: chalk.green;
logger.always(` securityLevel: ${secColor(annotations.securityLevel)}`);
}
if (annotations.auditRequired !== undefined) {
logger.always(` auditRequired: ${annotations.auditRequired ? chalk.yellow("true") : chalk.gray("false")}`);
}
logger.always();
logger.always(chalk.gray(" Summary: ") + getAnnotationSummary(annotations));
logger.always();
logger.always(chalk.blue("Note: Annotations displayed above. To persist annotations, add them to your MCP server configuration."));
logger.always(chalk.gray("Example: Add to your server's tool definition or use environment-specific annotation overrides."));
}
}
//# sourceMappingURL=mcp.js.map