@agentdesk/workflows-mcp
Version:
MCP workflow orchestration tool with presets for thinking, coding and more
508 lines (419 loc) • 17.6 kB
text/typescript
/// <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"
);
});
});
});