@pixel-normal-edit/mcp 2.0.10 → 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.
- package/core/command-bus.js +6 -3
- package/core/server.js +98 -3
- package/package.json +1 -1
- package/rules/README.md +358 -285
- package/rules/index.js +9 -9
- package/rules/prerequisites.js +118 -61
- package/rules/rules.js +75 -34
- package/rules/workflow.js +262 -56
package/core/command-bus.js
CHANGED
|
@@ -15,11 +15,14 @@ const TIMEOUT = parseInt(process.env.MCP_TIMEOUT || '20000');
|
|
|
15
15
|
/**
|
|
16
16
|
* Send a command payload to the browser via Firestore and wait for response.
|
|
17
17
|
* @param {Object} payload - The command action and parameters
|
|
18
|
+
* @param {number} timeoutOverride - Optional timeout in milliseconds
|
|
18
19
|
* @returns {Promise<Object>} MCP-compatible response object
|
|
19
20
|
*/
|
|
20
|
-
async function sendCommand(payload) {
|
|
21
|
+
async function sendCommand(payload, timeoutOverride = null) {
|
|
21
22
|
const id = crypto.randomUUID();
|
|
22
23
|
const ref = doc(db, 'mcp_sessions', SESSION, 'commands', id);
|
|
24
|
+
const actualTimeout = timeoutOverride || TIMEOUT;
|
|
25
|
+
|
|
23
26
|
await setDoc(ref, { ...payload, status: 'pending', timestamp: Date.now() });
|
|
24
27
|
|
|
25
28
|
return new Promise((resolve) => {
|
|
@@ -27,9 +30,9 @@ async function sendCommand(payload) {
|
|
|
27
30
|
unsub();
|
|
28
31
|
resolve({
|
|
29
32
|
isError: true,
|
|
30
|
-
content: [{ type: 'text', text: `⏱ Timeout (${
|
|
33
|
+
content: [{ type: 'text', text: `⏱ Timeout (${actualTimeout}ms). Is the browser tab open with session: ${SESSION}?` }]
|
|
31
34
|
});
|
|
32
|
-
},
|
|
35
|
+
}, actualTimeout);
|
|
33
36
|
|
|
34
37
|
const unsub = onSnapshot(ref, (snap) => {
|
|
35
38
|
const d = snap.data();
|
package/core/server.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Provides a tool registration factory for consistent tool definitions.
|
|
7
7
|
*/
|
|
8
8
|
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
9
|
-
const { sendCommand } = require('./command-bus');
|
|
9
|
+
const { sendCommand, SESSION } = require('./command-bus');
|
|
10
|
+
const workflow = require('../rules/workflow');
|
|
11
|
+
const { DRAWING_STEPS } = require('../rules/rules');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Create a new MCP server instance
|
|
@@ -16,16 +18,109 @@ function createServer() {
|
|
|
16
18
|
return new McpServer({ name: 'PixelNormalEdit', version: '2.0.0' });
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Read-only tools that do not modify the canvas and should bypass strict workflow checks
|
|
23
|
+
*/
|
|
24
|
+
const READ_ONLY_TOOLS = [
|
|
25
|
+
'ping',
|
|
26
|
+
'layer_get_info',
|
|
27
|
+
'workspace_list_tabs',
|
|
28
|
+
'workspace_get_active_tab',
|
|
29
|
+
'animation_get_info',
|
|
30
|
+
'anchor_list',
|
|
31
|
+
'query_snapshot',
|
|
32
|
+
'query_bounding_box',
|
|
33
|
+
'query_palette',
|
|
34
|
+
'query_pixel',
|
|
35
|
+
'query_export_image',
|
|
36
|
+
'query_document_state',
|
|
37
|
+
'workflow_status',
|
|
38
|
+
'workflow_get_progress'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Identify if a tool is a drawing tool that should only be allowed in drawing steps
|
|
43
|
+
*/
|
|
44
|
+
function isDrawingTool(name) {
|
|
45
|
+
return name.startsWith('draw_') ||
|
|
46
|
+
name.startsWith('bulk_') ||
|
|
47
|
+
name.startsWith('filter_') ||
|
|
48
|
+
name.startsWith('sprite_') ||
|
|
49
|
+
name.startsWith('region_') ||
|
|
50
|
+
name.startsWith('layer_') ||
|
|
51
|
+
name.startsWith('animation_') ||
|
|
52
|
+
name.startsWith('art_tree_');
|
|
53
|
+
}
|
|
54
|
+
|
|
19
55
|
/**
|
|
20
56
|
* Register a single-command tool on the server
|
|
21
57
|
* @param {McpServer} server - The MCP server instance
|
|
22
58
|
* @param {string} name - Tool name
|
|
23
59
|
* @param {string} desc - Tool description
|
|
24
60
|
* @param {Object} schema - Zod schema for parameters
|
|
25
|
-
* @param {Function} mapToCmd - Function mapping params to command payload
|
|
61
|
+
* @param {Function} mapToCmd - Function mapping params to command payload or returning MCP response
|
|
26
62
|
*/
|
|
27
63
|
function registerTool(server, name, desc, schema, mapToCmd) {
|
|
28
|
-
server.tool(name, desc, schema, async (params) =>
|
|
64
|
+
server.tool(name, desc, schema, async (params) => {
|
|
65
|
+
// 1. Strict Middleware Enforcement (Run BEFORE tool execution)
|
|
66
|
+
if (!READ_ONLY_TOOLS.includes(name)) {
|
|
67
|
+
const state = workflow.getState(SESSION);
|
|
68
|
+
|
|
69
|
+
// Allow workflow_start to run even if state is null
|
|
70
|
+
if (!state && name !== 'workflow_start') {
|
|
71
|
+
return {
|
|
72
|
+
isError: true,
|
|
73
|
+
content: [{ type: 'text', text: "⛔ Middleware Block: Workflow not started! Call workflow_start first." }]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (state) {
|
|
78
|
+
if (state.step_status === 'WAITING_FOR_USER') {
|
|
79
|
+
return {
|
|
80
|
+
isError: true,
|
|
81
|
+
content: [{ type: 'text', text: "⛔ Middleware Block: Workflow is paused! Waiting for user approval. You cannot execute this tool." }]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (state.step_status === 'COMPLETED') {
|
|
86
|
+
return {
|
|
87
|
+
isError: true,
|
|
88
|
+
content: [{ type: 'text', text: "⛔ Middleware Block: Workflow is completed! Please start a new workflow." }]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Block creating new tabs in EDIT_IMAGE mode
|
|
93
|
+
if (name === 'workspace_create_tab' && state.current_mode === 'EDIT_IMAGE') {
|
|
94
|
+
return {
|
|
95
|
+
isError: true,
|
|
96
|
+
content: [{ type: 'text', text: "⛔ Middleware Block: You cannot create new images in EDIT_IMAGE mode. Please only edit the existing image." }]
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check Drawing Steps Enforcement
|
|
101
|
+
if (isDrawingTool(name)) {
|
|
102
|
+
const currentStep = state.steps[state.currentStepIndex];
|
|
103
|
+
if (!currentStep || !DRAWING_STEPS.includes(currentStep.id)) {
|
|
104
|
+
return {
|
|
105
|
+
isError: true,
|
|
106
|
+
content: [{ type: 'text', text: `⛔ Middleware Block: You cannot use drawing/layer tools in the current step ("${currentStep ? currentStep.label : 'None'}"). You must advance to a drawing step (e.g. EXECUTE_PHASES) first.` }]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Execute the tool implementation
|
|
114
|
+
const res = mapToCmd(params);
|
|
115
|
+
|
|
116
|
+
// 3. If the tool implementation returns an MCP response directly (local tool like workflow_*)
|
|
117
|
+
if (res && res.content) {
|
|
118
|
+
return res;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 4. Forward the command payload to the browser
|
|
122
|
+
return sendCommand(res);
|
|
123
|
+
});
|
|
29
124
|
}
|
|
30
125
|
|
|
31
126
|
module.exports = { createServer, registerTool, sendCommand };
|