index.ts•5.56 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { loadConfig } from './config';
import { buildJsonSchema, buildZodSchema, executeDynamicTool } from './tools/DynamicToolFactory';
import { RunCliToolSchema, executeRunCli } from './tools/RunCliTool';
import { ToolConfig } from './types';
/** Reserved tool name for fallback mode */
const FALLBACK_TOOL_NAME = 'run_cli';
/**
* Static JSON schema for run_cli tool
*/
const RUN_CLI_INPUT_SCHEMA = {
type: 'object' as const,
properties: {
args: {
type: 'array',
items: { type: 'string' },
description: 'Array of arguments to pass to the CLI tool (e.g., ["status"] for "git status")',
},
},
required: ['args'],
};
/**
* Parse command line arguments
*/
function parseArgs(): { baseCli: string; configPath?: string } {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error(
'Usage: mcp-cli-wrapper <base-cli> [config.json]\n\n' +
'Arguments:\n' +
' base-cli The CLI command to wrap (e.g., "git", "docker")\n' +
' config.json Optional path to tool configuration file\n\n' +
'Examples:\n' +
' mcp-cli-wrapper git\n' +
' mcp-cli-wrapper git ./git-tools.json\n' +
' mcp-cli-wrapper docker ./docker-config.json'
);
process.exit(1);
}
return {
baseCli: args[0],
configPath: args[1],
};
}
/**
* Validate that config doesn't use reserved tool names
*/
function validateToolNames(tools: ToolConfig[]): void {
for (const tool of tools) {
if (tool.name === FALLBACK_TOOL_NAME) {
throw new Error(
`Tool name "${FALLBACK_TOOL_NAME}" is reserved for fallback mode. ` +
`Please use a different name for your tool.`
);
}
}
}
/**
* Build tools list for ListTools response
*/
function buildToolsList(baseCli: string, toolConfigs: ToolConfig[] | null): Tool[] {
if (toolConfigs === null) {
// Fallback mode: single run_cli tool
return [
{
name: FALLBACK_TOOL_NAME,
description: `Executes "${baseCli}" with the provided arguments and returns the output.`,
inputSchema: RUN_CLI_INPUT_SCHEMA,
},
];
}
// Config mode: dynamic tools from config
return toolConfigs.map((config) => ({
name: config.name,
description: config.description,
inputSchema: buildJsonSchema(config.parameters),
}));
}
/**
* Main entry point
*/
async function main(): Promise<void> {
const { baseCli, configPath } = parseArgs();
// Load config if provided
let toolConfigs: ToolConfig[] | null = null;
if (configPath) {
try {
const config = loadConfig(configPath);
// Validate no reserved names are used
validateToolNames(config.tools);
toolConfigs = config.tools;
console.error(`Loaded ${toolConfigs.length} tools for "${baseCli}" from config`);
} catch (error) {
console.error(`Error loading config: ${(error as Error).message}`);
console.error('Falling back to run_cli tool');
}
} else {
console.error(`Using run_cli tool for "${baseCli}"`);
}
// Create lookup map for tool configs
const toolConfigMap = new Map<string, ToolConfig>();
if (toolConfigs) {
for (const config of toolConfigs) {
toolConfigMap.set(config.name, config);
}
}
// Create MCP server
const server = new Server(
{
name: `mcp-cli-${baseCli}`,
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Handle ListTools request
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: buildToolsList(baseCli, toolConfigs),
};
});
// Handle CallTool request
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result: string;
if (toolConfigs === null) {
// Fallback mode: only run_cli is available
if (name !== FALLBACK_TOOL_NAME) {
throw new Error(`Unknown tool: ${name}`);
}
const parsed = RunCliToolSchema.parse(args);
result = await executeRunCli(baseCli, parsed);
} else {
// Config mode: find and execute the configured tool
const config = toolConfigMap.get(name);
if (!config) {
throw new Error(`Unknown tool: ${name}`);
}
// Validate input against the tool's schema
const schema = buildZodSchema(config.parameters);
const parsed = schema.parse(args);
result = await executeDynamicTool(baseCli, config, parsed);
}
return {
content: [
{
type: 'text',
text: result,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Connect to stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`MCP server for "${baseCli}" started`);
}
// Run main and handle errors
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});