From 1557fa1dc852f66a5cfb7ed73d5d6673ce76241d Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 29 Jul 2026 16:03:58 +0530 Subject: [PATCH 01/12] Prepare Screenly MCP for Claude Desktop marketplace listing. Add tool annotations, an MCPB bundle manifest, release packaging for desktop platforms, and route logger output to stderr so the MCP stdio JSON-RPC stream stays clean. --- .github/workflows/release.yml | 25 +++ .gitignore | 3 + Cargo.toml | 4 +- README.md | 22 ++- mcpb/README.md | 98 +++++++++++ mcpb/manifest.json | 199 +++++++++++++++++++++ src/mcp/server.rs | 319 ++++++++++++++++++++++++++++++---- 7 files changed, 629 insertions(+), 41 deletions(-) create mode 100644 mcpb/README.md create mode 100644 mcpb/manifest.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02cea212..a9c35bc3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,14 +50,17 @@ jobs: os: macos-15 rust: stable target: x86_64-apple-darwin + mcpb_platform: darwin - build: macos-aarch64 os: macos-15 rust: stable target: aarch64-apple-darwin + mcpb_platform: darwin - build: windows os: ubuntu-22.04 rust: stable target: x86_64-pc-windows-gnu + mcpb_platform: win32 - build: windows-32 os: ubuntu-22.04 rust: stable @@ -105,6 +108,28 @@ jobs: fi cd - + # Only desktop targets are bundled: Claude Desktop runs on macOS and Windows. + - name: Package MCP Bundle + if: matrix.mcpb_platform != '' + shell: bash + run: | + set -euo pipefail + mkdir -p mcpb-build/server + cp mcpb/README.md mcpb-build/ + if [[ "${{ matrix.build }}" == windows* ]]; then + cp "target/${{ matrix.target }}/release/screenly.exe" mcpb-build/server/ + else + cp "target/${{ matrix.target }}/release/screenly" mcpb-build/server/ + fi + # The committed manifest carries a 0.0.0 placeholder so the version cannot + # drift from Cargo.toml; the tag is the single source of truth. + jq --arg version "${GITHUB_REF_NAME#v}" \ + --arg platform "${{ matrix.mcpb_platform }}" \ + '.version = $version | .compatibility.platforms = [$platform]' \ + mcpb/manifest.json > mcpb-build/manifest.json + npx --yes @anthropic-ai/mcpb@2 pack \ + mcpb-build "screenly-cli-${{ matrix.target }}.mcpb" + - name: Publish uses: softprops/action-gh-release@v1 # TODO: if any of the build step fails, the release should be deleted. diff --git a/.gitignore b/.gitignore index 6d5bfc95..61b20296 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ # Build output for nix result* + +# Local MCP Bundle builds (release artifacts) +*.mcpb diff --git a/Cargo.toml b/Cargo.toml index e334b29b..1f3d21f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,9 @@ serde_with = "3.8.3" serde_yaml = "0.9.17" sha1 = "0.10.5" sha2 = "0.10.7" -simple_logger = { version = "5", features = ["colors"] } +# The "stderr" feature keeps stdout free of log output, which the `mcp` +# subcommand needs for its JSON-RPC stream. +simple_logger = { version = "5", features = ["colors", "stderr"] } strum = "0.27" strum_macros = "0.27" temp-env = "0.3.6" diff --git a/README.md b/README.md index 983b48e9..e981c869 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,8 @@ $ screenly --output json screen list > screens.json ``` > [!NOTE] -> In debug builds, the CLI outputs log messages to stdout. Use `RUST_LOG=off` to suppress them -> when redirecting output to a file: -> ```bash -> $ RUST_LOG=off screenly --output csv screen list > screens.csv -> $ RUST_LOG=off screenly --output json screen list > screens.json -> ``` +> Log messages go to stderr, so redirecting stdout to a file captures only command output. +> Use `RUST_LOG` to change the log level, or `RUST_LOG=off` to silence logging entirely. ## MCP Server (AI Assistant Integration) @@ -124,9 +120,21 @@ The server communicates over stdio and exposes the full Screenly API as tools. | **Shared Playlists** | `shared_playlist_list`, `shared_playlist_create`, `shared_playlist_delete` | | **Edge Apps** | `edge_app_list`, `edge_app_list_settings`, `edge_app_list_instances` | +Every tool is annotated with behaviour hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`), so MCP clients can tell read-only tools apart from ones that modify or delete data and prompt for confirmation before destructive actions. + ### Configuration Examples -#### Cursor / Claude Desktop +#### Claude Desktop Extension (`.mcpb`) + +For [Claude Desktop](https://claude.ai/download), the easiest option is the one-click MCP Bundle, which requires no manual JSON editing: + +1. Download `screenly-cli-.mcpb` from the [latest release](https://github.com/Screenly/cli/releases/latest) (for example `screenly-cli-aarch64-apple-darwin.mcpb` on an Apple Silicon Mac). +2. Open the file. Claude Desktop shows an installation dialog. +3. Paste your Screenly API token when prompted. + +The token is stored in your operating system's keychain rather than a plaintext config file. See [`mcpb/README.md`](mcpb/README.md) for details. + +#### Cursor / other clients Add to your MCP configuration file: diff --git a/mcpb/README.md b/mcpb/README.md new file mode 100644 index 00000000..640389a9 --- /dev/null +++ b/mcpb/README.md @@ -0,0 +1,98 @@ +# Screenly for Claude Desktop + +Manage your [Screenly](https://www.screenly.io) digital signage network from Claude. + +This is the packaging directory for the Screenly MCP Bundle (`.mcpb`). The bundle wraps +the MCP server that ships inside the [Screenly CLI](https://github.com/Screenly/cli) so it +can be installed into Claude Desktop with a single click, with no terminal setup required. + +## Installation + +1. Download `screenly-cli-.mcpb` from the + [latest release](https://github.com/Screenly/cli/releases/latest), for example + `screenly-cli-aarch64-apple-darwin.mcpb` on an Apple Silicon Mac. +2. Open the file. Claude Desktop shows an installation dialog. +3. Paste your Screenly API token when prompted. You can generate a token in the + [Screenly console](https://console.screenly.io) under **Settings → Tokens**. + +## What you can do + +Once installed, you can ask Claude to: + +- Review your screens and check which ones are offline or out of sync +- Add a web page, image, or video as an asset +- Build a playlist and schedule it, for example "only during business hours on weekdays" +- Organise content with asset groups and labels +- Share a playlist with another team +- Inspect Edge Apps, their settings, and their instances + +## Capabilities + +The bundle exposes 33 tools. Every tool is annotated so Claude knows whether it only reads +data or modifies your account, which means Claude will ask for confirmation before doing +anything destructive. + +| Category | Tools | +| --- | --- | +| Screens | `screen_list`, `screen_get` | +| Assets | `asset_list`, `asset_get`, `asset_create`, `asset_update`, `asset_delete` | +| Asset Groups | `asset_group_list`, `asset_group_create`, `asset_group_update`, `asset_group_delete` | +| Playlists | `playlist_list`, `playlist_create`, `playlist_update`, `playlist_delete` | +| Playlist Items | `playlist_item_list`, `playlist_item_create`, `playlist_item_update`, `playlist_item_delete` | +| Labels | `label_list`, `label_create`, `label_update`, `label_delete`, `label_link_screen`, `label_unlink_screen`, `label_link_playlist`, `label_unlink_playlist` | +| Shared Playlists | `shared_playlist_list`, `shared_playlist_create`, `shared_playlist_delete` | +| Edge Apps | `edge_app_list`, `edge_app_list_settings`, `edge_app_list_instances` | + +Twelve of these tools are read-only. Eight are destructive and delete content or remove +associations. + +## Authentication + +The bundle authenticates with a Screenly API token, which you provide during installation. +The token is marked as sensitive in the bundle manifest, so Claude Desktop stores it in your +operating system's keychain rather than in a plaintext configuration file. + +Tokens are scoped to a single Screenly team. To limit what the extension can reach, use a +token for a team that contains only the screens you want Claude to manage. You can revoke a +token at any time from the Screenly console. + +## Privacy Policy + +Screenly's privacy policy is available at +. + +This extension connects only to the Screenly API at `api.screenly.io` over HTTPS. Requests +are made directly from your machine to Screenly using the API token you supply. + +The data returned to Claude is the data you ask for: your screens, assets, playlists, +labels, and Edge Apps, along with their metadata. The extension collects no analytics, sends +no telemetry, and transmits data to no third party other than Screenly itself. Your API +token is sent only to Screenly, as the credential for those API requests. + +## Support + +- Documentation: +- Issues: + +## Building the bundle + +Bundles are built automatically for each tagged release by +[`.github/workflows/release.yml`](../.github/workflows/release.yml), which injects the +release version into `manifest.json` and packs it together with the compiled `screenly` +binary. + +To build one locally: + +```bash +npm install -g @anthropic-ai/mcpb + +cargo build --release + +mkdir -p build/server +cp mcpb/manifest.json mcpb/README.md build/ +cp target/release/screenly build/server/ +mcpb pack build screenly.mcpb +``` + +The `version` field in the committed `manifest.json` is a `0.0.0` placeholder. The release +workflow replaces it with the Git tag so it cannot drift from `Cargo.toml`. diff --git a/mcpb/manifest.json b/mcpb/manifest.json new file mode 100644 index 00000000..b0cf2ea7 --- /dev/null +++ b/mcpb/manifest.json @@ -0,0 +1,199 @@ +{ + "manifest_version": "0.3", + "name": "screenly", + "display_name": "Screenly", + "version": "0.0.0", + "description": "Manage your Screenly digital signage network: screens, playlists, assets, labels, and Edge Apps.", + "long_description": "Screenly is a digital signage platform for managing screens at scale. This extension exposes your Screenly account to Claude so you can manage your signage network in plain language.\n\nYou can list and inspect screens along with their sync state and hardware details, create assets from web pages, images, or videos, build and schedule playlists, organise content with asset groups and labels, share playlists with other teams, and inspect Edge Apps and their settings.\n\nPlaylists support Screenly's scheduling predicate language, so you can ask for content that only appears during business hours or on weekdays and it will be translated into the correct predicate for you.\n\nThe extension runs the MCP server bundled in the official Screenly CLI, talking directly to the Screenly API over HTTPS using an API token you supply.", + "author": { + "name": "Screenly, Inc.", + "url": "https://www.screenly.io" + }, + "repository": { + "type": "git", + "url": "https://github.com/Screenly/cli.git" + }, + "homepage": "https://www.screenly.io", + "documentation": "https://developer.screenly.io/mcp", + "support": "https://github.com/Screenly/cli/issues", + "license": "MIT", + "keywords": [ + "digital-signage", + "screenly", + "screens", + "playlists", + "edge-apps" + ], + "privacy_policies": [ + "https://www.screenly.io/privacy-policy/" + ], + "server": { + "type": "binary", + "entry_point": "server/screenly", + "mcp_config": { + "command": "${__dirname}/server/screenly", + "args": [ + "mcp" + ], + "env": { + "API_TOKEN": "${user_config.api_token}" + }, + "platform_overrides": { + "win32": { + "command": "${__dirname}/server/screenly.exe" + } + } + } + }, + "user_config": { + "api_token": { + "type": "string", + "title": "Screenly API Token", + "description": "API token for your Screenly team. Generate one at https://[your-workspace].screenlyapp.com under Settings > Security > API tokens.", + "sensitive": true, + "required": true + } + }, + "tools_generated": false, + "tools": [ + { + "name": "screen_list", + "description": "List all screens with their status, hardware info, and sync state." + }, + { + "name": "screen_get", + "description": "Get a screen by UUID." + }, + { + "name": "asset_list", + "description": "List all assets with their type, status, and metadata." + }, + { + "name": "asset_get", + "description": "Get an asset by UUID." + }, + { + "name": "asset_create", + "description": "Create a new asset from a URL. Supports web pages, images, and videos." + }, + { + "name": "asset_update", + "description": "Update an asset's properties (title, js_injection, headers)." + }, + { + "name": "asset_delete", + "description": "Delete an asset by UUID." + }, + { + "name": "asset_group_list", + "description": "List all asset groups (folders for organizing assets)." + }, + { + "name": "asset_group_create", + "description": "Create a new asset group." + }, + { + "name": "asset_group_update", + "description": "Update an asset group." + }, + { + "name": "asset_group_delete", + "description": "Delete an asset group. WARNING: Also deletes all assets in the group." + }, + { + "name": "playlist_list", + "description": "List all playlists." + }, + { + "name": "playlist_create", + "description": "Create a new playlist." + }, + { + "name": "playlist_update", + "description": "Update a playlist." + }, + { + "name": "playlist_delete", + "description": "Delete a playlist by UUID." + }, + { + "name": "playlist_item_list", + "description": "List all items in a playlist." + }, + { + "name": "playlist_item_create", + "description": "Add an asset to a playlist." + }, + { + "name": "playlist_item_update", + "description": "Update a playlist item (duration, position)." + }, + { + "name": "playlist_item_delete", + "description": "Remove an item from a playlist." + }, + { + "name": "label_list", + "description": "List all labels. Labels group screens and target playlists." + }, + { + "name": "label_create", + "description": "Create a new label." + }, + { + "name": "label_update", + "description": "Update a label." + }, + { + "name": "label_delete", + "description": "Delete a label." + }, + { + "name": "label_link_screen", + "description": "Attach a label to a screen." + }, + { + "name": "label_unlink_screen", + "description": "Remove a label from a screen." + }, + { + "name": "label_link_playlist", + "description": "Attach a label to a playlist." + }, + { + "name": "label_unlink_playlist", + "description": "Remove a label from a playlist." + }, + { + "name": "shared_playlist_list", + "description": "List shared playlists." + }, + { + "name": "shared_playlist_create", + "description": "Share a playlist with another team." + }, + { + "name": "shared_playlist_delete", + "description": "Unshare a playlist from a team." + }, + { + "name": "edge_app_list", + "description": "List all Edge Apps." + }, + { + "name": "edge_app_list_settings", + "description": "List settings for an Edge App." + }, + { + "name": "edge_app_list_instances", + "description": "List instances of an Edge App." + } + ], + "compatibility": { + "claude_desktop": ">=0.10.0", + "platforms": [ + "darwin", + "win32" + ] + } +} diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 29f9bb33..488f7410 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -220,7 +220,10 @@ impl ScreenlyMcpServer { impl ScreenlyMcpServer { // ============ SCREEN TOOLS ============ - #[tool(description = "List all screens with their status, hardware info, and sync state.")] + #[tool( + description = "List all screens with their status, hardware info, and sync state.", + annotations(title = "List Screens", read_only_hint = true, open_world_hint = false) + )] fn screen_list(&self) -> String { match ScreenTools::list(&self.auth) { Ok(result) => result, @@ -228,7 +231,10 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Get a screen by UUID.")] + #[tool( + description = "Get a screen by UUID.", + annotations(title = "Get Screen", read_only_hint = true, open_world_hint = false) + )] fn screen_get(&self, Parameters(UuidParam { uuid }): Parameters) -> String { match ScreenTools::get(&self.auth, &uuid) { Ok(result) => result, @@ -238,7 +244,10 @@ impl ScreenlyMcpServer { // ============ ASSET TOOLS ============ - #[tool(description = "List all assets with their type, status, and metadata.")] + #[tool( + description = "List all assets with their type, status, and metadata.", + annotations(title = "List Assets", read_only_hint = true, open_world_hint = false) + )] fn asset_list(&self) -> String { match AssetTools::list(&self.auth) { Ok(result) => result, @@ -246,7 +255,10 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Get an asset by UUID.")] + #[tool( + description = "Get an asset by UUID.", + annotations(title = "Get Asset", read_only_hint = true, open_world_hint = false) + )] fn asset_get(&self, Parameters(UuidParam { uuid }): Parameters) -> String { match AssetTools::get(&self.auth, &uuid) { Ok(result) => result, @@ -254,7 +266,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Create a new asset from a URL. Supports web pages, images, and videos.")] + #[tool( + description = "Create a new asset from a URL. Supports web pages, images, and videos.", + annotations( + title = "Create Asset", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn asset_create( &self, Parameters(AssetCreateParam { title, source_url }): Parameters, @@ -265,7 +286,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Update an asset's properties (title, js_injection, headers).")] + #[tool( + description = "Update an asset's properties (title, js_injection, headers).", + annotations( + title = "Update Asset", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = false + ) + )] fn asset_update( &self, Parameters(AssetUpdateParam { @@ -281,7 +311,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Delete an asset by UUID.")] + #[tool( + description = "Delete an asset by UUID.", + annotations( + title = "Delete Asset", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn asset_delete(&self, Parameters(UuidParam { uuid }): Parameters) -> String { match AssetTools::delete(&self.auth, &uuid) { Ok(result) => result, @@ -291,7 +330,14 @@ impl ScreenlyMcpServer { // ============ ASSET GROUP TOOLS ============ - #[tool(description = "List all asset groups (folders for organizing assets).")] + #[tool( + description = "List all asset groups (folders for organizing assets).", + annotations( + title = "List Asset Groups", + read_only_hint = true, + open_world_hint = false + ) + )] fn asset_group_list(&self) -> String { match AssetGroupTools::list(&self.auth) { Ok(result) => result, @@ -299,7 +345,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Create a new asset group.")] + #[tool( + description = "Create a new asset group.", + annotations( + title = "Create Asset Group", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn asset_group_create( &self, Parameters(TitleParam { title }): Parameters, @@ -310,7 +365,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Update an asset group.")] + #[tool( + description = "Update an asset group.", + annotations( + title = "Update Asset Group", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = false + ) + )] fn asset_group_update( &self, Parameters(AssetGroupUpdateParam { uuid, title }): Parameters, @@ -321,7 +385,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Delete an asset group. WARNING: Also deletes all assets in the group.")] + #[tool( + description = "Delete an asset group. WARNING: Also deletes all assets in the group.", + annotations( + title = "Delete Asset Group", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn asset_group_delete(&self, Parameters(UuidParam { uuid }): Parameters) -> String { match AssetGroupTools::delete(&self.auth, &uuid) { Ok(result) => result, @@ -331,7 +404,14 @@ impl ScreenlyMcpServer { // ============ PLAYLIST TOOLS ============ - #[tool(description = "List all playlists.")] + #[tool( + description = "List all playlists.", + annotations( + title = "List Playlists", + read_only_hint = true, + open_world_hint = false + ) + )] fn playlist_list(&self) -> String { match PlaylistTools::list(&self.auth) { Ok(result) => result, @@ -339,7 +419,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Create a new playlist.")] + #[tool( + description = "Create a new playlist.", + annotations( + title = "Create Playlist", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn playlist_create( &self, Parameters(PlaylistCreateParam { @@ -355,7 +444,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Update a playlist.")] + #[tool( + description = "Update a playlist.", + annotations( + title = "Update Playlist", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = false + ) + )] fn playlist_update( &self, Parameters(PlaylistUpdateParam { @@ -372,7 +470,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Delete a playlist by UUID.")] + #[tool( + description = "Delete a playlist by UUID.", + annotations( + title = "Delete Playlist", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn playlist_delete(&self, Parameters(UuidParam { uuid }): Parameters) -> String { match PlaylistTools::delete(&self.auth, &uuid) { Ok(result) => result, @@ -382,7 +489,14 @@ impl ScreenlyMcpServer { // ============ PLAYLIST ITEM TOOLS ============ - #[tool(description = "List all items in a playlist.")] + #[tool( + description = "List all items in a playlist.", + annotations( + title = "List Playlist Items", + read_only_hint = true, + open_world_hint = false + ) + )] fn playlist_item_list(&self, Parameters(UuidParam { uuid }): Parameters) -> String { match PlaylistItemTools::list(&self.auth, &uuid) { Ok(result) => result, @@ -390,7 +504,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Add an asset to a playlist.")] + #[tool( + description = "Add an asset to a playlist.", + annotations( + title = "Add Asset to Playlist", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn playlist_item_create( &self, Parameters(PlaylistItemCreateParam { @@ -407,7 +530,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Update a playlist item (duration, position).")] + #[tool( + description = "Update a playlist item (duration, position).", + annotations( + title = "Update Playlist Item", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = false + ) + )] fn playlist_item_update( &self, Parameters(PlaylistItemUpdateParam { @@ -424,7 +556,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Remove an item from a playlist.")] + #[tool( + description = "Remove an item from a playlist.", + annotations( + title = "Remove Playlist Item", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn playlist_item_delete( &self, Parameters(PlaylistItemDeleteParam { @@ -440,7 +581,10 @@ impl ScreenlyMcpServer { // ============ LABEL TOOLS ============ - #[tool(description = "List all labels. Labels group screens and target playlists.")] + #[tool( + description = "List all labels. Labels group screens and target playlists.", + annotations(title = "List Labels", read_only_hint = true, open_world_hint = false) + )] fn label_list(&self) -> String { match LabelTools::list(&self.auth) { Ok(result) => result, @@ -448,7 +592,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Create a new label.")] + #[tool( + description = "Create a new label.", + annotations( + title = "Create Label", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn label_create(&self, Parameters(NameParam { name }): Parameters) -> String { match LabelTools::create(&self.auth, &name) { Ok(result) => result, @@ -456,7 +609,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Update a label.")] + #[tool( + description = "Update a label.", + annotations( + title = "Update Label", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = false + ) + )] fn label_update( &self, Parameters(LabelUpdateParam { uuid, name }): Parameters, @@ -467,7 +629,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Delete a label.")] + #[tool( + description = "Delete a label.", + annotations( + title = "Delete Label", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn label_delete(&self, Parameters(UuidParam { uuid }): Parameters) -> String { match LabelTools::delete(&self.auth, &uuid) { Ok(result) => result, @@ -475,7 +646,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Attach a label to a screen.")] + #[tool( + description = "Attach a label to a screen.", + annotations( + title = "Attach Label to Screen", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn label_link_screen( &self, Parameters(LabelScreenParam { @@ -489,7 +669,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Remove a label from a screen.")] + #[tool( + description = "Remove a label from a screen.", + annotations( + title = "Detach Label from Screen", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn label_unlink_screen( &self, Parameters(LabelScreenParam { @@ -503,7 +692,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Attach a label to a playlist.")] + #[tool( + description = "Attach a label to a playlist.", + annotations( + title = "Attach Label to Playlist", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn label_link_playlist( &self, Parameters(LabelPlaylistParam { @@ -517,7 +715,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Remove a label from a playlist.")] + #[tool( + description = "Remove a label from a playlist.", + annotations( + title = "Detach Label from Playlist", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn label_unlink_playlist( &self, Parameters(LabelPlaylistParam { @@ -533,7 +740,14 @@ impl ScreenlyMcpServer { // ============ SHARED PLAYLIST TOOLS ============ - #[tool(description = "List shared playlists.")] + #[tool( + description = "List shared playlists.", + annotations( + title = "List Shared Playlists", + read_only_hint = true, + open_world_hint = false + ) + )] fn shared_playlist_list(&self) -> String { match SharedPlaylistTools::list(&self.auth) { Ok(result) => result, @@ -541,7 +755,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Share a playlist with another team.")] + #[tool( + description = "Share a playlist with another team.", + annotations( + title = "Share Playlist with Team", + read_only_hint = false, + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false + ) + )] fn shared_playlist_create( &self, Parameters(SharedPlaylistParam { @@ -555,7 +778,16 @@ impl ScreenlyMcpServer { } } - #[tool(description = "Unshare a playlist from a team.")] + #[tool( + description = "Unshare a playlist from a team.", + annotations( + title = "Unshare Playlist from Team", + read_only_hint = false, + destructive_hint = true, + idempotent_hint = true, + open_world_hint = false + ) + )] fn shared_playlist_delete( &self, Parameters(SharedPlaylistParam { @@ -571,7 +803,14 @@ impl ScreenlyMcpServer { // ============ EDGE APP TOOLS ============ - #[tool(description = "List all Edge Apps.")] + #[tool( + description = "List all Edge Apps.", + annotations( + title = "List Edge Apps", + read_only_hint = true, + open_world_hint = false + ) + )] fn edge_app_list(&self) -> String { match EdgeAppTools::list(&self.auth) { Ok(result) => result, @@ -579,7 +818,14 @@ impl ScreenlyMcpServer { } } - #[tool(description = "List settings for an Edge App.")] + #[tool( + description = "List settings for an Edge App.", + annotations( + title = "List Edge App Settings", + read_only_hint = true, + open_world_hint = false + ) + )] fn edge_app_list_settings( &self, Parameters(AppUuidParam { app_uuid }): Parameters, @@ -590,7 +836,14 @@ impl ScreenlyMcpServer { } } - #[tool(description = "List instances of an Edge App.")] + #[tool( + description = "List instances of an Edge App.", + annotations( + title = "List Edge App Instances", + read_only_hint = true, + open_world_hint = false + ) + )] fn edge_app_list_instances( &self, Parameters(AppUuidParam { app_uuid }): Parameters, From 90a6bd31cb98f93ef0e859f0e16702a64986a232 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 29 Jul 2026 16:15:47 +0530 Subject: [PATCH 02/12] Skip List screens action check on fork pull requests. Fork pull_request runs do not receive repository secrets, so SCREENLY_API_TOKEN is empty and `screen list` fails auth. The job also uses screenly/cli@master, so it never exercises the PR's own code. --- .github/workflows/actions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index c233f295..c70af714 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -5,6 +5,10 @@ on: jobs: test-github-action-workflow: + # Secrets are not available to pull_request runs from forks, so the + # SCREENLY_API_TOKEN would be empty and `screen list` would fail auth. + # Only run this integration check for same-repository pull requests. + if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest name: List screens steps: From 7bd86b9b70570a2f48d2c5b83de1aac1bf588af7 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 10:30:06 +0530 Subject: [PATCH 03/12] Correct MCPB privacy policy: real API host and Sentry disclosure. The listing text claimed api.screenly.io and no third-party telemetry; production uses api.screenlyapp.com and the CLI always initializes Sentry. --- mcpb/README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/mcpb/README.md b/mcpb/README.md index 640389a9..d4ec5aa7 100644 --- a/mcpb/README.md +++ b/mcpb/README.md @@ -61,13 +61,19 @@ token at any time from the Screenly console. Screenly's privacy policy is available at . -This extension connects only to the Screenly API at `api.screenly.io` over HTTPS. Requests -are made directly from your machine to Screenly using the API token you supply. +This extension connects to the Screenly API at `api.screenlyapp.com` over HTTPS (or another +Screenly API host if configured via `API_BASE_URL`). Requests are made directly from your +machine using the API token you supply. The data returned to Claude is the data you ask for: your screens, assets, playlists, -labels, and Edge Apps, along with their metadata. The extension collects no analytics, sends -no telemetry, and transmits data to no third party other than Screenly itself. Your API -token is sent only to Screenly, as the credential for those API requests. +labels, and Edge Apps, along with their metadata. Your API token is sent only to Screenly, +as the credential for those API requests. + +Separately, the Screenly CLI initializes [Sentry](https://sentry.io) crash reporting on +startup (including when run as this extension). If the process panics, diagnostic details +such as the stack trace and device/OS context may be sent to Sentry's ingest endpoint +(`*.ingest.sentry.io`). This is used for reliability debugging, not product analytics. +Screenly's handling of that data is covered by the privacy policy linked above. ## Support From 789f0e07eeac954705b0f79abb1b6b15c6ba8a62 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 10:48:34 +0530 Subject: [PATCH 04/12] Document Desktop Extensions as the primary MCPB install path. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefer Settings → Extensions over GitHub sideloads, and guide macOS Gatekeeper blocks via Privacy & Security → Open Anyway instead of xattr. --- README.md | 19 ++++++++++++------- mcpb/README.md | 27 +++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e981c869..c100baca 100644 --- a/README.md +++ b/README.md @@ -126,13 +126,18 @@ Every tool is annotated with behaviour hints (`readOnlyHint`, `destructiveHint`, #### Claude Desktop Extension (`.mcpb`) -For [Claude Desktop](https://claude.ai/download), the easiest option is the one-click MCP Bundle, which requires no manual JSON editing: - -1. Download `screenly-cli-.mcpb` from the [latest release](https://github.com/Screenly/cli/releases/latest) (for example `screenly-cli-aarch64-apple-darwin.mcpb` on an Apple Silicon Mac). -2. Open the file. Claude Desktop shows an installation dialog. -3. Paste your Screenly API token when prompted. - -The token is stored in your operating system's keychain rather than a plaintext config file. See [`mcpb/README.md`](mcpb/README.md) for details. +For [Claude Desktop](https://claude.ai/download), the expected install path is +**Desktop Extensions** (Settings → Extensions) — the same idea as installing +the CLI with Homebrew. Once Screenly is listed, install it there and paste your +API token when prompted. No manual JSON editing required. + +For testing before the listing is live, you can sideload a `.mcpb` from the +[latest release](https://github.com/Screenly/cli/releases/latest). macOS release bundles are +not Developer ID–signed yet (same as the CLI `.tar.gz` artifacts); a browser download may be +blocked by Gatekeeper. If that happens, use **System Settings → Privacy & Security → Open Anyway**. +Details: [`mcpb/README.md`](mcpb/README.md). + +The token is stored in your operating system's keychain rather than a plaintext config file. #### Cursor / other clients diff --git a/mcpb/README.md b/mcpb/README.md index d4ec5aa7..ce82af4a 100644 --- a/mcpb/README.md +++ b/mcpb/README.md @@ -8,12 +8,35 @@ can be installed into Claude Desktop with a single click, with no terminal setup ## Installation +### Recommended: Claude Desktop Extensions + +Once listed, install Screenly from **Desktop Extensions** in Claude Desktop +(Settings → Extensions) — the same idea as installing the CLI with Homebrew. +Claude handles download and setup; you only need to paste your Screenly API token +when prompted. + +You can generate a token in the [Screenly console](https://console.screenly.io) under +**Settings → Tokens**. + +### Sideload from a GitHub release (testing / pre-listing) + 1. Download `screenly-cli-.mcpb` from the [latest release](https://github.com/Screenly/cli/releases/latest), for example `screenly-cli-aarch64-apple-darwin.mcpb` on an Apple Silicon Mac. 2. Open the file. Claude Desktop shows an installation dialog. -3. Paste your Screenly API token when prompted. You can generate a token in the - [Screenly console](https://console.screenly.io) under **Settings → Tokens**. +3. Paste your Screenly API token when prompted. + +#### macOS Gatekeeper note + +Release `.mcpb` bundles currently ship the same unsigned macOS binary as the CLI +`.tar.gz` artifacts. A bundle downloaded in a browser may be blocked by Gatekeeper. + +If Claude Desktop cannot start the extension after a sideload install, open +**System Settings → Privacy & Security**, look for the blocked `screenly` +message, and click **Open Anyway**. Then try the extension again. + +Prefer the Desktop Extensions install once it is available. Developer ID signing and +notarization for release binaries is tracked separately and is not unique to the MCP bundle. ## What you can do From 5e6453c397739369e60d9c98864713a779ca6181 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 10:52:05 +0530 Subject: [PATCH 05/12] Tighten MCP tool annotation semantics per review. Mark asset_create as open-world, treat updates as destructive, and mark link/share tools idempotent so hints match MCP spec meaning. --- mcpb/README.md | 4 ++-- src/mcp/server.rs | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mcpb/README.md b/mcpb/README.md index ce82af4a..46a2be0f 100644 --- a/mcpb/README.md +++ b/mcpb/README.md @@ -66,8 +66,8 @@ anything destructive. | Shared Playlists | `shared_playlist_list`, `shared_playlist_create`, `shared_playlist_delete` | | Edge Apps | `edge_app_list`, `edge_app_list_settings`, `edge_app_list_instances` | -Twelve of these tools are read-only. Eight are destructive and delete content or remove -associations. +Twelve of these tools are read-only. Thirteen are marked destructive (deletes, unlinks, +and updates that overwrite existing fields) so clients can prompt before running them. ## Authentication diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 488f7410..4613ed31 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -273,7 +273,7 @@ impl ScreenlyMcpServer { read_only_hint = false, destructive_hint = false, idempotent_hint = false, - open_world_hint = false + open_world_hint = true ) )] fn asset_create( @@ -291,7 +291,7 @@ impl ScreenlyMcpServer { annotations( title = "Update Asset", read_only_hint = false, - destructive_hint = false, + destructive_hint = true, idempotent_hint = true, open_world_hint = false ) @@ -370,7 +370,7 @@ impl ScreenlyMcpServer { annotations( title = "Update Asset Group", read_only_hint = false, - destructive_hint = false, + destructive_hint = true, idempotent_hint = true, open_world_hint = false ) @@ -449,7 +449,7 @@ impl ScreenlyMcpServer { annotations( title = "Update Playlist", read_only_hint = false, - destructive_hint = false, + destructive_hint = true, idempotent_hint = true, open_world_hint = false ) @@ -535,7 +535,7 @@ impl ScreenlyMcpServer { annotations( title = "Update Playlist Item", read_only_hint = false, - destructive_hint = false, + destructive_hint = true, idempotent_hint = true, open_world_hint = false ) @@ -614,7 +614,7 @@ impl ScreenlyMcpServer { annotations( title = "Update Label", read_only_hint = false, - destructive_hint = false, + destructive_hint = true, idempotent_hint = true, open_world_hint = false ) @@ -652,7 +652,7 @@ impl ScreenlyMcpServer { title = "Attach Label to Screen", read_only_hint = false, destructive_hint = false, - idempotent_hint = false, + idempotent_hint = true, open_world_hint = false ) )] @@ -698,7 +698,7 @@ impl ScreenlyMcpServer { title = "Attach Label to Playlist", read_only_hint = false, destructive_hint = false, - idempotent_hint = false, + idempotent_hint = true, open_world_hint = false ) )] @@ -761,7 +761,7 @@ impl ScreenlyMcpServer { title = "Share Playlist with Team", read_only_hint = false, destructive_hint = false, - idempotent_hint = false, + idempotent_hint = true, open_world_hint = false ) )] From 4334ae813e5bd6f791d0b3167e5ead983e6730e0 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 10:57:41 +0530 Subject: [PATCH 06/12] Add a test that MCPB manifest tools match server.rs. Keeps the duplicated 33-tool name/description catalog from drifting between mcpb/manifest.json and the #[tool] handlers. --- src/mcp/tests.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/mcp/tests.rs b/src/mcp/tests.rs index 86a4a0b3..0a366d62 100644 --- a/src/mcp/tests.rs +++ b/src/mcp/tests.rs @@ -635,3 +635,77 @@ fn test_edge_app_list_instances() { let result = EdgeAppTools::list_instances(&auth, "app-uuid"); assert!(result.is_ok()); } + +/// Guard against the 33-tool catalog drifting between the MCPB manifest and +/// the `#[tool]` definitions in `server.rs` (names + descriptions). +#[test] +fn test_mcpb_manifest_tools_match_server() { + use regex::Regex; + use std::collections::BTreeMap; + use std::fs; + use std::path::PathBuf; + + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let manifest: serde_json::Value = serde_json::from_str( + &fs::read_to_string(manifest_dir.join("mcpb/manifest.json")) + .expect("mcpb/manifest.json should exist"), + ) + .expect("mcpb/manifest.json should be valid JSON"); + + assert_eq!( + manifest["tools_generated"], false, + "tools_generated must stay false while the tools array is the source of truth" + ); + + let mut manifest_tools = BTreeMap::new(); + for tool in manifest["tools"] + .as_array() + .expect("manifest tools must be an array") + { + let name = tool["name"].as_str().expect("tool name").to_string(); + let description = tool["description"] + .as_str() + .expect("tool description") + .to_string(); + assert!( + manifest_tools.insert(name.clone(), description).is_none(), + "duplicate tool in manifest: {name}" + ); + } + + let server_src = fs::read_to_string(manifest_dir.join("src/mcp/server.rs")) + .expect("src/mcp/server.rs should exist"); + let tool_re = + Regex::new(r#"(?s)#\[tool\(\s*description\s*=\s*"([^"]+)"[\s\S]*?\)\]\s*fn\s+(\w+)"#) + .unwrap(); + + let mut server_tools = BTreeMap::new(); + for caps in tool_re.captures_iter(&server_src) { + let description = caps[1].to_string(); + let name = caps[2].to_string(); + assert!( + server_tools.insert(name.clone(), description).is_none(), + "duplicate tool in server.rs: {name}" + ); + } + + assert_eq!( + server_tools.len(), + 33, + "expected 33 #[tool] handlers in server.rs, found {}", + server_tools.len() + ); + assert_eq!( + manifest_tools.keys().collect::>(), + server_tools.keys().collect::>(), + "tool names in mcpb/manifest.json must match src/mcp/server.rs" + ); + + for (name, server_description) in &server_tools { + assert_eq!( + manifest_tools.get(name).map(String::as_str), + Some(server_description.as_str()), + "description drift for tool `{name}`" + ); + } +} From 4a522e42dec1cd8def25b1e2aa372d34959aec4d Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 10:58:17 +0530 Subject: [PATCH 07/12] Set MCPB entry_point per platform in the release pack step. Windows bundles ship screenly.exe; bake matching entry_point/command into each single-platform manifest instead of relying on a stale override. --- .github/workflows/release.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9c35bc3..5d9a73c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,9 +123,20 @@ jobs: fi # The committed manifest carries a 0.0.0 placeholder so the version cannot # drift from Cargo.toml; the tag is the single source of truth. + # Windows packs screenly.exe, so entry_point/command must match the artifact. + if [[ "${{ matrix.build }}" == windows* ]]; then + entry_point="server/screenly.exe" + else + entry_point="server/screenly" + fi jq --arg version "${GITHUB_REF_NAME#v}" \ --arg platform "${{ matrix.mcpb_platform }}" \ - '.version = $version | .compatibility.platforms = [$platform]' \ + --arg entry_point "$entry_point" \ + '.version = $version + | .compatibility.platforms = [$platform] + | .server.entry_point = $entry_point + | .server.mcp_config.command = ("${__dirname}/" + $entry_point) + | del(.server.mcp_config.platform_overrides)' \ mcpb/manifest.json > mcpb-build/manifest.json npx --yes @anthropic-ai/mcpb@2 pack \ mcpb-build "screenly-cli-${{ matrix.target }}.mcpb" From 0fbd08626df434f072a83c736941fe2da913fa97 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 10:58:33 +0530 Subject: [PATCH 08/12] Drop unexplained claude_desktop version floor from the MCPB manifest. The >=0.10.0 bound had no Screenly-specific source and could silently block installs; keep platform compatibility only. --- mcpb/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/mcpb/manifest.json b/mcpb/manifest.json index b0cf2ea7..10da2c92 100644 --- a/mcpb/manifest.json +++ b/mcpb/manifest.json @@ -190,7 +190,6 @@ } ], "compatibility": { - "claude_desktop": ">=0.10.0", "platforms": [ "darwin", "win32" From 0ac561bfae88024aff1e47d784321a4cd14c24b4 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 10:58:48 +0530 Subject: [PATCH 09/12] Align MCPB token instructions with the manifest. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the workspace screenlyapp.com Settings → Security → API tokens path in both places so install docs no longer conflict. --- mcpb/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcpb/README.md b/mcpb/README.md index 46a2be0f..99a10bc4 100644 --- a/mcpb/README.md +++ b/mcpb/README.md @@ -15,8 +15,8 @@ Once listed, install Screenly from **Desktop Extensions** in Claude Desktop Claude handles download and setup; you only need to paste your Screenly API token when prompted. -You can generate a token in the [Screenly console](https://console.screenly.io) under -**Settings → Tokens**. +You can generate a token at `https://[your-workspace].screenlyapp.com` under +**Settings → Security → API tokens**. ### Sideload from a GitHub release (testing / pre-listing) From a8616e5ed5a1b5f85966d74f461d003b544f6020 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 11:00:43 +0530 Subject: [PATCH 10/12] Add a 512x512 icon to the MCPB bundle. Gives Claude Desktop and the marketplace listing a proper brand mark; the release pack step and local build docs now include icon.png. --- .github/workflows/release.yml | 2 +- mcpb/README.md | 2 +- mcpb/icon.png | Bin 0 -> 44460 bytes mcpb/manifest.json | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 mcpb/icon.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d9a73c8..cf82c142 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,7 +115,7 @@ jobs: run: | set -euo pipefail mkdir -p mcpb-build/server - cp mcpb/README.md mcpb-build/ + cp mcpb/README.md mcpb/icon.png mcpb-build/ if [[ "${{ matrix.build }}" == windows* ]]; then cp "target/${{ matrix.target }}/release/screenly.exe" mcpb-build/server/ else diff --git a/mcpb/README.md b/mcpb/README.md index 99a10bc4..7dc5d3e8 100644 --- a/mcpb/README.md +++ b/mcpb/README.md @@ -118,7 +118,7 @@ npm install -g @anthropic-ai/mcpb cargo build --release mkdir -p build/server -cp mcpb/manifest.json mcpb/README.md build/ +cp mcpb/manifest.json mcpb/README.md mcpb/icon.png build/ cp target/release/screenly build/server/ mcpb pack build screenly.mcpb ``` diff --git a/mcpb/icon.png b/mcpb/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..eb6e040fdf32bfae7658d9a5d32370572d3fa734 GIT binary patch literal 44460 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

