@pixel-normal-edit/mcp 2.0.4 → 2.0.5

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,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * animation.js — Animation Frame tools
4
+ *
5
+ * Tools: animation_enable, animation_add_frame, animation_go_to_frame,
6
+ * animation_ensure_frame, animation_get_info, animation_remove_frame,
7
+ * animation_reorder_frame, animation_compare_frames
8
+ */
9
+ const { z } = require('zod');
10
+ const { registerTool, sendCommand } = require('../core/server');
11
+
12
+ function register(server) {
13
+ registerTool(server, 'animation_enable',
14
+ 'Enable or disable animation mode. When enabled, the canvas has multiple frames.',
15
+ { enabled: z.boolean().describe('true to enable, false to disable') },
16
+ (p) => ({ action: 'setAnimationMode', enabled: p.enabled }));
17
+
18
+ registerTool(server, 'animation_add_frame',
19
+ 'Append a new blank frame to the animation',
20
+ {},
21
+ () => ({ action: 'addFrame' }));
22
+
23
+ registerTool(server, 'animation_go_to_frame',
24
+ 'Switch to editing a specific frame by index (0-based)',
25
+ { index: z.number().int().min(0).describe('Frame index') },
26
+ (p) => ({ action: 'goToFrame', index: p.index }));
27
+
28
+ registerTool(server, 'animation_ensure_frame',
29
+ 'Make sure the animation has at least (index+1) frames, then switch to that frame',
30
+ { index: z.number().int().min(0).describe('Target frame index') },
31
+ (p) => ({ action: 'ensureFrame', index: p.index }));
32
+
33
+ registerTool(server, 'animation_get_info',
34
+ 'Get current frame count and active frame index',
35
+ {},
36
+ async () => {
37
+ const [count, idx] = await Promise.all([
38
+ sendCommand({ action: 'getFrameCount' }),
39
+ sendCommand({ action: 'getActiveFrameIndex' }),
40
+ ]);
41
+ return { content: [{ type: 'text', text: JSON.stringify({ frameCount: count, activeIndex: idx }) }] };
42
+ });
43
+
44
+ registerTool(server, 'animation_remove_frame',
45
+ 'Remove frame at the given index',
46
+ { index: z.number().int().min(0) },
47
+ (p) => ({ action: 'removeFrame', index: p.index }));
48
+
49
+ registerTool(server, 'animation_reorder_frame',
50
+ 'Move a frame from one position to another',
51
+ { from: z.number().int().min(0), to: z.number().int().min(0) },
52
+ (p) => ({ action: 'reorderFrame', from: p.from, to: p.to }));
53
+
54
+ registerTool(server, 'animation_compare_frames',
55
+ 'Get list of pixel differences between two frames',
56
+ { frameIndex1: z.number().int().min(0), frameIndex2: z.number().int().min(0) },
57
+ (p) => ({ action: 'getFrameDifferences', ...p }));
58
+ }
59
+
60
+ module.exports = { register };
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * canvas.js — Canvas & Setup tools
4
+ *
5
+ * Tools: canvas_get_size, canvas_resize, canvas_clear, canvas_trim
6
+ */
7
+ const { z } = require('zod');
8
+ const { registerTool } = require('../core/server');
9
+
10
+ function register(server) {
11
+ registerTool(server, 'canvas_get_size',
12
+ 'Get current canvas dimensions {width, height}',
13
+ {},
14
+ () => ({ action: 'getSize' }));
15
+
16
+ registerTool(server, 'canvas_resize',
17
+ 'Resize canvas to new dimensions. mode=clear resets pixels, extend keeps them.',
18
+ {
19
+ width: z.number().int().min(1).max(1024).describe('New width in pixels'),
20
+ height: z.number().int().min(1).max(1024).describe('New height in pixels'),
21
+ mode: z.enum(['clear', 'extend', 'fit']).default('clear').describe('How to handle existing content'),
22
+ dx: z.number().int().default(0).describe('X offset for content when extending'),
23
+ dy: z.number().int().default(0).describe('Y offset for content when extending')
24
+ },
25
+ (p) => ({ action: 'resize', ...p }));
26
+
27
+ registerTool(server, 'canvas_clear',
28
+ 'Erase all pixels on the current canvas frame',
29
+ {},
30
+ () => ({ action: 'clear' }));
31
+
32
+ registerTool(server, 'canvas_trim',
33
+ 'Auto-trim transparent borders from canvas',
34
+ {},
35
+ () => ({ action: 'trim' }));
36
+ }
37
+
38
+ module.exports = { register };
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * colors.js — Color tools
4
+ *
5
+ * Tools: color_set, color_get
6
+ */
7
+ const { z } = require('zod');
8
+ const { registerTool } = require('../core/server');
9
+
10
+ const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
11
+
12
+ function register(server) {
13
+ registerTool(server, 'color_set',
14
+ 'Set the primary and/or secondary color',
15
+ { primary: hexColor.optional(), secondary: hexColor.optional() },
16
+ (p) => ({ action: 'setColor', ...p }));
17
+
18
+ registerTool(server, 'color_get',
19
+ 'Get current primary and secondary colors',
20
+ {},
21
+ () => ({ action: 'getColor' }));
22
+ }
23
+
24
+ module.exports = { register };
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * drawing.js — Drawing Primitives tools
4
+ *
5
+ * Tools: draw_pixel, draw_erase, draw_line, draw_rect, draw_circle,
6
+ * draw_ellipse, draw_polygon, draw_fill, draw_gradient_rect,
7
+ * bulk_replace_color, bulk_flood_fill_all
8
+ */
9
+ const { z } = require('zod');
10
+ const { registerTool } = require('../core/server');
11
+
12
+ const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
13
+
14
+ function register(server) {
15
+ registerTool(server, 'draw_pixel',
16
+ 'Set a single pixel to a color',
17
+ { x: z.number().int().describe('X coordinate'), y: z.number().int().describe('Y coordinate'), color: hexColor },
18
+ (p) => ({ action: 'drawPixel', ...p }));
19
+
20
+ registerTool(server, 'draw_erase',
21
+ 'Erase a single pixel (make transparent)',
22
+ { x: z.number().int(), y: z.number().int() },
23
+ (p) => ({ action: 'erasePixel', ...p }));
24
+
25
+ registerTool(server, 'draw_line',
26
+ 'Draw a straight line using Bresenham algorithm',
27
+ { x0: z.number().int(), y0: z.number().int(), x1: z.number().int(), y1: z.number().int(), color: hexColor },
28
+ (p) => ({ action: 'drawLine', ...p }));
29
+
30
+ registerTool(server, 'draw_rect',
31
+ 'Draw a rectangle (outline or filled)',
32
+ {
33
+ x: z.number().int().describe('Top-left X'), y: z.number().int().describe('Top-left Y'),
34
+ w: z.number().int().min(1).describe('Width'), h: z.number().int().min(1).describe('Height'),
35
+ color: hexColor, filled: z.boolean().default(false).describe('Fill interior?')
36
+ },
37
+ (p) => ({ action: 'drawRect', ...p }));
38
+
39
+ registerTool(server, 'draw_circle',
40
+ 'Draw a circle (outline or filled)',
41
+ { cx: z.number().int().describe('Center X'), cy: z.number().int().describe('Center Y'),
42
+ r: z.number().int().min(1).describe('Radius'), color: hexColor, filled: z.boolean().default(false) },
43
+ (p) => ({ action: 'drawCircle', ...p }));
44
+
45
+ registerTool(server, 'draw_ellipse',
46
+ 'Draw an ellipse (outline or filled)',
47
+ { cx: z.number().int(), cy: z.number().int(), rx: z.number().int().min(1).describe('Horizontal radius'),
48
+ ry: z.number().int().min(1).describe('Vertical radius'), color: hexColor, filled: z.boolean().default(false) },
49
+ (p) => ({ action: 'drawEllipse', ...p }));
50
+
51
+ registerTool(server, 'draw_polygon',
52
+ 'Draw a polygon from a list of points (outline or filled)',
53
+ { points: z.array(z.object({ x: z.number().int(), y: z.number().int() })).min(3).describe('Array of {x,y} vertices'),
54
+ color: hexColor, filled: z.boolean().default(false) },
55
+ (p) => ({ action: 'drawPolygon', ...p }));
56
+
57
+ registerTool(server, 'draw_fill',
58
+ 'Flood-fill starting from (x,y) with a color',
59
+ { x: z.number().int(), y: z.number().int(), color: hexColor },
60
+ (p) => ({ action: 'fill', ...p }));
61
+
62
+ registerTool(server, 'draw_gradient_rect',
63
+ 'Draw a rectangle filled with a smooth gradient',
64
+ { x: z.number().int(), y: z.number().int(), w: z.number().int().min(1), h: z.number().int().min(1),
65
+ colorFrom: hexColor.describe('Start color'), colorTo: hexColor.describe('End color'),
66
+ direction: z.enum(['h', 'v']).default('h').describe('h=horizontal, v=vertical') },
67
+ (p) => ({ action: 'drawGradientRect', ...p }));
68
+
69
+ registerTool(server, 'bulk_replace_color',
70
+ 'Replace ALL pixels of one color with another across the entire canvas',
71
+ { from: hexColor.describe('Color to replace'), to: hexColor.describe('New color') },
72
+ (p) => ({ action: 'replaceColor', from: p.from, to: p.to }));
73
+
74
+ registerTool(server, 'bulk_flood_fill_all',
75
+ 'Flood-fill every disconnected region of a given color',
76
+ { color: hexColor.describe('Color to replace'), to: hexColor.describe('Replacement color') },
77
+ (p) => ({ action: 'floodFillAll', color: p.color, to: p.to }));
78
+ }
79
+
80
+ module.exports = { register };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * filters.js — Visual Filter tools
4
+ *
5
+ * Tools: filter_apply
6
+ */
7
+ const { z } = require('zod');
8
+ const { registerTool } = require('../core/server');
9
+
10
+ function register(server) {
11
+ registerTool(server, 'filter_apply',
12
+ `Apply a visual filter to the canvas (or a region).
13
+ Types: brightness (value: -255 to 255), invert, grayscale, hue-rotate (value: degrees 0-360)`,
14
+ { type: z.enum(['brightness', 'invert', 'grayscale', 'hue-rotate']),
15
+ value: z.number().optional().describe('Parameter: brightness=-255..255, hue-rotate=0..360'),
16
+ x: z.number().int().optional(), y: z.number().int().optional(),
17
+ w: z.number().int().optional(), h: z.number().int().optional() },
18
+ (p) => ({ action: 'applyFilter', ...p }));
19
+ }
20
+
21
+ module.exports = { register };
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * health.js — Health Check / Ping tools
4
+ *
5
+ * Tools: ping
6
+ */
7
+ const { registerTool } = require('../core/server');
8
+
9
+ function register(server) {
10
+ registerTool(server, 'ping',
11
+ 'Check if the browser editor is connected and responding',
12
+ {},
13
+ () => ({ action: 'ping' }));
14
+ }
15
+
16
+ module.exports = { register };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * history.js — History (Undo/Redo) tools
4
+ *
5
+ * Tools: history_undo, history_redo
6
+ */
7
+ const { registerTool } = require('../core/server');
8
+
9
+ function register(server) {
10
+ registerTool(server, 'history_undo',
11
+ 'Undo the last drawing action',
12
+ {},
13
+ () => ({ action: 'undo' }));
14
+
15
+ registerTool(server, 'history_redo',
16
+ 'Redo the last undone action',
17
+ {},
18
+ () => ({ action: 'redo' }));
19
+ }
20
+
21
+ module.exports = { register };
package/tools/index.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tools/index.js — Tool registry aggregator
4
+ *
5
+ * Imports and registers all core drawing/utility tools on an MCP server instance.
6
+ * To add a new tool category, create a new file in this folder and add it here.
7
+ */
8
+ const canvas = require('./canvas');
9
+ const workspace = require('./workspace');
10
+ const animation = require('./animation');
11
+ const drawing = require('./drawing');
12
+ const sprite = require('./sprite');
13
+ const region = require('./region');
14
+ const filters = require('./filters');
15
+ const anchors = require('./anchors');
16
+ const query = require('./query');
17
+ const history = require('./history');
18
+ const modes = require('./modes');
19
+ const colors = require('./colors');
20
+ const health = require('./health');
21
+
22
+ /**
23
+ * Register all core tools on the given MCP server instance.
24
+ * @param {McpServer} server
25
+ */
26
+ function registerAll(server) {
27
+ canvas.register(server);
28
+ workspace.register(server);
29
+ animation.register(server);
30
+ drawing.register(server);
31
+ sprite.register(server);
32
+ region.register(server);
33
+ filters.register(server);
34
+ anchors.register(server);
35
+ query.register(server);
36
+ history.register(server);
37
+ modes.register(server);
38
+ colors.register(server);
39
+ health.register(server);
40
+ }
41
+
42
+ module.exports = { registerAll };
package/tools/modes.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * modes.js — Mode settings tools
4
+ *
5
+ * Tools: mode_set_mirror, mode_set_onion_skin, mode_set_grid
6
+ */
7
+ const { z } = require('zod');
8
+ const { registerTool } = require('../core/server');
9
+
10
+ function register(server) {
11
+ registerTool(server, 'mode_set_mirror',
12
+ 'Enable/disable mirror drawing mode (symmetric left-right)',
13
+ { enabled: z.boolean() },
14
+ (p) => ({ action: 'setMirror', enabled: p.enabled }));
15
+
16
+ registerTool(server, 'mode_set_onion_skin',
17
+ 'Enable/disable onion skin (shows previous frame as ghost)',
18
+ { enabled: z.boolean() },
19
+ (p) => ({ action: 'setOnionSkin', enabled: p.enabled }));
20
+
21
+ registerTool(server, 'mode_set_grid',
22
+ 'Show/hide the pixel grid overlay',
23
+ { enabled: z.boolean() },
24
+ (p) => ({ action: 'setGrid', enabled: p.enabled }));
25
+ }
26
+
27
+ module.exports = { register };
package/tools/query.js ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * query.js — Query & Visual Feedback tools
4
+ *
5
+ * Tools: query_snapshot, query_bounding_box, query_palette, query_pixel,
6
+ * query_export_image, query_document_state
7
+ */
8
+ const { z } = require('zod');
9
+ const { registerTool } = require('../core/server');
10
+
11
+ function register(server) {
12
+ registerTool(server, 'query_snapshot',
13
+ `Get an ASCII art representation of the current canvas frame. Use this to "see" what you've drawn before continuing.
14
+ Returns: { ascii, legend, width, height } where each symbol in ascii maps to a hex color via legend.
15
+ Increase scale to get a smaller grid (scale=2 means 1 char = 2×2 pixels).`,
16
+ { scale: z.number().int().min(1).max(8).default(1).describe('Downscale factor (1=full resolution)'),
17
+ maxColors: z.number().int().min(2).max(32).default(12).describe('Max distinct colors in legend') },
18
+ (p) => ({ action: 'querySnapshot', ...p }));
19
+
20
+ registerTool(server, 'query_bounding_box',
21
+ 'Get the bounding box of all non-transparent pixels {minX, minY, maxX, maxY, width, height}',
22
+ {},
23
+ () => ({ action: 'query', type: 'getBoundingBox' }));
24
+
25
+ registerTool(server, 'query_palette',
26
+ 'Get list of all distinct colors used on the canvas',
27
+ {},
28
+ () => ({ action: 'query', type: 'getPalette' }));
29
+
30
+ registerTool(server, 'query_pixel',
31
+ 'Get the color of a specific pixel. Returns hex string or null if transparent.',
32
+ { x: z.number().int(), y: z.number().int() },
33
+ (p) => ({ action: 'getPixel', x: p.x, y: p.y }));
34
+
35
+ registerTool(server, 'query_export_image',
36
+ `Export the current frame as a base64 PNG data URL.
37
+ Use this to visually verify your work — paste the dataUrl into an image viewer.
38
+ The dataUrl starts with "data:image/png;base64,..."`,
39
+ { format: z.enum(['png', 'webp', 'jpeg']).default('png') },
40
+ (p) => ({ action: 'exportBase64', format: p.format }));
41
+
42
+ registerTool(server, 'query_document_state',
43
+ 'Get full document state: tab name, canvas size, animation info, undo availability',
44
+ {},
45
+ () => ({ action: 'query', type: 'getDocumentState' }));
46
+ }
47
+
48
+ module.exports = { register };
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * region.js — Region Clipboard tools
4
+ *
5
+ * Tools: region_copy, region_paste
6
+ */
7
+ const { z } = require('zod');
8
+ const { registerTool } = require('../core/server');
9
+
10
+ function register(server) {
11
+ registerTool(server, 'region_copy',
12
+ 'Copy a rectangular region of pixels to an internal clipboard',
13
+ { x: z.number().int().default(0), y: z.number().int().default(0),
14
+ w: z.number().int().min(1).describe('Width of region'), h: z.number().int().min(1).describe('Height of region') },
15
+ (p) => ({ action: 'copyRegion', ...p }));
16
+
17
+ registerTool(server, 'region_paste',
18
+ 'Paste the clipboard region at (x,y). Defaults to original position.',
19
+ { x: z.number().int().optional().describe('X destination (default: original X)'),
20
+ y: z.number().int().optional().describe('Y destination (default: original Y)') },
21
+ (p) => ({ action: 'pasteRegion', ...p }));
22
+ }
23
+
24
+ module.exports = { register };
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sprite.js — Sprite / Stamp System tools
4
+ *
5
+ * Tools: sprite_draw, sprite_save_stamp, sprite_use_stamp, sprite_list_stamps
6
+ */
7
+ const { z } = require('zod');
8
+ const { registerTool } = require('../core/server');
9
+
10
+ function register(server) {
11
+ registerTool(server, 'sprite_draw',
12
+ `Draw a sprite using ASCII art + color palette. Each character in the data array maps to a color.
13
+ EXAMPLE:
14
+ palette: { "H": "#ffcc99", "B": "#1565c0", ".": null }
15
+ data: [ "..H..", ".BBB.", "..H.." ]
16
+ This is the most efficient way to draw complex shapes — replaces dozens of drawPixel calls.`,
17
+ {
18
+ x: z.number().int().default(0).describe('Top-left X offset'),
19
+ y: z.number().int().default(0).describe('Top-left Y offset'),
20
+ palette: z.record(z.string().length(1), z.string().nullable()).describe('Char→hex map. null = transparent/skip'),
21
+ data: z.array(z.string()).min(1).describe('Rows of ASCII art characters')
22
+ },
23
+ (p) => ({ action: 'drawSprite', ...p }));
24
+
25
+ registerTool(server, 'sprite_save_stamp',
26
+ 'Save a named sprite for reuse across frames. Use sprite_use_stamp to place it.',
27
+ { name: z.string().describe('Unique stamp name'), palette: z.record(z.string().length(1), z.string().nullable()),
28
+ data: z.array(z.string()).min(1) },
29
+ (p) => ({ action: 'saveStamp', ...p }));
30
+
31
+ registerTool(server, 'sprite_use_stamp',
32
+ 'Place a previously saved stamp at a position. Optionally override palette colors.',
33
+ { name: z.string().describe('Stamp name (from sprite_save_stamp)'), x: z.number().int().default(0),
34
+ y: z.number().int().default(0), palette: z.record(z.string().length(1), z.string().nullable()).optional().describe('Override specific palette colors') },
35
+ (p) => ({ action: 'useStamp', ...p }));
36
+
37
+ registerTool(server, 'sprite_list_stamps',
38
+ 'List all saved stamp names',
39
+ {},
40
+ () => ({ action: 'listStamps' }));
41
+ }
42
+
43
+ module.exports = { register };
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * workspace.js — Workspace / Tabs tools
4
+ *
5
+ * Tools: workspace_list_tabs, workspace_get_active_tab, workspace_create_tab,
6
+ * workspace_switch_tab, workspace_rename_tab, workspace_save
7
+ */
8
+ const { z } = require('zod');
9
+ const { registerTool } = require('../core/server');
10
+
11
+ function register(server) {
12
+ registerTool(server, 'workspace_list_tabs',
13
+ 'List all open document tabs with their IDs and names',
14
+ {},
15
+ () => ({ action: 'listTabs' }));
16
+
17
+ registerTool(server, 'workspace_get_active_tab',
18
+ 'Get the currently active tab ID',
19
+ {},
20
+ () => ({ action: 'getActiveTabId' }));
21
+
22
+ registerTool(server, 'workspace_create_tab',
23
+ 'Create a new blank canvas tab',
24
+ {
25
+ name: z.string().optional().describe('Tab name'),
26
+ width: z.number().int().default(32).describe('Canvas width'),
27
+ height: z.number().int().default(32).describe('Canvas height')
28
+ },
29
+ (p) => ({ action: 'createTab', ...p }));
30
+
31
+ registerTool(server, 'workspace_switch_tab',
32
+ 'Switch to a different tab by ID (get IDs from workspace_list_tabs)',
33
+ { tabId: z.string().describe('Tab ID to switch to') },
34
+ (p) => ({ action: 'switchTab', tabId: p.tabId }));
35
+
36
+ registerTool(server, 'workspace_rename_tab',
37
+ 'Rename a tab',
38
+ { tabId: z.string(), name: z.string() },
39
+ (p) => ({ action: 'renameTab', ...p }));
40
+
41
+ registerTool(server, 'workspace_save',
42
+ 'Quick-save the current workspace',
43
+ {},
44
+ () => ({ action: 'quickSave' }));
45
+ }
46
+
47
+ module.exports = { register };
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * transport/http.js — HTTP transport for MCP server
4
+ *
5
+ * Starts an HTTP server that accepts MCP requests at /mcp.
6
+ * Supports CORS for browser tools, health checks at /health.
7
+ * Each HTTP request gets its own transport instance (stateless).
8
+ */
9
+ const http = require('http');
10
+ const crypto = require('crypto');
11
+ const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
12
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
13
+ const { registerAll } = require('../tools/index');
14
+ const { SESSION } = require('../core/command-bus');
15
+
16
+ /**
17
+ * Build a fresh MCP server instance (used per HTTP request)
18
+ * @returns {McpServer}
19
+ */
20
+ function buildServer() {
21
+ const { createServer } = require('../core/server');
22
+ const s = createServer();
23
+ registerAll(s);
24
+ return s;
25
+ }
26
+
27
+ /**
28
+ * Start the MCP server as an HTTP server on the given port
29
+ * @param {number} port - HTTP port to listen on
30
+ */
31
+ async function start(port) {
32
+ const app = http.createServer(async (req, res) => {
33
+ // CORS for browser tools
34
+ res.setHeader('Access-Control-Allow-Origin', '*');
35
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
36
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
37
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
38
+
39
+ if (req.url === '/health') {
40
+ res.writeHead(200, { 'Content-Type': 'application/json' });
41
+ res.end(JSON.stringify({ ok: true, session: SESSION, ts: Date.now() }));
42
+ return;
43
+ }
44
+
45
+ if (req.url !== '/mcp') {
46
+ res.writeHead(404); res.end('Use /mcp'); return;
47
+ }
48
+
49
+ // Read body
50
+ let body = '';
51
+ req.on('data', chunk => body += chunk);
52
+ req.on('end', async () => {
53
+ const transport = new StreamableHTTPServerTransport({
54
+ sessionIdGenerator: () => crypto.randomUUID(),
55
+ enableJsonResponse: true,
56
+ });
57
+ const freshServer = buildServer();
58
+ await freshServer.connect(transport);
59
+ await transport.handleRequest(
60
+ { method: req.method, headers: req.headers, body: JSON.parse(body || '{}') },
61
+ res,
62
+ JSON.parse(body || '{}')
63
+ );
64
+ });
65
+ });
66
+
67
+ app.listen(port, () => {
68
+ console.log(`\nPixel Normal Edit MCP HTTP Server`);
69
+ console.log(` Endpoint : http://localhost:${port}/mcp`);
70
+ console.log(` Health : http://localhost:${port}/health`);
71
+ console.log(` Session : ${SESSION}`);
72
+ console.log(` Editor : http://localhost:5173?mcp_session=${SESSION}`);
73
+ console.log(`\n── AI config (paste into mcp_config.json) ──`);
74
+ console.log(JSON.stringify({
75
+ mcpServers: {
76
+ 'pixel-normal-edit': {
77
+ command: 'npx',
78
+ args: ['mcp-remote', `http://localhost:${port}/mcp`]
79
+ }
80
+ }
81
+ }, null, 2));
82
+ console.log('────────────────────────────────────────────\n');
83
+ });
84
+ }
85
+
86
+ module.exports = { start };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * transport/stdio.js — Stdio transport for MCP server
4
+ *
5
+ * Connects the server to standard input/output for use with
6
+ * Claude Desktop, Cursor, Windsurf, and other stdio-based MCP clients.
7
+ */
8
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
9
+
10
+ /**
11
+ * Start the MCP server using the stdio transport
12
+ * @param {McpServer} server - The configured MCP server instance
13
+ */
14
+ async function start(server) {
15
+ const transport = new StdioServerTransport();
16
+ await server.connect(transport);
17
+ // Console.error output is suppressed to avoid JSON parsing issues
18
+ // with MCP clients that merge stdout and stderr.
19
+ }
20
+
21
+ module.exports = { start };