dfa-mcp-server
Version:
DFA-based workflow MCP server for guiding LLM task completion
902 lines (840 loc) • 29.8 kB
JavaScript
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const zod_1 = require("zod");
const workflow_engine_js_1 = require("./workflow-engine.js");
// Initialize workflow engine
const engine = new workflow_engine_js_1.WorkflowEngine();
// Create MCP server
const server = new mcp_js_1.McpServer({
name: "dfa-workflow-server",
version: "0.2.0"
});
// Tool: Define a new workflow
server.registerTool("workflow.define", {
title: "Define Workflow",
description: `Define a new workflow type with states and transitions. A workflow is a state machine that controls process flow.
IMPORTANT: Transitions now use conditional rules evaluated by LLM for intelligent routing.
Example with conditional transitions:
{
"name": "smart-review",
"description": "Intelligent document review with conditional routing",
"states": {
"submitted": {
"transitions": {
"analyze": [
{
"condition": "context.documentType === 'legal' && context.requiresCompliance === true",
"target": "legal_review",
"description": "Legal documents need specialized review"
},
{
"condition": "context.wordCount > 10000 || context.complexity === 'high'",
"target": "senior_review",
"description": "Long or complex documents need senior review"
},
{
"condition": "context.priority === 'urgent' && context.riskLevel < 5",
"target": "fast_track",
"description": "Urgent but low-risk items can be fast-tracked"
},
{
"condition": "true",
"target": "standard_review",
"description": "Default path for all other cases"
}
]
}
},
"standard_review": {
"transitions": {
"decide": [
{ "condition": "context.reviewScore >= 8", "target": "approved" },
{ "condition": "context.reviewScore >= 5", "target": "needs_revision" },
{ "condition": "true", "target": "rejected" }
]
}
},
"approved": { "final": true },
"rejected": { "final": true }
},
"initialState": "submitted"
}
Simple workflow (always true conditions):
{
"name": "basic-approval",
"states": {
"draft": {
"transitions": {
"submit": [{ "condition": "true", "target": "review" }]
}
},
"review": {
"transitions": {
"approve": [{ "condition": "true", "target": "approved" }],
"reject": [{ "condition": "true", "target": "rejected" }]
}
},
"approved": { "final": true },
"rejected": { "final": true }
},
"initialState": "draft"
}
Conditions are code-like expressions evaluated by LLM with context understanding:
- Simple: "true" (always matches)
- Comparison: "context.amount > 1000"
- String: "context.status === 'active'"
- Complex: "context.score > 7 && (context.category === 'A' || context.override === true)"
- Nested: "context.user.role === 'admin' && context.user.permissions.includes('approve')"
The LLM can intelligently infer values not explicitly in context and reason about conditions.`,
inputSchema: {
name: zod_1.z.string().describe("Unique workflow name (alphanumeric with hyphens/underscores)"),
description: zod_1.z.string().optional().describe("Human-readable description of the workflow's purpose"),
states: zod_1.z.record(zod_1.z.object({
transitions: zod_1.z.record(zod_1.z.array(zod_1.z.object({
condition: zod_1.z.string(),
target: zod_1.z.string(),
description: zod_1.z.string().optional()
}))).optional(),
final: zod_1.z.boolean().optional()
})).describe("Object where keys are state names and values define transitions or final status"),
initialState: zod_1.z.string().describe("The state name where new workflow instances will start")
}
}, async ({ name, description, states, initialState }) => {
try {
const definition = {
name,
description,
states,
initialState
};
await engine.registerWorkflow(definition);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Workflow '${name}' registered successfully`,
definition
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Start a new workflow
server.registerTool("workflow.start", {
title: "Start Workflow",
description: `Start a new workflow instance. Creates a unique instance of a previously defined workflow type.
Example request:
{
"type": "document-review",
"context": {
"documentId": "DOC-123",
"author": "john.doe@example.com",
"createdAt": "2024-01-10T10:00:00Z"
}
}
Returns:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"state": "draft",
"nextActions": ["submit"],
"progress": "Current state: draft"
}
The workflow ID returned should be used for all subsequent operations.
Context data is optional but can store any information needed throughout the workflow lifecycle.`,
inputSchema: {
type: zod_1.z.string().describe("The name of a previously defined workflow type"),
context: zod_1.z.any().optional().describe("Initial data/context for the workflow instance (any JSON object)")
}
}, async ({ type, context = {} }) => {
try {
const instance = await engine.createWorkflow(type, context);
const status = await engine.getStatus(instance.id);
return {
content: [{
type: "text",
text: JSON.stringify({
id: instance.id,
state: status.state,
nextActions: status.nextActions,
progress: status.progress
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Advance workflow state
server.registerTool("workflow.advance", {
title: "Advance Workflow",
description: `Move workflow to next state by performing an action. The target state is determined by evaluating conditional rules.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"action": "route",
"data": {
"amount": 5000,
"priority": "high",
"department": "finance"
},
"expectedTargetState": "manager_approval" // Optional: your expectation
}
Success response (conditions evaluated, routed to manager_approval):
{
"state": "manager_approval",
"nextActions": ["approve", "reject", "escalate"],
"progress": "Requires manager approval (amount > 1000)",
"complete": false
}
Response with target mismatch warning:
{
"state": "ceo_approval", // Actual state based on conditions
"warning": "Based on condition \\"context.amount > 10000\\", workflow engine has determined 'ceo_approval' as target state and changed the state to 'ceo_approval' instead of 'manager_approval'.",
"conditionMatched": "context.amount > 10000",
"conditionDescription": "Large purchases need CEO approval",
"nextActions": ["approve", "reject"],
"complete": false
}
How it works:
1. The action must exist in current state's transitions
2. All conditions for that action are evaluated using LLM
3. The first matching condition determines the target state
4. If expectedTargetState differs, a warning is included but transition proceeds
5. The transition is then validated by the judge
Example with conditional routing:
Current state has: "route": [
{ "condition": "context.amount > 10000", "target": "ceo_approval" },
{ "condition": "context.amount > 1000", "target": "manager_approval" },
{ "condition": "true", "target": "auto_approved" }
]
With context.amount = 5000, it matches the second condition and routes to "manager_approval".
The 'data' parameter updates the workflow context and may be validated by the judge.
The 'expectedTargetState' parameter is optional and helps verify your understanding of the routing logic.`,
inputSchema: {
id: zod_1.z.string().describe("The workflow instance ID from workflow.start"),
action: zod_1.z.string().describe("The action to perform (must be valid for current state)"),
data: zod_1.z.any().optional().describe("Data to include with the transition (updates workflow context)"),
expectedTargetState: zod_1.z.string().optional().describe("Optional: Expected target state for verification")
}
}, async ({ id, action, data, expectedTargetState }) => {
try {
const result = await engine.transition(id, action, data, expectedTargetState);
return {
content: [{
type: "text",
text: JSON.stringify({
state: result.state,
nextActions: result.nextActions,
progress: result.progress,
complete: result.complete
}, null, 2)
}]
};
}
catch (error) {
// Check if this is a judge rejection
if (error instanceof Error && error.judgeDecision) {
const decision = error.judgeDecision;
return {
content: [{
type: "text",
text: JSON.stringify({
error: "Transition rejected by judge",
decision: decision
}, null, 2)
}],
isError: true
};
}
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Get workflow status
server.registerTool("workflow.status", {
title: "Get Workflow Status",
description: `Get current status of a workflow instance. Shows current state, available actions, and context.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000"
}
Response:
{
"state": "reviewing",
"context": {
"documentId": "DOC-123",
"author": "john.doe@example.com",
"reviewerEmail": "manager@example.com",
"priority": "high",
"lastAction": "submit"
},
"nextActions": ["approve", "reject", "request_info"],
"progress": "Current state: reviewing",
"complete": false
}
Note: If context is very large (>100KB), it will be truncated with a warning.
Use this to check workflow state before deciding which action to take next.`,
inputSchema: {
id: zod_1.z.string().describe("The workflow instance ID to check status for")
}
}, async ({ id }) => {
try {
const status = await engine.getStatus(id);
return {
content: [{
type: "text",
text: JSON.stringify({
state: status.state,
context: status.context,
nextActions: status.nextActions,
progress: status.progress,
complete: status.complete
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Create checkpoint
server.registerTool("workflow.checkpoint", {
title: "Create Checkpoint",
description: `Create a checkpoint (snapshot) of workflow state. Allows rollback to this point later.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"description": "Before approval process",
"metadata": {
"reason": "Saving state before risky operation",
"createdBy": "system"
}
}
Response:
{
"id": "cp-660e8400-e29b-41d4-a716-446655440001",
"workflowId": "wf-550e8400-e29b-41d4-a716-446655440000",
"state": "reviewing",
"timestamp": "2024-01-10T11:00:00Z",
"description": "Before approval process",
"metadata": {
"reason": "Saving state before risky operation",
"createdBy": "system"
}
}
Checkpoints capture:
- Current workflow state
- Complete context data
- Timestamp and metadata
Use cases:
- Save state before complex operations
- Create restore points for testing
- Implement "undo" functionality`,
inputSchema: {
id: zod_1.z.string().describe("The workflow instance ID"),
description: zod_1.z.string().optional().describe("Human-readable description of why checkpoint was created"),
metadata: zod_1.z.any().optional().describe("Any additional data to store with checkpoint")
}
}, async ({ id, description, metadata }) => {
try {
const checkpoint = await engine.createCheckpoint(id, description, metadata);
return {
content: [{
type: "text",
text: JSON.stringify({
checkpointId: checkpoint.id,
workflowId: checkpoint.workflowId,
state: checkpoint.state,
timestamp: checkpoint.timestamp,
description: checkpoint.description
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Rollback to checkpoint
server.registerTool("workflow.rollback", {
title: "Rollback to Checkpoint",
description: `Rollback workflow to a previous checkpoint. Restores state and context from the checkpoint.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"checkpointId": "cp-660e8400-e29b-41d4-a716-446655440001"
}
Response:
{
"state": "reviewing",
"context": {
"documentId": "DOC-123",
"author": "john.doe@example.com",
"reviewerEmail": "manager@example.com",
"lastAction": "submit"
},
"nextActions": ["approve", "reject", "request_info"],
"progress": "Current state: reviewing",
"complete": false
}
Effects of rollback:
- Workflow state is restored to checkpoint state
- Context is completely replaced with checkpoint context
- Any changes made after checkpoint are lost
- Transition history shows ROLLBACK action
Use workflow.listCheckpoints first to find available checkpoints.`,
inputSchema: {
id: zod_1.z.string().describe("The workflow instance ID"),
checkpointId: zod_1.z.string().describe("The checkpoint ID to restore (from workflow.listCheckpoints)")
}
}, async ({ id, checkpointId }) => {
try {
const result = await engine.rollbackToCheckpoint(id, checkpointId);
return {
content: [{
type: "text",
text: JSON.stringify({
state: result.state,
context: result.context,
nextActions: result.nextActions,
progress: result.progress,
message: "Successfully rolled back to checkpoint"
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: List workflows
server.registerTool("workflow.list", {
title: "List Workflows",
description: `List all registered workflow types. Shows available workflow definitions that can be instantiated.
Example request: {} (no parameters needed)
Response:
{
"workflows": [
{
"name": "document-review",
"description": "Document review and approval process"
},
{
"name": "user-onboarding",
"description": "New user onboarding workflow"
}
],
"count": 2
}
Use this to discover available workflow types before calling workflow.start.`,
inputSchema: {}
}, async () => {
try {
const workflows = engine.listWorkflows();
return {
content: [{
type: "text",
text: JSON.stringify({
workflows,
count: workflows.length
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Validate transition without executing
server.registerTool("workflow.judge.validate", {
title: "Validate Transition",
description: `Validate a transition without executing it. Uses the judge to check if an action would be allowed.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"action": "approve",
"data": {
"comments": "Looks good",
"approvalLevel": "manager"
}
}
Response (validation passes):
{
"approved": true,
"confidence": 0.95,
"reasoning": "✅ draft → [submit] → reviewing validated successfully with 95% confidence"
}
Response (validation fails):
{
"approved": false,
"confidence": 0.2,
"reasoning": "❌ Invalid Action Error\\n──────────────\\n📍 Current State: 'draft'\\n🚫 Attempted Action: 'approve'\\n...",
"violations": ["Action 'approve' not available in current state"],
"suggestions": [
"Valid actions for state 'draft':\\n • submit",
"Example: workflow.advance({ id: \\"...\\", action: \\"submit\\", data: { ... } })"
]
}
This is useful for checking if an action is valid before attempting it, or for providing UI hints about available actions.`,
inputSchema: {
id: zod_1.z.string().describe("The workflow instance ID"),
action: zod_1.z.string().describe("The action to validate"),
data: zod_1.z.any().optional().describe("Data that would be sent with the transition"),
expectedTargetState: zod_1.z.string().optional().describe("Optional: Expected target state for verification")
}
}, async ({ id, action, data, expectedTargetState }) => {
try {
const decision = await engine.validateTransition(id, action, data, expectedTargetState);
return {
content: [{
type: "text",
text: JSON.stringify(decision, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Get judge history
server.registerTool("workflow.judge.history", {
title: "Judge History",
description: `Get judge decision history for a workflow. Shows all validation decisions made by the judge.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"limit": 10,
"offset": 0
}
Response:
{
"workflowId": "wf-550e8400-e29b-41d4-a716-446655440000",
"decisions": [
{
"approved": true,
"confidence": 0.95,
"reasoning": "Transition validated successfully",
"metadata": { "timestamp": "2024-01-10T10:30:00Z" }
},
{
"approved": false,
"confidence": 0.3,
"reasoning": "Missing required fields",
"violations": ["Missing required fields: reviewerEmail"],
"suggestions": ["Include reviewerEmail in transition data"],
"metadata": { "timestamp": "2024-01-10T10:25:00Z" }
}
],
"count": 2,
"total": 15,
"hasMore": true,
"pagination": { "limit": 10, "offset": 0 }
}
History includes both successful and failed validation attempts.
Use pagination (limit/offset) to browse through large histories.`,
inputSchema: {
id: zod_1.z.string().describe("The workflow instance ID"),
limit: zod_1.z.number().optional().describe("Max number of decisions to return (default: 20, max: 100)"),
offset: zod_1.z.number().optional().describe("Number of decisions to skip for pagination (default: 0)")
}
}, async ({ id, limit, offset }) => {
try {
const history = await engine.getJudgeHistory(id, limit, offset);
return {
content: [{
type: "text",
text: JSON.stringify({
workflowId: id,
decisions: history.decisions,
count: history.decisions.length,
total: history.total,
hasMore: history.hasMore,
pagination: {
limit: limit || 20,
offset: offset || 0
}
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: Update judge config
server.registerTool("workflow.judge.config", {
title: "Configure Judge",
description: `Update judge configuration for a workflow. Controls how strictly transitions are validated.
Example request:
{
"name": "document-review",
"enabled": true,
"strictMode": true,
"minConfidence": 0.8,
"useLLM": true
}
Configuration options:
- enabled: Turn judge on/off (false = all transitions auto-approved)
- strictMode: When true, rejects transitions below minConfidence threshold
- minConfidence: 0.0-1.0, threshold for approval in strict mode (0.8 = 80%)
- useLLM: Use AI model for intelligent validation vs rule-based only
Example configurations:
Strict validation (production):
{ "enabled": true, "strictMode": true, "minConfidence": 0.9, "useLLM": true }
Relaxed validation (development):
{ "enabled": true, "strictMode": false, "minConfidence": 0.5, "useLLM": false }
Bypass validation (testing):
{ "enabled": false }
Note: LLM validation requires LLM_BASE_URL and LLM_API_KEY environment variables.`,
inputSchema: {
name: zod_1.z.string().describe("The workflow type name to configure"),
enabled: zod_1.z.boolean().describe("Enable (true) or disable (false) the judge entirely"),
strictMode: zod_1.z.boolean().optional().describe("In strict mode, transitions below minConfidence are rejected"),
minConfidence: zod_1.z.number().min(0).max(1).optional().describe("Confidence threshold for strict mode (0.0-1.0)"),
useLLM: zod_1.z.boolean().optional().describe("Use LLM for intelligent validation (requires API credentials)")
}
}, async ({ name, enabled, strictMode, minConfidence, useLLM }) => {
try {
const judgeConfig = {
enabled,
strictMode,
minConfidence,
useLLM
};
await engine.updateJudgeConfig(name, judgeConfig);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Judge configuration updated for workflow '${name}'`,
config: judgeConfig
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Tool: List checkpoints
server.registerTool("workflow.listCheckpoints", {
title: "List Checkpoints",
description: `List all checkpoints for a workflow. Shows available restore points sorted by newest first.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000"
}
Response:
{
"workflowId": "wf-550e8400-e29b-41d4-a716-446655440000",
"checkpoints": [
{
"id": "cp-660e8400-e29b-41d4-a716-446655440002",
"state": "approved",
"timestamp": "2024-01-10T12:00:00Z",
"description": "After final approval",
"hasMetadata": true
},
{
"id": "cp-660e8400-e29b-41d4-a716-446655440001",
"state": "reviewing",
"timestamp": "2024-01-10T11:00:00Z",
"description": "Before approval process",
"hasMetadata": true
}
],
"count": 2
}
Checkpoints are sorted newest first. Use the checkpoint ID with workflow.rollback to restore.`,
inputSchema: {
id: zod_1.z.string().describe("The workflow instance ID")
}
}, async ({ id }) => {
try {
const checkpoints = await engine.listCheckpoints(id);
return {
content: [{
type: "text",
text: JSON.stringify({
workflowId: id,
checkpoints: checkpoints.map(cp => ({
id: cp.id,
state: cp.state,
timestamp: cp.timestamp,
description: cp.description,
hasMetadata: !!cp.metadata
})),
count: checkpoints.length
}, null, 2)
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
// Register a prompt for workflow help
server.registerPrompt("workflow-guide", {
title: "Workflow Guide",
description: "Get guidance on using the workflow system",
argsSchema: {}
}, () => ({
messages: [{
role: "assistant",
content: {
type: "text",
text: `# Generic DFA Workflow System Guide
## Available Tools:
1. **workflow.define** - Define a new workflow type
- name: Unique workflow name
- description: Optional description
- states: Object with state definitions
- initialState: The starting state
2. **workflow.list** - List all registered workflows
3. **workflow.start** - Start a new workflow instance
- type: The workflow type name
- context: Optional initial context data
4. **workflow.advance** - Move to the next state
- id: The workflow ID
- action: The action to perform (check nextActions from status)
- data: Optional data for the action
5. **workflow.status** - Check workflow status
- id: The workflow ID
6. **workflow.checkpoint** - Create a checkpoint
- id: The workflow ID
- description: Optional description
- metadata: Optional metadata to store
7. **workflow.rollback** - Rollback to a checkpoint
- id: The workflow ID
- checkpointId: The checkpoint to rollback to
8. **workflow.listCheckpoints** - List all checkpoints
- id: The workflow ID
## Defining a Workflow:
States must include:
- transitions: Object mapping actions to next states
- final: Boolean indicating terminal states
Example workflow definition:
{
"name": "approval-process",
"description": "Generic approval workflow",
"states": {
"draft": {
"transitions": { "submit": "pending" }
},
"pending": {
"transitions": {
"approve": "approved",
"reject": "rejected",
"request_changes": "draft"
}
},
"approved": { "final": true },
"rejected": { "final": true }
},
"initialState": "draft"
}
## Example Usage:
1. Define: workflow.define({ name: "my-workflow", states: {...}, initialState: "start" })
2. List: workflow.list()
3. Start: workflow.start({ type: "my-workflow", context: { data: "value" } })
4. Advance: workflow.advance({ id: "wf-123", action: "submit" })
5. Checkpoint: workflow.checkpoint({ id: "wf-123", description: "Before review" })
6. Status: workflow.status({ id: "wf-123" })
7. Rollback: workflow.rollback({ id: "wf-123", checkpointId: "cp-xxx" })
## Key Features:
- Define any workflow with custom states and transitions
- Context is completely generic - store any data
- Checkpoints save complete state for recovery
- Prevents LLMs from losing context in multi-step processes`
}
}]
}));
// Main server initialization
async function main() {
// Initialize the workflow engine
await engine.initialize();
// Connect to stdio transport
const transport = new stdio_js_1.StdioServerTransport();
await server.connect(transport);
console.error("DFA Workflow MCP Server started");
}
// Run the server
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
//# sourceMappingURL=index.js.map