@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.
@@ -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 (${TIMEOUT}ms). Is the browser tab open with session: ${SESSION}?` }]
33
+ content: [{ type: 'text', text: `⏱ Timeout (${actualTimeout}ms). Is the browser tab open with session: ${SESSION}?` }]
31
34
  });
32
- }, TIMEOUT);
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) => sendCommand(mapToCmd(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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixel-normal-edit/mcp",
3
- "version": "2.0.10",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for Pixel Normal Edit canvas - enables AI agents to draw pixel art",
5
5
  "main": "index.js",
6
6
  "bin": {