Skip to main content
Glama

Spec Workflow MCP

approvals.ts14.6 kB
import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { ToolContext, ToolResponse } from '../types.js'; import { ApprovalStorage } from '../dashboard/approval-storage.js'; import { join } from 'path'; import { validateProjectPath } from '../core/path-utils.js'; export const approvalsTool: Tool = { name: 'approvals', description: `Manage approval requests through the dashboard interface. # Instructions Use this tool to request, check status, or delete approval requests. The action parameter determines the operation: - 'request': Create a new approval request after creating each document - 'status': Check the current status of an approval request - 'delete': Clean up completed approval requests CRITICAL: Only provide filePath parameter for requests - the dashboard reads files directly. Never include document content. Wait for user to review and approve before continuing.`, inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['request', 'status', 'delete'], description: 'The action to perform: request, status, or delete' }, projectPath: { type: 'string', description: 'Absolute path to the project root (required for request, optional for status/delete)' }, approvalId: { type: 'string', description: 'The ID of the approval request (required for status and delete actions)' }, title: { type: 'string', description: 'Brief title describing what needs approval (required for request action)' }, filePath: { type: 'string', description: 'Path to the file that needs approval, relative to project root (required for request action)' }, type: { type: 'string', enum: ['document', 'action'], description: 'Type of approval request - "document" for content approval, "action" for action approval (required for request)' }, category: { type: 'string', enum: ['spec', 'steering'], description: 'Category of the approval request - "spec" for specifications, "steering" for steering documents (required for request)' }, categoryName: { type: 'string', description: 'Name of the spec or "steering" for steering documents (required for request)' } }, required: ['action'] } }; // Type definitions for discriminated unions type RequestApprovalArgs = { action: 'request'; projectPath: string; title: string; filePath: string; type: 'document' | 'action'; category: 'spec' | 'steering'; categoryName: string; }; type StatusApprovalArgs = { action: 'status'; projectPath?: string; approvalId: string; }; type DeleteApprovalArgs = { action: 'delete'; projectPath?: string; approvalId: string; }; type ApprovalArgs = RequestApprovalArgs | StatusApprovalArgs | DeleteApprovalArgs; // Type guard functions function isRequestApproval(args: ApprovalArgs): args is RequestApprovalArgs { return args.action === 'request'; } function isStatusApproval(args: ApprovalArgs): args is StatusApprovalArgs { return args.action === 'status'; } function isDeleteApproval(args: ApprovalArgs): args is DeleteApprovalArgs { return args.action === 'delete'; } export async function approvalsHandler( args: { action: 'request' | 'status' | 'delete'; projectPath?: string; approvalId?: string; title?: string; filePath?: string; type?: 'document' | 'action'; category?: 'spec' | 'steering'; categoryName?: string; }, context: ToolContext ): Promise<ToolResponse> { // Cast to discriminated union type const typedArgs = args as ApprovalArgs; switch (typedArgs.action) { case 'request': if (isRequestApproval(typedArgs)) { // Validate required fields for request if (!args.projectPath || !args.title || !args.filePath || !args.type || !args.category || !args.categoryName) { return { success: false, message: 'Missing required fields for request action. Required: projectPath, title, filePath, type, category, categoryName' }; } return handleRequestApproval(typedArgs, context); } break; case 'status': if (isStatusApproval(typedArgs)) { // Validate required fields for status if (!args.approvalId) { return { success: false, message: 'Missing required field for status action. Required: approvalId' }; } return handleGetApprovalStatus(typedArgs, context); } break; case 'delete': if (isDeleteApproval(typedArgs)) { // Validate required fields for delete if (!args.approvalId) { return { success: false, message: 'Missing required field for delete action. Required: approvalId' }; } return handleDeleteApproval(typedArgs, context); } break; default: return { success: false, message: `Unknown action: ${(args as any).action}. Use 'request', 'status', or 'delete'.` }; } // This should never be reached due to exhaustive type checking return { success: false, message: 'Invalid action configuration' }; } async function handleRequestApproval( args: RequestApprovalArgs, context: ToolContext ): Promise<ToolResponse> { // No need for validation here as types ensure all fields are present try { // Validate and resolve project path const validatedProjectPath = await validateProjectPath(args.projectPath); const approvalStorage = new ApprovalStorage(validatedProjectPath); await approvalStorage.start(); const approvalId = await approvalStorage.createApproval( args.title, args.filePath, args.category, args.categoryName, args.type ); await approvalStorage.stop(); return { success: true, message: `Approval request created successfully. Please review in dashboard: ${context.dashboardUrl || 'Please start the dashboard or use VS Code extension "Spec Workflow MCP"'}`, data: { approvalId, title: args.title, filePath: args.filePath, type: args.type, status: 'pending', dashboardUrl: context.dashboardUrl }, nextSteps: [ 'BLOCKING - Dashboard or VS Code extension approval required', 'VERBAL APPROVAL NOT ACCEPTED', 'Do not proceed on verbal confirmation', context.dashboardUrl ? `Use dashboard: ${context.dashboardUrl} or VS Code extension 'Spec Workflow MCP'` : 'VS Code extension Spec Workflow MCP', `Poll status with: approvals action:"status" approvalId:"${approvalId}"` ], projectContext: { projectPath: validatedProjectPath, workflowRoot: join(validatedProjectPath, '.spec-workflow'), dashboardUrl: context.dashboardUrl } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, message: `Failed to create approval request: ${errorMessage}` }; } } async function handleGetApprovalStatus( args: StatusApprovalArgs, context: ToolContext ): Promise<ToolResponse> { // approvalId is guaranteed by type try { // Use provided projectPath or fall back to context const projectPath = args.projectPath || context.projectPath; if (!projectPath) { return { success: false, message: 'Project path is required. Please provide projectPath parameter.' }; } // Validate and resolve project path const validatedProjectPath = await validateProjectPath(projectPath); const approvalStorage = new ApprovalStorage(validatedProjectPath); await approvalStorage.start(); const approval = await approvalStorage.getApproval(args.approvalId); if (!approval) { await approvalStorage.stop(); return { success: false, message: `Approval request not found: ${args.approvalId}` }; } await approvalStorage.stop(); const isCompleted = approval.status === 'approved' || approval.status === 'rejected'; const canProceed = approval.status === 'approved'; const mustWait = approval.status !== 'approved'; const nextSteps: string[] = []; if (approval.status === 'pending') { nextSteps.push('BLOCKED - Do not proceed'); nextSteps.push('VERBAL APPROVAL NOT ACCEPTED - Use dashboard or VS Code extension only'); nextSteps.push('Approval must be done via dashboard or VS Code extension'); nextSteps.push('Continue polling with approvals action:"status"'); } else if (approval.status === 'approved') { nextSteps.push('APPROVED - Can proceed'); nextSteps.push('Run approvals action:"delete" before continuing'); if (approval.response) { nextSteps.push(`Response: ${approval.response}`); } } else if (approval.status === 'rejected') { nextSteps.push('BLOCKED - REJECTED'); nextSteps.push('Do not proceed'); nextSteps.push('Review feedback and revise'); if (approval.response) { nextSteps.push(`Reason: ${approval.response}`); } if (approval.annotations) { nextSteps.push(`Notes: ${approval.annotations}`); } } else if (approval.status === 'needs-revision') { nextSteps.push('BLOCKED - Do not proceed'); nextSteps.push('Update document with feedback'); nextSteps.push('Create NEW approval request'); if (approval.response) { nextSteps.push(`Feedback: ${approval.response}`); } if (approval.annotations) { nextSteps.push(`Notes: ${approval.annotations}`); } if (approval.comments && approval.comments.length > 0) { nextSteps.push(`${approval.comments.length} comments for targeted fixes:`); // Add each comment to nextSteps for visibility approval.comments.forEach((comment, index) => { if (comment.type === 'selection' && comment.selectedText) { nextSteps.push(` Comment ${index + 1} on "${comment.selectedText.substring(0, 50)}...": ${comment.comment}`); } else { nextSteps.push(` Comment ${index + 1} (general): ${comment.comment}`); } }); } } return { success: true, message: approval.status === 'pending' ? `BLOCKED: Status is ${approval.status}. Verbal approval is NOT accepted. Use dashboard or VS Code extension only.` : `Approval status: ${approval.status}`, data: { approvalId: args.approvalId, title: approval.title, type: approval.type, status: approval.status, createdAt: approval.createdAt, respondedAt: approval.respondedAt, response: approval.response, annotations: approval.annotations, comments: approval.comments, isCompleted, canProceed, mustWait, blockNext: !canProceed, dashboardUrl: context.dashboardUrl }, nextSteps, projectContext: { projectPath: validatedProjectPath, workflowRoot: join(validatedProjectPath, '.spec-workflow'), dashboardUrl: context.dashboardUrl } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, message: `Failed to check approval status: ${errorMessage}` }; } } async function handleDeleteApproval( args: DeleteApprovalArgs, context: ToolContext ): Promise<ToolResponse> { // approvalId is guaranteed by type try { // Use provided projectPath or fall back to context const projectPath = args.projectPath || context.projectPath; if (!projectPath) { return { success: false, message: 'Project path is required. Please provide projectPath parameter.' }; } // Validate and resolve project path const validatedProjectPath = await validateProjectPath(projectPath); const approvalStorage = new ApprovalStorage(validatedProjectPath); await approvalStorage.start(); // Check if approval exists and its status const approval = await approvalStorage.getApproval(args.approvalId); if (!approval) { return { success: false, message: `Approval request "${args.approvalId}" not found`, nextSteps: [ 'Verify approval ID', 'Check status with approvals action:"status"' ] }; } // Only allow deletion of approved requests if (approval.status !== 'approved') { return { success: false, message: `BLOCKED: Cannot proceed - status is "${approval.status}". VERBAL APPROVAL NOT ACCEPTED. Use dashboard or VS Code extension.`, data: { approvalId: args.approvalId, currentStatus: approval.status, title: approval.title, blockProgress: true, canProceed: false }, nextSteps: [ 'STOP - Do not proceed to next phase', 'Wait for approval', 'Poll with approvals action:"status"' ] }; } // Delete the approval const deleted = await approvalStorage.deleteApproval(args.approvalId); await approvalStorage.stop(); if (deleted) { return { success: true, message: `Approval request "${args.approvalId}" deleted successfully`, data: { deletedApprovalId: args.approvalId, title: approval.title, category: approval.category, categoryName: approval.categoryName }, nextSteps: [ 'Cleanup complete', 'Continue to next phase' ], projectContext: { projectPath: validatedProjectPath, workflowRoot: join(validatedProjectPath, '.spec-workflow'), dashboardUrl: context.dashboardUrl } }; } else { return { success: false, message: `Failed to delete approval request "${args.approvalId}"`, nextSteps: [ 'Check file permissions', 'Verify approval exists', 'Retry' ] }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, message: `Failed to delete approval: ${errorMessage}`, nextSteps: [ 'Check project path', 'Verify permissions', 'Check approval system' ] }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Pimzino/spec-workflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server