UNPKG

@agentdesk/workflows-mcp

Version:

MCP workflow orchestration tool with presets for thinking, coding and more

508 lines (419 loc) 17.6 kB
/// <reference types="node" /> /// <reference types="mocha" /> import { expect } from "chai"; import { McpTestClient } from "../src/client.js"; import * as fs from "fs"; import * as path from "path"; import { fileURLToPath } from "url"; // Create dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Create temporary test directories for config testing const TEST_CONFIG_DIR = path.join(__dirname, "test-workflows"); const EMPTY_CONFIG_DIR = path.join(TEST_CONFIG_DIR, "empty-workflows"); const INVALID_CONFIG_DIR = path.join(TEST_CONFIG_DIR, "not-workflows"); const WORKFLOWS_DIR = path.join(TEST_CONFIG_DIR, ".workflows"); const MCP_WORKFLOWS_DIR = path.join(TEST_CONFIG_DIR, ".mcp-workflows"); describe("MCP Server Configuration Tests", function () { this.timeout(15000); // Increase timeout for server startup let client: McpTestClient; // Setup test directories before all tests before(() => { // Create test directories if they don't exist if (!fs.existsSync(TEST_CONFIG_DIR)) { fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); } if (!fs.existsSync(EMPTY_CONFIG_DIR)) { fs.mkdirSync(EMPTY_CONFIG_DIR, { recursive: true }); } if (!fs.existsSync(WORKFLOWS_DIR)) { fs.mkdirSync(WORKFLOWS_DIR, { recursive: true }); } if (!fs.existsSync(MCP_WORKFLOWS_DIR)) { fs.mkdirSync(MCP_WORKFLOWS_DIR, { recursive: true }); } if (!fs.existsSync(INVALID_CONFIG_DIR)) { fs.mkdirSync(INVALID_CONFIG_DIR, { recursive: true }); } // Create a temporary test preset file in both src/presets and dist/presets const srcPresetDir = path.join(__dirname, "..", "src", "presets"); const distPresetDir = path.join(__dirname, "..", "dist", "presets"); const testPresetContent = `test_mode: description: "Test mode for testing data-driven approach" prompt: | # Test Mode This is a test prompt for verifying the data-driven preset approach. No code changes should be needed to add this test mode. `; fs.writeFileSync( path.join(srcPresetDir, "test-preset.yaml"), testPresetContent ); // Also write to dist/presets since that's what gets used at runtime fs.writeFileSync( path.join(distPresetDir, "test-preset.yaml"), testPresetContent ); // Add test YAML files to .workflows directory fs.writeFileSync( path.join(WORKFLOWS_DIR, "override-description.yaml"), `debugger_mode: description: "Custom debugging tool description"` ); fs.writeFileSync( path.join(WORKFLOWS_DIR, "override-prompt.yaml"), `debugger_mode: prompt: | # Custom Debugger Mode This is a completely custom prompt for the debugger mode.` ); fs.writeFileSync( path.join(WORKFLOWS_DIR, "disable-tool.yaml"), `planner_mode: disabled: true` ); fs.writeFileSync( path.join(WORKFLOWS_DIR, "custom-tool.yaml"), `custom_tool: description: "A completely custom tool" prompt: | # Custom Tool This is a custom tool that doesn't exist in presets.` ); fs.writeFileSync( path.join(WORKFLOWS_DIR, "malformed.yaml"), `this is not valid: yaml: - missing colon indentation problem` ); // Add test YAML files to .mcp-workflows directory fs.writeFileSync( path.join(MCP_WORKFLOWS_DIR, "custom-mcp-tool.yaml"), `custom_mcp_tool: description: "A custom tool from .mcp-workflows" prompt: | # Custom MCP Tool This is a custom tool from the .mcp-workflows directory.` ); }); // Clean up after all tests after(() => { // Remove test directories if desired // fs.rmSync(TEST_CONFIG_DIR, { recursive: true }); // Remove the temporary test preset files try { const srcPresetPath = path.join( __dirname, "..", "src", "presets", "test-preset.yaml" ); const distPresetPath = path.join( __dirname, "..", "dist", "presets", "test-preset.yaml" ); if (fs.existsSync(srcPresetPath)) { fs.unlinkSync(srcPresetPath); } if (fs.existsSync(distPresetPath)) { fs.unlinkSync(distPresetPath); } } catch (error) { console.error("Error cleaning up test preset:", error); } }); beforeEach(() => { client = new McpTestClient(); }); afterEach(async () => { try { await client.close(); } catch (error) { console.error("Error closing client:", error); } }); // Basic Scenarios describe("Basic Scenarios", () => { it("B1: Default run - should load only thinking tools", async () => { await client.connect(); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("thinking_mode"); }); it("B2: Invalid command line args - should use default config", async () => { await client.connect(["--invalid", "arg"]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("thinking_mode"); }); }); // Preset Scenarios describe("Preset Scenarios", () => { it("P1: Thinking preset - should load only thinking tools", async () => { await client.connect(["--preset", "thinking"]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("thinking_mode"); }); it("P1.1: Thinking mode should have thought parameter", async () => { await client.connect(["--preset", "thinking"]); const tools = await client.listTools(); // Find the thinking_mode tool const thinkingTool = tools.tools.find( (t: any) => t.name === "thinking_mode" ); expect(thinkingTool).to.exist; // Check that it has the inputSchema property expect(thinkingTool).to.have.property("inputSchema"); // Note: Due to known issues with schema validation in the MCP SDK, // we can't directly test the schema properties as they may be empty in the response. // We can test this functionality more comprehensively in integration tests. }); it("P2: Coding preset - should load only coding tools", async () => { await client.connect(["--preset", "coding"]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("debugger_mode"); expect(toolNames).to.include("architecture_mode"); }); it("P3: Multiple presets - should load tools from all presets", async () => { await client.connect(["--preset", "coding,thinking"]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("thinking_mode"); // From thinking expect(toolNames).to.include("debugger_mode"); // From coding }); it("P4: Duplicate presets - should load each tool only once", async () => { await client.connect(["--preset", "thinking,thinking"]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); // Count occurrences of each tool name const counts = toolNames.reduce( (acc: Record<string, number>, name: string) => { acc[name] = (acc[name] || 0) + 1; return acc; }, {} ); // Ensure no duplicates Object.values(counts).forEach((count) => { expect(count).to.equal(1); }); }); it("P5: Non-existent preset - should start with no tools from that preset", async () => { await client.connect(["--preset", "nonexistent"]); const tools = await client.listTools(); // Server should still start but with a placeholder tool expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); // Should include the placeholder tool expect(toolNames).to.include("placeholder"); }); it("P6: Mixed valid/invalid presets - should load tools from valid preset only", async () => { await client.connect(["--preset", "coding,nonexistent"]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("debugger_mode"); // From coding }); it("P7: Empty preset arg - should start with no tools", async () => { await client.connect(["--preset", ""]); const tools = await client.listTools(); // Server should start with just the placeholder tool expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); // Should include the placeholder tool expect(toolNames).to.include("placeholder"); }); it("P8: Data-driven approach - should load tools from new preset file without code changes", async () => { await client.connect(["--preset", "test-preset"]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("test_mode"); // Verify the tool can be invoked const response = await client.callTool("test_mode"); expect(response.content[0].text).to.include("Test Mode"); expect(response.content[0].text).to.include( "data-driven preset approach" ); }); }); // Configuration Scenarios describe("Configuration Scenarios", () => { it("C1: Basic config - should load configs from directory", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".workflows"), ]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("custom_tool"); // Custom tool from config }); it("C1.2: Config without preset - should not load thinking preset by default", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".workflows"), ]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); // Should include tools from config expect(toolNames).to.include("custom_tool"); // Should NOT include tools from thinking preset when only config is provided expect(toolNames).to.not.include("thinking_mode"); }); it("C1.1: Alternate folder name - should load configs from .mcp-workflows", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".mcp-workflows"), ]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("custom_mcp_tool"); // Custom tool from .mcp-workflows }); it("C2: Config with preset - should merge configs with preset overriding", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".workflows"), "--preset", "coding", ]); const tools = await client.listTools(); expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); // Should include tools from both, but config overrides preset expect(toolNames).to.include("custom_tool"); // From config // debugger_mode is renamed to custom_debugger due to name override expect(toolNames).to.include("custom_debugger"); // Renamed from debugger_mode expect(toolNames).to.not.include("planner_mode"); // Disabled in config }); it("C3: Non-existent config path - should not load thinking preset", async () => { await client.connect(["--config", "./nonexistent"]); const tools = await client.listTools(); // Should not fall back to thinking preset, just use placeholder tool expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("placeholder"); expect(toolNames).to.not.include("thinking_mode"); // Should NOT include thinking preset tools }); it("C4: Config path is not .workflows - should not load thinking preset", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", "not-workflows"), ]); const tools = await client.listTools(); // Should not fall back to thinking preset, just use placeholder tool expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("placeholder"); expect(toolNames).to.not.include("thinking_mode"); // Should NOT include thinking preset tools }); it("C5: Empty config directory - should not load thinking preset", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", "empty-workflows"), ]); const tools = await client.listTools(); // Should not fall back to thinking preset, just use placeholder tool expect(tools.tools).to.be.an("array"); const toolNames = tools.tools.map((t: any) => t.name); expect(toolNames).to.include("placeholder"); expect(toolNames).to.not.include("thinking_mode"); // Should NOT include thinking preset tools }); }); // Configuration Content Tests describe("Configuration Content Tests", () => { it("CC1: Override tool description - description should match override", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".workflows"), "--preset", "coding", ]); const tools = await client.listTools(); // Find the custom_debugger tool (renamed from debugger_mode) const debuggerTool = tools.tools.find( (t: any) => t.name === "custom_debugger" ); expect(debuggerTool).to.exist; // Check the description matches the override expect(debuggerTool.description).to.equal( "A debugger tool with a custom name" ); }); it("CC2: Override tool name - should register with custom name", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".workflows"), ]); const tools = await client.listTools(); // Find the custom_debugger tool (renamed from debugger_mode) const customNameTool = tools.tools.find( (t: any) => t.name === "custom_debugger" ); expect(customNameTool).to.exist; expect(customNameTool.description).to.equal( "A debugger tool with a custom name" ); // The original debugger_mode shouldn't exist in this case const originalTool = tools.tools.find( (t: any) => t.name === "debugger_mode" ); expect(originalTool).to.not.exist; }); it("CC5: Custom tool - should be available with specified properties", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".workflows"), ]); const tools = await client.listTools(); // Find the custom_tool const customTool = tools.tools.find((t: any) => t.name === "custom_tool"); expect(customTool).to.exist; expect(customTool.description).to.equal("Custom test tool"); }); it("CC6: Optional tool descriptions - should work with and without descriptions", async () => { await client.connect([ "--config", path.join(__dirname, "test-workflows", ".workflows"), ]); const tools = await client.listTools(); // Find the optional_description_mode tool const optionalDescriptionTool = tools.tools.find( (t: any) => t.name === "optional_description_mode" ); expect(optionalDescriptionTool).to.exist; expect(optionalDescriptionTool.description).to.equal( "Mode to test optional tool descriptions" ); // Call the tool to get the prompt const response = await client.callTool("optional_description_mode"); const promptText = response.content[0].text; // Verify that tools with descriptions have them in the prompt expect(promptText).to.include( "**toolWithDescription**: This tool has a description" ); // Verify that tools without descriptions are still listed, but without a colon+description expect(promptText).to.include("**toolWithoutDescription**\n"); // Make sure the pattern is not something like "**toolWithoutDescription**: undefined" expect(promptText).to.not.include("**toolWithoutDescription**:"); // Verify the other tool with description works correctly expect(promptText).to.include( "**anotherToolWithDescription**: Another tool with a description" ); }); }); });