This is page 1 of 2. Use http://codebase.md/snehil-shah/libresprite-mcp?page={x} to view the full context. # Directory Structure ``` ├── .github │ └── workflows │ └── release.yml ├── .gitignore ├── .python-version ├── ARCHITECTURE.md ├── assets │ ├── architecture.svg │ ├── connect.png │ ├── demo.mp4 │ ├── design │ │ └── architecture.drawio │ └── scripts-folder.png ├── LICENSE ├── pyproject.toml ├── README.md ├── remote │ └── mcp.js ├── src │ └── libresprite_mcp │ ├── __init__.py │ ├── libresprite_proxy.py │ ├── mcp_server.py │ └── resources │ ├── examples.txt │ └── reference.txt └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 3.11 ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # LibreSprite-MCP [](https://cursor.com/install-mcp?name=libresprite&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMGxpYnJlc3ByaXRlLW1jcCUyMiU3RA%3D%3D) [](https://pypi.org/project/libresprite-mcp/) > Prompt your way into LibreSprite Model Context Protocol (MCP) server for prompt-assisted editing, designing, and scripting inside LibreSprite. https://github.com/user-attachments/assets/71440bba-16a5-4ee2-af10-2c346978a290 ## Prerequisites [`uv`](https://docs.astral.sh/uv/) is the recommended way to install and use this server. Here are quick one-liners to install it if you haven't: - **Windows**: (run as administrator) ```powershell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` - **Unix**: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` More on [installing `uv`](https://docs.astral.sh/uv/getting-started/installation/). The package is published on [PyPI](https://pypi.org/project/librespsrite-mcp/), so feel free to consume it any other way you prefer (`pipx`, etc) ## Usage ### Step 1: Setting up the client Add the MCP server with the following entrypoint command (or something else if you are not using `uv`) to your MCP client: ```bash uvx libresprite-mcp ``` #### Examples: - **Claude Desktop & Cursor** Edit _Claude > Settings > Developer > Edit Config > claude_desktop_config.json_ or _.cursor > mcp.json_ to include the server: ```json { "mcpServers": { // ...existing servers... "libresprite": { "type": "stdio", "command": "uvx", "args": [ "libresprite-mcp" ] } // ...existing servers... } } ``` You can also use this fancy badge to make it quick: [](https://cursor.com/install-mcp?name=libresprite&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMnV2eCUyMGxpYnJlc3ByaXRlLW1jcCUyMiU3RA%3D%3D) > [!NOTE] > You will have to restart Claude Desktop to load the MCP Server. ### Step 2: Setting up LibreSprite Download the latest stable remote script `mcp.js` from [releases](https://github.com/Snehil-Shah/libresprite-mcp/releases/latest) and add it to LibreSprite's scripts folder:  ### Step 3: Connect and use Run the `mcp.js` script (that you see in the screenshot above), and make sure your MCP server is running (Claude Desktop/Cursor is loaded and running). If all went well, you should see the following screen:  Click the "Connect" button and you can now start talking to Claude about your next big pixel-art project! ## Some pointers - You can only run one instance of the MCP server at a time. - The server expects port `64823` to be free. - The server has a hacky and brittle implementation (see [ARCHITECTURE](https://github.com/Snehil-Shah/libresprite-mcp/blob/main/ARCHITECTURE.md)), and is not extensively tested. - The MCP resources are kinda low quality with unclear API reference and limited examples, leaving the LLM confused at times. If you're a LibreSprite expert, we need your help. *** ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [project] name = "libresprite-mcp" version = "0.1.3" description = "Prompt your way into LibreSprite" readme = "README.md" authors = [ { name = "Snehil Shah", email = "[email protected]" } ] requires-python = ">=3.11" dependencies = [ "flask>=3.1.1", "mcp[cli]>=1.12.2", ] [project.scripts] libresprite-mcp = "libresprite_mcp:main" [build-system] requires = ["uv_build>=0.8.3,<0.9.0"] build-backend = "uv_build" ``` -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- ```markdown # Architecture Unlike Aseprite, LibreSprite lacks a command-line interface for doing things. The only thing it provides to programmatically control its interface is a JavaScript scripting interface. These scripts run in a very limited context and the closest thing I could find around networking was the `storage.fetch` method which can make a network request. To make it work, I wrote a relay server that acts as a convenient proxy for the MCP server to interact with LibreSprite, while a remote script running inside LibreSprite polls the endpoints for scripts to execute.  ``` -------------------------------------------------------------------------------- /src/libresprite_mcp/__init__.py: -------------------------------------------------------------------------------- ```python """ Main entry point for the Libresprite MCP server application. """ import click import logging from .libresprite_proxy import LibrespriteProxy from .mcp_server import MCPServer def main() -> None: """Main entry point for the application.""" # Disable unwanted logging to avoid messing with stdio logging.disable(logging.WARN) # HACK: https://stackoverflow.com/a/57086684 def secho(text, file=None, nl=None, err=None, color=None, **styles): pass def echo(text, file=None, nl=None, err=None, color=None, **styles): pass click.echo = echo click.secho = secho # Initialize and start HTTP relay server libresprite_proxy = LibrespriteProxy(port=64823) libresprite_proxy.start() # Initialize and run MCP server (this blocks) mcp_server = MCPServer(libresprite_proxy) mcp_server.run(transport='stdio') if __name__ == '__main__': main() ``` -------------------------------------------------------------------------------- /src/libresprite_mcp/mcp_server.py: -------------------------------------------------------------------------------- ```python """ MCP server implementation that exposes tools for interacting with the libresprite-proxy server. """ import os from mcp.server.fastmcp import FastMCP, Context from .libresprite_proxy import LibrespriteProxy class MCPServer: """The LibreSprite MCP Server.""" def __init__(self, libresprite_proxy: LibrespriteProxy, server_name: str = "libresprite"): # Cache the libresprite proxy instance self._libresprite_proxy = libresprite_proxy # Initialize FastMCP self.mcp = FastMCP(server_name) # Setup MCP tools, prompts, and resources self._setup_tools() self._setup_resources() self._setup_prompts() def _setup_tools(self): """Setup MCP tools.""" @self.mcp.tool() def run_script(script: str, ctx: Context) -> str: """ Run a JavaScript script inside Libresprite. IMPORTANT: Make sure you are well versed with the documentation and examples provided in the resources `docs:reference` and `docs:examples`. Args: script: The script to execute Returns: Console output """ return self._libresprite_proxy.run_script(script, ctx) def _setup_resources(self): """Setup MCP resources.""" base_dir = os.path.dirname(os.path.abspath(__file__)) @self.mcp.resource("docs://reference") def read_reference() -> str: """Read the libresprite command reference documentation.""" doc_path = os.path.join(base_dir, "resources", "reference.txt") try: with open(doc_path, "r", encoding="utf-8") as f: return f.read() except Exception as e: return f"Error reading reference.txt: {e}" @self.mcp.resource("docs://examples") def read_examples() -> str: """Read example scripts using libresprite commands.""" example_path = os.path.join(base_dir, "resources", "examples.txt") try: with open(example_path, "r", encoding="utf-8") as f: return f.read() except Exception as e: return f"Error reading examples.txt: {e}" def _setup_prompts(self): """Setup MCP prompts.""" @self.mcp.prompt(title="libresprite") def libresprite(prompt: str) -> str: """ Prompt template to use the libresprite tool with proper context conditioning. Args: prompt: User prompt Returns: Prompt to process """ return f""" Libresprite is a program for creating and editing pixel art and animations using JavaScript. Before proceeding, please ensure you are well versed with the documentation and examples provided in the resources `docs:reference` and `docs:examples`. You can use the `run_script` tool to execute JavaScript scripts in the context of libresprite. Here's what you need to do using the above tools and resources: {prompt} """ def run(self, transport: str = 'stdio'): """Run the MCP server.""" self.mcp.run(transport=transport) ``` -------------------------------------------------------------------------------- /src/libresprite_mcp/libresprite_proxy.py: -------------------------------------------------------------------------------- ```python """ Proxy server for Libresprite. """ import threading from typing import Optional from mcp.server.fastmcp import Context from flask import Flask, jsonify, request class LibrespriteProxy: """ Proxy server for Libresprite. This is a relay server that acts as a bridge between the MCP server and the remote libresprite script. From the POV of the MCP server, this fully abstracts libresprite into a convenient IO interface to execute remote scripts. """ def __init__(self, port: int): # Stores current script self._script: str | None = None # Stores output of the script execution self._output: str | None = None # Stores flag indicating if execution is in progress self._lock: bool = False # Initialize event handlers self._script_event = threading.Event() self._output_event = threading.Event() # Relay server configuration self.port = port self.app = Flask(__name__) # Initialize server routes self._setup_routes() self._server_thread: Optional[threading.Thread] = None def _setup_routes(self): """Setup Flask routes.""" @self.app.get('/') def get_script(): """Get the current script.""" if self._script is None: self._script_event.wait() self._script_event.clear() script = self._script self._script = None return jsonify({"script": script}) @self.app.post('/') def post_output(): """Post execution output.""" if not self._lock: # ignore random requests return jsonify({"status": "ignored"}) req = request.get_json(force=True, silent=True) if req: output = req.get('output') else: return jsonify({"status": "invalid"}) self._output = output self._output_event.set() return jsonify({"status": "success"}) @self.app.get('/ping') def ping(): """Ping endpoint for health checks.""" return jsonify({"status": "pong"}) def _run_server(self): """Run the Flask server.""" self.app.run( host='localhost', port=self.port, debug=False, use_reloader=False ) def start(self): """Start the HTTP server in a background thread.""" if self._server_thread and self._server_thread.is_alive(): return self._server_thread = threading.Thread( target=self._run_server, daemon=True ) self._server_thread.start() def run_script(self, script: str, ctx: Context) -> str: """ Run a script in the execution context. WARNING: This method is synchronous and blocking. Args: script: The script to execute """ # This proxy only allows one script to be executed at a time if self._lock: ctx.error("Script execution is already in progress...") raise RuntimeError("Script execution is already in progress.") # Sending the script ctx.info("Sending script to libresprite...") self._lock = True self._script = script self._script_event.set() # Waiting for execution if not self._output_event.wait(timeout=15): ctx.warning("This is taking longer than usual, make sure the user has the Libresprite application with the remote script running?") self._output_event.wait() self._output_event.clear() # Return the output ctx.info("Script execution completed, checking the logs...") output = self._output self._output = None self._lock = False return output ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml name: Release on: workflow_dispatch: jobs: pypi: name: PyPI runs-on: ubuntu-latest permissions: id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v6 - name: Set up Python run: uv python install - name: Build package run: uv build - name: Publish package run: uv publish github: name: GitHub runs-on: ubuntu-latest needs: [pypi] permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Get version from pyproject.toml id: version run: | VERSION=$(grep "^version" pyproject.toml | sed 's/version = "\(.*\)"/\1/') echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Check if version exists id: version_check run: | VERSION=${{ steps.version.outputs.version }} if git tag | grep -q "v$VERSION"; then echo "Version v$VERSION already exists, skipping release" echo "should_release=false" >> $GITHUB_OUTPUT else echo "New version v$VERSION detected, proceeding with release" echo "should_release=true" >> $GITHUB_OUTPUT fi - name: Generate changelog id: changelog if: ${{ steps.version_check.outputs.should_release == 'true' }} run: | LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -z "$LAST_TAG" ]; then COMMITS=$(git log --pretty=format:"%s" --no-merges) else COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s" --no-merges) fi if [ -z "$COMMITS" ] || [ "$COMMITS" = "" ]; then echo "No commits since last tag, trying to get commits from previous tag..." PREV_TAG=$(git tag --sort=-version:refname | head -2 | tail -1) echo "Previous tag: $PREV_TAG" if [ -n "$PREV_TAG" ] && [ "$PREV_TAG" != "$LAST_TAG" ]; then COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"%s" --no-merges) echo "Commits since $PREV_TAG:" echo "$COMMITS" fi fi FEATURES=$(echo "$COMMITS" | grep -E "^feat" | sed 's/^/- /' || true) FIXES=$(echo "$COMMITS" | grep -E "^fix" | sed 's/^/- /' || true) CHORES=$(echo "$COMMITS" | grep -E "^chore" | sed 's/^/- /' || true) BUILDS=$(echo "$COMMITS" | grep -E "^build" | sed 's/^/- /' || true) DOCS=$(echo "$COMMITS" | grep -E "^docs" | sed 's/^/- /' || true) TEST=$(echo "$COMMITS" | grep -E "^test" | sed 's/^/- /' || true) OTHER_COMMITS=$(echo "$COMMITS" | grep -v -E "^(feat|fix|chore|build|docs|test)" | sed 's/^/- /' || true) CHANGELOG="" if [ -n "$FEATURES" ]; then CHANGELOG="${CHANGELOG}### Features"$'\n'"$FEATURES"$'\n\n' fi if [ -n "$FIXES" ]; then CHANGELOG="${CHANGELOG}### Bug Fixes"$'\n'"$FIXES"$'\n\n' fi if [ -n "$DOCS" ]; then CHANGELOG="${CHANGELOG}### Documentation"$'\n'"$DOCS"$'\n\n' fi if [ -n "$TEST" ]; then CHANGELOG="${CHANGELOG}### Tests"$'\n'"$TEST"$'\n\n' fi if [ -n "$CHORES" ] || [ -n "$BUILDS" ]; then CHANGELOG="${CHANGELOG}### Maintenance"$'\n' [ -n "$CHORES" ] && CHANGELOG="${CHANGELOG}$CHORES"$'\n' [ -n "$BUILDS" ] && CHANGELOG="${CHANGELOG}$BUILDS"$'\n' fi if [ -n "$OTHER_COMMITS" ]; then CHANGELOG="${CHANGELOG}### Other Changes"$'\n'"$OTHER_COMMITS"$'\n' fi { echo "changelog<<EOF" printf '%s' "$CHANGELOG" echo "EOF" } >> $GITHUB_OUTPUT - name: Create GitHub Release uses: softprops/action-gh-release@v1 if: ${{ steps.version_check.outputs.should_release == 'true' }} with: tag_name: v${{ steps.version.outputs.version }} name: v${{ steps.version.outputs.version }} body: | Release of `libresprite-mcp` version ${{ steps.version.outputs.version }}. Find the official MCP distribution from [PyPI](https://pypi.org/project/libresprite-mcp/) and the remote script `mcp.js` attached to this release. For the installation instructions, refer to the project [README](https://github.com/Snehil-Shah/libresprite-mcp/blob/main/README.md). ## Changes ${{ steps.changelog.outputs.changelog }} files: remote/mcp.js ``` -------------------------------------------------------------------------------- /src/libresprite_mcp/resources/examples.txt: -------------------------------------------------------------------------------- ``` # Examples ## Random ```javascript const col = app.pixelColor; const img = app.activeImage; const h = img.height; const w = img.width; for (var y = 0; y < h; ++y) { for (var x = 0; x < w; ++x) { const c = Math.random() * 256 >>> 0; img.putPixel(x, y, col.rgba(c, c, c, 255)) } } ``` ## White to Alpha ```javascript var col = app.pixelColor var img = app.activeImage for (y=0; y<img.height; ++y) { for (x=0; x<img.width; ++x) { var c = img.getPixel(x, y) var v = (col.rgbaR(c)+ col.rgbaG(c)+ col.rgbaB(c))/3 img.putPixel(x, y, col.rgba(col.rgbaR(c), col.rgbaG(c), col.rgbaB(c), 255-v)) } } ``` ## PerLineOscillation ```javascript var dialog; function wrap(x, n) { if (x < 0) return ((x % n + n) % n) | 0; return ((x + n) % n) | 0; } const effects = [ // horizontal oscillation function(src, angle, width, height, scale) { const out = new Uint8Array(src.length); for (var y = 0; y < height; ++y) { var ox = Math.sin(y / scale + angle * Math.PI * 2) * scale | 0; for (var x = 0; x < width; ++x) { var oi = (y * width + x) * 4; var ii = (y * width + wrap(x + ox, width)) * 4; out[oi++] = src[ii++]; out[oi++] = src[ii++]; out[oi++] = src[ii++]; out[oi++] = src[ii++]; } } return out; }, // same as above, but with anti-aliasing function(src, angle, width, height, scale) { const out = new Uint8Array(src.length); for (var y = 0; y < height; ++y) { var ox = Math.sin(y / scale + angle * Math.PI * 2) * scale; var ox0 = ox | 0; var ox1 = ox0 + 1; var a = ox - ox0; for (var x = 0; x < width; ++x) { var oi = (y * width + x) * 4; var ii0 = (y * width + wrap(x + ox0, width)) * 4; var ii1 = (y * width + wrap(x + ox1, width)) * 4; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; } } return out; }, // horizontal oscillation, RGB desync'd function(src, angle, width, height, scale) { const out = new Uint8Array(src.length); for (var y = 0; y < height; ++y) { for (var x = 0; x < width; ++x) { var d = 0; var oi = (y * width + x) * 4; var ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0; var ii = (y * width + wrap(x + ox, width)) * 4; out[oi++] = src[ii++]; d += 0.03; ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0; ii = (y * width + wrap(x + ox, width)) * 4 + 1; out[oi++] = src[ii++]; ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0; ii = (y * width + wrap(x + ox, width)) * 4 + 2; out[oi++] = src[ii++]; ox = Math.sin(y / scale + (angle + d) * Math.PI * 2) * scale | 0; ii = (y * width + wrap(x + ox, width)) * 4 + 3; out[oi++] = src[ii++]; } } return out; }, // vertical oscillation function(src, angle, width, height, scale) { const out = new Uint8Array(src.length); for (var y = 0; y < height; ++y) { var oy = Math.sin(y / scale + angle * Math.PI * 2) * scale | 0; for (var x = 0; x < width; ++x) { var oi = (y * width + x) * 4; var ii = (wrap(y + oy, height) * width + x) * 4; out[oi++] = src[ii++]; out[oi++] = src[ii++]; out[oi++] = src[ii++]; out[oi++] = src[ii++]; } } return out; }, // same as above, but with anti-aliasing function(src, angle, width, height, scale) { const out = new Uint8Array(src.length); for (var y = 0; y < height; ++y) { var oy = y + Math.sin(y / scale + angle * Math.PI * 2) * scale; var oy0 = wrap(oy | 0, height) * width; var ny = (y + 1) + Math.sin((y + 1) / scale + angle * Math.PI * 2) * scale; var oy1 = wrap(ny | 0, height) * width; var a = oy - Math.floor(oy); for (var x = 0; x < width; ++x) { var oi = (y * width + x) * 4; var ii0 = (oy0 + x) * 4; var ii1 = (oy1 + x) * 4; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; out[oi++] = src[ii0++] * (1 - a) + src[ii1++] * a; } } return out; } ]; const eventHandlers = { init:function(){ if (!app.activeSprite) { app.createDialog('Error').addLabel('Need an image to oscillate.'); return; } if (dialog) dialog.close(); dialog = app.createDialog('dialog'); dialog.addIntEntry("Frames to animate:", "frameCount", 0, 100); dialog.addBreak(); dialog.addIntEntry("Wave size:", "waveSize", 3, 100); dialog.addBreak(); dialog.addIntEntry("Effect:", "effect", 0, effects.length - 1); dialog.addBreak(); dialog.addButton("Run", "run"); }, run_click:function(){ dialog.close(); dialog = null; app.command.setParameter("format", "rgb"); app.command.ChangePixelFormat(); app.command.clearParameters(); const frameCount = storage.get("frameCount")|0; const waveSize = storage.get("waveSize")|0; const effect = storage.get("effect")|0; const oscillate = effects[effect] || effects[0]; const sprite = app.activeSprite; const layerNumber = app.activeLayerNumber; const reference = sprite.layer(layerNumber).cel(0).image; const refWidth = reference.width; const refHeight = reference.height; const src = reference.getImageData(); app.command.setParameter("content", "current"); for (var i = 0; i < frameCount; ++i) { if (i) app.command.NewFrame(); sprite.layer(layerNumber).cel(i).image.putImageData(oscillate(src, i / frameCount, refWidth, refHeight, waveSize)); } } }; function onEvent(eventName) { var handler = eventHandlers[eventName]; if (typeof handler == 'function') handler(); } ``` ``` -------------------------------------------------------------------------------- /remote/mcp.js: -------------------------------------------------------------------------------- ```javascript /** * Source: https://github.com/Snehil-Shah/libresprite-mcp */ // Cache the global context. const global = this; /** * Model Context Protocol (MCP) remote script that interacts with the libresprite-mcp server. * * NOTE: Defined as an IIFE to avoid global namespace pollution. */ (function MCP() { // CONSTANTS // /** * URL to the relay server exposing the next command. */ const RELAY_SERVER_URL = 'http://localhost:64823'; /** * Delay between polling requests. (in number of rendering cycles) */ const POLL_DELAY = 120; // VARIABLES // /** * Flag indicating extension state. * * @type {boolean} */ let active = false; /** * Flag indicating whether polling is active. * * @type {boolean} */ let polling = false; /** * Flag indicating whether the client is connected to the server. * * @type {boolean} */ let connected = false; /** * Stores stdout. * * @type {string} */ let output = ''; /** * Function to get response from storage in the next cycle. * * @type {Function|null} */ let _get_response = null; /** * Function to post response to storage in the next cycle. * * @type {Function|null} */ let _post_response = null; /** * Dialog instance for UI. */ let dialog = null; // FUNCTIONS // /** * Global `console.log`. */ const _clientLogger = global.console.log; // Override global console object to capture stdout. const console = Object.assign({}, global.console); /** * Modified `console.log` that captures output before logging. */ console.log = function() { var args = Array.prototype.slice.call(arguments); output += args.join(' ') + '\n'; _clientLogger.apply(null, args); } /** * Makes a GET request. * * @private * @param {string} url - url to fetch * @param {Function} cb - callback function to handle the response */ function _get(url, cb) { storage.fetch(url, '_get_response'); _get_response = function() { const status = storage.get('_get_response' + '_status'); const string = storage.get('_get_response'); cb({ string, status }); }; } /** * Makes a POST request. * * @private * @param {string} url - url to fetch * @param {string} body - request body * @param {Function} cb - callback function to handle the response */ function _post(url, body, cb) { storage.fetch(url, '_post_response', "", "POST", body, "Content-Type", "application/json"); _post_response = function() { const status = storage.get('_post_response' + '_status'); const string = storage.get('_post_response'); cb({ string, status }); } } /** * Makes a GET request. * * @param {string} url - url to fetch * @param {Function} cb - callback function to handle the response */ function get(url, cb) { _get(url, function(rsp) { var data, error = rsp.status != 200 ? 'status:' + rsp.status : 0; try { if (!error) data = JSON.parse(rsp.string); } catch (ex) { error = ex; } cb(data, error); }); } /** * Makes a POST request. * * @param {string} url - url to fetch * @param {Object} body - request body * @param {Function} cb - callback function to handle the response */ function post(url, body, cb) { _post(url, body, function(rsp) { var data, error = rsp.status != 200 ? 'status:' + rsp.status : 0; try { if (!error) data = JSON.parse(rsp.string); else error += rsp.string; } catch (ex) { error = ex; } cb(data, error); }); } /** * Pings for server health. * * This yields a "bad_health" event if the server is unreachable, * or an "init" event if the server is reachable. */ function checkServerHealth() { get(RELAY_SERVER_URL + '/ping', function(data, error) { if (error) { connected = false; app.yield("bad_health", POLL_DELAY); return; } if (data && data.status === 'pong') { connected = true; app.yield("good_health"); } else { connected = false; app.yield("bad_health", POLL_DELAY); } }); } /** * Fetches the next script from the server. * * @param {Function} cb - script handler */ function getScript(cb) { get(RELAY_SERVER_URL, function(data, error) { if (error) { // The post request will log the error. cb(''); return; } cb((data && data.script) ? data.script : ''); }); } /** * Posts the output to the server. */ function postOutput() { const body = JSON.stringify({output: output}); post(RELAY_SERVER_URL, body, function(data, error) { // NOTE: This is the last interaction with the MCP server for a tool call and hence we ensure updates to the UI and connection status. if (error) { _clientLogger('The MCP server was shut down.'); connected = false; paintUI(); app.yield("bad_health", POLL_DELAY); return; } if ( !data ) { _clientLogger('Something went wrong. Please report it on https://github.com/Snehil-Shah/libresprite-mcp/issues.'); return; } if ( data.status === 'invalid' ) _clientLogger('Something is wrong. Please report it on https://github.com/Snehil-Shah/libresprite-mcp/issues.'); // Other status types can be ignored... // Continue polling... if (!polling) { return; } app.yield("poll", POLL_DELAY); }); } /** * Runs a script in the current context. * * @param {string} script - script to run */ function runScript(script) { if (!script) { return; } try { // Execute in global scope with our custom logger... new Function('console', script)(console); } catch (e) { console.log('Error in script:', e.message); } } /** * Fetches, executes, and posts the output for the next script. * * NOTE: This is the entry point for the polling loop. */ function exec() { if (!polling) return; getScript(script => { output = ''; // sanity reset runScript(script); postOutput(); }); } /** * Starts the polling loop. */ function startPolling() { if (polling) return; polling = true; exec(); } /** * Stops the polling loop. */ function stopPolling() { polling = false; } /** * Paints the UI dialog based on the current state. */ function paintUI() { let label; if (!connected) { label = 'Discovering MCP servers... Make sure the libresprite-mcp server is running.'; } else if (polling) { label = 'Connected to the libresprite-mcp server!'; } else { label = 'Found an active libresprite-mcp server, "Connect" when you are ready!'; } if (dialog) { dialog.close(); } dialog = app.createDialog(); dialog.title = 'libresprite-mcp'; dialog.addLabel(label); dialog.addBreak(); dialog.canClose = !connected || !polling; if ( connected ) { dialog.addButton( polling ? 'Disconnect': 'Connect', 'toggle' ); } } // MAIN // /** * Event handler. * * @global * @param {string} event */ function onEvent(event) { switch (event) { /** * Initialize script. */ case 'init': active = true; checkServerHealth(); paintUI(); return; /** * Cleanup script. */ case '_close': active = false; connected = false; polling = false; return; /** * Events triggered by initial health checks. */ case 'bad_health': if (!active) { // The extension was closed, stop recursion... return; } checkServerHealth(); return; case 'good_health': paintUI(); return; /** * UI operation. */ case 'toggle_click': if (polling) { stopPolling(); } else { startPolling(); } paintUI(); return; /** * Successful 'GET' event response triggered by `storage.fetch`. */ case '_get_response_fetch': _get_response && _get_response(); _get_response = null; return; /** * Successful 'POST' event response triggered by `storage.fetch`. */ case '_post_response_fetch': _post_response && _post_response(); _post_response = null; return; /** * Event triggered to continue polling the endpoint. */ case 'poll': if (!active) { // The extension was closed, stop recursion... // NOTE: This is a sanity check and should never be executed given the close button is not visible during polling stopPolling(); return; } exec(); return; default: // No action for unknown events break; } } global.onEvent = onEvent; })(); ``` -------------------------------------------------------------------------------- /src/libresprite_mcp/resources/reference.txt: -------------------------------------------------------------------------------- ``` # [class Sprite] ## Properties: - `palette`: read-only. Returns the sprite's palette. - `selection`: Placeholder. Do not use. - `height`: read+write. Returns and sets the height of the sprite. - `width`: read+write. Returns and sets the width of the sprite. - `filename`: read-only. Returns the file name of the sprite. - `colorMode`: read-only. Returns the sprite's ColorMode. - `layerCount`: read-only. Returns the amount of layers in the sprite. ## Methods: - `loadPalette(fileName)`: - fileName: The name of the palette file to load returns: Nothing loads a palette file. - `crop(x, y, width, height)`: - x: The left-most edge of the crop. - y: The top-most edge of the crop. - width: The width of the cropped area. - height: The height of the cropped area. returns: Nothing crops the sprite to the specified dimensions. - `saveAs(fileName, asCopy)`: - fileName: String. The new name of the file - asCopy: If true, the file is saved as a copy. Requires fileName to be specified. returns: Nothing saves the sprite. - `resize(width, height)`: - width: The new width. - height: The new height. returns: Nothing resizes the sprite. - `save()`: returns: Nothing saves the sprite. - `commit()`: returns: Nothing commits the current transaction. - `layer(layerNumber)`: - layerNumber: The number of they layer, starting with zero from the bottom. returns: a Layer object or null if invalid. allows you to access a given layer. # global storage [class Storage] ## No Properties. ## Methods: - `decodeBase64()`: returns: Nothing - `get()`: returns: Nothing - `save()`: returns: Nothing - `set()`: returns: Nothing - `fetch()`: returns: Nothing - `load()`: returns: Nothing - `unload()`: returns: Nothing # [class PalettelistboxWidget] ## Properties: - `selected`: - `id`: ## Methods: - `addPalette()`: returns: Nothing # [class pixelColor] ## No Properties. ## Methods: - `grayaA(color)`: - color: A 32-bit color in 888 RGBA format returns: The alpha component of the color Extracts the alpha (opacity) from a 32-bit color - `grayaV(color)`: - color: A 32-bit color in 888 RGBA format returns: The luminance Value of the color Extracts the luminance from a 32-bit color - `rgbaA(color)`: - color: A 32-bit color in 8888 RGBA format returns: The alpha component of the color Extracts the alpha channel from a 32-bit color - `graya(gray, alpha)`: - gray: The luminance of color - alpha: The alpha (opacity) of the color) returns: The color with the given luminance/opacity - `rgba(r, g, b, a)`: - r: red, 0-255. - g: green, 0-255. - b: blue, 0-255. - a: alpha (opacity), 0-255. returns: A 32-bit color in 8888 RGBA format. Converts R, G, B, A values into a single 32-bit RGBA color. - `rgbaG(color)`: - color: A 32-bit color in 8888 RGBA format returns: The green component of the color Extracts the green channel from a 32-bit color - `rgbaB(color)`: - color: A 32-bit color in 8888 RGBA format returns: The blue component of the color Extracts the blue channel from a 32-bit color - `rgbaR(color)`: - color: A 32-bit color in 8888 RGBA format returns: The red component of the color Extracts the red channel from a 32-bit color # [class LabelWidget] ## Properties: - `text`: - `id`: ## No Methods. # [class Layer] ## Properties: - `flags`: read-only. Returns all flags OR'd together as an int - `isContinuous`: read-only. Prefer to link cels when the user copies them. - `celCount`: read-only. Returns the number of cels. - `isMovable`: read-only. Returns true if the layer is movable. - `isVisible`: read+write. Gets/sets whether the layer is visible or not. - `isTransparent`: read-only. Returns true if the layer is a non-background image layer. - `isBackground`: read-only. Returns true if the layer is a background layer. - `isImage`: read-only. Returns true if the layer is an image, false if it is a folder. - `isEditable`: read+write. Gets/sets whether the layer is editable (unlocked) or not (locked). - `name`: read+write. The name of the layer. ## Methods: - `cel(index)`: - index: The number of the Cel returns: A Cel object or null if an invalid index is passed retrieves a Cel # [class IntentryWidget] ## Properties: - `value`: - `min`: - `max`: - `id`: ## No Methods. # [class Image] ## Properties: - `format`: read-only. The PixelFormat of the image. - `stride`: read-only. The number of bytes per image row. - `height`: read-only. The height of the image. - `width`: read-only. The width of the image. ## Methods: - `putPixel(x, y, color)`: - x: integer - y: integer - color: a 32-bit color in 8888 RGBA format. returns: Nothing writes the color onto the image at the the given coordinate. - `getImageData()`: returns: All pixels in a Uint8Array creates an array containing all of the image's pixels. - `putImageData(data)`: - data: All of the pixels in the image. returns: Nothing writes the given pixels onto the image. Must be the same size as the image. - `getPNGData()`: returns: The image as a Base64-encoded PNG string. Encodes the image as a PNG. - `clear(color)`: - color: a 32-bit color in 8888 RGBA format. returns: Nothing clears the image with the specified color. - `getPixel(x, y)`: - x: integer - y: integer returns: a color value reads a color from the given coordinate of the image. # [class PN2ui6DialogE] ## Properties: - `canClose`: write only. Determines if the user can close the dialog window. - `title`: read+write. Sets the title of the dialog window. - `id`: ## Methods: - `addBreak()`: returns: Nothing - `addPaletteListBox()`: returns: Nothing - `addEntry()`: returns: Nothing - `addButton()`: returns: Nothing - `add()`: returns: Nothing - `close()`: returns: Nothing - `addLabel()`: returns: Nothing - `addDropDown()`: returns: Nothing - `addIntEntry()`: returns: Nothing - `get()`: returns: Nothing # global console [class Console] ## No Properties. ## Methods: - `assert()`: returns: Nothing - `log()`: returns: Nothing # [class ButtonWidget] ## Properties: - `text`: - `id`: ## No Methods. # [class command] ## No Properties. ## Methods: - `Zoom()`: returns: Nothing Zoom in - `ToggleFullscreen()`: returns: Nothing Toggle Fullscreen - `Timeline()`: returns: Nothing Switch Timeline - `TiledMode()`: returns: Nothing Tiled Mode - `SymmetryMode()`: returns: Nothing Symmetry Mode - `SpriteSize()`: returns: Nothing Sprite Size - `SnapToGrid()`: returns: Nothing Snap to Grid - `ShowLayerEdges()`: returns: Nothing Show Layer Edges - `TogglePreview()`: returns: Nothing Toggle Preview - `ShowGrid()`: returns: Nothing Show Grid - `ShowExtras()`: returns: Nothing Show Extras - `ShowBrushPreview()`: returns: Nothing Show Brush Preview - `Share()`: returns: Nothing Share - `SetSameInk()`: returns: Nothing Same Ink in All Tools - `SetInkType()`: returns: Nothing Set Ink Type: Simple Ink - `SetColorSelector()`: returns: Nothing Set Color Selector: Color Spectrum - `SelectionAsGrid()`: returns: Nothing Selection as Grid - `SelectTile()`: returns: Nothing Select Tile - `Scroll()`: returns: Nothing Scroll 0 pixels left - `SavePalette()`: returns: Nothing Save Palette - `SaveFileCopyAs()`: returns: Nothing Save File Copy As - `SaveFileAs()`: returns: Nothing Save File As - `SaveFile()`: returns: Nothing Save File - `SetLoopSection()`: returns: Nothing Set Loop Section - `RunScript()`: returns: Nothing Run Script - `ReverseFrames()`: returns: Nothing Reverse Frames - `ReselectMask()`: returns: Nothing Reselect Mask - `RescanScripts()`: returns: Nothing Rescan Scripts - `ReplaceColor()`: returns: Nothing Replace Color - `RepeatLastExport()`: returns: Nothing Repeat Last Export - `PlayAnimation()`: returns: Nothing Play Animation - `PixelPerfectMode()`: returns: Nothing Switch Pixel Perfect Mode - `Options()`: returns: Nothing Options - `OpenFile()`: returns: Nothing Open Sprite - `NewSpriteFromSelection()`: returns: Nothing New Sprite From Selection - `MaskByColor()`: returns: Nothing Mask By Color - `NewFrameTag()`: returns: Nothing New Frame Tag - `NewFile()`: returns: Nothing New File - `UndoHistory()`: returns: Nothing Undo History - `GotoNextLayer()`: returns: Nothing Go to Next Layer - `NewBrush()`: returns: Nothing New Brush - `OpenInFolder()`: returns: Nothing Open In Folder - `ClearCel()`: returns: Nothing Clear Cel - `MoveMask()`: returns: Nothing Move Selection Boundaries 0 pixels left - `FrameTagProperties()`: returns: Nothing Frame Tag Properties - `AddColor()`: returns: Nothing Add Foreground Color to Palette - `MoveCel()`: returns: Nothing Move Cel - `MergeDownLayer()`: returns: Nothing Merge Down Layer - `MaskAll()`: returns: Nothing Mask All - `SaveMask()`: returns: Nothing Save Mask - `LoadMask()`: returns: Nothing LoadMask - `LayerProperties()`: returns: Nothing Layer Properties - `LayerFromBackground()`: returns: Nothing Layer From Background - `SetPaletteEntrySize()`: returns: Nothing Set Palette Entry Size - `Launch()`: returns: Nothing Launch - `KeyboardShortcuts()`: returns: Nothing Keyboard Shortcuts - `InvertMask()`: returns: Nothing Invert Mask - `InvertColor()`: returns: Nothing Invert Color - `GridSettings()`: returns: Nothing Grid Settings - `GotoPreviousTab()`: returns: Nothing Go to Previous tab - `ScrollCenter()`: returns: Nothing Scroll to center of canvas - `GotoPreviousLayer()`: returns: Nothing Go to Previous Layer - `GotoPreviousFrameWithSameTag()`: returns: Nothing Go to Previous Frame with same tag - `GotoPreviousFrame()`: returns: Nothing Go to Previous Frame - `GotoNextTab()`: returns: Nothing Go to Next Tab - `AutocropSprite()`: returns: Nothing Trim Sprite - `ImportSpriteSheet()`: returns: Nothing Import Sprite Sheet - `ShowPixelGrid()`: returns: Nothing Show Pixel Grid - `Home()`: returns: Nothing Home - `UnlinkCel()`: returns: Nothing Unlink Cel - `GotoNextFrameWithSameTag()`: returns: Nothing Go to Next Frame with same tag - `CropSprite()`: returns: Nothing Crop Sprite - `GotoLastFrame()`: returns: Nothing Go to Last Frame - `OpenWithApp()`: returns: Nothing Open With Associated Application - `GotoFirstFrame()`: returns: Nothing Go to First Frame - `RemoveFrameTag()`: returns: Nothing Remove Frame Tag - `NewFrame()`: returns: Nothing New Frame - `FullscreenPreview()`: returns: Nothing Fullscreen Preview - `SpriteProperties()`: returns: Nothing Sprite Properties - `NewLayer()`: returns: Nothing New Layer - `FrameProperties()`: returns: Nothing Frame Properties - `DeselectMask()`: returns: Nothing Deselect Mask - `AlternateTouchbar()`: returns: Nothing Alternate Touchbar - `ExportSpriteSheet()`: returns: Nothing Export Sprite Sheet - `NewLayerSet()`: returns: Nothing New Layer Set - `ModifySelection()`: returns: Nothing Expand Selection - `Paste()`: returns: Nothing Paste - `DiscardBrush()`: returns: Nothing Discard Brush - `BackgroundFromLayer()`: returns: Nothing BackgroundFromLayer - `DuplicateView()`: returns: Nothing Duplicate View - `About()`: returns: Nothing About - `DeveloperConsole()`: returns: Nothing Developer Console - `DuplicateSprite()`: returns: Nothing Duplicate Sprite - `LinkCels()`: returns: Nothing Links Cels - `CopyMerged()`: returns: Nothing Copy Merged - `MaskContent()`: returns: Nothing Mask Content - `DuplicateLayer()`: returns: Nothing Duplicate Layer - `CopyCel()`: returns: Nothing Copy Cel - `Refresh()`: returns: Nothing Refresh - `Copy()`: returns: Nothing Copy - `RemoveFrame()`: returns: Nothing Remove Frame - `SetPalette()`: returns: Nothing Set Palette - `OpenScriptsFolder()`: returns: Nothing Open Scripts Folder - `FlattenLayers()`: returns: Nothing Flatten Layers - `Eyedropper()`: returns: Nothing Eyedropper - `PaletteSize()`: returns: Nothing Palette Size - `ConvolutionMatrix()`: returns: Nothing Convolution Matrix - `clearParameters()`: returns: Nothing - `Cut()`: returns: Nothing Cut - `PaletteEditor()`: returns: Nothing Palette Editor - `RemoveLayer()`: returns: Nothing Remove Layer - `Clear()`: returns: Nothing Clear - `Exit()`: returns: Nothing Exit - `ColorQuantization()`: returns: Nothing Create Palette from Current Sprite (Color Quantization) - `AlternateToolbar()`: returns: Nothing Alternate Toolbar - `ChangeColor()`: returns: Nothing Color - `ChangeBrush()`: returns: Nothing Brush - `Cancel()`: returns: Nothing Cancel Current Operation - `SwitchColors()`: returns: Nothing Switch Colors - `ShowOnionSkin()`: returns: Nothing Show Onion Skin - `ChangePixelFormat()`: returns: Nothing Change Pixel Format - `ColorCurve()`: returns: Nothing Color Curve - `PasteText()`: returns: Nothing Insert Text - `CelProperties()`: returns: Nothing Cel Properties - `Despeckle()`: returns: Nothing Despeckle - `CloseAllFiles()`: returns: Nothing Close All Files - `LoadPalette()`: returns: Nothing Load Palette - `CanvasSize()`: returns: Nothing Canvas Size - `Undo()`: returns: Nothing Undo - `LayerVisibility()`: returns: Nothing Layer Visibility - `Flip()`: returns: Nothing Flip Canvas Horizontal - `Rotate()`: returns: Nothing Rotate Sprite 0° - `Redo()`: returns: Nothing Redo - `AlternateTimeline()`: returns: Nothing Alternate Timeline - `ShowSelectionEdges()`: returns: Nothing Show Selection Edges - `GotoFrame()`: returns: Nothing Go to Frame - `CloseFile()`: returns: Nothing Close File - `ToggleTouchbar()`: returns: Nothing Toggle Touchbar - `GotoNextFrame()`: returns: Nothing Go to Next Frame - `AdvancedMode()`: returns: Nothing Advanced Mode - `setParameter()`: returns: Nothing # [class EntryWidget] ## Properties: - `value`: - `maxsize`: - `id`: ## No Methods. # [class Palette] ## Properties: - `length`: ## Methods: - `set()`: returns: Nothing - `get()`: returns: Nothing # [class Document] ## Properties: - `sprite`: ## Methods: - `close()`: returns: Nothing # global ColorMode [class ColorMode] ## Properties: - `BITMAP`: - `INDEXED`: - `GRAYSCALE`: - `RGB`: ## No Methods. # [class Cel] ## Properties: - `frame`: - `image`: - `y`: - `x`: ## Methods: - `setPosition()`: returns: Nothing # global app [class App] ## Properties: - `platform`: read-only. Returns one of: emscripten, windows, macos, android, linux. - `version`: read-only. Returns LibreSprite's current version as a string. - `activeDocument`: read-only. Returns the currently active Document. - `command`: read-only. Returns an object with functions for running commands. - `activeSprite`: read-only. Returns the currently active Sprite. - `activeLayerNumber`: read-only. Returns the number of the current layer. - `activeImage`: read-only, can be null. Returns the current layer/frame's image. - `pixelColor`: read-only. Returns an object with functions for color conversion. - `activeFrameNumber`: read-only. Returns the number of the currently active animation frame. ## Methods: - `launch()`: returns: Nothing - `open()`: returns: Nothing Opens a document for editing - `yield(event)`: - event: Name of the event to be raised. The default is yield. returns: Nothing Schedules a yield event on the next frame - `createDialog()`: returns: Nothing Creates a dialog window - `documentation()`: returns: Nothing Prints this text. ```