@pixel-normal-edit/mcp 2.0.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * prerequisites.js — Prerequisites Check
4
+ *
5
+ * This is an MCP Tool for the agent to call before starting to draw.
6
+ * It will check all prerequisites and return detailed instructions.
7
+ *
8
+ * IMPORTANT: The Agent MUST call this tool FIRST
9
+ * before performing any drawing operations.
10
+ */
11
+
12
+ const { z } = require('zod');
13
+ const { validateCanvasName, validateCanvasSize, DRAWING_CONSTRAINTS, MODES, DRAWING_PHASES } = require('./rules');
14
+ const workflow = require('./workflow');
15
+ const { registerTool } = require('../core/server');
16
+
17
+ /**
18
+ * Register workflow/prerequisite tools on the MCP server
19
+ * @param {McpServer} server
20
+ */
21
+ function register(server) {
22
+ registerTool(server, 'workflow_start',
23
+ `🚀 REQUIRED: Call this tool FIRST before drawing anything.
24
+ This tool initializes a drawing workflow with sequential steps for a specific mode.
25
+ Modes: CREATE_IMAGE, EDIT_IMAGE, CREATE_ANIMATION`,
26
+ { mode: z.enum([MODES.CREATE_IMAGE, MODES.EDIT_IMAGE, MODES.CREATE_ANIMATION]).describe('Workflow mode to start') },
27
+ (p) => {
28
+ const session = require('../core/command-bus').SESSION;
29
+ const state = workflow.start(session, p.mode);
30
+ return {
31
+ content: [{ type: 'text', text: `🚀 **Workflow started in mode: ${p.mode}!**\n\n${workflow.getProgressReport(session)}\n\n---\nStart with step: **${state.steps[0].label}** → ${state.steps[0].description}` }]
32
+ };
33
+ });
34
+
35
+ registerTool(server, 'workflow_status',
36
+ 'View current workflow progress: current mode, layer, frame, steps and statuses.',
37
+ {},
38
+ () => {
39
+ const session = require('../core/command-bus').SESSION;
40
+ const report = workflow.getProgressReport(session);
41
+ const current = workflow.getCurrentStep(session);
42
+ if (!current) {
43
+ return { content: [{ type: 'text', text: '⚠️ Workflow has not started. Call **workflow_start** first.' }] };
44
+ }
45
+ return {
46
+ content: [{
47
+ type: 'text',
48
+ text: `${report}\n\n👉 **Pending:** ${current.label} — ${current.description}`
49
+ }]
50
+ };
51
+ });
52
+
53
+ registerTool(server, 'workflow_advance',
54
+ '✅ Mark current step as complete and move to the next step. Only call AFTER successfully finishing the current step.',
55
+ {},
56
+ () => {
57
+ const session = require('../core/command-bus').SESSION;
58
+ const result = workflow.advance(session);
59
+ if (!result.success) {
60
+ return { isError: true, content: [{ type: 'text', text: `❌ ${result.message}` }] };
61
+ }
62
+ if (!result.nextStep) {
63
+ return { content: [{ type: 'text', text: `🎉 ${result.message}` }] };
64
+ }
65
+ return { content: [{ type: 'text', text: result.message }] };
66
+ });
67
+
68
+ registerTool(server, 'workflow_validate',
69
+ `🔍 Check if the agent is allowed to perform a certain action.
70
+ Call this tool BEFORE performing operations to ensure correct workflow order.`,
71
+ { stepId: z.string().describe('ID of the step to validate (e.g., "INITIALIZE_CANVAS")') },
72
+ (p) => {
73
+ const session = require('../core/command-bus').SESSION;
74
+ const result = workflow.validateStep(session, p.stepId);
75
+ if (!result.allowed) {
76
+ return { isError: true, content: [{ type: 'text', text: result.message }] };
77
+ }
78
+ return { content: [{ type: 'text', text: result.message || `✅ You can perform "${p.stepId}". Please proceed!` }] };
79
+ });
80
+
81
+ registerTool(server, 'workflow_set_layer',
82
+ 'Set the active layer for the workflow tracking.',
83
+ { layer: z.number().int().describe('The layer index') },
84
+ (p) => {
85
+ const session = require('../core/command-bus').SESSION;
86
+ const success = workflow.setLayerAndFrame(session, p.layer, undefined);
87
+ if (!success) {
88
+ return { isError: true, content: [{ type: 'text', text: '❌ Workflow not started' }] };
89
+ }
90
+ return { content: [{ type: 'text', text: `✅ Active layer set to ${p.layer}` }] };
91
+ });
92
+
93
+ registerTool(server, 'workflow_set_frame',
94
+ 'Set the active frame for the workflow tracking (useful for animation mode).',
95
+ { frame: z.number().int().describe('The frame index') },
96
+ (p) => {
97
+ const session = require('../core/command-bus').SESSION;
98
+ const success = workflow.setLayerAndFrame(session, undefined, p.frame);
99
+ if (!success) {
100
+ return { isError: true, content: [{ type: 'text', text: '❌ Workflow not started' }] };
101
+ }
102
+ return { content: [{ type: 'text', text: `✅ Active frame set to ${p.frame}` }] };
103
+ });
104
+
105
+ registerTool(server, 'workflow_set_phase',
106
+ 'Set the active drawing phase (e.g. PHASE_0_SKETCH_BLOCKING, PHASE_1_BACKGROUND, etc.).',
107
+ { phase: z.enum(DRAWING_PHASES).describe('The drawing phase') },
108
+ (p) => {
109
+ const session = require('../core/command-bus').SESSION;
110
+ const success = workflow.setPhase(session, p.phase);
111
+ if (!success) {
112
+ return { isError: true, content: [{ type: 'text', text: '❌ Workflow not started' }] };
113
+ }
114
+ return { content: [{ type: 'text', text: `✅ Active phase set to ${p.phase}` }] };
115
+ });
116
+
117
+ registerTool(server, 'workflow_setup_animation',
118
+ 'Declare animation metadata for tracking (fps, total_frames, loop).',
119
+ {
120
+ fps: z.number().int().optional().describe('Frames per second'),
121
+ total_frames: z.number().int().optional().describe('Total frames in the animation'),
122
+ loop: z.boolean().optional().describe('Whether the animation loops')
123
+ },
124
+ (p) => {
125
+ const session = require('../core/command-bus').SESSION;
126
+ const success = workflow.setAnimationMeta(session, p.fps, p.total_frames, p.loop);
127
+ if (!success) {
128
+ return { isError: true, content: [{ type: 'text', text: '❌ Workflow not started' }] };
129
+ }
130
+ return { content: [{ type: 'text', text: `✅ Animation meta updated: FPS=${p.fps}, Frames=${p.total_frames}, Loop=${p.loop}` }] };
131
+ });
132
+
133
+ registerTool(server, 'workflow_create_input_request',
134
+ 'Pause the workflow and ask the user for specific input via the UI (e.g. CANVAS_SIZE, APPROVE_PLAN, SELECT_EDIT_REGION).',
135
+ {
136
+ type: z.enum(['CANVAS_SIZE', 'APPROVE_PLAN', 'REVIEW_RESULT', 'SELECT_EDIT_REGION', 'ANIMATION_SETUP']).describe('The type of request'),
137
+ fields: z.record(z.any()).optional().describe('Context or fields for the request')
138
+ },
139
+ async (p) => {
140
+ const { sendCommand, SESSION } = require('../core/command-bus');
141
+ const reqId = workflow.createInputRequest(SESSION, p.type, p.fields || {});
142
+ if (!reqId) {
143
+ return { isError: true, content: [{ type: 'text', text: '❌ Workflow not started' }] };
144
+ }
145
+
146
+ const res = await sendCommand({ action: 'showUserInputRequest', type: p.type, fields: p.fields || {}, reqId }, 35000);
147
+
148
+ if (!res.isError) {
149
+ workflow.completeInputRequest(SESSION, reqId, res.content[0].text);
150
+ }
151
+
152
+ return res;
153
+ });
154
+
155
+ registerTool(server, 'workflow_require_approval',
156
+ 'Pause the workflow and require user approval before continuing.',
157
+ { required: z.boolean().describe('True to pause and wait for user, False to resume') },
158
+ (p) => {
159
+ const session = require('../core/command-bus').SESSION;
160
+ const success = workflow.setApprovalRequired(session, p.required);
161
+ if (!success) {
162
+ return { isError: true, content: [{ type: 'text', text: '❌ Workflow not started' }] };
163
+ }
164
+ return { content: [{ type: 'text', text: p.required ? '✅ Workflow paused, waiting for user approval.' : '✅ Workflow resumed.' }] };
165
+ });
166
+
167
+ registerTool(server, 'workflow_reset',
168
+ '🔄 Reset workflow to initial state. Clears all progress.',
169
+ {},
170
+ () => {
171
+ const session = require('../core/command-bus').SESSION;
172
+ workflow.reset(session);
173
+ return { content: [{ type: 'text', text: '🔄 Workflow reset. Please start from the beginning.' }] };
174
+ });
175
+
176
+ registerTool(server, 'validate_canvas_name',
177
+ `Validate canvas name before setting it.
178
+ Rules: 3-64 characters, letters, numbers, _, -, and spaces only.
179
+ Example: "Painting_1", "My Canvas 2024"`,
180
+ { name: z.string().describe('Canvas name to validate') },
181
+ (p) => {
182
+ const result = validateCanvasName(p.name);
183
+ if (!result.valid) {
184
+ return { isError: true, content: [{ type: 'text', text: `❌ ${result.message}` }] };
185
+ }
186
+ return { content: [{ type: 'text', text: `✅ Name "${p.name}" is valid!` }] };
187
+ });
188
+
189
+ registerTool(server, 'validate_canvas_size',
190
+ `Validate if canvas size is within allowed limits.
191
+ Limits: ${DRAWING_CONSTRAINTS.minCanvasSize.width}x${DRAWING_CONSTRAINTS.minCanvasSize.height} to ${DRAWING_CONSTRAINTS.maxCanvasSize.width}x${DRAWING_CONSTRAINTS.maxCanvasSize.height}`,
192
+ { width: z.number().int().describe('Width'), height: z.number().int().describe('Height') },
193
+ (p) => {
194
+ const result = validateCanvasSize(p.width, p.height);
195
+ if (!result.valid) {
196
+ return { isError: true, content: [{ type: 'text', text: `❌ ${result.message}` }] };
197
+ }
198
+ return { content: [{ type: 'text', text: `✅ Size ${p.width}x${p.height} is valid!` }] };
199
+ });
200
+ }
201
+
202
+ module.exports = { register };
package/rules/rules.js ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * rules.js — Core Drawing Rules & Constraints
4
+ *
5
+ * IMPORTANT: Every function in here is a STRICT rule.
6
+ * The Agent MUST NOT skip or execute them out of order.
7
+ */
8
+
9
+ const NAMING_RULES = {
10
+ canvasTab: {
11
+ pattern: /^[A-Za-z0-9_\-\sÀ-ỹ]{3,64}$/,
12
+ message: 'Canvas name must be 3-64 characters long, containing only letters, numbers, _, -, and spaces',
13
+ example: 'Painting_1',
14
+ },
15
+ stamp: {
16
+ pattern: /^[a-z][a-z0-9_]{2,32}$/,
17
+ message: 'Stamp name must start with a lowercase letter, 3-32 characters long, containing only a-z, 0-9, _',
18
+ example: 'my_tree_01',
19
+ },
20
+ anchor: {
21
+ pattern: /^[a-z][a-z0-9_]{2,32}$/,
22
+ message: 'Anchor name must start with a lowercase letter, 3-32 characters long',
23
+ example: 'head_top',
24
+ },
25
+ };
26
+
27
+ const MODES = {
28
+ CREATE_IMAGE: 'CREATE_IMAGE',
29
+ EDIT_IMAGE: 'EDIT_IMAGE',
30
+ CREATE_ANIMATION: 'CREATE_ANIMATION'
31
+ };
32
+
33
+ const STATUSES = {
34
+ PENDING: 'PENDING',
35
+ IN_PROGRESS: 'IN_PROGRESS',
36
+ WAITING_FOR_USER: 'WAITING_FOR_USER',
37
+ COMPLETED: 'COMPLETED',
38
+ FAILED: 'FAILED',
39
+ CANCELLED: 'CANCELLED'
40
+ };
41
+
42
+ const DRAWING_PHASES = [
43
+ 'PHASE_0_SKETCH_BLOCKING',
44
+ 'PHASE_1_BACKGROUND',
45
+ 'PHASE_2_MAIN_SHAPES',
46
+ 'PHASE_3_LINE_SILHOUETTE',
47
+ 'PHASE_4_BASE_COLORS',
48
+ 'PHASE_5_SHADING',
49
+ 'PHASE_6_LIGHTING',
50
+ 'PHASE_7_DETAILS',
51
+ 'PHASE_8_FINAL_REVIEW'
52
+ ];
53
+
54
+ // Workflow Steps are now categorized by MODE
55
+ const WORKFLOW_STEPS = {
56
+ CREATE_IMAGE: [
57
+ { id: 'INITIALIZE_CANVAS', label: 'Khởi tạo Canvas', description: 'Kiểm tra và khởi tạo kích thước canvas' },
58
+ { id: 'ANALYZE_REQUEST', label: 'Phân tích yêu cầu', description: 'Phân tích yêu cầu của người dùng thành các thành phần có cấu trúc' },
59
+ { id: 'PLAN_DRAWING', label: 'Lập kế hoạch vẽ', description: 'Tạo kế hoạch vẽ theo thứ tự từ tổng thể đến chi tiết (cần user xác nhận)' },
60
+ { id: 'EXECUTE_PHASES', label: 'Thực hiện vẽ theo giai đoạn', description: 'Vẽ theo từng giai đoạn (PHASE_0 -> PHASE_8)' },
61
+ { id: 'FINALIZE', label: 'Hoàn thiện', description: 'Lưu và hoàn tất ảnh' }
62
+ ],
63
+ EDIT_IMAGE: [
64
+ { id: 'ANALYZE_REQUEST', label: 'Nhận yêu cầu sửa', description: 'Phân tích chính xác nội dung cần sửa' },
65
+ { id: 'GET_CURRENT_IMAGE', label: 'Lấy ảnh hiện tại', description: 'Lấy ảnh hiện tại làm nguồn gốc' },
66
+ { id: 'COMPARE_AND_LOCATE', label: 'So sánh và xác định', description: 'Xác định vùng hoặc layer cần chỉnh sửa' },
67
+ { id: 'EDIT_TARGET_AREA', label: 'Thực hiện chỉnh sửa', description: 'Chỉ sửa phần được yêu cầu trong phạm vi khoanh vùng' },
68
+ { id: 'CHECK_HARMONY', label: 'Kiểm tra sự tương thích', description: 'Kiểm tra sự hòa hợp với các vùng xung quanh và điều chỉnh phần chuyển tiếp' },
69
+ { id: 'FINALIZE', label: 'Cập nhật thay đổi', description: 'Cập nhật thay đổi vào ảnh hiện tại và kiểm tra kết quả' }
70
+ ],
71
+ CREATE_ANIMATION: [
72
+ { id: 'INITIALIZE_CANVAS_AND_ANIMATION', label: 'Khởi tạo Canvas và Animation', description: 'Xác định kích thước, fps, total_frames, loop' },
73
+ { id: 'ANALYZE_ANIMATION', label: 'Phân tích animation', description: 'Phân tích đối tượng, chuyển động, keyframe, layer...' },
74
+ { id: 'PROCESS_FRAMES', label: 'Tạo và xử lý từng frame', description: 'Xử lý từng frame theo đúng thứ tự (0 -> n)' },
75
+ { id: 'CHECK_CONTINUITY', label: 'Kiểm tra tính liên tục', description: 'Kiểm tra tính liên tục của animation giữa các frame' },
76
+ { id: 'FINALIZE', label: 'Hoàn thiện', description: 'Lưu và hoàn tất animation' }
77
+ ]
78
+ };
79
+
80
+ const DRAWING_STEPS = [
81
+ 'EXECUTE_PHASES',
82
+ 'EDIT_TARGET_AREA',
83
+ 'PROCESS_FRAMES'
84
+ ];
85
+
86
+ const DRAWING_CONSTRAINTS = {
87
+ maxCanvasSize: { width: 1024, height: 1024 },
88
+ minCanvasSize: { width: 8, height: 8 },
89
+ allowedColors: null,
90
+ maxStampNameLength: 32,
91
+ maxAnchorNameLength: 32,
92
+ };
93
+
94
+ /**
95
+ * Validate if the canvas name is valid
96
+ * @param {string} name
97
+ * @returns {{ valid: boolean, message?: string }}
98
+ */
99
+ function validateCanvasName(name) {
100
+ if (!name || typeof name !== 'string') {
101
+ return { valid: false, message: 'Canvas name cannot be empty' };
102
+ }
103
+ if (!NAMING_RULES.canvasTab.pattern.test(name)) {
104
+ return { valid: false, message: NAMING_RULES.canvasTab.message };
105
+ }
106
+ return { valid: true };
107
+ }
108
+
109
+ /**
110
+ * Validate if the stamp name is valid
111
+ * @param {string} name
112
+ * @returns {{ valid: boolean, message?: string }}
113
+ */
114
+ function validateStampName(name) {
115
+ if (!name || typeof name !== 'string') {
116
+ return { valid: false, message: 'Stamp name cannot be empty' };
117
+ }
118
+ if (!NAMING_RULES.stamp.pattern.test(name)) {
119
+ return { valid: false, message: NAMING_RULES.stamp.message };
120
+ }
121
+ return { valid: true };
122
+ }
123
+
124
+ /**
125
+ * Validate if the canvas size is within limits
126
+ * @param {number} w
127
+ * @param {number} h
128
+ * @returns {{ valid: boolean, message?: string }}
129
+ */
130
+ function validateCanvasSize(w, h) {
131
+ const { minCanvasSize, maxCanvasSize } = DRAWING_CONSTRAINTS;
132
+ if (w < minCanvasSize.width || h < minCanvasSize.height) {
133
+ return { valid: false, message: `Minimum canvas size is ${minCanvasSize.width}x${minCanvasSize.height}` };
134
+ }
135
+ if (w > maxCanvasSize.width || h > maxCanvasSize.height) {
136
+ return { valid: false, message: `Maximum canvas size is ${maxCanvasSize.width}x${maxCanvasSize.height}` };
137
+ }
138
+ return { valid: true };
139
+ }
140
+
141
+ module.exports = {
142
+ NAMING_RULES,
143
+ MODES,
144
+ STATUSES,
145
+ DRAWING_PHASES,
146
+ DRAWING_STEPS,
147
+ WORKFLOW_STEPS,
148
+ DRAWING_CONSTRAINTS,
149
+ validateCanvasName,
150
+ validateStampName,
151
+ validateCanvasSize,
152
+ };