#!/usr/bin/env bun
/**
* @aegis-ai/mcp-toolselect - Adaptive Tool Selection MCP Server
*
* Recommends which tools to use for a given task based on past success patterns.
* Learns from usage history and adapts recommendations over time.
*
* Tools:
* recommend_tools - Get tool recommendations for a task description
* register_tool - Register a new tool with its capabilities
* record_usage - Record tool usage outcome (success/failure)
* get_tool_stats - Get usage statistics for registered tools
* list_tools - List all registered tools
*
* Data is stored in ~/.mcp-toolselect/ as JSON files.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
// --- Storage ---
const DATA_DIR = join(homedir(), ".mcp-toolselect");
const REGISTRY_FILE = join(DATA_DIR, "tool-registry.json");
const STATS_FILE = join(DATA_DIR, "tool-stats.json");
const USAGE_LOG_FILE = join(DATA_DIR, "usage-log.jsonl");
function ensureDataDir(): void {
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true });
}
}
// --- Types ---
interface ToolEntry {
name: string;
description: string;
category: string;
strengths: string[];
useCases: string[];
registeredAt: string;
}
interface ToolRegistry {
tools: Record<string, ToolEntry>;
lastUpdated: string;
}
interface ToolStats {
name: string;
timesUsed: number;
successCount: number;
failCount: number;
avgDurationMs: number;
lastUsed: string;
contextSuccessRates: Record<string, number>;
}
interface StatsStore {
tools: Record<string, ToolStats>;
lastUpdated: string;
}
interface UsageLogEntry {
timestamp: string;
tool: string;
task: string;
context: string;
success: boolean;
durationMs: number;
notes?: string;
}
interface ToolRecommendation {
tool: string;
confidence: number;
reason: string;
priority: "required" | "recommended" | "optional";
successRate: number | null;
timesUsed: number;
}
// --- Data Access ---
function loadRegistry(): ToolRegistry {
ensureDataDir();
if (existsSync(REGISTRY_FILE)) {
try {
return JSON.parse(readFileSync(REGISTRY_FILE, "utf-8"));
} catch {
// Corrupted file, start fresh
}
}
const empty: ToolRegistry = { tools: {}, lastUpdated: new Date().toISOString() };
saveRegistry(empty);
return empty;
}
function saveRegistry(registry: ToolRegistry): void {
ensureDataDir();
registry.lastUpdated = new Date().toISOString();
writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2));
}
function loadStats(): StatsStore {
ensureDataDir();
if (existsSync(STATS_FILE)) {
try {
return JSON.parse(readFileSync(STATS_FILE, "utf-8"));
} catch {
// Corrupted file, start fresh
}
}
const empty: StatsStore = { tools: {}, lastUpdated: new Date().toISOString() };
saveStats(empty);
return empty;
}
function saveStats(store: StatsStore): void {
ensureDataDir();
store.lastUpdated = new Date().toISOString();
writeFileSync(STATS_FILE, JSON.stringify(store, null, 2));
}
function appendUsageLog(entry: UsageLogEntry): void {
ensureDataDir();
appendFileSync(USAGE_LOG_FILE, JSON.stringify(entry) + "\n");
}
// --- Task Analysis ---
interface TaskAnalysis {
keywords: string[];
complexity: "simple" | "medium" | "complex";
estimatedDuration: "instant" | "fast" | "medium" | "slow";
}
function analyzeTask(description: string): TaskAnalysis {
const text = description.toLowerCase();
const keywords: string[] = [];
const patterns: { pattern: RegExp; keyword: string }[] = [
{ pattern: /\b(code|implement|build|create|develop|program|write code)\b/, keyword: "coding" },
{ pattern: /\b(search|find|look up|research|discover|browse)\b/, keyword: "research" },
{ pattern: /\b(test|verify|validate|check|assert|qa)\b/, keyword: "testing" },
{ pattern: /\b(deploy|ship|release|publish|ci|cd)\b/, keyword: "deployment" },
{ pattern: /\b(email|message|notify|communicate|send)\b/, keyword: "communication" },
{ pattern: /\b(analyze|report|metric|statistic|data)\b/, keyword: "analysis" },
{ pattern: /\b(design|ui|ux|frontend|layout|style)\b/, keyword: "design" },
{ pattern: /\b(database|sql|query|schema|migrate)\b/, keyword: "database" },
{ pattern: /\b(api|endpoint|rest|graphql|request|fetch)\b/, keyword: "api" },
{ pattern: /\b(debug|fix|error|bug|issue|troubleshoot)\b/, keyword: "debugging" },
{ pattern: /\b(security|auth|permission|encrypt|token)\b/, keyword: "security" },
{ pattern: /\b(document|readme|docs|write up|explain)\b/, keyword: "documentation" },
{ pattern: /\b(refactor|clean|optimize|improve|performance)\b/, keyword: "optimization" },
{ pattern: /\b(monitor|log|trace|observe|alert)\b/, keyword: "monitoring" },
{ pattern: /\b(automate|script|pipeline|workflow|batch)\b/, keyword: "automation" },
];
for (const { pattern, keyword } of patterns) {
if (pattern.test(text)) {
keywords.push(keyword);
}
}
let complexity: "simple" | "medium" | "complex" = "simple";
if (text.length > 200 || keywords.length > 3) {
complexity = "complex";
} else if (text.length > 80 || keywords.length > 1) {
complexity = "medium";
}
let estimatedDuration: "instant" | "fast" | "medium" | "slow" = "fast";
if (complexity === "complex") estimatedDuration = "slow";
else if (complexity === "medium") estimatedDuration = "medium";
return { keywords, complexity, estimatedDuration };
}
// --- Recommendation Engine ---
function computeRecommendations(taskDescription: string): ToolRecommendation[] {
const analysis = analyzeTask(taskDescription);
const registry = loadRegistry();
const stats = loadStats();
const recommendations: ToolRecommendation[] = [];
const taskText = taskDescription.toLowerCase();
for (const [name, tool] of Object.entries(registry.tools)) {
let score = 0;
let matchReasons: string[] = [];
// Check keyword overlap between task analysis and tool strengths/useCases
const toolTerms = [
...tool.strengths.map((s) => s.toLowerCase()),
...tool.useCases.map((u) => u.toLowerCase()),
tool.category.toLowerCase(),
];
for (const keyword of analysis.keywords) {
for (const term of toolTerms) {
if (term.includes(keyword) || keyword.includes(term)) {
score += 0.3;
matchReasons.push(`matches keyword "${keyword}"`);
break;
}
}
}
// Check direct text match against tool description and strengths
for (const strength of tool.strengths) {
if (taskText.includes(strength.toLowerCase())) {
score += 0.2;
matchReasons.push(`strength "${strength}" found in task`);
}
}
for (const useCase of tool.useCases) {
if (taskText.includes(useCase.toLowerCase())) {
score += 0.25;
matchReasons.push(`use case "${useCase}" found in task`);
}
}
// Category match
if (analysis.keywords.includes(tool.category.toLowerCase())) {
score += 0.2;
matchReasons.push(`category "${tool.category}" matches task`);
}
if (score <= 0) continue;
// Cap base score at 0.9
const baseConfidence = Math.min(score, 0.9);
// Adjust with historical performance
const toolStats = stats.tools[name];
let finalConfidence = baseConfidence;
let successRate: number | null = null;
let timesUsed = 0;
if (toolStats && toolStats.timesUsed > 0) {
timesUsed = toolStats.timesUsed;
successRate = toolStats.successCount / toolStats.timesUsed;
// Check context-specific success rate
const contextKey = analysis.keywords[0] || "general";
const contextRate = toolStats.contextSuccessRates[contextKey];
if (contextRate !== undefined && toolStats.timesUsed >= 3) {
// Blend base confidence with historical context performance
finalConfidence = baseConfidence * 0.6 + contextRate * 0.4;
} else if (toolStats.timesUsed >= 5) {
// Use overall success rate
finalConfidence = baseConfidence * 0.7 + successRate * 0.3;
}
}
// Determine priority
let priority: "required" | "recommended" | "optional" = "optional";
if (finalConfidence >= 0.7) priority = "required";
else if (finalConfidence >= 0.4) priority = "recommended";
const reason =
matchReasons.length > 0
? matchReasons.slice(0, 3).join("; ")
: `Category "${tool.category}" is relevant`;
recommendations.push({
tool: name,
confidence: Math.round(finalConfidence * 100) / 100,
reason,
priority,
successRate: successRate !== null ? Math.round(successRate * 100) / 100 : null,
timesUsed,
});
}
// Sort: required first, then by confidence descending
const priorityOrder = { required: 0, recommended: 1, optional: 2 };
recommendations.sort((a, b) => {
const pd = priorityOrder[a.priority] - priorityOrder[b.priority];
if (pd !== 0) return pd;
return b.confidence - a.confidence;
});
return recommendations;
}
// --- MCP Server ---
const server = new Server(
{
name: "mcp-toolselect",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "recommend_tools",
description:
"Given a task description, recommend which tools to use based on registered tool capabilities and past success patterns. Returns ranked recommendations with confidence scores.",
inputSchema: {
type: "object" as const,
properties: {
task: {
type: "string",
description: "Description of the task you need to accomplish",
},
max_results: {
type: "number",
description: "Maximum number of recommendations to return (default: 5)",
},
},
required: ["task"],
},
},
{
name: "register_tool",
description:
"Register a tool with its capabilities, strengths, and typical use cases. This builds the tool catalog that powers recommendations.",
inputSchema: {
type: "object" as const,
properties: {
name: {
type: "string",
description: "Unique name for the tool",
},
description: {
type: "string",
description: "What the tool does",
},
category: {
type: "string",
description:
"Tool category (e.g. coding, testing, deployment, research, communication, analysis, design, database, api, debugging, security, documentation, optimization, monitoring, automation)",
},
strengths: {
type: "array",
items: { type: "string" },
description: "List of what this tool is good at (e.g. ['fast execution', 'typescript', 'web scraping'])",
},
use_cases: {
type: "array",
items: { type: "string" },
description: "Typical use cases or scenarios where this tool shines",
},
},
required: ["name", "description", "category", "strengths", "use_cases"],
},
},
{
name: "record_usage",
description:
"Record that a tool was used for a task and whether it succeeded. This feedback loop improves future recommendations.",
inputSchema: {
type: "object" as const,
properties: {
tool: {
type: "string",
description: "Name of the tool that was used",
},
task: {
type: "string",
description: "Description of the task it was used for",
},
success: {
type: "boolean",
description: "Whether the tool successfully completed the task",
},
duration_ms: {
type: "number",
description: "How long the tool took in milliseconds (optional)",
},
notes: {
type: "string",
description: "Any additional notes about the usage (optional)",
},
},
required: ["tool", "task", "success"],
},
},
{
name: "get_tool_stats",
description:
"Get usage statistics and success rates for one or all registered tools. Includes per-context success rates when available.",
inputSchema: {
type: "object" as const,
properties: {
tool: {
type: "string",
description: "Name of a specific tool to get stats for. Omit to get stats for all tools.",
},
},
required: [],
},
},
{
name: "list_tools",
description: "List all registered tools with their metadata, categories, and strengths.",
inputSchema: {
type: "object" as const,
properties: {
category: {
type: "string",
description: "Filter by category (optional)",
},
},
required: [],
},
},
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "recommend_tools": {
const task = args?.task as string;
const maxResults = (args?.max_results as number) || 5;
if (!task) {
return {
content: [{ type: "text" as const, text: "Error: 'task' parameter is required." }],
isError: true,
};
}
const analysis = analyzeTask(task);
const recommendations = computeRecommendations(task).slice(0, maxResults);
if (recommendations.length === 0) {
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
task,
analysis: {
keywords: analysis.keywords,
complexity: analysis.complexity,
estimatedDuration: analysis.estimatedDuration,
},
recommendations: [],
message: "No matching tools found. Register tools with register_tool to build the catalog.",
},
null,
2
),
},
],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
task,
analysis: {
keywords: analysis.keywords,
complexity: analysis.complexity,
estimatedDuration: analysis.estimatedDuration,
},
recommendations,
},
null,
2
),
},
],
};
}
case "register_tool": {
const toolName = args?.name as string;
const description = args?.description as string;
const category = args?.category as string;
const strengths = args?.strengths as string[];
const useCases = args?.use_cases as string[];
if (!toolName || !description || !category || !strengths || !useCases) {
return {
content: [
{
type: "text" as const,
text: "Error: All fields are required: name, description, category, strengths, use_cases.",
},
],
isError: true,
};
}
const registry = loadRegistry();
const isUpdate = !!registry.tools[toolName];
registry.tools[toolName] = {
name: toolName,
description,
category,
strengths,
useCases,
registeredAt: isUpdate ? registry.tools[toolName].registeredAt : new Date().toISOString(),
};
saveRegistry(registry);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
status: isUpdate ? "updated" : "registered",
tool: registry.tools[toolName],
},
null,
2
),
},
],
};
}
case "record_usage": {
const tool = args?.tool as string;
const task = args?.task as string;
const success = args?.success as boolean;
const durationMs = (args?.duration_ms as number) || 0;
const notes = args?.notes as string | undefined;
if (!tool || !task || success === undefined) {
return {
content: [
{
type: "text" as const,
text: "Error: 'tool', 'task', and 'success' are required.",
},
],
isError: true,
};
}
// Append to log
const analysis = analyzeTask(task);
const contextKey = analysis.keywords[0] || "general";
const logEntry: UsageLogEntry = {
timestamp: new Date().toISOString(),
tool,
task,
context: contextKey,
success,
durationMs,
notes,
};
appendUsageLog(logEntry);
// Update stats
const store = loadStats();
if (!store.tools[tool]) {
store.tools[tool] = {
name: tool,
timesUsed: 0,
successCount: 0,
failCount: 0,
avgDurationMs: 0,
lastUsed: "",
contextSuccessRates: {},
};
}
const s = store.tools[tool];
s.timesUsed++;
if (success) s.successCount++;
else s.failCount++;
// Exponential moving average for duration
if (durationMs > 0) {
if (s.avgDurationMs === 0) {
s.avgDurationMs = durationMs;
} else {
s.avgDurationMs = Math.round(s.avgDurationMs * 0.8 + durationMs * 0.2);
}
}
s.lastUsed = new Date().toISOString();
// Update context-specific success rate (exponential moving average)
const currentRate = s.contextSuccessRates[contextKey] ?? 0.5;
s.contextSuccessRates[contextKey] = Math.round((currentRate * 0.8 + (success ? 1 : 0) * 0.2) * 100) / 100;
saveStats(store);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
status: "recorded",
tool,
context: contextKey,
success,
totalUses: s.timesUsed,
overallSuccessRate: Math.round((s.successCount / s.timesUsed) * 100) / 100,
},
null,
2
),
},
],
};
}
case "get_tool_stats": {
const toolName = args?.tool as string | undefined;
const store = loadStats();
if (toolName) {
const s = store.tools[toolName];
if (!s) {
return {
content: [
{
type: "text" as const,
text: `No stats found for tool "${toolName}". It may not have been used yet.`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
...s,
overallSuccessRate: s.timesUsed > 0 ? Math.round((s.successCount / s.timesUsed) * 100) / 100 : null,
},
null,
2
),
},
],
};
}
// All tools
const allStats = Object.values(store.tools)
.map((s) => ({
...s,
overallSuccessRate: s.timesUsed > 0 ? Math.round((s.successCount / s.timesUsed) * 100) / 100 : null,
}))
.sort((a, b) => b.timesUsed - a.timesUsed);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
totalTools: allStats.length,
totalUsages: allStats.reduce((sum, s) => sum + s.timesUsed, 0),
tools: allStats,
lastUpdated: store.lastUpdated,
},
null,
2
),
},
],
};
}
case "list_tools": {
const category = args?.category as string | undefined;
const registry = loadRegistry();
let tools = Object.values(registry.tools);
if (category) {
tools = tools.filter((t) => t.category.toLowerCase() === category.toLowerCase());
}
// Group by category
const byCategory: Record<string, ToolEntry[]> = {};
for (const tool of tools) {
if (!byCategory[tool.category]) byCategory[tool.category] = [];
byCategory[tool.category].push(tool);
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
totalTools: tools.length,
categories: Object.keys(byCategory),
tools: byCategory,
lastUpdated: registry.lastUpdated,
},
null,
2
),
},
],
};
}
default:
return {
content: [
{
type: "text" as const,
text: `Unknown tool: ${name}. Available tools: recommend_tools, register_tool, record_usage, get_tool_stats, list_tools.`,
},
],
isError: true,
};
}
});
// --- Start ---
async function main() {
ensureDataDir();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-toolselect server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});