@agentdesk/workflows-mcp
Version:
MCP workflow orchestration tool with presets for thinking, coding and more
332 lines • 13.6 kB
JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { promptFunctions } from "./prompts.js";
import { loadConfigSync, loadPresetConfigs, listAvailablePresets, mergeConfigs, validateToolConfig, convertParametersToZodSchema, } from "./config.js";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import fs from "fs";
/**
* Gets the package version from package.json
* @returns {Object} The parsed package.json content
*/
function getPackageInfo() {
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, "..", "package.json");
// Check if file exists before reading
if (!fs.existsSync(packageJsonPath)) {
console.error(`Package.json not found at ${packageJsonPath}`);
return { version: "0.1.7" }; // Fallback to hardcoded version
}
try {
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
}
catch (parseError) {
console.error(`Error parsing package.json: ${parseError}`);
return { version: "0.1.7" }; // Fallback to hardcoded version
}
}
catch (error) {
console.error(`Error accessing package info: ${error}`);
return { version: "0.1.7" }; // Fallback to hardcoded version
}
}
/**
* Parses command line arguments to extract config path and presets
* @returns {Object} Object containing configPath and presets
*/
function parseCommandLineArgs() {
const args = process.argv.slice(2);
let configPath;
let presets = []; // Initialize with empty array
let hasPreset = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--config" && i + 1 < args.length) {
configPath = args[i + 1];
}
else if (args[i] === "--preset" && i + 1 < args.length) {
// Split comma-separated presets
presets = args[i + 1].split(",");
hasPreset = true;
}
}
// Only default to thinking preset if no preset specified AND no config path provided
if (presets.length === 0 && !configPath) {
presets = ["thinking"];
}
return { configPath, presets };
}
/**
* Loads and merges configuration from presets and user config
* @param presets - Array of preset names to load
* @param configPath - Optional path to user config
*/
function loadAndMergeConfig(presets, configPath) {
// Log available presets
const availablePresets = listAvailablePresets();
console.error(`Available presets: ${availablePresets.join(", ")}`);
console.error(`Using presets: ${presets.join(", ")}`);
// 1. Load preset configs
const presetConfig = loadPresetConfigs(presets);
console.error(`Loaded ${Object.keys(presetConfig).length} tools from presets`);
// 2. Load user configs from .workflows directory if provided
const userConfig = configPath ? loadConfigSync(configPath) : {};
if (configPath) {
console.error(`Loaded ${Object.keys(userConfig).length} tool configurations from user config directory: ${configPath}`);
}
// 3. Merge configs (user config overrides preset config)
const finalConfig = mergeConfigs(presetConfig, userConfig);
console.error(`Final configuration contains ${Object.keys(finalConfig).length} tools`);
return finalConfig;
}
/**
* Creates and configures the MCP server with tools from the provided configuration
* @param config - The tool configuration object
* @param version - Server version
*/
function createMcpServer(config, version) {
// Create an MCP server
const server = new McpServer({
name: "DevTools MCP",
version: version,
}, {
capabilities: {
tools: {},
},
});
// Register all tools from the config
registerToolsFromConfig(server, config);
// If no tools were registered, add a dummy tool
if (Object.keys(config).filter((key) => !config[key]?.disabled).length === 0) {
console.error("No tools registered, adding placeholder tool");
server.tool("placeholder", "This is a placeholder tool when no other tools are loaded", async () => {
return {
content: [
{
type: "text",
text: "No tools are currently configured",
},
],
};
});
}
return server;
}
/**
* Registers tools to the MCP server based on the provided configuration
* @param server - The MCP server instance
* @param config - The tool configuration object
*/
function registerToolsFromConfig(server, config) {
// Function to add a tool if it's not disabled
const addTool = (name, description, inputSchema, callback) => {
server.tool(name, description, inputSchema, async (params) => {
try {
// Log parameters for debugging
console.error(`Tool ${name} called with params:`, params);
// Validate tool configuration
const validationError = validateToolConfig(config, name);
if (validationError) {
return {
content: [{ type: "text", text: `Error: ${validationError}` }],
isError: true,
};
}
// Pass the params to the callback
return callback(params);
}
catch (error) {
// Improved error handling
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Error executing tool ${name}:`, errorMessage);
return {
content: [
{
type: "text",
text: `Error executing tool: ${errorMessage}`,
},
],
isError: true,
};
}
});
};
// Dynamically register all tools from the config
Object.entries(config).forEach(([key, toolConfig]) => {
// Skip if toolConfig is undefined or disabled
if (!toolConfig || toolConfig.disabled) {
return;
}
// Find corresponding prompt function if available
const promptFunction = promptFunctions[key];
// Use custom name if provided, otherwise use the key
const toolName = toolConfig.name || key;
// Convert parameters to Zod schema if provided
let inputSchema = undefined;
if (toolConfig.parameters &&
Object.keys(toolConfig.parameters).length > 0) {
// Use Zod schema instead of JSON Schema for better MCP SDK compatibility
inputSchema = convertParametersToZodSchema(toolConfig.parameters);
// Log the input schema for debugging
console.error(`Tool ${toolName} input schema:`, JSON.stringify(Object.keys(inputSchema), null, 2));
}
addTool(toolName, toolConfig.description || `${key.replace(/_/g, " ")} tool`, inputSchema, async (params) => {
// Generate the tool prompt
let text = generateToolPrompt(key, toolConfig, promptFunction, config);
// If we have parameters, add them to the response
if (params && Object.keys(params).length > 0) {
text += `\n\nParameters: ${JSON.stringify(params, null, 2)}`;
}
return { content: [{ type: "text", text }] };
});
});
}
/**
* Generates the prompt text for a tool based on its configuration
* @param key - The tool key
* @param toolConfig - The tool configuration
* @param promptFunction - Optional prompt generation function
* @param fullConfig - The complete configuration object
*/
function generateToolPrompt(key, toolConfig, promptFunction, fullConfig) {
// Use the prompt function if available
if (promptFunction) {
return promptFunction(fullConfig);
}
// Otherwise use the prompt directly from config
if (toolConfig.prompt) {
let text = toolConfig.prompt;
// Add context if provided
if (toolConfig.context) {
text += `\n\n${toolConfig.context}`;
}
// Add tools section if provided
if (toolConfig.tools && toolConfig.tools.length > 0) {
text += "\n\n## Available Tools\n";
if (toolConfig.toolMode === "sequential") {
text +=
"If all required user input/feedback is acquired or if no input/feedback is needed, execute this exact sequence of tools to complete this task:\n\n";
toolConfig.tools.forEach((tool, index) => {
text += `${index + 1}. **${tool.name}**`;
if (tool.description) {
text += `: ${tool.description}`;
}
text += "\n";
});
}
else {
// Default to dynamic mode
text += `Use these tools as needed to complete the user's request:\n\n`;
toolConfig.tools.forEach((tool) => {
text += `- **${tool.name}**`;
if (tool.description) {
text += `: ${tool.description}`;
}
text += "\n";
});
}
}
return text;
}
// No prompt available
console.error(`Tool "${key}" has no prompt defined`);
return `# ${key}\n\nNo prompt defined for this tool.`;
}
/**
* Starts the MCP server with the specified transport
* @param server - The configured MCP server
* @param presets - Array of preset names used
* @param configPath - Optional path to user config
*/
async function startServer(server, presets, configPath) {
const transport = new StdioServerTransport();
// Critical: Make sure stdin/stdout are in raw mode
// This prevents the shell from interpreting JSON as commands
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
// Ensure stdio is properly set up to handle binary data
process.stdin.setEncoding("utf8");
// Completely replace stdout.write to ensure strict JSON-RPC handling
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = (chunk, encoding, callback) => {
try {
// Only process valid JSON objects
if (typeof chunk === "string" && chunk.trim().startsWith("{")) {
// Validate JSON format before sending
JSON.parse(chunk.trim());
return originalStdoutWrite(chunk, encoding, callback);
}
// Log non-JSON to stderr
if (typeof chunk === "string" && chunk.trim()) {
console.error(`[stdout filtered]: ${chunk.substring(0, 100)}${chunk.length > 100 ? "..." : ""}`);
}
// Indicate success without writing anything
if (callback)
callback();
return true;
}
catch (e) {
// Log JSON parsing errors to stderr
console.error(`[JSON error]: ${e instanceof Error ? e.message : String(e)}`);
if (callback)
callback();
return true;
}
};
try {
// Connect to the MCP server
await server.connect(transport);
// Log successful connection
console.error(`DevTools MCP server running with presets: ${presets.join(", ")}${configPath ? ` and user config from: ${configPath}` : ""}`);
// Keep the process alive
process.on("SIGINT", () => {
console.error("MCP server shutting down...");
process.exit(0);
});
}
catch (err) {
console.error("Error starting server:", err);
process.exit(1);
}
}
// Main execution
(async function main() {
try {
// Use hardcoded version to avoid package.json issues with npx
const version = "0.1.7"; // Match the current package version
// Parse command line arguments
const { configPath, presets } = parseCommandLineArgs();
// Log startup information
console.error(`Starting MCP server v${version} with presets: ${presets.join(", ")}`);
// Load and merge configuration - wrap in try/catch for better error handling
let finalConfig;
try {
finalConfig = loadAndMergeConfig(presets, configPath);
}
catch (error) {
console.error("Error loading configuration:", error);
process.exit(1);
}
// Create and configure the MCP server
const server = createMcpServer(finalConfig, version);
// Start the server
await startServer(server, presets, configPath);
// Keep process alive for incoming connections
process.stdin.on("end", () => {
console.error("Input stream ended, shutting down...");
process.exit(0);
});
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
process.exit(1);
});
}
catch (err) {
console.error("Fatal error:", err);
process.exit(1);
}
})();
//# sourceMappingURL=server.js.map