pixelweaver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.development +1 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +16 -0
- package/.python-version +1 -0
- package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
- package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
- package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/bases/CHANGELOG.md +5 -0
- package/.rlsbl/bases/LICENSE +21 -0
- package/.rlsbl/changes/.validated +1 -0
- package/.rlsbl/changes/0.1.0.jsonl +85 -0
- package/.rlsbl/changes/0.1.0.md +13 -0
- package/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/config.json +6 -0
- package/.rlsbl/hashes.json +14 -0
- package/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/releases/unreleased.toml +0 -0
- package/.rlsbl/releases/v0.1.0.toml +3 -0
- package/.rlsbl/version +1 -0
- package/.selfdoc/hashes/hashes.json +146 -0
- package/.strictcli/schema.json +227 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +100 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/icon.png +0 -0
- package/docs/_README.md +117 -0
- package/docs/cli-config.md +35 -0
- package/docs/cli-dev.md +21 -0
- package/docs/cli-diagnose.md +12 -0
- package/docs/cli-index.md +30 -0
- package/docs/cli-list.md +18 -0
- package/docs/cli-mcp.md +18 -0
- package/docs/cli-new.md +26 -0
- package/docs/cli-open.md +21 -0
- package/docs/cli-serve.md +21 -0
- package/docs/cli-stop.md +12 -0
- package/docs/gen-index.md +36 -0
- package/docs/index.md +13 -0
- package/docs/server-src-pixelweaver-__main__.md +12 -0
- package/docs/server-src-pixelweaver-autosave.md +12 -0
- package/docs/server-src-pixelweaver-bridge.md +12 -0
- package/docs/server-src-pixelweaver-cli.md +12 -0
- package/docs/server-src-pixelweaver-config.md +12 -0
- package/docs/server-src-pixelweaver-connections.md +12 -0
- package/docs/server-src-pixelweaver-main.md +12 -0
- package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
- package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
- package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
- package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
- package/docs/server-src-pixelweaver-mcp_server.md +12 -0
- package/docs/server-src-pixelweaver-protocol.md +12 -0
- package/docs/server-src-pixelweaver-state.md +12 -0
- package/docs/server-src-pixelweaver-storage.md +12 -0
- package/docs/server-src-pixelweaver-websocket.md +12 -0
- package/docs/server-src-pixelweaver.md +12 -0
- package/e2e/app-launch.test.ts +35 -0
- package/e2e/menus.test.ts +26 -0
- package/e2e/tools.test.ts +27 -0
- package/e2e/undo-redo.test.ts +11 -0
- package/eslint.config.js +62 -0
- package/index.html +13 -0
- package/package.json +48 -0
- package/playwright.config.ts +19 -0
- package/plugins/builtin/.gitkeep +0 -0
- package/plugins/builtin/advanced-fill-tool.ts +146 -0
- package/plugins/builtin/circle-tool.ts +186 -0
- package/plugins/builtin/diamond-tool.ts +182 -0
- package/plugins/builtin/dither-tool.ts +186 -0
- package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
- package/plugins/builtin/drawing-utils.test.ts +495 -0
- package/plugins/builtin/drawing-utils.ts +431 -0
- package/plugins/builtin/effects/blur.ts +97 -0
- package/plugins/builtin/effects/color-effects.ts +278 -0
- package/plugins/builtin/effects/flip.ts +83 -0
- package/plugins/builtin/effects/glow.ts +118 -0
- package/plugins/builtin/effects/outline.ts +110 -0
- package/plugins/builtin/effects/rotate.ts +357 -0
- package/plugins/builtin/effects/scale.ts +258 -0
- package/plugins/builtin/effects/shadow.ts +111 -0
- package/plugins/builtin/effects/sharpen.ts +102 -0
- package/plugins/builtin/effects.test.ts +715 -0
- package/plugins/builtin/eraser-tool.ts +23 -0
- package/plugins/builtin/eyedropper-tool.ts +83 -0
- package/plugins/builtin/fill-tool.ts +93 -0
- package/plugins/builtin/gradient-tool.ts +204 -0
- package/plugins/builtin/gradient.test.ts +142 -0
- package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
- package/plugins/builtin/importers/aseprite-parser.ts +497 -0
- package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
- package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
- package/plugins/builtin/line-tool.ts +267 -0
- package/plugins/builtin/make-stroke-tool.ts +271 -0
- package/plugins/builtin/noise-dither.test.ts +151 -0
- package/plugins/builtin/noise-tool.ts +131 -0
- package/plugins/builtin/pattern-stamp-tool.ts +162 -0
- package/plugins/builtin/pencil-tool.ts +25 -0
- package/plugins/builtin/rect-tool.ts +179 -0
- package/plugins/builtin/selection-tool.ts +388 -0
- package/plugins/builtin/selection.test.ts +195 -0
- package/plugins/builtin/tool-plugins.test.ts +529 -0
- package/public/favicon.svg +7 -0
- package/public/sw.js +91 -0
- package/pyproject.toml +49 -0
- package/scripts/eslint-wave-a-list.sh +24 -0
- package/scripts/eslint-wave-a-status.sh +25 -0
- package/scripts/fix-index-signature-access.py +127 -0
- package/scripts/fix-unchecked-index.py +128 -0
- package/scripts/fix-unchecked-vars.py +127 -0
- package/scripts/fix-wave-a-bangs.py +36 -0
- package/scripts/fix-wave-a-templates.py +108 -0
- package/scripts/fix-wave-b-void-expr.py +167 -0
- package/scripts/generate-command-params.py +540 -0
- package/scripts/migrate-single-frame-to-multi.py +171 -0
- package/scripts/smoke-test.sh +77 -0
- package/selfdoc.json +10 -0
- package/server/README.md +0 -0
- package/server/src/pixelweaver/__init__.py +1 -0
- package/server/src/pixelweaver/__main__.py +4 -0
- package/server/src/pixelweaver/autosave.py +114 -0
- package/server/src/pixelweaver/bridge.py +127 -0
- package/server/src/pixelweaver/cli.py +199 -0
- package/server/src/pixelweaver/config.py +24 -0
- package/server/src/pixelweaver/connections.py +54 -0
- package/server/src/pixelweaver/main.py +271 -0
- package/server/src/pixelweaver/mcp_bridge.py +189 -0
- package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
- package/server/src/pixelweaver/mcp_export_tools.py +291 -0
- package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
- package/server/src/pixelweaver/mcp_history_tools.py +106 -0
- package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
- package/server/src/pixelweaver/mcp_lock.py +37 -0
- package/server/src/pixelweaver/mcp_project_tools.py +302 -0
- package/server/src/pixelweaver/mcp_read_tools.py +163 -0
- package/server/src/pixelweaver/mcp_registry.py +247 -0
- package/server/src/pixelweaver/mcp_resources.py +312 -0
- package/server/src/pixelweaver/mcp_server.py +234 -0
- package/server/src/pixelweaver/protocol.py +219 -0
- package/server/src/pixelweaver/state.py +267 -0
- package/server/src/pixelweaver/storage.py +293 -0
- package/server/src/pixelweaver/websocket.py +282 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/conftest.py +17 -0
- package/server/tests/test_api.py +96 -0
- package/server/tests/test_bridge.py +161 -0
- package/server/tests/test_health.py +9 -0
- package/server/tests/test_integration.py +86 -0
- package/server/tests/test_mcp_bridge.py +293 -0
- package/server/tests/test_mcp_lock.py +34 -0
- package/server/tests/test_mcp_registry.py +679 -0
- package/server/tests/test_mcp_resources.py +648 -0
- package/server/tests/test_protocol.py +125 -0
- package/server/tests/test_state.py +87 -0
- package/server/tests/test_storage.py +306 -0
- package/server/tests/test_websocket.py +275 -0
- package/src/App.svelte +107 -0
- package/src/app.css +215 -0
- package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
- package/src/lib/animation/animation-commands.test.ts +228 -0
- package/src/lib/animation/animation-commands.ts +540 -0
- package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
- package/src/lib/animation/animation-preview.svelte.ts +151 -0
- package/src/lib/animation/clipboard.ts +134 -0
- package/src/lib/animation/frame-model.svelte.ts +437 -0
- package/src/lib/animation/frame-model.test.ts +314 -0
- package/src/lib/animation/frame-selection.svelte.ts +77 -0
- package/src/lib/animation/frame-tags.svelte.ts +238 -0
- package/src/lib/animation/frame-tags.test.ts +136 -0
- package/src/lib/animation/import.test.ts +141 -0
- package/src/lib/animation/import.ts +112 -0
- package/src/lib/animation/spritesheet-export.test.ts +239 -0
- package/src/lib/animation/spritesheet-export.ts +314 -0
- package/src/lib/canvas/CanvasViewport.svelte +216 -0
- package/src/lib/canvas/canvas-init-plugin.ts +178 -0
- package/src/lib/canvas/canvas-renderer.ts +408 -0
- package/src/lib/canvas/canvas-state.svelte.ts +232 -0
- package/src/lib/canvas/canvas-state.test.ts +139 -0
- package/src/lib/canvas/input-handler.ts +221 -0
- package/src/lib/canvas/input-plugin.ts +150 -0
- package/src/lib/canvas/onion-skin.ts +94 -0
- package/src/lib/canvas/pixel-buffer.test.ts +249 -0
- package/src/lib/canvas/pixel-buffer.ts +151 -0
- package/src/lib/canvas/render-state.svelte.ts +18 -0
- package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
- package/src/lib/canvas/tile-mode.test.ts +53 -0
- package/src/lib/canvas/tile-mode.ts +92 -0
- package/src/lib/canvas/viewport-utils.ts +24 -0
- package/src/lib/canvas/zoom-utils.ts +31 -0
- package/src/lib/color/.gitkeep +0 -0
- package/src/lib/color/color-commands.ts +87 -0
- package/src/lib/color/color-state.svelte.ts +98 -0
- package/src/lib/color/color-state.test.ts +91 -0
- package/src/lib/color/color-utils.test.ts +220 -0
- package/src/lib/color/color-utils.ts +243 -0
- package/src/lib/color/palette-state.svelte.ts +127 -0
- package/src/lib/color/palette-state.test.ts +154 -0
- package/src/lib/color/palette.ts +79 -0
- package/src/lib/core/bootstrap.ts +66 -0
- package/src/lib/core/command-params.generated.ts +1549 -0
- package/src/lib/core/command-params.ts +20 -0
- package/src/lib/core/command-runner.ts +79 -0
- package/src/lib/core/commands.ts +134 -0
- package/src/lib/core/dispatcher.test.ts +548 -0
- package/src/lib/core/dispatcher.ts +361 -0
- package/src/lib/core/index.test.ts +7 -0
- package/src/lib/core/notification-state.svelte.ts +119 -0
- package/src/lib/core/plugin-api.ts +210 -0
- package/src/lib/core/plugin-discovery.test.ts +53 -0
- package/src/lib/core/plugin-discovery.ts +65 -0
- package/src/lib/core/plugin-loader.test.ts +159 -0
- package/src/lib/core/plugin-loader.ts +240 -0
- package/src/lib/core/plugin-types.ts +286 -0
- package/src/lib/core/registries.svelte.ts +74 -0
- package/src/lib/core/tool-options-state.svelte.ts +61 -0
- package/src/lib/dock/DockLayout.svelte +375 -0
- package/src/lib/dock/TabAddMenu.svelte +90 -0
- package/src/lib/dock/dock-persistence.ts +46 -0
- package/src/lib/dock/dock-plugin.ts +49 -0
- package/src/lib/dock/dock-presets.ts +156 -0
- package/src/lib/dock/dock-state.svelte.ts +77 -0
- package/src/lib/dock/dock-types.ts +2 -0
- package/src/lib/dock/dockview-theme.css +226 -0
- package/src/lib/dock/dockview-theme.ts +17 -0
- package/src/lib/dock/header-action-renderer.ts +77 -0
- package/src/lib/edit/clipboard-state.ts +34 -0
- package/src/lib/export/download.ts +201 -0
- package/src/lib/export/png-metadata.ts +181 -0
- package/src/lib/history/ActionLogPanel.svelte +418 -0
- package/src/lib/history/action-log-panel-plugin.ts +24 -0
- package/src/lib/history/action-log.svelte.ts +172 -0
- package/src/lib/history/action-log.test.ts +168 -0
- package/src/lib/history/history-commands.ts +403 -0
- package/src/lib/history/macros.svelte.ts +320 -0
- package/src/lib/history/macros.test.ts +224 -0
- package/src/lib/history/replay-engine.test.ts +241 -0
- package/src/lib/history/replay-engine.ts +149 -0
- package/src/lib/history/spec-format.test.ts +250 -0
- package/src/lib/history/spec-format.ts +210 -0
- package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
- package/src/lib/iso/iso-math.test.ts +77 -0
- package/src/lib/iso/iso-math.ts +77 -0
- package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
- package/src/lib/iso/seam-checker.test.ts +126 -0
- package/src/lib/iso/seam-checker.ts +93 -0
- package/src/lib/iso/tessellation.ts +67 -0
- package/src/lib/layers/compositor.test.ts +193 -0
- package/src/lib/layers/compositor.ts +175 -0
- package/src/lib/layers/layer-commands.test.ts +263 -0
- package/src/lib/layers/layer-commands.ts +429 -0
- package/src/lib/layers/layer-tree.svelte.ts +516 -0
- package/src/lib/layers/layer-tree.test.ts +383 -0
- package/src/lib/layers/layer-types.ts +56 -0
- package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
- package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
- package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
- package/src/lib/leveleditor/depth-sort.test.ts +70 -0
- package/src/lib/leveleditor/depth-sort.ts +39 -0
- package/src/lib/leveleditor/level-editor-commands.ts +353 -0
- package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
- package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/map-state.svelte.ts +372 -0
- package/src/lib/leveleditor/map-state.test.ts +243 -0
- package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
- package/src/lib/leveleditor/tiled-export.test.ts +144 -0
- package/src/lib/leveleditor/tiled-export.ts +374 -0
- package/src/lib/pywebview.d.ts +15 -0
- package/src/lib/recovery/.gitkeep +0 -0
- package/src/lib/recovery/idb-store.ts +118 -0
- package/src/lib/recovery/recovery-manager.test.ts +321 -0
- package/src/lib/recovery/recovery-manager.ts +115 -0
- package/src/lib/recovery/recovery-plugin.ts +55 -0
- package/src/lib/recovery/recovery-state.svelte.ts +21 -0
- package/src/lib/recovery/sw-plugin.ts +18 -0
- package/src/lib/recovery/sw-register.ts +17 -0
- package/src/lib/save/directory-format.ts +42 -0
- package/src/lib/save/project-snapshot.ts +139 -0
- package/src/lib/save/recent-projects.ts +56 -0
- package/src/lib/save/save-state.svelte.ts +35 -0
- package/src/lib/save/storage.ts +167 -0
- package/src/lib/save/zip-format.ts +45 -0
- package/src/lib/shortcuts/.gitkeep +0 -0
- package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
- package/src/lib/shortcuts/default-bindings.ts +61 -0
- package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
- package/src/lib/shortcuts/shortcut-init.ts +380 -0
- package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
- package/src/lib/shortcuts/shortcut-manager.ts +281 -0
- package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
- package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
- package/src/lib/sync/patch-applicator.ts +300 -0
- package/src/lib/sync/patch-types.ts +65 -0
- package/src/lib/sync/project-manager.ts +108 -0
- package/src/lib/sync/sync-init.ts +152 -0
- package/src/lib/sync/sync-plugin.ts +19 -0
- package/src/lib/sync/sync-state.svelte.ts +56 -0
- package/src/lib/sync/ws-client.test.ts +604 -0
- package/src/lib/sync/ws-client.ts +574 -0
- package/src/lib/tools/.gitkeep +0 -0
- package/src/lib/ui/.gitkeep +0 -0
- package/src/lib/ui/AboutDialog.svelte +113 -0
- package/src/lib/ui/ColorPicker.svelte +761 -0
- package/src/lib/ui/ContextMenu.svelte +216 -0
- package/src/lib/ui/ExportDialog.svelte +747 -0
- package/src/lib/ui/FrameStrip.svelte +854 -0
- package/src/lib/ui/LayerPanel.svelte +810 -0
- package/src/lib/ui/MenuBar.svelte +590 -0
- package/src/lib/ui/NewProjectDialog.svelte +803 -0
- package/src/lib/ui/PluginManagerPanel.svelte +475 -0
- package/src/lib/ui/PromptDialog.svelte +252 -0
- package/src/lib/ui/RecoveryDialog.svelte +295 -0
- package/src/lib/ui/ResizeDialog.svelte +416 -0
- package/src/lib/ui/StatusBar.svelte +145 -0
- package/src/lib/ui/ToolbarPanel.svelte +488 -0
- package/src/lib/ui/animation-menu-commands.ts +194 -0
- package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
- package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
- package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
- package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
- package/src/lib/ui/dialog-state.svelte.ts +70 -0
- package/src/lib/ui/edit-commands.ts +271 -0
- package/src/lib/ui/file-commands.ts +275 -0
- package/src/lib/ui/file-open.ts +99 -0
- package/src/lib/ui/help-commands.ts +93 -0
- package/src/lib/ui/image-commands.ts +181 -0
- package/src/lib/ui/layer-menu-commands.ts +420 -0
- package/src/lib/ui/menu-builder.ts +224 -0
- package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
- package/src/lib/ui/notifications/notification-plugin.ts +29 -0
- package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
- package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
- package/src/lib/ui/plugin-state.svelte.ts +62 -0
- package/src/lib/ui/select-commands.ts +75 -0
- package/src/lib/ui/theme-plugin.ts +18 -0
- package/src/lib/ui/theme.svelte.ts +45 -0
- package/src/lib/ui/theme.test.ts +51 -0
- package/src/lib/ui/toolbar-config.ts +90 -0
- package/src/lib/ui/toolbar-plugin.ts +39 -0
- package/src/lib/ui/view-commands.ts +629 -0
- package/src/lib/variants/BisectionExportDialog.svelte +500 -0
- package/src/lib/variants/VariantPanel.svelte +822 -0
- package/src/lib/variants/bisection-export.test.ts +113 -0
- package/src/lib/variants/bisection-export.ts +148 -0
- package/src/lib/variants/palette-extraction.test.ts +111 -0
- package/src/lib/variants/palette-extraction.ts +84 -0
- package/src/lib/variants/palette-interpolation.test.ts +113 -0
- package/src/lib/variants/palette-interpolation.ts +87 -0
- package/src/lib/variants/palette-swap.test.ts +101 -0
- package/src/lib/variants/palette-swap.ts +114 -0
- package/src/lib/variants/variant-commands.ts +594 -0
- package/src/lib/variants/variant-panel-plugin.ts +27 -0
- package/src/lib/variants/variant-randomizer.ts +101 -0
- package/src/lib/variants/variant-state.svelte.ts +166 -0
- package/src/lib/variants/variant-state.test.ts +138 -0
- package/src/main.ts +14 -0
- package/src/vite-env.d.ts +3 -0
- package/svelte.config.js +2 -0
- package/todo/.done/audit-design-decisions.md +812 -0
- package/todo/.done/audit-implementation-plan.md +1235 -0
- package/todo/.done/happy-path-polish.md +177 -0
- package/todo/.done/pixelweaver-full-build.md +937 -0
- package/todo/.done/server-multi-frame-design.md +405 -0
- package/todo/.done/typed-dispatcher-design.md +435 -0
- package/todo/.done/unified-toolbar-and-action-system.md +323 -0
- package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
- package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
- package/todo/comprehensive-audit.md +1085 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +20 -0
- package/uv.lock +1167 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""MCP command registry for PixelWeaver.
|
|
2
|
+
|
|
3
|
+
Maps MCP tool names to internal state mutations and read operations.
|
|
4
|
+
Each tool handler receives validated arguments and returns a result dict.
|
|
5
|
+
|
|
6
|
+
Tools are split into three categories:
|
|
7
|
+
- Raw command tools: one per drawing/layer/frame/project/canvas/history/export command
|
|
8
|
+
- Curated high-level tools: combine multiple raw tools behind a friendlier API
|
|
9
|
+
- Read/introspection tools: return data without mutating state
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pixelweaver.connections import ConnectionManager
|
|
20
|
+
from pixelweaver.state import ServerState
|
|
21
|
+
from pixelweaver.storage import export_frame_png
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Tool definition helpers
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolDef:
|
|
29
|
+
"""Lightweight descriptor for an MCP tool."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
name: str,
|
|
34
|
+
description: str,
|
|
35
|
+
parameters: dict[str, Any],
|
|
36
|
+
*,
|
|
37
|
+
mutates: bool = True,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.name = name
|
|
40
|
+
self.description = description
|
|
41
|
+
self.parameters = parameters
|
|
42
|
+
# Whether the tool mutates state (controls thumbnail inclusion)
|
|
43
|
+
self.mutates = mutates
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Registry
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MCPCommandRegistry:
|
|
52
|
+
"""Registry that maps MCP tool names to command dispatch."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, state: ServerState, connections: ConnectionManager) -> None:
|
|
55
|
+
self.state = state
|
|
56
|
+
self.connections = connections
|
|
57
|
+
self._handlers: dict[str, Any] = {}
|
|
58
|
+
self._tool_defs: dict[str, ToolDef] = {}
|
|
59
|
+
self._register_all()
|
|
60
|
+
|
|
61
|
+
# -- public API ----------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
async def execute_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
"""Execute an MCP tool and return the result."""
|
|
65
|
+
handler = self._handlers.get(tool_name)
|
|
66
|
+
if handler is None:
|
|
67
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
|
68
|
+
|
|
69
|
+
tool_def = self._tool_defs.get(tool_name)
|
|
70
|
+
|
|
71
|
+
# Mutating tools take the shared lock so MCP and WebSocket mutation
|
|
72
|
+
# paths can't interleave a partial state change. Read-only tools skip
|
|
73
|
+
# the lock to keep introspection cheap and non-blocking.
|
|
74
|
+
try:
|
|
75
|
+
if tool_def and tool_def.mutates:
|
|
76
|
+
async with self.state.mutation_lock:
|
|
77
|
+
result = await handler(arguments)
|
|
78
|
+
else:
|
|
79
|
+
result = await handler(arguments)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
return {"success": False, "error": str(exc)}
|
|
82
|
+
|
|
83
|
+
# Attach thumbnail for mutation operations
|
|
84
|
+
if tool_def and tool_def.mutates:
|
|
85
|
+
thumb = self._get_active_canvas_thumbnail()
|
|
86
|
+
if thumb:
|
|
87
|
+
result["thumbnail"] = thumb
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
def get_tool_definitions(self) -> list[dict[str, Any]]:
|
|
92
|
+
"""Return MCP-compatible tool definitions for all registered tools."""
|
|
93
|
+
defs = []
|
|
94
|
+
for td in self._tool_defs.values():
|
|
95
|
+
defs.append({
|
|
96
|
+
"name": td.name,
|
|
97
|
+
"description": td.description,
|
|
98
|
+
"inputSchema": td.parameters,
|
|
99
|
+
})
|
|
100
|
+
return defs
|
|
101
|
+
|
|
102
|
+
def get_tool_def(self, name: str) -> ToolDef | None:
|
|
103
|
+
"""Get a single tool definition by name."""
|
|
104
|
+
return self._tool_defs.get(name)
|
|
105
|
+
|
|
106
|
+
# -- registration --------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def _register(self, tool_def: ToolDef, handler: Any) -> None:
|
|
109
|
+
"""Register a tool definition and its async handler."""
|
|
110
|
+
self._tool_defs[tool_def.name] = tool_def
|
|
111
|
+
self._handlers[tool_def.name] = handler
|
|
112
|
+
|
|
113
|
+
def _register_all(self) -> None:
|
|
114
|
+
"""Register every tool via domain modules."""
|
|
115
|
+
from .mcp_drawing_tools import register_drawing_tools
|
|
116
|
+
from .mcp_export_tools import register_export_tools
|
|
117
|
+
from .mcp_frame_tools import register_frame_tools
|
|
118
|
+
from .mcp_history_tools import register_history_tools
|
|
119
|
+
from .mcp_layer_tools import register_layer_tools
|
|
120
|
+
from .mcp_project_tools import register_project_tools
|
|
121
|
+
from .mcp_read_tools import register_read_tools
|
|
122
|
+
|
|
123
|
+
register_drawing_tools(self)
|
|
124
|
+
register_layer_tools(self)
|
|
125
|
+
register_frame_tools(self)
|
|
126
|
+
register_project_tools(self)
|
|
127
|
+
register_history_tools(self)
|
|
128
|
+
register_export_tools(self)
|
|
129
|
+
register_read_tools(self)
|
|
130
|
+
|
|
131
|
+
# -- helpers -------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def _get_active_canvas_thumbnail(self) -> str | None:
|
|
134
|
+
"""Get a thumbnail of the first canvas in the active project."""
|
|
135
|
+
project = self.state.get_active_project()
|
|
136
|
+
if project is None:
|
|
137
|
+
return None
|
|
138
|
+
canvas = next(iter(project.canvases.values()), None)
|
|
139
|
+
if canvas is None:
|
|
140
|
+
return None
|
|
141
|
+
# Composite all visible layers
|
|
142
|
+
try:
|
|
143
|
+
png_bytes = export_frame_png(project, canvas.name)
|
|
144
|
+
# Re-encode as base64 thumbnail (the export already is a PNG)
|
|
145
|
+
return base64.b64encode(png_bytes).decode("ascii")
|
|
146
|
+
except Exception:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def _make_command(
|
|
150
|
+
self,
|
|
151
|
+
cmd_type: str,
|
|
152
|
+
params: dict[str, Any],
|
|
153
|
+
*,
|
|
154
|
+
plugin: str = "mcp",
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
"""Build a command dict matching the protocol format."""
|
|
157
|
+
return {
|
|
158
|
+
"type": cmd_type,
|
|
159
|
+
"plugin": plugin,
|
|
160
|
+
"version": "1.0.0",
|
|
161
|
+
"params": params,
|
|
162
|
+
"id": str(uuid.uuid4()),
|
|
163
|
+
"timestamp": int(time.time() * 1000),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async def _dispatch_command(
|
|
167
|
+
self,
|
|
168
|
+
cmd_type: str,
|
|
169
|
+
params: dict[str, Any],
|
|
170
|
+
*,
|
|
171
|
+
plugin: str = "mcp",
|
|
172
|
+
) -> dict[str, Any]:
|
|
173
|
+
"""Create a command, store in history, and broadcast to WebSocket clients."""
|
|
174
|
+
project = self.state.get_active_project()
|
|
175
|
+
if project is None:
|
|
176
|
+
return {"success": False, "error": "No active project"}
|
|
177
|
+
|
|
178
|
+
cmd = self._make_command(cmd_type, params, plugin=plugin)
|
|
179
|
+
project.command_history.append(cmd)
|
|
180
|
+
project.redo_stack.clear()
|
|
181
|
+
|
|
182
|
+
# Broadcast to all connected WebSocket clients
|
|
183
|
+
await self.connections.broadcast({
|
|
184
|
+
"type": "command_broadcast",
|
|
185
|
+
"command": cmd,
|
|
186
|
+
"source": "mcp",
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return {"success": True, "command_id": cmd["id"]}
|
|
190
|
+
|
|
191
|
+
def _require_active_project(self) -> dict[str, Any] | None:
|
|
192
|
+
"""Return an error dict if no project is active, else None."""
|
|
193
|
+
if self.state.get_active_project() is None:
|
|
194
|
+
return {"success": False, "error": "No active project"}
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def _register_drawing_tool(
|
|
198
|
+
self, name: str, description: str, schema: dict[str, Any],
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Register a single drawing tool that dispatches a command.
|
|
201
|
+
|
|
202
|
+
Automatically injects an optional ``frame_index`` parameter so
|
|
203
|
+
MCP clients can target a specific animation frame. When provided,
|
|
204
|
+
the server sets ``canvas.current_frame_index`` before dispatching
|
|
205
|
+
the command (so the frontend draws on the correct frame) and
|
|
206
|
+
includes ``frame_index`` in the dispatched params for future
|
|
207
|
+
frontend-side routing.
|
|
208
|
+
"""
|
|
209
|
+
# Inject frame_index into every drawing tool schema
|
|
210
|
+
schema["properties"]["frame_index"] = {
|
|
211
|
+
"type": "integer",
|
|
212
|
+
"description": "Target frame index (default: current frame)",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
tool_def = ToolDef(name, description, schema, mutates=True)
|
|
216
|
+
|
|
217
|
+
async def handler(args: dict[str, Any], _name: str = name) -> dict[str, Any]:
|
|
218
|
+
err = self._require_active_project()
|
|
219
|
+
if err:
|
|
220
|
+
return err
|
|
221
|
+
|
|
222
|
+
project = self.state.get_active_project()
|
|
223
|
+
assert project is not None # guaranteed by _require_active_project
|
|
224
|
+
canvas = next(iter(project.canvases.values()))
|
|
225
|
+
|
|
226
|
+
# Resolve frame_index: use provided value or fall back to current
|
|
227
|
+
frame_index = args.pop("frame_index", canvas.current_frame_index)
|
|
228
|
+
|
|
229
|
+
# Validate bounds
|
|
230
|
+
if frame_index < 0 or frame_index >= len(canvas.frames):
|
|
231
|
+
return {
|
|
232
|
+
"success": False,
|
|
233
|
+
"error": (
|
|
234
|
+
f"frame_index {frame_index} out of range "
|
|
235
|
+
f"[0, {len(canvas.frames)})"
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Point the canvas at the target frame before dispatch
|
|
240
|
+
canvas.current_frame_index = frame_index
|
|
241
|
+
|
|
242
|
+
# Include frame_index in dispatched params for frontend use
|
|
243
|
+
args["frame_index"] = frame_index
|
|
244
|
+
|
|
245
|
+
return await self._dispatch_command(_name, args)
|
|
246
|
+
|
|
247
|
+
self._register(tool_def, handler)
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""MCP resources for PixelWeaver.
|
|
2
|
+
|
|
3
|
+
Exposes project state as MCP resources so that LLM clients can read and
|
|
4
|
+
subscribe to state changes. Resources are registered on the FastMCP
|
|
5
|
+
``mcp`` instance; subscriptions are wired through the low-level server
|
|
6
|
+
so that mutating tool calls can push ``resource/updated`` notifications.
|
|
7
|
+
|
|
8
|
+
Resource URIs
|
|
9
|
+
-------------
|
|
10
|
+
- ``project://state`` -- full project overview
|
|
11
|
+
- ``project://canvas/{name}`` -- single canvas metadata
|
|
12
|
+
- ``project://canvas/{name}/frame/{index}`` -- composited frame as PNG
|
|
13
|
+
- ``project://palette`` -- colours in use on canvas
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
from pydantic import AnyUrl
|
|
23
|
+
|
|
24
|
+
from pixelweaver.mcp_bridge import CollabServerUnreachableError
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from mcp.server.fastmcp import FastMCP
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Subscription tracking
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Tracks which resource URIs the client has subscribed to. Populated via
|
|
35
|
+
# the low-level subscribe/unsubscribe handlers registered in
|
|
36
|
+
# ``init_mcp_subscriptions``.
|
|
37
|
+
_subscribed_uris: set[str] = set()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_subscribed_uris() -> frozenset[str]:
|
|
41
|
+
"""Return a snapshot of currently subscribed URIs (for testing)."""
|
|
42
|
+
return frozenset(_subscribed_uris)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Helpers shared by resource handlers and the notification path
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def _unreachable_error(exc: Exception) -> str:
|
|
50
|
+
return json.dumps({
|
|
51
|
+
"error": (
|
|
52
|
+
f"Collab server unreachable: {exc}. "
|
|
53
|
+
"Make sure the PixelWeaver server is running "
|
|
54
|
+
"(pixelweaver serve --port 7779)."
|
|
55
|
+
),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _synced_state():
|
|
60
|
+
"""Pull latest state and return it, or raise on failure."""
|
|
61
|
+
# Import here to avoid circular imports at module level.
|
|
62
|
+
from pixelweaver.mcp_server import _ensure_initialized, _state, _sync_from_collab
|
|
63
|
+
|
|
64
|
+
_ensure_initialized()
|
|
65
|
+
await _sync_from_collab()
|
|
66
|
+
return _state
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Resource registration
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def init_mcp_resources(mcp: FastMCP) -> None:
|
|
74
|
+
"""Register all MCP resources on the FastMCP server instance.
|
|
75
|
+
|
|
76
|
+
Called from ``mcp_server.init_mcp_tools`` so resources and tools are
|
|
77
|
+
initialised together.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# -- project://state ------------------------------------------------
|
|
81
|
+
@mcp.resource(
|
|
82
|
+
"project://state",
|
|
83
|
+
name="project_state",
|
|
84
|
+
description=(
|
|
85
|
+
"Full project overview: project name, dimensions, canvas list "
|
|
86
|
+
"with layer trees, frame counts, and playback settings."
|
|
87
|
+
),
|
|
88
|
+
mime_type="application/json",
|
|
89
|
+
)
|
|
90
|
+
async def project_state() -> str:
|
|
91
|
+
try:
|
|
92
|
+
state = await _synced_state()
|
|
93
|
+
except CollabServerUnreachableError as exc:
|
|
94
|
+
return _unreachable_error(exc)
|
|
95
|
+
|
|
96
|
+
project = state.get_active_project()
|
|
97
|
+
if project is None:
|
|
98
|
+
return json.dumps({"error": "No active project"})
|
|
99
|
+
|
|
100
|
+
canvases_info: dict[str, Any] = {}
|
|
101
|
+
for cname, canvas in project.canvases.items():
|
|
102
|
+
canvases_info[cname] = {
|
|
103
|
+
"name": canvas.name,
|
|
104
|
+
"width": canvas.width,
|
|
105
|
+
"height": canvas.height,
|
|
106
|
+
"layer_count": len(canvas.layers),
|
|
107
|
+
"layers": [
|
|
108
|
+
{
|
|
109
|
+
"id": layer["id"],
|
|
110
|
+
"name": layer.get("name", ""),
|
|
111
|
+
"visible": layer.get("visible", True),
|
|
112
|
+
"opacity": layer.get("opacity", 1.0),
|
|
113
|
+
}
|
|
114
|
+
for layer in canvas.layers
|
|
115
|
+
],
|
|
116
|
+
"frame_count": canvas.frame_count,
|
|
117
|
+
"current_frame_index": canvas.current_frame_index,
|
|
118
|
+
"global_fps": canvas.global_fps,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return json.dumps(
|
|
122
|
+
{
|
|
123
|
+
"name": project.name,
|
|
124
|
+
"width": project.width,
|
|
125
|
+
"height": project.height,
|
|
126
|
+
"canvases": canvases_info,
|
|
127
|
+
},
|
|
128
|
+
indent=2,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# -- project://canvas/{name} ----------------------------------------
|
|
132
|
+
@mcp.resource(
|
|
133
|
+
"project://canvas/{name}",
|
|
134
|
+
name="canvas_info",
|
|
135
|
+
description=(
|
|
136
|
+
"Metadata for a single canvas: dimensions, layer tree, "
|
|
137
|
+
"frame count, playback state."
|
|
138
|
+
),
|
|
139
|
+
mime_type="application/json",
|
|
140
|
+
)
|
|
141
|
+
async def canvas_info(name: str) -> str:
|
|
142
|
+
try:
|
|
143
|
+
state = await _synced_state()
|
|
144
|
+
except CollabServerUnreachableError as exc:
|
|
145
|
+
return _unreachable_error(exc)
|
|
146
|
+
|
|
147
|
+
project = state.get_active_project()
|
|
148
|
+
if project is None:
|
|
149
|
+
return json.dumps({"error": "No active project"})
|
|
150
|
+
|
|
151
|
+
canvas = project.canvases.get(name)
|
|
152
|
+
if canvas is None:
|
|
153
|
+
return json.dumps({"error": f"Canvas {name!r} not found"})
|
|
154
|
+
|
|
155
|
+
return json.dumps(
|
|
156
|
+
{
|
|
157
|
+
"name": canvas.name,
|
|
158
|
+
"width": canvas.width,
|
|
159
|
+
"height": canvas.height,
|
|
160
|
+
"layers": [
|
|
161
|
+
{
|
|
162
|
+
"id": layer["id"],
|
|
163
|
+
"name": layer.get("name", ""),
|
|
164
|
+
"visible": layer.get("visible", True),
|
|
165
|
+
"opacity": layer.get("opacity", 1.0),
|
|
166
|
+
"locked": layer.get("locked", False),
|
|
167
|
+
"blend_mode": layer.get("blendMode", "normal"),
|
|
168
|
+
}
|
|
169
|
+
for layer in canvas.layers
|
|
170
|
+
],
|
|
171
|
+
"frame_count": canvas.frame_count,
|
|
172
|
+
"current_frame_index": canvas.current_frame_index,
|
|
173
|
+
"global_fps": canvas.global_fps,
|
|
174
|
+
},
|
|
175
|
+
indent=2,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# -- project://canvas/{name}/frame/{index} --------------------------
|
|
179
|
+
@mcp.resource(
|
|
180
|
+
"project://canvas/{name}/frame/{index}",
|
|
181
|
+
name="frame_png",
|
|
182
|
+
description="Composited frame pixel data as a base64-encoded PNG.",
|
|
183
|
+
mime_type="image/png",
|
|
184
|
+
)
|
|
185
|
+
async def frame_png(name: str, index: str) -> bytes:
|
|
186
|
+
"""Return composited PNG bytes for the given canvas frame."""
|
|
187
|
+
from pixelweaver.storage import export_frame_png
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
state = await _synced_state()
|
|
191
|
+
except CollabServerUnreachableError as exc:
|
|
192
|
+
# Resources returning bytes cannot easily embed error JSON;
|
|
193
|
+
# raise so the MCP framework surfaces the error to the client.
|
|
194
|
+
raise RuntimeError(_unreachable_error(exc)) from exc
|
|
195
|
+
|
|
196
|
+
project = state.get_active_project()
|
|
197
|
+
if project is None:
|
|
198
|
+
raise RuntimeError("No active project")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
frame_index = int(index)
|
|
202
|
+
except ValueError:
|
|
203
|
+
raise RuntimeError(f"Invalid frame index: {index!r}")
|
|
204
|
+
|
|
205
|
+
return export_frame_png(project, name, frame_index)
|
|
206
|
+
|
|
207
|
+
# -- project://palette -----------------------------------------------
|
|
208
|
+
@mcp.resource(
|
|
209
|
+
"project://palette",
|
|
210
|
+
name="palette",
|
|
211
|
+
description="Unique colours currently present on the active canvas.",
|
|
212
|
+
mime_type="application/json",
|
|
213
|
+
)
|
|
214
|
+
async def palette_resource() -> str:
|
|
215
|
+
try:
|
|
216
|
+
state = await _synced_state()
|
|
217
|
+
except CollabServerUnreachableError as exc:
|
|
218
|
+
return _unreachable_error(exc)
|
|
219
|
+
|
|
220
|
+
project = state.get_active_project()
|
|
221
|
+
if project is None:
|
|
222
|
+
return json.dumps({"error": "No active project"})
|
|
223
|
+
|
|
224
|
+
canvas = next(iter(project.canvases.values()), None)
|
|
225
|
+
if canvas is None:
|
|
226
|
+
return json.dumps({"error": "No canvas in project"})
|
|
227
|
+
|
|
228
|
+
max_colors = 256
|
|
229
|
+
colors: set[str] = set()
|
|
230
|
+
|
|
231
|
+
for layer in canvas.layers:
|
|
232
|
+
data = canvas.current_frame().pixel_data.get(layer["id"])
|
|
233
|
+
if data is None:
|
|
234
|
+
continue
|
|
235
|
+
for i in range(0, len(data), 4):
|
|
236
|
+
r, g, b, a = data[i], data[i + 1], data[i + 2], data[i + 3]
|
|
237
|
+
if a > 0:
|
|
238
|
+
colors.add(f"#{r:02x}{g:02x}{b:02x}{a:02x}")
|
|
239
|
+
if len(colors) >= max_colors:
|
|
240
|
+
break
|
|
241
|
+
if len(colors) >= max_colors:
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
return json.dumps({"colors": sorted(colors)}, indent=2)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Subscription handlers and notification support
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def init_mcp_subscriptions(mcp: FastMCP) -> None:
|
|
252
|
+
"""Wire subscribe/unsubscribe handlers and enable the capability.
|
|
253
|
+
|
|
254
|
+
Must be called after ``init_mcp_resources`` but before the server
|
|
255
|
+
starts accepting connections.
|
|
256
|
+
"""
|
|
257
|
+
low_level = mcp._mcp_server
|
|
258
|
+
|
|
259
|
+
@low_level.subscribe_resource()
|
|
260
|
+
async def handle_subscribe(uri: AnyUrl) -> None:
|
|
261
|
+
uri_str = str(uri)
|
|
262
|
+
_subscribed_uris.add(uri_str)
|
|
263
|
+
logger.info("Client subscribed to resource: %s", uri_str)
|
|
264
|
+
|
|
265
|
+
@low_level.unsubscribe_resource()
|
|
266
|
+
async def handle_unsubscribe(uri: AnyUrl) -> None:
|
|
267
|
+
uri_str = str(uri)
|
|
268
|
+
_subscribed_uris.discard(uri_str)
|
|
269
|
+
logger.info("Client unsubscribed from resource: %s", uri_str)
|
|
270
|
+
|
|
271
|
+
# Patch the capability so the server advertises subscribe=True.
|
|
272
|
+
# The low-level server hardcodes subscribe=False in get_capabilities;
|
|
273
|
+
# we wrap it to flip the flag after the fact.
|
|
274
|
+
_original_get_capabilities = low_level.get_capabilities
|
|
275
|
+
|
|
276
|
+
def _patched_get_capabilities(notification_options, experimental_capabilities):
|
|
277
|
+
caps = _original_get_capabilities(notification_options, experimental_capabilities)
|
|
278
|
+
if caps.resources is not None:
|
|
279
|
+
caps.resources.subscribe = True
|
|
280
|
+
return caps
|
|
281
|
+
|
|
282
|
+
low_level.get_capabilities = _patched_get_capabilities
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def notify_resource_subscribers(mcp: FastMCP) -> None:
|
|
286
|
+
"""Send ``resource/updated`` for every currently-subscribed URI.
|
|
287
|
+
|
|
288
|
+
Called after a mutating tool pushes state back to the collab server,
|
|
289
|
+
so the MCP client knows to re-read any resources it is watching.
|
|
290
|
+
|
|
291
|
+
This must run inside a request context (i.e. while a tool handler is
|
|
292
|
+
executing) so we can access the session.
|
|
293
|
+
"""
|
|
294
|
+
if not _subscribed_uris:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
session = mcp._mcp_server.request_context.session
|
|
299
|
+
except LookupError:
|
|
300
|
+
# No active request context -- nothing we can do.
|
|
301
|
+
logger.debug("No request context; skipping resource notifications")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
for uri_str in _subscribed_uris:
|
|
305
|
+
try:
|
|
306
|
+
uri = AnyUrl(uri_str)
|
|
307
|
+
await session.send_resource_updated(uri=uri)
|
|
308
|
+
logger.debug("Sent resource/updated for %s", uri_str)
|
|
309
|
+
except Exception:
|
|
310
|
+
logger.warning(
|
|
311
|
+
"Failed to send resource/updated for %s", uri_str, exc_info=True
|
|
312
|
+
)
|