nJ76^DaufZzcCmd4}metPH54hMOq^7!YH)6}a z2-PFt#9kr2PV#A}`XbwBL+5pPc(c>!F0`ZFwXt#c{$0}=Wj@TM^)`Z_Vv5>1Abp3R zt!y+iDMBXTjMd8fx9;vJI&^nrZ1m?KAIN23kK)ii<=J_j+?-TP?{r_{S|Te7)jiR2 z96cEH?+8JN(DKR7L_#~bturou-q+Ew@EN?ERv7LN_Zs0{{`2Fk6j>`GkO&@_w2W|34a zskE$`$B?w~5JwZ3>aix6-sc@v`0iImXb4U<#vhYOTXsW->&{DNF%$CbH?%|E@w;AJ z;`4mukRReJnl%)W&onl-+ylRhzv?;LpOL%5cb9kgz}5u#ha@dNk3EMH@V|zS(7$c0 z_fvKj69d+sO^+%+Hihyk3j7dpMY(hvJ1DBXmkPFd)h@CV#RA}T- ziwpOL-=I`N-|Hh+7#WMa&qXYwG-bJM#mHdDu&h;x3MgA`x|-`7J+II1_Y`) zUG{Y3ptXBXRk&9}ZE?L``{r7r;XD6y{2MR3^bU4DxJv7J8ZPK*tM#rP&P!329pzd6 zB7IldF#-)sTdL>h=bVjZmmMQwTVd>6kZo`;$4GsC0px+KeK@ns0SE_S({Gm|rth=1 z;a40;csL1wY49QwPfmmZWD17A`?7OWqwZ0v;5#aceND-uzmaG)Nd);5z$7LAy82Nm z3ak#(jB%$i{(#v#;2|gL_TO^NN$o-qIG;~(J2_u(GP*)*IrrT9yDiOI+Rm=M)g2ah z>UsAz(sMW4qpr^9=kZUtT=z>=%+(dkFI$~k;cWMMoH^ULrSl3YaYYOBG_fX+CuJb* zHiFUw7~Le7Nz6-par~hlRB6bifvWnROe7LjO+m=XeIhEVItwff)=RPSJ0s=S#bk8T zwrY%H)GXy-04Z#H;*0ADn8XzO-$5j~Q^yQ;rI)S&KWv-@A4uMgju8kr-}{tN^fawB z`sH8ArY5-TE<5%-_CD`^&E-@!-|w;m6dI`R64DfmZfbjlCvw^@59PGqkhh?)#rNEQ zTmZ35tLtFU4h;imUu^YUVnBdrZ_Rbney%59(e0@Bzpoozlv^PPsav>@SN`S*3an(4 z$&|uw;{Xjq%ngPFPnRqkw5Q`>q1gLl|2#O9W}`TVBFN@)RpK3P;ewr;kg2HrP$m?m z4FPQ$RzFG&%$Ngj?}HMKDpdjA6q;$Wd{E=)i?r^jhDgz zf?AehYv_bE_Mp8GbUbas`YptCOG6%3XyPm>8yAbzDWpo4yeI`iCRvooJV23V-<)g> zI3+i|@s6+FD(1J6zhhaG5ba9KmeGL@Cr1+zI#m&(Sgjeuobfr-+hP zB)^p}tgP-SF>;={%CKF(jt;2CX?TkP(u&X7~< zLF&P%aj=La0TwWdR1RvcS2{jB=Vf7fJBiy_j*jeX@2Vo`6{4u=6SH;l2j9N7{Rcaq zNWy!~<{l^@S}fd#CYk3=r}#^hc^5sZl`;udG5eKXOXU>qI$@>h%D)q}S88h2n3owc zD?H-{);RoM)zrayiuKpblZlHdatECgf=+7t;o$xThS|l$s(f0RD%Z8l;Zw6(%$?ts z#LVpoxNiBm@9{x<{FgsyYi-UI+ zW+wkA=n^t(*9#H5f~_p2GYw2C;@CU{i$1B4JepQ@mT1npq8+E)Aj=R-zvmdGD{D{? zH^_p+>TesRa-wTr(f zzEaiUYvA=6@s|xj9R0S^N0snK_0Dqz90`?Vk!T4F*|!9aJqkqWC|Zouuo2s&5W6hv z@0Oj_*3w#opZU?W&Ys=nakt=h9v+B)>Q4AxkN)771`E&%)|V=4lxR+B6-0&deKhzttp&%a<+7uDNcz5< zAL=OGSer`H6c`CAn>9$GOK3*g49g}8Ew0sYq58t6Py!6zc0Nw#PuAb&%LYn5fM=d3 zbyuADASd28&IkTp_jtVV25qq0cYsGJdR0QRhS)aAr7F`D*Ec1yz=PTRU@^autS2Up zWBHzdV;SaH^rNn;@DK5`yNn98eLCb8+fu1=^~eav^o$pWtWW})2}Tpb=^e!1-3JSx z_~mw-qDP{dfw&tYR@`5!xb41+C6Rh)9&KBui~dGHs(@2^_B9q(MZ# zfB1~}eralT#@mkd?m6Rq7m6O;hbVn`5*>6XQ%FdrDbsOk@Li z+)fI+!)=7$ft2u9SvAheX1C4AUxY2+)6Z4a^FfTjFq3|MIw%_}QA~(VeI)CkPGmA< z=P))hwG$ePztsIo%d@0o?uw8m3}A9AxoU!_XpWMQi)L$M$dVbW$`WP7P`|9o^E6qtrN>`Cf2AT0!4jbY|?S0LKMlq}2FDsor zZ$D38XYuoKy$HJl2fGt`GxH!V;nZp@0Qp&E2rTS~rvIXWub4%eLDWXSk^WVDE?!hKu{~rETx>Eoze9*OcbS?W6=mTGKPB6V zRf7^9(R%&)+wr?A_glfjv@Qc9A`LsDj`h%Oc|P_^ zRyAE(!l>D|>Y^=pr|-5|p0T^^X(%!3EU1FetE12n?ekXbfwn|(G`_+fdL1ohT>l*e zT34kM9Be%BW>h31jqOnF_3iwxgyjw3z^868JwD!tT3(*;`nM%jt1&hvR>T1!9YC5T zL8gdA>OWd7lH?U7ksHX-ER4!>ltbX|8t6B9NV zvw@Ux{l%WH%hv~l9ux1iM+8@bK{Y4pBv+_4w0L%{6iUl1 zV2=9`|D7x<18BJ~JM_NlD2kbP^cN3lcsI(|`F;BWw4PClhk$4C; zMVNu<@+ReYHPzUT?&fw5Mz7DbvKqbA=ZkQdoD4lKx91Eh_ZQb(tIvxa_Q?AlNP6ef zORm`5Egaq7M4qeAqdE~E@xoT)+*UNn;1JKa5)`^8iT9-4_ zD{Rg0a0P2ytxjiCt5=k!9fzP8lWXa*v~JN^VM82YXmZ;txx%p8X0UXwZT*I%~Tlp?fRx38t+u@k)%o`ZPp#5sqK%wpi+j#D%Jfr6&1HA z4G4UmFT(PfuTPV^;r;6-`&W>vdWV}q1^liPPcyq&wi*T6(qx_)O^)LD+4>avCF2lT zI&>Hwqe&bTg8awmy0rVmYFl4JL1Am}cD(_bobB}lQLq<1$TDYE zL~_{5FuhvP8h7NOD5~dCCDS=>^HH8QxC?G zEuf=leN`S~t0Po2hvd{EtBBtkI^w`xCQH3imFu23Q0On+NIi?Kwp-dobe>%(jvapg zz&4lX;&mlZco=L`}*gbZ~=d~+6(Lht`eH*<|z}iHL#^yIwzGHfNE{L zWAN!+!j!e%QMs+`1R0hQSzS-UAXDO!r>cy3Ek>s6o5*#=%sS2U@ocTEFmkoO%X+!? zH1s@@b&{bSxg}|c>9dMHw1qo{z zyPvb}ZS0tk287{wM;2^DL6Tve4pEgMaqM(FA;*+Vt7}UxRL%Dt4*7=jvNZ!zxX#vY zGDc{C; zpN{r`+|^Xm7B*{NV0FB?oq2plac{0D;%(a>)h6&*N$52H^SK(XB?yV+M4QLeu0>^t zXR6d1Sy0wJk-Y&eQm^TcsozX3#6JZ8E~f>@EHtYMcTY8TensH9EcRPjb}Fr!&+@C8 z1E>KEY#5CZjWPQKe*KNV>Z2$-{sp)H`Z*Yz>L5s@oGHT?L__uqy%Tx<_o6&$qk; zzwSxBv-W&?*Cx0}o=Z}QkNfF9wO#Hmo2TKXXcfHa%9-C+;k)?}=8Y8FLWsAfnkW%; zfe9wYvuAzkne?&IjVAi5{e-#e(ijVa z$xV%`98FcQ#acQTE`U?|iCo`s4a};hx`1ifxQCDr)P*Yfs~yJxFTa#&jYrAV;P@_% znc(AM?i1$qW1?aqU&nS{_k4r#seXC=iNKS~wNOvnz=x~TSJmosG4ZUKq%d|U%mkF2 zoDn?jP#FYvo_S+1`{JE=O}EZ!MC&@z z&ub(%7g&1G@Z%7QKAAC7T7VLYNP4?64~ZU{<@!7}is;NJAMHV)jMH&Nt#+D4v}o2P zPbgU_WXf0}-h^8t0QsSpg1bP8O^O?zxSjy^y$k+lBE_uj!KG_f+Q+}T+Z;a6j|BIG z23TF&dDUa=i!Phx?Nj*Z?xq?mH)_9f3>(D;6Wm`CaSN=*WqpxK`lNZdb&hvZvay1v z5m-T@F2JM<%^&Fo$O*C<+fzo-wn|8h@nY9sE+>XnW>%P$g}3$?+2iE?^z*%|#o6Vu zwAqz348xhD7zqNBX)_p@F_Vbx7K(Vhh4;LhL0u$?Bo>xDBDX+@C(>}#!Zo3GdAFXl zFfMs5kwG7^-{jem4@&|VKzoUjRA$bqsVuAM@k6({02Ub~Nnt$sYPlj^izk4bTVCGR z#PK7pEo3i(I$^rCQ@N?h=yEjU5#e)oIw0lV<#I0rvv3qO$y324#iF;8a`ek(?Y$av zi88AA{ylKd-#*mf{itmzh4d^ZM&Q1ejk~V^lJQaJ1rEPx-u(mkMsh&VoeWD|h0|ek zUJCpjzu(9M``sO`^&Uc#9ak8DZxLup-osd%QW~0e9CU_zW3pWTDWVeOdPz#9wQ(vx z3Fq9dT;d0qlr=NzBvs{K%sw3tLz(8SQ zEq@Zn#8ybu|2oIMsxW0F1}YGIhgE{TtIFp5`3+V+uA6tSVLwa#?dXRG-<)F)A}^z} z`RoS0qa6KU?TL&)3`*Lw|U7o)z=*qKgYA(Lp9YysY&VyBGb&Ymw(S`UJ&gc z2DZ>mI`FUy zo4!c1I+1y%m5Vzr+(OfeROsW$-#Qs3QB2^1zXgT^5t)JaNVLm&SH{JD%4_`ishtU? zew4$zwJQdL){JPXY+@2rxYL6j?t>|rA8Gbnca@XrwLiDucfVc6->1bBF`g|3AM;yo zl9$Efjvv44Dh=b}Ica09yQje>x2UMfyu3vg)FqVj&CI%C0wX!0^TinK^(KJGSQey4KLBfKqC_TxBGlmlKa<}w+g^Dp>{`=gea=zZ`(@FeAqVjQs^o5Dl(_~>nFpn?6zQn4oD|H8 zlq^n<{`&3(=yBFQ9>NvlRWdrYDNIU>_GLC9vha_x-Z2Lr&uwRO#Sl*oXAa4gsz2ZBDN-oW(d)wRNQ0{#*0chD^=YK zmF<`gSEAJ4Br zci;Gmu5sn)Qh}9u&C&j?ep*)kj|Rg5P?w+knIOuy6_bUjtb~fCw!DX~xkcB2pugK9dDp)lktu*p>eo zuCxT>L^Qc-TAIXhaXALLnw zq@>1XT@dn*r?>_`R?b&Q-;O#HA66(H>B9!*N9|Ovr>m9E_J+?P_QgLQdc>3eEY)} zaJ1uRmu$6O*R`y-e!jb}FZea2pxdOcR?4-eSQ0@Of_Q|*8!A{DCEI~?9xvGOyCI)x%8pb6=Qs!vt|E?VK60}tk zJg==bt*oh}!_9a*>k{|7++7Eerd~Kz`|vI~ac_t6B|+As;$!!^9BK-0Vv46V`=2GUueKN0MM>tAw` zq)g0`_HR6FC&(|E>)pKQ68Ovu;HLVyezNRc!kSb30gW-x8tI${CzyxNiu-c=_KW2v zIUd1#c8?`on@xzXNOr}UsTP<$j4;pzCAPIn&Q9!{?i z{?&sd9wTyp4qu_0!JDBK@z1Hv0JW{=Ti$_AzRTKhc=jW}py{NB^clrf^|UWspQ3Ac1L9Yw3)pR7kg*SfMJiqc|;KAqTQBp(3k@ zTY3r->LpLTy{yldebpC2T;$}m9lqVUVW`n5<$u6iiy3g6ySUhT*!W&*%k9__UKF{1ZIr=PPQ zde(LXbpHxvEpt|bHL5%~KS>9eKe%Bvah51=NkUQS$7Z~!>a~O*nhXl6*(a;gexp&e zp}izfP^%32HK^AoR4^2;Tti#RlcgPGt*q9!bMbcBpG^Kyi_Yu=pXh>_IJCMysblGK ze+yv#{33Frn|ULyaNUNjx5Lv9I}er!7`N>s5w?bjT@7tBH;V0C<8qCW&j?vV2O@RR z0?`D-F@H6R@O_YN+7rLzW9@O~D5;TnzqM^?yN+viP3kNU*9zue$z^(cWvros#8e!= z{~c$GL@>j(t4FEGii%(?k|r{uG3QAk-;q?XwiDSFlW1gtv6eA&l(1IPno=6dG%FO7 z0P0b1!yUsxMP!ncAZH$Gs_AK~<+5@u($x$W>h z*1B!%Fi(91&9wOS$i_eBEXwFDP9rt}Xf$UJe|PAR_Jy;(_?*pM(V|jGO^gYThuavx z(@)0XVEPRG-U9dV16dgfSgLixEx?S?#mdvbu$m=$qz6a?**ej}C@ag=! z$h+;OQhnOLf{vZO1*GkCBN8uz*bI13V6P(sZMVOCl`_rvbZpKowypg;|JD6Gxtw^9 zm6;cx|Lb^I6)U_CM4HDR9?CdMp-{oK`sZHic*1hVViajPdSN`G;?Z7uIrr(Vl&Azk zaqwRT{3m#&>Y46o8VL{xxN$*AO;D=x{eQW)OUC0&;u)vX)EF^x-Y*dd@7TVLS-Wt4 zm9&*I>rN*zIjL{+Yj&^pc89HP(Lu5;M<@a_qYz_NT*`rlccgkZeHEg&BodCvZS(zs z2RNHrvP)khb9u&JjVjB%>N8;DlCphJL&|zjQAP)y;Tk0|K_|T{c7gD&5x9*hNd+2c zA9paxNxh7b!a!cV0zSaSR;64yxZZo%UJ^49qkrj4``-dp`AV=fB~G|ptf6@kmk43? z99pS`hsn+0AO=47XE?DLe_+X()#}_f7!Mf>Aq$PTx+B&OR03M-plEFp)CLg0u}-dR^bn>oJJl3&D1ZMbkSV_ zl|eHlRKM$OlKDHjZ-;y(hUVM^k9V+;cQ)!dfoq$Y{j#u!!N<<{3fMe%%F6S@j!JGj zaN6R!_VatZ%UN%Kw_%TYdvf2Bwl9Fx<_hqYxUg4?qo8s>R_*irxEF8Rszz+S@~CEJ zaykEc&Mv$>DJ!i~R%_Ge{LTa2kN2+U2Qot6h1-0~J_VcYcy|1g;)?=6b z;j8O?*W|@Y1B7qZeh%3ESPx+wIE-yGO>NHRz2H>I@Re}m{P#Cfo=+5!uwCB9M^?Eq{lr z{~QA?5(xD~H%_=K&0mxclN_o%9X=P4b$lXlt?Vcpt=^H}5RqeQBdmq3)ysKZjq~#| z+`Tw1+B>y+8loF#>8tB*Wb3GZ8hpEX4i6a__R_mM@#&dcWGkFw2t791(=FnsabNHl z+nan2$@Tf0`+?&XqdUXQHsq_UiNb+}KwEPLQ4)G%=PQr@x~Ae+B#ckKzu!J_)~D;y2=bXB!12}5o9Uanok5>xNMdHmsGGiG zRg^l>2;Ak6q9Oj_f>oOd1MFz>lj*FQzKkvzj$z=ShxP}`ay9wn?*)KF_wUgA`uBd+ zy)a8*+K}M8IRD*Z`xYnLYgFA`SHsAq-6`ufw8e%7pbIU5{NW0Iz?hBAb z)$S*-&^&yl0iGY$E6C8m>)3`GKQwZ2$$C~;h^9)YTI=AuH8A4D9DjvKYg1#UC!UBm zmr7`-FXg1sjE&n;GOD7}IVdUjC2;=OSl`9!96w*bYmt-leLf12r>$Z8>(*iE=|&54 z`Xz0MlkvIuUI?xut#Ch`bT?mLJnTeU+LaH2g9gQ?zzzW<#SGkT;F7=o9`HZ^0vHgd zr5tI?{+<)a7C$UoTUu-Kx}0utafwVT6*QoQ=LW`B6{4A0kQr-NxU12APISZkH$Lsp z-2^GdRytANz}_%A#il5*3Ge03fFZiy660>)by*C(?;vFW%VewBf?_=0W=_22tf8Ti zHq1Bk1#He|{btiM-WXf4_0UALUiyyL*Bvn*uR9Iw>y78)j|je>>N7CytKgbicM(SI zszLVH*v33ATR!(TS+|JH9y}%xs84#iwuy3+c!n4 z4=d!W=`x2dLdEW+rv5!)8u^R2!z^Q;^jGt~*{8+^cp`%ol&VNA8*v0U=Us-rErZr? zeg|2xDyF2#_=}s>{Wxa7_bj6x<_7m&PoHk)wm0lddK%p4(1g!xufq>Vx6cW^?o$wM zPhEe#ES8vYuCD0XcEXl&8}8Opl1x((8b2DcxUlDTG6+HM z-seNZ_MqMeIUDpdj0kJJyNo)GZ3P$S#C{|*34Ol2OG~UJi1cjikQEM~7-*2J1B{TJ zBZDq7kGi{e5_1r~cvY4G&0_vUm`7bRtMxE^tLycOe?Y$UG^09XZd9ijLUjopzw60^ zhccJ|1@G*y8RHL+j6J!~+d3~iO)x0GB#Vik!XBLxT2P}m5B2wSJsd&UasWqD`V|13C2qM|j(FdIni7f2c zX*wfORGNg=@;<~Z8K#*b-hG*dMxZ3>kmgid6U&deJ&eot&nE&II$RAMEv7n&nsJb) z)xY~eC~d-NBv10`h&aCbcf}5N{P)-x{rk3 zil!7RT(g`94B4#Cyk0^ljXaOP#&SFqtxAWo=l7NfkM~PZ33-pJ!^hRl$)wZ0Ro8*5 zE`&$r(e>likUU<_^S^Uq{<@k^R!ER{ypA2$SO2`27+Jra>VnUSkPz|TGvh9^w`51a z_|JH@fwsD4I@af3wOB#)pU!S0blwgnt-z({2jGK^W0w6Us+>7+u@d$}8tT>pr__Ae z+SGHhjOWW8SYs3p0vO%o3CM`V04;Fz8g}boxmwh_S-B7xsR2Ql}kBV zp1MXR4Tzp4t&2IAgqpji7}q8noa1(f%EyPeY2ihw*Rz{87?@CkoR^!G=g_4khpwNm zX|1;HzFDqcUw&cR>0d8yeQXA8;Bk8pS94hQA(1kmOzrci7<=ZOa3qvQu` z)gm)WGZyIYh*@GPNhZ>QAv0&0_>-DfM&3j^ziC!~Su>behT$Oo1^%`6Q#PJDGNt&zEk~q>?nCk^)|0tu^l7-qVEKl_`$0&Fm~Ho+ zE?ft-JtnBSBe!CEk~t_wId6lOhnJOo0cI^41G@yM<<*`~RDn^%ag0-kN)0m_h5gNn z<1N-63{irGq=OoULoNsiCrHIprOTi`7D$x)tC2#q!oW#FOkv*j_f~gjN9XT*_r5#U zhvyerysl?Z``sTqld9<-h5WSMREMjDW`p-o<5C`>8si!($9By#t#XhAi*oZA4ij9Z zKE;R@ddZGl8k=soAcc$%YZB9XE!uUUz&9;Ar(U;XF)a;5WRqROzNNGbfRs^JyrtnF z5tIL`bi~{B_&z&3y+yP_?8kDY{O2N|{ASgmWKQJ*^SrGTobP;y*weSXQ2-PQ`Ux=G z2x^TC@lsk`FkkeMgm%~KV4sJTbBFI<74gxFr5LH1bY@@ zV??9O{3~Zu`gAm3)esLA?~}-E}ulOsCZ5 zkVD+!Qp8RbT?|aoLqjcXtgk+}@p;&zN&KOS(v7cowFjxX4))@Ih7)t!!hSb}+gR*3 zE6g)0C)lSgiP%&?a>|iFpD-i1EaGhJNkTQPRUHJwjy=s?C8DiTK4a^-bd1D5SVoOkiM2apf7!-zxU26>%7nal8EX!@TcOH~Ykh<=GGvW1k-`$uCC$|M=$24no`13Z|0bKoeWb*03 zDZ)kRl>wpL;x0I7)6w#@JX+clFIHENw3mO*ld?)Vm>58dN|qPfxh(PW^3u*aVUL1; zQ6j3RvmTw_Jw1Vq^fZxR1yqYA*_;u(pAF?#+NqzBuWKGjYpQ>qUDLl9%6h7hm4SG2 zI+-UlzoOX5l)a_CCLQRicgzaXFO*tP8Wxu<*~YMt_AIoxAC69$aqWlv&H%M19_ zBp-c?Ydt0|il^7#pVN5%{&a4xTAsOa!G$9VSq%ZS&Z2rLF3;UChcb)R-p)N`aSgHs zgF*^HSKHvIE|VgoVjONtD(T?#kCZH6j?c-r|DhpKG5!yi$7A0R`R5wt7R}uJkn2?@ zo6TD+uzxk%JzI6^6BySM%=Qz1n+-45TL*Ex3lIAZn`@}IA{P~>_rdovY!7(dFPSMT zGqrJv`Hgpra_jbJmOkF6@_oz=A@};>v zV7qFgT6n+C;hNJ9(O|qIt;-#K=1zLPf$J=-F*skfz{}8OYv^RMc#B}+xk~&bVF~nw zSpLG<%^6l7Fw*VGkQk;-4bT80I#;kD5(EZ{8)jhM9ks?pq}VZ8z?mRrK`IZpVIqLq z1V;~-7{8DRhVJ1Ca#QqaKFIWtbD#QJRz8lWV>Z063Fta_kk>voU|e@FP8XNmp_LP5 zHw5w^1w2RMt44yQRrL$+h39JPGj7|h@3~c0n~SxZzLV#Ss8DZg0C?k%8klB7p}7BB z87KM%amf7%0f{*+c|l5qp@jiLMT&SEHAy)Q@zBw&2=wfz4iI+ActH}%q(cO!QCma> z?@t4&7=Dm@&oI}ui^rkGccSyg$^Ja-JuUFcpvu6+FOvK*iXhci4301_1hz3?a|LmR za#J5=H@7<6T`wic2@s^;&Zy6mnvR)7a(n?{6IW*i+zLT5;~VPEXp>(IK#{5FwT!qn zWEmC-gWZoGr^$Ud*hBwX(}w@L8%PZI{XArs++p||ALU=2Ank_sp5rdhJeswc+_5*v z=jFPq_@wiECR)tPC!0Koh_}4@os1eEB|Q~xczhnst0x=^diAp(eVk`uO%t(>u>f)A z6uKmS7tA4vA?bF4e2|U&(+f7TM_eG4r~>yhj!2*mjYw@cjbn+qF`r5dZ3*Um>#hcQ zP1iJ6h=}+O;?bI0D%|2&B%T*_4_itaD>nEJvp-*Bbky8`w*sTLhp?t-xU~#ep>_b> z2);?t3RYFF!*s(m4=0}+GT)qJA1`+kt0D3p$(*Xu7C8fAT zfL`O3zqbSo3qN>kDl3>rUHAJNPgC23^k9y!!kO-dT;Oo;PSamCXX> zbnx{+a2l;RY77s6bpRl}z>j*(s0M1=y1G_P?)5Q*6!C&iXt9ZqS!&KShGs#=G*Uj1 zvZ2ihs$h^-D}QbgBd00d;`ArW9|nJek+YO8)JDq>D-TU`Mg$^>6}$wB^rsb@k~Rhd_-Omt`995 zM$wSZVv%4W<^t^T2uzdl=0Q+IP?ITViCHO#i|9A3D6Z&fGke(^6t#!<%3DNYF&>oi z*Aj6xiecb`kUxi@NpMOQHLu$)mS4XfZ@wIyV52MgYXZTyC%F|uVfoUVHkvTZ+qn0N ze+QD3-u;!-v{(~c%2mC5J^=Qt=HPxv?NiLoR3Ef484OP}f1l3?{GP_MUcUU|w{YoI zy$VDf6A}!UbXLfbt`bg=wqH@26eB5LppY|01{azwoGJ;3Fg#42${e6;$$lLRed%LS z$&2CP7ag%H7SU2m}?1198;YP{@-Es+AbR zOEEkJDBLn+X4*T_HnR=$-ICSClFB*gXkNRWcv-FYbFex2iU%LCaKSu1JG$2JJBswM zXfg3Eiqy>w*5CsR`+eeT$5BP>T}=o>huUH!w=rO#Ql2v`xcQi&|Kb%lfN^MVR~siGkTr2Czy zW8`4c?*F3N)5ltOrGdeK!eDc7g&`hdd&)E}_p4h0{dlVBivJOU2N)4JF*0=5pUFv5fh5YHRt zgQKDdLBmQ*I{j-@;&7$;^!4bT{S@o}QQbpDnpGg&(>lqGpvfP}n!{TTy7JqW&|lBe zZ7bW52SQ<{x>QV>pFrLR01)?Ir8KZT>{cvt#C!$q7%yYhzV`eME@=ueY&lftO{5(J zIVBaMo$<}Qpvl+S`s_*V#IVf;;NK~5KOhWVhk{!Xy)<~(+C-?4@r9Uz8r!n-my~Gd z&DTpoZ+rdEEO$DO{p)-5Nm}j%WnzbpAFeKOBz2L+$s(gDSHvX(3=Wilj%SR3X8Qt z0ss#pz*rz6Ac%OPKA0PQzm;7CON^dVJF3=g+c$3USo5>9!u4J(^o4V{^c?i!&7J|&>L^4*d{^% z1rzd7U-b__9W%g_4@F-Am06NFG<}j*Qssct)4Au)HJ6JM-&aj)^(NoTh~nMTP7}^J zf5B4Zf14#&lp5iXbr4Dx1~q99X2M+84XU}H{0j4TBs_$PWtKKCy56?JrIpa%ZwebWOP(8~ZXi%f2e0o~P7HbDO~e3- zk?NSFuqqu_-(pZIP$YFG%|4|)u-0bTb(p%^9}ef6%R8o;uyMGKb{LUdjOIv6vw3#b z!Tj?0gxDM%Ei^-Cq&%rc8>kT!c7l4<)*&vNfvv|X|BFiP=m}3N=Y#jz)pLpV&R;Jr zxl`Y>CfjFrgPSd52MO_i?Gw8D7h+t$4eh&1SUQF#SffZxdgDkmqySE_;utrc zjjt8lY~k-a_5VDDck;Z#e!HnUu%2dVa(a85 zj)8L6C!YbnF2fz#kNDSX!w^xVJbA(}=fLbM?U!sH?Nyq(gfg3keR+j%fuZ+K>!Nza zwMO#REl3*86FW zk2=RFLJ7yrFGcQEU@5bKwKRcwXFeuc12PEKSs$1?G#V1R^xuU0Qqi0+aL@YJr?2m& z&zp3(_5iMSm5j zq0?-wKLuUjvcb4qew?=OY3QUU3z5K{_&$@_`om%y6Oa?a8&jKj^7Eq7SZGz<2ZE=J z4&TZ8f!vT(w@k!Aw(F>m|paAQqYp-C6fImj3S1SJNJs5+JQ9Ph{mL zc|FYhdL?)MvT@ZBTO>9U`+}N#g;C0azW!6`Qb-q-zn3_#rQgKkD;LL&ifV6i zMLTPpp9N*qNYE@)%*|Zq_`xysnCV`2SmRlsjGi zxqWiAZ8XO!CX!2WP`0h`@V!yWh4=ynW2Ajf!zH(Ci4@!>Sihr}Vjwjo^oJTYa<=Mf*=C+@I>pWeb6hQ-1ng~hSo_U+pYDi%NI@v!!!Fc^qqj0FQ#)Ab_>LJ~6+3 zs5GD8z zT4SKDXSB3t9EcZE$Q!QG)0n+t&>k>e1v>)N&9q**Rj1~IhbOUz2s*0 zF)(8g>>zC`D4nl18Ub0lP03$b4 zgg^|~<7;YvcaL;sSf>$uUxnoHiNu^%7Xprhql?T_fmqgfOKPMjIHUGsz9>_>X$St> zAq2wC8s_H!McA!QkHSC=+oo3Y#oM9Du-~4u=)~^60&uG`uk?@KXNcouL=JKR7D;kx zvBqVU&7nd_Ft>Hq{`2s1z9DI`)5--m9~|I@f!26_qjBz<(Sf-AD@)}F;g`mT9T*TT ziS-w|&2s(xZM*iHlITsH93^z*`lJ+Oee5ml;#ATfZTWw+ZH1Ol)-!slSn=kuiWy-P z5q9MGcuD)cHOMi0^!$hvvYdx}ypx^LOv(VHGDWfQzdXB;xD^*}=mMkGEG#1C|6o#+ zoVZDNTb`FkUx^^?v`WDlX`$~YW|mvV5a8Wc-jY`2s8v%ywKDevJ3ZrKMrL{&Dx4Lm z8v2_F$QiJ@woJ?*9-yQ^_jb~fAFQqvQmZCet4@{vOrJw2O70p&g|oZha^C$t4rafQH#VZ112E{3SGCcBKPB9Wpf3SAhc@xs(9 z5BoIZn`P_-Y@Aagvfl&G4+aeJH^9Z(Bl`#Pa-vMCtudx%%>9F8n*;7Xta zI)kyNWL`*IOOJs(P2UtmSLSTT&Hy^B(F>iyqAs1`3X5|+B9+Dv_>3e@oz)|_FuXkl zN1O&|JF*>m${c<^y4pLwiy0qvPsttQndu~+Yw@v60)S^0^e~kykKE-sWbt|BS<$Em(@wkE`4k{zr6JJ(txaRNaCaY6t5i`l4N0{D4C~F5K2zg4KI!zhqjk)G`GtyL9LMu*= zs%vEbcC3Y|z8ki(5*OSjHtf61_wS)V;Km+Fy%*0>#(qQ^Lp9^_PKZ-a{-%HXKF+u@ z^)KctR?kBKJa955o1({R$4?lSDx^2c>4tvYSVpNB4X4{yGW2$I!oUayE%n2;xA)9w z1{VBIiui}-wFB^r%^l*^@E-%wDB77(Hv#0zg0dx`wph`w#{ixW_QcTr(P1Aiq$A%@ zkH@6~g_O+O< zfD;gKKAILLnS9S?7>}DZ)(Zh}H+IAZA$lXhBj$qJUWSM_Vdq0&I^tv0|=q$dOLCOre25mfpy{ujol?$J=&tx0n>Q zk6Ooha{4{bNrwm{Sn<9@R|j3C4T^1eICwr-#`@Bh281h$o@leyWtudWSiK1@nAhr) z5m}L%G9E!{Yn~TN5)fH*VwqH*?{w9yr=vHpPu>tN%vAV`y%g{smb@evMn%Z_E3 zhBKh)n`W8OF+8@Dxy2|57DbiOoZ+hFP(Pq6xVYR_$2RtbQ?M20V~IffZ?(fW+kp{z zMENhkq@(xj19RMad3(jG4{3|g>`I^%gS@FYr!_~qGAg!y*N9lZg%3bD+FSetJ6p+5 z8!_8$#=4r9%?oKwmoh|VjJ!D4`AiP|0Hf-OVoBydd^;xon4oaFZ9-Ij|vXii|9xkkq z*qd?u8Vs5xsSpTIkvtq9nhk`gc5sxjG>)Ck=Vuw+1c~XxBO)-Y{4#g37&}Ze#`#K? zX-Oug8lQ81(ppt#9HA{a$kPa&r4WETXJ$!aO8hskxCs=;tqH|wu&KTe52)M_B%xwe zv%vQppUjN(0$0U-w=xXbn&$e0K6yb)1GQBl6b!v%osk-7W0<2PG-F43u$`!5X5!qn z50~Fj^}zdEvp%{mS=WLd4p5*D8Y#_+)xo6bzEFyY%Lwv7ti!Z7CtMRjKrGxd7$69z z&N6S7fK^^q6`~d8-=Lbdb>hOHof95{pCXG}J-q>nzs{HgPQR!;3KfI2aE3IPkR5|* zl^;=y4EYQis)$J{zR&Kk+f+)XAAZaKL})bWCF;Ta0M~TW>!dBGz<-!#nJ72|!#vXI zQBfttVyGFPX;U_I`SHB#30Q>#J{9T}V3Vf_#RGw$`t4x3!q_L9(%%#>6Fspp39w z19)n|l)@b5Me>&k^$F_XB`rdwM|i(Zne(b3;DTSU<>%T_i}*}8nk^;5XE=<4ZiT59B$L;{5|;SQ>ph^hyDs!N>C_Jnx%9wjfY!>9ns=x&kZhc_nVhoDsgcsd)ruvS6Hnpg`Q$ z+;rkabEs4tjO254`%HYE3e^1YZk06h_i!(J)Vn31Q*H0$~bsutjb{OA_hQ9Ut0&hux!?-@B{R8J<$ETwp;JgJCwhX7J5 zM3>TPiqLt7NPw$w>x7Ni5BpnN=mcw9QH#NN57*x?d-}>PgL*WjQyKL{*)^w}QO&9{ zHP_J$%Y5xDzX|y@xtxNlE90p~+}T{s=p*4t<2+wp6VZuf){wU_*N}eF>A*KFT2;G& z8mq1W+~doQnRX4W9_!cJd&7`KDvyRCJe+RQm=Y+R4M zQv)SJQPREVd^~^S{})$9($tHSQ`VK1ybNKUNxDsc*h!08A;CNq5&@H4HBxI}q@@>> zs*!G&F)k8UQQ9m1BW9mApEs4tqOh}|kVnpX#lrD)%_ zKpOWIs8fm4l3_8m_cRxUJ1sqcslp<6zf)8sqk=j4QSOwCG+%mVMlCP)$G&h9#qPX9 zlraV+3PMaWp`hcxIF=L|jQK-c2#h*olq8Ly>gV3m2o5r2C*w%SiRzj4w)73WosU9r3hLc;@3bky`_po0^k#@X z;d`gta%QEE4UvtNO(skBKF1z-!*8zY9Eg$Uap|NIy;%$YQ1O9iq1DE~l%Wh2%>C6% z^sE~36JCGxD6e?g(v@ROv3Ciwn+5|W?ypD#%nR@H)W%Ug-Fp7qG?!Mh1`?87cA;xA z1Ngtbd+&6^phyF5c>=mU6|pkv86h0mIpDR`r+bwqWes-09l6M|^!4+tNFa4i0XNJd zo+=Z}6*|kL;}&SpR}+O%L$tDx{GFc+*%JhVyugsbdXDr6w2E2kW3*W9+2L ztZV!vF`#FV0r3%vV_8bdS?>P_oKLh(Ki|{y4nqyl%pkw z4m;FJjx+M^DKAHyB+xG27O6Ih3L3|AtP(8|in7v>#OebGVTzF{Dg26~y6cXbOmCy? zI57FgdS>euTFxi@8Emsj6{`z-@o2g!D-e@sUrzPGK#BncSDm1e0y!+^haXi0K*(+byi0X zmyC{%a<&$Obvwf5K{%U;hzN;%X1vO0I<-*tZknFYp5jT(20bY|Jb|G%*5~3H=cScW z`UCnFk)=0JLjXZ*s*)Mt%1Hmlx}z)9H1dihD~{COtulUZNl`3K#pvR^P$-F|3+vga z?h62&2mm1WF;0v8Tcc!g@SAil>6r0}dl?WFh`zA?*#C`qBaKQ)Zf?4et|fxF?Ao^r z=$damAVS4~pJW-Va9M1z#L$WW-Fz9$XeJ<#G`s6{7p#OLDc^EWB7;vD<>lUIs*B z9CplnBZKc(0OkVgmt`dK6@4t;yHz0;dKv=Y5_Df0y$l>AqR&#rSkpPs&aO@<)o(xZ z$PeT(zkwRmm#t-&31dr8Ces0%0W=v%|Joeumhw58K0~6!86GFvR{BzreqSVcgw7XY z3LuC0WEWDjp?n(l^uYv3!%|A};Vr%racRo*Y=!fyZu18g+dql*5iRrW5Vk<3n=N)C zDrJU4(4rG#HnznmS2hB)q7outLqL?%Ro2Z27b%bdptW6MI6+YYN6TPeGkUz{Y4_1c z%pzBZB1m<{Z)j;Lkgm`X-3{9KQiH3or{Hu6z)~(qnt&{mn7A|XoeU59RVCu5{=keq zsr6)Y*VrjT2Zmf)X^3|&bwu5eIqD{ZG*!Qi^eJ2b#Q(%JEO&%19q>lI5h(XpSt~^v z`-{p!Tzw!x1=XHQ?*pLh8Vu7H>i+FkdA9?w_^pP90t`CVGA)Wrkpot*OKby(sJ8aP z&$tRV7(o8poX+Ap4+{WJkyLl7Vp>g#XLP`Em${ekjqGrdR>7S2M1Yk;i}F#+QUZh> zkolnA6(o@gK0CuTUxjL?fxmfe2ao*GNLS}sHSno1gc2guC!b;Y?grdfU_zff7=HFQ zE+wEK2KWP+qAoI)FI-A~=;;b|x^LJfe+c%2l+kAf5swjYcr%kOn;+T;injWY z5&Eh3t~jlcER{zZ1|ls)!12NG)MQYMPRhWdewwTNY|`1dYkwkDCKu~1(0NjSjo-w= z2b{BFjCdS4-6AbaM06#E0P?b(Suxt9NPLVKvS5f#jHaz1VuIyY0PqZvl9Y1U^^p=n zi0B@bqDybFZvnu%J--7%cd~>3aeBLrnM?-gwUjelX?E8~C^v-`BD3Wr;)kNaxQJ>ooLAFn)-VQ&G1}e+*W7+LE)d}0# z>+*_Z^Wt0+ODzky=x|9~o=vv2yz#|Qzf?*G1C0fJkooH-#4=qe7kX_kkKg+E$`(eu z5t|(i)?Sn-zdf8TslC3ixq()IRK2!M0B>8^#dXg#8KZQ8ry1FC$4|1SleBW3rOpY{ zEv;L2eF3!YTPi+QvD@&iBb`Fr5DR3L&U?;f0(ej!Xt9+PCro9OLp7X5@*3@1%n-Lh zrt1~=Fx6@}TTHIq{aZY*apYu4-GMJV^oxseQQ^tOeN*_oe)lLb;mE~rU@yEsy4g^% z$x;J@Y-fDCn%@IUt}ecI>y4)+gE6y(aF?)@{Cc~+TDz(iXx;ujNBA9zCq)I;{I-18 zx?WpUAKt9;-0Fekf*}uTtAGc^wnT6jxk7E6^*f=OoLRv(@&bfT5Tq+1NbfrYULe~} zLUJrs#Cb-f2_;E+VML6+Hq3}XpQa$bYRsFNTCVO_bcCh}Sp)6Uqwv1suDy6v<&?p5 zSNs_bV61Zfq0GgbjQ>7T>icO)8?py0wQy=oMZVvcKK~m00>{2PcP5+H;ofENRx}63 zn&ejq&`kXZyuL{we1_$=-Mc96+yD+*aX!%u4|~9d#v$5#u*h6)NsQRDh>aZo)N|m7 z|9&khi|d^T0d963?SpUDK2m@5Y<(yp1}0L#;;OAz1tcQmnz`dIGv>&ue@y9QGJiIt z)qe4Wi&t9=KuL#wgF{h9(x4wF=jW(w)W?q4r*3Il`C{2W*|fI88vMB`^R3cAn2-GB zee+zR2v{ik5qzFFMFmy%J_3qTAM@RWy5CZUVI4Pzk^J0oDcSAza~V?@a>xag&eluT zyHcTU@lmj0s<1}b$E%5vXz)e@lhmV*uY@qxWfM zk4-fCYY0MwEH|H28@DzVNz^fHkcf8y8FX!!RaJq{FF;7VXwCt?d}4c}dJXHDXwy6( z?^gm_x1q?UM-P4ZHA){8*4F|c-wG)CJ_9BSomH;+Yf=B(zTTNjs%zGHFU{w_JA;S6 zeX_w;wZ6;qw42$Jqqk32m!U3d$&FfW&!N>8K5&>w_wsE!Vn{VFU%YKK8LdG`s61nq z-{NYRNCIcImK1$FB?SiNNINl13L>HA7R}pq=pcvWbX}!y`ArWVqbYO@z#yf$((G$O zv|d<mz%jxGxg7CcEh^G;+{@QL{ zRbI4r@gxD!1T3bq1NVf1zirdRRO+G6qm8>kLj$#gpd_W1A(M0eYE_z$LYq3rb~_F2 z%U)w^$0Yd&z1N~uqlXDi=1nY!0i^5f_AFvf0SIb$0Iwm3x*8DRIF0*`jBkv%R8|@h zE-b=zdVMIA!;2R{_kuEhLk9&9 zGu}l_mrQN*`zOop6i87ONYL=99DASaXMfdSb>U&T=PFPk`R|cix{koV3<15{{0l%B zHPRtO5$_>!EI_6XN9#*;)vu?{SFNcH=+MOkZFeKKRlPDQeKp6Q*`Ba2rcu00@3*@ONxR`U2?)RUln2(aFPfIbkU?s}Nmkl0} z34P~rWSP+ym=mSPOs19&Xs)V`+Ow>M4S4F)=1$>u^Y!IBJ4k?N0occ4P<*v>K!(qf zAtQW){Eqc~&v|rlU=7lTcv&+&joYR${(724x2=pRoAVDxqBJFJ>JmE%OPX5!UKjY6}3w$5Bae`;`wlfc-`>nUn=ao-B%n{sQ$_p z*z*Bc3pp^@a{eTUe_6GO4fR}`6FRre_%cIZnw&k~^PxN01MS)zn%jV3-BKhtdveUu zp+nZhJIelv-* z>GAzN=u(!(Qd>NPBUBl;JPj|#7SgynW2a}JOpvnNx>DsDz8nB{Dou8OTB*XgEH>;Q zLsB~W2Su{;ppkW3lIT=#-$cDa>|$mo`g8l^&AlF(Kh_mvWm z&L-y37D4Gy?jVNNH>A2M@w?SBDpaBItg5d^{FM8H>Mf=WiRLXe`d^Z`GxTxD@{vJyzNy-v92Vc)B^7`5O|?L<%6}+A%i{eaq2=)p}tlB zI^=TM%D2==yp)H6+791GTPNSRc6qAwR)<%uFh%`k7F5ST{xQhI)!Y3pk%s*}9z{tW z{0wOhM_%?TVI~Ghu2R2cFMTvsA+xc97d*sGfxRPm zJU;-Dpo|+`3)wOYjVcVBS>RGXqRyg;1p_(XP$C6>omm=zQigeBAQEd|)ACcnt=~Dw z=a&P%;bVE97#gcqF}4=H_Hc|)&C)m+#G<)2_~1bxMkDOe0h^QODg~C2#Y)#z#Y8j| z?{-}4BrBJ4*&M%4&o(+u5+Y$l##f#SWCAo)oPyA@;UU5FoNn;87!)cFXj~$t-FDlu z#S{M5)fR;mf}MZergp8cJ&r%{;A?Jt=*o^MaQi|Y1y4-m06fQ`0F;BHG2~=*7;8)p zUa2tyvOUpSYkEq*X05e(o5|Rha!P1u6NW+|(iQ+n@X~XnDXE&HUm69sv}y2^F8%_u zlmud*xmUF8n(G6J1 zAxLg!HRiudX47hmSlh~M1+VfJm7gYG9#~G=j>wX^z>5{yN#0?4ys`l`?g`6;`~l=Ccn>#% zHk-cosa{z?8jM{E1R&AwM80OgoA5P1`vZCTY%Bck&*Y}9=W9PcjSV;Sgod5c_EHu= zp?Y7q#KPK0Jy0JC`SU_Q`RV09DPm52Dho=DTxdRm*?w=oSZ`4+XT8hwDxLL%|jHq;204+6hR0XAZJ&a`0G^@!XQ$$9Ou;~3Ov ziSx+&4Dp?3dSoqmpO*yW4?+^)+|66c_fGU(Uuo?er@p_ruV%McP3A8-dJ!N>Mxng^ z!F_#oH{iqk_1Hsvltyz7aN|gR=xx}8u3BU>l<(zHS>%u2^GlwdM_8~?P$2gZtZh)U zNy~D*{6+{d6ZE2-QI(dSiWyrWOuGzw4K~ zDfv4Q2%8M>ykMs+^8!brHt4F5sf6H1*>-xZbLcO7+j^eGmIBX>v?ytdwoBkgwxl;- zicq!%vsff`TV-J6UG>r;&eTxG&+Tcyn^3Je6hKvrM1vOG1Ip~rZ#++T&dvZRg54p# z0NLw;Zv||TLL;Utyfj<7gYH_ zdftx4A|L8XSu3%_g_%t`{ZWnIdim%!n5nG6^b@$Jz>+ zf^t=egc}ApN$3j_63T>W6t6kJ@ZMe)=a8l2f%M-~HiC${L56HLkSYnQ_v>~(wb)kJ zq|cdB`(~a7UainZKy<9~({r1rZ9_eGu0goh~R{ zjIj)WUUmzdJ7&a?#sP&TXWr)AqxiA4>R@1RH6TWA^WZhIkCywRx&1sV7g-@PpmnT1rzq$FJ5W+Ybv{4?7# zm_I_L(8N=}FFD3#%`yzs*?*b-3qA_vZ3+$U6615zH^7px_&J^WfiA^(F&#bZ& z^{*nNUGEzJu;SFRQ;j0w7cdweU>~sHma>YZ1q6aijmp0yPm7nKCNijx3~%Mz0ZrqO z1X2#kVN=?%n*nVl&1%9y*5~yNVv_Y%+DpOlHTR1>-WbDIQ_6z_Ojx+??e!TGfu=;O zX<;sOVmc4o?O(O6cQPeN2s$`NkU{!6G;{8RR14+}8Yrqyu}(YKWA%`drC^Lpj1W6R zer9{U8(9qHut^=yFF7hk^fI}pfTIn=u4dN_u{QXV*8b4^4I))RDBtL-m)7hEgw!pv zk8~5izG@;3DJRSb#A~Z2^nTEF{+X)@K{2CjygXzhJg$2qJy6NXl)h79H!SLOq9NipliTzCv1Km?|0&Kt zK|jBg2gTWwFp@%zoz$K$CyF<)uiAS*iU|9k#jv=p8*5fT6D7Gbpj;EglKhrP0OSK* zEV%ObzLK?cJ2_WON?jHQQl!m4rZZ=^#xGQStPG#j|$ZAnZ^cO3-aYRt+Q>33k zb0(>)2BJ2MGFTOP> z=|>%NOMxmxR5^1$$kEkK>A6%@gMWdy0TXQlvyy@h?V@vgtJ%uu*2OqVZLwI~Z-4Eao7*!A~B5lSV4 z!n48`oZM*)^C8=xOjqmct3Q}8hlWP%?>5h7mgC7OXkS1OYXQVfCk*9pZSt&6B~aNj zrg=Pz8%a`D8F%Wgl`rYU(fBDIcD0O9N5M`mJEct`Ytp-~lZY{lQLo$AjwJ3_n)*Uw!oP2y z)M}}F<6V?&E2RP^(L%UbZ%$(SBEnTb>1|?c&Blkzz522lcWXV5U^_IEt{l5W|+Q?Xsj`zHK=S9@vb+{1l;d4Xj!Nk7Gpg z>A=Tctp|r(<`YT0;Gb<%rb5P1knXrN5GF}a24l`g&6Uob7410F^|19&41SAb$JYRC zdSRi8hnsB7CAL5DMJN5Q^vDp?fHd90u@9~SWT)v@=K0d!rfV~!G3|kiSvdzio@2Ia zKA|pwRnkw@|C$|4PV*I(=X?5}iQ#b5PIs$eT(Lw;1nr})KqmL@VKE6iW@55-S-1Ro zp5NjzcyK6uxhcX7)ql9w!J5FI&QLoO|DNH~C-L_=diaAG;P#svJ0&MrSk_sc<`us{ zD5Rgpi4J_ICA-pZ`yk13ZAat%Q=orjEt$hHch|3x_x;@%@df- zC%7=p&>Eep4#BH>hGlI1i_`FvS&wDL+2Z_EgyU>I*3_fREhmzi$U*?lm&7uSo|qN> zsgjAW97n6a+)>`@oF!-;FlVo%>y+&gZounyUx6S zd2!6Qn~!r$kT}h^hEKKR{(iHiHUKMy!1T*umg2hjCkFSp9lbA90l?(C$ZwAGYRBHx z_IR5*O4w&a>_!1C;!JUdu+-k~n!4YWl>KJny@QIs#_zABj0)EsRr4wPd(ZdndJF`h z+H@1WkUB!{HGc0MULN0EJ}*VaE<<2t)Xf9jEJ+de3PDaTS49aOokit$cX%V8Y12m) z&2ByI_dX%L?CRL_6~LMYVB{zii~uni4?|IW-qkwkrzSL91S6^=Fq-yHRBc@(ZeDaB zK`0Ted&NO3Qg&W|#Jo%s=F9X1G(dug$f*s${XPEN%>u@4A3XP1oV!%7+!AWeqYbjvrg>nL#dTSGmW zJ3tLq%A-@7rENx}&r4lWUSS9;Qw^nibv9EsW1ox=s}Fg^gWgT2eSzU-B#k&fQ0|Ki z`W)};?MW|Ti%>yDMUrTFCfIkqKVe_Nm`6zxHuE#+Bl~*1yh4A2T1eufL)_DT<>YA= z^U!rtZbAM&Gj(`b5z^?n`1<&l;-eaesnd7ICR~nV6WW`nv?)K*>g-^ZR{| zYh~aO;X-ftZX)r7B7bP?^c22gD8D8eSN=iwjhw-?GQB9%po;xbcwe=|oZaSwLGV`# z-n8{I%zM|b6b9}nL|-SASWib!*SMgMmR7m$*%KtvG|;u}&Dqknxx&-WbFKbH%aP4D zZo=j&1AH;kcx9R>=*a&-!omlgxP7&zT+E@x+ z%MB>ryrpB5&)WVsF(A5TV#b^%VN31QOiX_;3<6(wxXZMbzpPk^{`=nA^Pl9CRzg#? z6@DKdgF~sBSubSW$%L5RoDi2NZdp%Sa*QqPqYYPaKKHwngS~*Y~(Tu&K1j1jOWY9aryXjzQQ{+&*`8a9up7?|ssF`O^`uDCF z!`^=3<_RRNV@!+nZKT!~@o{lwn7n&3?1#pcVGdU$4io}1ZTY$0AS$I^R9m@aBzUY) z%>yOJn7`1KDvR)E%xqGv5=Cx*4irZ}8|L=btqFZ^Vc59SdBIe%jiFo*vp`a}O{KaH z)Tz2JjA3k{8owS2CWHoTo07t6sg~U$4*m#GTm2+-b!kvavcjz-&!v6W8c&?PDx!82 zV0XD(9VaZTy2~KQk8KJD`l&E@5h}i;Tf(~7A4K}>Yr?2Fwx#8=ifYA3)P!T+p<0Gu3^rN`Yt@HAkEl<~d}JorabR@#t4^!hwLAPlG$X zJ-Ytn+oS?=Xz^{&Y^Cg~>F&bKEeZs~cJFm)o^tD$Op5iI9?@rHdJv9f* zS8(lGKn+f+=hn$R1;@aA>DT2h zgKl-nQU9g&l1x{D72YYTiPl2oNh1Czy}Ws#dEirZvf#+yhG9fQm(@%NnQ?L$O_=C= zoahrOrP@kD%tzbr?U*E1-A!JZDHqYy=UOhUl^!)_8an`|rxOeueN#V#QTbcekgt-< zFnbyFYbpc*(+~G~n`mwJd2RtOF6;K#E`R+k>y;+8+4%SJ=s+ewUIt&R)ZWp-1lt9w zE9$*-j>k7)EP337NY3C;{0;*iBdFg18d9ZhiC4K%&8$`Qt zEowPL%d2zXH5@tiBtdYA!43WIK|9vatO$#2k(*?2PagHkZvQ8!>({;nZ)8O%H1cZ2 z=^<1a#w$!KGr~r)Azpd-Zz&3Xu+mz_Wd6?(dU*oHanPC9V^~$(n1WH>`k>7X)~Mu} z*1dt5;`w_zf;PfA?iSV1d;{vQbSwgPJSZ5F*Npm=tX&K($#&M=wmeAXjVKZVY}bS( zB!6j@ZPudXlgqpJCL4%2AS(!kl;TBe67S zMMi6__;%ji600R1MH$@GGj}vz1aIwdM=0+3fmkY}U-6xHxsx`NmSf^SxyZwAxI zEH9V!o;M;hbM9ZIMeg%Qdh1a04TO7-8)w}IZ_lb;$C`lpqdS^hjZq@&O_1Ak6uKrM zBW7jUoAAd2^lsM@YisK_$hjNb+T-NU7!pWMi8zg>;{s&uFyV@6cwoDL&;q{L*kI=7wN zcz;WEdt`Q*tO>*R?5B*_KGkGvGdvYk^pitUrW}Fn#k<4(CgVk!=^YRayAn{DDB^XI z@HRDv4fPjbb^f9?DGRA2Nz;&eHp{Po`?&Ey(%z=O(n`vDof;;LDAx^Hxd8c$8x6~u z7o>-IXx1Ro2~a!0xA*t=A<3=kRnsI~e@Bn81PniW=+xWf@&oYx?M#%*F1mKEDlRy8 z;C2g8WD5)djWgNRGrDEqae2)UCs&m?rD23wsR$zm=9myN8ieMt&SEoF>Ls=8kuC8J zkk6lY#=hS9FNiQKt$3O(-Oj7Z?N9szrX`pJpKM8<4*_hoyN#^9 zvLzHJjqp{zh@TUQed532WQ%7>{!$*1NES5u`=h|~42Dd1!L}wyDMH8D{`?%MO)q!W zx?6ANWIHV@!z>c`7X$}4SCcJkDD4$om@V6H`8mT6@;9d?vq%lgw{Vg7m`vEP{I+&B z9AdR6pjTPK?9S7duM8$X^o*|HEXm8jM}4v7iHOz~eZw9344=ZO%G=S(HZT3-X9s$pJA=$%wYZQK8n_YwA?hVCEHW0;uT_M7`)32mQNBEVSS!gC&j#yUPN;*FaM&8dv zPE|4ow(GfTnIUnqE9Kzl&T6ubld)O)*O$dn&J!zM;F8}G#ZrvhosJ%#j2@G|=X!V$ z9abVj0H6UPggLZ>nPd&uDlWMJRLR6!Z5A$7=hG=6@_7TR#%E0{CbZn@6g$mt5{o5o zM$&~#J^L&8E$d6}=F5bQ3{0?h8)~M0O!vcwb5{38zO+zR+|GN2HGX1S=|e`42Kr7GrfR{rSl`rDTf@nB9NNVj(_Yan95TKmjdli}(6k0CWZBI&3t zIEqS|NcAYKX^t{{M|iLDDn3+A^N5vKKtnt9AbwN|8j;2?g5+>^_%3(jItY~N1`d*en59}*G;dq`t zUTjWSaCp>x9%Sc#Tw)a>k1~w&%eEmCH3vd~onM1|iSHVM*Q1`sq*O%Ytgw3~r&%|7 z+h{JyFJ$QXvKo=|oAdFLM4`V@VtI5IIv`l%H{ChT^LHb+!;fpT2W^FgQrv1YqTR`L z1z9KR8f^I>8POm2)%w*3h@u*}l3 z-jPVGZvP0~jvs66-gN4y)Rssn=ZbypUFQV7NBCYEOEG#3qZc7#hsRK=L_@_kpHQda z*I>(qpSiyX(ukQWt?68hs3r(KTyNVLI+2N;Jd%FNq9w?M9ISUgr%8yYb#m z5X5x|UB)5R>LW$ToF^-OkATLckdc5dUK{@xq1*ju@D_N3qZT)2M%=i8xhB*!w3JO( zsGYw8TM#VrVjazxN>U)Yzx`6P8xEvW%Mfu3jiojDS8H{8Cx7$eM%#4Cu6KN@mu5nV za?EpQ;e^LEbrs6@bbr zHR$KBOnBQDcs7))HomSuBvzlK`yP7QGcZs8@O~Ue=4J!pN4kf-0Zp*>K{oTz@o0M+ zr(bv?COC;UV-aPIGH!t1>1L|w3`3%2$opnYmEEo)#y2+nG+f$iAFo`LvAQh^GNF&2FWV`pVIA%_D=!Na z|KR8fK)1LByB-yosMivDwSQYxOVw@E%2}=5_sVKz1oTo(hk;3ma2d9@)vU70Iq58# zJb6oB8r~=f-F1vr%}aN-Tb$nwk6?>TiuH$&egVkrqB`aygbYHm&KEyk1Pp|)Htquc z{?IZ0fMcaz54yzbE>j=of!~)W`o$_Y7gdz!kVCI;$}ZltF~n*+lFO7-h$$o(9vZw=GlQ^aWPkJ}%jStZOw23$ zVrH*aGdjoPAkzUyE-qllY3b==Yg4~f)zUdNU7M8Rp+Q|Yvb6e}{vViD7wl!|f<+$*Tg-TT+$U4o7$WOoH}FBSw#wjGlGbmsDuaRko(`BFyBS zWCcu>xr^47Zsoi#`x@w|KO2U#jQXV1Y29ZFpcHAII}J614;QD=grD}xOcbA#sdRCT zUcqgTlZv5@uYE54CIHFn> zJhnNNUoLx94f4Dy9BH>5jw2L8-r#}wf zhF%J4ZEoXI%8+%-$nA9vWe3l-WWdR)ft!JzBN!*m>~(1~V4Ncy8^8)aXdgvMZ#>2K z_uI|a4{rF^q{(|6rnPThxpgJ5W^rqhIhh^mt;gu|R9SiPDY)6@=|18I4IXzZv_V*o zt79LxperAWcmlZK>hORtR_9{$MRmW$<@4$X=w>jQ|LI7q{s!GwmPuY&_$5oj`3|19 z#?l5Nswp!VOx@2xylzqNXP#2c6=g8`tw0|-hsj0q# zFj8VaJpBs!zyEGcHc{I1>~Q@uuypxnW#(y!=fGzprN*u&rz#b~)L!JQc4wAC6+_cffh*4Sja#viIOBQ zGb7;Rtd-h;^PRQr+T~UeCxsh^l=@1J^uX@&dmIk#MXdNjG8wTJD(Qp@4LB5*GT8`y zvD1+QqzZ%3GCpH>%C~H53|N1?`7d^-gFg!=__e8IX{e_r+a+QR|T-jlt>XpkkwNqN7IjoV1EgiluHWE{~S`GjOtMyHEFqu+c7GMY{V zWvD9G^YK7!6*!zX_D?^Ya#{G9#3qw$%H2m2XVyfBuZ0 zp>`CRpSE~`TU&v{axuU5pc)ENKm^V>bwn{^3*f4^;N9JHOx^6qWKp&Q>=2^Hst`PQ z^YSMZ(v*F=8wCtqs_bM<)ihAtr>LiQyiLRvzM-AyNARMDln0~j7b9nUH_9Ez#UsZX zE-^UG_1F*uN2_tZ7(ycze^7vN?c=9Y#pla`39M?l^}2dDJ0bHzSd>Vn?h!5iV3-W~ z8q#%nrE0i?PqUGNq1&n+>UdM48-Ht#^iP$9_*8_D`4qc8{60*tgQH<@d#zF0_U2t& z&p7cse=h1AxHjksY6~|0eA;FDz{_mgzBCFmq($dlR))*g`+j)1axS$T>93V(K5||1 zdrk=e=Lo+Hrg@#CU2#U8-b$UxN&?xX+S+jGSew_4V#lw5&3>-J-a>18(#e|lH0;a3 z?%9{kxw||eB)EH#gB!aWfM!x2ha$a|VYBHJrich;qS8z@Y_JaOAmwmkZ!x2V|0Veo zLCjj~%r+W2wfOWS^+s8!&E@eualUzvrgR{%=dJ*FT(DlFAl!Teka^uEn+2>-*D~VR z5S<3aqn_*;RPLe~a(@2v^CsV*SW?OH3)b%vzlYmI5&oy*R?c}6Sm`bxDGrQ42tWg7 zh&-F=4$J*={4Cc_s})z{<;Nmq@Hn70xi}#+iB&QC4+o4gS7G}19Ge#KapItz^29Ul z0;2HeFE_Vb9jz-v$Oeai>Nn54z!?k%fI45u7=%ScS~dZiQuXmY1RyOL&Sr`?XBR`s z1EY8k(_?#X_jPrrtFf_S94k0QPJj#mzc?v`v2eq8lY^A`B7v!TQ-+15T}2XAuDl$lPcUc6GPat72~LhIRy6P}K_gN5CbQXOqa za(^FNLy4IZ+pRzKm&*ZIQw0l9?knupkmU`%VP_T*9U)$g!vK+skwK!T_d*)6XI&D` zt#~>HDy0;8WIL7xJabDF|IT`C0<$awBs=d!RCtbhaNu72*2lyOKGoYOklzTxv zY1ojMEf9!d$>5-}^{ey{R2qmVOjQXu^eVsQ80efP)?5P=pi5a*!8h}sx_Z5pZ}NAz z&U}^LtD*sn3IP^{YI%p;&oI{NTRZXvzfCTOPceOKM4Hejc5RJnXPV*XOc7YEfRJP` zH7%0dJ%2un{)o??>Elwcu%4kaMR*!51yxh6uB4lL6Q{ez)6s(Kr28Elnl8!RLht6m zxmHzqT!J}EuN&6H6`TA95@YxW^ZEE1N{NY&nM5Ns8#xMy1{gNYgdr7+a6<;`m8N7c zVkswu4=@EMCX49|fVp9+)8k&8HhCGG^$P$;Py8B2~}lHV#py>Rs<6xITNk zZRdP>3a4CWo7@5%+W&{JM0p1lI9!rBJjc#G?-W66)03PGn12%Rs&NBNVZslQ=s$xs zY1j&kNsuUnIaGx|hT!I28_bh)it#v^H`Zt!vc9|%NirjF&*c4ug`PIBkH5MwvQOZp)?TlN1GtS4xWExsnn}s}LY|cDjNiWM zf7sTho?LGMh ziW=`@aBl_B*?^u~H7_x!zm=0rD3rythu>|;CCS7+Nyni=xFv^qp)(Ld(9171JD$&G zp#aaNu$1>zR3sWr0G9#yab!Ng^m;x9kbk7{kgjA^QwxWJY{7~mr(r7T$`ZXEPK Dl~gDB literal 0 HcmV?d00001 diff --git a/mcpb/manifest.json b/mcpb/manifest.json index 10da2c92..8d17caab 100644 --- a/mcpb/manifest.json +++ b/mcpb/manifest.json @@ -17,6 +17,7 @@ "documentation": "https://developer.screenly.io/mcp", "support": "https://github.com/Screenly/cli/issues", "license": "MIT", + "icon": "icon.png", "keywords": [ "digital-signage", "screenly", From b73fbe9f4f4d38b8773d408e525d865b139a0e7d Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 11:03:17 +0530 Subject: [PATCH 11/12] Polish MCPB release packaging: pin mcpb and add friendly aliases. Pin @anthropic-ai/mcpb to 2.1.2 for reproducible releases, and publish screenly-macos-arm64/x64 and screenly-windows-x64 aliases alongside the rustc-target filenames. --- .github/workflows/release.yml | 8 +++++++- mcpb/README.md | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf82c142..0bb1aa0d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,16 +51,19 @@ jobs: rust: stable target: x86_64-apple-darwin mcpb_platform: darwin + mcpb_alias: macos-x64 - build: macos-aarch64 os: macos-15 rust: stable target: aarch64-apple-darwin mcpb_platform: darwin + mcpb_alias: macos-arm64 - build: windows os: ubuntu-22.04 rust: stable target: x86_64-pc-windows-gnu mcpb_platform: win32 + mcpb_alias: windows-x64 - build: windows-32 os: ubuntu-22.04 rust: stable @@ -138,8 +141,11 @@ jobs: | .server.mcp_config.command = ("${__dirname}/" + $entry_point) | del(.server.mcp_config.platform_overrides)' \ mcpb/manifest.json > mcpb-build/manifest.json - npx --yes @anthropic-ai/mcpb@2 pack \ + npx --yes @anthropic-ai/mcpb@2.1.2 pack \ mcpb-build "screenly-cli-${{ matrix.target }}.mcpb" + # Friendlier alias next to the rustc-target name (both are published). + cp "screenly-cli-${{ matrix.target }}.mcpb" \ + "screenly-${{ matrix.mcpb_alias }}.mcpb" - name: Publish uses: softprops/action-gh-release@v1 diff --git a/mcpb/README.md b/mcpb/README.md index 7dc5d3e8..d4b45e36 100644 --- a/mcpb/README.md +++ b/mcpb/README.md @@ -20,9 +20,10 @@ You can generate a token at `https://[your-workspace].screenlyapp.com` under ### Sideload from a GitHub release (testing / pre-listing) -1. Download `screenly-cli-.mcpb` from the +1. Download the bundle for your machine from the [latest release](https://github.com/Screenly/cli/releases/latest), for example - `screenly-cli-aarch64-apple-darwin.mcpb` on an Apple Silicon Mac. + `screenly-macos-arm64.mcpb` on an Apple Silicon Mac (aliases: + `screenly-macos-x64.mcpb`, `screenly-windows-x64.mcpb`). 2. Open the file. Claude Desktop shows an installation dialog. 3. Paste your Screenly API token when prompted. From f352a1eccd2a9188ecee0d1f3935bcc1a80c2217 Mon Sep 17 00:00:00 2001 From: Salman Faris Date: Wed, 5 Aug 2026 11:45:22 +0530 Subject: [PATCH 12/12] Fix rustfmt import order in the MCPB drift test. --- src/mcp/tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mcp/tests.rs b/src/mcp/tests.rs index 0a366d62..35614e32 100644 --- a/src/mcp/tests.rs +++ b/src/mcp/tests.rs @@ -640,11 +640,12 @@ fn test_edge_app_list_instances() { /// the `#[tool]` definitions in `server.rs` (names + descriptions). #[test] fn test_mcpb_manifest_tools_match_server() { - use regex::Regex; use std::collections::BTreeMap; use std::fs; use std::path::PathBuf; + use regex::Regex; + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let manifest: serde_json::Value = serde_json::from_str( &fs::read_to_string(manifest_dir.join("mcpb/manifest.json"))