From 83ec950df782ac3b51c77fb6a5cc00dca8e713a9 Mon Sep 17 00:00:00 2001 From: Gregory Gauthier Date: Wed, 8 Apr 2026 12:11:04 +0100 Subject: [PATCH] first commit --- .gitignore | 4 + .mcp.json | 28 + README.md | 70 + config/claude_desktop_config.json | 27 + .../.continue/rules/xai-model-fix.md | 6 + mcps/dicom_mcp/.gitignore | 7 + mcps/dicom_mcp/.mcp.json | 7 + mcps/dicom_mcp/CLAUDE.md | 152 + mcps/dicom_mcp/INSTALL.md | 389 + mcps/dicom_mcp/README.md | 254 + mcps/dicom_mcp/bitbucket-pipelines.yml | 84 + mcps/dicom_mcp/build_standalone.sh | 160 + mcps/dicom_mcp/build_standalone_linux.sh | 163 + mcps/dicom_mcp/dicom_mcp.py | 11 + mcps/dicom_mcp/dicom_mcp/__init__.py | 81 + mcps/dicom_mcp/dicom_mcp/__main__.py | 5 + mcps/dicom_mcp/dicom_mcp/config.py | 13 + mcps/dicom_mcp/dicom_mcp/constants.py | 96 + mcps/dicom_mcp/dicom_mcp/helpers/__init__.py | 61 + mcps/dicom_mcp/dicom_mcp/helpers/files.py | 62 + mcps/dicom_mcp/dicom_mcp/helpers/filters.py | 120 + mcps/dicom_mcp/dicom_mcp/helpers/philips.py | 129 + mcps/dicom_mcp/dicom_mcp/helpers/pixels.py | 81 + mcps/dicom_mcp/dicom_mcp/helpers/sequence.py | 114 + mcps/dicom_mcp/dicom_mcp/helpers/tags.py | 97 + mcps/dicom_mcp/dicom_mcp/helpers/tree.py | 162 + mcps/dicom_mcp/dicom_mcp/pii.py | 32 + mcps/dicom_mcp/dicom_mcp/server.py | 55 + mcps/dicom_mcp/dicom_mcp/tools/__init__.py | 17 + mcps/dicom_mcp/dicom_mcp/tools/discovery.py | 291 + mcps/dicom_mcp/dicom_mcp/tools/metadata.py | 260 + mcps/dicom_mcp/dicom_mcp/tools/philips.py | 184 + mcps/dicom_mcp/dicom_mcp/tools/pixels.py | 511 + mcps/dicom_mcp/dicom_mcp/tools/query.py | 421 + mcps/dicom_mcp/dicom_mcp/tools/search.py | 249 + .../dicom_mcp/dicom_mcp/tools/segmentation.py | 235 + mcps/dicom_mcp/dicom_mcp/tools/ti_analysis.py | 310 + mcps/dicom_mcp/dicom_mcp/tools/tree.py | 84 + .../dicom_mcp/tools/uid_comparison.py | 149 + mcps/dicom_mcp/dicom_mcp/tools/validation.py | 254 + mcps/dicom_mcp/docs/CAPABILITIES.md | 25 + mcps/dicom_mcp/docs/GUIDELINES.md | 51 + mcps/dicom_mcp/docs/TODO.md | 95 + mcps/dicom_mcp/docs/USAGE.md | 664 + mcps/dicom_mcp/img/claude_desktop_example.png | Bin 0 -> 158974 bytes mcps/dicom_mcp/img/mcp_servers_panel.png | Bin 0 -> 66292 bytes mcps/dicom_mcp/img/profile_menu.png | Bin 0 -> 28030 bytes .../img/settings_developer_option.png | Bin 0 -> 26280 bytes mcps/dicom_mcp/install.sh | 72 + mcps/dicom_mcp/pyproject.toml | 29 + mcps/dicom_mcp/run_dicom_mcp_server.sh | 15 + mcps/dicom_mcp/test_dicom_mcp.py | 1883 ++ mcps/filesystem_mcp/install.sh | 26 + mcps/filesystem_mcp/launch_filesystem_mcp.sh | 19 + mcps/filesystem_mcp/mcp.json | 12 + mcps/open_meteo_mcp/README.md | 60 + .../open_meteo_mcp_server.cpython-312.pyc | Bin 0 -> 10693 bytes mcps/open_meteo_mcp/open_meteo_mcp_server.py | 242 + mcps/open_meteo_mcp/poetry.lock | 941 + mcps/open_meteo_mcp/pyproject.toml | 21 + .../run_open_meteo_mcp_server.sh | 14 + mcps/playwright_mcp/README.md | 4 + mcps/playwright_mcp/launch_playwright_mcp.sh | 26 + .../node_modules/.bin/playwright | 1 + .../node_modules/.bin/playwright-core | 1 + .../node_modules/.bin/playwright-mcp | 1 + .../node_modules/.package-lock.json | 114 + .../node_modules/@playwright/mcp/LICENSE | 201 + .../node_modules/@playwright/mcp/README.md | 1455 + .../node_modules/@playwright/mcp/cli.js | 32 + .../node_modules/@playwright/mcp/config.d.ts | 230 + .../node_modules/@playwright/mcp/index.d.ts | 23 + .../node_modules/@playwright/mcp/index.js | 19 + .../node_modules/@playwright/mcp/package.json | 42 + .../node_modules/@playwright/test/LICENSE | 202 + .../node_modules/@playwright/test/NOTICE | 5 + .../node_modules/@playwright/test/README.md | 170 + .../node_modules/@playwright/test/cli.js | 19 + .../node_modules/@playwright/test/index.d.ts | 18 + .../node_modules/@playwright/test/index.js | 17 + .../node_modules/@playwright/test/index.mjs | 18 + .../test/node_modules/.bin/playwright | 1 + .../test/node_modules/.bin/playwright-core | 1 + .../test/node_modules/playwright-core/LICENSE | 202 + .../test/node_modules/playwright-core/NOTICE | 5 + .../node_modules/playwright-core/README.md | 3 + .../playwright-core/ThirdPartyNotices.txt | 3552 +++ .../bin/install_media_pack.ps1 | 5 + .../bin/install_webkit_wsl.ps1 | 33 + .../bin/reinstall_chrome_beta_linux.sh | 42 + .../bin/reinstall_chrome_beta_mac.sh | 13 + .../bin/reinstall_chrome_beta_win.ps1 | 24 + .../bin/reinstall_chrome_stable_linux.sh | 42 + .../bin/reinstall_chrome_stable_mac.sh | 12 + .../bin/reinstall_chrome_stable_win.ps1 | 24 + .../bin/reinstall_msedge_beta_linux.sh | 48 + .../bin/reinstall_msedge_beta_mac.sh | 11 + .../bin/reinstall_msedge_beta_win.ps1 | 23 + .../bin/reinstall_msedge_dev_linux.sh | 48 + .../bin/reinstall_msedge_dev_mac.sh | 11 + .../bin/reinstall_msedge_dev_win.ps1 | 23 + .../bin/reinstall_msedge_stable_linux.sh | 48 + .../bin/reinstall_msedge_stable_mac.sh | 11 + .../bin/reinstall_msedge_stable_win.ps1 | 24 + .../playwright-core/browsers.json | 81 + .../test/node_modules/playwright-core/cli.js | 18 + .../node_modules/playwright-core/index.d.ts | 17 + .../node_modules/playwright-core/index.js | 32 + .../node_modules/playwright-core/index.mjs | 28 + .../playwright-core/lib/androidServerImpl.js | 65 + .../playwright-core/lib/bootstrap.js | 77 + .../playwright-core/lib/browserServerImpl.js | 120 + .../playwright-core/lib/cli/browserActions.js | 308 + .../playwright-core/lib/cli/driver.js | 98 + .../playwright-core/lib/cli/installActions.js | 171 + .../playwright-core/lib/cli/program.js | 225 + .../lib/cli/programWithTestStub.js | 74 + .../playwright-core/lib/client/android.js | 361 + .../playwright-core/lib/client/api.js | 137 + .../playwright-core/lib/client/artifact.js | 79 + .../playwright-core/lib/client/browser.js | 169 + .../lib/client/browserContext.js | 563 + .../playwright-core/lib/client/browserType.js | 153 + .../playwright-core/lib/client/cdpSession.js | 55 + .../lib/client/channelOwner.js | 194 + .../lib/client/clientHelper.js | 64 + .../lib/client/clientInstrumentation.js | 55 + .../lib/client/clientStackTrace.js | 69 + .../playwright-core/lib/client/clock.js | 68 + .../playwright-core/lib/client/connect.js | 143 + .../playwright-core/lib/client/connection.js | 322 + .../lib/client/consoleMessage.js | 61 + .../playwright-core/lib/client/coverage.js | 44 + .../playwright-core/lib/client/debugger.js | 57 + .../playwright-core/lib/client/dialog.js | 63 + .../playwright-core/lib/client/disposable.js | 76 + .../playwright-core/lib/client/download.js | 62 + .../playwright-core/lib/client/electron.js | 139 + .../lib/client/elementHandle.js | 281 + .../playwright-core/lib/client/errors.js | 77 + .../lib/client/eventEmitter.js | 314 + .../playwright-core/lib/client/events.js | 103 + .../playwright-core/lib/client/fetch.js | 367 + .../playwright-core/lib/client/fileChooser.js | 46 + .../playwright-core/lib/client/fileUtils.js | 34 + .../playwright-core/lib/client/frame.js | 404 + .../playwright-core/lib/client/harRouter.js | 99 + .../playwright-core/lib/client/input.js | 84 + .../playwright-core/lib/client/jsHandle.js | 105 + .../playwright-core/lib/client/jsonPipe.js | 39 + .../playwright-core/lib/client/localUtils.js | 60 + .../playwright-core/lib/client/locator.js | 367 + .../playwright-core/lib/client/network.js | 750 + .../playwright-core/lib/client/page.js | 731 + .../playwright-core/lib/client/platform.js | 74 + .../playwright-core/lib/client/playwright.js | 71 + .../playwright-core/lib/client/screencast.js | 88 + .../playwright-core/lib/client/selectors.js | 57 + .../playwright-core/lib/client/stream.js | 39 + .../lib/client/timeoutSettings.js | 79 + .../playwright-core/lib/client/tracing.js | 126 + .../playwright-core/lib/client/types.js | 28 + .../playwright-core/lib/client/video.js | 52 + .../playwright-core/lib/client/waiter.js | 142 + .../playwright-core/lib/client/webError.js | 39 + .../playwright-core/lib/client/worker.js | 85 + .../lib/client/writableStream.js | 39 + .../lib/generated/bindingsControllerSource.js | 28 + .../lib/generated/clockSource.js | 28 + .../lib/generated/injectedScriptSource.js | 28 + .../lib/generated/pollingRecorderSource.js | 28 + .../lib/generated/storageScriptSource.js | 28 + .../lib/generated/utilityScriptSource.js | 28 + .../lib/generated/webSocketMockSource.js | 336 + .../playwright-core/lib/inProcessFactory.js | 60 + .../playwright-core/lib/inprocess.js | 3 + .../playwright-core/lib/mcpBundle.js | 78 + .../playwright-core/lib/mcpBundleImpl.js | 91 + .../playwright-core/lib/outofprocess.js | 76 + .../lib/protocol/serializers.js | 197 + .../playwright-core/lib/protocol/validator.js | 3067 ++ .../lib/protocol/validatorPrimitives.js | 193 + .../lib/remote/playwrightConnection.js | 131 + .../lib/remote/playwrightPipeServer.js | 100 + .../lib/remote/playwrightServer.js | 339 + .../lib/remote/playwrightWebSocketServer.js | 73 + .../lib/remote/serverTransport.js | 96 + .../lib/server/android/android.js | 465 + .../lib/server/android/backendAdb.js | 177 + .../playwright-core/lib/server/artifact.js | 127 + .../lib/server/bidi/bidiBrowser.js | 571 + .../lib/server/bidi/bidiChromium.js | 162 + .../lib/server/bidi/bidiConnection.js | 213 + .../lib/server/bidi/bidiDeserializer.js | 116 + .../lib/server/bidi/bidiExecutionContext.js | 267 + .../lib/server/bidi/bidiFirefox.js | 128 + .../lib/server/bidi/bidiInput.js | 146 + .../lib/server/bidi/bidiNetworkManager.js | 411 + .../lib/server/bidi/bidiOverCdp.js | 102 + .../lib/server/bidi/bidiPage.js | 599 + .../lib/server/bidi/bidiPdf.js | 106 + .../server/bidi/third_party/bidiCommands.d.js | 22 + .../server/bidi/third_party/bidiKeyboard.js | 256 + .../server/bidi/third_party/bidiProtocol.js | 24 + .../bidi/third_party/bidiProtocolCore.js | 180 + .../third_party/bidiProtocolPermissions.js | 42 + .../server/bidi/third_party/bidiSerializer.js | 148 + .../server/bidi/third_party/firefoxPrefs.js | 261 + .../playwright-core/lib/server/browser.js | 212 + .../lib/server/browserContext.js | 741 + .../playwright-core/lib/server/browserType.js | 338 + .../playwright-core/lib/server/callLog.js | 82 + .../lib/server/chromium/appIcon.png | Bin 0 -> 16565 bytes .../lib/server/chromium/chromium.js | 399 + .../lib/server/chromium/chromiumSwitches.js | 104 + .../lib/server/chromium/crBrowser.js | 528 + .../lib/server/chromium/crConnection.js | 197 + .../lib/server/chromium/crCoverage.js | 235 + .../lib/server/chromium/crDevTools.js | 111 + .../lib/server/chromium/crDragDrop.js | 131 + .../lib/server/chromium/crExecutionContext.js | 146 + .../lib/server/chromium/crInput.js | 187 + .../lib/server/chromium/crNetworkManager.js | 707 + .../lib/server/chromium/crPage.js | 963 + .../lib/server/chromium/crPdf.js | 121 + .../lib/server/chromium/crProtocolHelper.js | 145 + .../lib/server/chromium/crServiceWorker.js | 137 + .../server/chromium/defaultFontFamilies.js | 162 + .../lib/server/chromium/protocol.d.js | 16 + .../playwright-core/lib/server/clock.js | 149 + .../lib/server/codegen/csharp.js | 327 + .../lib/server/codegen/java.js | 274 + .../lib/server/codegen/javascript.js | 247 + .../lib/server/codegen/jsonl.js | 52 + .../lib/server/codegen/language.js | 132 + .../lib/server/codegen/languages.js | 68 + .../lib/server/codegen/python.js | 279 + .../lib/server/codegen/types.js | 16 + .../playwright-core/lib/server/console.js | 61 + .../playwright-core/lib/server/cookieStore.js | 206 + .../lib/server/debugController.js | 197 + .../playwright-core/lib/server/debugger.js | 112 + .../lib/server/deviceDescriptors.js | 39 + .../lib/server/deviceDescriptorsSource.json | 1779 ++ .../playwright-core/lib/server/dialog.js | 116 + .../server/dispatchers/androidDispatcher.js | 325 + .../server/dispatchers/artifactDispatcher.js | 118 + .../dispatchers/browserContextDispatcher.js | 381 + .../server/dispatchers/browserDispatcher.js | 124 + .../dispatchers/browserTypeDispatcher.js | 71 + .../dispatchers/cdpSessionDispatcher.js | 47 + .../dispatchers/debugControllerDispatcher.js | 78 + .../server/dispatchers/debuggerDispatcher.js | 84 + .../server/dispatchers/dialogDispatcher.js | 47 + .../lib/server/dispatchers/dispatcher.js | 364 + .../dispatchers/disposableDispatcher.js | 39 + .../server/dispatchers/electronDispatcher.js | 90 + .../dispatchers/elementHandlerDispatcher.js | 181 + .../lib/server/dispatchers/frameDispatcher.js | 227 + .../server/dispatchers/jsHandleDispatcher.js | 85 + .../server/dispatchers/jsonPipeDispatcher.js | 58 + .../dispatchers/localUtilsDispatcher.js | 185 + .../server/dispatchers/networkDispatchers.js | 214 + .../lib/server/dispatchers/pageDispatcher.js | 456 + .../dispatchers/playwrightDispatcher.js | 108 + .../server/dispatchers/streamDispatcher.js | 67 + .../server/dispatchers/tracingDispatcher.js | 68 + .../dispatchers/webSocketRouteDispatcher.js | 164 + .../dispatchers/writableStreamDispatcher.js | 79 + .../playwright-core/lib/server/disposable.js | 41 + .../playwright-core/lib/server/dom.js | 833 + .../playwright-core/lib/server/download.js | 71 + .../lib/server/electron/electron.js | 278 + .../lib/server/electron/loader.js | 29 + .../playwright-core/lib/server/errors.js | 69 + .../playwright-core/lib/server/fetch.js | 621 + .../playwright-core/lib/server/fileChooser.js | 43 + .../lib/server/fileUploadUtils.js | 84 + .../lib/server/firefox/ffBrowser.js | 408 + .../lib/server/firefox/ffConnection.js | 142 + .../lib/server/firefox/ffExecutionContext.js | 150 + .../lib/server/firefox/ffInput.js | 175 + .../lib/server/firefox/ffNetworkManager.js | 256 + .../lib/server/firefox/ffPage.js | 494 + .../lib/server/firefox/firefox.js | 114 + .../lib/server/firefox/protocol.d.js | 16 + .../playwright-core/lib/server/formData.js | 147 + .../lib/server/frameSelectors.js | 160 + .../playwright-core/lib/server/frames.js | 1500 + .../lib/server/har/harRecorder.js | 147 + .../lib/server/har/harTracer.js | 608 + .../playwright-core/lib/server/harBackend.js | 157 + .../playwright-core/lib/server/helper.js | 96 + .../playwright-core/lib/server/index.js | 58 + .../playwright-core/lib/server/input.js | 322 + .../lib/server/instrumentation.js | 77 + .../playwright-core/lib/server/javascript.js | 291 + .../playwright-core/lib/server/launchApp.js | 127 + .../playwright-core/lib/server/localUtils.js | 214 + .../lib/server/macEditingCommands.js | 143 + .../playwright-core/lib/server/network.js | 668 + .../playwright-core/lib/server/overlay.js | 138 + .../playwright-core/lib/server/page.js | 890 + .../lib/server/pipeTransport.js | 89 + .../playwright-core/lib/server/playwright.js | 69 + .../playwright-core/lib/server/progress.js | 138 + .../lib/server/protocolError.js | 52 + .../playwright-core/lib/server/recorder.js | 535 + .../lib/server/recorder/chat.js | 161 + .../lib/server/recorder/recorderApp.js | 367 + .../lib/server/recorder/recorderRunner.js | 138 + .../recorder/recorderSignalProcessor.js | 83 + .../lib/server/recorder/recorderUtils.js | 157 + .../lib/server/recorder/throttledFile.js | 57 + .../lib/server/registry/browserFetcher.js | 177 + .../lib/server/registry/dependencies.js | 371 + .../lib/server/registry/index.js | 1395 + .../lib/server/registry/nativeDeps.js | 1281 + .../server/registry/oopDownloadBrowserMain.js | 127 + .../playwright-core/lib/server/screencast.js | 137 + .../lib/server/screenshotter.js | 333 + .../playwright-core/lib/server/selectors.js | 112 + .../socksClientCertificatesInterceptor.js | 383 + .../lib/server/socksInterceptor.js | 95 + .../lib/server/trace/recorder/snapshotter.js | 147 + .../trace/recorder/snapshotterInjected.js | 561 + .../lib/server/trace/recorder/tracing.js | 655 + .../lib/server/trace/viewer/traceViewer.js | 244 + .../playwright-core/lib/server/transport.js | 181 + .../playwright-core/lib/server/types.js | 28 + .../lib/server/usKeyboardLayout.js | 152 + .../playwright-core/lib/server/utils/ascii.js | 44 + .../lib/server/utils/comparators.js | 139 + .../lib/server/utils/crypto.js | 216 + .../playwright-core/lib/server/utils/debug.js | 42 + .../lib/server/utils/debugLogger.js | 122 + .../lib/server/utils/disposable.js | 32 + .../playwright-core/lib/server/utils/env.js | 73 + .../lib/server/utils/eventsHelper.js | 41 + .../lib/server/utils/expectUtils.js | 123 + .../lib/server/utils/fileUtils.js | 205 + .../lib/server/utils/happyEyeballs.js | 210 + .../lib/server/utils/hostPlatform.js | 123 + .../lib/server/utils/httpServer.js | 205 + .../server/utils/image_tools/colorUtils.js | 89 + .../lib/server/utils/image_tools/compare.js | 109 + .../server/utils/image_tools/imageChannel.js | 78 + .../lib/server/utils/image_tools/stats.js | 102 + .../lib/server/utils/linuxUtils.js | 71 + .../lib/server/utils/network.js | 243 + .../lib/server/utils/nodePlatform.js | 148 + .../lib/server/utils/pipeTransport.js | 84 + .../lib/server/utils/processLauncher.js | 242 + .../lib/server/utils/profiler.js | 65 + .../lib/server/utils/socksProxy.js | 511 + .../lib/server/utils/spawnAsync.js | 41 + .../playwright-core/lib/server/utils/task.js | 51 + .../lib/server/utils/userAgent.js | 98 + .../lib/server/utils/wsServer.js | 121 + .../lib/server/utils/zipFile.js | 74 + .../playwright-core/lib/server/utils/zones.js | 57 + .../lib/server/videoRecorder.js | 194 + .../lib/server/webkit/protocol.d.js | 16 + .../lib/server/webkit/webkit.js | 108 + .../lib/server/webkit/wkBrowser.js | 330 + .../lib/server/webkit/wkConnection.js | 144 + .../lib/server/webkit/wkExecutionContext.js | 154 + .../lib/server/webkit/wkInput.js | 181 + .../server/webkit/wkInterceptableRequest.js | 197 + .../lib/server/webkit/wkPage.js | 1161 + .../lib/server/webkit/wkProvisionalPage.js | 83 + .../lib/server/webkit/wkWorkers.js | 106 + .../playwright-core/lib/serverRegistry.js | 156 + .../lib/third_party/pixelmatch.js | 255 + .../lib/tools/backend/browserBackend.js | 79 + .../lib/tools/backend/common.js | 63 + .../lib/tools/backend/config.js | 41 + .../lib/tools/backend/console.js | 66 + .../lib/tools/backend/context.js | 296 + .../lib/tools/backend/cookies.js | 152 + .../lib/tools/backend/devtools.js | 69 + .../lib/tools/backend/dialogs.js | 59 + .../lib/tools/backend/evaluate.js | 64 + .../lib/tools/backend/files.js | 60 + .../playwright-core/lib/tools/backend/form.js | 64 + .../lib/tools/backend/keyboard.js | 155 + .../lib/tools/backend/logFile.js | 95 + .../lib/tools/backend/mouse.js | 168 + .../lib/tools/backend/navigate.js | 106 + .../lib/tools/backend/network.js | 135 + .../playwright-core/lib/tools/backend/pdf.js | 48 + .../lib/tools/backend/response.js | 305 + .../lib/tools/backend/route.js | 140 + .../lib/tools/backend/runCode.js | 77 + .../lib/tools/backend/screenshot.js | 88 + .../lib/tools/backend/sessionLog.js | 74 + .../lib/tools/backend/snapshot.js | 208 + .../lib/tools/backend/storage.js | 68 + .../playwright-core/lib/tools/backend/tab.js | 445 + .../playwright-core/lib/tools/backend/tabs.js | 67 + .../playwright-core/lib/tools/backend/tool.js | 47 + .../lib/tools/backend/tools.js | 102 + .../lib/tools/backend/tracing.js | 78 + .../lib/tools/backend/utils.js | 83 + .../lib/tools/backend/verify.js | 151 + .../lib/tools/backend/video.js | 98 + .../playwright-core/lib/tools/backend/wait.js | 63 + .../lib/tools/backend/webstorage.js | 223 + .../lib/tools/cli-client/cli.js | 6 + .../lib/tools/cli-client/help.json | 399 + .../lib/tools/cli-client/minimist.js | 128 + .../lib/tools/cli-client/program.js | 350 + .../lib/tools/cli-client/registry.js | 176 + .../lib/tools/cli-client/session.js | 289 + .../lib/tools/cli-client/skill/SKILL.md | 328 + .../skill/references/element-attributes.md | 23 + .../skill/references/playwright-tests.md | 39 + .../skill/references/request-mocking.md | 87 + .../skill/references/running-code.md | 231 + .../skill/references/session-management.md | 169 + .../skill/references/storage-state.md | 275 + .../skill/references/test-generation.md | 88 + .../cli-client/skill/references/tracing.md | 139 + .../skill/references/video-recording.md | 143 + .../lib/tools/cli-daemon/command.js | 73 + .../lib/tools/cli-daemon/commands.js | 956 + .../lib/tools/cli-daemon/daemon.js | 157 + .../lib/tools/cli-daemon/helpGenerator.js | 177 + .../lib/tools/cli-daemon/program.js | 129 + .../lib/tools/dashboard/appIcon.png | Bin 0 -> 16565 bytes .../lib/tools/dashboard/dashboardApp.js | 284 + .../tools/dashboard/dashboardController.js | 296 + .../playwright-core/lib/tools/exports.js | 60 + .../lib/tools/mcp/browserFactory.js | 233 + .../playwright-core/lib/tools/mcp/cdpRelay.js | 352 + .../playwright-core/lib/tools/mcp/cli-stub.js | 7 + .../playwright-core/lib/tools/mcp/config.d.js | 16 + .../playwright-core/lib/tools/mcp/config.js | 446 + .../lib/tools/mcp/configIni.js | 189 + .../lib/tools/mcp/extensionContextFactory.js | 55 + .../playwright-core/lib/tools/mcp/index.js | 62 + .../playwright-core/lib/tools/mcp/log.js | 35 + .../playwright-core/lib/tools/mcp/program.js | 107 + .../playwright-core/lib/tools/mcp/protocol.js | 28 + .../playwright-core/lib/tools/mcp/watchdog.js | 44 + .../playwright-core/lib/tools/trace/SKILL.md | 171 + .../lib/tools/trace/installSkill.js | 48 + .../lib/tools/trace/traceActions.js | 142 + .../lib/tools/trace/traceAttachments.js | 69 + .../lib/tools/trace/traceCli.js | 87 + .../lib/tools/trace/traceConsole.js | 97 + .../lib/tools/trace/traceErrors.js | 55 + .../lib/tools/trace/traceOpen.js | 69 + .../lib/tools/trace/traceParser.js | 96 + .../lib/tools/trace/traceRequests.js | 182 + .../lib/tools/trace/traceScreenshot.js | 68 + .../lib/tools/trace/traceSnapshot.js | 149 + .../lib/tools/trace/traceUtils.js | 153 + .../lib/tools/utils/connect.js | 32 + .../lib/tools/utils/mcp/http.js | 152 + .../lib/tools/utils/mcp/server.js | 230 + .../lib/tools/utils/mcp/tool.js | 47 + .../lib/tools/utils/socketConnection.js | 108 + .../node_modules/playwright-core/lib/utils.js | 115 + .../lib/utils/isomorphic/ariaSnapshot.js | 455 + .../lib/utils/isomorphic/assert.js | 31 + .../lib/utils/isomorphic/colors.js | 72 + .../lib/utils/isomorphic/cssParser.js | 245 + .../lib/utils/isomorphic/cssTokenizer.js | 1051 + .../lib/utils/isomorphic/formatUtils.js | 64 + .../lib/utils/isomorphic/headers.js | 53 + .../lib/utils/isomorphic/imageUtils.js | 141 + .../lib/utils/isomorphic/jsonSchema.js | 89 + .../lib/utils/isomorphic/locatorGenerators.js | 689 + .../lib/utils/isomorphic/locatorParser.js | 176 + .../lib/utils/isomorphic/locatorUtils.js | 81 + .../lib/utils/isomorphic/lruCache.js | 51 + .../lib/utils/isomorphic/manualPromise.js | 114 + .../lib/utils/isomorphic/mimeType.js | 464 + .../lib/utils/isomorphic/multimap.js | 80 + .../lib/utils/isomorphic/protocolFormatter.js | 81 + .../lib/utils/isomorphic/protocolMetainfo.js | 351 + .../lib/utils/isomorphic/rtti.js | 43 + .../lib/utils/isomorphic/selectorParser.js | 386 + .../lib/utils/isomorphic/semaphore.js | 54 + .../lib/utils/isomorphic/stackTrace.js | 158 + .../lib/utils/isomorphic/stringUtils.js | 204 + .../lib/utils/isomorphic/time.js | 49 + .../lib/utils/isomorphic/timeoutRunner.js | 66 + .../lib/utils/isomorphic/trace/entries.js | 16 + .../isomorphic/trace/snapshotRenderer.js | 492 + .../utils/isomorphic/trace/snapshotServer.js | 120 + .../utils/isomorphic/trace/snapshotStorage.js | 89 + .../lib/utils/isomorphic/trace/traceLoader.js | 132 + .../lib/utils/isomorphic/trace/traceModel.js | 366 + .../utils/isomorphic/trace/traceModernizer.js | 401 + .../lib/utils/isomorphic/trace/traceUtils.js | 58 + .../isomorphic/trace/versions/traceV3.js | 16 + .../isomorphic/trace/versions/traceV4.js | 16 + .../isomorphic/trace/versions/traceV5.js | 16 + .../isomorphic/trace/versions/traceV6.js | 16 + .../isomorphic/trace/versions/traceV7.js | 16 + .../isomorphic/trace/versions/traceV8.js | 16 + .../lib/utils/isomorphic/types.js | 16 + .../lib/utils/isomorphic/urlMatch.js | 243 + .../isomorphic/utilityScriptSerializers.js | 262 + .../lib/utils/isomorphic/yaml.js | 84 + .../playwright-core/lib/utilsBundle.js | 91 + .../lib/utilsBundleImpl/index.js | 217 + .../lib/utilsBundleImpl/xdg-open | 1066 + .../vite/dashboard/assets/index-BAOybkp8.js | 50 + .../vite/dashboard/assets/index-CZAYOG76.css | 1 + .../lib/vite/dashboard/index.html | 28 + .../lib/vite/htmlReport/index.html | 16 + .../lib/vite/htmlReport/report.css | 1 + .../lib/vite/htmlReport/report.js | 72 + .../assets/codeMirrorModule-C8KMvO9L.js | 32 + .../assets/codeMirrorModule-DYBRYzYX.css | 1 + .../vite/recorder/assets/codicon-DCmgc-ay.ttf | Bin 0 -> 80340 bytes .../vite/recorder/assets/index-BSjZa4pk.css | 1 + .../vite/recorder/assets/index-CqAYX1I3.js | 193 + .../lib/vite/recorder/index.html | 29 + .../lib/vite/recorder/playwright-logo.svg | 9 + .../assets/codeMirrorModule-DS0FLvoc.js | 32 + .../assets/defaultSettingsView-GTWI-W_B.js | 262 + .../assets/xtermModule-CsJ4vdCR.js | 9 + .../traceViewer/codeMirrorModule.DYBRYzYX.css | 1 + .../lib/vite/traceViewer/codicon.DCmgc-ay.ttf | Bin 0 -> 80340 bytes .../defaultSettingsView.B4dS75f0.css | 1 + .../lib/vite/traceViewer/index.C5466mMT.js | 2 + .../lib/vite/traceViewer/index.CzXZzn5A.css | 1 + .../lib/vite/traceViewer/index.html | 43 + .../lib/vite/traceViewer/manifest.webmanifest | 16 + .../lib/vite/traceViewer/playwright-logo.svg | 9 + .../lib/vite/traceViewer/snapshot.html | 21 + .../lib/vite/traceViewer/sw.bundle.js | 5 + .../lib/vite/traceViewer/uiMode.Btcz36p_.css | 1 + .../lib/vite/traceViewer/uiMode.Vipi55dB.js | 6 + .../lib/vite/traceViewer/uiMode.html | 17 + .../vite/traceViewer/xtermModule.DYP7pi_n.css | 32 + .../playwright-core/lib/zipBundle.js | 34 + .../playwright-core/lib/zipBundleImpl.js | 5 + .../playwright-core/lib/zodBundle.js | 39 + .../playwright-core/lib/zodBundleImpl.js | 40 + .../node_modules/playwright-core/package.json | 48 + .../playwright-core/types/protocol.d.ts | 24720 ++++++++++++++++ .../playwright-core/types/structs.d.ts | 45 + .../playwright-core/types/types.d.ts | 23623 +++++++++++++++ .../test/node_modules/playwright/LICENSE | 202 + .../test/node_modules/playwright/NOTICE | 5 + .../test/node_modules/playwright/README.md | 170 + .../playwright/ThirdPartyNotices.txt | 3919 +++ .../test/node_modules/playwright/cli.js | 19 + .../test/node_modules/playwright/index.d.ts | 17 + .../test/node_modules/playwright/index.js | 17 + .../test/node_modules/playwright/index.mjs | 18 + .../node_modules/playwright/jsx-runtime.js | 42 + .../node_modules/playwright/jsx-runtime.mjs | 21 + .../playwright/lib/agents/agentParser.js | 89 + .../lib/agents/copilot-setup-steps.yml | 34 + .../playwright/lib/agents/generateAgents.js | 346 + .../agents/playwright-test-coverage.prompt.md | 31 + .../agents/playwright-test-generate.prompt.md | 8 + .../agents/playwright-test-generator.agent.md | 88 + .../lib/agents/playwright-test-heal.prompt.md | 6 + .../agents/playwright-test-healer.agent.md | 55 + .../lib/agents/playwright-test-plan.prompt.md | 9 + .../agents/playwright-test-planner.agent.md | 73 + .../playwright/lib/common/config.js | 281 + .../playwright/lib/common/configLoader.js | 344 + .../playwright/lib/common/esmLoaderHost.js | 104 + .../playwright/lib/common/expectBundle.js | 28 + .../playwright/lib/common/expectBundleImpl.js | 407 + .../playwright/lib/common/fixtures.js | 302 + .../playwright/lib/common/globals.js | 58 + .../node_modules/playwright/lib/common/ipc.js | 60 + .../playwright/lib/common/poolBuilder.js | 85 + .../playwright/lib/common/process.js | 133 + .../playwright/lib/common/suiteUtils.js | 140 + .../playwright/lib/common/test.js | 330 + .../playwright/lib/common/testLoader.js | 101 + .../playwright/lib/common/testType.js | 301 + .../playwright/lib/common/validators.js | 68 + .../playwright/lib/errorContext.js | 121 + .../node_modules/playwright/lib/fsWatcher.js | 67 + .../test/node_modules/playwright/lib/index.js | 764 + .../playwright/lib/internalsForTest.js | 42 + .../playwright/lib/isomorphic/events.js | 77 + .../playwright/lib/isomorphic/folders.js | 30 + .../lib/isomorphic/stringInternPool.js | 69 + .../playwright/lib/isomorphic/teleReceiver.js | 538 + .../lib/isomorphic/teleSuiteUpdater.js | 157 + .../lib/isomorphic/testServerConnection.js | 223 + .../lib/isomorphic/testServerInterface.js | 16 + .../playwright/lib/isomorphic/testTree.js | 329 + .../playwright/lib/isomorphic/types.d.js | 16 + .../playwright/lib/loader/loaderMain.js | 59 + .../playwright/lib/matchers/expect.js | 311 + .../playwright/lib/matchers/matcherHint.js | 44 + .../playwright/lib/matchers/matchers.js | 385 + .../playwright/lib/matchers/toBeTruthy.js | 75 + .../playwright/lib/matchers/toEqual.js | 100 + .../playwright/lib/matchers/toHaveURL.js | 101 + .../lib/matchers/toMatchAriaSnapshot.js | 163 + .../lib/matchers/toMatchSnapshot.js | 349 + .../playwright/lib/matchers/toMatchText.js | 99 + .../playwright/lib/mcp/test/browserBackend.js | 121 + .../playwright/lib/mcp/test/generatorTools.js | 122 + .../playwright/lib/mcp/test/plannerTools.js | 145 + .../playwright/lib/mcp/test/seed.js | 82 + .../playwright/lib/mcp/test/streams.js | 44 + .../playwright/lib/mcp/test/testBackend.js | 99 + .../playwright/lib/mcp/test/testContext.js | 283 + .../playwright/lib/mcp/test/testTool.js | 30 + .../playwright/lib/mcp/test/testTools.js | 108 + .../lib/plugins/gitCommitInfoPlugin.js | 198 + .../playwright/lib/plugins/index.js | 28 + .../playwright/lib/plugins/webServerPlugin.js | 238 + .../node_modules/playwright/lib/program.js | 239 + .../playwright/lib/reportActions.js | 80 + .../playwright/lib/reporters/base.js | 633 + .../playwright/lib/reporters/blob.js | 138 + .../playwright/lib/reporters/dot.js | 99 + .../playwright/lib/reporters/empty.js | 32 + .../playwright/lib/reporters/github.js | 127 + .../playwright/lib/reporters/html.js | 666 + .../lib/reporters/internalReporter.js | 138 + .../playwright/lib/reporters/json.js | 254 + .../playwright/lib/reporters/junit.js | 321 + .../playwright/lib/reporters/line.js | 131 + .../playwright/lib/reporters/list.js | 252 + .../lib/reporters/listModeReporter.js | 69 + .../playwright/lib/reporters/markdown.js | 144 + .../playwright/lib/reporters/merge.js | 579 + .../playwright/lib/reporters/multiplexer.js | 116 + .../playwright/lib/reporters/reporterV2.js | 102 + .../playwright/lib/reporters/teleEmitter.js | 319 + .../lib/reporters/versions/blobV1.js | 16 + .../playwright/lib/runner/dispatcher.js | 522 + .../playwright/lib/runner/failureTracker.js | 72 + .../playwright/lib/runner/lastRun.js | 77 + .../playwright/lib/runner/loadUtils.js | 340 + .../playwright/lib/runner/loaderHost.js | 89 + .../playwright/lib/runner/processHost.js | 180 + .../playwright/lib/runner/projectUtils.js | 241 + .../playwright/lib/runner/rebase.js | 189 + .../playwright/lib/runner/reporters.js | 143 + .../playwright/lib/runner/sigIntWatcher.js | 96 + .../playwright/lib/runner/taskRunner.js | 127 + .../playwright/lib/runner/tasks.js | 413 + .../playwright/lib/runner/testGroups.js | 125 + .../playwright/lib/runner/testRunner.js | 398 + .../playwright/lib/runner/testServer.js | 269 + .../playwright/lib/runner/uiModeReporter.js | 30 + .../node_modules/playwright/lib/runner/vcs.js | 72 + .../playwright/lib/runner/watchMode.js | 396 + .../playwright/lib/runner/workerHost.js | 101 + .../playwright/lib/testActions.js | 220 + .../playwright/lib/third_party/pirates.js | 62 + .../lib/third_party/tsconfig-loader.js | 103 + .../playwright/lib/transform/babelBundle.js | 43 + .../lib/transform/babelBundleImpl.js | 461 + .../lib/transform/compilationCache.js | 272 + .../playwright/lib/transform/esmLoader.js | 105 + .../playwright/lib/transform/portTransport.js | 67 + .../playwright/lib/transform/transform.js | 296 + .../test/node_modules/playwright/lib/util.js | 400 + .../playwright/lib/utilsBundle.js | 43 + .../playwright/lib/utilsBundleImpl.js | 100 + .../playwright/lib/worker/fixtureRunner.js | 262 + .../playwright/lib/worker/testInfo.js | 532 + .../playwright/lib/worker/testTracing.js | 351 + .../playwright/lib/worker/timeoutManager.js | 185 + .../playwright/lib/worker/util.js | 31 + .../playwright/lib/worker/workerMain.js | 533 + .../test/node_modules/playwright/package.json | 68 + .../test/node_modules/playwright/test.d.ts | 18 + .../test/node_modules/playwright/test.js | 24 + .../test/node_modules/playwright/test.mjs | 34 + .../node_modules/playwright/types/test.d.ts | 10322 +++++++ .../playwright/types/testReporter.d.ts | 822 + .../@playwright/test/package.json | 35 + .../@playwright/test/reporter.d.ts | 17 + .../node_modules/@playwright/test/reporter.js | 17 + .../@playwright/test/reporter.mjs | 17 + .../node_modules/playwright-core/LICENSE | 202 + .../node_modules/playwright-core/NOTICE | 5 + .../node_modules/playwright-core/README.md | 3 + .../playwright-core/ThirdPartyNotices.txt | 3552 +++ .../bin/install_media_pack.ps1 | 5 + .../bin/install_webkit_wsl.ps1 | 33 + .../bin/reinstall_chrome_beta_linux.sh | 42 + .../bin/reinstall_chrome_beta_mac.sh | 13 + .../bin/reinstall_chrome_beta_win.ps1 | 24 + .../bin/reinstall_chrome_stable_linux.sh | 42 + .../bin/reinstall_chrome_stable_mac.sh | 12 + .../bin/reinstall_chrome_stable_win.ps1 | 24 + .../bin/reinstall_msedge_beta_linux.sh | 48 + .../bin/reinstall_msedge_beta_mac.sh | 11 + .../bin/reinstall_msedge_beta_win.ps1 | 23 + .../bin/reinstall_msedge_dev_linux.sh | 48 + .../bin/reinstall_msedge_dev_mac.sh | 11 + .../bin/reinstall_msedge_dev_win.ps1 | 23 + .../bin/reinstall_msedge_stable_linux.sh | 48 + .../bin/reinstall_msedge_stable_mac.sh | 11 + .../bin/reinstall_msedge_stable_win.ps1 | 24 + .../playwright-core/browsers.json | 81 + .../node_modules/playwright-core/cli.js | 18 + .../node_modules/playwright-core/index.d.ts | 17 + .../node_modules/playwright-core/index.js | 32 + .../node_modules/playwright-core/index.mjs | 28 + .../playwright-core/lib/androidServerImpl.js | 65 + .../playwright-core/lib/bootstrap.js | 77 + .../playwright-core/lib/browserServerImpl.js | 120 + .../playwright-core/lib/cli/browserActions.js | 308 + .../playwright-core/lib/cli/driver.js | 98 + .../playwright-core/lib/cli/installActions.js | 171 + .../playwright-core/lib/cli/program.js | 225 + .../lib/cli/programWithTestStub.js | 74 + .../playwright-core/lib/client/android.js | 361 + .../playwright-core/lib/client/api.js | 137 + .../playwright-core/lib/client/artifact.js | 79 + .../playwright-core/lib/client/browser.js | 169 + .../lib/client/browserContext.js | 563 + .../playwright-core/lib/client/browserType.js | 153 + .../playwright-core/lib/client/cdpSession.js | 55 + .../lib/client/channelOwner.js | 194 + .../lib/client/clientHelper.js | 64 + .../lib/client/clientInstrumentation.js | 55 + .../lib/client/clientStackTrace.js | 69 + .../playwright-core/lib/client/clock.js | 68 + .../playwright-core/lib/client/connect.js | 143 + .../playwright-core/lib/client/connection.js | 322 + .../lib/client/consoleMessage.js | 61 + .../playwright-core/lib/client/coverage.js | 44 + .../playwright-core/lib/client/debugger.js | 57 + .../playwright-core/lib/client/dialog.js | 63 + .../playwright-core/lib/client/disposable.js | 76 + .../playwright-core/lib/client/download.js | 62 + .../playwright-core/lib/client/electron.js | 139 + .../lib/client/elementHandle.js | 281 + .../playwright-core/lib/client/errors.js | 77 + .../lib/client/eventEmitter.js | 314 + .../playwright-core/lib/client/events.js | 103 + .../playwright-core/lib/client/fetch.js | 367 + .../playwright-core/lib/client/fileChooser.js | 46 + .../playwright-core/lib/client/fileUtils.js | 34 + .../playwright-core/lib/client/frame.js | 404 + .../playwright-core/lib/client/harRouter.js | 99 + .../playwright-core/lib/client/input.js | 84 + .../playwright-core/lib/client/jsHandle.js | 105 + .../playwright-core/lib/client/jsonPipe.js | 39 + .../playwright-core/lib/client/localUtils.js | 60 + .../playwright-core/lib/client/locator.js | 367 + .../playwright-core/lib/client/network.js | 750 + .../playwright-core/lib/client/page.js | 731 + .../playwright-core/lib/client/platform.js | 74 + .../playwright-core/lib/client/playwright.js | 71 + .../playwright-core/lib/client/screencast.js | 88 + .../playwright-core/lib/client/selectors.js | 57 + .../playwright-core/lib/client/stream.js | 39 + .../lib/client/timeoutSettings.js | 79 + .../playwright-core/lib/client/tracing.js | 126 + .../playwright-core/lib/client/types.js | 28 + .../playwright-core/lib/client/video.js | 52 + .../playwright-core/lib/client/waiter.js | 142 + .../playwright-core/lib/client/webError.js | 39 + .../playwright-core/lib/client/worker.js | 85 + .../lib/client/writableStream.js | 39 + .../lib/generated/bindingsControllerSource.js | 28 + .../lib/generated/clockSource.js | 28 + .../lib/generated/injectedScriptSource.js | 28 + .../lib/generated/pollingRecorderSource.js | 28 + .../lib/generated/storageScriptSource.js | 28 + .../lib/generated/utilityScriptSource.js | 28 + .../lib/generated/webSocketMockSource.js | 336 + .../playwright-core/lib/inProcessFactory.js | 60 + .../playwright-core/lib/inprocess.js | 3 + .../playwright-core/lib/mcpBundle.js | 78 + .../playwright-core/lib/mcpBundleImpl.js | 91 + .../playwright-core/lib/outofprocess.js | 76 + .../lib/protocol/serializers.js | 197 + .../playwright-core/lib/protocol/validator.js | 3067 ++ .../lib/protocol/validatorPrimitives.js | 193 + .../lib/remote/playwrightConnection.js | 131 + .../lib/remote/playwrightPipeServer.js | 100 + .../lib/remote/playwrightServer.js | 339 + .../lib/remote/playwrightWebSocketServer.js | 73 + .../lib/remote/serverTransport.js | 96 + .../lib/server/android/android.js | 465 + .../lib/server/android/backendAdb.js | 177 + .../playwright-core/lib/server/artifact.js | 127 + .../lib/server/bidi/bidiBrowser.js | 571 + .../lib/server/bidi/bidiChromium.js | 162 + .../lib/server/bidi/bidiConnection.js | 213 + .../lib/server/bidi/bidiDeserializer.js | 116 + .../lib/server/bidi/bidiExecutionContext.js | 267 + .../lib/server/bidi/bidiFirefox.js | 128 + .../lib/server/bidi/bidiInput.js | 146 + .../lib/server/bidi/bidiNetworkManager.js | 411 + .../lib/server/bidi/bidiOverCdp.js | 102 + .../lib/server/bidi/bidiPage.js | 599 + .../lib/server/bidi/bidiPdf.js | 106 + .../server/bidi/third_party/bidiCommands.d.js | 22 + .../server/bidi/third_party/bidiKeyboard.js | 256 + .../server/bidi/third_party/bidiProtocol.js | 24 + .../bidi/third_party/bidiProtocolCore.js | 180 + .../third_party/bidiProtocolPermissions.js | 42 + .../server/bidi/third_party/bidiSerializer.js | 148 + .../server/bidi/third_party/firefoxPrefs.js | 261 + .../playwright-core/lib/server/browser.js | 212 + .../lib/server/browserContext.js | 741 + .../playwright-core/lib/server/browserType.js | 338 + .../playwright-core/lib/server/callLog.js | 82 + .../lib/server/chromium/appIcon.png | Bin 0 -> 16565 bytes .../lib/server/chromium/chromium.js | 399 + .../lib/server/chromium/chromiumSwitches.js | 110 + .../lib/server/chromium/crBrowser.js | 528 + .../lib/server/chromium/crConnection.js | 197 + .../lib/server/chromium/crCoverage.js | 235 + .../lib/server/chromium/crDevTools.js | 111 + .../lib/server/chromium/crDragDrop.js | 131 + .../lib/server/chromium/crExecutionContext.js | 146 + .../lib/server/chromium/crInput.js | 187 + .../lib/server/chromium/crNetworkManager.js | 707 + .../lib/server/chromium/crPage.js | 963 + .../lib/server/chromium/crPdf.js | 121 + .../lib/server/chromium/crProtocolHelper.js | 145 + .../lib/server/chromium/crServiceWorker.js | 137 + .../server/chromium/defaultFontFamilies.js | 162 + .../lib/server/chromium/protocol.d.js | 16 + .../playwright-core/lib/server/clock.js | 149 + .../lib/server/codegen/csharp.js | 327 + .../lib/server/codegen/java.js | 274 + .../lib/server/codegen/javascript.js | 247 + .../lib/server/codegen/jsonl.js | 52 + .../lib/server/codegen/language.js | 132 + .../lib/server/codegen/languages.js | 68 + .../lib/server/codegen/python.js | 279 + .../lib/server/codegen/types.js | 16 + .../playwright-core/lib/server/console.js | 61 + .../playwright-core/lib/server/cookieStore.js | 206 + .../lib/server/debugController.js | 197 + .../playwright-core/lib/server/debugger.js | 112 + .../lib/server/deviceDescriptors.js | 39 + .../lib/server/deviceDescriptorsSource.json | 1779 ++ .../playwright-core/lib/server/dialog.js | 116 + .../server/dispatchers/androidDispatcher.js | 325 + .../server/dispatchers/artifactDispatcher.js | 118 + .../dispatchers/browserContextDispatcher.js | 381 + .../server/dispatchers/browserDispatcher.js | 124 + .../dispatchers/browserTypeDispatcher.js | 71 + .../dispatchers/cdpSessionDispatcher.js | 47 + .../dispatchers/debugControllerDispatcher.js | 78 + .../server/dispatchers/debuggerDispatcher.js | 84 + .../server/dispatchers/dialogDispatcher.js | 47 + .../lib/server/dispatchers/dispatcher.js | 364 + .../dispatchers/disposableDispatcher.js | 39 + .../server/dispatchers/electronDispatcher.js | 90 + .../dispatchers/elementHandlerDispatcher.js | 181 + .../lib/server/dispatchers/frameDispatcher.js | 227 + .../server/dispatchers/jsHandleDispatcher.js | 85 + .../server/dispatchers/jsonPipeDispatcher.js | 58 + .../dispatchers/localUtilsDispatcher.js | 185 + .../server/dispatchers/networkDispatchers.js | 214 + .../lib/server/dispatchers/pageDispatcher.js | 456 + .../dispatchers/playwrightDispatcher.js | 108 + .../server/dispatchers/streamDispatcher.js | 67 + .../server/dispatchers/tracingDispatcher.js | 68 + .../dispatchers/webSocketRouteDispatcher.js | 164 + .../dispatchers/writableStreamDispatcher.js | 79 + .../playwright-core/lib/server/disposable.js | 41 + .../playwright-core/lib/server/dom.js | 833 + .../playwright-core/lib/server/download.js | 71 + .../lib/server/electron/electron.js | 278 + .../lib/server/electron/loader.js | 29 + .../playwright-core/lib/server/errors.js | 69 + .../playwright-core/lib/server/fetch.js | 621 + .../playwright-core/lib/server/fileChooser.js | 43 + .../lib/server/fileUploadUtils.js | 84 + .../lib/server/firefox/ffBrowser.js | 408 + .../lib/server/firefox/ffConnection.js | 142 + .../lib/server/firefox/ffExecutionContext.js | 150 + .../lib/server/firefox/ffInput.js | 175 + .../lib/server/firefox/ffNetworkManager.js | 256 + .../lib/server/firefox/ffPage.js | 494 + .../lib/server/firefox/firefox.js | 114 + .../lib/server/firefox/protocol.d.js | 16 + .../playwright-core/lib/server/formData.js | 147 + .../lib/server/frameSelectors.js | 160 + .../playwright-core/lib/server/frames.js | 1500 + .../lib/server/har/harRecorder.js | 147 + .../lib/server/har/harTracer.js | 608 + .../playwright-core/lib/server/harBackend.js | 157 + .../playwright-core/lib/server/helper.js | 96 + .../playwright-core/lib/server/index.js | 58 + .../playwright-core/lib/server/input.js | 322 + .../lib/server/instrumentation.js | 77 + .../playwright-core/lib/server/javascript.js | 291 + .../playwright-core/lib/server/launchApp.js | 127 + .../playwright-core/lib/server/localUtils.js | 214 + .../lib/server/macEditingCommands.js | 143 + .../playwright-core/lib/server/network.js | 668 + .../playwright-core/lib/server/overlay.js | 138 + .../playwright-core/lib/server/page.js | 890 + .../lib/server/pipeTransport.js | 89 + .../playwright-core/lib/server/playwright.js | 69 + .../playwright-core/lib/server/progress.js | 138 + .../lib/server/protocolError.js | 52 + .../playwright-core/lib/server/recorder.js | 535 + .../lib/server/recorder/chat.js | 161 + .../lib/server/recorder/recorderApp.js | 367 + .../lib/server/recorder/recorderRunner.js | 138 + .../recorder/recorderSignalProcessor.js | 83 + .../lib/server/recorder/recorderUtils.js | 157 + .../lib/server/recorder/throttledFile.js | 57 + .../lib/server/registry/browserFetcher.js | 177 + .../lib/server/registry/dependencies.js | 371 + .../lib/server/registry/index.js | 1395 + .../lib/server/registry/nativeDeps.js | 1281 + .../server/registry/oopDownloadBrowserMain.js | 127 + .../playwright-core/lib/server/screencast.js | 137 + .../lib/server/screenshotter.js | 333 + .../playwright-core/lib/server/selectors.js | 112 + .../socksClientCertificatesInterceptor.js | 383 + .../lib/server/socksInterceptor.js | 95 + .../lib/server/trace/recorder/snapshotter.js | 147 + .../trace/recorder/snapshotterInjected.js | 561 + .../lib/server/trace/recorder/tracing.js | 655 + .../lib/server/trace/viewer/traceViewer.js | 244 + .../playwright-core/lib/server/transport.js | 181 + .../playwright-core/lib/server/types.js | 28 + .../lib/server/usKeyboardLayout.js | 152 + .../playwright-core/lib/server/utils/ascii.js | 44 + .../lib/server/utils/comparators.js | 139 + .../lib/server/utils/crypto.js | 216 + .../playwright-core/lib/server/utils/debug.js | 42 + .../lib/server/utils/debugLogger.js | 122 + .../lib/server/utils/disposable.js | 32 + .../playwright-core/lib/server/utils/env.js | 73 + .../lib/server/utils/eventsHelper.js | 41 + .../lib/server/utils/expectUtils.js | 123 + .../lib/server/utils/fileUtils.js | 205 + .../lib/server/utils/happyEyeballs.js | 210 + .../lib/server/utils/hostPlatform.js | 123 + .../lib/server/utils/httpServer.js | 205 + .../server/utils/image_tools/colorUtils.js | 89 + .../lib/server/utils/image_tools/compare.js | 109 + .../server/utils/image_tools/imageChannel.js | 78 + .../lib/server/utils/image_tools/stats.js | 102 + .../lib/server/utils/linuxUtils.js | 71 + .../lib/server/utils/network.js | 243 + .../lib/server/utils/nodePlatform.js | 148 + .../lib/server/utils/pipeTransport.js | 84 + .../lib/server/utils/processLauncher.js | 243 + .../lib/server/utils/profiler.js | 65 + .../lib/server/utils/socksProxy.js | 511 + .../lib/server/utils/spawnAsync.js | 41 + .../playwright-core/lib/server/utils/task.js | 51 + .../lib/server/utils/userAgent.js | 98 + .../lib/server/utils/wsServer.js | 121 + .../lib/server/utils/zipFile.js | 74 + .../playwright-core/lib/server/utils/zones.js | 57 + .../lib/server/videoRecorder.js | 194 + .../lib/server/webkit/protocol.d.js | 16 + .../lib/server/webkit/webkit.js | 108 + .../lib/server/webkit/wkBrowser.js | 330 + .../lib/server/webkit/wkConnection.js | 144 + .../lib/server/webkit/wkExecutionContext.js | 154 + .../lib/server/webkit/wkInput.js | 181 + .../server/webkit/wkInterceptableRequest.js | 197 + .../lib/server/webkit/wkPage.js | 1161 + .../lib/server/webkit/wkProvisionalPage.js | 83 + .../lib/server/webkit/wkWorkers.js | 106 + .../playwright-core/lib/serverRegistry.js | 156 + .../lib/third_party/pixelmatch.js | 255 + .../lib/tools/backend/browserBackend.js | 79 + .../lib/tools/backend/common.js | 63 + .../lib/tools/backend/config.js | 41 + .../lib/tools/backend/console.js | 66 + .../lib/tools/backend/context.js | 296 + .../lib/tools/backend/cookies.js | 152 + .../lib/tools/backend/devtools.js | 69 + .../lib/tools/backend/dialogs.js | 59 + .../lib/tools/backend/evaluate.js | 64 + .../lib/tools/backend/files.js | 60 + .../playwright-core/lib/tools/backend/form.js | 64 + .../lib/tools/backend/keyboard.js | 155 + .../lib/tools/backend/logFile.js | 95 + .../lib/tools/backend/mouse.js | 168 + .../lib/tools/backend/navigate.js | 106 + .../lib/tools/backend/network.js | 135 + .../playwright-core/lib/tools/backend/pdf.js | 48 + .../lib/tools/backend/response.js | 305 + .../lib/tools/backend/route.js | 140 + .../lib/tools/backend/runCode.js | 77 + .../lib/tools/backend/screenshot.js | 88 + .../lib/tools/backend/sessionLog.js | 74 + .../lib/tools/backend/snapshot.js | 208 + .../lib/tools/backend/storage.js | 68 + .../playwright-core/lib/tools/backend/tab.js | 445 + .../playwright-core/lib/tools/backend/tabs.js | 67 + .../playwright-core/lib/tools/backend/tool.js | 47 + .../lib/tools/backend/tools.js | 102 + .../lib/tools/backend/tracing.js | 78 + .../lib/tools/backend/utils.js | 83 + .../lib/tools/backend/verify.js | 151 + .../lib/tools/backend/video.js | 98 + .../playwright-core/lib/tools/backend/wait.js | 63 + .../lib/tools/backend/webstorage.js | 223 + .../lib/tools/cli-client/cli.js | 6 + .../lib/tools/cli-client/help.json | 399 + .../lib/tools/cli-client/minimist.js | 128 + .../lib/tools/cli-client/program.js | 350 + .../lib/tools/cli-client/registry.js | 176 + .../lib/tools/cli-client/session.js | 289 + .../lib/tools/cli-client/skill/SKILL.md | 328 + .../skill/references/element-attributes.md | 23 + .../skill/references/playwright-tests.md | 39 + .../skill/references/request-mocking.md | 87 + .../skill/references/running-code.md | 231 + .../skill/references/session-management.md | 169 + .../skill/references/storage-state.md | 275 + .../skill/references/test-generation.md | 88 + .../cli-client/skill/references/tracing.md | 139 + .../skill/references/video-recording.md | 143 + .../lib/tools/cli-daemon/command.js | 73 + .../lib/tools/cli-daemon/commands.js | 956 + .../lib/tools/cli-daemon/daemon.js | 157 + .../lib/tools/cli-daemon/helpGenerator.js | 177 + .../lib/tools/cli-daemon/program.js | 129 + .../lib/tools/dashboard/appIcon.png | Bin 0 -> 16565 bytes .../lib/tools/dashboard/dashboardApp.js | 284 + .../tools/dashboard/dashboardController.js | 296 + .../playwright-core/lib/tools/exports.js | 60 + .../lib/tools/mcp/browserFactory.js | 233 + .../playwright-core/lib/tools/mcp/cdpRelay.js | 352 + .../playwright-core/lib/tools/mcp/cli-stub.js | 7 + .../playwright-core/lib/tools/mcp/config.d.js | 16 + .../playwright-core/lib/tools/mcp/config.js | 446 + .../lib/tools/mcp/configIni.js | 189 + .../lib/tools/mcp/extensionContextFactory.js | 55 + .../playwright-core/lib/tools/mcp/index.js | 62 + .../playwright-core/lib/tools/mcp/log.js | 35 + .../playwright-core/lib/tools/mcp/program.js | 107 + .../playwright-core/lib/tools/mcp/protocol.js | 28 + .../playwright-core/lib/tools/mcp/watchdog.js | 44 + .../playwright-core/lib/tools/trace/SKILL.md | 171 + .../lib/tools/trace/installSkill.js | 48 + .../lib/tools/trace/traceActions.js | 142 + .../lib/tools/trace/traceAttachments.js | 69 + .../lib/tools/trace/traceCli.js | 87 + .../lib/tools/trace/traceConsole.js | 97 + .../lib/tools/trace/traceErrors.js | 55 + .../lib/tools/trace/traceOpen.js | 69 + .../lib/tools/trace/traceParser.js | 96 + .../lib/tools/trace/traceRequests.js | 182 + .../lib/tools/trace/traceScreenshot.js | 68 + .../lib/tools/trace/traceSnapshot.js | 149 + .../lib/tools/trace/traceUtils.js | 153 + .../lib/tools/utils/connect.js | 32 + .../lib/tools/utils/mcp/http.js | 152 + .../lib/tools/utils/mcp/server.js | 230 + .../lib/tools/utils/mcp/tool.js | 47 + .../lib/tools/utils/socketConnection.js | 108 + .../node_modules/playwright-core/lib/utils.js | 115 + .../lib/utils/isomorphic/ariaSnapshot.js | 455 + .../lib/utils/isomorphic/assert.js | 31 + .../lib/utils/isomorphic/colors.js | 72 + .../lib/utils/isomorphic/cssParser.js | 245 + .../lib/utils/isomorphic/cssTokenizer.js | 1051 + .../lib/utils/isomorphic/formatUtils.js | 64 + .../lib/utils/isomorphic/headers.js | 53 + .../lib/utils/isomorphic/imageUtils.js | 141 + .../lib/utils/isomorphic/jsonSchema.js | 89 + .../lib/utils/isomorphic/locatorGenerators.js | 689 + .../lib/utils/isomorphic/locatorParser.js | 176 + .../lib/utils/isomorphic/locatorUtils.js | 81 + .../lib/utils/isomorphic/lruCache.js | 51 + .../lib/utils/isomorphic/manualPromise.js | 114 + .../lib/utils/isomorphic/mimeType.js | 464 + .../lib/utils/isomorphic/multimap.js | 80 + .../lib/utils/isomorphic/protocolFormatter.js | 81 + .../lib/utils/isomorphic/protocolMetainfo.js | 351 + .../lib/utils/isomorphic/rtti.js | 43 + .../lib/utils/isomorphic/selectorParser.js | 386 + .../lib/utils/isomorphic/semaphore.js | 54 + .../lib/utils/isomorphic/stackTrace.js | 158 + .../lib/utils/isomorphic/stringUtils.js | 204 + .../lib/utils/isomorphic/time.js | 49 + .../lib/utils/isomorphic/timeoutRunner.js | 66 + .../lib/utils/isomorphic/trace/entries.js | 16 + .../isomorphic/trace/snapshotRenderer.js | 492 + .../utils/isomorphic/trace/snapshotServer.js | 120 + .../utils/isomorphic/trace/snapshotStorage.js | 89 + .../lib/utils/isomorphic/trace/traceLoader.js | 132 + .../lib/utils/isomorphic/trace/traceModel.js | 366 + .../utils/isomorphic/trace/traceModernizer.js | 401 + .../lib/utils/isomorphic/trace/traceUtils.js | 58 + .../isomorphic/trace/versions/traceV3.js | 16 + .../isomorphic/trace/versions/traceV4.js | 16 + .../isomorphic/trace/versions/traceV5.js | 16 + .../isomorphic/trace/versions/traceV6.js | 16 + .../isomorphic/trace/versions/traceV7.js | 16 + .../isomorphic/trace/versions/traceV8.js | 16 + .../lib/utils/isomorphic/types.js | 16 + .../lib/utils/isomorphic/urlMatch.js | 243 + .../isomorphic/utilityScriptSerializers.js | 262 + .../lib/utils/isomorphic/yaml.js | 84 + .../playwright-core/lib/utilsBundle.js | 91 + .../lib/utilsBundleImpl/index.js | 217 + .../lib/utilsBundleImpl/xdg-open | 1066 + .../vite/dashboard/assets/index-BAOybkp8.js | 50 + .../vite/dashboard/assets/index-CZAYOG76.css | 1 + .../lib/vite/dashboard/index.html | 28 + .../lib/vite/htmlReport/index.html | 16 + .../lib/vite/htmlReport/report.css | 1 + .../lib/vite/htmlReport/report.js | 72 + .../assets/codeMirrorModule-C8KMvO9L.js | 32 + .../assets/codeMirrorModule-DYBRYzYX.css | 1 + .../vite/recorder/assets/codicon-DCmgc-ay.ttf | Bin 0 -> 80340 bytes .../vite/recorder/assets/index-BSjZa4pk.css | 1 + .../vite/recorder/assets/index-CqAYX1I3.js | 193 + .../lib/vite/recorder/index.html | 29 + .../lib/vite/recorder/playwright-logo.svg | 9 + .../assets/codeMirrorModule-DqGGleTZ.js | 32 + .../assets/defaultSettingsView-CWq3uECg.js | 262 + .../assets/xtermModule-CsJ4vdCR.js | 9 + .../traceViewer/codeMirrorModule.DYBRYzYX.css | 1 + .../lib/vite/traceViewer/codicon.DCmgc-ay.ttf | Bin 0 -> 80340 bytes .../defaultSettingsView.B4dS75f0.css | 1 + .../lib/vite/traceViewer/index.C5r2ipUR.js | 2 + .../lib/vite/traceViewer/index.CzXZzn5A.css | 1 + .../lib/vite/traceViewer/index.html | 43 + .../lib/vite/traceViewer/manifest.webmanifest | 16 + .../lib/vite/traceViewer/playwright-logo.svg | 9 + .../lib/vite/traceViewer/snapshot.html | 21 + .../lib/vite/traceViewer/sw.bundle.js | 5 + .../lib/vite/traceViewer/uiMode.Btcz36p_.css | 1 + .../lib/vite/traceViewer/uiMode.ClIxK9Iu.js | 6 + .../lib/vite/traceViewer/uiMode.html | 17 + .../vite/traceViewer/xtermModule.DYP7pi_n.css | 32 + .../playwright-core/lib/zipBundle.js | 34 + .../playwright-core/lib/zipBundleImpl.js | 5 + .../playwright-core/lib/zodBundle.js | 39 + .../playwright-core/lib/zodBundleImpl.js | 40 + .../node_modules/playwright-core/package.json | 48 + .../playwright-core/types/protocol.d.ts | 24720 ++++++++++++++++ .../playwright-core/types/structs.d.ts | 45 + .../playwright-core/types/types.d.ts | 23623 +++++++++++++++ .../node_modules/playwright/LICENSE | 202 + .../node_modules/playwright/NOTICE | 5 + .../node_modules/playwright/README.md | 170 + .../playwright/ThirdPartyNotices.txt | 3919 +++ .../node_modules/playwright/cli.js | 19 + .../node_modules/playwright/index.d.ts | 17 + .../node_modules/playwright/index.js | 17 + .../node_modules/playwright/index.mjs | 18 + .../node_modules/playwright/jsx-runtime.js | 42 + .../node_modules/playwright/jsx-runtime.mjs | 21 + .../playwright/lib/agents/agentParser.js | 89 + .../lib/agents/copilot-setup-steps.yml | 34 + .../playwright/lib/agents/generateAgents.js | 346 + .../agents/playwright-test-coverage.prompt.md | 31 + .../agents/playwright-test-generate.prompt.md | 8 + .../agents/playwright-test-generator.agent.md | 88 + .../lib/agents/playwright-test-heal.prompt.md | 6 + .../agents/playwright-test-healer.agent.md | 55 + .../lib/agents/playwright-test-plan.prompt.md | 9 + .../agents/playwright-test-planner.agent.md | 73 + .../playwright/lib/common/config.js | 281 + .../playwright/lib/common/configLoader.js | 344 + .../playwright/lib/common/esmLoaderHost.js | 104 + .../playwright/lib/common/expectBundle.js | 28 + .../playwright/lib/common/expectBundleImpl.js | 407 + .../playwright/lib/common/fixtures.js | 302 + .../playwright/lib/common/globals.js | 58 + .../node_modules/playwright/lib/common/ipc.js | 60 + .../playwright/lib/common/poolBuilder.js | 85 + .../playwright/lib/common/process.js | 133 + .../playwright/lib/common/suiteUtils.js | 140 + .../playwright/lib/common/test.js | 330 + .../playwright/lib/common/testLoader.js | 101 + .../playwright/lib/common/testType.js | 301 + .../playwright/lib/common/validators.js | 68 + .../playwright/lib/errorContext.js | 121 + .../node_modules/playwright/lib/fsWatcher.js | 67 + .../node_modules/playwright/lib/index.js | 764 + .../playwright/lib/internalsForTest.js | 42 + .../playwright/lib/isomorphic/events.js | 77 + .../playwright/lib/isomorphic/folders.js | 30 + .../lib/isomorphic/stringInternPool.js | 69 + .../playwright/lib/isomorphic/teleReceiver.js | 538 + .../lib/isomorphic/teleSuiteUpdater.js | 157 + .../lib/isomorphic/testServerConnection.js | 223 + .../lib/isomorphic/testServerInterface.js | 16 + .../playwright/lib/isomorphic/testTree.js | 329 + .../playwright/lib/isomorphic/types.d.js | 16 + .../playwright/lib/loader/loaderMain.js | 59 + .../playwright/lib/matchers/expect.js | 311 + .../playwright/lib/matchers/matcherHint.js | 44 + .../playwright/lib/matchers/matchers.js | 385 + .../playwright/lib/matchers/toBeTruthy.js | 75 + .../playwright/lib/matchers/toEqual.js | 100 + .../playwright/lib/matchers/toHaveURL.js | 101 + .../lib/matchers/toMatchAriaSnapshot.js | 163 + .../lib/matchers/toMatchSnapshot.js | 349 + .../playwright/lib/matchers/toMatchText.js | 99 + .../playwright/lib/mcp/test/browserBackend.js | 121 + .../playwright/lib/mcp/test/generatorTools.js | 122 + .../playwright/lib/mcp/test/plannerTools.js | 145 + .../playwright/lib/mcp/test/seed.js | 82 + .../playwright/lib/mcp/test/streams.js | 44 + .../playwright/lib/mcp/test/testBackend.js | 99 + .../playwright/lib/mcp/test/testContext.js | 283 + .../playwright/lib/mcp/test/testTool.js | 30 + .../playwright/lib/mcp/test/testTools.js | 108 + .../lib/plugins/gitCommitInfoPlugin.js | 198 + .../playwright/lib/plugins/index.js | 28 + .../playwright/lib/plugins/webServerPlugin.js | 238 + .../node_modules/playwright/lib/program.js | 239 + .../playwright/lib/reportActions.js | 80 + .../playwright/lib/reporters/base.js | 633 + .../playwright/lib/reporters/blob.js | 138 + .../playwright/lib/reporters/dot.js | 99 + .../playwright/lib/reporters/empty.js | 32 + .../playwright/lib/reporters/github.js | 127 + .../playwright/lib/reporters/html.js | 666 + .../lib/reporters/internalReporter.js | 138 + .../playwright/lib/reporters/json.js | 254 + .../playwright/lib/reporters/junit.js | 321 + .../playwright/lib/reporters/line.js | 131 + .../playwright/lib/reporters/list.js | 252 + .../lib/reporters/listModeReporter.js | 69 + .../playwright/lib/reporters/markdown.js | 144 + .../playwright/lib/reporters/merge.js | 579 + .../playwright/lib/reporters/multiplexer.js | 116 + .../playwright/lib/reporters/reporterV2.js | 102 + .../playwright/lib/reporters/teleEmitter.js | 319 + .../lib/reporters/versions/blobV1.js | 16 + .../playwright/lib/runner/dispatcher.js | 522 + .../playwright/lib/runner/failureTracker.js | 72 + .../playwright/lib/runner/lastRun.js | 77 + .../playwright/lib/runner/loadUtils.js | 340 + .../playwright/lib/runner/loaderHost.js | 89 + .../playwright/lib/runner/processHost.js | 180 + .../playwright/lib/runner/projectUtils.js | 241 + .../playwright/lib/runner/rebase.js | 189 + .../playwright/lib/runner/reporters.js | 143 + .../playwright/lib/runner/sigIntWatcher.js | 96 + .../playwright/lib/runner/taskRunner.js | 127 + .../playwright/lib/runner/tasks.js | 413 + .../playwright/lib/runner/testGroups.js | 125 + .../playwright/lib/runner/testRunner.js | 398 + .../playwright/lib/runner/testServer.js | 269 + .../playwright/lib/runner/uiModeReporter.js | 30 + .../node_modules/playwright/lib/runner/vcs.js | 72 + .../playwright/lib/runner/watchMode.js | 396 + .../playwright/lib/runner/workerHost.js | 101 + .../playwright/lib/testActions.js | 220 + .../playwright/lib/third_party/pirates.js | 62 + .../lib/third_party/tsconfig-loader.js | 103 + .../playwright/lib/transform/babelBundle.js | 43 + .../lib/transform/babelBundleImpl.js | 461 + .../lib/transform/compilationCache.js | 272 + .../playwright/lib/transform/esmLoader.js | 105 + .../playwright/lib/transform/portTransport.js | 67 + .../playwright/lib/transform/transform.js | 296 + .../node_modules/playwright/lib/util.js | 400 + .../playwright/lib/utilsBundle.js | 43 + .../playwright/lib/utilsBundleImpl.js | 100 + .../playwright/lib/worker/fixtureRunner.js | 262 + .../playwright/lib/worker/testInfo.js | 532 + .../playwright/lib/worker/testTracing.js | 351 + .../playwright/lib/worker/timeoutManager.js | 185 + .../playwright/lib/worker/util.js | 31 + .../playwright/lib/worker/workerMain.js | 533 + .../node_modules/playwright/package.json | 68 + .../node_modules/playwright/test.d.ts | 18 + .../node_modules/playwright/test.js | 24 + .../node_modules/playwright/test.mjs | 34 + .../node_modules/playwright/types/test.d.ts | 10322 +++++++ .../playwright/types/testReporter.d.ts | 822 + mcps/playwright_mcp/package-lock.json | 122 + mcps/playwright_mcp/package.json | 17 + mcps/selenium_mcp/README.md | 126 + .../selenium_mcp_server.cpython-312.pyc | Bin 0 -> 46821 bytes .../launch_selenium_mcp_server.sh | 14 + mcps/selenium_mcp/mcp_config.json | 6 + mcps/selenium_mcp/poetry.lock | 1105 + mcps/selenium_mcp/pyproject.toml | 22 + mcps/selenium_mcp/selenium_mcp_server.py | 1075 + mcps/selenium_mcp/test_client.py | 66 + skills/artifacts-builder/LICENSE.txt | 202 + skills/artifacts-builder/SKILL.md | 74 + .../scripts/bundle-artifact.sh | 54 + .../scripts/init-artifact.sh | 322 + .../scripts/shadcn-components.tar.gz | Bin 0 -> 19967 bytes skills/document-skills/docx/LICENSE.txt | 30 + skills/document-skills/docx/SKILL.md | 197 + skills/document-skills/docx/docx-js.md | 350 + skills/document-skills/docx/ooxml.md | 610 + .../schemas/ISO-IEC29500-4_2016/dml-chart.xsd | 1499 + .../ISO-IEC29500-4_2016/dml-chartDrawing.xsd | 146 + .../ISO-IEC29500-4_2016/dml-diagram.xsd | 1085 + .../ISO-IEC29500-4_2016/dml-lockedCanvas.xsd | 11 + .../schemas/ISO-IEC29500-4_2016/dml-main.xsd | 3081 ++ .../ISO-IEC29500-4_2016/dml-picture.xsd | 23 + .../dml-spreadsheetDrawing.xsd | 185 + .../dml-wordprocessingDrawing.xsd | 287 + .../ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd | 1676 ++ .../shared-additionalCharacteristics.xsd | 28 + .../shared-bibliography.xsd | 144 + .../shared-commonSimpleTypes.xsd | 174 + .../shared-customXmlDataProperties.xsd | 25 + .../shared-customXmlSchemaProperties.xsd | 18 + .../shared-documentPropertiesCustom.xsd | 59 + .../shared-documentPropertiesExtended.xsd | 56 + .../shared-documentPropertiesVariantTypes.xsd | 195 + .../ISO-IEC29500-4_2016/shared-math.xsd | 582 + .../shared-relationshipReference.xsd | 25 + .../ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd | 4439 +++ .../schemas/ISO-IEC29500-4_2016/vml-main.xsd | 570 + .../ISO-IEC29500-4_2016/vml-officeDrawing.xsd | 509 + .../vml-presentationDrawing.xsd | 12 + .../vml-spreadsheetDrawing.xsd | 108 + .../vml-wordprocessingDrawing.xsd | 96 + .../ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd | 3646 +++ .../ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd | 116 + .../ecma/fouth-edition/opc-contentTypes.xsd | 42 + .../ecma/fouth-edition/opc-coreProperties.xsd | 50 + .../schemas/ecma/fouth-edition/opc-digSig.xsd | 49 + .../ecma/fouth-edition/opc-relationships.xsd | 33 + .../docx/ooxml/schemas/mce/mc.xsd | 75 + .../docx/ooxml/schemas/microsoft/wml-2010.xsd | 560 + .../docx/ooxml/schemas/microsoft/wml-2012.xsd | 67 + .../docx/ooxml/schemas/microsoft/wml-2018.xsd | 14 + .../ooxml/schemas/microsoft/wml-cex-2018.xsd | 20 + .../ooxml/schemas/microsoft/wml-cid-2016.xsd | 13 + .../microsoft/wml-sdtdatahash-2020.xsd | 4 + .../schemas/microsoft/wml-symex-2015.xsd | 8 + .../docx/ooxml/scripts/pack.py | 159 + .../docx/ooxml/scripts/unpack.py | 29 + .../docx/ooxml/scripts/validate.py | 69 + .../docx/ooxml/scripts/validation/__init__.py | 15 + .../docx/ooxml/scripts/validation/base.py | 951 + .../docx/ooxml/scripts/validation/docx.py | 274 + .../docx/ooxml/scripts/validation/pptx.py | 315 + .../ooxml/scripts/validation/redlining.py | 279 + .../document-skills/docx/scripts/__init__.py | 1 + .../document-skills/docx/scripts/document.py | 1276 + .../docx/scripts/templates/comments.xml | 3 + .../scripts/templates/commentsExtended.xml | 3 + .../scripts/templates/commentsExtensible.xml | 3 + .../docx/scripts/templates/commentsIds.xml | 3 + .../docx/scripts/templates/people.xml | 3 + .../document-skills/docx/scripts/utilities.py | 374 + skills/document-skills/pdf/LICENSE.txt | 30 + skills/document-skills/pdf/SKILL.md | 294 + skills/document-skills/pdf/forms.md | 205 + skills/document-skills/pdf/reference.md | 612 + .../pdf/scripts/check_bounding_boxes.py | 70 + .../pdf/scripts/check_bounding_boxes_test.py | 226 + .../pdf/scripts/check_fillable_fields.py | 12 + .../pdf/scripts/convert_pdf_to_images.py | 35 + .../pdf/scripts/create_validation_image.py | 41 + .../pdf/scripts/extract_form_field_info.py | 152 + .../pdf/scripts/fill_fillable_fields.py | 114 + .../scripts/fill_pdf_form_with_annotations.py | 108 + skills/document-skills/pptx/LICENSE.txt | 30 + skills/document-skills/pptx/SKILL.md | 484 + skills/document-skills/pptx/html2pptx.md | 625 + skills/document-skills/pptx/ooxml.md | 427 + .../schemas/ISO-IEC29500-4_2016/dml-chart.xsd | 1499 + .../ISO-IEC29500-4_2016/dml-chartDrawing.xsd | 146 + .../ISO-IEC29500-4_2016/dml-diagram.xsd | 1085 + .../ISO-IEC29500-4_2016/dml-lockedCanvas.xsd | 11 + .../schemas/ISO-IEC29500-4_2016/dml-main.xsd | 3081 ++ .../ISO-IEC29500-4_2016/dml-picture.xsd | 23 + .../dml-spreadsheetDrawing.xsd | 185 + .../dml-wordprocessingDrawing.xsd | 287 + .../ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd | 1676 ++ .../shared-additionalCharacteristics.xsd | 28 + .../shared-bibliography.xsd | 144 + .../shared-commonSimpleTypes.xsd | 174 + .../shared-customXmlDataProperties.xsd | 25 + .../shared-customXmlSchemaProperties.xsd | 18 + .../shared-documentPropertiesCustom.xsd | 59 + .../shared-documentPropertiesExtended.xsd | 56 + .../shared-documentPropertiesVariantTypes.xsd | 195 + .../ISO-IEC29500-4_2016/shared-math.xsd | 582 + .../shared-relationshipReference.xsd | 25 + .../ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd | 4439 +++ .../schemas/ISO-IEC29500-4_2016/vml-main.xsd | 570 + .../ISO-IEC29500-4_2016/vml-officeDrawing.xsd | 509 + .../vml-presentationDrawing.xsd | 12 + .../vml-spreadsheetDrawing.xsd | 108 + .../vml-wordprocessingDrawing.xsd | 96 + .../ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd | 3646 +++ .../ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd | 116 + .../ecma/fouth-edition/opc-contentTypes.xsd | 42 + .../ecma/fouth-edition/opc-coreProperties.xsd | 50 + .../schemas/ecma/fouth-edition/opc-digSig.xsd | 49 + .../ecma/fouth-edition/opc-relationships.xsd | 33 + .../pptx/ooxml/schemas/mce/mc.xsd | 75 + .../pptx/ooxml/schemas/microsoft/wml-2010.xsd | 560 + .../pptx/ooxml/schemas/microsoft/wml-2012.xsd | 67 + .../pptx/ooxml/schemas/microsoft/wml-2018.xsd | 14 + .../ooxml/schemas/microsoft/wml-cex-2018.xsd | 20 + .../ooxml/schemas/microsoft/wml-cid-2016.xsd | 13 + .../microsoft/wml-sdtdatahash-2020.xsd | 4 + .../schemas/microsoft/wml-symex-2015.xsd | 8 + .../pptx/ooxml/scripts/pack.py | 159 + .../pptx/ooxml/scripts/unpack.py | 29 + .../pptx/ooxml/scripts/validate.py | 69 + .../pptx/ooxml/scripts/validation/__init__.py | 15 + .../pptx/ooxml/scripts/validation/base.py | 951 + .../pptx/ooxml/scripts/validation/docx.py | 274 + .../pptx/ooxml/scripts/validation/pptx.py | 315 + .../ooxml/scripts/validation/redlining.py | 279 + .../document-skills/pptx/scripts/html2pptx.js | 979 + .../document-skills/pptx/scripts/inventory.py | 1020 + .../document-skills/pptx/scripts/rearrange.py | 231 + .../document-skills/pptx/scripts/replace.py | 385 + .../document-skills/pptx/scripts/thumbnail.py | 450 + skills/document-skills/xlsx/LICENSE.txt | 30 + skills/document-skills/xlsx/SKILL.md | 289 + skills/document-skills/xlsx/recalc.py | 178 + skills/mcp-builder/LICENSE.txt | 202 + skills/mcp-builder/SKILL.md | 328 + skills/mcp-builder/reference/evaluation.md | 602 + .../reference/mcp_best_practices.md | 915 + .../mcp-builder/reference/node_mcp_server.md | 916 + .../reference/python_mcp_server.md | 752 + skills/mcp-builder/scripts/connections.py | 151 + skills/mcp-builder/scripts/evaluation.py | 373 + .../scripts/example_evaluation.xml | 22 + skills/mcp-builder/scripts/requirements.txt | 2 + skills/skill-creator/LICENSE.txt | 202 + skills/skill-creator/SKILL.md | 209 + skills/skill-creator/scripts/init_skill.py | 303 + skills/skill-creator/scripts/package_skill.py | 110 + .../skill-creator/scripts/quick_validate.py | 65 + skills/webapp-testing/LICENSE.txt | 202 + skills/webapp-testing/SKILL.md | 96 + .../examples/console_logging.py | 35 + .../examples/element_discovery.py | 40 + .../examples/static_html_automation.py | 33 + skills/webapp-testing/scripts/with_server.py | 106 + 1449 files changed, 412954 insertions(+) create mode 100644 .gitignore create mode 100644 .mcp.json create mode 100644 README.md create mode 100644 config/claude_desktop_config.json create mode 100644 mcps/dicom_mcp/.continue/rules/xai-model-fix.md create mode 100644 mcps/dicom_mcp/.gitignore create mode 100644 mcps/dicom_mcp/.mcp.json create mode 100644 mcps/dicom_mcp/CLAUDE.md create mode 100644 mcps/dicom_mcp/INSTALL.md create mode 100644 mcps/dicom_mcp/README.md create mode 100644 mcps/dicom_mcp/bitbucket-pipelines.yml create mode 100755 mcps/dicom_mcp/build_standalone.sh create mode 100755 mcps/dicom_mcp/build_standalone_linux.sh create mode 100644 mcps/dicom_mcp/dicom_mcp.py create mode 100644 mcps/dicom_mcp/dicom_mcp/__init__.py create mode 100644 mcps/dicom_mcp/dicom_mcp/__main__.py create mode 100644 mcps/dicom_mcp/dicom_mcp/config.py create mode 100644 mcps/dicom_mcp/dicom_mcp/constants.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/__init__.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/files.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/filters.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/philips.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/pixels.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/sequence.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/tags.py create mode 100644 mcps/dicom_mcp/dicom_mcp/helpers/tree.py create mode 100644 mcps/dicom_mcp/dicom_mcp/pii.py create mode 100644 mcps/dicom_mcp/dicom_mcp/server.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/__init__.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/discovery.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/metadata.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/philips.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/pixels.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/query.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/search.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/segmentation.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/ti_analysis.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/tree.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/uid_comparison.py create mode 100644 mcps/dicom_mcp/dicom_mcp/tools/validation.py create mode 100644 mcps/dicom_mcp/docs/CAPABILITIES.md create mode 100644 mcps/dicom_mcp/docs/GUIDELINES.md create mode 100644 mcps/dicom_mcp/docs/TODO.md create mode 100644 mcps/dicom_mcp/docs/USAGE.md create mode 100644 mcps/dicom_mcp/img/claude_desktop_example.png create mode 100644 mcps/dicom_mcp/img/mcp_servers_panel.png create mode 100644 mcps/dicom_mcp/img/profile_menu.png create mode 100644 mcps/dicom_mcp/img/settings_developer_option.png create mode 100755 mcps/dicom_mcp/install.sh create mode 100644 mcps/dicom_mcp/pyproject.toml create mode 100755 mcps/dicom_mcp/run_dicom_mcp_server.sh create mode 100644 mcps/dicom_mcp/test_dicom_mcp.py create mode 100755 mcps/filesystem_mcp/install.sh create mode 100755 mcps/filesystem_mcp/launch_filesystem_mcp.sh create mode 100644 mcps/filesystem_mcp/mcp.json create mode 100644 mcps/open_meteo_mcp/README.md create mode 100644 mcps/open_meteo_mcp/__pycache__/open_meteo_mcp_server.cpython-312.pyc create mode 100644 mcps/open_meteo_mcp/open_meteo_mcp_server.py create mode 100644 mcps/open_meteo_mcp/poetry.lock create mode 100644 mcps/open_meteo_mcp/pyproject.toml create mode 100755 mcps/open_meteo_mcp/run_open_meteo_mcp_server.sh create mode 100644 mcps/playwright_mcp/README.md create mode 100755 mcps/playwright_mcp/launch_playwright_mcp.sh create mode 120000 mcps/playwright_mcp/node_modules/.bin/playwright create mode 120000 mcps/playwright_mcp/node_modules/.bin/playwright-core create mode 120000 mcps/playwright_mcp/node_modules/.bin/playwright-mcp create mode 100644 mcps/playwright_mcp/node_modules/.package-lock.json create mode 100644 mcps/playwright_mcp/node_modules/@playwright/mcp/LICENSE create mode 100644 mcps/playwright_mcp/node_modules/@playwright/mcp/README.md create mode 100755 mcps/playwright_mcp/node_modules/@playwright/mcp/cli.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/mcp/config.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/mcp/index.d.ts create mode 100755 mcps/playwright_mcp/node_modules/@playwright/mcp/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/mcp/package.json create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/LICENSE create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/NOTICE create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/README.md create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/cli.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/index.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/index.mjs create mode 120000 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright create mode 120000 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright-core create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/LICENSE create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/NOTICE create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/README.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/ThirdPartyNotices.txt create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_media_pack.ps1 create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_webkit_wsl.ps1 create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/browsers.json create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/cli.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.mjs create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/androidServerImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/bootstrap.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/browserServerImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/browserActions.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/driver.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/installActions.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/program.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/programWithTestStub.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/android.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/api.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/artifact.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/browser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/browserContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/browserType.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/cdpSession.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/channelOwner.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/clientHelper.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/clientInstrumentation.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/clientStackTrace.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/clock.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/connect.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/connection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/consoleMessage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/coverage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/debugger.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/dialog.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/disposable.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/download.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/electron.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/elementHandle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/errors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/eventEmitter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/events.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/fetch.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/fileChooser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/fileUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/frame.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/harRouter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/input.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/jsHandle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/jsonPipe.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/localUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/locator.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/network.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/page.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/platform.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/playwright.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/screencast.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/selectors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/stream.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/timeoutSettings.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/tracing.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/types.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/video.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/waiter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/webError.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/worker.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/client/writableStream.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/generated/bindingsControllerSource.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/generated/clockSource.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/generated/injectedScriptSource.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/generated/pollingRecorderSource.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/generated/storageScriptSource.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/generated/utilityScriptSource.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/generated/webSocketMockSource.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/inProcessFactory.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/inprocess.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/mcpBundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/mcpBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/outofprocess.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/protocol/serializers.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/protocol/validator.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/protocol/validatorPrimitives.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/remote/playwrightConnection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/remote/playwrightPipeServer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/remote/playwrightServer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/remote/playwrightWebSocketServer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/remote/serverTransport.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/android/android.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/android/backendAdb.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/artifact.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiChromium.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiConnection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiDeserializer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiFirefox.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiInput.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiNetworkManager.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiOverCdp.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiPage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/bidiPdf.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/third_party/bidiCommands.d.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/third_party/bidiKeyboard.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/third_party/bidiProtocol.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/third_party/bidiProtocolCore.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/third_party/bidiProtocolPermissions.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/third_party/bidiSerializer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/bidi/third_party/firefoxPrefs.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/browser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/browserContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/browserType.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/callLog.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/appIcon.png create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/chromium.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/chromiumSwitches.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crConnection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crCoverage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crDevTools.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crDragDrop.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crInput.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crNetworkManager.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crPage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crPdf.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crProtocolHelper.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/crServiceWorker.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/defaultFontFamilies.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/chromium/protocol.d.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/clock.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/csharp.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/java.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/javascript.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/jsonl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/language.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/languages.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/python.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/codegen/types.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/console.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/cookieStore.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/debugController.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/debugger.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/deviceDescriptors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dialog.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/androidDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/artifactDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/browserContextDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/browserDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/browserTypeDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/cdpSessionDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/debugControllerDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/debuggerDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/dialogDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/dispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/disposableDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/electronDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/elementHandlerDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/frameDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/jsHandleDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/jsonPipeDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/localUtilsDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/networkDispatchers.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/pageDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/playwrightDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/streamDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/tracingDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/webSocketRouteDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dispatchers/writableStreamDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/disposable.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/dom.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/download.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/electron/electron.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/electron/loader.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/errors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/fetch.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/fileChooser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/fileUploadUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/ffBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/ffConnection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/ffExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/ffInput.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/ffNetworkManager.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/ffPage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/firefox.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/firefox/protocol.d.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/formData.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/frameSelectors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/frames.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/har/harRecorder.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/har/harTracer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/harBackend.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/helper.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/input.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/instrumentation.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/javascript.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/launchApp.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/localUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/macEditingCommands.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/network.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/overlay.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/page.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/pipeTransport.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/playwright.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/progress.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/protocolError.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/recorder.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/recorder/chat.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/recorder/recorderApp.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/recorder/recorderRunner.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/recorder/recorderUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/recorder/throttledFile.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/registry/browserFetcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/registry/dependencies.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/registry/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/registry/nativeDeps.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/registry/oopDownloadBrowserMain.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/screencast.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/screenshotter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/selectors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/socksClientCertificatesInterceptor.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/socksInterceptor.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/trace/recorder/snapshotter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/trace/recorder/snapshotterInjected.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/trace/recorder/tracing.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/trace/viewer/traceViewer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/transport.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/types.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/usKeyboardLayout.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/ascii.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/comparators.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/crypto.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/debug.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/debugLogger.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/disposable.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/env.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/eventsHelper.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/expectUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/fileUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/happyEyeballs.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/hostPlatform.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/httpServer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/image_tools/colorUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/image_tools/compare.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/image_tools/imageChannel.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/image_tools/stats.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/linuxUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/network.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/nodePlatform.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/pipeTransport.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/processLauncher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/profiler.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/socksProxy.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/spawnAsync.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/task.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/userAgent.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/wsServer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/zipFile.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/utils/zones.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/videoRecorder.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/protocol.d.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/webkit.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkConnection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkInput.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkInterceptableRequest.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkPage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkProvisionalPage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/server/webkit/wkWorkers.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/serverRegistry.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/third_party/pixelmatch.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/browserBackend.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/common.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/config.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/console.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/context.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/cookies.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/devtools.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/dialogs.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/evaluate.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/files.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/form.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/keyboard.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/logFile.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/mouse.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/navigate.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/network.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/pdf.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/response.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/route.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/runCode.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/screenshot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/sessionLog.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/snapshot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/storage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/tab.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/tabs.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/tool.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/tools.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/tracing.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/utils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/verify.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/video.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/wait.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/backend/webstorage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/cli.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/help.json create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/minimist.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/program.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/registry.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/session.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/SKILL.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/element-attributes.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/playwright-tests.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/request-mocking.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/running-code.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/session-management.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/storage-state.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/tracing.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-client/skill/references/video-recording.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-daemon/command.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-daemon/commands.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-daemon/daemon.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-daemon/helpGenerator.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/cli-daemon/program.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/dashboard/appIcon.png create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/dashboard/dashboardApp.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/dashboard/dashboardController.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/exports.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/browserFactory.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/cdpRelay.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/cli-stub.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/config.d.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/config.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/configIni.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/extensionContextFactory.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/log.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/program.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/protocol.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/mcp/watchdog.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/SKILL.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/installSkill.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceActions.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceAttachments.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceCli.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceConsole.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceErrors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceOpen.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceParser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceRequests.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceScreenshot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/trace/traceUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/utils/connect.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/utils/mcp/http.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/utils/mcp/server.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/utils/mcp/tool.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/tools/utils/socketConnection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/ariaSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/assert.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/colors.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/cssParser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/cssTokenizer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/formatUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/headers.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/imageUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/jsonSchema.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/locatorGenerators.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/locatorParser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/locatorUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/lruCache.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/manualPromise.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/mimeType.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/multimap.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/protocolFormatter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/protocolMetainfo.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/rtti.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/selectorParser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/semaphore.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/stackTrace.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/stringUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/time.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/timeoutRunner.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/entries.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/snapshotRenderer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/snapshotServer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/snapshotStorage.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/traceLoader.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/traceModel.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/traceModernizer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/traceUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV3.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV4.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV5.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV6.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV7.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV8.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/types.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/urlMatch.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/utilityScriptSerializers.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utils/isomorphic/yaml.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utilsBundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utilsBundleImpl/index.js create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/utilsBundleImpl/xdg-open create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/dashboard/assets/index-BAOybkp8.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/dashboard/assets/index-CZAYOG76.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/dashboard/index.html create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/htmlReport/index.html create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/htmlReport/report.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/htmlReport/report.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-C8KMvO9L.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/recorder/assets/index-BSjZa4pk.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/recorder/assets/index-CqAYX1I3.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/recorder/index.html create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-DS0FLvoc.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-GTWI-W_B.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.B4dS75f0.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/index.C5466mMT.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/index.html create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Btcz36p_.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Vipi55dB.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/zipBundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/zipBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/zodBundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/zodBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/package.json create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/types/protocol.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/types/structs.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/LICENSE create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/NOTICE create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/README.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/ThirdPartyNotices.txt create mode 100755 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/cli.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/index.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/index.mjs create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/jsx-runtime.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/jsx-runtime.mjs create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/agentParser.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/copilot-setup-steps.yml create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/generateAgents.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/playwright-test-generator.agent.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/playwright-test-healer.agent.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/agents/playwright-test-planner.agent.md create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/config.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/configLoader.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/esmLoaderHost.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/expectBundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/expectBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/fixtures.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/globals.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/ipc.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/poolBuilder.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/process.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/suiteUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/test.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/testLoader.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/testType.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/common/validators.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/errorContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/fsWatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/internalsForTest.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/events.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/folders.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/stringInternPool.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/teleReceiver.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/teleSuiteUpdater.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/testServerConnection.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/testServerInterface.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/testTree.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/isomorphic/types.d.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/loader/loaderMain.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/expect.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/matcherHint.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/matchers.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/toBeTruthy.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/toEqual.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/toHaveURL.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/toMatchAriaSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/toMatchSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/matchers/toMatchText.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/browserBackend.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/generatorTools.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/plannerTools.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/seed.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/streams.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/testBackend.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/testContext.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/testTool.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/mcp/test/testTools.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/plugins/gitCommitInfoPlugin.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/plugins/index.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/plugins/webServerPlugin.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/program.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reportActions.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/base.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/blob.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/dot.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/empty.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/github.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/html.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/internalReporter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/json.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/junit.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/line.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/list.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/listModeReporter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/markdown.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/merge.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/multiplexer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/reporterV2.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/teleEmitter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/reporters/versions/blobV1.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/dispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/failureTracker.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/lastRun.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/loadUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/loaderHost.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/processHost.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/projectUtils.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/rebase.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/reporters.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/sigIntWatcher.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/taskRunner.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/tasks.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/testGroups.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/testRunner.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/testServer.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/uiModeReporter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/vcs.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/watchMode.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/runner/workerHost.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/testActions.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/third_party/pirates.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/third_party/tsconfig-loader.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/transform/babelBundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/transform/babelBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/transform/compilationCache.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/transform/esmLoader.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/transform/portTransport.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/transform/transform.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/util.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/utilsBundle.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/utilsBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/worker/fixtureRunner.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/worker/testInfo.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/worker/testTracing.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/worker/timeoutManager.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/worker/util.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/lib/worker/workerMain.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/package.json create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/test.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/test.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/test.mjs create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/types/test.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright/types/testReporter.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/package.json create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/reporter.d.ts create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/reporter.js create mode 100644 mcps/playwright_mcp/node_modules/@playwright/test/reporter.mjs create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/LICENSE create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/NOTICE create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/README.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/ThirdPartyNotices.txt create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/bin/install_media_pack.ps1 create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/bin/install_webkit_wsl.ps1 create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/browsers.json create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/cli.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/index.d.ts create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/index.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/index.mjs create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/androidServerImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/bootstrap.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/browserServerImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/cli/browserActions.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/cli/driver.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/cli/installActions.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/cli/program.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/cli/programWithTestStub.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/android.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/api.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/artifact.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/browser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/browserContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/browserType.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/cdpSession.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/channelOwner.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/clientHelper.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/clientInstrumentation.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/clientStackTrace.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/clock.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/connect.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/connection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/consoleMessage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/coverage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/debugger.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/dialog.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/disposable.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/download.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/electron.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/elementHandle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/errors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/eventEmitter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/events.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/fetch.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/fileChooser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/fileUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/frame.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/harRouter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/input.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/jsHandle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/jsonPipe.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/localUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/locator.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/network.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/page.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/platform.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/playwright.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/screencast.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/selectors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/stream.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/timeoutSettings.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/tracing.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/types.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/video.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/waiter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/webError.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/worker.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/client/writableStream.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/generated/bindingsControllerSource.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/generated/clockSource.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/generated/injectedScriptSource.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/generated/pollingRecorderSource.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/generated/storageScriptSource.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/generated/utilityScriptSource.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/generated/webSocketMockSource.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/inProcessFactory.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/inprocess.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/mcpBundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/mcpBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/outofprocess.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/protocol/serializers.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/protocol/validator.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/protocol/validatorPrimitives.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/remote/playwrightConnection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/remote/playwrightPipeServer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/remote/playwrightServer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/remote/playwrightWebSocketServer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/remote/serverTransport.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/android/android.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/android/backendAdb.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/artifact.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiChromium.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiConnection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiDeserializer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiFirefox.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiInput.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiNetworkManager.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiOverCdp.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiPage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/bidiPdf.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/third_party/bidiCommands.d.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/third_party/bidiKeyboard.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/third_party/bidiProtocol.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/third_party/bidiProtocolCore.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/third_party/bidiProtocolPermissions.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/third_party/bidiSerializer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/bidi/third_party/firefoxPrefs.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/browser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/browserContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/browserType.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/callLog.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/appIcon.png create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/chromium.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/chromiumSwitches.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crConnection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crCoverage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crDevTools.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crDragDrop.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crInput.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crNetworkManager.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crPage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crPdf.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crProtocolHelper.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/crServiceWorker.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/defaultFontFamilies.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/chromium/protocol.d.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/clock.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/csharp.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/java.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/javascript.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/jsonl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/language.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/languages.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/python.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/codegen/types.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/console.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/cookieStore.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/debugController.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/debugger.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/deviceDescriptors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dialog.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/androidDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/artifactDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/browserContextDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/browserDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/browserTypeDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/cdpSessionDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/debugControllerDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/debuggerDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/dialogDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/dispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/disposableDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/electronDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/elementHandlerDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/frameDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/jsHandleDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/jsonPipeDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/localUtilsDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/networkDispatchers.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/pageDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/playwrightDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/streamDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/tracingDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/webSocketRouteDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dispatchers/writableStreamDispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/disposable.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/dom.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/download.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/electron/electron.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/electron/loader.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/errors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/fetch.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/fileChooser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/fileUploadUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/ffBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/ffConnection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/ffExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/ffInput.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/ffNetworkManager.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/ffPage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/firefox.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/firefox/protocol.d.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/formData.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/frameSelectors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/frames.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/har/harRecorder.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/har/harTracer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/harBackend.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/helper.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/index.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/input.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/instrumentation.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/javascript.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/launchApp.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/localUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/macEditingCommands.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/network.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/overlay.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/page.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/pipeTransport.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/playwright.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/progress.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/protocolError.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/recorder.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/recorder/chat.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/recorder/recorderApp.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/recorder/recorderRunner.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/recorder/recorderUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/recorder/throttledFile.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/registry/browserFetcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/registry/dependencies.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/registry/index.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/registry/nativeDeps.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/registry/oopDownloadBrowserMain.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/screencast.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/screenshotter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/selectors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/socksClientCertificatesInterceptor.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/socksInterceptor.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/trace/recorder/snapshotter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/trace/recorder/snapshotterInjected.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/trace/recorder/tracing.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/trace/viewer/traceViewer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/transport.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/types.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/usKeyboardLayout.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/ascii.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/comparators.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/crypto.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/debug.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/debugLogger.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/disposable.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/env.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/eventsHelper.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/expectUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/fileUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/happyEyeballs.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/hostPlatform.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/httpServer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/image_tools/colorUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/image_tools/compare.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/image_tools/imageChannel.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/image_tools/stats.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/linuxUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/network.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/nodePlatform.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/pipeTransport.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/processLauncher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/profiler.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/socksProxy.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/spawnAsync.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/task.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/userAgent.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/wsServer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/zipFile.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/utils/zones.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/videoRecorder.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/protocol.d.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/webkit.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkBrowser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkConnection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkExecutionContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkInput.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkInterceptableRequest.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkPage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkProvisionalPage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/server/webkit/wkWorkers.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/serverRegistry.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/third_party/pixelmatch.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/browserBackend.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/common.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/config.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/console.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/context.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/cookies.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/devtools.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/dialogs.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/evaluate.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/files.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/form.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/keyboard.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/logFile.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/mouse.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/navigate.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/network.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/pdf.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/response.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/route.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/runCode.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/screenshot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/sessionLog.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/snapshot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/storage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/tab.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/tabs.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/tool.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/tools.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/tracing.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/utils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/verify.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/video.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/wait.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/backend/webstorage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/cli.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/help.json create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/minimist.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/program.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/registry.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/session.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/SKILL.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/element-attributes.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/playwright-tests.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/request-mocking.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/running-code.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/session-management.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/storage-state.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/tracing.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-client/skill/references/video-recording.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-daemon/command.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-daemon/commands.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-daemon/daemon.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-daemon/helpGenerator.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/cli-daemon/program.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/dashboard/appIcon.png create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/dashboard/dashboardApp.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/dashboard/dashboardController.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/exports.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/browserFactory.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/cdpRelay.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/cli-stub.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/config.d.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/config.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/configIni.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/extensionContextFactory.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/index.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/log.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/program.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/protocol.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/mcp/watchdog.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/SKILL.md create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/installSkill.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceActions.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceAttachments.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceCli.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceConsole.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceErrors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceOpen.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceParser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceRequests.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceScreenshot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/trace/traceUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/utils/connect.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/utils/mcp/http.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/utils/mcp/server.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/utils/mcp/tool.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/tools/utils/socketConnection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/ariaSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/assert.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/colors.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/cssParser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/cssTokenizer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/formatUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/headers.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/imageUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/jsonSchema.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/locatorGenerators.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/locatorParser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/locatorUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/lruCache.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/manualPromise.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/mimeType.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/multimap.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/protocolFormatter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/protocolMetainfo.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/rtti.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/selectorParser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/semaphore.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/stackTrace.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/stringUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/time.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/timeoutRunner.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/entries.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/snapshotRenderer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/snapshotServer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/snapshotStorage.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/traceLoader.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/traceModel.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/traceModernizer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/traceUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV3.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV4.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV5.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV6.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV7.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/trace/versions/traceV8.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/types.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/urlMatch.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/utilityScriptSerializers.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utils/isomorphic/yaml.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utilsBundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/utilsBundleImpl/index.js create mode 100755 mcps/playwright_mcp/node_modules/playwright-core/lib/utilsBundleImpl/xdg-open create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/dashboard/assets/index-BAOybkp8.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/dashboard/assets/index-CZAYOG76.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/dashboard/index.html create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/htmlReport/index.html create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/htmlReport/report.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/htmlReport/report.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-C8KMvO9L.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/recorder/assets/index-BSjZa4pk.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/recorder/assets/index-CqAYX1I3.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/recorder/index.html create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-DqGGleTZ.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-CWq3uECg.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.B4dS75f0.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/index.C5r2ipUR.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/index.html create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Btcz36p_.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/uiMode.ClIxK9Iu.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/zipBundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/zipBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/zodBundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/lib/zodBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/package.json create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/types/protocol.d.ts create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/types/structs.d.ts create mode 100644 mcps/playwright_mcp/node_modules/playwright-core/types/types.d.ts create mode 100644 mcps/playwright_mcp/node_modules/playwright/LICENSE create mode 100644 mcps/playwright_mcp/node_modules/playwright/NOTICE create mode 100644 mcps/playwright_mcp/node_modules/playwright/README.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/ThirdPartyNotices.txt create mode 100755 mcps/playwright_mcp/node_modules/playwright/cli.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/index.d.ts create mode 100644 mcps/playwright_mcp/node_modules/playwright/index.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/index.mjs create mode 100644 mcps/playwright_mcp/node_modules/playwright/jsx-runtime.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/jsx-runtime.mjs create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/agentParser.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/copilot-setup-steps.yml create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/generateAgents.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/playwright-test-generator.agent.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/playwright-test-healer.agent.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/agents/playwright-test-planner.agent.md create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/config.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/configLoader.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/esmLoaderHost.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/expectBundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/expectBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/fixtures.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/globals.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/ipc.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/poolBuilder.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/process.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/suiteUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/test.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/testLoader.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/testType.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/common/validators.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/errorContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/fsWatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/index.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/internalsForTest.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/events.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/folders.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/stringInternPool.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/teleReceiver.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/teleSuiteUpdater.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/testServerConnection.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/testServerInterface.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/testTree.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/isomorphic/types.d.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/loader/loaderMain.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/expect.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/matcherHint.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/matchers.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/toBeTruthy.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/toEqual.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/toHaveURL.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/toMatchAriaSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/toMatchSnapshot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/matchers/toMatchText.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/browserBackend.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/generatorTools.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/plannerTools.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/seed.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/streams.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/testBackend.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/testContext.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/testTool.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/mcp/test/testTools.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/plugins/gitCommitInfoPlugin.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/plugins/index.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/plugins/webServerPlugin.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/program.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reportActions.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/base.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/blob.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/dot.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/empty.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/github.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/html.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/internalReporter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/json.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/junit.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/line.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/list.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/listModeReporter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/markdown.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/merge.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/multiplexer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/reporterV2.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/teleEmitter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/reporters/versions/blobV1.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/dispatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/failureTracker.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/lastRun.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/loadUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/loaderHost.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/processHost.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/projectUtils.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/rebase.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/reporters.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/sigIntWatcher.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/taskRunner.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/tasks.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/testGroups.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/testRunner.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/testServer.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/uiModeReporter.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/vcs.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/watchMode.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/runner/workerHost.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/testActions.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/third_party/pirates.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/third_party/tsconfig-loader.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/transform/babelBundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/transform/babelBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/transform/compilationCache.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/transform/esmLoader.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/transform/portTransport.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/transform/transform.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/util.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/utilsBundle.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/utilsBundleImpl.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/worker/fixtureRunner.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/worker/testInfo.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/worker/testTracing.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/worker/timeoutManager.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/worker/util.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/lib/worker/workerMain.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/package.json create mode 100644 mcps/playwright_mcp/node_modules/playwright/test.d.ts create mode 100644 mcps/playwright_mcp/node_modules/playwright/test.js create mode 100644 mcps/playwright_mcp/node_modules/playwright/test.mjs create mode 100644 mcps/playwright_mcp/node_modules/playwright/types/test.d.ts create mode 100644 mcps/playwright_mcp/node_modules/playwright/types/testReporter.d.ts create mode 100644 mcps/playwright_mcp/package-lock.json create mode 100644 mcps/playwright_mcp/package.json create mode 100644 mcps/selenium_mcp/README.md create mode 100644 mcps/selenium_mcp/__pycache__/selenium_mcp_server.cpython-312.pyc create mode 100755 mcps/selenium_mcp/launch_selenium_mcp_server.sh create mode 100644 mcps/selenium_mcp/mcp_config.json create mode 100644 mcps/selenium_mcp/poetry.lock create mode 100644 mcps/selenium_mcp/pyproject.toml create mode 100644 mcps/selenium_mcp/selenium_mcp_server.py create mode 100644 mcps/selenium_mcp/test_client.py create mode 100644 skills/artifacts-builder/LICENSE.txt create mode 100644 skills/artifacts-builder/SKILL.md create mode 100755 skills/artifacts-builder/scripts/bundle-artifact.sh create mode 100755 skills/artifacts-builder/scripts/init-artifact.sh create mode 100644 skills/artifacts-builder/scripts/shadcn-components.tar.gz create mode 100644 skills/document-skills/docx/LICENSE.txt create mode 100644 skills/document-skills/docx/SKILL.md create mode 100644 skills/document-skills/docx/docx-js.md create mode 100644 skills/document-skills/docx/ooxml.md create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/mce/mc.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/microsoft/wml-2010.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/microsoft/wml-2012.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/microsoft/wml-2018.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd create mode 100644 skills/document-skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd create mode 100755 skills/document-skills/docx/ooxml/scripts/pack.py create mode 100755 skills/document-skills/docx/ooxml/scripts/unpack.py create mode 100755 skills/document-skills/docx/ooxml/scripts/validate.py create mode 100644 skills/document-skills/docx/ooxml/scripts/validation/__init__.py create mode 100644 skills/document-skills/docx/ooxml/scripts/validation/base.py create mode 100644 skills/document-skills/docx/ooxml/scripts/validation/docx.py create mode 100644 skills/document-skills/docx/ooxml/scripts/validation/pptx.py create mode 100644 skills/document-skills/docx/ooxml/scripts/validation/redlining.py create mode 100755 skills/document-skills/docx/scripts/__init__.py create mode 100755 skills/document-skills/docx/scripts/document.py create mode 100644 skills/document-skills/docx/scripts/templates/comments.xml create mode 100644 skills/document-skills/docx/scripts/templates/commentsExtended.xml create mode 100644 skills/document-skills/docx/scripts/templates/commentsExtensible.xml create mode 100644 skills/document-skills/docx/scripts/templates/commentsIds.xml create mode 100644 skills/document-skills/docx/scripts/templates/people.xml create mode 100755 skills/document-skills/docx/scripts/utilities.py create mode 100644 skills/document-skills/pdf/LICENSE.txt create mode 100644 skills/document-skills/pdf/SKILL.md create mode 100644 skills/document-skills/pdf/forms.md create mode 100644 skills/document-skills/pdf/reference.md create mode 100644 skills/document-skills/pdf/scripts/check_bounding_boxes.py create mode 100644 skills/document-skills/pdf/scripts/check_bounding_boxes_test.py create mode 100644 skills/document-skills/pdf/scripts/check_fillable_fields.py create mode 100644 skills/document-skills/pdf/scripts/convert_pdf_to_images.py create mode 100644 skills/document-skills/pdf/scripts/create_validation_image.py create mode 100644 skills/document-skills/pdf/scripts/extract_form_field_info.py create mode 100644 skills/document-skills/pdf/scripts/fill_fillable_fields.py create mode 100644 skills/document-skills/pdf/scripts/fill_pdf_form_with_annotations.py create mode 100644 skills/document-skills/pptx/LICENSE.txt create mode 100644 skills/document-skills/pptx/SKILL.md create mode 100644 skills/document-skills/pptx/html2pptx.md create mode 100644 skills/document-skills/pptx/ooxml.md create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/mce/mc.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd create mode 100644 skills/document-skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd create mode 100755 skills/document-skills/pptx/ooxml/scripts/pack.py create mode 100755 skills/document-skills/pptx/ooxml/scripts/unpack.py create mode 100755 skills/document-skills/pptx/ooxml/scripts/validate.py create mode 100644 skills/document-skills/pptx/ooxml/scripts/validation/__init__.py create mode 100644 skills/document-skills/pptx/ooxml/scripts/validation/base.py create mode 100644 skills/document-skills/pptx/ooxml/scripts/validation/docx.py create mode 100644 skills/document-skills/pptx/ooxml/scripts/validation/pptx.py create mode 100644 skills/document-skills/pptx/ooxml/scripts/validation/redlining.py create mode 100755 skills/document-skills/pptx/scripts/html2pptx.js create mode 100755 skills/document-skills/pptx/scripts/inventory.py create mode 100755 skills/document-skills/pptx/scripts/rearrange.py create mode 100755 skills/document-skills/pptx/scripts/replace.py create mode 100755 skills/document-skills/pptx/scripts/thumbnail.py create mode 100644 skills/document-skills/xlsx/LICENSE.txt create mode 100644 skills/document-skills/xlsx/SKILL.md create mode 100644 skills/document-skills/xlsx/recalc.py create mode 100644 skills/mcp-builder/LICENSE.txt create mode 100644 skills/mcp-builder/SKILL.md create mode 100644 skills/mcp-builder/reference/evaluation.md create mode 100644 skills/mcp-builder/reference/mcp_best_practices.md create mode 100644 skills/mcp-builder/reference/node_mcp_server.md create mode 100644 skills/mcp-builder/reference/python_mcp_server.md create mode 100644 skills/mcp-builder/scripts/connections.py create mode 100644 skills/mcp-builder/scripts/evaluation.py create mode 100644 skills/mcp-builder/scripts/example_evaluation.xml create mode 100644 skills/mcp-builder/scripts/requirements.txt create mode 100644 skills/skill-creator/LICENSE.txt create mode 100644 skills/skill-creator/SKILL.md create mode 100755 skills/skill-creator/scripts/init_skill.py create mode 100755 skills/skill-creator/scripts/package_skill.py create mode 100755 skills/skill-creator/scripts/quick_validate.py create mode 100644 skills/webapp-testing/LICENSE.txt create mode 100644 skills/webapp-testing/SKILL.md create mode 100644 skills/webapp-testing/examples/console_logging.py create mode 100644 skills/webapp-testing/examples/element_discovery.py create mode 100644 skills/webapp-testing/examples/static_html_automation.py create mode 100755 skills/webapp-testing/scripts/with_server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f76c3c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/.DS_Store +**/.idea/ +**/deploy/ +*.log diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..7d6acf8 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,28 @@ +{ + "mcpServers": { + "dicom-mcp": { + "command": "zsh", + "args": ["mcps/dicom_mcp/run_dicom_mcp_server.sh"] + }, + "filesystem": { + "command": "zsh", + "args": ["mcps/filesystem_mcp/launch_filesystem_mcp.sh"], + "env": { + "MCP_LOG_LEVEL": "debug", + "MCP_TRANSPORT_TYPE": "stdio" + } + }, + "open-meteo-mcp": { + "command": "zsh", + "args": ["mcps/open_meteo_mcp/run_open_meteo_mcp_server.sh"] + }, + "playwright-mcp": { + "command": "zsh", + "args": ["mcps/playwright_mcp/launch_playwright_mcp.sh"] + }, + "selenium-mcp": { + "command": "zsh", + "args": ["mcps/selenium_mcp/launch_selenium_mcp_server.sh"] + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9059dea --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Claude Tools + +## Overview + +A collection of Claude skills, and "Model Context Protocol" servers, that are useful to the functional quality team, or anyone else interested in using some of Claude's more exotic capabilities. + +## Requirements + +* Python 3.10+ +* Node.js (for filesystem and playwright MCPs) +* Poetry (for Python-based MCPs) +* Claude Desktop and/or Claude Code + +## Structure + +``` +claude-tools/ + |-- .claude/ Claude Code project settings + |-- .mcp.json MCP server registration for Claude Code + |-- config/ Config files for Claude Desktop (~/.config/Claude/) + |-- mcps/ MCP servers + | |-- dicom_mcp/ DICOM medical imaging inspection (17 read-only tools) + | |-- filesystem_mcp/ File system operations via @cyanheads/filesystem-mcp-server + | |-- open_meteo_mcp/ Global weather data via the Open-Meteo API + | |-- playwright_mcp/ Browser automation via Playwright + | |-- selenium_mcp/ Browser automation via Selenium WebDriver + |-- skills/ Skill packages + |-- artifacts-builder/ Build multi-component HTML artifacts with React, Tailwind, shadcn/ui + |-- document-skills/ Document creation and manipulation + | |-- docx/ Word document creation, editing, and analysis + | |-- pdf/ PDF extraction, creation, merging, splitting, and forms + | |-- pptx/ PowerPoint creation, editing, and analysis + | |-- xlsx/ Spreadsheet creation, analysis, and formula support + |-- mcp-builder/ Guide for building MCP servers (Python FastMCP / Node SDK) + |-- skill-creator/ Guide for creating new Claude Code skills + |-- webapp-testing/ Test local web applications with Playwright scripts +``` + +## Setup + +### Claude Code + +The `.mcp.json` at the project root registers all MCP servers. Opening this project in Claude Code will make them available. The `.claude/settings.local.json` enables all servers and registers skill paths. + +### Claude Desktop + +Copy (or symlink) `config/claude_desktop_config.json` to your Claude Desktop config directory: + +```bash +cp config/claude_desktop_config.json ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +## Claude Docs + +### Skills + +- **Skills Overview**: [Agent Skills - Claude Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) +- **Creating Skills in Claude Code**: [Agent Skills - Claude Code Docs](https://code.claude.com/docs/en/skills) +- **Using Skills with the API**: [Using Agent Skills with the API - Claude Docs](https://platform.claude.com/docs/en/build-with-claude/skills-guide) +- **Skills API Quickstart**: [Agent Skills - Claude Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) +- **Skills on Console**: [Agent Skills - Claude Docs](https://console.anthropic.com/docs/en/agents-and-tools/agent-skills/overview) +- **GitHub - anthropics/skills**: [GitHub - anthropics/skills: Public repository for Agent Skills](https://github.com/anthropics/skills) + +### MCPs + +- **MCP Documentation (main)**: [MCP Docs – Model Context Protocol (MCP)](https://modelcontextprotocol.info/docs/) +- **MCP Specification**: [GitHub - modelcontextprotocol/modelcontextprotocol: Specification and documentation for the Model Context Protocol](https://github.com/modelcontextprotocol/modelcontextprotocol) +- **MCP Specification Repository**: [GitHub - modelcontextprotocol/modelcontextprotocol: Specification and documentation for the Model Context Protocol](https://github.com/modelcontextprotocol/modelcontextprotocol) +- **MCP Documentation Repository**: [GitHub - modelcontextprotocol/docs: Documentation for the Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/docs) +- **MCP in Claude Docs**: https://anthropic.mintlify.app/en/docs/mcp diff --git a/config/claude_desktop_config.json b/config/claude_desktop_config.json new file mode 100644 index 0000000..506d936 --- /dev/null +++ b/config/claude_desktop_config.json @@ -0,0 +1,27 @@ +{ + "mcpServers": { + "dicom-mcp": { + "command": "/Users/gregory.gauthier/Projects/pd/claude-tools/mcps/dicom_mcp/run_dicom_mcp_server.sh" + }, + "filesystem": { + "command": "zsh", + "args": ["/Users/gregory.gauthier/Projects/pd/claude-tools/mcps/filesystem_mcp/launch_filesystem_mcp.sh"], + "env": { + "MCP_LOG_LEVEL": "debug", + "MCP_TRANSPORT_TYPE": "stdio" + } + }, + "open-meteo-mcp": { + "command": "/Users/gregory.gauthier/Projects/pd/claude-tools/mcps/open_meteo_mcp/run_open_meteo_mcp_server.sh" + }, + "playwright-mcp": { + "command": "/Users/gregory.gauthier/Projects/pd/claude-tools/mcps/playwright_mcp/launch_playwright_mcp.sh" + }, + "selenium-mcp": { + "command": "/Users/gregory.gauthier/Projects/pd/claude-tools/mcps/selenium_mcp/launch_selenium_mcp_server.sh" + } + }, + "preferences": { + "quickEntryDictationShortcut": "capslock" + } +} diff --git a/mcps/dicom_mcp/.continue/rules/xai-model-fix.md b/mcps/dicom_mcp/.continue/rules/xai-model-fix.md new file mode 100644 index 0000000..3779262 --- /dev/null +++ b/mcps/dicom_mcp/.continue/rules/xai-model-fix.md @@ -0,0 +1,6 @@ +--- +description: Use when Edit feature fails with xAI "Raw sampling not supported" error +alwaysApply: false +--- + +For Edit operations (Cmd/Ctrl+I), prefer non-xAI models like gpt-4o or claude-3.5-sonnet. xAI reasoning models don't support raw sampling needed for Edit. \ No newline at end of file diff --git a/mcps/dicom_mcp/.gitignore b/mcps/dicom_mcp/.gitignore new file mode 100644 index 0000000..ac46434 --- /dev/null +++ b/mcps/dicom_mcp/.gitignore @@ -0,0 +1,7 @@ +.ai/ +.idea/ +.claude/ +__pycache__/ +build/ +dist/ +poetry.lock diff --git a/mcps/dicom_mcp/.mcp.json b/mcps/dicom_mcp/.mcp.json new file mode 100644 index 0000000..b4ef447 --- /dev/null +++ b/mcps/dicom_mcp/.mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "dicom_mcp": { + "command": "/data/Projects/mcp_servers/dicom_mcp/run_dicom_mcp_server.sh" + } + } +} diff --git a/mcps/dicom_mcp/CLAUDE.md b/mcps/dicom_mcp/CLAUDE.md new file mode 100644 index 0000000..fedd5f6 --- /dev/null +++ b/mcps/dicom_mcp/CLAUDE.md @@ -0,0 +1,152 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +A Model Context Protocol (MCP) server that provides DICOM medical imaging QA tools to Claude. Built with FastMCP and pydicom, it exposes 17 read-only async tools for analyzing DICOM files, with a focus on body composition analysis, Dixon sequence validation, pixel analysis, Philips private tag resolution, UID comparison, segmentation verification, and T1 mapping (MOLLI/NOLLI) analysis. + +## Commands + +```bash +# Install dependencies +poetry install --with dev + +# Run all tests +poetry run pytest -v --tb=short + +# Run a single test class +poetry run pytest test_dicom_mcp.py::TestToolRegistration -v + +# Run a single test +poetry run pytest test_dicom_mcp.py::TestToolRegistration::test_all_tools_registered -v + +# Run the MCP server directly +poetry run python -m dicom_mcp + +# Run with PII filtering enabled +DICOM_MCP_PII_FILTER=true poetry run python -m dicom_mcp +``` + +## Architecture + +The project is structured as a Python package (`dicom_mcp/`) with a backward-compatible shim at the root (`dicom_mcp.py`). All tests live in `test_dicom_mcp.py`. + +### Package Structure + +``` +dicom_mcp/ + __init__.py # Re-exports all public symbols for backward compat + __main__.py # Entry point: python -m dicom_mcp + server.py # mcp = FastMCP("dicom_mcp") + run() + config.py # Env-var-driven config (PII_FILTER_ENABLED, MAX_FILES) + constants.py # Enums (ResponseFormat, SequenceType), COMMON_TAGS, VALID_TAG_GROUPS + pii.py # PII tag set + redaction functions + helpers/ + __init__.py # Re-exports all helper functions + tags.py # _safe_get_tag, _format_tag_value, _resolve_tag, _validate_tag_groups, _format_markdown_table + sequence.py # _identify_sequence_type, _is_dixon_sequence, _get_dixon_image_types + files.py # _is_dicom_file, _find_dicom_files + philips.py # _resolve_philips_private_tag, _list_philips_private_creators + pixels.py # _get_pixel_array, _extract_roi, _compute_stats, _apply_windowing + filters.py # _parse_filter, _apply_filter + tree.py # _build_tree_text, _build_tree_json, _format_tree_value + tools/ + __init__.py # Imports all tool modules to trigger @mcp.tool() registration + discovery.py # dicom_list_files, dicom_find_dixon_series + metadata.py # dicom_get_metadata, dicom_compare_headers + query.py # dicom_query, dicom_summarize_directory + validation.py # dicom_validate_sequence, dicom_analyze_series + search.py # dicom_search + philips.py # dicom_query_philips_private + pixels.py # dicom_read_pixels, dicom_compute_snr, dicom_render_image + tree.py # dicom_dump_tree + uid_comparison.py # dicom_compare_uids + segmentation.py # dicom_verify_segmentations + ti_analysis.py # dicom_analyze_ti +dicom_mcp.py # Thin shim so `python dicom_mcp.py` still works +``` + +### Import Chain (No Circular Dependencies) + +- `config.py` and `constants.py` are leaf modules (no internal imports) +- `server.py` only imports `FastMCP` externally — owns the `mcp` instance +- `helpers/*.py` import from `constants.py`, `config.py`, pydicom/numpy (never `server.py`) +- `tools/*.py` import `mcp` from `server.py`, plus helpers and constants +- `tools/__init__.py` imports all tool modules (triggers `@mcp.tool()` registration) +- `server.run()` does a deferred import of `dicom_mcp.tools` before starting +- `__init__.py` re-exports everything so `import dicom_mcp` still works + +**Tool structure:** Each tool is an async function that takes a directory/file path, performs DICOM analysis using pydicom, and returns formatted results (markdown or JSON). All tools are read-only and annotated with `ToolAnnotations(readOnlyHint=True)`. All blocking I/O is wrapped in `asyncio.to_thread()` to prevent event loop blocking. + +**The 17 tools:** +- `dicom_list_files` — Recursively find DICOM files, optionally filtered by sequence type; supports count_only mode +- `dicom_get_metadata` — Extract DICOM header info from a single file using tag groups; supports Philips private tags via `philips_private_tags` parameter +- `dicom_compare_headers` — Compare headers across 2-10 files side-by-side +- `dicom_find_dixon_series` — Identify Dixon sequences and detect image types (water/fat/in-phase/out-phase) +- `dicom_validate_sequence` — Validate sequence parameters (TR, TE, flip angle) against expected values +- `dicom_analyze_series` — Comprehensive series analysis checking parameter consistency and completeness +- `dicom_summarize_directory` — High-level overview of directory contents with field-level summaries +- `dicom_query` — Query arbitrary DICOM tags across a directory with optional grouping +- `dicom_search` — Search DICOM files using filter syntax (text, numeric, presence operators) +- `dicom_query_philips_private` — Query Philips private DICOM tags by DD number and element offset; can list all Private Creator tags or resolve specific private elements +- `dicom_read_pixels` — Extract pixel statistics with optional ROI and histogram +- `dicom_compute_snr` — Compute signal-to-noise ratio from two ROIs +- `dicom_render_image` — Render DICOM to PNG with windowing and ROI overlays +- `dicom_dump_tree` — Full hierarchical dump of DICOM structure including nested sequences; configurable depth and private tag visibility +- `dicom_compare_uids` — Compare UID sets (e.g. SeriesInstanceUID) between two DICOM directories; supports any tag keyword or hex code +- `dicom_verify_segmentations` — Validate that segmentation DICOM files reference valid source images via SourceImageSequence +- `dicom_analyze_ti` — Extract and validate inversion times from MOLLI/T1 mapping sequences across vendors; handles Philips private TI tags automatically + +**Key constants:** +- `MAX_FILES` — Safety limit for directory scans (default 1000, configurable via `DICOM_MCP_MAX_FILES` env var) +- `COMMON_TAGS` — Dictionary of 9 tag groups (patient_info, study_info, series_info, image_info, acquisition, manufacturer, equipment, geometry, pixel_data) mapping to DICOM tag tuples +- `VALID_TAG_GROUPS` — Sorted list of valid tag group names + +## PII Filtering + +Patient-identifying tags can be redacted from tool output via an environment variable: + +- **Enable:** `DICOM_MCP_PII_FILTER=true` (accepts `true`, `1`, `yes`, case-insensitive) +- **Disable:** unset or any other value (default) + +**Redacted tags** (patient tags only): PatientName `(0010,0010)`, PatientID `(0010,0020)`, PatientBirthDate `(0010,0030)`, PatientSex `(0010,0040)`. + +**Affected tools:** `dicom_get_metadata`, `dicom_compare_headers`, `dicom_summarize_directory`, `dicom_query`. All other tools do not expose patient data and are unaffected. + +Redaction is applied at the output formatting level via `redact_if_pii()` in `pii.py`. Internal logic (sequence identification, grouping) uses raw values. + +## Behavioural Constraints + +This MCP is a **data inspection tool**, not a clinical decision support system. When using these tools, keep responses strictly factual and descriptive: + +- **Report** what is observed in the DICOM data (tag values, pixel statistics, parameters, counts) +- **Describe** technical characteristics (acquisition settings, sequence types, vendor differences) +- **Do not** suggest clinical utility, diagnostic applications, or workflow suitability +- **Do not** interpret findings in a clinical or diagnostic context +- **Do not** assess data quality relative to specific clinical use cases +- **Do not** recommend clinical actions based on the data + +> Present data as-is. Qualified professionals draw the conclusions. + +These constraints are enforced at protocol level via `FastMCP(instructions=...)` in `server.py`, which sends them to any MCP client at connection time. See **[docs/GUIDELINES.md](docs/GUIDELINES.md)** for the full policy and regulatory context. + +## Key Patterns + +- All tools use `ResponseFormat` enum (MARKDOWN/JSON) for output formatting +- `SequenceType` enum covers: DIXON, T1_MAPPING, MULTI_ECHO_GRE, SPIN_ECHO_IR, T1, T2, FLAIR, DWI, LOCALIZER, UNKNOWN +- DICOM files are validated by checking for the 128-byte preamble + "DICM" magic bytes in `_is_dicom_file()` +- Custom DICOM tags can be specified in hex format `(GGGG,EEEE)` +- Philips private tags are resolved dynamically per-file via `_resolve_philips_private_tag()` — block assignments vary across scanners +- All synchronous I/O (pydicom.dcmread, file globbing) is wrapped in `asyncio.to_thread()` to keep the event loop responsive +- `_find_dicom_files()` returns `tuple[list[tuple[Path, Dataset]], bool]` — pre-read datasets + truncation flag — to avoid double-reading files +- The server imports `FastMCP` from `mcp.server.fastmcp` (not directly from `fastmcp`) + +## Documentation + +Additional documentation lives in the `docs/` directory: + +- **[docs/USAGE.md](docs/USAGE.md)** — Detailed tool reference, parameter guide, and QA workflow examples +- **[docs/TODO.md](docs/TODO.md)** — Planned improvements and known issues +- **[docs/CAPABILITIES.md](docs/CAPABILITIES.md)** — Plain-English summary of all 17 tool capabilities +- **[docs/GUIDELINES.md](docs/GUIDELINES.md)** — Behavioural constraints and regulatory context diff --git a/mcps/dicom_mcp/INSTALL.md b/mcps/dicom_mcp/INSTALL.md new file mode 100644 index 0000000..19b50ca --- /dev/null +++ b/mcps/dicom_mcp/INSTALL.md @@ -0,0 +1,389 @@ +# Installation Guide + +This guide covers installing the DICOM MCP server for use with Claude Desktop, Claude Code, or any other MCP-compatible client. + +## Table of Contents + +- [Supported Clients](#supported-clients) +- [1. Installation](#1-installation) + - [1a. Standalone Package (Non-Developers)](#1a-standalone-package-non-developers) + - [1b. Developer Installation (Poetry)](#1b-developer-installation-poetry) +- [2. Client Configuration](#2-client-configuration) + - [2a. Claude Desktop](#2a-claude-desktop) + - [2b. Claude Code](#2b-claude-code) +- [3. PII Filtering](#3-pii-filtering) +- [4. Environment Variables](#4-environment-variables) +- [5. Verifying the Installation](#5-verifying-the-installation) +- [6. Troubleshooting](#6-troubleshooting) + +--- + +## Supported Clients + +This MCP server communicates via **stdio** — it runs as a local subprocess on your machine. This means it only works with Claude clients that can spawn local processes. + +| Client | Supported? | Config file | Notes | +|--------|-----------|-------------|-------| +| **Claude Desktop** (native app) | Yes | `claude_desktop_config.json` | GUI-based, single config file per platform | +| **Claude Code** (terminal) | Yes | `.mcp.json` or `~/.claude.json` | CLI-based, multiple scope levels | +| **Claude Code** (VS Code / IDE) | Yes | Same as above | Same CLI, same config files — the IDE terminal makes no difference | +| **claude.ai** (web browser) | No | N/A | Cannot spawn local processes; would require an HTTP/SSE remote server wrapper | + +This server implements the open [Model Context Protocol](https://modelcontextprotocol.io/) standard, so it also works with other MCP-compatible clients such as **ChatGPT**, Cursor, and Windsurf. Configuration will vary by client — refer to your client's MCP documentation. The instructions below cover Claude Desktop and Claude Code specifically. + +If you're unsure which client you have: **Claude Desktop** is the native app with a chat window. **Claude Code** is the `claude` command you run in a terminal (standalone, VS Code, or any other IDE). **claude.ai** is the website at claude.ai. + +--- + +## 1. Installation + +Choose the installation method that matches your environment. + +### 1a. Standalone Package (Non-Developers) + +**No Python installation required.** Ideal for radiographers, QA analysts, and other team members who don't have a development environment. Everything is self-contained — Python, pydicom, numpy, and all dependencies are bundled. + +#### macOS + +1. Download the standalone package for your Mac: + - Apple Silicon (M1/M2/M3/M4): `dicom_mcp_standalone_arm64.tar.gz` + - Intel Mac: `dicom_mcp_standalone_x86_64.tar.gz` + +2. Extract and install: +```bash +tar -xzf dicom_mcp_standalone_arm64.tar.gz +cd dicom_mcp_standalone_arm64 +./install_to_claude.sh +``` + +3. Restart Claude Desktop. + +The installer configures Claude Desktop automatically. + +#### Linux + +1. Download `dicom_mcp_standalone_linux_x86_64.tar.gz` (or `aarch64` for ARM). + +2. Extract and install: +```bash +tar -xzf dicom_mcp_standalone_linux_x86_64.tar.gz +cd dicom_mcp_standalone_linux_x86_64 +./install_to_claude.sh +``` + +3. Restart your MCP client. + +### 1b. Developer Installation (Poetry) + +For developers who want to modify the code, run tests, or extend the server. + +#### Prerequisites + +- Python 3.12 or higher +- [Poetry](https://python-poetry.org/docs/#installation) (Python package manager) + +#### Quick Setup + +```bash +# Clone the repository +git clone +cd dicom_mcp + +# Run the automated installer (macOS, configures Claude Desktop) +./install.sh +``` + +#### Manual Setup + +1. Install dependencies: +```bash +poetry install --with dev +``` + +2. Verify the installation: +```bash +poetry run pytest -v --tb=short +``` +You should see all 138 tests passing. + +3. Start the server (to test it manually): +```bash +poetry run python -m dicom_mcp +``` + +After installation, proceed to [Client Configuration](#2-client-configuration) below. + +--- + +## 2. Client Configuration + +Configuration is different for Claude Desktop and Claude Code. If you used the standalone installer (`install_to_claude.sh`), Claude Desktop is already configured — skip to [Verifying the Installation](#5-verifying-the-installation). + +### 2a. Claude Desktop + +Claude Desktop reads its MCP server configuration from a single JSON file. The location depends on your OS: + +| OS | Config file path | +|----|-----------------| +| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| Linux | `~/.config/Claude/claude_desktop_config.json` | +| Windows | `%APPDATA%\Claude\claude_desktop_config.json` | + +#### Using the GUI + +1. Open Claude Desktop and go to Settings (gear icon). +2. Navigate to the Developer section. + + Developer settings + +3. Click **Edit Config** or **Add MCP Server**. + + MCP servers panel + +4. Add the server configuration using one of the JSON examples below. + +#### Poetry (Recommended for Developers) + +```json +{ + "mcpServers": { + "dicom_mcp": { + "command": "poetry", + "args": ["run", "python", "-m", "dicom_mcp"], + "cwd": "/absolute/path/to/dicom_mcp" + } + } +} +``` + +Replace `/absolute/path/to/dicom_mcp` with the actual directory where the project lives. + +#### Virtualenv Python Directly + +If Claude Desktop has trouble finding Poetry, you can point it at the virtualenv Python directly. First, get the path: + +```bash +poetry env info --path +``` + +Then configure: + +```json +{ + "mcpServers": { + "dicom_mcp": { + "command": "/path/to/poetry/virtualenv/bin/python", + "args": ["-m", "dicom_mcp"], + "cwd": "/absolute/path/to/dicom_mcp" + } + } +} +``` + +After adding the configuration, **restart Claude Desktop** to load the server. + +### 2b. Claude Code + +Claude Code supports multiple configuration scopes for MCP servers, each stored in a dedicated file (separate from Claude Code's settings files). Choose the scope that matches your use case. + +#### Scope Overview + +| Scope | Config file | Shared via git? | Use case | +|-------|-------------|-----------------|----------| +| **Project** | `.mcp.json` (project root) | Yes | Team-shared — everyone on the project gets the server | +| **User** | `~/.claude.json` | No | Personal — available in all your projects | +| **Local** | `~/.claude.json` (project-specific section) | No | Personal — available only in the current project | +| **Managed** | System directory (see below) | Enterprise IT | Organisation-wide enforcement | + +**Precedence** (highest to lowest): Managed > Local > Project > User. If the same MCP server name appears at multiple scopes, the higher-precedence scope wins. + +#### Project Scope (Team-Shared) + +Create `.mcp.json` in your project root and commit it to version control. All collaborators will get the server automatically: + +```json +{ + "mcpServers": { + "dicom_mcp": { + "command": "poetry", + "args": ["run", "python", "-m", "dicom_mcp"], + "cwd": "/absolute/path/to/dicom_mcp" + } + } +} +``` + +Replace `/absolute/path/to/dicom_mcp` with the actual directory where the DICOM MCP project lives. + +**Note:** Project-scoped servers require user approval on first use. Users can reset approval with `claude mcp reset-project-choices`. + +#### User Scope (Personal, All Projects) + +Add to `~/.claude.json` to make the server available in every Claude Code session: + +```json +{ + "mcpServers": { + "dicom_mcp": { + "command": "poetry", + "args": ["run", "python", "-m", "dicom_mcp"], + "cwd": "/absolute/path/to/dicom_mcp" + } + } +} +``` + +#### Local Scope (Personal, One Project) + +The local scope is stored in `~/.claude.json` but scoped to a specific project directory. The easiest way to add a local-scoped server is via the CLI: + +```bash +claude mcp add dicom_mcp \ + --scope local \ + -- poetry run python -m dicom_mcp +``` + +#### Managed Scope (Enterprise) + +For organisation-wide deployment, IT administrators can place a managed configuration in: + +| OS | Path | +|----|------| +| macOS | `/Library/Application Support/ClaudeCode/managed-mcp.json` | +| Linux/WSL | `/etc/claude-code/managed-mcp.json` | +| Windows | `C:\Program Files\ClaudeCode\managed-mcp.json` | + +Managed servers cannot be overridden by users and take the highest precedence. + +#### Using the CLI + +Claude Code provides a CLI for managing MCP servers without editing JSON files directly: + +```bash +# Add a server (default: local scope) +claude mcp add dicom_mcp -- poetry run python -m dicom_mcp + +# Add at a specific scope +claude mcp add dicom_mcp --scope user -- poetry run python -m dicom_mcp +claude mcp add dicom_mcp --scope project -- poetry run python -m dicom_mcp + +# List all configured servers +claude mcp list + +# Get details for a specific server +claude mcp get dicom_mcp + +# Remove a server +claude mcp remove dicom_mcp +``` + +#### Virtualenv Python Fallback + +If Claude Code has trouble finding Poetry, point directly at the virtualenv Python. First, get the path: + +```bash +poetry env info --path +``` + +Then use the full Python path in your configuration (at any scope): + +```json +{ + "mcpServers": { + "dicom_mcp": { + "command": "/path/to/poetry/virtualenv/bin/python", + "args": ["-m", "dicom_mcp"], + "cwd": "/absolute/path/to/dicom_mcp" + } + } +} +``` + +After saving any configuration, restart Claude Code (or start a new session) for the changes to take effect. You can verify the server is loaded by running `/mcp` in the Claude Code interface. + +--- + +## 3. PII Filtering + +Patient-identifying tags can be redacted from all tool output by setting an environment variable. This works with any client and any configuration method. + +Add an `env` block to your MCP server configuration: + +```json +{ + "mcpServers": { + "dicom_mcp": { + "command": "...", + "args": ["..."], + "env": { + "DICOM_MCP_PII_FILTER": "true" + } + } + } +} +``` + +When enabled, the following tags are replaced with `[REDACTED]` in all tool output: +- PatientName +- PatientID +- PatientBirthDate +- PatientSex + +This affects `dicom_get_metadata`, `dicom_compare_headers`, `dicom_summarize_directory`, and `dicom_query`. All other tools do not expose patient data and are unaffected. + +**Standalone users:** Edit the Claude Desktop config file that `install_to_claude.sh` created (see [Claude Desktop config locations](#2a-claude-desktop)) and add the `env` block to the existing `dicom_mcp` entry. Keep the `command` value the installer wrote — you're only adding the `env` section. + +To disable, unset the variable or set it to any value other than `true`, `1`, or `yes`. + +--- + +## 4. Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `DICOM_MCP_PII_FILTER` | `false` | Set to `true`, `1`, or `yes` to redact patient tags in tool output | +| `DICOM_MCP_MAX_FILES` | `1000` | Maximum number of DICOM files to scan per directory operation | + +--- + +## 5. Verifying the Installation + +Once configured, restart your client and verify the server is working: + +1. **Claude Desktop**: Ask Claude "What MCP tools do you have available?" — you should see 17 DICOM tools listed. + +2. **Claude Code**: The tools will appear in the tool list. You can also test directly: + ``` + List your available DICOM MCP tools + ``` + +3. **MCP Inspector** (for debugging): + ```bash + npx @modelcontextprotocol/inspector poetry run python -m dicom_mcp + ``` + +--- + +## 6. Troubleshooting + +### Server not appearing after configuration + +- Double-check that the `cwd` path is correct and contains the `dicom_mcp/` package directory. +- Ensure Poetry is installed and on the PATH, or use the virtualenv Python directly. +- Restart the client after any configuration change. + +### ModuleNotFoundError + +The configured Python environment is missing dependencies. Either: +- Run `poetry install` in the project directory, or +- Switch to the virtualenv Python configuration (see above). + +### Permission denied on standalone launcher + +```bash +chmod +x run_dicom_mcp.sh install_to_claude.sh +``` + +### Poetry not found by Claude Desktop + +Claude Desktop may not inherit your shell's PATH. Use the virtualenv Python directly instead of the Poetry command (see configuration examples above). diff --git a/mcps/dicom_mcp/README.md b/mcps/dicom_mcp/README.md new file mode 100644 index 0000000..591f08f --- /dev/null +++ b/mcps/dicom_mcp/README.md @@ -0,0 +1,254 @@ +# DICOM MCP Server + +A Model Context Protocol (MCP) server for analyzing DICOM medical imaging files, specifically designed for QA workflows in body composition analysis and liver imaging. + +## Who is this for? + +This server is designed primarily for **Claude Desktop users** — radiographers, QA analysts, and other non-technical team members who need to inspect and validate DICOM files but don't write code. Claude Desktop has no shell or filesystem access, so the MCP server is the only way to give it DICOM analysis capabilities. + +**Claude Code users** (developers, engineers) generally don't need this. Claude Code can run Python and use pydicom directly, offering more flexibility than the predefined tools here. That said, the MCP can still be useful in Claude Code as a convenience layer if you want consistent, structured QA outputs without writing ad-hoc scripts each session. + +## What is this for? + +This MCP server provides a **plain-language API** for interacting with DICOM data. It doesn't eliminate the need for Python/pydicom knowledge, but it significantly reduces the cognitive load for many common tasks. + +**What it does well:** +- Makes common QA tasks instant (Dixon detection, header comparison, protocol validation) +- Removes boilerplate — no more writing the same pydicom scripts repeatedly +- Natural language interface — "find Dixon sequences" vs `pydicom.dcmread()` loops +- Pixel-level analysis — render images, measure signal statistics, compute SNR +- Consistent output formats across all operations +- Configurable PII filtering to redact patient tags when required + +**What it doesn't (and shouldn't) try to do:** +- Replace pydicom for custom/novel analyses +- Handle every edge case in DICOM +- Be a full DICOM viewer/editor + +It's the perfect "80/20" tool — handles 80% of routine QA tasks with 20% of the effort. For the remaining 20% of complex cases, users still have full Python/pydicom access. + +## Tools + +The DICOM MCP server provides 17 tools across six categories: + +### Directory & File Discovery + +| Tool | Description | +|------|-------------| +| `dicom_list_files` | List DICOM files with metadata filtering and optional `count_only` mode | +| `dicom_summarize_directory` | High-level overview with unique tag values and file counts | +| `dicom_find_dixon_series` | Locate and classify Dixon (chemical shift) sequences | +| `dicom_search` | Find files matching filter criteria (text, numeric, presence operators) | + +### Metadata & Validation + +| Tool | Description | +|------|-------------| +| `dicom_get_metadata` | Extract header information organised by tag groups | +| `dicom_compare_headers` | Side-by-side comparison of up to 10 files | +| `dicom_validate_sequence` | Check acquisition parameters against expected values | +| `dicom_analyze_series` | Comprehensive series consistency and completeness check | +| `dicom_query` | Query arbitrary tags across all files with optional grouping | + +### Philips Private Tags + +| Tool | Description | +|------|-------------| +| `dicom_query_philips_private` | Query Philips private DICOM tags by DD number and element offset | + +### Pixel Analysis + +| Tool | Description | +|------|-------------| +| `dicom_read_pixels` | Pixel statistics (min, max, mean, std, percentiles, histogram) with optional ROI | +| `dicom_compute_snr` | Signal-to-noise ratio from signal and noise ROIs | +| `dicom_render_image` | Render DICOM to PNG with configurable windowing and ROI overlays | + +### Structure & Comparison + +| Tool | Description | +|------|-------------| +| `dicom_dump_tree` | Full hierarchical dump of DICOM structure including nested sequences | +| `dicom_compare_uids` | Compare UID sets between two directories to find shared, missing, or extra UIDs | + +### Segmentation & T1 Mapping + +| Tool | Description | +|------|-------------| +| `dicom_verify_segmentations` | Validate that segmentation files correctly reference their source images | +| `dicom_analyze_ti` | Extract and validate inversion times from MOLLI/NOLLI T1 mapping sequences across vendors | + +For detailed usage examples, parameter reference, and QA workflows, see **[docs/USAGE.md](docs/USAGE.md)**. For a plain-English summary of what each tool does, see **[docs/CAPABILITIES.md](docs/CAPABILITIES.md)**. For behavioural constraints and regulatory context, see **[docs/GUIDELINES.md](docs/GUIDELINES.md)**. + +## Installation + +See **[INSTALL.md](INSTALL.md)** for complete installation instructions covering: + +- **Standalone package** — No Python required, ideal for non-developers +- **Claude Desktop** — Poetry-based developer setup with GUI configuration +- **Claude Code** — Adding the MCP server to your Claude Code environment +- **PII filtering** — How to enable patient data redaction for team deployments + +### Quick Start (Developer) + +```bash +# Install dependencies +poetry install --with dev + +# Run tests to verify +poetry run pytest -v --tb=short + +# Start the server +poetry run python -m dicom_mcp +``` + +## PII Filtering + +Patient-identifying tags can be redacted from all tool output by setting an environment variable: + +```bash +export DICOM_MCP_PII_FILTER=true +``` + +When enabled, the following tags are replaced with `[REDACTED]` in tool responses: +- PatientName +- PatientID +- PatientBirthDate +- PatientSex + +This affects `dicom_get_metadata`, `dicom_compare_headers`, `dicom_summarize_directory`, and `dicom_query`. All other tools do not expose patient data and are unaffected. + +To disable, unset the variable or set it to any value other than `true`, `1`, or `yes`. + +## Project Structure + +``` +dicom_mcp/ # Python package + __init__.py # Re-exports all public symbols + __main__.py # Entry point: python -m dicom_mcp + server.py # FastMCP instance and run() + config.py # Environment-driven configuration + constants.py # Enums and tag group definitions + pii.py # PII redaction functions + helpers/ # Internal helper functions + tags.py # Tag reading, formatting, resolution + sequence.py # Sequence type identification + files.py # DICOM file discovery + philips.py # Philips private tag resolution + pixels.py # Pixel array and statistics + filters.py # Search filter parsing + tree.py # DICOM tree building and formatting + tools/ # MCP tool definitions + discovery.py # dicom_list_files, dicom_find_dixon_series + metadata.py # dicom_get_metadata, dicom_compare_headers + query.py # dicom_query, dicom_summarize_directory + validation.py # dicom_validate_sequence, dicom_analyze_series + search.py # dicom_search + philips.py # dicom_query_philips_private + pixels.py # dicom_read_pixels, dicom_compute_snr, dicom_render_image + tree.py # dicom_dump_tree + uid_comparison.py # dicom_compare_uids + segmentation.py # dicom_verify_segmentations + ti_analysis.py # dicom_analyze_ti +docs/ # Documentation + USAGE.md # Detailed tool reference and QA workflows + TODO.md # Planned improvements and known issues + CAPABILITIES.md # Plain-English summary of all capabilities + GUIDELINES.md # Behavioural constraints and regulatory context +dicom_mcp.py # Backward-compatible entry point shim +test_dicom_mcp.py # Test suite (138 tests) +``` + +## Development + +### Running Tests + +```bash +poetry run pytest -v --tb=short +``` + +The test suite includes 138 tests covering tool registration, helper functions, PII filtering, and full pipeline integration tests using synthetic DICOM files. + +### Linting + +```bash +ruff check dicom_mcp/ dicom_mcp.py test_dicom_mcp.py +black --check dicom_mcp/ dicom_mcp.py test_dicom_mcp.py +``` + +### Building Standalone Packages + +```bash +# macOS (detects architecture automatically) +./build_standalone.sh + +# Linux +./build_standalone_linux.sh +``` + +This bundles Python 3.12, fastmcp, pydicom, numpy, and Pillow into a self-contained package (~50MB) that requires no system Python. + +### MCP Inspector + +To test the server interactively outside of Claude: + +```bash +npx @modelcontextprotocol/inspector poetry run python -m dicom_mcp +``` + +### Extending the Server + +New tools can be added by creating a module in `dicom_mcp/tools/` and importing it from `dicom_mcp/tools/__init__.py`. Each tool module imports the shared `mcp` instance from `dicom_mcp.server` and uses the `@mcp.tool()` decorator. + +Other extension points: +- **Custom tag groups**: Edit `COMMON_TAGS` in `dicom_mcp/constants.py` +- **Sequence type detection**: Modify `_identify_sequence_type()` in `dicom_mcp/helpers/sequence.py` +- **PII tag set**: Edit `PII_TAGS` in `dicom_mcp/pii.py` (derived from `COMMON_TAGS["patient_info"]`) + +## Troubleshooting + +### "Module not found" errors + +Install all dependencies: +```bash +poetry install --with dev +``` + +If the MCP server starts but fails with `ModuleNotFoundError` for numpy or Pillow, the Claude Desktop config may be pointing to a different Python than the Poetry virtualenv. Either install the missing packages into that Python or update the config to use the virtualenv Python directly (see [INSTALL.md](INSTALL.md)). + +### "File not found" errors + +Use absolute paths, not relative paths. Check that the path exists and is accessible. + +### "Not a valid DICOM file" errors + +Verify the file is actually a DICOM file. Try opening with another DICOM viewer or `pydicom.dcmread()` directly. + +### Server not appearing in Claude Desktop + +- Verify the configuration file path is correct +- Check that the configured Python can find all dependencies +- Restart Claude Desktop after configuration changes +- Check Claude Desktop logs for error messages + +### "No pixel data" errors + +Some DICOM files (presentation states, structured reports, derived objects) don't contain pixel data. Use `dicom_get_metadata` to check the file's SOP Class or ImageType before attempting pixel operations. + +## Example Screenshot + +Claude Desktop session example + +## License + +This MCP server is provided as-is for QA and testing purposes. + +## Support + +For issues or questions: + +1. Check the Troubleshooting section above +2. See **[docs/USAGE.md](docs/USAGE.md)** for detailed tool reference and workflow examples +3. See **[INSTALL.md](INSTALL.md)** for installation help +4. Verify your DICOM files with pydicom directly +5. Review MCP server logs in Claude Desktop diff --git a/mcps/dicom_mcp/bitbucket-pipelines.yml b/mcps/dicom_mcp/bitbucket-pipelines.yml new file mode 100644 index 0000000..5b8c413 --- /dev/null +++ b/mcps/dicom_mcp/bitbucket-pipelines.yml @@ -0,0 +1,84 @@ +image: python:3.12 + +definitions: + caches: + poetry: ~/.cache/pypoetry + +pipelines: + default: + - step: + name: Test + caches: + - pip + - poetry + script: + # Install Poetry + - curl -sSL https://install.python-poetry.org | python3 - + - export PATH="/root/.local/bin:$PATH" + + # Verify Poetry installation + - poetry --version + + # Install dependencies + - poetry install --with dev + + # Run tests + - poetry run pytest -v --tb=short + + # Optional: Run linting/type checking if you add them later + # - poetry run ruff check . + # - poetry run mypy . + + branches: + main: + - step: + name: Test on Main + caches: + - pip + - poetry + script: + # Install Poetry + - curl -sSL https://install.python-poetry.org | python3 - + - export PATH="/root/.local/bin:$PATH" + + # Verify Poetry installation + - poetry --version + + # Install dependencies + - poetry install --with dev + + # Run tests with coverage + - poetry run pytest -v --tb=short + + # Optional: Generate coverage report + # - poetry run pytest --cov=. --cov-report=term-missing + + - step: + name: Build Standalone Package + script: + # Build standalone distributable for Linux x86_64 + - chmod +x build_standalone_linux.sh + - ./build_standalone_linux.sh + artifacts: + - dist/*.tar.gz + + pull-requests: + '**': + - step: + name: Test PR + caches: + - pip + - poetry + script: + # Install Poetry + - curl -sSL https://install.python-poetry.org | python3 - + - export PATH="/root/.local/bin:$PATH" + + # Verify Poetry installation + - poetry --version + + # Install dependencies + - poetry install --with dev + + # Run tests + - poetry run pytest -v --tb=short diff --git a/mcps/dicom_mcp/build_standalone.sh b/mcps/dicom_mcp/build_standalone.sh new file mode 100755 index 0000000..7090d36 --- /dev/null +++ b/mcps/dicom_mcp/build_standalone.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +# Build a standalone distributable package for DICOM MCP Server +# Uses python-build-standalone to bundle Python 3.12 + dependencies + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${GREEN}=== Building Standalone DICOM MCP Package ===${NC}\n" + +# Detect architecture +ARCH=$(uname -m) +RELEASE="20260203" +if [ "$ARCH" = "x86_64" ]; then + PYTHON_BUILD="cpython-3.12.12+${RELEASE}-x86_64-apple-darwin-install_only_stripped.tar.gz" + DIST_ARCH="x86_64" +elif [ "$ARCH" = "arm64" ]; then + PYTHON_BUILD="cpython-3.12.12+${RELEASE}-aarch64-apple-darwin-install_only_stripped.tar.gz" + DIST_ARCH="arm64" +else + echo -e "${RED}Unsupported architecture: $ARCH${NC}" + exit 1 +fi + +TEMP_BUILD_DIR="build/temp" +DIST_DIR="build/dicom_mcp_standalone_${DIST_ARCH}" +PYTHON_URL="https://github.com/indygreg/python-build-standalone/releases/download/${RELEASE}/$PYTHON_BUILD" + +# Clean previous builds +rm -rf "build" "dist" +mkdir -p "$TEMP_BUILD_DIR" "$DIST_DIR" + +# Download standalone Python +echo -e "${YELLOW}Downloading Python 3.12 standalone build for ${ARCH}...${NC}" +curl -L "$PYTHON_URL" -o "$TEMP_BUILD_DIR/python.tar.gz" + +# Extract Python +echo -e "${YELLOW}Extracting Python...${NC}" +tar -xzf "$TEMP_BUILD_DIR/python.tar.gz" -C "$TEMP_BUILD_DIR" +mv "$TEMP_BUILD_DIR/python" "$DIST_DIR/python" + +# Set up Python environment +PYTHON_BIN="$DIST_DIR/python/bin/python3" + +# Install dependencies +echo -e "${YELLOW}Installing dependencies...${NC}" +"$PYTHON_BIN" -m pip install --upgrade pip +"$PYTHON_BIN" -m pip install fastmcp==2.0.0 pydicom==3.0.1 numpy==2.4.2 Pillow==12.1.1 + +# Copy server code +echo -e "${YELLOW}Copying server code...${NC}" +cp -r dicom_mcp/ "$DIST_DIR/dicom_mcp/" +cp dicom_mcp.py "$DIST_DIR/" + +# Create launcher script +echo -e "${YELLOW}Creating launcher script...${NC}" +cat > "$DIST_DIR/run_dicom_mcp.sh" << 'EOF' +#!/bin/bash +# DICOM MCP Server Launcher +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" +exec "$SCRIPT_DIR/python/bin/python3" -m dicom_mcp +EOF +chmod +x "$DIST_DIR/run_dicom_mcp.sh" + +# Create installation instructions +cat > "$DIST_DIR/INSTALL.txt" << EOF +DICOM MCP Server - Installation Instructions + +1. Move this entire folder to your preferred location, e.g.: + /Users/yourusername/Applications/dicom_mcp_standalone_${DIST_ARCH} + +2. Edit your Claude Desktop configuration file: + ~/Library/Application Support/Claude/claude_desktop_config.json + +3. Add the following to the "mcpServers" section: + + "dicom_mcp": { + "command": "/path/to/dicom_mcp_standalone_${DIST_ARCH}/run_dicom_mcp.sh" + } + + (Replace /path/to/ with the actual location where you placed this folder) + +4. Restart Claude Desktop + +5. Verify the server is working by asking Claude: + "List available MCP tools" + +You should see 12 DICOM tools available. + +No Python installation required - everything is bundled! +EOF + +# Create a simple installer script for end users +cat > "$DIST_DIR/install_to_claude.sh" << 'EOF' +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +CONFIG_FILE="$HOME/Library/Application Support/Claude/claude_desktop_config.json" + +echo -e "${GREEN}=== DICOM MCP Server - Claude Desktop Setup ===${NC}\n" + +# Create config directory if needed +mkdir -p "$HOME/Library/Application Support/Claude" + +# Check if config exists +if [ -f "$CONFIG_FILE" ]; then + echo -e "${YELLOW}Existing configuration found.${NC}" + echo -e "Please add the following to your mcpServers section:\n" + echo '{ + "dicom_mcp": { + "command": "'"$SCRIPT_DIR/run_dicom_mcp.sh"'" + } +}' + echo -e "\n${YELLOW}Configuration file location:${NC}" + echo "$CONFIG_FILE" +else + echo -e "${YELLOW}Creating new configuration...${NC}" + cat > "$CONFIG_FILE" << CONFIGEOF +{ + "mcpServers": { + "dicom_mcp": { + "command": "$SCRIPT_DIR/run_dicom_mcp.sh" + } + } +} +CONFIGEOF + echo -e "${GREEN}✓ Configuration created${NC}" +fi + +echo -e "\n${GREEN}Next step: Restart Claude Desktop${NC}\n" +EOF +chmod +x "$DIST_DIR/install_to_claude.sh" + +# Create tarball for distribution +echo -e "${YELLOW}Creating distribution package...${NC}" +mkdir -p dist +tar -czf "dist/dicom_mcp_standalone_${DIST_ARCH}.tar.gz" -C build "dicom_mcp_standalone_${DIST_ARCH}" + +# Clean up build directory +rm -rf build + +echo -e "\n${GREEN}=== Build Complete ===${NC}\n" +echo -e "Distribution package created at:" +echo -e "${YELLOW}dist/dicom_mcp_standalone_${DIST_ARCH}.tar.gz${NC}\n" +echo -e "Package size: $(du -h "dist/dicom_mcp_standalone_${DIST_ARCH}.tar.gz" | cut -f1)\n" +echo -e "To distribute:" +echo -e "1. Send the .tar.gz file to users" +echo -e "2. Users extract it: ${YELLOW}tar -xzf dicom_mcp_standalone_${DIST_ARCH}.tar.gz${NC}" +echo -e "3. Users run: ${YELLOW}cd dicom_mcp_standalone_${DIST_ARCH} && ./install_to_claude.sh${NC}\n" diff --git a/mcps/dicom_mcp/build_standalone_linux.sh b/mcps/dicom_mcp/build_standalone_linux.sh new file mode 100755 index 0000000..9e0da4f --- /dev/null +++ b/mcps/dicom_mcp/build_standalone_linux.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +# Build a standalone distributable package for DICOM MCP Server (Linux) +# Uses python-build-standalone to bundle Python 3.12 + dependencies + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${GREEN}=== Building Standalone DICOM MCP Package (Linux) ===${NC}\n" + +# Detect architecture +ARCH=$(uname -m) +RELEASE="20260203" +if [ "$ARCH" = "x86_64" ]; then + PYTHON_BUILD="cpython-3.12.12+${RELEASE}-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" + DIST_ARCH="x86_64" +elif [ "$ARCH" = "aarch64" ]; then + PYTHON_BUILD="cpython-3.12.12+${RELEASE}-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" + DIST_ARCH="aarch64" +else + echo -e "${RED}Unsupported architecture: $ARCH${NC}" + exit 1 +fi + +TEMP_BUILD_DIR="build/temp" +DIST_DIR="build/dicom_mcp_standalone_linux_${DIST_ARCH}" +PYTHON_URL="https://github.com/indygreg/python-build-standalone/releases/download/${RELEASE}/$PYTHON_BUILD" + +# Clean previous builds +rm -rf "build" "dist" +mkdir -p "$TEMP_BUILD_DIR" "$DIST_DIR" + +# Download standalone Python +echo -e "${YELLOW}Downloading Python 3.12 standalone build for Linux ${ARCH}...${NC}" +curl -L "$PYTHON_URL" -o "$TEMP_BUILD_DIR/python.tar.gz" + +# Extract Python +echo -e "${YELLOW}Extracting Python...${NC}" +tar -xzf "$TEMP_BUILD_DIR/python.tar.gz" -C "$TEMP_BUILD_DIR" +mv "$TEMP_BUILD_DIR/python" "$DIST_DIR/python" + +# Set up Python environment +PYTHON_BIN="$DIST_DIR/python/bin/python3" + +# Install dependencies +echo -e "${YELLOW}Installing dependencies...${NC}" +"$PYTHON_BIN" -m pip install --upgrade pip +"$PYTHON_BIN" -m pip install fastmcp==2.0.0 pydicom==3.0.1 numpy==2.4.2 Pillow==12.1.1 + +# Copy server code +echo -e "${YELLOW}Copying server code...${NC}" +cp -r dicom_mcp/ "$DIST_DIR/dicom_mcp/" +cp dicom_mcp.py "$DIST_DIR/" + +# Create launcher script +echo -e "${YELLOW}Creating launcher script...${NC}" +cat > "$DIST_DIR/run_dicom_mcp.sh" << 'EOF' +#!/bin/bash +# DICOM MCP Server Launcher +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" +exec "$SCRIPT_DIR/python/bin/python3" -m dicom_mcp +EOF +chmod +x "$DIST_DIR/run_dicom_mcp.sh" + +# Create installation instructions +cat > "$DIST_DIR/INSTALL.txt" << EOF +DICOM MCP Server - Installation Instructions (Linux) + +1. Move this entire folder to your preferred location, e.g.: + /opt/dicom_mcp_standalone_linux_${DIST_ARCH} + +2. Edit your MCP client configuration file to add this server. + + For Claude Desktop on Linux (~/.config/Claude/claude_desktop_config.json): + + { + "mcpServers": { + "dicom_mcp": { + "command": "/path/to/dicom_mcp_standalone_linux_${DIST_ARCH}/run_dicom_mcp.sh" + } + } + } + + (Replace /path/to/ with the actual location where you placed this folder) + +3. Restart your MCP client + +4. Verify the server is working by asking Claude: + "List available MCP tools" + +You should see 12 DICOM tools available. + +No Python installation required - everything is bundled! +EOF + +# Create a simple installer script for end users +cat > "$DIST_DIR/install_to_claude.sh" << 'EOF' +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +CONFIG_FILE="$HOME/.config/Claude/claude_desktop_config.json" + +echo -e "${GREEN}=== DICOM MCP Server - Claude Desktop Setup (Linux) ===${NC}\n" + +# Create config directory if needed +mkdir -p "$HOME/.config/Claude" + +# Check if config exists +if [ -f "$CONFIG_FILE" ]; then + echo -e "${YELLOW}Existing configuration found.${NC}" + echo -e "Please add the following to your mcpServers section:\n" + echo '{ + "dicom_mcp": { + "command": "'"$SCRIPT_DIR/run_dicom_mcp.sh"'" + } +}' + echo -e "\n${YELLOW}Configuration file location:${NC}" + echo "$CONFIG_FILE" +else + echo -e "${YELLOW}Creating new configuration...${NC}" + cat > "$CONFIG_FILE" << CONFIGEOF +{ + "mcpServers": { + "dicom_mcp": { + "command": "$SCRIPT_DIR/run_dicom_mcp.sh" + } + } +} +CONFIGEOF + echo -e "${GREEN}✓ Configuration created${NC}" +fi + +echo -e "\n${GREEN}Next step: Restart Claude Desktop${NC}\n" +EOF +chmod +x "$DIST_DIR/install_to_claude.sh" + +# Create tarball for distribution +echo -e "${YELLOW}Creating distribution package...${NC}" +mkdir -p dist +tar -czf "dist/dicom_mcp_standalone_linux_${DIST_ARCH}.tar.gz" -C build "dicom_mcp_standalone_linux_${DIST_ARCH}" + +# Clean up build directory +rm -rf build + +echo -e "\n${GREEN}=== Build Complete ===${NC}\n" +echo -e "Distribution package created at:" +echo -e "${YELLOW}dist/dicom_mcp_standalone_linux_${DIST_ARCH}.tar.gz${NC}\n" +echo -e "Package size: $(du -h "dist/dicom_mcp_standalone_linux_${DIST_ARCH}.tar.gz" | cut -f1)\n" +echo -e "To distribute:" +echo -e "1. Send the .tar.gz file to users" +echo -e "2. Users extract it: ${YELLOW}tar -xzf dicom_mcp_standalone_linux_${DIST_ARCH}.tar.gz${NC}" +echo -e "3. Users run: ${YELLOW}cd dicom_mcp_standalone_linux_${DIST_ARCH} && ./install_to_claude.sh${NC}\n" diff --git a/mcps/dicom_mcp/dicom_mcp.py b/mcps/dicom_mcp/dicom_mcp.py new file mode 100644 index 0000000..d2cd7be --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""Backward-compatible entry point. + +Delegates to the ``dicom_mcp`` package so that +``python dicom_mcp.py`` continues to work. +""" + +from dicom_mcp.server import run + +if __name__ == "__main__": + run() diff --git a/mcps/dicom_mcp/dicom_mcp/__init__.py b/mcps/dicom_mcp/dicom_mcp/__init__.py new file mode 100644 index 0000000..6f7c91d --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/__init__.py @@ -0,0 +1,81 @@ +"""DICOM MCP Server package. + +Re-exports all public symbols so that ``import dicom_mcp`` continues to +provide the same top-level namespace as the original single-file module. +""" + +# --- Server instance --- +from dicom_mcp.server import mcp # noqa: F401 + +# --- Configuration --- +from dicom_mcp.config import MAX_FILES, PII_FILTER_ENABLED # noqa: F401 + +# --- PII filtering --- +from dicom_mcp.pii import ( # noqa: F401 + PII_TAGS, + PII_REDACTED_VALUE, + is_pii_tag, + redact_if_pii, +) + +# --- Enums and constants --- +from dicom_mcp.constants import ( # noqa: F401 + ResponseFormat, + SequenceType, + COMMON_TAGS, + VALID_TAG_GROUPS, +) + +# --- Helper functions --- +from dicom_mcp.helpers import ( # noqa: F401 + _safe_get_tag, + _format_tag_value, + _resolve_tag, + _validate_tag_groups, + _format_markdown_table, + _identify_sequence_type, + _is_dixon_sequence, + _get_dixon_image_types, + _is_dicom_file, + _find_dicom_files, + _resolve_philips_private_tag, + _list_philips_private_creators, + _get_pixel_array, + _extract_roi, + _compute_stats, + _apply_windowing, + _parse_filter, + _apply_filter, + _build_tree_text, + _build_tree_json, + _format_tree_value, +) + +# --- Tool functions (importing triggers @mcp.tool() registration) --- +from dicom_mcp.tools.discovery import ( # noqa: F401 + dicom_list_files, + dicom_find_dixon_series, +) +from dicom_mcp.tools.metadata import ( # noqa: F401 + dicom_get_metadata, + dicom_compare_headers, +) +from dicom_mcp.tools.validation import ( # noqa: F401 + dicom_validate_sequence, + dicom_analyze_series, +) +from dicom_mcp.tools.query import ( # noqa: F401 + dicom_query, + dicom_summarize_directory, +) +from dicom_mcp.tools.search import dicom_search # noqa: F401 +from dicom_mcp.tools.philips import dicom_query_philips_private # noqa: F401 +from dicom_mcp.tools.pixels import ( # noqa: F401 + dicom_read_pixels, + dicom_compute_snr, + dicom_render_image, +) +from dicom_mcp.tools.tree import dicom_dump_tree # noqa: F401 +from dicom_mcp.tools.uid_comparison import dicom_compare_uids # noqa: F401 +from dicom_mcp.tools.segmentation import dicom_verify_segmentations # noqa: F401 +from dicom_mcp.tools.ti_analysis import dicom_analyze_ti # noqa: F401 diff --git a/mcps/dicom_mcp/dicom_mcp/__main__.py b/mcps/dicom_mcp/dicom_mcp/__main__.py new file mode 100644 index 0000000..0c5ddcd --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/__main__.py @@ -0,0 +1,5 @@ +"""Entry point for ``python -m dicom_mcp``.""" + +from dicom_mcp.server import run + +run() diff --git a/mcps/dicom_mcp/dicom_mcp/config.py b/mcps/dicom_mcp/dicom_mcp/config.py new file mode 100644 index 0000000..b86df80 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/config.py @@ -0,0 +1,13 @@ +"""Environment-driven configuration for the DICOM MCP server.""" + +import os + +# PII filtering: set DICOM_MCP_PII_FILTER=true to redact patient tags +PII_FILTER_ENABLED: bool = os.environ.get("DICOM_MCP_PII_FILTER", "").lower() in ( + "true", + "1", + "yes", +) + +# Safety limit for directory scans +MAX_FILES: int = int(os.environ.get("DICOM_MCP_MAX_FILES", "1000")) diff --git a/mcps/dicom_mcp/dicom_mcp/constants.py b/mcps/dicom_mcp/dicom_mcp/constants.py new file mode 100644 index 0000000..f80ac40 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/constants.py @@ -0,0 +1,96 @@ +"""Enums and constants for the DICOM MCP server.""" + +from enum import Enum + + +class ResponseFormat(str, Enum): + """Output format for tool responses.""" + + MARKDOWN = "markdown" + JSON = "json" + + +class SequenceType(str, Enum): + """Common MRI sequence types.""" + + DIXON = "dixon" + T1_MAPPING = "t1_mapping" + MULTI_ECHO_GRE = "multi_echo_gre" + SPIN_ECHO_IR = "spin_echo_ir" + T1 = "t1" + T2 = "t2" + FLAIR = "flair" + DWI = "dwi" + LOCALIZER = "localizer" + UNKNOWN = "unknown" + + +# Tag names for common DICOM attributes +COMMON_TAGS = { + "patient_info": [ + (0x0010, 0x0010), # PatientName + (0x0010, 0x0020), # PatientID + (0x0010, 0x0030), # PatientBirthDate + (0x0010, 0x0040), # PatientSex + ], + "study_info": [ + (0x0008, 0x0020), # StudyDate + (0x0008, 0x0030), # StudyTime + (0x0020, 0x000D), # StudyInstanceUID + (0x0008, 0x1030), # StudyDescription + ], + "series_info": [ + (0x0008, 0x0060), # Modality + (0x0020, 0x000E), # SeriesInstanceUID + (0x0008, 0x103E), # SeriesDescription + (0x0020, 0x0011), # SeriesNumber + ], + "image_info": [ + (0x0028, 0x0010), # Rows + (0x0028, 0x0011), # Columns + (0x0028, 0x0008), # NumberOfFrames + (0x0020, 0x0013), # InstanceNumber + (0x0008, 0x0008), # ImageType + ], + "acquisition": [ + (0x0018, 0x0020), # ScanningSequence + (0x0018, 0x0021), # SequenceVariant + (0x0018, 0x0023), # MRAcquisitionType + (0x0018, 0x0080), # RepetitionTime + (0x0018, 0x0081), # EchoTime + (0x0018, 0x0082), # InversionTime + (0x0018, 0x1314), # FlipAngle + ], + "manufacturer": [ + (0x0008, 0x0070), # Manufacturer + (0x0008, 0x1090), # ManufacturerModelName + (0x0018, 0x1020), # SoftwareVersions + ], + "equipment": [ + (0x0008, 0x1010), # StationName + (0x0018, 0x1000), # DeviceSerialNumber + (0x0008, 0x0080), # InstitutionName + (0x0008, 0x1040), # InstitutionalDepartmentName + (0x0018, 0x0087), # MagneticFieldStrength + (0x0018, 0x1250), # ReceiveCoilName + (0x0018, 0x1251), # TransmitCoilName + ], + "geometry": [ + (0x0028, 0x0030), # PixelSpacing + (0x0018, 0x0050), # SliceThickness + (0x0018, 0x0088), # SpacingBetweenSlices + (0x0020, 0x1041), # SliceLocation + (0x0020, 0x0032), # ImagePositionPatient + (0x0020, 0x0037), # ImageOrientationPatient + ], + "pixel_data": [ + (0x0028, 0x0100), # BitsAllocated + (0x0028, 0x0101), # BitsStored + (0x0028, 0x0102), # HighBit + (0x0028, 0x0103), # PixelRepresentation + (0x0028, 0x1050), # WindowCenter + (0x0028, 0x1051), # WindowWidth + ], +} + +VALID_TAG_GROUPS = sorted(COMMON_TAGS.keys()) diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/__init__.py b/mcps/dicom_mcp/dicom_mcp/helpers/__init__.py new file mode 100644 index 0000000..93f30ad --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/__init__.py @@ -0,0 +1,61 @@ +"""Re-export all helper functions for convenient importing.""" + +from dicom_mcp.helpers.tags import ( + _safe_get_tag, + _format_tag_value, + _resolve_tag, + _validate_tag_groups, + _format_markdown_table, +) +from dicom_mcp.helpers.sequence import ( + _identify_sequence_type, + _is_dixon_sequence, + _get_dixon_image_types, +) +from dicom_mcp.helpers.files import ( + _is_dicom_file, + _find_dicom_files, +) +from dicom_mcp.helpers.philips import ( + _resolve_philips_private_tag, + _list_philips_private_creators, +) +from dicom_mcp.helpers.pixels import ( + _get_pixel_array, + _extract_roi, + _compute_stats, + _apply_windowing, +) +from dicom_mcp.helpers.filters import ( + _parse_filter, + _apply_filter, +) +from dicom_mcp.helpers.tree import ( + _build_tree_text, + _build_tree_json, + _format_tree_value, +) + +__all__ = [ + "_safe_get_tag", + "_format_tag_value", + "_resolve_tag", + "_validate_tag_groups", + "_format_markdown_table", + "_identify_sequence_type", + "_is_dixon_sequence", + "_get_dixon_image_types", + "_is_dicom_file", + "_find_dicom_files", + "_resolve_philips_private_tag", + "_list_philips_private_creators", + "_get_pixel_array", + "_extract_roi", + "_compute_stats", + "_apply_windowing", + "_parse_filter", + "_apply_filter", + "_build_tree_text", + "_build_tree_json", + "_format_tree_value", +] diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/files.py b/mcps/dicom_mcp/dicom_mcp/helpers/files.py new file mode 100644 index 0000000..ea134cf --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/files.py @@ -0,0 +1,62 @@ +"""DICOM file discovery and validation helpers.""" + +from pathlib import Path + +import pydicom +from pydicom.errors import InvalidDicomError + +from dicom_mcp.config import MAX_FILES + + +def _is_dicom_file(file_path: Path) -> bool: + """Quick check if a file is likely DICOM by checking the preamble. + + DICOM files have a 128-byte preamble followed by 'DICM' magic bytes. + Files with .dcm extension are assumed to be DICOM without preamble check. + """ + if file_path.suffix.lower() == ".dcm": + return True + + try: + with open(file_path, "rb") as f: + f.seek(128) + magic = f.read(4) + return magic == b"DICM" + except (OSError, IOError): + return False + + +def _find_dicom_files( + directory: Path, + recursive: bool = True, + max_files: int = MAX_FILES, +) -> tuple[list[tuple[Path, pydicom.Dataset]], bool]: + """Find DICOM files and return paths with pre-read datasets. + + Returns: + Tuple of (list of (path, dataset) pairs, truncated flag). + truncated is True if max_files limit was hit. + """ + results: list[tuple[Path, pydicom.Dataset]] = [] + truncated = False + + pattern = "**/*" if recursive else "*" + + for file_path in sorted(directory.glob(pattern)): + if not file_path.is_file(): + continue + + if not _is_dicom_file(file_path): + continue + + try: + ds = pydicom.dcmread(file_path, stop_before_pixels=True) + results.append((file_path, ds)) + except (InvalidDicomError, Exception): + continue + + if len(results) >= max_files: + truncated = True + break + + return results, truncated diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/filters.py b/mcps/dicom_mcp/dicom_mcp/helpers/filters.py new file mode 100644 index 0000000..253107b --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/filters.py @@ -0,0 +1,120 @@ +"""Filter parsing and application for dicom_search.""" + +from typing import Any, Dict, Optional + +# Text operators (case-insensitive matching on string tag values) +_TEXT_OPERATORS = { + "is": lambda actual, expected: actual.lower() == expected.lower(), + "is not": lambda actual, expected: actual.lower() != expected.lower(), + "contains": lambda actual, expected: expected.lower() in actual.lower(), + "starts with": lambda actual, expected: actual.lower().startswith(expected.lower()), + "ends with": lambda actual, expected: actual.lower().endswith(expected.lower()), +} + +# Symbolic operators (numeric with string fallback) +_SYMBOLIC_OPERATORS = { + ">=": lambda a, b: a >= b, + "<=": lambda a, b: a <= b, + "!=": lambda a, b: a != b, + ">": lambda a, b: a > b, + "<": lambda a, b: a < b, + "=": lambda a, b: a == b, +} + +# Presence operators (no value needed) +_PRESENCE_OPERATORS = { + "exists": lambda actual: actual != "N/A" and actual.strip() != "", + "missing": lambda actual: actual == "N/A" or actual.strip() == "", +} + + +def _parse_filter(filter_str: str) -> Optional[Dict[str, Any]]: + """Parse a filter string into a structured filter dict. + + Supported formats: + Text: "SeriesDescription contains MOST" + Symbolic: "EchoTime > 10" + Presence: "InversionTime exists" + + Returns dict with keys: tag_spec, operator, value (None for presence), + operator_type ('text', 'symbolic', 'presence'). + Returns None if parsing fails. + """ + filter_str = filter_str.strip() + if not filter_str: + return None + + # --- Try presence operators first (no value part) --- + for op in _PRESENCE_OPERATORS: + if filter_str.lower().endswith(f" {op}"): + tag_spec = filter_str[: -(len(op) + 1)].strip() + if tag_spec: + return { + "tag_spec": tag_spec, + "operator": op, + "value": None, + "operator_type": "presence", + } + + # --- Try text operators (multi-word, so check longest first) --- + for op in sorted(_TEXT_OPERATORS.keys(), key=len, reverse=True): + # Look for " op " in the string (space-delimited) + marker = f" {op} " + idx = filter_str.lower().find(marker) + if idx >= 0: + tag_spec = filter_str[:idx].strip() + value = filter_str[idx + len(marker) :].strip() + if tag_spec and value: + return { + "tag_spec": tag_spec, + "operator": op, + "value": value, + "operator_type": "text", + } + + # --- Try symbolic operators (check >= <= != before > < =) --- + for op in sorted(_SYMBOLIC_OPERATORS.keys(), key=len, reverse=True): + marker = f" {op} " + idx = filter_str.find(marker) + if idx >= 0: + tag_spec = filter_str[:idx].strip() + value = filter_str[idx + len(marker) :].strip() + if tag_spec and value: + return { + "tag_spec": tag_spec, + "operator": op, + "value": value, + "operator_type": "symbolic", + } + + return None + + +def _apply_filter(filter_def: Dict[str, Any], actual_value: str) -> bool: + """Apply a parsed filter to an actual DICOM tag value. + + Returns True if the value passes the filter. + """ + op = filter_def["operator"] + op_type = filter_def["operator_type"] + expected = filter_def.get("value") + + if op_type == "presence": + return _PRESENCE_OPERATORS[op](actual_value) + + if op_type == "text": + return _TEXT_OPERATORS[op](actual_value, expected) + + if op_type == "symbolic": + comparator = _SYMBOLIC_OPERATORS[op] + # Try numeric comparison first + try: + num_actual = float(actual_value) + num_expected = float(expected) + return comparator(num_actual, num_expected) + except (ValueError, TypeError): + pass + # Fall back to string comparison + return comparator(actual_value, expected) + + return False diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/philips.py b/mcps/dicom_mcp/dicom_mcp/helpers/philips.py new file mode 100644 index 0000000..9f395df --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/philips.py @@ -0,0 +1,129 @@ +"""Philips private tag resolution helpers.""" + +from typing import Any, Dict, List, Optional + +import pydicom + + +def _resolve_philips_private_tag( + ds: pydicom.Dataset, + dd_number: int, + element_offset: int, + private_group: int = 0x2005, +) -> tuple[Optional[tuple[int, int]], Optional[str], Optional[str]]: + """Resolve a Philips private tag by DD number and element offset. + + Philips stores private data using Private Creator tags that reserve + 256-element blocks within a private group. Each Private Creator tag + at (group, 00xx) contains a string like "Philips MR Imaging DD 001". + The block number is the last byte of the Private Creator tag address. + + To find a specific private element: + 1. Scan Private Creator tags (group, 0x0010)-(group, 0x00FF) for the + DD string matching the requested dd_number. + 2. Take the block byte (last byte of the Private Creator tag address). + 3. Construct the resolved tag: (group, block_byte << 8 | element_offset). + + IMPORTANT: Block assignments are NOT fixed across scanners or software + versions. They must be looked up dynamically per file. + + Args: + ds: A pydicom Dataset (already read). + dd_number: The DD number to look for (e.g. 1 for "DD 001"). + element_offset: The element offset within the DD block (e.g. 0x85). + private_group: The DICOM private group to search (default 0x2005). + + Returns: + Tuple of (resolved_tag, creator_string, value_str): + - resolved_tag: (group, element) tuple, or None if not found. + - creator_string: The Private Creator string that matched, or None. + - value_str: String representation of the tag value, or None. + """ + # Format the DD number for matching (e.g. 1 -> "001", 5 -> "005") + dd_suffix = f"{dd_number:03d}" + target_pattern = f"dd {dd_suffix}" + + # Scan Private Creator slots: (group, 0x0010) through (group, 0x00FF) + for slot in range(0x0010, 0x0100): + creator_tag = (private_group, slot) + creator_elem = ds.get(creator_tag) + if creator_elem is None: + continue + + creator_str = str(creator_elem.value).strip() + if not creator_str: + continue + + # Check if this creator string matches our DD number + if target_pattern in creator_str.lower(): + # The block byte is the slot number (last byte of creator tag) + block_byte = slot + resolved_element = (block_byte << 8) | element_offset + resolved_tag = (private_group, resolved_element) + + # Try to read the value at the resolved tag + value_elem = ds.get(resolved_tag) + if value_elem is not None: + value_str = str(value_elem.value) + else: + value_str = None + + return resolved_tag, creator_str, value_str + + return None, None, None + + +def _list_philips_private_creators( + ds: pydicom.Dataset, + private_group: int = 0x2005, +) -> List[Dict[str, Any]]: + """List all Philips Private Creator tags in a dataset. + + Scans Private Creator slots (group, 0x0010)-(group, 0x00FF) and returns + information about each occupied slot, including the DD string and block byte. + + Args: + ds: A pydicom Dataset (already read). + private_group: The DICOM private group to scan (default 0x2005). + + Returns: + List of dicts with keys: slot, tag, block_byte, creator_string, dd_number. + """ + creators = [] + for slot in range(0x0010, 0x0100): + creator_tag = (private_group, slot) + creator_elem = ds.get(creator_tag) + if creator_elem is None: + continue + + creator_str = str(creator_elem.value).strip() + if not creator_str: + continue + + # Try to extract DD number + dd_number = None + lower = creator_str.lower() + dd_idx = lower.find("dd ") + if dd_idx >= 0: + dd_part = lower[dd_idx + 3 :].strip() + # Extract leading digits + digits = "" + for ch in dd_part: + if ch.isdigit(): + digits += ch + else: + break + if digits: + dd_number = int(digits) + + creators.append( + { + "slot": slot, + "tag": f"({private_group:04X},{slot:04X})", + "block_byte": f"0x{slot:02X}", + "creator_string": creator_str, + "dd_number": dd_number, + } + ) + + return creators diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/pixels.py b/mcps/dicom_mcp/dicom_mcp/helpers/pixels.py new file mode 100644 index 0000000..f7410f0 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/pixels.py @@ -0,0 +1,81 @@ +"""Pixel array extraction, ROI handling, statistics, and windowing helpers.""" + +from typing import Any, Dict, List + +import numpy as np +import pydicom + + +def _get_pixel_array(ds: pydicom.Dataset) -> np.ndarray: + """Extract pixel array with rescale slope/intercept applied. + + Handles Philips (and other vendor) rescaling so that returned values + are in the rescaled domain (e.g. signal intensity). + """ + pixels = ds.pixel_array.astype(np.float64) + slope = float(getattr(ds, "RescaleSlope", 1.0) or 1.0) + intercept = float(getattr(ds, "RescaleIntercept", 0.0) or 0.0) + if slope != 1.0 or intercept != 0.0: + pixels = pixels * slope + intercept + return pixels + + +def _extract_roi(pixels: np.ndarray, roi: List[int]) -> np.ndarray: + """Extract a rectangular ROI from a 2D pixel array. + + Args: + pixels: 2D numpy array (rows, cols) + roi: [x, y, width, height] where x,y is the top-left corner. + Coordinates are in image space (x = column, y = row). + + Returns: + 2D numpy array of the ROI region. + + Raises: + ValueError: If the ROI is out of bounds or invalid. + """ + if len(roi) != 4: + raise ValueError("ROI must be [x, y, width, height]") + x, y, w, h = int(roi[0]), int(roi[1]), int(roi[2]), int(roi[3]) + if w <= 0 or h <= 0: + raise ValueError(f"ROI dimensions must be positive (got {w}x{h})") + rows, cols = pixels.shape + if x < 0 or y < 0 or x + w > cols or y + h > rows: + raise ValueError( + f"ROI [{x},{y},{w},{h}] exceeds image bounds ({cols}x{rows}). " + f"ROI right edge: {x + w}, bottom edge: {y + h}" + ) + return pixels[y : y + h, x : x + w] + + +def _compute_stats(pixels: np.ndarray) -> Dict[str, Any]: + """Compute descriptive statistics for a pixel array.""" + flat = pixels.ravel() + return { + "min": float(np.min(flat)), + "max": float(np.max(flat)), + "mean": float(np.mean(flat)), + "std": float(np.std(flat)), + "median": float(np.median(flat)), + "p5": float(np.percentile(flat, 5)), + "p25": float(np.percentile(flat, 25)), + "p75": float(np.percentile(flat, 75)), + "p95": float(np.percentile(flat, 95)), + "pixel_count": int(flat.size), + } + + +def _apply_windowing( + pixels: np.ndarray, window_center: float, window_width: float +) -> np.ndarray: + """Apply DICOM windowing to produce an 8-bit display image. + + Uses the standard DICOM linear VOI LUT: + if pixel <= (center - width/2): output = 0 + if pixel > (center + width/2): output = 255 + otherwise: linear mapping to 0-255 + """ + lower = window_center - window_width / 2.0 + upper = window_center + window_width / 2.0 + windowed = np.clip((pixels - lower) / (upper - lower) * 255.0, 0, 255) + return windowed.astype(np.uint8) diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/sequence.py b/mcps/dicom_mcp/dicom_mcp/helpers/sequence.py new file mode 100644 index 0000000..100a4be --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/sequence.py @@ -0,0 +1,114 @@ +"""MRI sequence identification helpers.""" + +from typing import List + +import pydicom + +from dicom_mcp.constants import SequenceType +from dicom_mcp.helpers.tags import _safe_get_tag + + +def _identify_sequence_type(ds: pydicom.Dataset) -> SequenceType: + """Identify the MRI sequence type from DICOM metadata. + + Uses a two-tier approach: + 1. Series description keyword matching (fast, covers named protocols) + 2. DICOM acquisition tag inspection (structural fallback) + + IMPORTANT: Keyword matching order matters. More specific terms (e.g., 't1_mapping', + 'molli') must be checked before generic terms (e.g., 't1') to avoid false positives. + """ + series_desc = _safe_get_tag(ds, (0x0008, 0x103E), "").lower() + + # --- Tier 1: keyword matches on series description --- + # NOTE: Order is intentional - specific patterns checked before generic ones + + # Dixon variants (check before MOST/IDEAL since some descriptions contain both) + if any(kw in series_desc for kw in ["dixon", "ideal", "flex"]): + return SequenceType.DIXON + + # T1 mapping protocols (MOLLI, NOLLI, ShMOLLI, SASHA, etc.) + # Must check before generic 't1' to avoid misclassification + if any(kw in series_desc for kw in ["molli", "nolli", "shmolli", "sasha"]): + return SequenceType.T1_MAPPING + + # Multi-echo GRE (MOST protocol for T2*/PDFF) + if "most" in series_desc or "multi" in series_desc and "echo" in series_desc: + return SequenceType.MULTI_ECHO_GRE + + # Localizer / survey / scout + if any(kw in series_desc for kw in ["localizer", "survey", "scout"]): + return SequenceType.LOCALIZER + + # Standard named sequences (specific before generic) + if "flair" in series_desc: + return SequenceType.FLAIR + if "dwi" in series_desc or "diffusion" in series_desc: + return SequenceType.DWI + # Check 't2' before 't1' since 't2' is more specific + if "t2" in series_desc: + return SequenceType.T2 + if "t1" in series_desc: + return SequenceType.T1 + + # --- Tier 2: inspect DICOM acquisition tags --- + + image_type = _safe_get_tag(ds, (0x0008, 0x0008), "").lower() + if "dixon" in image_type or "ideal" in image_type: + return SequenceType.DIXON + + scanning_seq = _safe_get_tag(ds, (0x0018, 0x0020), "") + seq_variant = _safe_get_tag(ds, (0x0018, 0x0021), "") + + has_ir = "IR" in scanning_seq + has_se = "SE" in scanning_seq + has_gr = "GR" in scanning_seq + has_mp = "MP" in seq_variant # Magnetization Prepared + + # GR + IR (+ MP) = gradient echo inversion recovery (MOLLI-like T1 mapping) + if has_gr and has_ir and has_mp: + return SequenceType.T1_MAPPING + + # SE + IR = spin echo inversion recovery + if has_se and has_ir: + return SequenceType.SPIN_ECHO_IR + + # Multi-echo detection via NumberOfEchoes tag + num_echoes = _safe_get_tag(ds, (0x0018, 0x0086), "") + try: + if int(num_echoes) > 1 and has_gr: + return SequenceType.MULTI_ECHO_GRE + except (ValueError, TypeError): + pass + + return SequenceType.UNKNOWN + + +def _is_dixon_sequence(ds: pydicom.Dataset) -> bool: + """Check if a DICOM file is from a Dixon sequence.""" + return _identify_sequence_type(ds) == SequenceType.DIXON + + +def _get_dixon_image_types(ds: pydicom.Dataset) -> List[str]: + """Extract Dixon image types (water, fat, in-phase, out-phase) from metadata.""" + image_types = [] + + image_type_tag = ds.get((0x0008, 0x0008)) + if image_type_tag: + image_type_str = str(image_type_tag.value).lower() + if "water" in image_type_str or "w" in image_type_str.split("\\"): + image_types.append("water") + if "fat" in image_type_str or "f" in image_type_str.split("\\"): + image_types.append("fat") + if "in" in image_type_str or "ip" in image_type_str: + image_types.append("in-phase") + if "out" in image_type_str or "op" in image_type_str: + image_types.append("out-phase") + + series_desc = _safe_get_tag(ds, (0x0008, 0x103E), "").lower() + if "water" in series_desc and "water" not in image_types: + image_types.append("water") + if "fat" in series_desc and "fat" not in image_types: + image_types.append("fat") + + return image_types if image_types else ["unknown"] diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/tags.py b/mcps/dicom_mcp/dicom_mcp/helpers/tags.py new file mode 100644 index 0000000..13578ab --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/tags.py @@ -0,0 +1,97 @@ +"""Tag reading, formatting, and resolution helpers.""" + +from typing import Any, List, Union + +import pydicom +from pydicom import datadict, multival + +from dicom_mcp.constants import COMMON_TAGS + + +def _safe_get_tag( + ds: pydicom.Dataset, tag: Union[str, int, tuple[int, int]], default: str = "N/A" +) -> str: + """Safely extract a DICOM tag value with fallback.""" + try: + value = ds.get(tag, default) + if value == default: + return default + if hasattr(value, "value"): + return str(value.value) + return str(value) + except Exception: + return default + + +def _format_tag_value(tag: tuple, value: Any) -> str: + """Format a DICOM tag value for display.""" + if value is None or value == "": + return "N/A" + + if isinstance(value, pydicom.sequence.Sequence): + return f"[Sequence with {len(value)} items]" + + if isinstance(value, multival.MultiValue): + return "\\".join(str(v) for v in value) + + return str(value) + + +def _resolve_tag(spec: str) -> tuple: + """Resolve a tag keyword or hex pair to ((group, elem), display_name). + + Accepts: + - Hex pairs: "0018,0081" or "0018, 0081" + - Keywords: "EchoTime", "PatientName", etc. + + Returns: + (tag_tuple, display_name) where tag_tuple is None if unresolvable. + """ + spec = spec.strip() + # Try hex pair first: "0018,0081" or "0018, 0081" + if "," in spec: + parts = spec.split(",") + if len(parts) == 2: + try: + group = int(parts[0].strip(), 16) + elem = int(parts[1].strip(), 16) + tag_tuple = (group, elem) + keyword = datadict.keyword_for_tag(tag_tuple) + name = ( + keyword if keyword else f"({parts[0].strip()},{parts[1].strip()})" + ) + return tag_tuple, name + except ValueError: + pass + # Try as keyword: "EchoTime", "PatientName", etc. + try: + tag_tuple = datadict.tag_for_keyword(spec) + if tag_tuple is not None: + return tag_tuple, spec + except Exception: + pass + return None, spec + + +def _validate_tag_groups(requested: list[str]) -> list[str]: + """Validate tag group names, returning list of invalid ones.""" + return [g for g in requested if g not in COMMON_TAGS] + + +def _format_markdown_table(headers: List[str], rows: List[List[str]]) -> str: + """Format data as a Markdown table.""" + col_widths = [len(h) for h in headers] + for row in rows: + for i, cell in enumerate(row): + col_widths[i] = max(col_widths[i], len(str(cell))) + + header_row = " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + separator = "-|-".join("-" * w for w in col_widths) + + data_rows = [] + for row in rows: + data_rows.append( + " | ".join(str(cell).ljust(col_widths[i]) for i, cell in enumerate(row)) + ) + + return f"{header_row}\n{separator}\n" + "\n".join(data_rows) diff --git a/mcps/dicom_mcp/dicom_mcp/helpers/tree.py b/mcps/dicom_mcp/dicom_mcp/helpers/tree.py new file mode 100644 index 0000000..7a4424f --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/helpers/tree.py @@ -0,0 +1,162 @@ +"""DICOM tree-building helpers for hierarchical structure display.""" + +from typing import Any, Dict, List + +import pydicom +from pydicom.dataelem import DataElement + +from dicom_mcp.pii import redact_if_pii + + +def _format_tree_value(elem: DataElement, max_value_len: int = 80) -> str: + """Format a DataElement value for tree display. + + Truncates long values, handles bytes, multivalue, and sequences. + """ + if elem.VR == "SQ": + count = len(elem.value) if elem.value else 0 + return f"" + + if elem.tag == (0x7FE0, 0x0010): + return "" + + try: + val = elem.value + except Exception: + return "" + + if isinstance(val, bytes): + if len(val) > 32: + return f"<{len(val)} bytes>" + return repr(val) + + val_str = str(val) + if len(val_str) > max_value_len: + return val_str[:max_value_len] + "..." + return val_str + + +def _build_tree_text( + dataset: pydicom.Dataset, + max_depth: int = 10, + show_private: bool = True, + depth: int = 0, + prefix: str = "", + is_last: bool = True, +) -> List[str]: + """Build an indented text tree of a DICOM dataset. + + Returns a list of formatted strings with tree characters. + """ + lines: List[str] = [] + elements = [elem for elem in dataset if elem.tag != (0x7FE0, 0x0010)] + + if not show_private: + elements = [elem for elem in elements if not elem.tag.is_private] + + for i, elem in enumerate(elements): + is_last_elem = i == len(elements) - 1 + tag_str = f"({elem.tag.group:04X},{elem.tag.element:04X})" + keyword = elem.keyword or "Unknown" + vr = elem.VR if hasattr(elem, "VR") else "??" + + # Apply PII redaction + tag_tuple = (elem.tag.group, elem.tag.element) + value_str = _format_tree_value(elem) + redacted = redact_if_pii(tag_tuple, value_str) + if redacted != value_str: + value_str = str(redacted) + + if depth == 0: + connector = "" + child_prefix = "" + else: + connector = "└─ " if is_last_elem else "├─ " + child_prefix = prefix + (" " if is_last_elem else "│ ") + + line = f"{prefix}{connector}{tag_str} {keyword} [{vr}]: {value_str}" + lines.append(line) + + # Recurse into sequences if within depth limit + if elem.VR == "SQ" and elem.value and depth < max_depth: + seq_items = list(elem.value) + for seq_idx, item in enumerate(seq_items): + is_last_item = seq_idx == len(seq_items) - 1 + + if depth == 0: + item_connector = "└─ " if is_last_item and is_last_elem else "├─ " + item_child_prefix = ( + " " if is_last_item and is_last_elem else "│ " + ) + else: + item_connector = ( + child_prefix + "└─ " if is_last_item else child_prefix + "├─ " + ) + item_child_prefix = child_prefix + ( + " " if is_last_item else "│ " + ) + + lines.append(f"{item_connector}Item {seq_idx + 1}") + sub_lines = _build_tree_text( + item, + max_depth=max_depth, + show_private=show_private, + depth=depth + 1, + prefix=item_child_prefix, + is_last=is_last_item, + ) + lines.extend(sub_lines) + + return lines + + +def _build_tree_json( + dataset: pydicom.Dataset, + max_depth: int = 10, + show_private: bool = True, + depth: int = 0, +) -> List[Dict[str, Any]]: + """Build a JSON-serializable tree of a DICOM dataset. + + Returns a list of dicts, each with tag, vr, keyword, value, and + optionally items (for sequences). + """ + nodes: List[Dict[str, Any]] = [] + elements = [elem for elem in dataset if elem.tag != (0x7FE0, 0x0010)] + + if not show_private: + elements = [elem for elem in elements if not elem.tag.is_private] + + for elem in elements: + tag_str = f"({elem.tag.group:04X},{elem.tag.element:04X})" + keyword = elem.keyword or "Unknown" + vr = elem.VR if hasattr(elem, "VR") else "??" + + tag_tuple = (elem.tag.group, elem.tag.element) + value_str = _format_tree_value(elem) + redacted = redact_if_pii(tag_tuple, value_str) + if redacted != value_str: + value_str = str(redacted) + + node: Dict[str, Any] = { + "tag": tag_str, + "vr": vr, + "keyword": keyword, + "value": value_str, + } + + if elem.VR == "SQ" and elem.value and depth < max_depth: + items = [] + for seq_idx, item in enumerate(elem.value): + children = _build_tree_json( + item, + max_depth=max_depth, + show_private=show_private, + depth=depth + 1, + ) + items.append({"index": seq_idx + 1, "children": children}) + node["items"] = items + + nodes.append(node) + + return nodes diff --git a/mcps/dicom_mcp/dicom_mcp/pii.py b/mcps/dicom_mcp/dicom_mcp/pii.py new file mode 100644 index 0000000..00b99b3 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/pii.py @@ -0,0 +1,32 @@ +"""PII tag set and redaction functions. + +When ``DICOM_MCP_PII_FILTER`` is enabled (via environment variable), +patient-identifying tags are replaced with ``[REDACTED]`` in tool output. +""" + +from typing import Any, Tuple + +from dicom_mcp.constants import COMMON_TAGS + +# The set of DICOM tags considered PII (patient tags only). +PII_TAGS: frozenset[Tuple[int, int]] = frozenset(COMMON_TAGS["patient_info"]) + +PII_REDACTED_VALUE = "[REDACTED]" + + +def is_pii_tag(tag_tuple: Tuple[int, int]) -> bool: + """Return True if *tag_tuple* is a patient-identifying tag.""" + return tag_tuple in PII_TAGS + + +def redact_if_pii(tag_tuple: Tuple[int, int], value: Any) -> Any: + """Return ``[REDACTED]`` when PII filtering is active and the tag is PII. + + The function checks the runtime config on every call so that + ``importlib.reload(dicom_mcp.config)`` toggles behaviour in tests. + """ + from dicom_mcp.config import PII_FILTER_ENABLED + + if PII_FILTER_ENABLED and is_pii_tag(tag_tuple): + return PII_REDACTED_VALUE + return value diff --git a/mcps/dicom_mcp/dicom_mcp/server.py b/mcps/dicom_mcp/dicom_mcp/server.py new file mode 100644 index 0000000..c0e597e --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/server.py @@ -0,0 +1,55 @@ +"""FastMCP server instance and entry point.""" + +import logging +import os +import sys +from mcp.server.fastmcp import FastMCP + +# --------------------------------------------------------------------------- +# Logging & Output Redirection +# --------------------------------------------------------------------------- +LOG_FILE = os.environ.get("DICOM_MCP_LOG_FILE", "dicom_mcp.log") + +def setup_redirection(): + """Reduces noise by suppressing stderr and logging to a file.""" + # Suppress stderr + devnull = open(os.devnull, "w") + os.dup2(devnull.fileno(), sys.stderr.fileno()) + + # Configure logging to write to the log file directly + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + filename=LOG_FILE, + filemode="a" + ) + +setup_redirection() +logger = logging.getLogger("dicom_mcp") + +mcp = FastMCP( + "dicom_mcp", + instructions=( + "You are a DICOM data inspection assistant. " + "Your role is strictly factual and descriptive. " + "Report exactly what is present in the DICOM data: tag values, " + "pixel statistics, acquisition parameters, series counts, " + "file structure, and vendor differences. " + "Do NOT provide clinical interpretation, diagnostic guidance, " + "or opinions on data suitability for specific clinical purposes. " + "Do NOT suggest what conditions the data could help diagnose, " + "or recommend clinical actions based on the data. " + "Do NOT assess whether data quality is adequate for specific " + "clinical workflows. " + "Present findings as-is. Qualified professionals will draw " + "their own conclusions from the data you report." + ), +) + + +def run(): + """Start the MCP server.""" + # Import tools to ensure they are registered before running + import dicom_mcp.tools # noqa: F401 + + mcp.run() diff --git a/mcps/dicom_mcp/dicom_mcp/tools/__init__.py b/mcps/dicom_mcp/dicom_mcp/tools/__init__.py new file mode 100644 index 0000000..f1f983e --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/__init__.py @@ -0,0 +1,17 @@ +"""Tool registration module. + +Importing this package triggers all @mcp.tool() decorators, registering +every tool with the FastMCP server instance. +""" + +from dicom_mcp.tools import discovery # noqa: F401 +from dicom_mcp.tools import metadata # noqa: F401 +from dicom_mcp.tools import validation # noqa: F401 +from dicom_mcp.tools import query # noqa: F401 +from dicom_mcp.tools import search # noqa: F401 +from dicom_mcp.tools import philips # noqa: F401 +from dicom_mcp.tools import pixels # noqa: F401 +from dicom_mcp.tools import tree # noqa: F401 +from dicom_mcp.tools import uid_comparison # noqa: F401 +from dicom_mcp.tools import segmentation # noqa: F401 +from dicom_mcp.tools import ti_analysis # noqa: F401 diff --git a/mcps/dicom_mcp/dicom_mcp/tools/discovery.py b/mcps/dicom_mcp/dicom_mcp/tools/discovery.py new file mode 100644 index 0000000..8292c87 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/discovery.py @@ -0,0 +1,291 @@ +"""DICOM file discovery tools: list_files and find_dixon_series.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict, Optional + +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.config import MAX_FILES +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _format_markdown_table +from dicom_mcp.helpers.sequence import ( + _identify_sequence_type, + _is_dixon_sequence, + _get_dixon_image_types, +) +from dicom_mcp.helpers.files import _find_dicom_files + + +@mcp.tool( + name="dicom_list_files", + annotations=ToolAnnotations( + title="List DICOM Files in Directory", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_list_files( + directory: str, + recursive: bool = True, + filter_sequence_type: Optional[str] = None, + count_only: bool = False, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """List all DICOM files in a directory with optional filtering. + + Recursively searches a directory for DICOM files and provides + basic metadata about each file including series information and sequence type. + Useful for discovering available test data and organizing QA workflows. + + Set count_only=True to return just the series breakdown with file counts + instead of listing every individual file. Much more efficient for large + directories when you only need an inventory overview. + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + if not dir_path.is_dir(): + return f"Error: Path is not a directory: {directory}" + + dicom_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, recursive + ) + + if not dicom_files: + return f"No DICOM files found in {directory}" + + # Build per-file info and optional series counts + file_info = [] + series_counts: Dict[str, Dict[str, Any]] = {} # keyed by series_number + + for file_path, ds in dicom_files: + sequence_type = _identify_sequence_type(ds) + + if filter_sequence_type: + normalized_filter = filter_sequence_type.lower().strip() + if sequence_type.value != normalized_filter: + continue + + series_num = _safe_get_tag(ds, (0x0020, 0x0011)) + series_desc = _safe_get_tag(ds, (0x0008, 0x103E)) + manufacturer = _safe_get_tag(ds, (0x0008, 0x0070)) + modality = _safe_get_tag(ds, (0x0008, 0x0060)) + + # Always accumulate series counts (cheap) + if series_num not in series_counts: + series_counts[series_num] = { + "series_number": series_num, + "series_description": series_desc, + "sequence_type": sequence_type.value, + "manufacturer": manufacturer, + "modality": modality, + "file_count": 0, + } + series_counts[series_num]["file_count"] += 1 + + # Only build full file list when not in count_only mode + if not count_only: + info = { + "path": str(file_path.relative_to(dir_path)), + "series_description": series_desc, + "series_number": series_num, + "sequence_type": sequence_type.value, + "manufacturer": manufacturer, + "modality": modality, + } + file_info.append(info) + + total_matched = sum(sc["file_count"] for sc in series_counts.values()) + + # Sort series by number + def _sort_series_key(item: tuple) -> tuple: + sn = item[1]["series_number"] + if sn is None or sn == "N/A": + return (2, "") + try: + return (0, int(sn)) + except (ValueError, TypeError): + return (1, str(sn)) + + sorted_series = sorted(series_counts.items(), key=_sort_series_key) + + if count_only: + # --- count_only mode: return series breakdown only --- + series_list = [data for _, data in sorted_series] + + if response_format == ResponseFormat.JSON: + result = { + "total_files": total_matched, + "directory": str(dir_path), + "truncated": truncated, + "series_count": len(series_list), + "series": series_list, + } + return json.dumps(result, indent=2) + else: + output = [ + f"# DICOM File Counts in {dir_path}\n", + f"Total files: {total_matched} across {len(series_list)} series\n", + ] + if truncated: + output.append( + f"**Warning**: Scan truncated at {MAX_FILES} files.\n" + ) + headers = ["Series", "Description", "Type", "Manufacturer", "Files"] + rows = [ + [ + s["series_number"], + s["series_description"], + s["sequence_type"], + s["manufacturer"], + str(s["file_count"]), + ] + for s in series_list + ] + output.append(_format_markdown_table(headers, rows)) + return "\n".join(output) + else: + # --- full listing mode (original behaviour) --- + if response_format == ResponseFormat.JSON: + result = { + "total_files": len(file_info), + "directory": str(dir_path), + "truncated": truncated, + "files": file_info, + } + return json.dumps(result, indent=2) + else: + output = [ + f"# DICOM Files in {dir_path}\n", + f"Total files found: {len(file_info)}\n", + ] + if truncated: + output.append( + f"**Warning**: Results truncated at {MAX_FILES} files. Narrow your search directory.\n" + ) + if file_info: + headers = ["Path", "Series", "Number", "Sequence", "Manufacturer"] + rows = [ + [ + f["path"], + f["series_description"], + f["series_number"], + f["sequence_type"], + f["manufacturer"], + ] + for f in file_info + ] + output.append(_format_markdown_table(headers, rows)) + return "\n".join(output) + + except Exception as e: + return f"Error listing DICOM files: {str(e)}" + + +@mcp.tool( + name="dicom_find_dixon_series", + annotations=ToolAnnotations( + title="Find Dixon Sequences", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_find_dixon_series( + directory: str, + recursive: bool = True, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Find and analyze Dixon sequences in a directory. + + Searches for Dixon (chemical shift) sequences and identifies the different + image types (water, fat, in-phase, out-phase) available in each series. + Critical for body composition QA workflows. + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + + dicom_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, recursive + ) + + if not dicom_files: + return f"No DICOM files found in {directory}" + + series_map = {} + for file_path, ds in dicom_files: + if not _is_dixon_sequence(ds): + continue + + series_uid = _safe_get_tag(ds, (0x0020, 0x000E)) + + if series_uid not in series_map: + series_map[series_uid] = { + "series_description": _safe_get_tag(ds, (0x0008, 0x103E)), + "series_number": _safe_get_tag(ds, (0x0020, 0x0011)), + "series_uid": series_uid, + "image_types": set(), + "files": [], + "sample_file": str(file_path), + } + + image_types = _get_dixon_image_types(ds) + series_map[series_uid]["image_types"].update(image_types) + series_map[series_uid]["files"].append(str(file_path)) + + if not series_map: + return f"No Dixon sequences found in {directory}" + + series_list = [] + for series_data in series_map.values(): + series_list.append( + { + "series_description": series_data["series_description"], + "series_number": series_data["series_number"], + "series_uid": series_data["series_uid"], + "image_types": sorted(list(series_data["image_types"])), + "file_count": len(series_data["files"]), + "sample_file": series_data["sample_file"], + } + ) + series_list.sort(key=lambda x: x["series_number"]) + + if response_format == ResponseFormat.JSON: + result = { + "total_dixon_series": len(series_list), + "directory": str(dir_path), + "truncated": truncated, + "series": series_list, + } + return json.dumps(result, indent=2) + else: + output = [ + f"# Dixon Sequences in {dir_path}\n", + f"Found {len(series_list)} Dixon series\n", + ] + if truncated: + output.append( + f"**Warning**: File scan truncated at {MAX_FILES} files.\n" + ) + for series in series_list: + output.append( + f"## Series {series['series_number']}: {series['series_description']}" + ) + output.append(f"- **Series UID**: {series['series_uid']}") + output.append(f"- **Image Types**: {', '.join(series['image_types'])}") + output.append(f"- **File Count**: {series['file_count']}") + output.append(f"- **Sample File**: {Path(series['sample_file']).name}") + output.append("") + return "\n".join(output) + + except Exception as e: + return f"Error finding Dixon sequences: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/metadata.py b/mcps/dicom_mcp/dicom_mcp/tools/metadata.py new file mode 100644 index 0000000..1fb212c --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/metadata.py @@ -0,0 +1,260 @@ +"""DICOM metadata extraction and comparison tools.""" + +import asyncio +import json +from pathlib import Path +from typing import Dict, List, Optional + +import pydicom +from pydicom import datadict +from pydicom.errors import InvalidDicomError +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.constants import COMMON_TAGS, VALID_TAG_GROUPS, ResponseFormat +from dicom_mcp.helpers.tags import ( + _safe_get_tag, + _validate_tag_groups, + _format_markdown_table, +) +from dicom_mcp.helpers.philips import _resolve_philips_private_tag +from dicom_mcp.pii import redact_if_pii + + +@mcp.tool( + name="dicom_get_metadata", + annotations=ToolAnnotations( + title="Get DICOM File Metadata", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_get_metadata( + file_path: str, + tag_groups: Optional[List[str]] = None, + custom_tags: Optional[List[str]] = None, + philips_private_tags: Optional[List[Dict[str, int]]] = None, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Extract metadata from a DICOM file. + + Reads DICOM headers and extracts commonly used tags organized by + category. Supports both predefined tag groups and custom tag specification. + + Available tag groups: patient_info, study_info, series_info, image_info, + acquisition, manufacturer, equipment, geometry, pixel_data. + + For Philips files, you can also resolve private tags by providing + philips_private_tags — a list of dicts with keys "dd_number" and + "element_offset" (and optionally "private_group", default 0x2005). + Example: [{"dd_number": 1, "element_offset": 133}] resolves the + DD 001 block's element at offset 0x85. + """ + try: + fp = Path(file_path) + if not fp.exists(): + return f"Error: File not found: {file_path}" + + ds = await asyncio.to_thread(pydicom.dcmread, fp, stop_before_pixels=True) + + if tag_groups: + invalid = _validate_tag_groups(tag_groups) + if invalid: + return ( + f"Error: Unknown tag group(s): {', '.join(invalid)}. " + f"Available groups: {', '.join(VALID_TAG_GROUPS)}" + ) + tag_groups_to_use = { + k: v for k, v in COMMON_TAGS.items() if k in tag_groups + } + else: + tag_groups_to_use = COMMON_TAGS + + metadata = {} + for group_name, tags in tag_groups_to_use.items(): + group_data = {} + for tag in tags: + tag_name = datadict.keyword_for_tag(tag) + value = _safe_get_tag(ds, tag) + group_data[tag_name] = redact_if_pii(tag, value) + metadata[group_name] = group_data + + if custom_tags: + custom_data = {} + for tag_str in custom_tags: + try: + group, element = tag_str.split(",") + tag = (int(group, 16), int(element, 16)) + tag_name = datadict.keyword_for_tag(tag) or f"Tag_{tag_str}" + value = _safe_get_tag(ds, tag) + custom_data[tag_name] = value + except Exception as e: + custom_data[f"Error_{tag_str}"] = f"Invalid tag: {str(e)}" + metadata["custom_tags"] = custom_data + + if philips_private_tags: + private_data = {} + for entry in philips_private_tags: + dd_num = entry.get("dd_number") + offset = entry.get("element_offset") + group = entry.get("private_group", 0x2005) + + if dd_num is None or offset is None: + private_data[ + ( + f"Error_DD{dd_num}_0x{offset:02X}" + if offset + else "Error_invalid" + ) + ] = "Both dd_number and element_offset are required" + continue + + resolved_tag, creator_str, value_str = _resolve_philips_private_tag( + ds, dd_num, offset, group + ) + + label = f"DD{dd_num:03d}_0x{offset:02X}" + if resolved_tag is not None: + tag_hex = f"({resolved_tag[0]:04X},{resolved_tag[1]:04X})" + private_data[label] = { + "resolved_tag": tag_hex, + "creator": creator_str, + "value": value_str if value_str is not None else "N/A", + } + else: + private_data[label] = { + "resolved_tag": None, + "error": f"No Private Creator found for DD {dd_num:03d} in group ({group:04X})", + } + metadata["philips_private"] = private_data + + if response_format == ResponseFormat.JSON: + result = {"file_path": str(fp), "metadata": metadata} + return json.dumps(result, indent=2) + else: + output = [f"# DICOM Metadata: {fp.name}\n"] + for group_name, group_data in metadata.items(): + output.append(f"## {group_name.replace('_', ' ').title()}\n") + if group_name == "philips_private": + for label, data in group_data.items(): + if isinstance(data, dict) and "error" in data: + output.append(f"- **{label}**: {data['error']}") + elif isinstance(data, dict): + output.append( + f"- **{label}** [{data['resolved_tag']}]: " + f"{data['value']} *(creator: {data['creator']})*" + ) + else: + output.append(f"- **{label}**: {data}") + else: + for tag_name, value in group_data.items(): + output.append(f"- **{tag_name}**: {value}") + output.append("") + return "\n".join(output) + + except InvalidDicomError: + return f"Error: Not a valid DICOM file: {file_path}" + except Exception as e: + return f"Error reading DICOM metadata: {str(e)}" + + +@mcp.tool( + name="dicom_compare_headers", + annotations=ToolAnnotations( + title="Compare DICOM Headers", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_compare_headers( + file_paths: List[str], + tag_groups: Optional[List[str]] = None, + show_differences_only: bool = False, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Compare DICOM headers across multiple files. + + Compares specified DICOM tags across 2-10 files, highlighting + differences. Useful for validating sequence consistency and + identifying which images were selected from Dixon sequences. + + Available tag groups: patient_info, study_info, series_info, image_info, + acquisition, manufacturer, equipment, geometry, pixel_data. + """ + try: + if len(file_paths) < 2 or len(file_paths) > 10: + return "Error: Provide between 2 and 10 file paths to compare." + + paths = [Path(p) for p in file_paths] + for fp in paths: + if not fp.exists(): + return f"Error: File not found: {fp}" + + def _read_all(): + datasets = [] + for fp in paths: + ds = pydicom.dcmread(fp, stop_before_pixels=True) + datasets.append((fp, ds)) + return datasets + + datasets = await asyncio.to_thread(_read_all) + + effective_groups = tag_groups if tag_groups else ["acquisition", "series_info"] + invalid = _validate_tag_groups(effective_groups) + if invalid: + return ( + f"Error: Unknown tag group(s): {', '.join(invalid)}. " + f"Available groups: {', '.join(VALID_TAG_GROUPS)}" + ) + + tags_to_compare = [] + for group in effective_groups: + tags_to_compare.extend(COMMON_TAGS[group]) + + comparison = {} + for tag in tags_to_compare: + tag_name = datadict.keyword_for_tag(tag) + values = [redact_if_pii(tag, _safe_get_tag(ds, tag)) for _, ds in datasets] + consistent = len(set(values)) == 1 + if show_differences_only and consistent: + continue + comparison[tag_name] = {"values": values, "consistent": consistent} + + if response_format == ResponseFormat.JSON: + result = { + "files": [str(fp) for fp, _ in datasets], + "comparison": comparison, + } + return json.dumps(result, indent=2) + else: + output = [ + "# DICOM Header Comparison\n", + f"Comparing {len(datasets)} files:\n", + ] + for i, (fp, _) in enumerate(datasets, 1): + output.append(f"{i}. {fp.name}") + output.append("") + + if comparison: + headers = ( + ["Tag"] + [f"File {i+1}" for i in range(len(datasets))] + ["Status"] + ) + rows = [] + for tag_name, data in comparison.items(): + status = "Consistent" if data["consistent"] else "Different" + row = [tag_name] + data["values"] + [status] + rows.append(row) + output.append(_format_markdown_table(headers, rows)) + else: + if show_differences_only: + output.append("All compared tags are consistent across files.") + else: + output.append("No tags to compare.") + return "\n".join(output) + + except Exception as e: + return f"Error comparing DICOM headers: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/philips.py b/mcps/dicom_mcp/dicom_mcp/tools/philips.py new file mode 100644 index 0000000..a03cc06 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/philips.py @@ -0,0 +1,184 @@ +"""Philips private tag query tool.""" + +import asyncio +import json +from pathlib import Path +from typing import Optional + +import pydicom +from pydicom.errors import InvalidDicomError +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _format_markdown_table +from dicom_mcp.helpers.philips import ( + _resolve_philips_private_tag, + _list_philips_private_creators, +) + + +@mcp.tool( + name="dicom_query_philips_private", + annotations=ToolAnnotations( + title="Query Philips Private Tags", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_query_philips_private( + file_path: str, + dd_number: Optional[int] = None, + element_offset: Optional[int] = None, + private_group: int = 0x2005, + list_creators: bool = False, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Query Philips private DICOM tags using DD number and element offset. + + Philips MRI scanners store proprietary metadata in private tag blocks. + Each block is reserved by a Private Creator tag containing a string + like "Philips MR Imaging DD 001". Block assignments vary across + scanners and software versions, so this tool resolves them dynamically. + + Usage modes: + 1. **List creators**: Set list_creators=True to see all Private Creator + tags and their DD numbers. Use this to discover what's available. + 2. **Resolve a specific tag**: Provide dd_number and element_offset to + look up a specific private element. For example, dd_number=1 with + element_offset=0x85 finds shim calculation values. + + Common Philips DD numbers and offsets (group 2005): + - DD 001, offset 0x85: Shim calculation values + - DD 001, offset 0x63: Stack ID + - DD 004, offset 0x00: MR Series data object + + Args: + file_path: Path to a Philips DICOM file + dd_number: The DD number to look up (e.g. 1 for "DD 001") + element_offset: The element offset within the DD block (e.g. 0x85). + Can be provided as decimal (133) or will be interpreted + as decimal. Use hex notation in the description for clarity. + private_group: The DICOM private group to search (default 0x2005) + list_creators: If True, list all Private Creator tags instead of + resolving a specific tag + response_format: Output format (markdown or json) + """ + try: + fp = Path(file_path) + if not fp.exists(): + return f"Error: File not found: {file_path}" + + ds = await asyncio.to_thread(pydicom.dcmread, fp, stop_before_pixels=True) + + manufacturer = _safe_get_tag(ds, (0x0008, 0x0070)) + if "philips" not in manufacturer.lower(): + return ( + f"Warning: This file's manufacturer is '{manufacturer}', not Philips. " + "Philips private tag resolution may not produce meaningful results." + ) + + if list_creators: + # --- Mode 1: List all Private Creator tags --- + creators = _list_philips_private_creators(ds, private_group) + + if not creators: + return ( + f"No Private Creator tags found in group " + f"({private_group:04X}) of {fp.name}" + ) + + if response_format == ResponseFormat.JSON: + result = { + "file": fp.name, + "manufacturer": manufacturer, + "private_group": f"({private_group:04X})", + "creators": creators, + } + return json.dumps(result, indent=2) + else: + output = [ + f"# Philips Private Creators: {fp.name}\n", + f"**Manufacturer**: {manufacturer}", + f"**Private group**: ({private_group:04X})", + f"**Creator slots found**: {len(creators)}\n", + ] + headers = ["Tag", "Block", "DD #", "Creator String"] + rows = [ + [ + c["tag"], + c["block_byte"], + str(c["dd_number"]) if c["dd_number"] is not None else "N/A", + c["creator_string"], + ] + for c in creators + ] + output.append(_format_markdown_table(headers, rows)) + return "\n".join(output) + + elif dd_number is not None and element_offset is not None: + # --- Mode 2: Resolve a specific private tag --- + resolved_tag, creator_str, value_str = _resolve_philips_private_tag( + ds, dd_number, element_offset, private_group + ) + + if resolved_tag is None: + # Show available creators to help the user + creators = _list_philips_private_creators(ds, private_group) + available = ", ".join( + f"DD {c['dd_number']:03d}" + for c in creators + if c["dd_number"] is not None + ) + return ( + f"Error: No Private Creator found for DD {dd_number:03d} " + f"in group ({private_group:04X}) of {fp.name}.\n\n" + f"Available DD numbers: {available or 'none found'}" + ) + + tag_hex = f"({resolved_tag[0]:04X},{resolved_tag[1]:04X})" + + if response_format == ResponseFormat.JSON: + result = { + "file": fp.name, + "manufacturer": manufacturer, + "query": { + "dd_number": dd_number, + "element_offset": f"0x{element_offset:02X}", + "private_group": f"({private_group:04X})", + }, + "resolution": { + "creator_string": creator_str, + "resolved_tag": tag_hex, + "value": value_str, + }, + } + return json.dumps(result, indent=2) + else: + output = [ + f"# Philips Private Tag Lookup: {fp.name}\n", + f"**Manufacturer**: {manufacturer}\n", + "## Query", + f"- **DD number**: {dd_number:03d}", + f"- **Element offset**: 0x{element_offset:02X}", + f"- **Private group**: ({private_group:04X})\n", + "## Resolution", + f"- **Creator string**: {creator_str}", + f"- **Resolved tag**: {tag_hex}", + f"- **Value**: {value_str if value_str is not None else 'Tag exists but no value'}\n", + ] + return "\n".join(output) + + else: + return ( + "Error: Provide either list_creators=True to discover available " + "Private Creator tags, or both dd_number and element_offset to " + "resolve a specific private tag." + ) + + except InvalidDicomError: + return f"Error: Not a valid DICOM file: {file_path}" + except Exception as e: + return f"Error querying Philips private tags: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/pixels.py b/mcps/dicom_mcp/dicom_mcp/tools/pixels.py new file mode 100644 index 0000000..ea54be0 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/pixels.py @@ -0,0 +1,511 @@ +"""Pixel analysis tools: read_pixels, compute_snr, render_image.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + +import numpy as np +import pydicom +from PIL import Image, ImageDraw, ImageFont +from pydicom.errors import InvalidDicomError +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _format_markdown_table +from dicom_mcp.helpers.pixels import ( + _get_pixel_array, + _extract_roi, + _compute_stats, + _apply_windowing, +) + + +@mcp.tool( + name="dicom_read_pixels", + annotations=ToolAnnotations( + title="Read DICOM Pixel Statistics", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_read_pixels( + file_path: str, + roi: Optional[List[int]] = None, + include_histogram: bool = False, + histogram_bins: int = 50, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Extract pixel statistics from a DICOM file. + + Reads pixel data and computes descriptive statistics (min, max, mean, + standard deviation, median, percentiles). Optionally restricts analysis + to a rectangular ROI. + + Pixel values are rescaled using RescaleSlope and RescaleIntercept + when present (standard for Philips, common on Siemens/GE). + + Args: + file_path: Path to the DICOM file + roi: Optional region of interest as [x, y, width, height] where + x,y is the top-left corner in pixel coordinates. + If omitted, statistics cover the entire image. + include_histogram: If True, include a binned histogram of pixel values + histogram_bins: Number of histogram bins (default 50) + response_format: Output format (markdown or json) + """ + try: + fp = Path(file_path) + if not fp.exists(): + return f"Error: File not found: {file_path}" + + ds = await asyncio.to_thread(pydicom.dcmread, fp) + + if not hasattr(ds, "pixel_array"): + return f"Error: No pixel data in file: {file_path}" + + pixels = _get_pixel_array(ds) + rows, cols = pixels.shape[:2] + + image_info = { + "rows": rows, + "columns": cols, + "bits_allocated": int(getattr(ds, "BitsAllocated", 0)), + "bits_stored": int(getattr(ds, "BitsStored", 0)), + "rescale_slope": float(getattr(ds, "RescaleSlope", 1.0) or 1.0), + "rescale_intercept": float(getattr(ds, "RescaleIntercept", 0.0) or 0.0), + } + + if pixels.ndim > 2: + return ( + f"Error: Multi-frame image with shape {pixels.shape}. " + "This tool supports single-frame 2D images only." + ) + + roi_desc = "Full image" + if roi: + try: + pixels = _extract_roi(pixels, roi) + roi_desc = f"ROI [{roi[0]},{roi[1]}] {roi[2]}x{roi[3]}" + except ValueError as e: + return f"Error: {str(e)}" + + stats = _compute_stats(pixels) + + result = { + "file": fp.name, + "series_description": _safe_get_tag(ds, (0x0008, 0x103E)), + "image_info": image_info, + "region": roi_desc, + "statistics": stats, + } + + if include_histogram: + counts, bin_edges = np.histogram(pixels.ravel(), bins=histogram_bins) + result["histogram"] = { + "bins": histogram_bins, + "counts": counts.tolist(), + "bin_edges": [round(float(e), 2) for e in bin_edges.tolist()], + } + + if response_format == ResponseFormat.JSON: + return json.dumps(result, indent=2) + else: + output = [ + f"# Pixel Statistics: {fp.name}\n", + f"**Series**: {result['series_description']}", + f"**Image size**: {rows} x {cols}", + f"**Region**: {roi_desc}", + f"**Rescale**: slope={image_info['rescale_slope']}, " + f"intercept={image_info['rescale_intercept']}\n", + "## Statistics\n", + ] + headers = ["Metric", "Value"] + stat_rows = [ + ["Min", f"{stats['min']:.2f}"], + ["Max", f"{stats['max']:.2f}"], + ["Mean", f"{stats['mean']:.2f}"], + ["Std Dev", f"{stats['std']:.2f}"], + ["Median", f"{stats['median']:.2f}"], + ["5th %ile", f"{stats['p5']:.2f}"], + ["25th %ile", f"{stats['p25']:.2f}"], + ["75th %ile", f"{stats['p75']:.2f}"], + ["95th %ile", f"{stats['p95']:.2f}"], + ["Pixel count", str(stats["pixel_count"])], + ] + output.append(_format_markdown_table(headers, stat_rows)) + + if include_histogram and "histogram" in result: + output.append("\n## Histogram\n") + hist = result["histogram"] + max_count = max(hist["counts"]) if hist["counts"] else 1 + for i, count in enumerate(hist["counts"]): + low = hist["bin_edges"][i] + high = hist["bin_edges"][i + 1] + bar_len = int(40 * count / max_count) if max_count > 0 else 0 + bar = "#" * bar_len + output.append(f" {low:8.1f} - {high:8.1f} | {bar} ({count})") + + return "\n".join(output) + + except InvalidDicomError: + return f"Error: Not a valid DICOM file: {file_path}" + except Exception as e: + return f"Error reading pixel data: {str(e)}" + + +@mcp.tool( + name="dicom_compute_snr", + annotations=ToolAnnotations( + title="Compute SNR from ROIs", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_compute_snr( + file_path: str, + signal_roi: List[int], + noise_roi: List[int], + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Compute signal-to-noise ratio from two ROIs in a DICOM image. + + Places a signal ROI (typically in tissue of interest) and a noise ROI + (typically in background air or a uniform region) and computes: + - SNR = mean(signal) / std(noise) + - Individual statistics for both ROIs + + This is the single-image method SNR. For a more robust estimate, + use two identical acquisitions and compute SNR from the difference + image, but this gives a practical per-image metric. + + Args: + file_path: Path to the DICOM file + signal_roi: Signal region as [x, y, width, height] + noise_roi: Noise/background region as [x, y, width, height] + response_format: Output format (markdown or json) + + Tip: Use dicom_render_image first to visualise the image and identify + appropriate ROI coordinates, then use this tool to measure SNR. + """ + try: + fp = Path(file_path) + if not fp.exists(): + return f"Error: File not found: {file_path}" + + ds = await asyncio.to_thread(pydicom.dcmread, fp) + + if not hasattr(ds, "pixel_array"): + return f"Error: No pixel data in file: {file_path}" + + pixels = _get_pixel_array(ds) + + if pixels.ndim > 2: + return ( + f"Error: Multi-frame image with shape {pixels.shape}. " + "This tool supports single-frame 2D images only." + ) + + rows, cols = pixels.shape + + try: + signal_pixels = _extract_roi(pixels, signal_roi) + except ValueError as e: + return f"Error in signal ROI: {str(e)}" + + try: + noise_pixels = _extract_roi(pixels, noise_roi) + except ValueError as e: + return f"Error in noise ROI: {str(e)}" + + signal_stats = _compute_stats(signal_pixels) + noise_stats = _compute_stats(noise_pixels) + + noise_std = noise_stats["std"] + if noise_std == 0 or noise_std < 1e-10: + snr = float("inf") + snr_note = "Noise std is zero or near-zero; check ROI placement" + else: + snr = signal_stats["mean"] / noise_std + snr_note = None + + result = { + "file": fp.name, + "series_description": _safe_get_tag(ds, (0x0008, 0x103E)), + "image_size": {"rows": rows, "columns": cols}, + "signal_roi": { + "position": signal_roi, + "description": f"[{signal_roi[0]},{signal_roi[1]}] {signal_roi[2]}x{signal_roi[3]}", + "statistics": signal_stats, + }, + "noise_roi": { + "position": noise_roi, + "description": f"[{noise_roi[0]},{noise_roi[1]}] {noise_roi[2]}x{noise_roi[3]}", + "statistics": noise_stats, + }, + "snr": round(snr, 2) if snr != float("inf") else "infinite", + } + if snr_note: + result["snr_note"] = snr_note + + if response_format == ResponseFormat.JSON: + return json.dumps(result, indent=2) + else: + output = [ + f"# SNR Analysis: {fp.name}\n", + f"**Series**: {result['series_description']}", + f"**Image size**: {rows} x {cols}\n", + "## Signal ROI\n", + f"**Position**: {result['signal_roi']['description']}", + ] + sig_headers = ["Metric", "Value"] + sig_rows = [ + ["Mean", f"{signal_stats['mean']:.2f}"], + ["Std", f"{signal_stats['std']:.2f}"], + ["Min", f"{signal_stats['min']:.2f}"], + ["Max", f"{signal_stats['max']:.2f}"], + ["Pixels", str(signal_stats["pixel_count"])], + ] + output.append(_format_markdown_table(sig_headers, sig_rows)) + output.append("\n## Noise ROI\n") + output.append(f"**Position**: {result['noise_roi']['description']}") + noise_headers = ["Metric", "Value"] + noise_rows = [ + ["Mean", f"{noise_stats['mean']:.2f}"], + ["Std", f"{noise_stats['std']:.2f}"], + ["Min", f"{noise_stats['min']:.2f}"], + ["Max", f"{noise_stats['max']:.2f}"], + ["Pixels", str(noise_stats["pixel_count"])], + ] + output.append(_format_markdown_table(noise_headers, noise_rows)) + output.append("\n## Result\n") + output.append(f"**SNR = {result['snr']}**") + output.append( + f"(mean_signal / std_noise = " + f"{signal_stats['mean']:.2f} / {noise_stats['std']:.2f})" + ) + if snr_note: + output.append(f"\nWarning: {snr_note}") + + return "\n".join(output) + + except InvalidDicomError: + return f"Error: Not a valid DICOM file: {file_path}" + except Exception as e: + return f"Error computing SNR: {str(e)}" + + +@mcp.tool( + name="dicom_render_image", + annotations=ToolAnnotations( + title="Render DICOM Image to PNG", + readOnlyHint=False, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_render_image( + file_path: str, + output_path: str, + window_center: Optional[float] = None, + window_width: Optional[float] = None, + auto_window: bool = False, + overlay_rois: Optional[List[Dict[str, Any]]] = None, + show_info: bool = True, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Render a DICOM image to PNG with configurable windowing. + + Applies a window/level transform and saves the result as an 8-bit + greyscale PNG. Optionally overlays ROI rectangles for visual + verification of ROI placement used in other tools. + + Windowing priority: + 1. Explicit window_center and window_width parameters + 2. auto_window=True: uses 5th-95th percentile range + 3. DICOM header WindowCenter/WindowWidth values + 4. Fallback: full pixel range (min to max) + + Args: + file_path: Path to the DICOM file + output_path: Path where the PNG will be saved + window_center: Optional manual window center value + window_width: Optional manual window width value + auto_window: If True, auto-calculate window from 5th-95th percentile + overlay_rois: Optional list of ROI overlays. Each dict has: + - roi: [x, y, width, height] + - label: Optional text label (e.g. "Signal", "Noise") + - color: Optional colour name ("red", "green", "blue", + "yellow", "cyan", "magenta", "white"; default "red") + show_info: If True, burn in series description and window values + response_format: Output format (markdown or json) + """ + try: + fp = Path(file_path) + if not fp.exists(): + return f"Error: File not found: {file_path}" + + out = Path(output_path) + out.parent.mkdir(parents=True, exist_ok=True) + + ds = await asyncio.to_thread(pydicom.dcmread, fp) + + if not hasattr(ds, "pixel_array"): + return f"Error: No pixel data in file: {file_path}" + + pixels = _get_pixel_array(ds) + + if pixels.ndim > 2: + return ( + f"Error: Multi-frame image with shape {pixels.shape}. " + "This tool supports single-frame 2D images only." + ) + + rows, cols = pixels.shape + + # --- Determine windowing --- + wc, ww = None, None + window_source = "none" + + if window_center is not None and window_width is not None: + wc, ww = float(window_center), float(window_width) + window_source = "manual" + elif auto_window: + p5 = float(np.percentile(pixels, 5)) + p95 = float(np.percentile(pixels, 95)) + ww = p95 - p5 + wc = p5 + ww / 2.0 + window_source = "auto (p5-p95)" + else: + try: + header_wc = ds.get((0x0028, 0x1050)) + header_ww = ds.get((0x0028, 0x1051)) + if header_wc is not None and header_ww is not None: + hc = header_wc.value + hw = header_ww.value + if hasattr(hc, "__iter__") and not isinstance(hc, str): + hc = hc[0] + if hasattr(hw, "__iter__") and not isinstance(hw, str): + hw = hw[0] + wc, ww = float(hc), float(hw) + window_source = "DICOM header" + except Exception: + pass + + if wc is None or ww is None: + pmin, pmax = float(np.min(pixels)), float(np.max(pixels)) + ww = pmax - pmin if pmax > pmin else 1.0 + wc = pmin + ww / 2.0 + window_source = "full range" + + display = _apply_windowing(pixels, wc, ww) + img = Image.fromarray(display, mode="L") + + # --- Overlays --- + colour_map = { + "red": (255, 0, 0), + "green": (0, 255, 0), + "blue": (0, 100, 255), + "yellow": (255, 255, 0), + "cyan": (0, 255, 255), + "magenta": (255, 0, 255), + "white": (255, 255, 255), + } + + if overlay_rois or show_info: + img = img.convert("RGB") + draw = ImageDraw.Draw(img) + + try: + font = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", 12) + except (OSError, IOError): + try: + font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 12 + ) + except (OSError, IOError): + font = ImageFont.load_default() + + if overlay_rois: + for roi_overlay in overlay_rois: + roi = roi_overlay.get("roi") + if not roi or len(roi) != 4: + continue + label = roi_overlay.get("label", "") + colour_name = roi_overlay.get("color", "red").lower() + colour = colour_map.get(colour_name, (255, 0, 0)) + x, y, w, h = int(roi[0]), int(roi[1]), int(roi[2]), int(roi[3]) + for offset in range(2): + draw.rectangle( + [ + x + offset, + y + offset, + x + w - 1 - offset, + y + h - 1 - offset, + ], + outline=colour, + ) + if label: + draw.text( + (x, max(0, y - 15)), + label, + fill=colour, + font=font, + ) + + if show_info: + series_desc = _safe_get_tag(ds, (0x0008, 0x103E)) + info_text = f"{series_desc} WC:{wc:.0f} WW:{ww:.0f} ({window_source})" + bbox = draw.textbbox((0, 0), info_text, font=font) + text_w = bbox[2] - bbox[0] + text_h = bbox[3] - bbox[1] + draw.rectangle( + [0, rows - text_h - 6, text_w + 8, rows], + fill=(0, 0, 0), + ) + draw.text( + (4, rows - text_h - 3), + info_text, + fill=(255, 255, 255), + font=font, + ) + + await asyncio.to_thread(img.save, str(out), "PNG") + + result = { + "file": fp.name, + "output_path": str(out), + "series_description": _safe_get_tag(ds, (0x0008, 0x103E)), + "image_size": {"rows": rows, "columns": cols}, + "windowing": { + "center": round(wc, 2), + "width": round(ww, 2), + "source": window_source, + }, + "overlays": len(overlay_rois) if overlay_rois else 0, + } + + if response_format == ResponseFormat.JSON: + return json.dumps(result, indent=2) + else: + output = [ + f"# Rendered Image: {fp.name}\n", + f"**Series**: {result['series_description']}", + f"**Image size**: {rows} x {cols}", + f"**Windowing**: WC={wc:.1f}, WW={ww:.1f} ({window_source})", + f"**Output**: `{out}`", + ] + if overlay_rois: + output.append(f"**ROI overlays**: {len(overlay_rois)}") + return "\n".join(output) + + except InvalidDicomError: + return f"Error: Not a valid DICOM file: {file_path}" + except Exception as e: + return f"Error rendering image: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/query.py b/mcps/dicom_mcp/dicom_mcp/tools/query.py new file mode 100644 index 0000000..19bc8da --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/query.py @@ -0,0 +1,421 @@ +"""DICOM tag query and directory summary tools.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.config import MAX_FILES +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _resolve_tag, _format_markdown_table +from dicom_mcp.helpers.sequence import _identify_sequence_type +from dicom_mcp.helpers.files import _find_dicom_files +from dicom_mcp.pii import redact_if_pii + + +@mcp.tool( + name="dicom_query", + annotations=ToolAnnotations( + title="Query DICOM Tags Across Directory", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_query( + directory: str, + tags: List[str], + recursive: bool = True, + group_by: Optional[str] = None, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Query arbitrary DICOM tags across all files in a directory. + + Aggregates unique values with file counts for any combination of DICOM + tags. Accepts tag names (keywords) or hex codes. Use this when you need + to check consistency or explore values for tags not covered by the + standard summary tool. + + Args: + directory: Path to the directory to scan + tags: List of DICOM tag keywords (e.g. "EchoTime", "StudyDate") or + hex pairs (e.g. "0018,0081", "0008,0020") + recursive: Whether to scan subdirectories recursively + group_by: Optional tag keyword or hex pair to group results by + (e.g. "SeriesDescription" to see per-series breakdowns) + response_format: Output format (markdown or json) + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + if not dir_path.is_dir(): + return f"Error: Path is not a directory: {directory}" + + if not tags: + return "Error: Provide at least one tag to query." + + resolved_tags = [] + errors = [] + for spec in tags: + tag_tuple, name = _resolve_tag(spec) + if tag_tuple is None: + errors.append(f"Could not resolve tag: '{spec}'") + else: + resolved_tags.append((tag_tuple, name)) + + if errors and not resolved_tags: + return "Error: " + "; ".join(errors) + + # Resolve group_by tag if provided + group_by_tag = None + group_by_name = None + if group_by: + group_by_tag, group_by_name = _resolve_tag(group_by) + if group_by_tag is None: + return f"Error: Could not resolve group_by tag: '{group_by}'" + + # --- Scan files --- + dicom_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, recursive + ) + + if not dicom_files: + return f"No DICOM files found in {directory}" + + # --- Aggregate values --- + if group_by_tag: + # Nested: group_value -> tag_name -> value -> count + grouped: Dict[str, Dict[str, Dict[str, int]]] = {} + for _, ds in dicom_files: + gv = _safe_get_tag(ds, group_by_tag) + if gv not in grouped: + grouped[gv] = {name: {} for _, name in resolved_tags} + for tag_tuple, name in resolved_tags: + value = redact_if_pii(tag_tuple, _safe_get_tag(ds, tag_tuple)) + grouped[gv][name][value] = grouped[gv][name].get(value, 0) + 1 + + if response_format == ResponseFormat.JSON: + result = { + "directory": str(dir_path), + "total_files_scanned": len(dicom_files), + "truncated": truncated, + "group_by": group_by_name, + "groups": {}, + } + if errors: + result["warnings"] = errors + for gv, tag_data in sorted(grouped.items()): + result["groups"][gv] = { + name: [ + {"value": v, "count": c} + for v, c in sorted(vals.items(), key=lambda x: -x[1]) + ] + for name, vals in tag_data.items() + } + return json.dumps(result, indent=2) + else: + output = [ + f"# DICOM Tag Query: {dir_path}\n", + f"**Files scanned**: {len(dicom_files)}", + f"**Grouped by**: {group_by_name}\n", + ] + if truncated: + output.append( + f"**Warning**: Scan truncated at {MAX_FILES} files.\n" + ) + if errors: + output.append(f"**Warnings**: {'; '.join(errors)}\n") + for gv in sorted(grouped.keys()): + output.append(f"## {group_by_name}: {gv}\n") + tag_data = grouped[gv] + for name, vals in tag_data.items(): + sorted_vals = sorted(vals.items(), key=lambda x: -x[1]) + if len(sorted_vals) == 1: + v, c = sorted_vals[0] + output.append(f"- **{name}**: {v} ({c} files)") + else: + output.append(f"- **{name}** ({len(sorted_vals)} unique):") + for v, c in sorted_vals: + output.append(f" - {v}: {c} files") + output.append("") + return "\n".join(output) + else: + # Flat: tag_name -> value -> count + collectors: Dict[str, Dict[str, int]] = { + name: {} for _, name in resolved_tags + } + for _, ds in dicom_files: + for tag_tuple, name in resolved_tags: + value = redact_if_pii(tag_tuple, _safe_get_tag(ds, tag_tuple)) + collectors[name][value] = collectors[name].get(value, 0) + 1 + + if response_format == ResponseFormat.JSON: + result = { + "directory": str(dir_path), + "total_files_scanned": len(dicom_files), + "truncated": truncated, + "tags": {}, + } + if errors: + result["warnings"] = errors + for name, vals in collectors.items(): + result["tags"][name] = { + "unique_count": len(vals), + "values": [ + {"value": v, "count": c} + for v, c in sorted(vals.items(), key=lambda x: -x[1]) + ], + } + return json.dumps(result, indent=2) + else: + output = [ + f"# DICOM Tag Query: {dir_path}\n", + f"**Files scanned**: {len(dicom_files)}\n", + ] + if truncated: + output.append( + f"**Warning**: Scan truncated at {MAX_FILES} files.\n" + ) + if errors: + output.append(f"**Warnings**: {'; '.join(errors)}\n") + for name, vals in collectors.items(): + sorted_vals = sorted(vals.items(), key=lambda x: -x[1]) + output.append(f"## {name} ({len(sorted_vals)} unique)\n") + headers = ["Value", "File Count"] + rows = [[v, str(c)] for v, c in sorted_vals] + output.append(_format_markdown_table(headers, rows)) + output.append("") + return "\n".join(output) + + except Exception as e: + return f"Error querying DICOM tags: {str(e)}" + + +@mcp.tool( + name="dicom_summarize_directory", + annotations=ToolAnnotations( + title="Summarize DICOM Directory", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_summarize_directory( + directory: str, + recursive: bool = True, + include_series_overview: bool = True, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Summarize a DICOM directory by returning unique values of key tags. + + Scans all DICOM files in a directory and returns distinct values for + important metadata fields (manufacturer, scanner model, series descriptions, + sequence types, institution, field strength, etc.) with file counts per + group. Much more efficient than listing every file when you need an + overview of what a directory contains. + + Args: + directory: Path to the directory to scan + recursive: Whether to scan subdirectories recursively + include_series_overview: Whether to include detailed series-by-series table + response_format: Output format (markdown or json) + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + if not dir_path.is_dir(): + return f"Error: Path is not a directory: {directory}" + + dicom_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, recursive + ) + + if not dicom_files: + return f"No DICOM files found in {directory}" + + # --- Series-level grouping (only if requested) --- + sorted_series = [] + if include_series_overview: + series_groups: Dict[str, Dict[str, Any]] = {} + for _, ds in dicom_files: + series_uid = _safe_get_tag(ds, (0x0020, 0x000E)) + if series_uid not in series_groups: + series_groups[series_uid] = { + "series_number": _safe_get_tag(ds, (0x0020, 0x0011)), + "series_description": _safe_get_tag(ds, (0x0008, 0x103E)), + "sequence_type": _identify_sequence_type(ds).value, + "tr": _safe_get_tag(ds, (0x0018, 0x0080)), + "te": _safe_get_tag(ds, (0x0018, 0x0081)), + "ti": _safe_get_tag(ds, (0x0018, 0x0082)), + "fa": _safe_get_tag(ds, (0x0018, 0x1314)), + "file_count": 0, + } + series_groups[series_uid]["file_count"] += 1 + + # Sort series by series number (numeric where possible) + def _sort_key(item: tuple) -> tuple: + series_num = item[1]["series_number"] + if series_num is None or series_num == "N/A": + return (2, "") # Sort None/N/A values last + try: + return (0, int(series_num)) + except (ValueError, TypeError): + return (1, str(series_num)) + + sorted_series = sorted(series_groups.items(), key=_sort_key) + + # Tags to summarize: tag tuple -> (display_name, collector dict) + summary_fields = { + (0x0010, 0x0010): "Patient Name", + (0x0010, 0x0020): "Patient ID", + (0x0010, 0x0030): "Patient Birth Date", + (0x0010, 0x0040): "Patient Sex", + (0x0008, 0x0070): "Manufacturer", + (0x0008, 0x1090): "Scanner Model", + (0x0018, 0x0087): "Field Strength (T)", + (0x0008, 0x0080): "Institution", + (0x0008, 0x1010): "Station Name", + (0x0008, 0x0060): "Modality", + (0x0008, 0x103E): "Series Description", + (0x0020, 0x0011): "Series Number", + } + + # Collect unique values with counts + collectors: Dict[str, Dict[str, int]] = { + name: {} for name in summary_fields.values() + } + sequence_type_counts: Dict[str, int] = {} + + for _, ds in dicom_files: + for tag, field_name in summary_fields.items(): + value = redact_if_pii(tag, _safe_get_tag(ds, tag)) + collectors[field_name][value] = collectors[field_name].get(value, 0) + 1 + + seq_type = _identify_sequence_type(ds).value + sequence_type_counts[seq_type] = sequence_type_counts.get(seq_type, 0) + 1 + + # Build summary + summary = { + "directory": str(dir_path), + "total_files_scanned": len(dicom_files), + "truncated": truncated, + "fields": {}, + } + + for field_name, value_counts in collectors.items(): + sorted_entries = sorted(value_counts.items(), key=lambda x: -x[1]) + summary["fields"][field_name] = { + "unique_count": len(sorted_entries), + "values": [{"value": v, "count": c} for v, c in sorted_entries], + } + + summary["fields"]["Sequence Type"] = { + "unique_count": len(sequence_type_counts), + "values": [ + {"value": v, "count": c} + for v, c in sorted(sequence_type_counts.items(), key=lambda x: -x[1]) + ], + } + + # Add series overview to summary (if enabled) + if include_series_overview and sorted_series: + summary["series_overview"] = [ + { + "series_number": data["series_number"], + "series_description": data["series_description"], + "sequence_type": data["sequence_type"], + "tr": data["tr"], + "te": data["te"], + "ti": data["ti"], + "fa": data["fa"], + "file_count": data["file_count"], + } + for _, data in sorted_series + ] + + if response_format == ResponseFormat.JSON: + return json.dumps(summary, indent=2) + else: + output = [ + f"# DICOM Directory Summary: {dir_path}\n", + f"**Total files scanned**: {len(dicom_files)}", + ] + if truncated: + output.append( + f"**Warning**: Scan truncated at {MAX_FILES} files. " + "Results may not reflect all data in the directory.\n" + ) + output.append("") + + # Order fields for readability + field_order = [ + "Patient Name", + "Patient ID", + "Patient Birth Date", + "Patient Sex", + "Manufacturer", + "Scanner Model", + "Field Strength (T)", + "Institution", + "Station Name", + "Modality", + "Sequence Type", + "Series Description", + "Series Number", + ] + + for field_name in field_order: + field_data = summary["fields"].get(field_name) + if not field_data: + continue + output.append( + f"## {field_name} ({field_data['unique_count']} unique)\n" + ) + headers = ["Value", "File Count"] + rows = [ + [entry["value"], str(entry["count"])] + for entry in field_data["values"] + ] + output.append(_format_markdown_table(headers, rows)) + output.append("") + + # --- Series Overview Table (if enabled) --- + if include_series_overview and sorted_series: + output.append("## Series Overview\n") + series_headers = [ + "Series", + "Description", + "Type", + "TR", + "TE", + "TI", + "FA", + "Files", + ] + series_rows = [ + [ + data["series_number"], + data["series_description"], + data["sequence_type"], + data["tr"], + data["te"], + data["ti"], + data["fa"], + str(data["file_count"]), + ] + for _, data in sorted_series + ] + output.append(_format_markdown_table(series_headers, series_rows)) + output.append("") + + return "\n".join(output) + + except Exception as e: + return f"Error summarizing directory: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/search.py b/mcps/dicom_mcp/dicom_mcp/tools/search.py new file mode 100644 index 0000000..ff780a0 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/search.py @@ -0,0 +1,249 @@ +"""DICOM file search tool with filter parsing.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict, List + +import pydicom +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.config import MAX_FILES +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _resolve_tag, _format_markdown_table +from dicom_mcp.helpers.sequence import _identify_sequence_type +from dicom_mcp.helpers.files import _find_dicom_files +from dicom_mcp.helpers.filters import _parse_filter, _apply_filter + + +@mcp.tool( + name="dicom_search", + annotations=ToolAnnotations( + title="Search DICOM Files by Criteria", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_search( + directory: str, + filters: List[str], + recursive: bool = True, + mode: str = "summary", + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Search for DICOM files matching specific criteria. + + Finds files where tag values match all given filter conditions (AND logic). + Returns matching file paths, a count, or a summary depending on mode. + + Filter syntax (one filter per list element): + + Text operators (case-insensitive): + "SeriesDescription contains MOST" + "SeriesDescription is LMS MOST" + "PatientName is not UNKNOWN" + "SeriesDescription starts with Thigh" + "SeriesDescription ends with Dixon" + + Numeric/symbolic operators (numeric when possible, string fallback): + "EchoTime > 10" + "FlipAngle <= 15" + "RepetitionTime = 516" + "SeriesNumber != 0" + + Presence operators: + "InversionTime exists" + "InversionTime missing" + + Tags can be keywords (EchoTime) or hex pairs (0018,0081). + All filters must match for a file to be included (AND logic). + + Args: + directory: Path to the directory to scan + filters: List of filter strings + recursive: Whether to scan subdirectories recursively + mode: Result mode - "count" (just the number), "paths" (file paths), + or "summary" (series breakdown of matches, default) + response_format: Output format (markdown or json) + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + if not dir_path.is_dir(): + return f"Error: Path is not a directory: {directory}" + + if not filters: + return "Error: Provide at least one filter." + + if mode not in ("count", "paths", "summary"): + return f"Error: Invalid mode '{mode}'. Use 'count', 'paths', or 'summary'." + + # --- Parse and resolve filters --- + parsed_filters = [] + for f_str in filters: + parsed = _parse_filter(f_str) + if parsed is None: + return ( + f"Error: Could not parse filter: '{f_str}'\n\n" + "Expected formats:\n" + " Text: TagName contains value\n" + " Symbolic: TagName > value\n" + " Presence: TagName exists" + ) + tag_tuple, tag_name = _resolve_tag(parsed["tag_spec"]) + if tag_tuple is None: + return f"Error: Could not resolve tag: '{parsed['tag_spec']}'" + parsed["tag_tuple"] = tag_tuple + parsed["tag_name"] = tag_name + parsed_filters.append(parsed) + + # --- Scan files --- + dicom_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, recursive + ) + + if not dicom_files: + return f"No DICOM files found in {directory}" + + # --- Apply filters --- + matches: list[tuple[Path, pydicom.Dataset]] = [] + for file_path, ds in dicom_files: + all_match = True + for pf in parsed_filters: + actual = _safe_get_tag(ds, pf["tag_tuple"]) + if not _apply_filter(pf, actual): + all_match = False + break + if all_match: + matches.append((file_path, ds)) + + # --- Format filter description --- + filter_desc = " AND ".join( + f"{pf['tag_name']} {pf['operator']}" + + (f" {pf['value']}" if pf["value"] is not None else "") + for pf in parsed_filters + ) + + # --- Output based on mode --- + if mode == "count": + if response_format == ResponseFormat.JSON: + result = { + "directory": str(dir_path), + "filters": filter_desc, + "total_scanned": len(dicom_files), + "truncated": truncated, + "match_count": len(matches), + } + return json.dumps(result, indent=2) + else: + output = [ + "# DICOM Search Results\n", + f"**Filter**: {filter_desc}", + f"**Scanned**: {len(dicom_files)} files", + f"**Matches**: {len(matches)} files", + ] + if truncated: + output.append( + f"\n**Warning**: Scan truncated at {MAX_FILES} files." + ) + return "\n".join(output) + + elif mode == "paths": + rel_paths = [str(fp.relative_to(dir_path)) for fp, _ in matches] + if response_format == ResponseFormat.JSON: + result = { + "directory": str(dir_path), + "filters": filter_desc, + "total_scanned": len(dicom_files), + "truncated": truncated, + "match_count": len(matches), + "paths": rel_paths, + } + return json.dumps(result, indent=2) + else: + output = [ + "# DICOM Search Results\n", + f"**Filter**: {filter_desc}", + f"**Scanned**: {len(dicom_files)} files", + f"**Matches**: {len(matches)} files\n", + ] + if truncated: + output.append( + f"**Warning**: Scan truncated at {MAX_FILES} files.\n" + ) + if matches: + for p in rel_paths: + output.append(f"- {p}") + else: + output.append("No files matched the filter criteria.") + return "\n".join(output) + + else: # summary + # Group matches by series + series_groups: Dict[str, Dict[str, Any]] = {} + for fp, ds in matches: + series_num = _safe_get_tag(ds, (0x0020, 0x0011)) + if series_num not in series_groups: + series_groups[series_num] = { + "series_number": series_num, + "series_description": _safe_get_tag(ds, (0x0008, 0x103E)), + "sequence_type": _identify_sequence_type(ds).value, + "file_count": 0, + } + series_groups[series_num]["file_count"] += 1 + + def _sort_key(item: tuple) -> tuple: + sn = item[1]["series_number"] + if sn is None or sn == "N/A": + return (2, "") + try: + return (0, int(sn)) + except (ValueError, TypeError): + return (1, str(sn)) + + sorted_series = sorted(series_groups.items(), key=_sort_key) + + if response_format == ResponseFormat.JSON: + result = { + "directory": str(dir_path), + "filters": filter_desc, + "total_scanned": len(dicom_files), + "truncated": truncated, + "match_count": len(matches), + "series_count": len(sorted_series), + "series": [data for _, data in sorted_series], + } + return json.dumps(result, indent=2) + else: + output = [ + "# DICOM Search Results\n", + f"**Filter**: {filter_desc}", + f"**Scanned**: {len(dicom_files)} files", + f"**Matches**: {len(matches)} files across {len(sorted_series)} series\n", + ] + if truncated: + output.append( + f"**Warning**: Scan truncated at {MAX_FILES} files.\n" + ) + if sorted_series: + headers = ["Series", "Description", "Type", "Files"] + rows = [ + [ + s["series_number"], + s["series_description"], + s["sequence_type"], + str(s["file_count"]), + ] + for _, s in sorted_series + ] + output.append(_format_markdown_table(headers, rows)) + else: + output.append("No files matched the filter criteria.") + return "\n".join(output) + + except Exception as e: + return f"Error searching DICOM files: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/segmentation.py b/mcps/dicom_mcp/dicom_mcp/tools/segmentation.py new file mode 100644 index 0000000..51c8d6a --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/segmentation.py @@ -0,0 +1,235 @@ +"""DICOM segmentation verification tool.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict, List, Tuple + +import pydicom +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag +from dicom_mcp.helpers.files import _find_dicom_files + +# Standard DICOM Segmentation Storage SOP Class UID +_SEG_SOP_CLASS_UID = "1.2.840.10008.5.1.4.1.1.66.4" + + +def _is_segmentation(ds: pydicom.Dataset) -> bool: + """Check if a dataset is a segmentation file.""" + sop_class = _safe_get_tag(ds, (0x0008, 0x0016)) + if sop_class == _SEG_SOP_CLASS_UID: + return True + if hasattr(ds, "SourceImageSequence"): + return True + return False + + +def _extract_references(ds: pydicom.Dataset) -> List[Dict[str, str]]: + """Extract referenced SOP Instance UIDs from a segmentation dataset.""" + refs: List[Dict[str, str]] = [] + + # Check SourceImageSequence + if hasattr(ds, "SourceImageSequence"): + for item in ds.SourceImageSequence: + ref_uid = _safe_get_tag(item, (0x0008, 0x1155)) + ref_class = _safe_get_tag(item, (0x0008, 0x1150)) + if ref_uid != "N/A": + refs.append( + { + "ReferencedSOPInstanceUID": str(ref_uid), + "ReferencedSOPClassUID": str(ref_class), + "source": "SourceImageSequence", + } + ) + + # Check ReferencedSeriesSequence → ReferencedInstanceSequence + if hasattr(ds, "ReferencedSeriesSequence"): + for series_item in ds.ReferencedSeriesSequence: + if hasattr(series_item, "ReferencedInstanceSequence"): + for inst_item in series_item.ReferencedInstanceSequence: + ref_uid = _safe_get_tag(inst_item, (0x0008, 0x1155)) + ref_class = _safe_get_tag(inst_item, (0x0008, 0x1150)) + if ref_uid != "N/A": + refs.append( + { + "ReferencedSOPInstanceUID": str(ref_uid), + "ReferencedSOPClassUID": str(ref_class), + "source": "ReferencedSeriesSequence", + } + ) + + return refs + + +@mcp.tool( + name="dicom_verify_segmentations", + annotations=ToolAnnotations( + title="Verify Segmentation Source References", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_verify_segmentations( + directory: str, + recursive: bool = True, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Validate that segmentation DICOM files reference valid source images. + + Scans a directory for segmentation files (identified by SOPClassUID or + SourceImageSequence presence), extracts their referenced SOPInstanceUIDs, + and verifies that each reference points to an existing source file in + the same directory. + + Useful for QA of segmentation outputs — ensuring every segmentation + is linked to a valid source image with no dangling references. + + Args: + directory: Directory containing segmentation and source DICOM files + recursive: Search subdirectories (default True) + response_format: Output format (markdown or json) + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + + all_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, recursive + ) + + if not all_files: + return f"No DICOM files found in {directory}" + + # Classify files and build source UID index + seg_files: List[Tuple[Path, pydicom.Dataset]] = [] + source_index: Dict[str, Path] = {} # SOPInstanceUID → Path + + for fp, ds in all_files: + sop_uid = _safe_get_tag(ds, (0x0008, 0x0018)) + if sop_uid != "N/A": + source_index[str(sop_uid)] = fp + + if _is_segmentation(ds): + seg_files.append((fp, ds)) + + if not seg_files: + return ( + f"No segmentation files found in {directory}. " + f"Scanned {len(all_files)} DICOM files." + ) + + # Verify references for each segmentation + results: List[Dict[str, Any]] = [] + total_refs = 0 + matched_refs = 0 + unmatched_refs = 0 + + for fp, ds in seg_files: + seg_sop_uid = _safe_get_tag(ds, (0x0008, 0x0018)) + seg_series_desc = _safe_get_tag(ds, (0x0008, 0x103E)) + references = _extract_references(ds) + total_refs += len(references) + + ref_results: List[Dict[str, Any]] = [] + for ref in references: + ref_uid = ref["ReferencedSOPInstanceUID"] + source_path = source_index.get(ref_uid) + if source_path: + matched_refs += 1 + ref_results.append( + { + "ReferencedSOPInstanceUID": ref_uid, + "source": ref["source"], + "matched": True, + "matched_file": source_path.name, + } + ) + else: + unmatched_refs += 1 + ref_results.append( + { + "ReferencedSOPInstanceUID": ref_uid, + "source": ref["source"], + "matched": False, + "matched_file": None, + } + ) + + results.append( + { + "segmentation_file": fp.name, + "segmentation_sop_uid": str(seg_sop_uid), + "series_description": str(seg_series_desc), + "reference_count": len(references), + "references": ref_results, + } + ) + + all_matched = unmatched_refs == 0 + + if response_format == ResponseFormat.JSON: + output = { + "directory": str(dir_path), + "total_files": len(all_files), + "truncated": truncated, + "segmentation_files": len(seg_files), + "source_files": len(all_files) - len(seg_files), + "total_references": total_refs, + "matched_references": matched_refs, + "unmatched_references": unmatched_refs, + "all_matched": all_matched, + "segmentations": results, + } + return json.dumps(output, indent=2) + else: + status = "ALL MATCHED" if all_matched else "UNMATCHED REFERENCES FOUND" + output_lines = [ + f"# Segmentation Verification: {dir_path.name}\n", + f"**Status**: {status}\n", + "## Summary\n", + f"- **Total files scanned**: {len(all_files)}", + f"- **Segmentation files**: {len(seg_files)}", + f"- **Source files**: {len(all_files) - len(seg_files)}", + f"- **Total references**: {total_refs}", + f"- **Matched**: {matched_refs}", + f"- **Unmatched**: {unmatched_refs}", + ] + + if truncated: + output_lines.append( + "\n**Warning**: File scan was truncated. " + "Results may be incomplete." + ) + + # Show details for segmentations with unmatched references + unmatched_segs = [ + r for r in results if any(not ref["matched"] for ref in r["references"]) + ] + if unmatched_segs: + output_lines.append("\n## Unmatched References\n") + for seg in unmatched_segs: + output_lines.append(f"### {seg['segmentation_file']}") + output_lines.append(f"Series: {seg['series_description']}\n") + for ref in seg["references"]: + if not ref["matched"]: + output_lines.append( + f"- `{ref['ReferencedSOPInstanceUID']}` " + f"(from {ref['source']}) — **NOT FOUND**" + ) + + if all_matched and seg_files: + output_lines.append( + "\nAll segmentation references successfully matched " + "to source files." + ) + + return "\n".join(output_lines) + + except Exception as e: + return f"Error verifying segmentations: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/ti_analysis.py b/mcps/dicom_mcp/dicom_mcp/tools/ti_analysis.py new file mode 100644 index 0000000..e6ae58c --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/ti_analysis.py @@ -0,0 +1,310 @@ +"""DICOM inversion time (TI) analysis tool for MOLLI sequences.""" + +import asyncio +import json +import statistics +from collections import defaultdict +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +import pydicom +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _format_markdown_table +from dicom_mcp.helpers.sequence import _identify_sequence_type +from dicom_mcp.helpers.files import _find_dicom_files +from dicom_mcp.helpers.philips import _resolve_philips_private_tag + + +def _extract_inversion_time(ds: pydicom.Dataset) -> Optional[float]: + """Extract inversion time from a DICOM dataset, handling vendor differences. + + Fallback chain: + 1. Standard tag (0018,0082) InversionTime — works for Siemens/GE + 2. Philips private tag DD 006, offset 0x72 in group 2005 — confirmed + at (2005,1572) on Philips Achieva dStream + 3. Philips private tag DD 001, offset 0x20 in group 2001 — alternate + location on some Philips software versions + + Returns the TI value in milliseconds, or None if not found. + """ + # 1. Standard InversionTime tag + ti_val = _safe_get_tag(ds, (0x0018, 0x0082)) + if ti_val != "N/A": + try: + return float(ti_val) + except (ValueError, TypeError): + pass + + # 2-3. Philips private tag fallbacks + manufacturer = _safe_get_tag(ds, (0x0008, 0x0070)) + if "philips" not in str(manufacturer).lower(): + return None + + # Try DD 006, offset 0x72 in group 2005 + resolved_tag, _, value_str = _resolve_philips_private_tag( + ds, dd_number=6, element_offset=0x72, private_group=0x2005 + ) + if resolved_tag is not None and value_str is not None: + try: + return float(value_str) + except (ValueError, TypeError): + pass + + # Try DD 001, offset 0x20 in group 2001 + resolved_tag, _, value_str = _resolve_philips_private_tag( + ds, dd_number=1, element_offset=0x20, private_group=0x2001 + ) + if resolved_tag is not None and value_str is not None: + try: + return float(value_str) + except (ValueError, TypeError): + pass + + return None + + +@mcp.tool( + name="dicom_analyze_ti", + annotations=ToolAnnotations( + title="Analyze Inversion Times (MOLLI)", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_analyze_ti( + directory: str, + recursive: bool = True, + gap_threshold: float = 2500.0, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Analyze inversion times from MOLLI/T1-mapping sequences. + + Extracts TI values across all T1 mapping files in a directory, handling + manufacturer-specific differences (Siemens/GE use standard tags, Philips + stores TI in private tags). Groups by series, sorts by instance number, + and computes statistics including gap analysis. + + Automatically handles: + - Siemens/GE: standard InversionTime tag (0018,0082) + - Philips: private tag (2005,xx72) via DD 006 block resolution + + Args: + directory: Directory containing MOLLI/T1 mapping DICOM files + recursive: Search subdirectories (default True) + gap_threshold: Flag gaps between consecutive TIs exceeding this + value in ms (default 2500.0) + response_format: Output format (markdown or json) + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + + all_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, recursive + ) + + if not all_files: + return f"No DICOM files found in {directory}" + + # Filter to T1 mapping / MOLLI files + ti_files: List[Tuple[Path, pydicom.Dataset]] = [] + for fp, ds in all_files: + seq_type = _identify_sequence_type(ds) + if seq_type.value == "t1_mapping": + ti_files.append((fp, ds)) + continue + # Also catch files with MOLLI in the description that might + # not be detected as t1_mapping by the heuristic + desc = str(_safe_get_tag(ds, (0x0008, 0x103E))).upper() + if "MOLLI" in desc: + ti_files.append((fp, ds)) + + if not ti_files: + return ( + f"No T1 mapping or MOLLI sequences found in {directory}. " + f"Scanned {len(all_files)} DICOM files." + ) + + # Group by (SeriesNumber, SeriesDescription) and extract TI + series_data: Dict[Tuple[str, str], List[Dict[str, Any]]] = defaultdict(list) + + for fp, ds in ti_files: + series_num = str(_safe_get_tag(ds, (0x0020, 0x0011))) + series_desc = str(_safe_get_tag(ds, (0x0008, 0x103E))) + instance_num = _safe_get_tag(ds, (0x0020, 0x0013)) + manufacturer = str(_safe_get_tag(ds, (0x0008, 0x0070))) + ti_value = _extract_inversion_time(ds) + + try: + inst_num = int(instance_num) + except (ValueError, TypeError): + inst_num = 0 + + series_data[(series_num, series_desc)].append( + { + "file": fp.name, + "instance_number": inst_num, + "inversion_time": ti_value, + "manufacturer": manufacturer, + } + ) + + # Analyze each series + series_results: List[Dict[str, Any]] = [] + + for (series_num, series_desc), files in sorted(series_data.items()): + # Sort by instance number + files.sort(key=lambda x: x["instance_number"]) + + all_tis = [f["inversion_time"] for f in files] + non_zero_tis = [ti for ti in all_tis if ti is not None and ti > 0] + zero_tis = [ti for ti in all_tis if ti is not None and ti == 0] + none_tis = [ti for ti in all_tis if ti is None] + + # Compute statistics on non-zero TIs + stats: Dict[str, Any] = { + "total_files": len(files), + "ti_found": len(all_tis) - len(none_tis), + "ti_not_found": len(none_tis), + "zero_ti_count": len(zero_tis), + "non_zero_ti_count": len(non_zero_tis), + } + + if non_zero_tis: + sorted_tis = sorted(non_zero_tis) + stats["ti_min"] = min(non_zero_tis) + stats["ti_max"] = max(non_zero_tis) + stats["ti_range"] = max(non_zero_tis) - min(non_zero_tis) + stats["ti_mean"] = statistics.mean(non_zero_tis) + stats["ti_median"] = statistics.median(non_zero_tis) + + # Compute consecutive gaps (based on sorted order) + gaps = [ + sorted_tis[i + 1] - sorted_tis[i] + for i in range(len(sorted_tis) - 1) + ] + if gaps: + stats["max_gap"] = max(gaps) + stats["gap_threshold_exceeded"] = max(gaps) > gap_threshold + stats["gaps"] = gaps + else: + stats["max_gap"] = 0.0 + stats["gap_threshold_exceeded"] = False + stats["gaps"] = [] + + manufacturer = files[0]["manufacturer"] if files else "Unknown" + + ordered_tis = [ + { + "instance": f["instance_number"], + "ti": f["inversion_time"], + "file": f["file"], + } + for f in files + ] + + series_results.append( + { + "series_number": series_num, + "series_description": series_desc, + "manufacturer": manufacturer, + "statistics": stats, + "ordered_inversion_times": ordered_tis, + } + ) + + if response_format == ResponseFormat.JSON: + output = { + "directory": str(dir_path), + "total_files_scanned": len(all_files), + "truncated": truncated, + "t1_mapping_files": len(ti_files), + "series_count": len(series_results), + "gap_threshold": gap_threshold, + "series": series_results, + } + return json.dumps(output, indent=2) + else: + output_lines = [ + f"# Inversion Time Analysis: {dir_path.name}\n", + f"**Files scanned**: {len(all_files)}", + f"**T1 mapping files**: {len(ti_files)}", + f"**Series found**: {len(series_results)}", + f"**Gap threshold**: {gap_threshold} ms", + ] + + if truncated: + output_lines.append( + "\n**Warning**: File scan was truncated. " + "Results may be incomplete." + ) + + for sr in series_results: + stats = sr["statistics"] + output_lines.append( + f"\n## Series {sr['series_number']}: " + f"{sr['series_description']}\n" + ) + output_lines.append(f"**Manufacturer**: {sr['manufacturer']}") + output_lines.append(f"**Total files**: {stats['total_files']}") + + if stats["ti_not_found"] > 0: + output_lines.append( + f"**TI not found**: {stats['ti_not_found']} files " + "(no standard or private TI tag)" + ) + + output_lines.append(f"**Non-zero TIs**: {stats['non_zero_ti_count']}") + output_lines.append( + f"**Zero TIs**: {stats['zero_ti_count']} " "(likely output maps)" + ) + + if stats.get("ti_min") is not None: + output_lines.append( + f"\n**TI range**: {stats['ti_min']:.1f} - " + f"{stats['ti_max']:.1f} ms " + f"(range: {stats['ti_range']:.1f} ms)" + ) + output_lines.append( + f"**Mean**: {stats['ti_mean']:.1f} ms | " + f"**Median**: {stats['ti_median']:.1f} ms" + ) + + if stats.get("max_gap", 0) > 0: + gap_warning = ( + " **WARNING: exceeds threshold**" + if stats.get("gap_threshold_exceeded") + else "" + ) + output_lines.append( + f"**Largest gap**: {stats['max_gap']:.1f} ms" + f"{gap_warning}" + ) + + # Ordered TI table + output_lines.append("\n### Inversion Times (ordered)\n") + headers = ["Instance", "TI (ms)", "File"] + rows = [] + for ti_entry in sr["ordered_inversion_times"]: + ti_display = ( + f"{ti_entry['ti']:.1f}" if ti_entry["ti"] is not None else "N/A" + ) + rows.append( + [ + str(ti_entry["instance"]), + ti_display, + ti_entry["file"], + ] + ) + output_lines.append(_format_markdown_table(headers, rows)) + + return "\n".join(output_lines) + + except Exception as e: + return f"Error analyzing inversion times: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/tree.py b/mcps/dicom_mcp/dicom_mcp/tools/tree.py new file mode 100644 index 0000000..e554926 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/tree.py @@ -0,0 +1,84 @@ +"""DICOM tree dump tool.""" + +import asyncio +import json +from pathlib import Path + +import pydicom +from pydicom.errors import InvalidDicomError +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tree import _build_tree_text, _build_tree_json + + +@mcp.tool( + name="dicom_dump_tree", + annotations=ToolAnnotations( + title="Dump DICOM Tree Structure", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_dump_tree( + file_path: str, + max_depth: int = 10, + show_private: bool = True, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Display the full hierarchical structure of a DICOM file. + + Recursively traverses the DICOM dataset including nested sequences, + producing an indented tree view. Useful for deep inspection of DICOM + structure, discovering nested sequence contents, and understanding + how data is organized across different manufacturers. + + Args: + file_path: Path to a DICOM file + max_depth: Maximum recursion depth for nested sequences (default 10) + show_private: Include private tags in the output (default True) + response_format: Output format (markdown or json) + """ + try: + fp = Path(file_path) + if not fp.exists(): + return f"Error: File not found: {file_path}" + + ds = await asyncio.to_thread(pydicom.dcmread, fp, stop_before_pixels=True) + + if response_format == ResponseFormat.JSON: + tree = _build_tree_json( + ds, + max_depth=max_depth, + show_private=show_private, + ) + result = { + "file": fp.name, + "element_count": len([e for e in ds if e.tag != (0x7FE0, 0x0010)]), + "max_depth": max_depth, + "show_private": show_private, + "tree": tree, + } + return json.dumps(result, indent=2) + else: + lines = _build_tree_text( + ds, + max_depth=max_depth, + show_private=show_private, + ) + header = [ + f"# DICOM Tree: {fp.name}\n", + f"**Max depth**: {max_depth}", + f"**Show private tags**: {show_private}\n", + "```", + ] + footer = ["```"] + return "\n".join(header + lines + footer) + + except InvalidDicomError: + return f"Error: Not a valid DICOM file: {file_path}" + except Exception as e: + return f"Error dumping DICOM tree: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/uid_comparison.py b/mcps/dicom_mcp/dicom_mcp/tools/uid_comparison.py new file mode 100644 index 0000000..7ec32e5 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/uid_comparison.py @@ -0,0 +1,149 @@ +"""DICOM UID comparison tool.""" + +import asyncio +import json +from pathlib import Path +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _resolve_tag +from dicom_mcp.helpers.files import _find_dicom_files + + +@mcp.tool( + name="dicom_compare_uids", + annotations=ToolAnnotations( + title="Compare DICOM UIDs Between Directories", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_compare_uids( + directory1: str, + directory2: str, + recursive: bool = True, + compare_tag: str = "SeriesInstanceUID", + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Compare UID sets between two DICOM directories. + + Extracts a specified UID tag from all files in both directories and + performs set comparison to identify shared, missing, and extra UIDs. + Useful for verifying that processed outputs contain all source series, + or that two datasets are aligned. + + Args: + directory1: First directory (the "reference" set) + directory2: Second directory (the "comparison" set) + recursive: Search subdirectories (default True) + compare_tag: DICOM tag keyword or hex code to compare + (default "SeriesInstanceUID"). Supports keywords + like "StudyInstanceUID" or hex codes like "0020,000E". + response_format: Output format (markdown or json) + """ + try: + dir1 = Path(directory1) + dir2 = Path(directory2) + + if not dir1.exists(): + return f"Error: Directory not found: {directory1}" + if not dir2.exists(): + return f"Error: Directory not found: {directory2}" + + # Resolve the tag specification + tag_tuple, tag_name = _resolve_tag(compare_tag) + if tag_tuple is None: + return ( + f"Error: Could not resolve tag '{compare_tag}'. " + "Use a keyword like 'SeriesInstanceUID' or hex code like '0020,000E'." + ) + + # Read files from both directories + files1, truncated1 = await asyncio.to_thread(_find_dicom_files, dir1, recursive) + files2, truncated2 = await asyncio.to_thread(_find_dicom_files, dir2, recursive) + + if not files1: + return f"No DICOM files found in {directory1}" + if not files2: + return f"No DICOM files found in {directory2}" + + # Extract UIDs from each directory + uids1 = set() + for _, ds in files1: + val = _safe_get_tag(ds, tag_tuple) + if val != "N/A": + uids1.add(str(val)) + + uids2 = set() + for _, ds in files2: + val = _safe_get_tag(ds, tag_tuple) + if val != "N/A": + uids2.add(str(val)) + + # Set comparison + shared = sorted(uids1 & uids2) + only_in_first = sorted(uids1 - uids2) + only_in_second = sorted(uids2 - uids1) + match = len(only_in_first) == 0 and len(only_in_second) == 0 + + result = { + "compare_tag": tag_name, + "directory1": str(dir1), + "directory2": str(dir2), + "directory1_files": len(files1), + "directory2_files": len(files2), + "directory1_truncated": truncated1, + "directory2_truncated": truncated2, + "unique_in_directory1": len(uids1), + "unique_in_directory2": len(uids2), + "shared": len(shared), + "only_in_directory1": len(only_in_first), + "only_in_directory2": len(only_in_second), + "match": match, + "shared_values": shared, + "only_in_directory1_values": only_in_first, + "only_in_directory2_values": only_in_second, + } + + if response_format == ResponseFormat.JSON: + return json.dumps(result, indent=2) + else: + status = "MATCH" if match else "MISMATCH" + output = [ + f"# UID Comparison: {tag_name}\n", + f"**Status**: {status}\n", + "## Summary\n", + "| | Directory 1 | Directory 2 |", + "|--|------------|------------|", + f"| **Path** | {dir1.name} | {dir2.name} |", + f"| **Files scanned** | {len(files1)} | {len(files2)} |", + f"| **Unique {tag_name}s** | {len(uids1)} | {len(uids2)} |", + "", + f"- **Shared**: {len(shared)}", + f"- **Only in directory 1**: {len(only_in_first)}", + f"- **Only in directory 2**: {len(only_in_second)}", + ] + + if truncated1 or truncated2: + output.append( + "\n**Warning**: File scan was truncated in one or both " + "directories. Results may be incomplete." + ) + + if only_in_first: + output.append(f"\n## Only in {dir1.name}\n") + for uid in only_in_first: + output.append(f"- `{uid}`") + + if only_in_second: + output.append(f"\n## Only in {dir2.name}\n") + for uid in only_in_second: + output.append(f"- `{uid}`") + + return "\n".join(output) + + except Exception as e: + return f"Error comparing UIDs: {str(e)}" diff --git a/mcps/dicom_mcp/dicom_mcp/tools/validation.py b/mcps/dicom_mcp/dicom_mcp/tools/validation.py new file mode 100644 index 0000000..7d8ecc6 --- /dev/null +++ b/mcps/dicom_mcp/dicom_mcp/tools/validation.py @@ -0,0 +1,254 @@ +"""DICOM sequence validation and series analysis tools.""" + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict, Optional + +from pydicom import datadict +from mcp.types import ToolAnnotations + +from dicom_mcp.server import mcp +from dicom_mcp.config import MAX_FILES +from dicom_mcp.constants import ResponseFormat +from dicom_mcp.helpers.tags import _safe_get_tag, _format_markdown_table +from dicom_mcp.helpers.sequence import _identify_sequence_type +from dicom_mcp.helpers.files import _find_dicom_files + + +@mcp.tool( + name="dicom_validate_sequence", + annotations=ToolAnnotations( + title="Validate Sequence Parameters", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_validate_sequence( + file_path: str, + expected_parameters: Optional[Dict[str, Any]] = None, + manufacturer: Optional[str] = None, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Validate MRI sequence parameters against expected values. + + Validates acquisition parameters (TR, TE, flip angle, etc.) against + expected values. Useful for verifying that test data matches protocol + specifications or that sequences are consistent across manufacturers. + """ + try: + fp = Path(file_path) + if not fp.exists(): + return f"Error: File not found: {file_path}" + + import pydicom + + ds = await asyncio.to_thread(pydicom.dcmread, fp, stop_before_pixels=True) + + sequence_type = _identify_sequence_type(ds) + actual_manufacturer = _safe_get_tag(ds, (0x0008, 0x0070)) + + validation_results = { + "file_path": str(fp), + "sequence_type": sequence_type.value, + "manufacturer": actual_manufacturer, + "parameters": {}, + "validation_passed": True, + } + + if manufacturer: + manufacturer_match = manufacturer.lower() in actual_manufacturer.lower() + validation_results["manufacturer_match"] = manufacturer_match + if not manufacturer_match: + validation_results["validation_passed"] = False + + if expected_parameters: + param_tag_map = { + "RepetitionTime": (0x0018, 0x0080), + "EchoTime": (0x0018, 0x0081), + "InversionTime": (0x0018, 0x0082), + "FlipAngle": (0x0018, 0x1314), + "ScanningSequence": (0x0018, 0x0020), + "SequenceVariant": (0x0018, 0x0021), + "MRAcquisitionType": (0x0018, 0x0023), + } + + for param_name, expected_value in expected_parameters.items(): + if param_name in param_tag_map: + tag = param_tag_map[param_name] + actual_value = _safe_get_tag(ds, tag) + match = str(actual_value) == str(expected_value) + validation_results["parameters"][param_name] = { + "actual": actual_value, + "expected": str(expected_value), + "match": match, + } + if not match: + validation_results["validation_passed"] = False + + if response_format == ResponseFormat.JSON: + return json.dumps(validation_results, indent=2) + else: + output = [f"# Sequence Validation: {fp.name}\n"] + status = "PASSED" if validation_results["validation_passed"] else "FAILED" + output.append(f"**Validation Status**: {status}\n") + output.append("## Sequence Information") + output.append(f"- **Sequence Type**: {sequence_type.value}") + output.append(f"- **Manufacturer**: {actual_manufacturer}") + if manufacturer: + match_status = ( + "Match" + if validation_results.get("manufacturer_match", True) + else "Mismatch" + ) + output.append( + f"- **Expected Manufacturer**: {manufacturer} ({match_status})" + ) + output.append("") + if validation_results["parameters"]: + output.append("## Parameter Validation\n") + headers = ["Parameter", "Actual", "Expected", "Status"] + rows = [] + for pname, data in validation_results["parameters"].items(): + s = "Match" if data["match"] else "Mismatch" + rows.append([pname, data["actual"], data["expected"], s]) + output.append(_format_markdown_table(headers, rows)) + return "\n".join(output) + + except Exception as e: + return f"Error validating sequence: {str(e)}" + + +@mcp.tool( + name="dicom_analyze_series", + annotations=ToolAnnotations( + title="Analyze DICOM Series", + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=False, + ), +) +async def dicom_analyze_series( + directory: str, + response_format: ResponseFormat = ResponseFormat.MARKDOWN, +) -> str: + """Analyze a complete DICOM series for consistency and completeness. + + Analyzes all files in a series directory, checking for consistency of + acquisition parameters, completeness of the series, and identifying + any anomalies. Useful for validating test datasets. + """ + try: + dir_path = Path(directory) + if not dir_path.exists(): + return f"Error: Directory not found: {directory}" + + dicom_files, truncated = await asyncio.to_thread( + _find_dicom_files, dir_path, False + ) + + if not dicom_files: + return f"No DICOM files found in {directory}" + + first_fp, first_ds = dicom_files[0] + series_info = { + "series_description": _safe_get_tag(first_ds, (0x0008, 0x103E)), + "series_number": _safe_get_tag(first_ds, (0x0020, 0x0011)), + "series_uid": _safe_get_tag(first_ds, (0x0020, 0x000E)), + "modality": _safe_get_tag(first_ds, (0x0008, 0x0060)), + "manufacturer": _safe_get_tag(first_ds, (0x0008, 0x0070)), + "sequence_type": _identify_sequence_type(first_ds).value, + } + + consistency_issues = [] + params_to_check = [ + (0x0018, 0x0080), # RepetitionTime + (0x0018, 0x0081), # EchoTime + (0x0020, 0x000E), # SeriesInstanceUID + (0x0028, 0x0010), # Rows + (0x0028, 0x0011), # Columns + ] + + for tag in params_to_check: + tag_name = datadict.keyword_for_tag(tag) + values = set(_safe_get_tag(ds, tag) for _, ds in dicom_files) + if len(values) > 1: + consistency_issues.append( + f"{tag_name} varies across series: {', '.join(str(v) for v in values)}" + ) + + instance_numbers = [] + for _, ds in dicom_files: + try: + inst_num = int(_safe_get_tag(ds, (0x0020, 0x0013))) + instance_numbers.append(inst_num) + except (ValueError, TypeError): + pass + + if instance_numbers: + instance_numbers.sort() + expected_instances = max(instance_numbers) + expected_set = set(range(1, expected_instances + 1)) + found_set = set(instance_numbers) + missing = sorted(list(expected_set - found_set)) + complete = len(missing) == 0 + else: + expected_instances = 0 + missing = [] + complete = False + consistency_issues.append("Could not determine instance numbers") + + result = { + "series_info": series_info, + "file_count": len(dicom_files), + "truncated": truncated, + "consistency_check": { + "consistent_parameters": len(consistency_issues) == 0, + "issues": consistency_issues, + }, + "completeness": { + "expected_instances": expected_instances, + "found_instances": len(instance_numbers), + "missing": missing, + "complete": complete, + }, + } + + if response_format == ResponseFormat.JSON: + return json.dumps(result, indent=2) + else: + output = [f"# Series Analysis: {dir_path.name}\n", "## Series Information"] + for key, value in series_info.items(): + output.append(f"- **{key.replace('_', ' ').title()}**: {value}") + output.append(f"- **Total Files**: {len(dicom_files)}") + if truncated: + output.append(f"- **Warning**: Scan truncated at {MAX_FILES} files") + output.append("") + output.append("## Consistency Check") + if result["consistency_check"]["consistent_parameters"]: + output.append("All parameters are consistent across the series") + else: + output.append("Inconsistencies detected:") + for issue in result["consistency_check"]["issues"]: + output.append(f" - {issue}") + output.append("") + output.append("## Completeness Check") + if result["completeness"]["complete"]: + output.append(f"Series is complete ({expected_instances} instances)") + else: + output.append("Series may be incomplete") + output.append( + f"- Expected: {result['completeness']['expected_instances']} instances" + ) + output.append( + f"- Found: {result['completeness']['found_instances']} instances" + ) + if missing: + output.append(f"- Missing: {', '.join(str(m) for m in missing)}") + return "\n".join(output) + + except Exception as e: + return f"Error analyzing series: {str(e)}" diff --git a/mcps/dicom_mcp/docs/CAPABILITIES.md b/mcps/dicom_mcp/docs/CAPABILITIES.md new file mode 100644 index 0000000..0da9a4d --- /dev/null +++ b/mcps/dicom_mcp/docs/CAPABILITIES.md @@ -0,0 +1,25 @@ +# Claude DICOM-MCP Capabilities List + +| # | What I can do | Module | Possible without the MCP? | +| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |---------------------------------------------------------------------------------------------------| +| 1 | **Find DICOM files** in a folder, including subfolders, and tell you what's in them (series, sequence type, manufacturer). Can also just give a count. | `dicom_list_files` | ❌ No — I could list files with bash, but I couldn't read DICOM headers to identify what they are. | +| 2 | **Find Dixon sequences** in a folder and identify which images are water, fat, in-phase, and out-phase. | `dicom_find_dixon_series` | ❌ No — requires both DICOM parsing and domain-specific sequence identification logic. | +| 3 | **Read the metadata** (headers) from a single DICOM file — patient info, study details, scanner settings, geometry, etc. Can also resolve Philips private tags. | `dicom_get_metadata` | ❌ No — DICOM is a binary format. I can't read it with standard text tools. | +| 4 | **Compare headers side-by-side** across 2–10 DICOM files, highlighting what's different. | `dicom_compare_headers` | ❌ No — same reason; I can't parse the binary headers. | +| 5 | **Query any DICOM tag** across all files in a directory, optionally grouped by another tag (e.g. "show me all echo times, grouped by series"). | `dicom_query` | ❌ No. | +| 6 | **Summarise a whole directory** — give a high-level overview of what vendors, scanners, sequences, and series are present, with file counts. | `dicom_summarize_directory` | ❌ No. | +| 7 | **Validate scan parameters** (TR, TE, flip angle, etc.) against expected values for a given sequence. | `dicom_validate_sequence` | ❌ No — can't extract the parameters to validate. | +| 8 | **Analyse a full series** for consistency — check that all files in a series have matching parameters and that the series is complete. | `dicom_analyze_series` | ❌ No. | +| 9 | **Search for files** matching specific criteria using a filter language — text matching, numeric comparisons, or presence checks on any tag. | `dicom_search` | ❌ No. | +| 10 | **Read Philips private tags** by DD number and element offset, or list all private creator blocks in a file. | `dicom_query_philips_private` | ❌ No — these are vendor-specific proprietary tags buried in private DICOM groups. | +| 11 | **Extract pixel statistics** from an image — min, max, mean, standard deviation, percentiles, and optionally a histogram. Can focus on a specific rectangular region. | `dicom_read_pixels` | ❌ No — pixel data is encoded in the DICOM binary. | +| 12 | **Compute signal-to-noise ratio** by placing two regions of interest (signal and noise) on an image. | `dicom_compute_snr` | ❌ No. | +| 13 | **Render a DICOM image to PNG** with adjustable windowing and optional ROI overlays — this is how I showed you those three images. | `dicom_render_image` | ❌ No — I have no way to decode DICOM pixel data and produce a viewable image. | +| 14 | **Dump the full DICOM tree** — show the entire hierarchical structure of a file including nested sequences, with configurable depth. | `dicom_dump_tree` | ❌ No. | +| 15 | **Compare UID sets** between two directories — find which series/studies/instances are shared, missing, or extra. | `dicom_compare_uids` | ❌ No. | +| 16 | **Verify segmentation references** — check that segmentation DICOM files correctly point back to their source images. | `dicom_verify_segmentations` | ❌ No. | +| 17 | **Analyse inversion times** from MOLLI/NOLLI T1 mapping sequences — extract TI values across vendors (handling Philips private tags automatically), flag gaps, and show the acquisition order. | `dicom_analyze_ti` | ❌ No. | + +All 17 modules live under `dicom_mcp/tools/` and are registered via the `@mcp.tool()` decorator at server startup. **In short: none of these capabilities exist without the MCP.** DICOM is a complex binary medical imaging format. Without the MCP, I can see that `.dcm` files exist, but I can't open, read, interpret, or visualise any of them. The MCP turns me from "completely DICOM-blind" into something that can meaningfully inspect, compare, and analyse medical imaging data across vendors. + + diff --git a/mcps/dicom_mcp/docs/GUIDELINES.md b/mcps/dicom_mcp/docs/GUIDELINES.md new file mode 100644 index 0000000..87be480 --- /dev/null +++ b/mcps/dicom_mcp/docs/GUIDELINES.md @@ -0,0 +1,51 @@ +# Behavioural Guidelines + +## Purpose + +This document defines the behavioural constraints for LLMs using the DICOM MCP server. These constraints exist to ensure that the MCP operates strictly as a **data inspection and QA tool**, and does not drift into clinical interpretation or diagnostic guidance. + +## Regulatory Context + +This MCP is intended for use with Software as a Medical Device (SaMD) workflows. When an LLM analyses DICOM data, there is a risk that its responses may cross the boundary from technical data reporting into clinical advice. Statements such as "this data would be useful for diagnosing condition X" or "these acquisition parameters are inadequate for clinical purpose Y" constitute clinical guidance, which carries serious regulatory implications. + +To mitigate this risk, the MCP enforces a clear boundary: **report and describe, never advise or interpret clinically**. + +## Constraints + +### What the LLM should do + +1. **Report** what is observed in the DICOM data — tag values, pixel statistics, series counts, parameter values, file counts, and structural relationships. +2. **Describe** technical characteristics — acquisition parameters, sequence types, vendor differences, file organisation, UID relationships, and image properties. +3. **Compare** data across files, series, vendors, or directories when asked — presenting the differences factually. +4. **Flag** technical anomalies such as missing files, inconsistent parameters, broken references, or unexpected tag values. + +### What the LLM must not do + +1. **Do not** suggest clinical utility or diagnostic applications for the data (e.g. "this sequence is useful for assessing liver fat"). +2. **Do not** interpret findings in a clinical or diagnostic context (e.g. "the T1 values suggest abnormal tissue"). +3. **Do not** assess data quality relative to specific clinical use cases (e.g. "this data is adequate for a clinical report"). +4. **Do not** recommend clinical actions based on the data (e.g. "you should re-scan this patient" or "this study should be flagged for review"). +5. **Do not** offer guidance on clinical workflows or protocols (e.g. "this type of series is typically used before treatment planning"). +6. **Do not** make assertions about what a clinician, radiologist, or pathologist should conclude from the data. + +### The principle + +> Present data as-is. Qualified professionals draw the conclusions. + +The MCP is a lens, not an adviser. It makes DICOM data visible and navigable. It does not tell the user what the data means in a clinical context. + +## Enforcement + +These constraints are enforced at three levels: + +| Layer | Mechanism | Audience | +|-------|-----------|----------| +| **Protocol** | `FastMCP(instructions=...)` in `server.py` | Any MCP client (Claude, ChatGPT, Cursor, etc.) | +| **Session** | `CLAUDE.md` Behavioural Constraints section | Claude Code sessions | +| **Documentation** | This file (`docs/GUIDELINES.md`) | Developers, reviewers, auditors | + +The protocol-level instructions are the primary enforcement mechanism. They are sent to any MCP-compliant client at connection time and apply to all tool interactions. The CLAUDE.md section reinforces these constraints for Claude Code users. This document serves as the authoritative human-readable reference. + +## Scope + +These constraints apply to the LLM's behaviour when using any of the 17 DICOM MCP tools. They do not restrict the tools themselves — the tools return raw data. The constraints govern how the LLM frames, contextualises, and presents that data to the user. diff --git a/mcps/dicom_mcp/docs/TODO.md b/mcps/dicom_mcp/docs/TODO.md new file mode 100644 index 0000000..150c323 --- /dev/null +++ b/mcps/dicom_mcp/docs/TODO.md @@ -0,0 +1,95 @@ +# DICOM MCP Server — TODO + +Two remaining items. The original four-item enhancement plan (2026-02-11) has been completed — `dicom_query` and `dicom_search` were implemented, along with four additional tools (`dicom_dump_tree`, `dicom_compare_uids`, `dicom_verify_segmentations`, `dicom_analyze_ti`). The items below are follow-on improvements identified during testing. + +--- + +## 1. Private Tag Exploration Tool (`dicom_private_tags`) + +**Priority:** High — this is the trickiest to get right but the most valuable for cross-manufacturer QA. + +### Problem + +Manufacturers store critical acquisition parameters in private (vendor-specific) DICOM tags +rather than standard public tags. This causes real issues in multi-vendor QA workflows: + +- **Philips TE=0 quirk:** The Erasmus Achieva 1.5T dataset shows `EchoTime = 0` for several + Dixon series (Body mDixon THRIVE, Thigh Dixon Volume). The actual multi-point Dixon echo + times are stored in Philips private tags, not the standard `(0018,0081)` EchoTime field. + This was confirmed via `dicom_query` grouped by SeriesDescription on the Erasmus dataset — + 328 of 648 Thigh Dixon files and 100 of 157 mDixon THRIVE files report TE=0. +- **Manufacturer encoding differences:** Siemens stores echo times in public tags normally + (e.g. MOST series TEs of 2.38–19.06 ms on Avanto_fit). Philips MOST TEs (2.371–18.961 ms) + are in public tags too, but Dixon TEs are hidden in private tags. GE embeds Dixon image type + info in `ImageType` fields rather than series descriptions. +- **Nested sequences and binary blobs:** Philips private tags frequently contain nested + DICOM sequences, and some values are only interpretable if you know the specific software + version. Binary data needs special handling to avoid dumping unreadable content. + +### Discussion Notes + +From our initial conversation, we decided **not** to implement this tool immediately because: + +1. Deciphering some private tags requires conditional logic based on the contents of certain + public tags (or other private tags). The exact rules are manufacturer-specific and need to + be rediscovered through hands-on exploration. +2. Building the wrong abstraction would be worse than no abstraction — we need to tinker with + real data first before committing to a tool design. + +### Proposed Design (Single tool with three modes) + +**`discover` mode** — Scan a file and list all private tag blocks with their creator strings. +Answers "what vendor modules are present?" Output: group number, creator string, tag count per block. + +**`dump` mode** — Show all private tags within a specific creator block (or all private tags in a file). +For each tag: hex address, creator, VR, value. Binary values show first N bytes as hex + length. +Nested sequences show item count with optional one-level-deep recursion. + +**`search` mode** — Scan across a directory looking for private tags matching a keyword in either +the creator string or the tag value. Useful for hunting down where manufacturers hide specific +parameters (e.g. "find any private tag with 'echo' in the creator or value"). + +### Additional Considerations + +- **Creator filtering:** Filter by creator substring, e.g. `creator="Philips"` to only see Philips blocks. +- **Known tag dictionaries:** Embed a small lookup table for commonly useful private tags + (e.g. Philips `(2005,xx10)` for actual echo times). Start without this and add later. +- **Binary value display:** Show first 64 bytes as hex + total length, rather than full dumps. + +### Suggested Next Steps + +1. Start by exploring the Erasmus Philips data with `dicom_get_metadata` using custom hex tags + to see what private blocks exist and specifically chase down the TE=0 mystery. +2. Do the same on Siemens and GE data to understand the differences. +3. Once the patterns and conditional logic are clear, design the tool around real use cases. + +--- + +## 2. `dicom_compare_headers` Directory Mode + +**Priority:** Medium — useful for cross-series protocol checks but less urgent than private tags. + +### Problem + +`dicom_compare_headers` currently requires 2–10 explicit file paths. For cross-series protocol +validation (e.g. "are all MOST series using the same TR/FA across a study?"), you have to +manually pick representative files from each series first. + +### Proposed Enhancement + +Add a **directory mode** that automatically picks one representative file per series and compares +them. This would enable single-call cross-series protocol checks. + +### Design Ideas + +- New parameter: `directory` as an alternative to `file_paths` +- Auto-select one file per unique SeriesInstanceUID (first file encountered, or configurable) +- Reuse existing comparison logic +- Show series description in output to identify which series each column represents +- Optionally filter which series to include (by description pattern or sequence type) + +--- + +*Last updated: 2026-02-25 — after adding 4 new tools (dicom_dump_tree, dicom_compare_uids, +dicom_verify_segmentations, dicom_analyze_ti) and smoke testing against Philips, Siemens, and +GE MOLLI/NOLLI data.* diff --git a/mcps/dicom_mcp/docs/USAGE.md b/mcps/dicom_mcp/docs/USAGE.md new file mode 100644 index 0000000..960c441 --- /dev/null +++ b/mcps/dicom_mcp/docs/USAGE.md @@ -0,0 +1,664 @@ +# DICOM MCP Server — Usage Guide + +Detailed usage examples, tool reference, and QA workflow patterns for the DICOM MCP server. + +For installation and setup, see [INSTALL.md](../INSTALL.md). For project overview, see [README.md](../README.md). + +> **Tip: Pair with a filesystem MCP server for maximum capability.** The DICOM MCP server works well on its own for metadata inspection and validation. However, if Claude also has access to a filesystem MCP server with media reading support (e.g. the `read_media_file` tool), it can *view* rendered images directly, enabling an interactive visual feedback loop — render an image, inspect it, adjust ROI placement, measure, iterate — all within a single conversation. Without a filesystem MCP, Claude can still render images and save them to disk, but you'll need to open them yourself in a viewer and describe what you see. + +--- + +## Quick Examples + +### Finding test data + +``` +List all Dixon sequences in my test data directory +``` + +The server will use `dicom_find_dixon_series` to locate all Dixon sequences and show what image types are available. + +### Comparing fat/water selection + +``` +Compare the headers of these two files to see which Dixon images were selected: +- /data/test01/water.dcm +- /data/test01/fat.dcm +``` + +### Validating a protocol + +``` +Validate that /data/scan01/image001.dcm matches our Siemens protocol: +- TR should be 4.5 +- TE should be 2.3 +- Manufacturer should be Siemens +``` + +### Quick directory overview + +``` +Summarize what's in the /data/large_study directory +``` + +### Finding specific files + +``` +Find all files in /data/study where the series description contains "MOST" +and echo time is greater than 10 +``` + +### Rendering an image + +``` +Render /data/liver_scan/slice_005.dcm with auto-windowing +``` + +### Measuring SNR + +``` +Compute the SNR on /data/scan.dcm with a signal ROI at [100, 200, 50, 50] +in the liver and a noise ROI at [20, 20, 40, 40] in background air +``` + +### Dumping full DICOM structure + +``` +Dump the full DICOM tree of /data/scan.dcm including nested sequences +``` + +### Comparing UIDs across directories + +``` +Compare the SeriesInstanceUIDs between /data/study_v1 and /data/study_v2 +``` + +### Verifying segmentation references + +``` +Check that all segmentation files in /data/segs reference valid source images +``` + +### Analysing MOLLI inversion times + +``` +Analyze the inversion times in /data/molli_series — is it a proper 5(3)3 scheme? +``` + +### Inspecting Philips private tags + +``` +List all Philips Private Creator tags in /data/philips_scan/image001.dcm +``` + +``` +Look up DD 001 element offset 0x85 in /data/philips_scan/image001.dcm +``` + +--- + +## PII Filtering + +When PII filtering is enabled (via `DICOM_MCP_PII_FILTER=true`), the following patient tags are replaced with `[REDACTED]` in tool output: + +- PatientName +- PatientID +- PatientBirthDate +- PatientSex + +**Affected tools**: `dicom_get_metadata`, `dicom_compare_headers`, `dicom_summarize_directory`, `dicom_query`. + +All other tools do not expose patient data and are unaffected. See [INSTALL.md](../INSTALL.md) for configuration instructions. + +--- + +## Tool Reference + +### Directory & File Discovery + +#### `dicom_list_files` + +List all DICOM files in a directory with metadata filtering. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Path to search | +| `recursive` | bool | `true` | Search subdirectories | +| `filter_sequence_type` | string | `null` | Filter by type: `dixon`, `t1_mapping`, `multi_echo_gre`, `spin_echo_ir`, `t1`, `t2`, `flair`, `dwi`, `localizer` | +| `count_only` | bool | `false` | Return series breakdown with file counts instead of individual file listing | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "directory": "/data/test_suite", + "filter_sequence_type": "dixon", + "count_only": true +} +``` + +#### `dicom_summarize_directory` + +Get a high-level overview of DICOM directory contents showing unique values and file counts for patient info, manufacturer, scanner model, field strength, institution, series descriptions, and sequence types. + +When PII filtering is enabled, patient tags (Name, ID, DOB, Sex) are redacted. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Path to directory | +| `recursive` | bool | `true` | Search subdirectories | +| `include_series_overview` | bool | `true` | Include per-series table with acquisition parameters (TR, TE, TI, FA) | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "directory": "/data/large_study", + "include_series_overview": true +} +``` + +#### `dicom_find_dixon_series` + +Find and analyse Dixon (chemical shift) sequences. Automatically identifies Dixon series and detects image types (water, fat, in-phase, out-phase). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Path to search | +| `recursive` | bool | `true` | Search subdirectories | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "directory": "/data/body_comp_study" +} +``` + +#### `dicom_search` + +Find DICOM files matching specific filter criteria using AND logic across multiple filters. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Path to directory | +| `filters` | list[string] | *required* | Filter expressions (see syntax below) | +| `recursive` | bool | `true` | Search subdirectories | +| `mode` | string | `summary` | `count`, `paths`, or `summary` | +| `response_format` | string | `markdown` | `markdown` or `json` | + +**Filter syntax:** + +| Operator type | Examples | +|---------------|----------| +| Text (case-insensitive) | `"SeriesDescription contains MOST"`, `"PatientName is not UNKNOWN"`, `"SeriesDescription starts with Thigh"`, `"SeriesDescription ends with Dixon"` | +| Numeric/symbolic | `"EchoTime > 10"`, `"FlipAngle <= 15"`, `"RepetitionTime = 516"`, `"SeriesNumber != 0"` | +| Presence | `"InversionTime exists"`, `"InversionTime missing"` | + +Tags can be keywords (`EchoTime`) or hex codes (`0018,0081`). + +```json +{ + "directory": "/data/study", + "filters": ["SeriesDescription contains MOST", "EchoTime > 10"], + "mode": "paths" +} +``` + +--- + +### Metadata & Validation + +#### `dicom_get_metadata` + +Extract DICOM header information organised by tag groups. When PII filtering is enabled, patient tags are redacted. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_path` | string | *required* | Path to DICOM file | +| `tag_groups` | list[string] | `null` | Groups to extract: `patient_info`, `study_info`, `series_info`, `image_info`, `acquisition`, `manufacturer`, `equipment`, `geometry`, `pixel_data` | +| `custom_tags` | list[string] | `null` | Additional tags as hex codes, e.g. `["0018,0080"]` | +| `philips_private_tags` | list[dict] | `null` | Philips private tags to resolve (see below) | +| `response_format` | string | `markdown` | `markdown` or `json` | + +**Philips private tags:** + +For Philips DICOM files, you can resolve private tags inline without using the separate `dicom_query_philips_private` tool. Each entry is a dict with `dd_number` and `element_offset` (and optionally `private_group`, default `0x2005`). + +```json +{ + "file_path": "/data/philips_scan.dcm", + "tag_groups": ["acquisition", "manufacturer"], + "philips_private_tags": [ + {"dd_number": 1, "element_offset": 133} + ] +} +``` + +**Standard example:** + +```json +{ + "file_path": "/data/scan.dcm", + "tag_groups": ["acquisition", "manufacturer"], + "custom_tags": ["0018,0080", "0018,0081"] +} +``` + +#### `dicom_compare_headers` + +Compare DICOM headers across multiple files side-by-side. When PII filtering is enabled, patient tags are redacted. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_paths` | list[string] | *required* | 2-10 files to compare | +| `tag_groups` | list[string] | `["acquisition", "series_info"]` | Which tag groups to compare | +| `show_differences_only` | bool | `false` | Only show tags that differ | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "file_paths": [ + "/data/test01/water.dcm", + "/data/test01/fat.dcm", + "/data/test01/in_phase.dcm" + ], + "show_differences_only": true +} +``` + +#### `dicom_validate_sequence` + +Validate MRI sequence parameters against expected values. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_path` | string | *required* | DICOM file to validate | +| `expected_parameters` | dict | `null` | Expected values. Supported keys: `RepetitionTime`, `EchoTime`, `InversionTime`, `FlipAngle`, `ScanningSequence`, `SequenceVariant`, `MRAcquisitionType` | +| `manufacturer` | string | `null` | Expected manufacturer name | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "file_path": "/data/test.dcm", + "expected_parameters": { + "RepetitionTime": 4.5, + "EchoTime": 2.3, + "InversionTime": 100, + "FlipAngle": 10 + }, + "manufacturer": "Siemens" +} +``` + +#### `dicom_analyze_series` + +Comprehensive analysis of a complete DICOM series — checks parameter consistency across all files, verifies completeness (no missing instances), and identifies anomalies. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Path containing series files | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "directory": "/data/series_001" +} +``` + +#### `dicom_query` + +Query arbitrary DICOM tags across all files in a directory. Aggregates unique values with file counts. When PII filtering is enabled, patient tags are redacted. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Path to directory | +| `tags` | list[string] | *required* | Tag keywords (e.g. `"EchoTime"`) or hex codes (e.g. `"0018,0081"`) | +| `recursive` | bool | `true` | Search subdirectories | +| `group_by` | string | `null` | Optional tag to group results by (e.g. `"SeriesDescription"`) | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "directory": "/data/study", + "tags": ["EchoTime", "FlipAngle"], + "group_by": "SeriesDescription" +} +``` + +--- + +### Structure & Comparison + +#### `dicom_dump_tree` + +Full hierarchical dump of DICOM structure, including nested sequences (SQ elements) with tree-character formatting. Useful for understanding complex DICOM files, inspecting nested structures (e.g. ReferencedSeriesSequence, SourceImageSequence), and debugging. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_path` | string | *required* | Path to DICOM file | +| `max_depth` | int | `10` | Maximum nesting depth to display | +| `show_private` | bool | `true` | Include private tags in output | +| `response_format` | string | `markdown` | `markdown` or `json` | + +When PII filtering is enabled, patient tags are redacted in the tree output. Pixel data `(7FE0,0010)` is always skipped. + +```json +{ + "file_path": "/data/scan.dcm", + "max_depth": 5, + "show_private": false +} +``` + +#### `dicom_compare_uids` + +Compare UID sets between two DICOM directories. Identifies shared UIDs, UIDs unique to each directory, and reports counts and details. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory1` | string | *required* | First directory to compare | +| `directory2` | string | *required* | Second directory to compare | +| `recursive` | bool | `true` | Search subdirectories | +| `compare_tag` | string | `SeriesInstanceUID` | Tag to compare — keyword (e.g. `StudyInstanceUID`, `SOPInstanceUID`) or hex code (e.g. `0020,000E`) | +| `response_format` | string | `markdown` | `markdown` or `json` | + +```json +{ + "directory1": "/data/study_original", + "directory2": "/data/study_reprocessed", + "compare_tag": "SOPInstanceUID" +} +``` + +--- + +### Segmentation & T1 Mapping + +#### `dicom_verify_segmentations` + +Validate that segmentation files correctly reference valid source images. Detects segmentation files by SOPClassUID (`1.2.840.10008.5.1.4.1.1.66.4`) or by the presence of `SourceImageSequence`. For each segmentation, checks that every `ReferencedSOPInstanceUID` points to an existing source file in the same directory. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Directory containing segmentation and source files | +| `recursive` | bool | `true` | Search subdirectories | +| `response_format` | string | `markdown` | `markdown` or `json` | + +Reports total segmentation files, total references, matched vs unmatched counts, and details for any unmatched references. + +```json +{ + "directory": "/data/study_with_segmentations" +} +``` + +#### `dicom_analyze_ti` + +Extract and validate inversion times from MOLLI / T1 mapping sequences. Handles vendor-specific differences automatically: + +- **Siemens / GE**: reads the standard `InversionTime` tag `(0018,0082)` +- **Philips**: falls back to private tag DD 006, offset 0x72 in group 2005 (confirmed at `(2005,xx72)`) or DD 001, offset 0x20 in group 2001 + +Groups by series, sorts by instance number, and computes statistics including gap analysis to detect missing or out-of-range inversion times. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `directory` | string | *required* | Directory containing MOLLI / T1 mapping files | +| `recursive` | bool | `true` | Search subdirectories | +| `gap_threshold` | float | `2500.0` | Flag consecutive TI gaps exceeding this value (ms) | +| `response_format` | string | `markdown` | `markdown` or `json` | + +**Output includes per series:** +- Ordered TI list sorted by instance number +- Count of zero-value TIs (typically output maps, not acquisitions) +- Count of non-zero TIs (actual inversions) +- TI range, mean, and median +- Largest consecutive gap, with warning if exceeding the threshold + +```json +{ + "directory": "/data/molli_series", + "gap_threshold": 3000.0 +} +``` + +--- + +### Philips Private Tags + +#### `dicom_query_philips_private` + +Query Philips private DICOM tags using DD number and element offset. Philips MRI scanners store proprietary metadata in private tag blocks whose assignments vary across scanners and software versions — this tool resolves them dynamically. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_path` | string | *required* | Path to a Philips DICOM file | +| `dd_number` | int | `null` | DD number to look up (e.g. `1` for "DD 001") | +| `element_offset` | int | `null` | Element offset within the DD block (e.g. `133` for 0x85) | +| `private_group` | int | `0x2005` | DICOM private group to search | +| `list_creators` | bool | `false` | List all Private Creator tags instead of resolving a specific one | +| `response_format` | string | `markdown` | `markdown` or `json` | + +**Two usage modes:** + +1. **List creators** — discover what private tag blocks are available: +```json +{ + "file_path": "/data/philips_scan.dcm", + "list_creators": true +} +``` + +2. **Resolve a specific tag** — look up a known DD number and offset: +```json +{ + "file_path": "/data/philips_scan.dcm", + "dd_number": 1, + "element_offset": 133 +} +``` + +**Common Philips DD numbers and offsets (group 2005):** + +| DD # | Offset | Description | +|------|--------|-------------| +| 001 | 0x85 | Shim calculation values | +| 001 | 0x63 | Stack ID | +| 004 | 0x00 | MR Series data object | + +--- + +### Pixel Analysis + +#### `dicom_read_pixels` + +Extract pixel statistics from a DICOM file. Values are rescaled using RescaleSlope and RescaleIntercept when present (standard for Philips, common on Siemens/GE). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_path` | string | *required* | Path to DICOM file | +| `roi` | list[int] | `null` | Region of interest as `[x, y, width, height]` (top-left corner in pixel coordinates). If omitted, statistics cover the entire image. | +| `include_histogram` | bool | `false` | Include a binned histogram of pixel values | +| `histogram_bins` | int | `50` | Number of histogram bins | +| `response_format` | string | `markdown` | `markdown` or `json` | + +**Returns**: min, max, mean, standard deviation, median, 5th/25th/75th/95th percentiles, and pixel count. + +```json +{ + "file_path": "/data/liver_scan/slice_005.dcm", + "roi": [100, 200, 50, 50], + "include_histogram": true, + "histogram_bins": 20 +} +``` + +#### `dicom_compute_snr` + +Compute signal-to-noise ratio from two ROIs in a DICOM image using the single-image method: SNR = mean(signal) / std(noise). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_path` | string | *required* | Path to DICOM file | +| `signal_roi` | list[int] | *required* | Signal region as `[x, y, width, height]` | +| `noise_roi` | list[int] | *required* | Noise/background region as `[x, y, width, height]` | +| `response_format` | string | `markdown` | `markdown` or `json` | + +**Tip**: Use `dicom_render_image` with `overlay_rois` first to visualise and verify ROI placement before measuring. + +**Note**: Some manufacturers (notably Philips) zero-fill background air outside the reconstruction FOV, which results in zero noise standard deviation and infinite SNR. In such cases, consider using a homogeneous tissue region (e.g. subcutaneous fat or muscle) as the noise ROI instead. + +```json +{ + "file_path": "/data/liver_scan/slice_005.dcm", + "signal_roi": [100, 200, 50, 50], + "noise_roi": [20, 20, 40, 40] +} +``` + +#### `dicom_render_image` + +Render a DICOM image to PNG with configurable windowing. Optionally overlays labelled ROI rectangles for visual verification. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_path` | string | *required* | Path to DICOM file | +| `output_path` | string | *required* | Path where the PNG will be saved | +| `window_center` | float | `null` | Manual window center | +| `window_width` | float | `null` | Manual window width | +| `auto_window` | bool | `false` | Auto-calculate window from 5th-95th percentile | +| `overlay_rois` | list[dict] | `null` | ROI overlays (see below) | +| `show_info` | bool | `true` | Burn in series description and windowing values | +| `response_format` | string | `markdown` | `markdown` or `json` | + +**Windowing priority:** +1. Explicit `window_center` + `window_width` parameters +2. `auto_window=True` → 5th-95th percentile range +3. DICOM header WindowCenter/WindowWidth values +4. Fallback → full pixel range (min to max) + +**ROI overlay format:** +```json +{ + "roi": [x, y, width, height], + "label": "Signal (Liver)", + "color": "green" +} +``` + +Available colours: `red`, `green`, `blue`, `yellow`, `cyan`, `magenta`, `white`. + +**Full example:** +```json +{ + "file_path": "/data/scan.dcm", + "output_path": "/data/renders/scan_auto.png", + "auto_window": true, + "overlay_rois": [ + {"roi": [100, 200, 50, 50], "label": "Signal", "color": "green"}, + {"roi": [20, 20, 40, 40], "label": "Noise", "color": "red"} + ] +} +``` + +**Viewing rendered images:** If Claude also has access to a filesystem MCP server with media reading capability (e.g. `read_media_file`), it can view the rendered PNG directly and use visual feedback to refine ROI placement iteratively — no need to switch to a separate DICOM viewer. + +--- + +## Output Formats + +All tools support two output formats via the `response_format` parameter: + +**Markdown** (default) — Human-readable with tables, formatting, and visual indicators. Best for conversational use in Claude Desktop. + +**JSON** — Machine-readable structured data with consistent schemas. Best for programmatic processing or piping into other tools. + +--- + +## Performance Considerations + +### File scanning limits + +All directory scanning tools are limited to processing **1000 files** per scan by default. This limit is configurable via the `DICOM_MCP_MAX_FILES` environment variable (see [INSTALL.md](INSTALL.md#environment-variables)). If this limit is reached, a warning indicates results were truncated (and `truncated: true` in JSON output). Narrow your search to a more specific subdirectory if needed. + +### Optimisation tips + +- Use `dicom_summarize_directory` instead of `dicom_list_files` when you only need an overview +- Use `dicom_list_files` with `count_only: true` for a compact series inventory +- Use `dicom_search` with `mode: "count"` to quickly check match counts before fetching details +- Use `dicom_query` instead of `dicom_get_metadata` on multiple files when you need aggregate tag values +- Set `recursive: false` to scan only the immediate directory +- For pixel tools, use `dicom_render_image` to verify ROI placement before running `dicom_compute_snr` +- Use `dicom_dump_tree` with `show_private: false` to focus on standard DICOM structure +- Use `dicom_compare_uids` to quickly detect differences between study directories without inspecting every file + +--- + +## QA Workflow Examples + +### Workflow 1: Dixon Image Selection Investigation + +When you suspect incorrect fat/water selection: + +1. `dicom_find_dixon_series` — locate the Dixon series +2. `dicom_list_files` with `filter_sequence_type: "dixon"` — see all Dixon files +3. `dicom_compare_headers` on suspected files — focus on ImageType tags +4. `dicom_get_metadata` — extract full headers for documentation + +### Workflow 2: Multi-Manufacturer Validation + +When testing across GE, Siemens, and Philips: + +1. `dicom_summarize_directory` — check patient info consistency and get a quick inventory +2. `dicom_query` with `group_by: "SeriesDescription"` — compare timing parameters across series +3. `dicom_validate_sequence` with manufacturer-specific expected parameters +4. `dicom_compare_headers` — identify parameter variations +5. Document differences for test specification updates + +### Workflow 3: Test Dataset Verification + +Before running automated tests: + +1. `dicom_analyze_series` on each test case directory +2. `dicom_find_dixon_series` to confirm Dixon sequences are present +3. `dicom_validate_sequence` to check protocol compliance +4. `dicom_search` to find files with unexpected parameter values (e.g. `"FlipAngle != 15"`) + +### Workflow 4: Image Quality Assessment + +For checking signal quality and image rendering: + +1. `dicom_render_image` with `auto_window: true` — render the image and visually inspect +2. `dicom_render_image` with `overlay_rois` — place and verify signal/noise ROI positions +3. `dicom_read_pixels` — check pixel value distributions and histograms +4. `dicom_compute_snr` — measure SNR with verified ROI placements +5. Repeat across series or manufacturers to compare image quality + +**Note on Philips data:** Background air is often zero-filled, making the traditional background-air SNR method return infinite. Use a homogeneous tissue region (subcutaneous fat, muscle) as the noise proxy instead. + +### Workflow 5: MOLLI / T1 Mapping Validation + +When verifying T1 mapping acquisitions across vendors: + +1. `dicom_analyze_ti` — extract all inversion times, check for proper MOLLI scheme (e.g. 5(3)3 = 11 TI-weighted images + output maps) +2. Check for gap warnings — large gaps between consecutive TIs may indicate missing heartbeats or acquisition failures +3. `dicom_dump_tree` on a representative file — inspect nested sequence structure if TI extraction fails +4. For Philips data: `dicom_query_philips_private` with `dd_number: 6, element_offset: 114` (0x72) — manually verify the private TI tag if needed + +### Workflow 6: Segmentation Verification + +When validating segmentation files reference the correct source images: + +1. `dicom_verify_segmentations` — check all segmentation-to-source references in one pass +2. Review any unmatched references — missing source files or incorrect ReferencedSOPInstanceUIDs +3. `dicom_compare_uids` with `compare_tag: "SOPInstanceUID"` — compare two directories to find missing or extra files + +### Workflow 7: Philips Private Tag Investigation + +When investigating Philips-specific metadata: + +1. `dicom_query_philips_private` with `list_creators: true` — discover available DD blocks +2. `dicom_query_philips_private` with specific `dd_number` and `element_offset` — resolve individual tags +3. Or use `dicom_get_metadata` with the `philips_private_tags` parameter to extract private tags alongside standard metadata in a single call + +### Example Screenshot From Claude Desktop + +Claude Desktop session example diff --git a/mcps/dicom_mcp/img/claude_desktop_example.png b/mcps/dicom_mcp/img/claude_desktop_example.png new file mode 100644 index 0000000000000000000000000000000000000000..86dcc246c8b0a4d83cc266a477d6b799ec7cf90e GIT binary patch literal 158974 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

ORP%~ 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

RG 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�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 /dev/null; then + echo -e "${RED}Error: Poetry is not installed.${NC}" + echo "Please install Poetry first: https://python-poetry.org/docs/#installation" + exit 1 +fi + +echo -e "${YELLOW}Installing dependencies...${NC}" +poetry install --with dev + +echo -e "\n${GREEN}✓ Dependencies installed successfully${NC}\n" + +# Configure Claude Desktop +CONFIG_FILE="$HOME/Library/Application Support/Claude/claude_desktop_config.json" + +echo -e "${YELLOW}Configuring Claude Desktop...${NC}" + +# Create config directory if it doesn't exist +mkdir -p "$HOME/Library/Application Support/Claude" + +# Check if config file exists +if [ -f "$CONFIG_FILE" ]; then + echo -e "${YELLOW}Existing configuration found.${NC}" + echo -e "Please add the following to your mcpServers section in:" + echo -e "${YELLOW}$CONFIG_FILE${NC}\n" + echo '{ + "dicom_mcp": { + "command": "poetry", + "args": ["run", "python", "-m", "dicom_mcp"], + "cwd": "'"$SCRIPT_DIR"'" + } +}' +else + echo -e "${YELLOW}Creating new configuration file...${NC}" + cat > "$CONFIG_FILE" << EOF +{ + "mcpServers": { + "dicom_mcp": { + "command": "poetry", + "args": ["run", "python", "-m", "dicom_mcp"], + "cwd": "$SCRIPT_DIR" + } + } +} +EOF + echo -e "${GREEN}✓ Configuration file created${NC}" +fi + +echo -e "\n${GREEN}=== Installation Complete ===${NC}\n" +echo -e "Next steps:" +echo -e "1. Restart Claude Desktop" +echo -e "2. The DICOM MCP server should be available with 7 tools\n" +echo -e "To test the server manually, run:" +echo -e "${YELLOW}poetry run python -m dicom_mcp${NC}\n" diff --git a/mcps/dicom_mcp/pyproject.toml b/mcps/dicom_mcp/pyproject.toml new file mode 100644 index 0000000..d693960 --- /dev/null +++ b/mcps/dicom_mcp/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "dicom-mcp" +version = "0.1.0" +description = "DICOM MCP Server for Medical Imaging QA" +authors = ["Gregory Gauthier "] +readme = "README.md" +packages = [{include = "dicom_mcp"}] + +[tool.poetry.dependencies] +python = "^3.12,<4.0" +fastmcp = "^2.0.0" +numpy = ">=1.24" +Pillow = ">=10.0" +pydicom = "^3.0.1" +black = "^26.1.0" +ruff = "^0.15.4" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.0" +pytest-asyncio = "^0.23.0" + +[build-system] +requires = ["poetry-core>=2.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::DeprecationWarning:pytest_asyncio.*", +] diff --git a/mcps/dicom_mcp/run_dicom_mcp_server.sh b/mcps/dicom_mcp/run_dicom_mcp_server.sh new file mode 100755 index 0000000..4af5cf3 --- /dev/null +++ b/mcps/dicom_mcp/run_dicom_mcp_server.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env zsh +# +if [[ "$(uname)" == "Linux" ]]; then + PATH_PREFIX="/data/Projects" +elif [[ "$(uname)" == "Darwin" ]]; then + PATH_PREFIX="/Users/gregory.gauthier/Projects" +else + # Always fallback to the linux default + PATH_PREFIX="/data/Projects" +fi + +# shellcheck disable=SC2164 +cd "${PATH_PREFIX}/mcp_servers/dicom_mcp" +poetry run python ./dicom_mcp + diff --git a/mcps/dicom_mcp/test_dicom_mcp.py b/mcps/dicom_mcp/test_dicom_mcp.py new file mode 100644 index 0000000..2b42a13 --- /dev/null +++ b/mcps/dicom_mcp/test_dicom_mcp.py @@ -0,0 +1,1883 @@ +#!/usr/bin/env python3 +""" +Pytest suite for DICOM MCP Server + +Run with: pytest test_dicom_mcp.py -v +""" + +import json +import sys +from pathlib import Path + +import numpy as np +import pydicom +import pytest +from PIL import Image +from pydicom.dataset import FileMetaDataset +from pydicom.sequence import Sequence as DicomSequence +from pydicom.uid import ( + ExplicitVRLittleEndian, + MRImageStorage, + SegmentationStorage, + UID, + generate_uid, +) + +# --------------------------------------------------------------------------- +# Shared DICOM factory helpers +# --------------------------------------------------------------------------- + + +def _make_base_dataset( + *, + sop_class=MRImageStorage, + sop_instance_uid=None, + study_instance_uid=None, + series_instance_uid=None, + rows=4, + columns=4, + bits_stored=12, + pixel_data=None, + **attrs, +): + """Create a minimal valid DICOM Dataset with correct typing. + + Handles boilerplate: FileMetaDataset, transfer syntax, pixel encoding, + and UID generation. Pass additional DICOM attributes as keyword + arguments (e.g. ``Modality="MR"``, ``Manufacturer="SIEMENS"``). + + Returns an in-memory Dataset (not saved to disk). + """ + ds = pydicom.Dataset() + ds.file_meta = FileMetaDataset() + ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian + + sop_uid = UID(sop_instance_uid) if sop_instance_uid else generate_uid() + ds.file_meta.MediaStorageSOPClassUID = sop_class + ds.file_meta.MediaStorageSOPInstanceUID = sop_uid + + ds.SOPClassUID = sop_class + ds.SOPInstanceUID = sop_uid + ds.StudyInstanceUID = ( + UID(study_instance_uid) if study_instance_uid else generate_uid() + ) + ds.SeriesInstanceUID = ( + UID(series_instance_uid) if series_instance_uid else generate_uid() + ) + + high_bit = bits_stored - 1 + ds.Rows = rows + ds.Columns = columns + ds.BitsAllocated = 16 + ds.BitsStored = bits_stored + ds.HighBit = high_bit + ds.PixelRepresentation = 0 + ds.SamplesPerPixel = 1 + ds.PhotometricInterpretation = "MONOCHROME2" + + if pixel_data is not None: + ds.PixelData = pixel_data.astype(np.uint16).tobytes() + else: + ds.PixelData = np.zeros((rows, columns), dtype=np.uint16).tobytes() + + for attr, value in attrs.items(): + setattr(ds, attr, value) + + return ds + + +def _save_dicom(ds, file_path): + """Save a Dataset to disk with DICOM file format enforcement.""" + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + ds.save_as(str(file_path), enforce_file_format=True) + return file_path + + +def _make_pii_dataset(): + """Create a Dataset with known patient PII fields (shared by PII test classes).""" + return _make_base_dataset( + Modality="MR", + Manufacturer="SIEMENS", + SeriesDescription="T1_VIBE_Dixon", + SeriesNumber=1, + PatientName="Doe^Jane", + PatientID="PII-TEST-001", + PatientBirthDate="19850315", + PatientSex="F", + ) + + +# --------------------------------------------------------------------------- +# Module-scoped fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def dicom_mcp_module(): + """Import and return the dicom_mcp module.""" + # Add the current directory to the path if needed + current_dir = Path(__file__).parent + if str(current_dir) not in sys.path: + sys.path.insert(0, str(current_dir)) + + import dicom_mcp + + return dicom_mcp + + +# --------------------------------------------------------------------------- +# Test classes +# --------------------------------------------------------------------------- + + +class TestImports: + """Test that all required modules can be imported.""" + + def test_mcp_import(self): + """Test that mcp can be imported.""" + import mcp # local: validates dependency is installed + + assert mcp is not None + + def test_pydicom_import(self): + """Test that pydicom can be imported.""" + assert pydicom is not None + + def test_pydantic_import(self): + """Test that pydantic can be imported.""" + import pydantic # local: validates dependency is installed + + assert pydantic is not None + + def test_fastmcp_import(self): + """Test that FastMCP can be imported.""" + from mcp.server.fastmcp import FastMCP # local: validates submodule path + + assert FastMCP is not None + + +class TestServerModule: + """Test the DICOM MCP server module.""" + + def test_module_import(self, dicom_mcp_module): + """Test that dicom_mcp module can be imported.""" + assert dicom_mcp_module is not None + + def test_mcp_instance_exists(self, dicom_mcp_module): + """Test that mcp instance exists in module.""" + assert hasattr(dicom_mcp_module, "mcp") + + def test_mcp_instance_name(self, dicom_mcp_module): + """Test that MCP instance has correct name.""" + assert dicom_mcp_module.mcp.name == "dicom_mcp" + + +class TestToolRegistration: + """Test that all tools are properly registered.""" + + EXPECTED_TOOLS = [ + "dicom_list_files", + "dicom_get_metadata", + "dicom_compare_headers", + "dicom_find_dixon_series", + "dicom_validate_sequence", + "dicom_analyze_series", + "dicom_summarize_directory", + "dicom_query", + "dicom_search", + "dicom_query_philips_private", + "dicom_read_pixels", + "dicom_compute_snr", + "dicom_render_image", + "dicom_dump_tree", + "dicom_compare_uids", + "dicom_verify_segmentations", + "dicom_analyze_ti", + ] + + def test_all_tools_exist(self, dicom_mcp_module): + """Test that all expected tool functions exist.""" + for tool_name in self.EXPECTED_TOOLS: + assert hasattr( + dicom_mcp_module, tool_name + ), f"Tool {tool_name} not found in module" + + def test_tool_count(self, dicom_mcp_module): + """Test that correct number of tools are registered.""" + registered_count = sum( + 1 for tool in self.EXPECTED_TOOLS if hasattr(dicom_mcp_module, tool) + ) + assert registered_count == len( + self.EXPECTED_TOOLS + ), f"Expected {len(self.EXPECTED_TOOLS)} tools, found {registered_count}" + + @pytest.mark.parametrize("tool_name", EXPECTED_TOOLS) + def test_tool_is_callable(self, dicom_mcp_module, tool_name): + """Test that each tool is callable.""" + tool_func = getattr(dicom_mcp_module, tool_name) + assert callable(tool_func), f"{tool_name} is not callable" + + +class TestHelperFunctions: + """Test helper functions.""" + + def test_safe_get_tag_exists(self, dicom_mcp_module): + """Test that _safe_get_tag helper exists.""" + assert hasattr(dicom_mcp_module, "_safe_get_tag") + + def test_identify_sequence_type_exists(self, dicom_mcp_module): + """Test that _identify_sequence_type helper exists.""" + assert hasattr(dicom_mcp_module, "_identify_sequence_type") + + def test_find_dicom_files_exists(self, dicom_mcp_module): + """Test that _find_dicom_files helper exists.""" + assert hasattr(dicom_mcp_module, "_find_dicom_files") + + def test_format_markdown_table_exists(self, dicom_mcp_module): + """Test that _format_markdown_table helper exists.""" + assert hasattr(dicom_mcp_module, "_format_markdown_table") + + def test_resolve_tag_exists(self, dicom_mcp_module): + """Test that _resolve_tag helper exists.""" + assert hasattr(dicom_mcp_module, "_resolve_tag") + + def test_parse_filter_exists(self, dicom_mcp_module): + """Test that _parse_filter helper exists.""" + assert hasattr(dicom_mcp_module, "_parse_filter") + + def test_apply_filter_exists(self, dicom_mcp_module): + """Test that _apply_filter helper exists.""" + assert hasattr(dicom_mcp_module, "_apply_filter") + + def test_get_pixel_array_exists(self, dicom_mcp_module): + """Test that _get_pixel_array helper exists.""" + assert hasattr(dicom_mcp_module, "_get_pixel_array") + + def test_extract_roi_exists(self, dicom_mcp_module): + """Test that _extract_roi helper exists.""" + assert hasattr(dicom_mcp_module, "_extract_roi") + + def test_compute_stats_exists(self, dicom_mcp_module): + """Test that _compute_stats helper exists.""" + assert hasattr(dicom_mcp_module, "_compute_stats") + + def test_apply_windowing_exists(self, dicom_mcp_module): + """Test that _apply_windowing helper exists.""" + assert hasattr(dicom_mcp_module, "_apply_windowing") + + def test_resolve_philips_private_tag_exists(self, dicom_mcp_module): + """Test that _resolve_philips_private_tag helper exists.""" + assert hasattr(dicom_mcp_module, "_resolve_philips_private_tag") + + def test_list_philips_private_creators_exists(self, dicom_mcp_module): + """Test that _list_philips_private_creators helper exists.""" + assert hasattr(dicom_mcp_module, "_list_philips_private_creators") + + +class TestEnums: + """Test enum definitions.""" + + def test_response_format_enum_exists(self, dicom_mcp_module): + """Test that ResponseFormat enum exists.""" + assert hasattr(dicom_mcp_module, "ResponseFormat") + + def test_response_format_values(self, dicom_mcp_module): + """Test ResponseFormat has expected values.""" + ResponseFormat = dicom_mcp_module.ResponseFormat + assert hasattr(ResponseFormat, "MARKDOWN") + assert hasattr(ResponseFormat, "JSON") + + def test_sequence_type_enum_exists(self, dicom_mcp_module): + """Test that SequenceType enum exists.""" + assert hasattr(dicom_mcp_module, "SequenceType") + + def test_sequence_type_values(self, dicom_mcp_module): + """Test SequenceType has expected values including liver analysis types.""" + SequenceType = dicom_mcp_module.SequenceType + assert hasattr(SequenceType, "DIXON") + assert hasattr(SequenceType, "T1_MAPPING") + assert hasattr(SequenceType, "MULTI_ECHO_GRE") + assert hasattr(SequenceType, "SPIN_ECHO_IR") + assert hasattr(SequenceType, "T1") + assert hasattr(SequenceType, "T2") + assert hasattr(SequenceType, "FLAIR") + assert hasattr(SequenceType, "DWI") + assert hasattr(SequenceType, "LOCALIZER") + assert hasattr(SequenceType, "UNKNOWN") + + +class TestConstants: + """Test constant definitions.""" + + def test_common_tags_exists(self, dicom_mcp_module): + """Test that COMMON_TAGS constant exists.""" + assert hasattr(dicom_mcp_module, "COMMON_TAGS") + + def test_common_tags_structure(self, dicom_mcp_module): + """Test that COMMON_TAGS has expected structure.""" + COMMON_TAGS = dicom_mcp_module.COMMON_TAGS + assert isinstance(COMMON_TAGS, dict) + + expected_groups = [ + "patient_info", + "study_info", + "series_info", + "image_info", + "acquisition", + "manufacturer", + ] + + for group in expected_groups: + assert group in COMMON_TAGS, f"Missing tag group: {group}" + assert isinstance(COMMON_TAGS[group], list) + + +class TestNewImports: + """Test that new pixel tool dependencies can be imported.""" + + def test_numpy_import(self): + """Test that numpy can be imported.""" + assert np is not None + + def test_pillow_import(self): + """Test that Pillow can be imported.""" + from PIL import ImageDraw, ImageFont + + assert Image is not None + assert ImageDraw is not None + assert ImageFont is not None + + +class TestExtractRoi: + """Test _extract_roi with synthetic pixel arrays.""" + + def test_valid_roi_centre(self, dicom_mcp_module): + """Test extracting a valid central ROI.""" + pixels = np.arange(100).reshape(10, 10).astype(np.float64) + roi = dicom_mcp_module._extract_roi(pixels, [2, 3, 4, 5]) + assert roi.shape == (5, 4) + # Top-left corner of ROI: row=3, col=2 → value = 3*10 + 2 = 32 + assert roi[0, 0] == 32.0 + + def test_valid_roi_full_image(self, dicom_mcp_module): + """Test ROI covering the entire image.""" + pixels = np.ones((8, 12), dtype=np.float64) + roi = dicom_mcp_module._extract_roi(pixels, [0, 0, 12, 8]) + assert roi.shape == (8, 12) + + def test_roi_out_of_bounds(self, dicom_mcp_module): + """Test that out-of-bounds ROI raises ValueError.""" + pixels = np.zeros((10, 10), dtype=np.float64) + with pytest.raises(ValueError, match="exceeds image bounds"): + dicom_mcp_module._extract_roi(pixels, [8, 8, 5, 5]) + + def test_roi_zero_width(self, dicom_mcp_module): + """Test that zero-width ROI raises ValueError.""" + pixels = np.zeros((10, 10), dtype=np.float64) + with pytest.raises(ValueError, match="must be positive"): + dicom_mcp_module._extract_roi(pixels, [0, 0, 0, 5]) + + def test_roi_negative_height(self, dicom_mcp_module): + """Test that negative-dimension ROI raises ValueError.""" + pixels = np.zeros((10, 10), dtype=np.float64) + with pytest.raises(ValueError, match="must be positive"): + dicom_mcp_module._extract_roi(pixels, [0, 0, 5, -3]) + + def test_roi_wrong_length(self, dicom_mcp_module): + """Test that ROI with wrong number of elements raises ValueError.""" + pixels = np.zeros((10, 10), dtype=np.float64) + with pytest.raises(ValueError, match="must be \\[x, y, width, height\\]"): + dicom_mcp_module._extract_roi(pixels, [0, 0, 5]) + + +class TestComputeStats: + """Test _compute_stats with known distributions.""" + + def test_uniform_array(self, dicom_mcp_module): + """Test stats on a constant array.""" + pixels = np.full((10, 10), 42.0) + stats = dicom_mcp_module._compute_stats(pixels) + assert stats["min"] == 42.0 + assert stats["max"] == 42.0 + assert stats["mean"] == 42.0 + assert stats["std"] == 0.0 + assert stats["median"] == 42.0 + assert stats["pixel_count"] == 100 + + def test_known_sequence(self, dicom_mcp_module): + """Test stats on a predictable sequence.""" + # 0 through 99 inclusive + pixels = np.arange(100).reshape(10, 10).astype(np.float64) + stats = dicom_mcp_module._compute_stats(pixels) + assert stats["min"] == 0.0 + assert stats["max"] == 99.0 + assert stats["mean"] == pytest.approx(49.5) + assert stats["median"] == pytest.approx(49.5) + assert stats["pixel_count"] == 100 + # std of uniform discrete dist + assert stats["std"] == pytest.approx(28.866, abs=0.01) + + def test_all_expected_keys(self, dicom_mcp_module): + """Test that all expected statistic keys are returned.""" + pixels = np.ones((5, 5), dtype=np.float64) + stats = dicom_mcp_module._compute_stats(pixels) + expected_keys = { + "min", + "max", + "mean", + "std", + "median", + "p5", + "p25", + "p75", + "p95", + "pixel_count", + } + assert set(stats.keys()) == expected_keys + + def test_percentile_ordering(self, dicom_mcp_module): + """Test that percentiles are in expected order.""" + pixels = np.random.RandomState(42).rand(50, 50).astype(np.float64) + stats = dicom_mcp_module._compute_stats(pixels) + assert stats["min"] <= stats["p5"] + assert stats["p5"] <= stats["p25"] + assert stats["p25"] <= stats["median"] + assert stats["median"] <= stats["p75"] + assert stats["p75"] <= stats["p95"] + assert stats["p95"] <= stats["max"] + + +class TestApplyWindowing: + """Test _apply_windowing produces correct 8-bit output.""" + + def test_full_range(self, dicom_mcp_module): + """Test windowing that maps full range to 0-255.""" + pixels = np.array([[0.0, 50.0], [100.0, 200.0]]) + result = dicom_mcp_module._apply_windowing(pixels, 100.0, 200.0) + assert result.dtype == np.uint8 + assert result[0, 0] == 0 # at lower bound + assert result[1, 1] == 255 # at upper bound + assert result[0, 1] == 63 # 50/200 * 255 = 63.75, truncated to 63 + + def test_clipping_below(self, dicom_mcp_module): + """Test that values below window are clipped to 0.""" + pixels = np.array([[-100.0, -50.0]]) + result = dicom_mcp_module._apply_windowing(pixels, 100.0, 100.0) + assert result[0, 0] == 0 + assert result[0, 1] == 0 + + def test_clipping_above(self, dicom_mcp_module): + """Test that values above window are clipped to 255.""" + pixels = np.array([[500.0, 1000.0]]) + result = dicom_mcp_module._apply_windowing(pixels, 100.0, 100.0) + assert result[0, 0] == 255 + assert result[0, 1] == 255 + + def test_narrow_window(self, dicom_mcp_module): + """Test a very narrow window (high contrast).""" + pixels = np.array([[99.0, 100.0, 101.0]]) + result = dicom_mcp_module._apply_windowing(pixels, 100.0, 2.0) + assert result[0, 0] == 0 + assert result[0, 1] == 127 # centre maps to 127.5, truncated to 127 + assert result[0, 2] == 255 + + def test_output_shape_preserved(self, dicom_mcp_module): + """Test that output has same shape as input.""" + pixels = np.random.rand(64, 128).astype(np.float64) * 1000 + result = dicom_mcp_module._apply_windowing(pixels, 500.0, 800.0) + assert result.shape == (64, 128) + + +class TestGetPixelArray: + """Test _get_pixel_array with synthetic DICOM datasets.""" + + def _make_dataset(self, pixel_values, slope=1.0, intercept=0.0): + """Create a minimal pydicom Dataset with pixel data.""" + ds = _make_base_dataset( + rows=pixel_values.shape[0], + columns=pixel_values.shape[1], + bits_stored=16, + pixel_data=pixel_values, + RescaleSlope=slope, + RescaleIntercept=intercept, + ) + return ds + + def test_identity_rescale(self, dicom_mcp_module): + """Test pixel extraction with slope=1, intercept=0.""" + raw = np.array([[100, 200], [300, 400]], dtype=np.uint16) + ds = self._make_dataset(raw) + result = dicom_mcp_module._get_pixel_array(ds) + assert result.dtype == np.float64 + np.testing.assert_array_equal(result, raw.astype(np.float64)) + + def test_slope_applied(self, dicom_mcp_module): + """Test that RescaleSlope is applied correctly.""" + raw = np.array([[10, 20]], dtype=np.uint16) + ds = self._make_dataset(raw, slope=2.5, intercept=0.0) + result = dicom_mcp_module._get_pixel_array(ds) + np.testing.assert_array_almost_equal(result, [[25.0, 50.0]]) + + def test_intercept_applied(self, dicom_mcp_module): + """Test that RescaleIntercept is applied correctly.""" + raw = np.array([[100, 200]], dtype=np.uint16) + ds = self._make_dataset(raw, slope=1.0, intercept=-100.0) + result = dicom_mcp_module._get_pixel_array(ds) + np.testing.assert_array_almost_equal(result, [[0.0, 100.0]]) + + def test_slope_and_intercept_combined(self, dicom_mcp_module): + """Test slope and intercept applied together: value * slope + intercept.""" + raw = np.array([[10, 20]], dtype=np.uint16) + ds = self._make_dataset(raw, slope=2.0, intercept=5.0) + result = dicom_mcp_module._get_pixel_array(ds) + # 10*2+5=25, 20*2+5=45 + np.testing.assert_array_almost_equal(result, [[25.0, 45.0]]) + + +class TestPixelToolsWithSyntheticDicom: + """Functional tests for pixel tools using synthetic DICOM files on disk. + + Creates temporary DICOM files with known pixel data to verify the + full tool pipeline including file I/O. + """ + + @pytest.fixture + def synthetic_dicom(self, tmp_path): + """Create a synthetic DICOM file with a gradient pattern. + + The image is 64x64 with values 0-4095 (12-bit range), arranged + so that pixel intensity increases linearly left-to-right, with + a known signal region (centre) and noise region (top-left corner). + """ + # Gradient pattern: each row is the same, columns go 0..4095 + row = np.linspace(0, 4095, 64).astype(np.uint16) + pixels = np.tile(row, (64, 1)) + + # Add a bright "signal" block in centre (rows 24-40, cols 24-40) + pixels[24:40, 24:40] = 3000 + + # Add a low "noise" block in top-left (rows 0-16, cols 0-16) + # with small random variation around a low mean + rng = np.random.RandomState(42) + pixels[0:16, 0:16] = (50 + rng.randint(0, 20, size=(16, 16))).astype(np.uint16) + + ds = _make_base_dataset( + rows=64, + columns=64, + pixel_data=pixels, + FrameOfReferenceUID=generate_uid(), + Modality="MR", + Manufacturer="TestVendor", + SeriesDescription="LMS MOST Test", + SeriesNumber=1, + InstanceNumber=1, + ImageType=["ORIGINAL", "PRIMARY", "M_FFE"], + RescaleSlope=1.0, + RescaleIntercept=0.0, + WindowCenter=500, + WindowWidth=1000, + ) + + file_path = tmp_path / "test_image.dcm" + _save_dicom(ds, file_path) + return file_path, pixels + + @pytest.mark.asyncio + async def test_read_pixels_full_image(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_read_pixels returns correct stats for full image.""" + file_path, pixels = synthetic_dicom + result = await dicom_mcp_module.dicom_read_pixels( + file_path=str(file_path), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["image_info"]["rows"] == 64 + assert data["image_info"]["columns"] == 64 + assert data["region"] == "Full image" + assert data["statistics"]["pixel_count"] == 64 * 64 + assert data["statistics"]["min"] >= 0 + assert data["statistics"]["max"] <= 4095 + + @pytest.mark.asyncio + async def test_read_pixels_with_roi(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_read_pixels with ROI restricts to correct region.""" + file_path, pixels = synthetic_dicom + # Signal block: cols 24-40, rows 24-40 → all 3000 + result = await dicom_mcp_module.dicom_read_pixels( + file_path=str(file_path), + roi=[24, 24, 16, 16], + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["statistics"]["pixel_count"] == 16 * 16 + assert data["statistics"]["mean"] == 3000.0 + assert data["statistics"]["std"] == 0.0 + + @pytest.mark.asyncio + async def test_read_pixels_with_histogram(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_read_pixels includes histogram when requested.""" + file_path, _ = synthetic_dicom + result = await dicom_mcp_module.dicom_read_pixels( + file_path=str(file_path), + include_histogram=True, + histogram_bins=10, + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert "histogram" in data + assert data["histogram"]["bins"] == 10 + assert len(data["histogram"]["counts"]) == 10 + assert len(data["histogram"]["bin_edges"]) == 11 + assert sum(data["histogram"]["counts"]) == 64 * 64 + + @pytest.mark.asyncio + async def test_read_pixels_invalid_roi(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_read_pixels returns error for out-of-bounds ROI.""" + file_path, _ = synthetic_dicom + result = await dicom_mcp_module.dicom_read_pixels( + file_path=str(file_path), + roi=[60, 60, 10, 10], + ) + assert "Error" in result + assert "exceeds image bounds" in result + + @pytest.mark.asyncio + async def test_read_pixels_file_not_found(self, dicom_mcp_module, tmp_path): + """Test dicom_read_pixels returns error for missing file.""" + result = await dicom_mcp_module.dicom_read_pixels( + file_path=str(tmp_path / "nonexistent.dcm"), + ) + assert "Error" in result + assert "not found" in result + + @pytest.mark.asyncio + async def test_read_pixels_markdown_format(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_read_pixels markdown output contains expected sections.""" + file_path, _ = synthetic_dicom + result = await dicom_mcp_module.dicom_read_pixels( + file_path=str(file_path), + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "# Pixel Statistics" in result + assert "## Statistics" in result + assert "Mean" in result + assert "Std Dev" in result + + @pytest.mark.asyncio + async def test_compute_snr_known_values(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_compute_snr with signal block and noise block.""" + file_path, _ = synthetic_dicom + result = await dicom_mcp_module.dicom_compute_snr( + file_path=str(file_path), + signal_roi=[24, 24, 16, 16], # bright block, mean=3000, std=0 + noise_roi=[0, 0, 16, 16], # noisy low block + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["signal_roi"]["statistics"]["mean"] == 3000.0 + assert data["noise_roi"]["statistics"]["mean"] < 100.0 + assert data["noise_roi"]["statistics"]["std"] > 0 + # SNR should be high (3000 / small_std) + assert isinstance(data["snr"], (int, float)) + assert data["snr"] > 100 + + @pytest.mark.asyncio + async def test_compute_snr_zero_noise(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_compute_snr handles zero noise std gracefully.""" + file_path, _ = synthetic_dicom + # Use the signal block as both regions (std=0 in constant block) + result = await dicom_mcp_module.dicom_compute_snr( + file_path=str(file_path), + signal_roi=[24, 24, 8, 8], + noise_roi=[28, 28, 8, 8], # also in the constant block + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["snr"] == "infinite" + assert "snr_note" in data + + @pytest.mark.asyncio + async def test_compute_snr_invalid_roi(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_compute_snr returns error for bad ROI.""" + file_path, _ = synthetic_dicom + result = await dicom_mcp_module.dicom_compute_snr( + file_path=str(file_path), + signal_roi=[0, 0, 10, 10], + noise_roi=[60, 60, 10, 10], # out of bounds + ) + assert "Error in noise ROI" in result + + @pytest.mark.asyncio + async def test_compute_snr_markdown_format(self, dicom_mcp_module, synthetic_dicom): + """Test dicom_compute_snr markdown output.""" + file_path, _ = synthetic_dicom + result = await dicom_mcp_module.dicom_compute_snr( + file_path=str(file_path), + signal_roi=[24, 24, 16, 16], + noise_roi=[0, 0, 16, 16], + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "# SNR Analysis" in result + assert "## Signal ROI" in result + assert "## Noise ROI" in result + assert "## Result" in result + assert "SNR =" in result + + @pytest.mark.asyncio + async def test_render_image_default_windowing( + self, dicom_mcp_module, synthetic_dicom, tmp_path + ): + """Test dicom_render_image creates a PNG with DICOM header windowing.""" + file_path, _ = synthetic_dicom + output = tmp_path / "rendered.png" + result = await dicom_mcp_module.dicom_render_image( + file_path=str(file_path), + output_path=str(output), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert output.exists() + assert data["windowing"]["source"] == "DICOM header" + assert data["windowing"]["center"] == 500.0 + assert data["windowing"]["width"] == 1000.0 + # Verify it's a valid image + img = Image.open(output) + assert img.size == (64, 64) + + @pytest.mark.asyncio + async def test_render_image_auto_windowing( + self, dicom_mcp_module, synthetic_dicom, tmp_path + ): + """Test dicom_render_image auto windowing uses percentiles.""" + file_path, _ = synthetic_dicom + output = tmp_path / "auto.png" + result = await dicom_mcp_module.dicom_render_image( + file_path=str(file_path), + output_path=str(output), + auto_window=True, + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert output.exists() + assert data["windowing"]["source"] == "auto (p5-p95)" + + @pytest.mark.asyncio + async def test_render_image_manual_windowing( + self, dicom_mcp_module, synthetic_dicom, tmp_path + ): + """Test dicom_render_image manual windowing overrides header.""" + file_path, _ = synthetic_dicom + output = tmp_path / "manual.png" + result = await dicom_mcp_module.dicom_render_image( + file_path=str(file_path), + output_path=str(output), + window_center=2000.0, + window_width=4000.0, + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["windowing"]["source"] == "manual" + assert data["windowing"]["center"] == 2000.0 + assert data["windowing"]["width"] == 4000.0 + + @pytest.mark.asyncio + async def test_render_image_with_roi_overlays( + self, dicom_mcp_module, synthetic_dicom, tmp_path + ): + """Test dicom_render_image draws ROI overlays.""" + file_path, _ = synthetic_dicom + output = tmp_path / "overlays.png" + result = await dicom_mcp_module.dicom_render_image( + file_path=str(file_path), + output_path=str(output), + auto_window=True, + overlay_rois=[ + {"roi": [24, 24, 16, 16], "label": "Signal", "color": "green"}, + {"roi": [0, 0, 16, 16], "label": "Noise", "color": "red"}, + ], + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["overlays"] == 2 + img = Image.open(output) + # With overlays, image should be RGB + assert img.mode == "RGB" + + @pytest.mark.asyncio + async def test_render_image_no_info_bar( + self, dicom_mcp_module, synthetic_dicom, tmp_path + ): + """Test dicom_render_image without info bar stays greyscale.""" + file_path, _ = synthetic_dicom + output = tmp_path / "no_info.png" + await dicom_mcp_module.dicom_render_image( + file_path=str(file_path), + output_path=str(output), + show_info=False, + auto_window=True, + ) + img = Image.open(output) + assert img.mode == "L" # greyscale, no overlays or info text + + @pytest.mark.asyncio + async def test_render_image_file_not_found(self, dicom_mcp_module, tmp_path): + """Test dicom_render_image returns error for missing file.""" + result = await dicom_mcp_module.dicom_render_image( + file_path=str(tmp_path / "nonexistent.dcm"), + output_path=str(tmp_path / "out.png"), + ) + assert "Error" in result + assert "not found" in result + + @pytest.mark.asyncio + async def test_render_image_creates_parent_dirs( + self, dicom_mcp_module, synthetic_dicom, tmp_path + ): + """Test dicom_render_image creates output directories if needed.""" + file_path, _ = synthetic_dicom + output = tmp_path / "sub" / "dir" / "image.png" + await dicom_mcp_module.dicom_render_image( + file_path=str(file_path), + output_path=str(output), + auto_window=True, + show_info=False, + ) + assert output.exists() + + +class TestPhilipsPrivateTagHelpers: + """Test Philips private tag resolution helpers with synthetic datasets.""" + + def _make_philips_dataset(self): + """Create a synthetic Philips DICOM dataset with private creator tags.""" + ds = _make_base_dataset( + bits_stored=16, + Manufacturer="Philips Medical Systems", + ManufacturerModelName="Ingenia", + SeriesDescription="Test Series", + ) + + # Simulate Philips Private Creator tags in group 0x2005 + # Slot 0x10 -> DD 001 + ds.add_new((0x2005, 0x0010), "LO", "Philips MR Imaging DD 001") + # Slot 0x11 -> DD 002 + ds.add_new((0x2005, 0x0011), "LO", "Philips MR Imaging DD 002") + # Slot 0x12 -> DD 004 + ds.add_new((0x2005, 0x0012), "LO", "Philips MR Imaging DD 004") + + # Add some private data elements in each block + # DD 001, block 0x10: element at offset 0x85 -> tag (2005, 1085) + ds.add_new((0x2005, 0x1085), "LO", "ShimValue_42") + # DD 001, block 0x10: element at offset 0x63 -> tag (2005, 1063) + ds.add_new((0x2005, 0x1063), "LO", "StackID_1") + # DD 002, block 0x11: element at offset 0x30 -> tag (2005, 1130) + ds.add_new((0x2005, 0x1130), "LO", "DD002_Value") + + return ds + + def test_resolve_known_tag(self, dicom_mcp_module): + """Test resolving DD 001 offset 0x85 -> (2005,1085).""" + ds = self._make_philips_dataset() + resolved_tag, creator_str, value_str = ( + dicom_mcp_module._resolve_philips_private_tag( + ds, dd_number=1, element_offset=0x85 + ) + ) + assert resolved_tag == (0x2005, 0x1085) + assert "DD 001" in creator_str + assert value_str == "ShimValue_42" + + def test_resolve_different_dd(self, dicom_mcp_module): + """Test resolving DD 002 offset 0x30 -> (2005,1130).""" + ds = self._make_philips_dataset() + resolved_tag, creator_str, value_str = ( + dicom_mcp_module._resolve_philips_private_tag( + ds, dd_number=2, element_offset=0x30 + ) + ) + assert resolved_tag == (0x2005, 0x1130) + assert "DD 002" in creator_str + assert value_str == "DD002_Value" + + def test_resolve_missing_dd(self, dicom_mcp_module): + """Test resolving a DD number that doesn't exist returns None.""" + ds = self._make_philips_dataset() + resolved_tag, creator_str, value_str = ( + dicom_mcp_module._resolve_philips_private_tag( + ds, dd_number=99, element_offset=0x85 + ) + ) + assert resolved_tag is None + assert creator_str is None + assert value_str is None + + def test_resolve_tag_no_value(self, dicom_mcp_module): + """Test resolving a valid DD but offset with no data returns None value.""" + ds = self._make_philips_dataset() + # DD 004 exists at slot 0x12, but no element at offset 0xFF + resolved_tag, creator_str, value_str = ( + dicom_mcp_module._resolve_philips_private_tag( + ds, dd_number=4, element_offset=0xFF + ) + ) + assert resolved_tag == (0x2005, 0x12FF) + assert "DD 004" in creator_str + assert value_str is None + + def test_resolve_second_offset_same_dd(self, dicom_mcp_module): + """Test resolving DD 001 offset 0x63 -> (2005,1063).""" + ds = self._make_philips_dataset() + resolved_tag, creator_str, value_str = ( + dicom_mcp_module._resolve_philips_private_tag( + ds, dd_number=1, element_offset=0x63 + ) + ) + assert resolved_tag == (0x2005, 0x1063) + assert value_str == "StackID_1" + + def test_list_creators(self, dicom_mcp_module): + """Test listing all Private Creator tags.""" + ds = self._make_philips_dataset() + creators = dicom_mcp_module._list_philips_private_creators(ds) + assert len(creators) == 3 + dd_numbers = [c["dd_number"] for c in creators] + assert 1 in dd_numbers + assert 2 in dd_numbers + assert 4 in dd_numbers + + def test_list_creators_empty(self, dicom_mcp_module): + """Test listing creators on a dataset with no private tags.""" + ds = pydicom.Dataset() + creators = dicom_mcp_module._list_philips_private_creators(ds) + assert creators == [] + + def test_list_creators_slots(self, dicom_mcp_module): + """Test that creator slot numbers are correct.""" + ds = self._make_philips_dataset() + creators = dicom_mcp_module._list_philips_private_creators(ds) + slots = {c["dd_number"]: c["slot"] for c in creators} + assert slots[1] == 0x10 + assert slots[2] == 0x11 + assert slots[4] == 0x12 + + def test_resolve_custom_group(self, dicom_mcp_module): + """Test resolving private tags in a non-default group.""" + ds = pydicom.Dataset() + # Use group 0x2001 instead of 0x2005 + ds.add_new((0x2001, 0x0010), "LO", "Philips MR Imaging DD 005") + ds.add_new((0x2001, 0x1042), "LO", "CustomGroupValue") + + resolved_tag, creator_str, value_str = ( + dicom_mcp_module._resolve_philips_private_tag( + ds, dd_number=5, element_offset=0x42, private_group=0x2001 + ) + ) + assert resolved_tag == (0x2001, 0x1042) + assert value_str == "CustomGroupValue" + + +class TestPhilipsPrivateToolWithSyntheticDicom: + """Functional tests for dicom_query_philips_private using synthetic DICOM files.""" + + @pytest.fixture + def philips_dicom(self, tmp_path): + """Create a synthetic Philips DICOM file with private creator tags.""" + ds = _make_base_dataset( + bits_stored=16, + Modality="MR", + Manufacturer="Philips Medical Systems", + ManufacturerModelName="Ingenia", + SeriesDescription="Philips Test Series", + SeriesNumber=1, + InstanceNumber=1, + FrameOfReferenceUID=generate_uid(), + ) + + # Private Creator tags + ds.add_new((0x2005, 0x0010), "LO", "Philips MR Imaging DD 001") + ds.add_new((0x2005, 0x0011), "LO", "Philips MR Imaging DD 002") + # Private data elements + ds.add_new((0x2005, 0x1085), "LO", "ShimCalcValue") + ds.add_new((0x2005, 0x1130), "LO", "DD002_TestVal") + + file_path = tmp_path / "philips_test.dcm" + _save_dicom(ds, file_path) + return file_path + + @pytest.mark.asyncio + async def test_list_creators_tool(self, dicom_mcp_module, philips_dicom): + """Test dicom_query_philips_private with list_creators=True.""" + result = await dicom_mcp_module.dicom_query_philips_private( + file_path=str(philips_dicom), + list_creators=True, + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert len(data["creators"]) == 2 + dd_nums = [c["dd_number"] for c in data["creators"]] + assert 1 in dd_nums + assert 2 in dd_nums + + @pytest.mark.asyncio + async def test_resolve_tag_tool(self, dicom_mcp_module, philips_dicom): + """Test dicom_query_philips_private resolving DD 001 offset 0x85.""" + result = await dicom_mcp_module.dicom_query_philips_private( + file_path=str(philips_dicom), + dd_number=1, + element_offset=0x85, + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["resolution"]["resolved_tag"] == "(2005,1085)" + assert data["resolution"]["value"] == "ShimCalcValue" + assert "DD 001" in data["resolution"]["creator_string"] + + @pytest.mark.asyncio + async def test_resolve_missing_dd_tool(self, dicom_mcp_module, philips_dicom): + """Test dicom_query_philips_private with non-existent DD number.""" + result = await dicom_mcp_module.dicom_query_philips_private( + file_path=str(philips_dicom), + dd_number=99, + element_offset=0x85, + ) + assert "Error" in result + assert "DD 099" in result + + @pytest.mark.asyncio + async def test_resolve_tag_markdown(self, dicom_mcp_module, philips_dicom): + """Test dicom_query_philips_private markdown output.""" + result = await dicom_mcp_module.dicom_query_philips_private( + file_path=str(philips_dicom), + dd_number=1, + element_offset=0x85, + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "Philips Private Tag Lookup" in result + assert "(2005,1085)" in result + assert "ShimCalcValue" in result + + @pytest.mark.asyncio + async def test_list_creators_markdown(self, dicom_mcp_module, philips_dicom): + """Test dicom_query_philips_private list_creators markdown output.""" + result = await dicom_mcp_module.dicom_query_philips_private( + file_path=str(philips_dicom), + list_creators=True, + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "Philips Private Creators" in result + assert "DD 001" in result + assert "DD 002" in result + + @pytest.mark.asyncio + async def test_no_args_error(self, dicom_mcp_module, philips_dicom): + """Test dicom_query_philips_private with no mode-specific args.""" + result = await dicom_mcp_module.dicom_query_philips_private( + file_path=str(philips_dicom), + ) + assert "Error" in result + assert "list_creators" in result + + @pytest.mark.asyncio + async def test_file_not_found(self, dicom_mcp_module, tmp_path): + """Test dicom_query_philips_private with missing file.""" + result = await dicom_mcp_module.dicom_query_philips_private( + file_path=str(tmp_path / "nonexistent.dcm"), + list_creators=True, + ) + assert "Error" in result + assert "not found" in result + + +class TestGetMetadataPhilipsPrivate: + """Test dicom_get_metadata with philips_private_tags parameter.""" + + @pytest.fixture + def philips_dicom(self, tmp_path): + """Create a synthetic Philips DICOM file with private creator tags.""" + ds = _make_base_dataset( + bits_stored=16, + Modality="MR", + Manufacturer="Philips Medical Systems", + SeriesDescription="Test", + SeriesNumber=1, + PatientName="TestPatient", + PatientID="12345", + ) + + # Private Creator tags + ds.add_new((0x2005, 0x0010), "LO", "Philips MR Imaging DD 001") + ds.add_new((0x2005, 0x1085), "LO", "ShimTestValue") + + file_path = tmp_path / "philips_meta_test.dcm" + _save_dicom(ds, file_path) + return file_path + + @pytest.mark.asyncio + async def test_metadata_with_philips_private(self, dicom_mcp_module, philips_dicom): + """Test dicom_get_metadata with philips_private_tags resolves tags.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(philips_dicom), + tag_groups=["manufacturer"], + philips_private_tags=[{"dd_number": 1, "element_offset": 0x85}], + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert "philips_private" in data["metadata"] + private = data["metadata"]["philips_private"] + # Key is DD001_0x85 + assert "DD001_0x85" in private + assert private["DD001_0x85"]["value"] == "ShimTestValue" + assert private["DD001_0x85"]["resolved_tag"] == "(2005,1085)" + + @pytest.mark.asyncio + async def test_metadata_philips_private_missing_dd( + self, dicom_mcp_module, philips_dicom + ): + """Test dicom_get_metadata with non-existent DD number.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(philips_dicom), + tag_groups=["manufacturer"], + philips_private_tags=[{"dd_number": 99, "element_offset": 0x01}], + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert "philips_private" in data["metadata"] + private = data["metadata"]["philips_private"] + assert "DD099_0x01" in private + assert "error" in private["DD099_0x01"] + + @pytest.mark.asyncio + async def test_metadata_philips_private_markdown( + self, dicom_mcp_module, philips_dicom + ): + """Test dicom_get_metadata philips_private_tags in markdown format.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(philips_dicom), + tag_groups=["manufacturer"], + philips_private_tags=[{"dd_number": 1, "element_offset": 0x85}], + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "Philips Private" in result + assert "(2005,1085)" in result + assert "ShimTestValue" in result + + +class TestPIIFiltering: + """Test PII filtering when DICOM_MCP_PII_FILTER is enabled.""" + + @pytest.fixture + def pii_dicom(self, tmp_path): + """Create a synthetic DICOM with known patient data.""" + ds = _make_pii_dataset() + ds.StudyDate = "20240101" + file_path = tmp_path / "pii_test.dcm" + _save_dicom(ds, file_path) + return file_path + + @pytest.fixture(autouse=True) + def enable_pii_filter(self, monkeypatch): + """Enable the PII filter for every test in this class.""" + import importlib + + import dicom_mcp.config + + monkeypatch.setenv("DICOM_MCP_PII_FILTER", "true") + importlib.reload(dicom_mcp.config) + yield + monkeypatch.delenv("DICOM_MCP_PII_FILTER", raising=False) + importlib.reload(dicom_mcp.config) + + @pytest.mark.asyncio + async def test_get_metadata_redacts_patient_info(self, dicom_mcp_module, pii_dicom): + """dicom_get_metadata should redact patient tags when filter is on.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(pii_dicom), + tag_groups=["patient_info"], + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + patient = data["metadata"]["patient_info"] + assert patient["PatientName"] == "[REDACTED]" + assert patient["PatientID"] == "[REDACTED]" + assert patient["PatientBirthDate"] == "[REDACTED]" + assert patient["PatientSex"] == "[REDACTED]" + + @pytest.mark.asyncio + async def test_get_metadata_does_not_redact_non_pii( + self, dicom_mcp_module, pii_dicom + ): + """Non-patient tags should NOT be redacted even when filter is on.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(pii_dicom), + tag_groups=["manufacturer"], + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + manufacturer = data["metadata"]["manufacturer"] + assert manufacturer["Manufacturer"] == "SIEMENS" + assert "[REDACTED]" not in json.dumps(manufacturer) + + @pytest.mark.asyncio + async def test_get_metadata_redacts_in_markdown(self, dicom_mcp_module, pii_dicom): + """Markdown output should also show [REDACTED] for patient tags.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(pii_dicom), + tag_groups=["patient_info"], + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "[REDACTED]" in result + assert "Doe^Jane" not in result + assert "PII-TEST-001" not in result + + +class TestPIIFilteringDisabled: + """Test that PII is NOT filtered when DICOM_MCP_PII_FILTER is off.""" + + @pytest.fixture + def pii_dicom(self, tmp_path): + """Create a synthetic DICOM with known patient data.""" + ds = _make_pii_dataset() + file_path = tmp_path / "pii_disabled_test.dcm" + _save_dicom(ds, file_path) + return file_path + + @pytest.fixture(autouse=True) + def disable_pii_filter(self, monkeypatch): + """Ensure the PII filter is OFF for every test in this class.""" + import importlib + + import dicom_mcp.config + + monkeypatch.delenv("DICOM_MCP_PII_FILTER", raising=False) + importlib.reload(dicom_mcp.config) + yield + importlib.reload(dicom_mcp.config) + + @pytest.mark.asyncio + async def test_get_metadata_shows_patient_data(self, dicom_mcp_module, pii_dicom): + """Patient data should be visible when filter is off.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(pii_dicom), + tag_groups=["patient_info"], + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + patient = data["metadata"]["patient_info"] + assert patient["PatientName"] == "Doe^Jane" + assert patient["PatientID"] == "PII-TEST-001" + assert patient["PatientBirthDate"] == "19850315" + assert patient["PatientSex"] == "F" + + @pytest.mark.asyncio + async def test_no_redaction_markers_present(self, dicom_mcp_module, pii_dicom): + """No [REDACTED] markers should appear when filter is off.""" + result = await dicom_mcp_module.dicom_get_metadata( + file_path=str(pii_dicom), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + assert "[REDACTED]" not in result + + +class TestDicomDumpTree: + """Tests for dicom_dump_tree tool.""" + + @pytest.fixture + def tree_dicom(self, tmp_path): + """Create a synthetic DICOM with nested sequences.""" + ds = _make_base_dataset( + Modality="MR", + PatientName="TreeTest^Patient", + PatientID="TREE-001", + SeriesDescription="Tree Test Series", + ) + + # Add a nested sequence using standard DICOM tags + code_item = pydicom.Dataset() + code_item.CodeValue = "12345" + code_item.CodingSchemeDesignator = "DCM" + code_item.CodeMeaning = "Test Code" + + ref_item = pydicom.Dataset() + ref_item.ReferencedSOPClassUID = MRImageStorage + ref_item.ReferencedSOPInstanceUID = generate_uid() + ref_item.PurposeOfReferenceCodeSequence = DicomSequence([code_item]) + + ds.ReferencedStudySequence = DicomSequence([ref_item]) + + # Add a private tag + ds.add_new((0x0009, 0x0010), "LO", "TEST PRIVATE") + ds.add_new((0x0009, 0x1001), "LO", "PrivateValue42") + + file_path = tmp_path / "tree_test.dcm" + _save_dicom(ds, file_path) + return file_path + + @pytest.mark.asyncio + async def test_tree_markdown_output(self, dicom_mcp_module, tree_dicom): + """Test markdown tree output contains expected elements.""" + result = await dicom_mcp_module.dicom_dump_tree( + file_path=str(tree_dicom), + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "# DICOM Tree:" in result + assert "PatientName" in result + assert "TreeTest^Patient" in result + assert "SeriesDescription" in result + + @pytest.mark.asyncio + async def test_tree_json_output(self, dicom_mcp_module, tree_dicom): + """Test JSON tree output has correct structure.""" + result = await dicom_mcp_module.dicom_dump_tree( + file_path=str(tree_dicom), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert "tree" in data + assert isinstance(data["tree"], list) + assert data["file"] == "tree_test.dcm" + assert len(data["tree"]) > 0 + + @pytest.mark.asyncio + async def test_tree_includes_sequences(self, dicom_mcp_module, tree_dicom): + """Test that nested sequences appear in tree output.""" + result = await dicom_mcp_module.dicom_dump_tree( + file_path=str(tree_dicom), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + # Find the sequence element + seq_nodes = [n for n in data["tree"] if n["vr"] == "SQ"] + assert len(seq_nodes) > 0 + # Check it has items with children + assert "items" in seq_nodes[0] + assert len(seq_nodes[0]["items"]) > 0 + + @pytest.mark.asyncio + async def test_tree_max_depth(self, dicom_mcp_module, tree_dicom): + """Test max_depth=0 prevents sequence recursion.""" + result = await dicom_mcp_module.dicom_dump_tree( + file_path=str(tree_dicom), + max_depth=0, + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + # Sequences should exist but have no items expanded + seq_nodes = [n for n in data["tree"] if n["vr"] == "SQ"] + for node in seq_nodes: + assert "items" not in node + + @pytest.mark.asyncio + async def test_tree_hide_private(self, dicom_mcp_module, tree_dicom): + """Test show_private=False excludes private tags.""" + result = await dicom_mcp_module.dicom_dump_tree( + file_path=str(tree_dicom), + show_private=False, + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "PrivateValue42" not in result + # Standard tags should still be present + assert "PatientName" in result + + @pytest.mark.asyncio + async def test_tree_show_private(self, dicom_mcp_module, tree_dicom): + """Test show_private=True includes private tags.""" + result = await dicom_mcp_module.dicom_dump_tree( + file_path=str(tree_dicom), + show_private=True, + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "PrivateValue42" in result + + @pytest.mark.asyncio + async def test_tree_file_not_found(self, dicom_mcp_module): + """Test error for non-existent file.""" + result = await dicom_mcp_module.dicom_dump_tree( + file_path="/nonexistent/file.dcm", + ) + assert "Error" in result + + +class TestDicomCompareUids: + """Tests for dicom_compare_uids tool.""" + + def _make_dicom_file(self, tmp_path, subdir, filename, series_uid, study_uid=None): + """Create a minimal DICOM file with specified UIDs.""" + ds = _make_base_dataset( + series_instance_uid=series_uid, + study_instance_uid=study_uid, + Modality="MR", + FrameOfReferenceUID=generate_uid(), + ) + + file_path = tmp_path / subdir / filename + _save_dicom(ds, file_path) + return file_path + + @pytest.mark.asyncio + async def test_identical_uids(self, dicom_mcp_module, tmp_path): + """Two directories with same SeriesInstanceUID should match.""" + uid = "1.2.3.4.5.6.7.8.9" + self._make_dicom_file(tmp_path, "dir1", "a.dcm", uid) + self._make_dicom_file(tmp_path, "dir2", "b.dcm", uid) + + result = await dicom_mcp_module.dicom_compare_uids( + directory1=str(tmp_path / "dir1"), + directory2=str(tmp_path / "dir2"), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["match"] is True + assert data["shared"] == 1 + assert data["only_in_directory1"] == 0 + assert data["only_in_directory2"] == 0 + + @pytest.mark.asyncio + async def test_disjoint_uids(self, dicom_mcp_module, tmp_path): + """Two directories with different UIDs should report mismatches.""" + self._make_dicom_file(tmp_path, "dir1", "a.dcm", "1.2.3.4.5") + self._make_dicom_file(tmp_path, "dir2", "b.dcm", "9.8.7.6.5") + + result = await dicom_mcp_module.dicom_compare_uids( + directory1=str(tmp_path / "dir1"), + directory2=str(tmp_path / "dir2"), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["match"] is False + assert data["shared"] == 0 + assert data["only_in_directory1"] == 1 + assert data["only_in_directory2"] == 1 + + @pytest.mark.asyncio + async def test_partial_overlap(self, dicom_mcp_module, tmp_path): + """Test partial overlap between directories.""" + shared_uid = "1.2.3.4.5" + self._make_dicom_file(tmp_path, "dir1", "a.dcm", shared_uid) + self._make_dicom_file(tmp_path, "dir1", "b.dcm", "1.2.3.4.6") + self._make_dicom_file(tmp_path, "dir2", "c.dcm", shared_uid) + self._make_dicom_file(tmp_path, "dir2", "d.dcm", "9.8.7.6.5") + + result = await dicom_mcp_module.dicom_compare_uids( + directory1=str(tmp_path / "dir1"), + directory2=str(tmp_path / "dir2"), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["match"] is False + assert data["shared"] == 1 + assert data["only_in_directory1"] == 1 + assert data["only_in_directory2"] == 1 + + @pytest.mark.asyncio + async def test_custom_compare_tag(self, dicom_mcp_module, tmp_path): + """Test comparing by StudyInstanceUID instead of SeriesInstanceUID.""" + study_uid = "1.2.3.100" + self._make_dicom_file(tmp_path, "dir1", "a.dcm", "1.2.3.101", study_uid) + self._make_dicom_file(tmp_path, "dir2", "b.dcm", "1.2.3.102", study_uid) + + result = await dicom_mcp_module.dicom_compare_uids( + directory1=str(tmp_path / "dir1"), + directory2=str(tmp_path / "dir2"), + compare_tag="StudyInstanceUID", + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["match"] is True + assert data["shared"] == 1 + + @pytest.mark.asyncio + async def test_markdown_output(self, dicom_mcp_module, tmp_path): + """Test markdown output format.""" + self._make_dicom_file(tmp_path, "dir1", "a.dcm", "1.2.3") + self._make_dicom_file(tmp_path, "dir2", "b.dcm", "1.2.3") + + result = await dicom_mcp_module.dicom_compare_uids( + directory1=str(tmp_path / "dir1"), + directory2=str(tmp_path / "dir2"), + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "# UID Comparison" in result + assert "MATCH" in result + + @pytest.mark.asyncio + async def test_directory_not_found(self, dicom_mcp_module, tmp_path): + """Test error for non-existent directory.""" + result = await dicom_mcp_module.dicom_compare_uids( + directory1="/nonexistent/dir1", + directory2=str(tmp_path), + ) + assert "Error" in result + + @pytest.mark.asyncio + async def test_empty_directory(self, dicom_mcp_module, tmp_path): + """Test with an empty directory.""" + (tmp_path / "dir1").mkdir() + (tmp_path / "dir2").mkdir() + result = await dicom_mcp_module.dicom_compare_uids( + directory1=str(tmp_path / "dir1"), + directory2=str(tmp_path / "dir2"), + ) + assert "No DICOM files" in result + + +class TestDicomVerifySegmentations: + """Tests for dicom_verify_segmentations tool.""" + + def _make_source_dicom(self, dir_path, filename, sop_uid): + """Create a minimal source DICOM with a known SOPInstanceUID.""" + ds = _make_base_dataset( + sop_instance_uid=sop_uid, + Modality="MR", + ) + + file_path = dir_path / filename + _save_dicom(ds, file_path) + return file_path + + def _make_seg_dicom(self, dir_path, filename, ref_sop_uids): + """Create a minimal segmentation DICOM referencing source UIDs.""" + ds = _make_base_dataset( + sop_class=SegmentationStorage, + SeriesDescription="Segmentation", + Modality="SEG", + ) + + # Build SourceImageSequence + source_items = [] + for ref_uid in ref_sop_uids: + item = pydicom.Dataset() + item.ReferencedSOPClassUID = MRImageStorage + item.ReferencedSOPInstanceUID = UID(ref_uid) + source_items.append(item) + ds.SourceImageSequence = DicomSequence(source_items) + + file_path = dir_path / filename + _save_dicom(ds, file_path) + return file_path + + @pytest.mark.asyncio + async def test_all_matched(self, dicom_mcp_module, tmp_path): + """Test with segmentations that all reference valid source files.""" + source_uid = "1.2.3.4.5.100" + d = tmp_path / "seg_test" + self._make_source_dicom(d, "source.dcm", source_uid) + self._make_seg_dicom(d, "seg.dcm", [source_uid]) + + result = await dicom_mcp_module.dicom_verify_segmentations( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["all_matched"] is True + assert data["matched_references"] == 1 + assert data["unmatched_references"] == 0 + + @pytest.mark.asyncio + async def test_unmatched_reference(self, dicom_mcp_module, tmp_path): + """Test with a dangling reference in the segmentation.""" + d = tmp_path / "seg_test" + self._make_source_dicom(d, "source.dcm", "1.2.3.200") + self._make_seg_dicom(d, "seg.dcm", ["1.2.3.999"]) + + result = await dicom_mcp_module.dicom_verify_segmentations( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["all_matched"] is False + assert data["unmatched_references"] == 1 + + @pytest.mark.asyncio + async def test_multiple_references(self, dicom_mcp_module, tmp_path): + """Test segmentation with multiple source references.""" + uid1 = "1.2.3.4.5.201" + uid2 = "1.2.3.4.5.202" + d = tmp_path / "seg_test" + self._make_source_dicom(d, "source1.dcm", uid1) + self._make_source_dicom(d, "source2.dcm", uid2) + self._make_seg_dicom(d, "seg.dcm", [uid1, uid2]) + + result = await dicom_mcp_module.dicom_verify_segmentations( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["all_matched"] is True + assert data["matched_references"] == 2 + + @pytest.mark.asyncio + async def test_no_segmentations(self, dicom_mcp_module, tmp_path): + """Test with no segmentation files in directory.""" + d = tmp_path / "seg_test" + self._make_source_dicom(d, "source.dcm", "1.2.3.4.5") + + result = await dicom_mcp_module.dicom_verify_segmentations( + directory=str(d), + ) + assert "No segmentation files" in result + + @pytest.mark.asyncio + async def test_markdown_output(self, dicom_mcp_module, tmp_path): + """Test markdown output format.""" + d = tmp_path / "seg_test" + self._make_source_dicom(d, "source.dcm", "1.2.3.400") + self._make_seg_dicom(d, "seg.dcm", ["1.2.3.400"]) + + result = await dicom_mcp_module.dicom_verify_segmentations( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "# Segmentation Verification" in result + assert "ALL MATCHED" in result + + @pytest.mark.asyncio + async def test_markdown_unmatched_details(self, dicom_mcp_module, tmp_path): + """Test markdown output shows unmatched reference details.""" + d = tmp_path / "seg_test" + self._make_source_dicom(d, "source.dcm", "1.2.3.200") + self._make_seg_dicom(d, "seg.dcm", ["1.2.3.999"]) + + result = await dicom_mcp_module.dicom_verify_segmentations( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "UNMATCHED REFERENCES FOUND" in result + assert "NOT FOUND" in result + + @pytest.mark.asyncio + async def test_directory_not_found(self, dicom_mcp_module): + """Test error for non-existent directory.""" + result = await dicom_mcp_module.dicom_verify_segmentations( + directory="/nonexistent/dir", + ) + assert "Error" in result + + +class TestDicomAnalyzeTi: + """Tests for dicom_analyze_ti tool.""" + + def _make_molli_file( + self, + dir_path, + filename, + instance_num, + inversion_time=None, + series_num=601, + series_desc="LMS MOLLI", + manufacturer="SIEMENS", + philips_private_ti=None, + ): + """Create a synthetic MOLLI DICOM file.""" + ds = _make_base_dataset( + Modality="MR", + Manufacturer=manufacturer, + SeriesNumber=series_num, + SeriesDescription=series_desc, + InstanceNumber=instance_num, + ScanningSequence="GR", + SequenceVariant="MP", + MRAcquisitionType="2D", + RepetitionTime=2.42, + EchoTime=1.05, + FlipAngle=35, + ) + + if inversion_time is not None: + ds.InversionTime = inversion_time + + # Add Philips private TI tag if requested + if philips_private_ti is not None and "philips" in manufacturer.lower(): + # Add DD 006 private creator at group 2005 + ds.add_new((0x2005, 0x0015), "LO", "Philips MR Imaging DD 006") + # Add the TI value at offset 0x72 from block 0x15 + ds.add_new((0x2005, 0x1572), "FL", philips_private_ti) + + file_path = dir_path / filename + _save_dicom(ds, file_path) + return file_path + + @pytest.mark.asyncio + async def test_siemens_standard_ti(self, dicom_mcp_module, tmp_path): + """Test TI extraction from standard InversionTime tag (Siemens/GE).""" + d = tmp_path / "molli" + tis = [100.0, 200.0, 350.0, 500.0, 750.0] + for i, ti in enumerate(tis, 1): + self._make_molli_file(d, f"img{i:02d}.dcm", i, inversion_time=ti) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["t1_mapping_files"] == 5 + assert data["series_count"] == 1 + + stats = data["series"][0]["statistics"] + assert stats["non_zero_ti_count"] == 5 + assert stats["ti_min"] == 100.0 + assert stats["ti_max"] == 750.0 + + @pytest.mark.asyncio + async def test_philips_private_ti(self, dicom_mcp_module, tmp_path): + """Test TI extraction from Philips private tag.""" + d = tmp_path / "molli_philips" + tis = [78.0, 159.0, 856.0, 1034.0, 1656.0] + for i, ti in enumerate(tis, 1): + self._make_molli_file( + d, + f"img{i:02d}.dcm", + i, + manufacturer="Philips Medical Systems", + philips_private_ti=ti, + ) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["t1_mapping_files"] == 5 + stats = data["series"][0]["statistics"] + assert stats["non_zero_ti_count"] == 5 + assert stats["ti_min"] == 78.0 + assert stats["ti_max"] == 1656.0 + + @pytest.mark.asyncio + async def test_zero_ti_filtering(self, dicom_mcp_module, tmp_path): + """Test that zero TIs are counted separately as output maps.""" + d = tmp_path / "molli" + self._make_molli_file(d, "img01.dcm", 1, inversion_time=100.0) + self._make_molli_file(d, "img02.dcm", 2, inversion_time=200.0) + self._make_molli_file(d, "img03.dcm", 3, inversion_time=0.0) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + stats = data["series"][0]["statistics"] + assert stats["non_zero_ti_count"] == 2 + assert stats["zero_ti_count"] == 1 + + @pytest.mark.asyncio + async def test_gap_threshold_exceeded(self, dicom_mcp_module, tmp_path): + """Test gap threshold warning is triggered.""" + d = tmp_path / "molli" + tis = [100.0, 200.0, 3500.0] # gap of 3300 exceeds default 2500 + for i, ti in enumerate(tis, 1): + self._make_molli_file(d, f"img{i:02d}.dcm", i, inversion_time=ti) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + stats = data["series"][0]["statistics"] + assert stats["gap_threshold_exceeded"] is True + assert stats["max_gap"] == 3300.0 + + @pytest.mark.asyncio + async def test_gap_threshold_not_exceeded(self, dicom_mcp_module, tmp_path): + """Test gap threshold warning is not triggered for small gaps.""" + d = tmp_path / "molli" + tis = [100.0, 200.0, 350.0] + for i, ti in enumerate(tis, 1): + self._make_molli_file(d, f"img{i:02d}.dcm", i, inversion_time=ti) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + stats = data["series"][0]["statistics"] + assert stats["gap_threshold_exceeded"] is False + + @pytest.mark.asyncio + async def test_custom_gap_threshold(self, dicom_mcp_module, tmp_path): + """Test custom gap_threshold parameter.""" + d = tmp_path / "molli" + tis = [100.0, 200.0, 500.0] # gap of 300 + for i, ti in enumerate(tis, 1): + self._make_molli_file(d, f"img{i:02d}.dcm", i, inversion_time=ti) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + gap_threshold=200.0, + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + stats = data["series"][0]["statistics"] + assert stats["gap_threshold_exceeded"] is True + + @pytest.mark.asyncio + async def test_no_t1_mapping_files(self, dicom_mcp_module, tmp_path): + """Test with no T1 mapping sequences in directory.""" + d = tmp_path / "not_molli" + ds = _make_base_dataset( + Modality="MR", + Manufacturer="SIEMENS", + SeriesDescription="LMS IDEAL", + ScanningSequence="GR", + SequenceVariant="SP", + ) + _save_dicom(ds, d / "ideal.dcm") + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + ) + assert "No T1 mapping" in result + + @pytest.mark.asyncio + async def test_markdown_output(self, dicom_mcp_module, tmp_path): + """Test markdown output format.""" + d = tmp_path / "molli" + self._make_molli_file(d, "img01.dcm", 1, inversion_time=100.0) + self._make_molli_file(d, "img02.dcm", 2, inversion_time=200.0) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.MARKDOWN, + ) + assert "# Inversion Time Analysis" in result + assert "LMS MOLLI" in result + assert "100.0" in result + + @pytest.mark.asyncio + async def test_multiple_series(self, dicom_mcp_module, tmp_path): + """Test with multiple MOLLI series in one directory.""" + d = tmp_path / "molli" + self._make_molli_file( + d, + "s601_01.dcm", + 1, + inversion_time=100.0, + series_num=601, + series_desc="LMS MOLLI", + ) + self._make_molli_file( + d, + "s801_01.dcm", + 1, + inversion_time=100.0, + series_num=801, + series_desc="LMS MOLLI simulated", + ) + + result = await dicom_mcp_module.dicom_analyze_ti( + directory=str(d), + response_format=dicom_mcp_module.ResponseFormat.JSON, + ) + data = json.loads(result) + assert data["series_count"] == 2 + + @pytest.mark.asyncio + async def test_directory_not_found(self, dicom_mcp_module): + """Test error for non-existent directory.""" + result = await dicom_mcp_module.dicom_analyze_ti( + directory="/nonexistent/dir", + ) + assert "Error" in result + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "--tb=short"]) diff --git a/mcps/filesystem_mcp/install.sh b/mcps/filesystem_mcp/install.sh new file mode 100755 index 0000000..2f28c9c --- /dev/null +++ b/mcps/filesystem_mcp/install.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +echo "=== Installing @cyanheads/filesystem-mcp-server ===" + +# Check for Node.js +if ! command -v node >/dev/null 2>&1; then + echo "Error: Node.js not found! Please install Node.js first." >&2 + exit 1 +fi + +echo "Node.js version: $(node --version)" + +# Install globally via npm +echo "Installing @cyanheads/filesystem-mcp-server globally..." +npm install -g @cyanheads/filesystem-mcp-server + +if [ $? -eq 0 ]; then + echo "Installation successful!" + echo "The package is now available at: $(npm list -g @cyanheads/filesystem-mcp-server --depth=0)" + echo "" + echo "You can now run the launcher: ./launch_filesystem_mcp.sh" + echo "Or use it directly with npx or in your MCP client config." +else + echo "Installation failed!" >&2 + exit 1 +fi diff --git a/mcps/filesystem_mcp/launch_filesystem_mcp.sh b/mcps/filesystem_mcp/launch_filesystem_mcp.sh new file mode 100755 index 0000000..9954317 --- /dev/null +++ b/mcps/filesystem_mcp/launch_filesystem_mcp.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env zsh + +echo "=== Filesystem MCP Server Launcher ===" + +# Use the globally installed package +if command -v node >/dev/null 2>&1; then + echo "Starting @cyanheads/filesystem-mcp-server (stdio transport)..." + echo "Log level: debug" + echo "Press Ctrl+C to stop." + echo "----------------------------------------" + + MCP_LOG_LEVEL=debug \ + MCP_TRANSPORT_TYPE=stdio \ + node "/Users/gregory.gauthier/.nvm/versions/node/v22.19.0/lib/node_modules/@cyanheads/filesystem-mcp-server/dist/index.js" \ + 2> filesystem_mcp.log +else + echo "Error: Node.js not found!" >&2 + exit 1 +fi diff --git a/mcps/filesystem_mcp/mcp.json b/mcps/filesystem_mcp/mcp.json new file mode 100644 index 0000000..46099de --- /dev/null +++ b/mcps/filesystem_mcp/mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "filesystem": { + "command": "zsh", + "args": ["./launch_filesystem_mcp.sh"], + "env": { + "MCP_LOG_LEVEL": "debug", + "MCP_TRANSPORT_TYPE": "stdio" + } + } + } +} diff --git a/mcps/open_meteo_mcp/README.md b/mcps/open_meteo_mcp/README.md new file mode 100644 index 0000000..905babe --- /dev/null +++ b/mcps/open_meteo_mcp/README.md @@ -0,0 +1,60 @@ +# open-meteo-mcp + +An MCP server for global weather data via the [Open-Meteo API](https://open-meteo.com/). No API key required. + +## Tools + +### get_current_weather +Returns current conditions for a given latitude/longitude: +- Temperature and "feels like" +- Humidity, wind speed/direction/gusts +- Precipitation, cloud cover, pressure +- WMO weather code decoded to plain English + +### get_forecast +Returns a multi-day forecast (1-16 days) for a given latitude/longitude: +- Daily high/low temperatures +- Precipitation totals and probability +- Wind speed, gusts, and dominant direction +- Sunrise/sunset times +- Supports celsius or fahrenheit + +## Setup + +Requires Python 3.10+ and [Poetry](https://python-poetry.org/). + +```sh +cd /data/Projects/open_meteo_mcp +poetry install +``` + +## Usage + +### Stdio (for Claude Code) + +```sh +.venv/bin/open-meteo-mcp-stdio +``` + +Add to `~/.claude/settings.json`: + +```json +"open_meteo": { + "type": "stdio", + "command": "/data/Projects/open_meteo_mcp/.venv/bin/open-meteo-mcp-stdio", + "args": [], + "env": {} +} +``` + +### Streamable HTTP + +```sh +.venv/bin/open-meteo-mcp +``` + +Starts on `http://127.0.0.1:8000/mcp` by default. + +## Data Source + +All weather data comes from [Open-Meteo](https://open-meteo.com/), which aggregates national weather services worldwide. Free for non-commercial use. diff --git a/mcps/open_meteo_mcp/__pycache__/open_meteo_mcp_server.cpython-312.pyc b/mcps/open_meteo_mcp/__pycache__/open_meteo_mcp_server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e514bcdd27256e390f64a538dafe42ba4f2ed507 GIT binary patch literal 10693 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 str: + return WMO_CODES.get(code, f"Unknown ({code})") + + +def _c_to_f(celsius: float) -> float: + return round(celsius * 9 / 5 + 32, 1) + + +def _temp_both(celsius: float) -> str: + return f"{celsius}°C / {_c_to_f(celsius)}°F" + + +def _kmh_to_mph(kmh: float) -> float: + return round(kmh * 0.621371, 1) + + +def _wind_both(kmh: float) -> str: + return f"{kmh}km/h / {_kmh_to_mph(kmh)}mph" + + +@mcp.tool() +async def get_current_weather(latitude: float, longitude: float) -> str: + """Get current weather conditions for a location. + + Args: + latitude: Latitude of the location (e.g. 51.752 for Oxford, UK) + longitude: Longitude of the location (e.g. -1.258 for Oxford, UK) + """ + try: + params = { + "latitude": latitude, + "longitude": longitude, + "current": ",".join([ + "temperature_2m", + "relative_humidity_2m", + "apparent_temperature", + "weather_code", + "wind_speed_10m", + "wind_direction_10m", + "wind_gusts_10m", + "precipitation", + "cloud_cover", + "pressure_msl", + ]), + "timezone": "auto", + } + + async with httpx.AsyncClient() as client: + resp = await client.get(OPEN_METEO_BASE, params=params, timeout=30) + resp.raise_for_status() + data = resp.json() + + current = data["current"] + units = data["current_units"] + tz = data.get("timezone", "Unknown") + + lines = [ + f"Current Weather (timezone: {tz})", + f" Time: {current['time']}", + f" Condition: {_describe_weather_code(current['weather_code'])}", + f" Temperature: {_temp_both(current['temperature_2m'])}", + f" Feels like: {_temp_both(current['apparent_temperature'])}", + f" Humidity: {current['relative_humidity_2m']}{units['relative_humidity_2m']}", + f" Wind: {_wind_both(current['wind_speed_10m'])} from {current['wind_direction_10m']}{units['wind_direction_10m']}", + f" Gusts: {_wind_both(current['wind_gusts_10m'])}", + f" Precipitation: {current['precipitation']}{units['precipitation']}", + f" Cloud cover: {current['cloud_cover']}{units['cloud_cover']}", + f" Pressure: {current['pressure_msl']}{units['pressure_msl']}", + ] + + return "\n".join(lines) + except Exception as e: + logger.error("Error in get_current_weather(%s, %s): %s", latitude, longitude, e, exc_info=True) + return f"Error fetching current weather: {type(e).__name__}: {e}" + + +@mcp.tool() +async def get_forecast( + latitude: float, + longitude: float, + days: int = 7, +) -> str: + """Get a multi-day weather forecast for a location. + + Args: + latitude: Latitude of the location + longitude: Longitude of the location + days: Number of forecast days (1-16, default 7) + """ + try: + days = max(1, min(16, days)) + + params = { + "latitude": latitude, + "longitude": longitude, + "daily": ",".join([ + "weather_code", + "temperature_2m_max", + "temperature_2m_min", + "apparent_temperature_max", + "apparent_temperature_min", + "precipitation_sum", + "precipitation_probability_max", + "wind_speed_10m_max", + "wind_gusts_10m_max", + "wind_direction_10m_dominant", + "sunrise", + "sunset", + ]), + "temperature_unit": "celsius", + "timezone": "auto", + "forecast_days": days, + } + + async with httpx.AsyncClient() as client: + resp = await client.get(OPEN_METEO_BASE, params=params, timeout=30) + resp.raise_for_status() + data = resp.json() + + daily = data["daily"] + units = data["daily_units"] + tz = data.get("timezone", "Unknown") + + sections = [f"Forecast for {days} day(s) (timezone: {tz})"] + + for i in range(len(daily["time"])): + section = [ + f"\n--- {daily['time'][i]} ---", + f" Condition: {_describe_weather_code(daily['weather_code'][i])}", + f" High: {_temp_both(daily['temperature_2m_max'][i])} Low: {_temp_both(daily['temperature_2m_min'][i])}", + f" Feels like: {_temp_both(daily['apparent_temperature_max'][i])} / {_temp_both(daily['apparent_temperature_min'][i])}", + f" Precipitation: {daily['precipitation_sum'][i]}{units['precipitation_sum']} (chance: {daily['precipitation_probability_max'][i]}{units['precipitation_probability_max']})", + f" Wind: {_wind_both(daily['wind_speed_10m_max'][i])} gusts {_wind_both(daily['wind_gusts_10m_max'][i])} from {daily['wind_direction_10m_dominant'][i]}{units['wind_direction_10m_dominant']}", + f" Sunrise: {daily['sunrise'][i]} Sunset: {daily['sunset'][i]}", + ] + sections.append("\n".join(section)) + + return "\n".join(sections) + except Exception as e: + logger.error("Error in get_forecast(%s, %s, days=%s): %s", latitude, longitude, days, e, exc_info=True) + return f"Error fetching forecast: {type(e).__name__}: {e}" + + +# --------------------------------------------------------------------------- +# Entrypoints +# --------------------------------------------------------------------------- +def main(): + logger.info("Starting open_meteo_mcp on streamable-http") + mcp.run(transport="streamable-http") + + +def main_stdio(): + logger.info("Starting open_meteo_mcp via stdio") + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/mcps/open_meteo_mcp/poetry.lock b/mcps/open_meteo_mcp/poetry.lock new file mode 100644 index 0000000..b2eeb64 --- /dev/null +++ b/mcps/open_meteo_mcp/poetry.lock @@ -0,0 +1,941 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.13.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "click" +version = "8.3.2" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "sys_platform != \"emscripten\"" +files = [ + {file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"}, + {file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "sys_platform != \"emscripten\" and platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "46.0.6" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +files = [ + {file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"}, + {file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"}, + {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"}, + {file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"}, + {file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"}, + {file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"}, + {file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"}, + {file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"}, + {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"}, + {file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"}, + {file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"}, + {file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"}, + {file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"}, + {file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"}, + {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"}, + {file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"}, + {file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"}, + {file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"}, + {file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"}, + {file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc"}, + {file = "httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d"}, +] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "jsonschema" +version = "4.26.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.25.0" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "mcp" +version = "1.27.0" +description = "Model Context Protocol SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741"}, + {file = "mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83"}, +] + +[package.dependencies] +anyio = ">=4.5" +httpx = ">=0.27.1" +httpx-sse = ">=0.4" +jsonschema = ">=4.20.0" +pydantic = ">=2.11.0,<3.0.0" +pydantic-settings = ">=2.5.2" +pyjwt = {version = ">=2.10.1", extras = ["crypto"]} +python-multipart = ">=0.0.9" +pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""} +sse-starlette = ">=1.6.1" +starlette = ">=0.27" +typing-extensions = ">=4.9.0" +typing-inspection = ">=0.4.1" +uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""} + +[package.extras] +cli = ["python-dotenv (>=1.0.0)", "typer (>=0.16.0)"] +rich = ["rich (>=13.9.4)"] +ws = ["websockets (>=15.0.1)"] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237"}, + {file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pyjwt" +version = "2.12.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, + {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} +typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.24" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950"}, + {file = "python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8"}, +] + +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + +[[package]] +name = "referencing" +version = "0.37.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "rpds-py" +version = "0.30.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, +] + +[[package]] +name = "sse-starlette" +version = "3.3.4" +description = "SSE plugin for Starlette" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1"}, + {file = "sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1"}, +] + +[package.dependencies] +anyio = ">=4.7.0" +starlette = ">=0.49.1" + +[package.extras] +daphne = ["daphne (>=4.2.0)"] +examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "uvicorn (>=0.34.0)"] +granian = ["granian (>=2.3.1)"] +uvicorn = ["uvicorn (>=0.34.0)"] + +[[package]] +name = "starlette" +version = "1.0.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b"}, + {file = "starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "uvicorn" +version = "0.44.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "sys_platform != \"emscripten\"" +files = [ + {file = "uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89"}, + {file = "uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=10.4)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "7b99892e3738f4bb5297bf8a112b5785559535e21026690e3afb1904a219a47f" diff --git a/mcps/open_meteo_mcp/pyproject.toml b/mcps/open_meteo_mcp/pyproject.toml new file mode 100644 index 0000000..d28fa66 --- /dev/null +++ b/mcps/open_meteo_mcp/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "open-meteo-mcp" +version = "0.1.0" +description = "MCP server for global weather data via Open-Meteo API" +authors = [] +license = "MIT" +readme = "README.md" +packages = [{include = "open_meteo_mcp_server.py"}] + +[tool.poetry.dependencies] +python = "^3.10" +mcp = ">=1.9.0" +httpx = ">=0.27.0" + +[tool.poetry.scripts] +open-meteo-mcp = "open_meteo_mcp_server:main" +open-meteo-mcp-stdio = "open_meteo_mcp_server:main_stdio" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/mcps/open_meteo_mcp/run_open_meteo_mcp_server.sh b/mcps/open_meteo_mcp/run_open_meteo_mcp_server.sh new file mode 100755 index 0000000..af6167b --- /dev/null +++ b/mcps/open_meteo_mcp/run_open_meteo_mcp_server.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env zsh +# +if [[ "$(uname)" == "Linux" ]]; then + PATH_PREFIX="/data/Projects" +elif [[ "$(uname)" == "Darwin" ]]; then + PATH_PREFIX="/Users/gregory.gauthier/Projects" +else + # Default fallback is Linux + PATH_PREFIX="/data/Projects" +fi + +# shellcheck disable=SC2164 +cd "${PATH_PREFIX}/mcp_servers/open_meteo_mcp" +poetry run open-meteo-mcp-stdio diff --git a/mcps/playwright_mcp/README.md b/mcps/playwright_mcp/README.md new file mode 100644 index 0000000..fb2e073 --- /dev/null +++ b/mcps/playwright_mcp/README.md @@ -0,0 +1,4 @@ +# Playwright MCP + +This is a placeholder sub-project, where management of the playwright mcp will be done through a +convenience script. diff --git a/mcps/playwright_mcp/launch_playwright_mcp.sh b/mcps/playwright_mcp/launch_playwright_mcp.sh new file mode 100755 index 0000000..ac40827 --- /dev/null +++ b/mcps/playwright_mcp/launch_playwright_mcp.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env zsh + +if [[ "$(uname)" == "Linux" ]]; then + PATH_PREFIX="/data/Projects" +elif [[ "$(uname)" == "Darwin" ]]; then + PATH_PREFIX="/Users/gregory.gauthier/Projects" +else + # Default fallback + PATH_PREFIX="/data/Projects" +fi + +# shellcheck disable=SC2164 +cd "${PATH_PREFIX}/mcp_servers/playwright_mcp" + +# Install dependencies if not present +if [[ ! -d "node_modules" ]]; then + npm install @playwright/test @playwright/mcp +fi + +# Ensure browsers are installed +npx playwright install chromium + +# Launch the server +npx @playwright/mcp --browser chromium --headless 2> playwright_mcp.log + + diff --git a/mcps/playwright_mcp/node_modules/.bin/playwright b/mcps/playwright_mcp/node_modules/.bin/playwright new file mode 120000 index 0000000..c30d07f --- /dev/null +++ b/mcps/playwright_mcp/node_modules/.bin/playwright @@ -0,0 +1 @@ +../@playwright/test/cli.js \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/.bin/playwright-core b/mcps/playwright_mcp/node_modules/.bin/playwright-core new file mode 120000 index 0000000..08d6c28 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/.bin/playwright-core @@ -0,0 +1 @@ +../playwright-core/cli.js \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/.bin/playwright-mcp b/mcps/playwright_mcp/node_modules/.bin/playwright-mcp new file mode 120000 index 0000000..4e7271e --- /dev/null +++ b/mcps/playwright_mcp/node_modules/.bin/playwright-mcp @@ -0,0 +1 @@ +../@playwright/mcp/cli.js \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/.package-lock.json b/mcps/playwright_mcp/node_modules/.package-lock.json new file mode 100644 index 0000000..3187160 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/.package-lock.json @@ -0,0 +1,114 @@ +{ + "name": "playwright_mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@playwright/mcp": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.70.tgz", + "integrity": "sha512-Kl0a6l9VL8rvT1oBou3hS5yArjwWV9UlwAkq+0skfK1YVg8XfmmNaAmwZhMeNx/ZhGiWXfCllo6rD/jvZz+WuA==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0-alpha-1774999321000", + "playwright-core": "1.60.0-alpha-1774999321000" + }, + "bin": { + "playwright-mcp": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "ideallyInert": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0-alpha-1774999321000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0-alpha-1774999321000.tgz", + "integrity": "sha512-Bd5DkzYKG+2g1jLO6NeTXmGLbBYSFffJIOsR4l4hUBkJvzvGGdLZ7jZb2tOtb0WIoWXQKdQj3Ap6WthV4DBS8w==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0-alpha-1774999321000" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0-alpha-1774999321000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0-alpha-1774999321000.tgz", + "integrity": "sha512-ams3Zo4VXxeOg5ZTTh16GkE8g48Bmxo/9pg9gXl9SVKlVohCU7Jaog7XntY8yFuzENA6dJc1Fz7Z/NNTm9nGEw==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/mcp/LICENSE b/mcps/playwright_mcp/node_modules/@playwright/mcp/LICENSE new file mode 100644 index 0000000..cefe596 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/mcp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/mcps/playwright_mcp/node_modules/@playwright/mcp/README.md b/mcps/playwright_mcp/node_modules/@playwright/mcp/README.md new file mode 100644 index 0000000..8d39454 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/mcp/README.md @@ -0,0 +1,1455 @@ +## Playwright MCP + +A Model Context Protocol (MCP) server that provides browser automation capabilities using [Playwright](https://playwright.dev). This server enables LLMs to interact with web pages through structured accessibility snapshots, bypassing the need for screenshots or visually-tuned models. + +### Playwright MCP vs Playwright CLI + +This package provides MCP interface into Playwright. If you are using a **coding agent**, you might benefit from using the [CLI+SKILLS](https://github.com/microsoft/playwright-cli) instead. + +- **CLI**: Modern **coding agents** increasingly favor CLI–based workflows exposed as SKILLs over MCP because CLI invocations are more token-efficient: they avoid loading large tool schemas and verbose accessibility trees into the model context, allowing agents to act through concise, purpose-built commands. This makes CLI + SKILLs better suited for high-throughput coding agents that must balance browser automation with large codebases, tests, and reasoning within limited context windows.
**Learn more about [Playwright CLI with SKILLS](https://github.com/microsoft/playwright-cli)**. + +- **MCP**: MCP remains relevant for specialized agentic loops that benefit from persistent state, rich introspection, and iterative reasoning over page structure, such as exploratory automation, self-healing tests, or long-running autonomous workflows where maintaining continuous browser context outweighs token cost concerns. + +### Key Features + +- **Fast and lightweight**. Uses Playwright's accessibility tree, not pixel-based input. +- **LLM-friendly**. No vision models needed, operates purely on structured data. +- **Deterministic tool application**. Avoids ambiguity common with screenshot-based approaches. + +### Requirements +- Node.js 18 or newer +- VS Code, Cursor, Windsurf, Claude Desktop, Goose or any other MCP client + + + +### Getting started + +First, install the Playwright MCP server with your client. + +**Standard config** works in most of the tools: + +```js +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +[Install in VS Code](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [Install in VS Code Insiders](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) + +

+Amp + +Add via the Amp VS Code extension settings screen or by updating your settings.json file: + +```json +"amp.mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } +} +``` + +**Amp CLI Setup:** + +Add via the `amp mcp add`command below + +```bash +amp mcp add playwright -- npx @playwright/mcp@latest +``` + +
+ +
+Antigravity + +Add via the Antigravity settings or by updating your configuration file: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +
+ +
+Claude Code + +Use the Claude Code CLI to add the Playwright MCP server: + +```bash +claude mcp add playwright npx @playwright/mcp@latest +``` +
+ +
+Claude Desktop + +Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user), use the standard config above. + +
+ +
+Cline + +Follow the instruction in the section [Configuring MCP Servers](https://docs.cline.bot/mcp/configuring-mcp-servers) + +**Example: Local Setup** + +Add the following to your [`cline_mcp_settings.json`](https://docs.cline.bot/mcp/configuring-mcp-servers#editing-mcp-settings-files) file: + +```json +{ + "mcpServers": { + "playwright": { + "type": "stdio", + "command": "npx", + "timeout": 30, + "args": [ + "-y", + "@playwright/mcp@latest" + ], + "disabled": false + } + } +} +``` + +
+ +
+Codex + +Use the Codex CLI to add the Playwright MCP server: + +```bash +codex mcp add playwright npx "@playwright/mcp@latest" +``` + +Alternatively, create or edit the configuration file `~/.codex/config.toml` and add: + +```toml +[mcp_servers.playwright] +command = "npx" +args = ["@playwright/mcp@latest"] +``` + +For more information, see the [Codex MCP documentation](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers). + +
+ +
+Copilot + +Use the Copilot CLI to interactively add the Playwright MCP server: + +```bash +/mcp add +``` + +Alternatively, create or edit the configuration file `~/.copilot/mcp-config.json` and add: + +```json +{ + "mcpServers": { + "playwright": { + "type": "local", + "command": "npx", + "tools": [ + "*" + ], + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +For more information, see the [Copilot CLI documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli). + +
+ +
+Cursor + +#### Click the button to install: + +[Install in Cursor](https://cursor.com/en/install-mcp?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D) + +#### Or install manually: + +Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx @playwright/mcp@latest`. You can also verify config or add command like arguments via clicking `Edit`. + +
+ +
+Factory + +Use the Factory CLI to add the Playwright MCP server: + +```bash +droid mcp add playwright "npx @playwright/mcp@latest" +``` + +Alternatively, type `/mcp` within Factory droid to open an interactive UI for managing MCP servers. + +For more information, see the [Factory MCP documentation](https://docs.factory.ai/cli/configuration/mcp). + +
+ +
+Gemini CLI + +Follow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#configure-the-mcp-server-in-settingsjson), use the standard config above. + +
+ +
+Goose + +#### Click the button to install: + +[![Install in Goose](https://block.github.io/goose/img/extension-install-dark.svg)](https://block.github.io/goose/extension?cmd=npx&arg=%40playwright%2Fmcp%40latest&id=playwright&name=Playwright&description=Interact%20with%20web%20pages%20through%20structured%20accessibility%20snapshots%20using%20Playwright) + +#### Or install manually: + +Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx @playwright/mcp`. Click "Add Extension". +
+ +
+Kiro + +[![Add to Kiro](https://kiro.dev/images/add-to-kiro.svg)](https://kiro.dev/launch/mcp/add?name=playwright&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22%40playwright%2Fmcp%40latest%22%5D%7D) + +Follow the MCP Servers [documentation](https://kiro.dev/docs/mcp/). For example in `.kiro/settings/mcp.json`: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` +
+ +
+LM Studio + +#### Click the button to install: + +[![Add MCP Server playwright to LM Studio](https://files.lmstudio.ai/deeplink/mcp-install-light.svg)](https://lmstudio.ai/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJAcGxheXdyaWdodC9tY3BAbGF0ZXN0Il19) + +#### Or install manually: + +Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above. +
+ +
+opencode + +Follow the MCP Servers [documentation](https://opencode.ai/docs/mcp-servers/). For example in `~/.config/opencode/opencode.json`: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "playwright": { + "type": "local", + "command": [ + "npx", + "@playwright/mcp@latest" + ], + "enabled": true + } + } +} + +``` +
+ +
+Qodo Gen + +Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ → Connect more tools → + Add new MCP → Paste the standard config above. + +Click Save. +
+ +
+VS Code + +#### Click the button to install: + +[Install in VS Code](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [Install in VS Code Insiders](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) + +#### Or install manually: + +Follow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server), use the standard config above. You can also install the Playwright MCP server using the VS Code CLI: + +```bash +# For VS Code +code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}' +``` + +After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code. +
+ +
+Warp + +Go to `Settings` -> `AI` -> `Manage MCP Servers` -> `+ Add` to [add an MCP Server](https://docs.warp.dev/knowledge-and-collaboration/mcp#adding-an-mcp-server). Use the standard config above. + +Alternatively, use the slash command `/add-mcp` in the Warp prompt and paste the standard config from above: +```js +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +
+ +
+Windsurf + +Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use the standard config above. + +
+ +### Configuration + +Playwright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `"args"` list: + + + +| Option | Description | +|--------|-------------| +| --allowed-hosts | comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.
*env* `PLAYWRIGHT_MCP_ALLOWED_HOSTS` | +| --allowed-origins | semicolon-separated list of TRUSTED origins to allow the browser to request. Default is to allow all. Important: *does not* serve as a security boundary and *does not* affect redirects.
*env* `PLAYWRIGHT_MCP_ALLOWED_ORIGINS` | +| --allow-unrestricted-file-access | allow access to files outside of the workspace roots. Also allows unrestricted access to file:// URLs. By default access to file system is restricted to workspace root directories (or cwd if no roots are configured) only, and navigation to file:// URLs is blocked.
*env* `PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS` | +| --blocked-origins | semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed. Important: *does not* serve as a security boundary and *does not* affect redirects.
*env* `PLAYWRIGHT_MCP_BLOCKED_ORIGINS` | +| --block-service-workers | block service workers
*env* `PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS` | +| --browser | browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.
*env* `PLAYWRIGHT_MCP_BROWSER` | +| --caps | comma-separated list of additional capabilities to enable, possible values: vision, pdf, devtools.
*env* `PLAYWRIGHT_MCP_CAPS` | +| --cdp-endpoint | CDP endpoint to connect to.
*env* `PLAYWRIGHT_MCP_CDP_ENDPOINT` | +| --cdp-header | CDP headers to send with the connect request, multiple can be specified.
*env* `PLAYWRIGHT_MCP_CDP_HEADER` | +| --cdp-timeout | timeout in milliseconds for connecting to CDP endpoint, defaults to 30000ms
*env* `PLAYWRIGHT_MCP_CDP_TIMEOUT` | +| --codegen | specify the language to use for code generation, possible values: "typescript", "none". Default is "typescript".
*env* `PLAYWRIGHT_MCP_CODEGEN` | +| --config | path to the configuration file.
*env* `PLAYWRIGHT_MCP_CONFIG` | +| --console-level | level of console messages to return: "error", "warning", "info", "debug". Each level includes the messages of more severe levels.
*env* `PLAYWRIGHT_MCP_CONSOLE_LEVEL` | +| --device | device to emulate, for example: "iPhone 15"
*env* `PLAYWRIGHT_MCP_DEVICE` | +| --executable-path | path to the browser executable.
*env* `PLAYWRIGHT_MCP_EXECUTABLE_PATH` | +| --extension | Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.
*env* `PLAYWRIGHT_MCP_EXTENSION` | +| --endpoint | Bound browser endpoint to connect to.
*env* `PLAYWRIGHT_MCP_ENDPOINT` | +| --grant-permissions | List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".
*env* `PLAYWRIGHT_MCP_GRANT_PERMISSIONS` | +| --headless | run browser in headless mode, headed by default
*env* `PLAYWRIGHT_MCP_HEADLESS` | +| --host | host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
*env* `PLAYWRIGHT_MCP_HOST` | +| --ignore-https-errors | ignore https errors
*env* `PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS` | +| --init-page | path to TypeScript file to evaluate on Playwright page object
*env* `PLAYWRIGHT_MCP_INIT_PAGE` | +| --init-script | path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.
*env* `PLAYWRIGHT_MCP_INIT_SCRIPT` | +| --isolated | keep the browser profile in memory, do not save it to disk.
*env* `PLAYWRIGHT_MCP_ISOLATED` | +| --image-responses | whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".
*env* `PLAYWRIGHT_MCP_IMAGE_RESPONSES` | +| --no-sandbox | disable the sandbox for all process types that are normally sandboxed.
*env* `PLAYWRIGHT_MCP_NO_SANDBOX` | +| --output-dir | path to the directory for output files.
*env* `PLAYWRIGHT_MCP_OUTPUT_DIR` | +| --output-mode | whether to save snapshots, console messages, network logs to a file or to the standard output. Can be "file" or "stdout". Default is "stdout".
*env* `PLAYWRIGHT_MCP_OUTPUT_MODE` | +| --port | port to listen on for SSE transport.
*env* `PLAYWRIGHT_MCP_PORT` | +| --proxy-bypass | comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"
*env* `PLAYWRIGHT_MCP_PROXY_BYPASS` | +| --proxy-server | specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
*env* `PLAYWRIGHT_MCP_PROXY_SERVER` | +| --sandbox | enable the sandbox for all process types that are normally not sandboxed.
*env* `PLAYWRIGHT_MCP_SANDBOX` | +| --save-session | Whether to save the Playwright MCP session into the output directory.
*env* `PLAYWRIGHT_MCP_SAVE_SESSION` | +| --secrets | path to a file containing secrets in the dotenv format
*env* `PLAYWRIGHT_MCP_SECRETS` | +| --shared-browser-context | reuse the same browser context between all connected HTTP clients.
*env* `PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT` | +| --snapshot-mode | when taking snapshots for responses, specifies the mode to use. Can be "full" or "none". Default is "full".
*env* `PLAYWRIGHT_MCP_SNAPSHOT_MODE` | +| --storage-state | path to the storage state file for isolated sessions.
*env* `PLAYWRIGHT_MCP_STORAGE_STATE` | +| --test-id-attribute | specify the attribute to use for test ids, defaults to "data-testid"
*env* `PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE` | +| --timeout-action | specify action timeout in milliseconds, defaults to 5000ms
*env* `PLAYWRIGHT_MCP_TIMEOUT_ACTION` | +| --timeout-navigation | specify navigation timeout in milliseconds, defaults to 60000ms
*env* `PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION` | +| --user-agent | specify user agent string
*env* `PLAYWRIGHT_MCP_USER_AGENT` | +| --user-data-dir | path to the user data directory. If not specified, a temporary directory will be created.
*env* `PLAYWRIGHT_MCP_USER_DATA_DIR` | +| --viewport-size | specify browser viewport size in pixels, for example "1280x720"
*env* `PLAYWRIGHT_MCP_VIEWPORT_SIZE` | + + + +### User profile + +You can run Playwright MCP with persistent profile like a regular browser (default), in isolated contexts for testing sessions, or connect to your existing browser using the browser extension. + +**Persistent profile** + +All the logged in information will be stored in the persistent profile, you can delete it between sessions if you'd like to clear the offline state. +Persistent profile is located at the following locations and you can override it with the `--user-data-dir` argument. + +```bash +# Windows +%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-profile + +# macOS +- ~/Library/Caches/ms-playwright/mcp-{channel}-profile + +# Linux +- ~/.cache/ms-playwright/mcp-{channel}-profile +``` + +**Isolated** + +In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser, +the session is closed and all the storage state for this session is lost. You can provide initial storage state +to the browser via the config's `contextOptions` or via the `--storage-state` argument. Learn more about the storage +state [here](https://playwright.dev/docs/auth). + +```js +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--isolated", + "--storage-state={path/to/storage.json}" + ] + } + } +} +``` + +**Browser Extension** + +The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [packages/extension/README.md](packages/extension/README.md) for installation and setup instructions. + +### Initial state + +There are multiple ways to provide the initial state to the browser context or a page. + +For the storage state, you can either: +- Start with a user data directory using the `--user-data-dir` argument. This will persist all browser data between the sessions. +- Start with a storage state file using the `--storage-state` argument. This will load cookies and local storage from the file into an isolated browser context. + +For the page state, you can use: + +- `--init-page` to point to a TypeScript file that will be evaluated on the Playwright page object. This allows you to run arbitrary code to set up the page. + +```ts +// init-page.ts +export default async ({ page }) => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); + await page.setViewportSize({ width: 1280, height: 720 }); +}; +``` + +- `--init-script` to point to a JavaScript file that will be added as an initialization script. The script will be evaluated in every page before any of the page's scripts. +This is useful for overriding browser APIs or setting up the environment. + +```js +// init-script.js +window.isPlaywrightMCP = true; +``` + +### Configuration file + +The Playwright MCP server can be configured using a JSON configuration file. You can specify the configuration file +using the `--config` command line option: + +```bash +npx @playwright/mcp@latest --config path/to/config.json +``` + +
+Configuration file schema + + + +```typescript +{ + /** + * The browser to use. + */ + browser?: { + /** + * The type of browser to use. + */ + browserName?: 'chromium' | 'firefox' | 'webkit'; + + /** + * Keep the browser profile in memory, do not save it to disk. + */ + isolated?: boolean; + + /** + * Path to a user data directory for browser profile persistence. + * Temporary directory is created by default. + */ + userDataDir?: string; + + /** + * Launch options passed to + * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context + * + * This is useful for settings options like `channel`, `headless`, `executablePath`, etc. + */ + launchOptions?: playwright.LaunchOptions; + + /** + * Context options for the browser context. + * + * This is useful for settings options like `viewport`. + */ + contextOptions?: playwright.BrowserContextOptions; + + /** + * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. + */ + cdpEndpoint?: string; + + /** + * CDP headers to send with the connect request. + */ + cdpHeaders?: Record; + + /** + * Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout. + */ + cdpTimeout?: number; + + /** + * Remote endpoint to connect to an existing Playwright server. + */ + remoteEndpoint?: string; + + /** + * Paths to TypeScript files to add as initialization scripts for Playwright page. + */ + initPage?: string[]; + + /** + * Paths to JavaScript files to add as initialization scripts. + * The scripts will be evaluated in every page before any of the page's scripts. + */ + initScript?: string[]; + }, + + /** + * Connect to a running browser instance (Edge/Chrome only). If specified, `browser` + * config is ignored. + * Requires the "Playwright MCP Bridge" browser extension to be installed. + */ + extension?: boolean; + + server?: { + /** + * The port to listen on for SSE or MCP transport. + */ + port?: number; + + /** + * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. + */ + host?: string; + + /** + * The hosts this server is allowed to serve from. Defaults to the host server is bound to. + * This is not for CORS, but rather for the DNS rebinding protection. + */ + allowedHosts?: string[]; + }, + + /** + * List of enabled tool capabilities. Possible values: + * - 'core': Core browser automation features. + * - 'pdf': PDF generation and manipulation. + * - 'vision': Coordinate-based interactions. + * - 'devtools': Developer tools features. + */ + capabilities?: ToolCapability[]; + + /** + * Whether to save the Playwright session into the output directory. + */ + saveSession?: boolean; + + /** + * Reuse the same browser context between all connected HTTP clients. + */ + sharedBrowserContext?: boolean; + + /** + * Secrets are used to replace matching plain text in the tool responses to prevent the LLM + * from accidentally getting sensitive data. It is a convenience and not a security feature, + * make sure to always examine information coming in and from the tool on the client. + */ + secrets?: Record; + + /** + * The directory to save output files. + */ + outputDir?: string; + + console?: { + /** + * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". + */ + level?: 'error' | 'warning' | 'info' | 'debug'; + }, + + network?: { + /** + * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + allowedOrigins?: string[]; + + /** + * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + blockedOrigins?: string[]; + }; + + /** + * Specify the attribute to use for test ids, defaults to "data-testid". + */ + testIdAttribute?: string; + + timeouts?: { + /* + * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms. + */ + action?: number; + + /* + * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms. + */ + navigation?: number; + + /** + * Configures default expect timeout: https://playwright.dev/docs/test-timeouts#expect-timeout. Defaults to 5000ms. + */ + expect?: number; + }; + + /** + * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them. + */ + imageResponses?: 'allow' | 'omit'; + + snapshot?: { + /** + * When taking snapshots for responses, specifies the mode to use. + */ + mode?: 'full' | 'none'; + }; + + /** + * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally + * wandering outside its intended workspace. It is a convenience defense to catch unintended + * file access, not a secure boundary; a deliberate attempt to reach other directories can be + * easily worked around, so always rely on client-level permissions for true security. + */ + allowUnrestrictedFileAccess?: boolean; + + /** + * Specify the language to use for code generation. + */ + codegen?: 'typescript' | 'none'; +} +``` + + + +
+ +### Standalone MCP server + +When running headed browser on system w/o display or from worker processes of the IDEs, +run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable HTTP transport. + +```bash +npx @playwright/mcp@latest --port 8931 +``` + +And then in MCP client config, set the `url` to the HTTP endpoint: + +```js +{ + "mcpServers": { + "playwright": { + "url": "http://localhost:8931/mcp" + } + } +} +``` + +## Security + +Playwright MCP is **not** a security boundary. See [MCP Security Best Practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) for guidance on securing your deployment. + +
+Docker + +**NOTE:** The Docker implementation only supports headless chromium at the moment. + +```js +{ + "mcpServers": { + "playwright": { + "command": "docker", + "args": ["run", "-i", "--rm", "--init", "--pull=always", "mcr.microsoft.com/playwright/mcp"] + } + } +} +``` + +Or If you prefer to run the container as a long-lived service instead of letting the MCP client spawn it, use: + +``` +docker run -d -i --rm --init --pull=always \ + --entrypoint node \ + --name playwright \ + -p 8931:8931 \ + mcr.microsoft.com/playwright/mcp \ + cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0 +``` + +The server will listen on host port **8931** and can be reached by any MCP client. + +You can build the Docker image yourself. + +``` +docker build -t mcr.microsoft.com/playwright/mcp . +``` +
+ +
+Programmatic usage + +```js +import http from 'http'; + +import { createConnection } from '@playwright/mcp'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; + +http.createServer(async (req, res) => { + // ... + + // Creates a headless Playwright MCP server with SSE transport + const connection = await createConnection({ browser: { launchOptions: { headless: true } } }); + const transport = new SSEServerTransport('/messages', res); + await connection.connect(transport); + + // ... +}); +``` +
+ +### Tools + + + +
+Core automation + + + +- **browser_click** + - Title: Click + - Description: Perform click on a web page + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `ref` (string): Exact target element reference from the page snapshot + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available + - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click + - `button` (string, optional): Button to click, defaults to left + - `modifiers` (array, optional): Modifier keys to press + - Read-only: **false** + + + +- **browser_close** + - Title: Close browser + - Description: Close the page + - Parameters: None + - Read-only: **false** + + + +- **browser_console_messages** + - Title: Get console messages + - Description: Returns all console messages + - Parameters: + - `level` (string): Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info". + - `all` (boolean, optional): Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false. + - `filename` (string, optional): Filename to save the console messages to. If not provided, messages are returned as text. + - Read-only: **true** + + + +- **browser_drag** + - Title: Drag mouse + - Description: Perform drag and drop between two elements + - Parameters: + - `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element + - `startRef` (string): Exact source element reference from the page snapshot + - `startSelector` (string, optional): CSS or role selector for the source element, when ref is not available + - `endElement` (string): Human-readable target element description used to obtain the permission to interact with the element + - `endRef` (string): Exact target element reference from the page snapshot + - `endSelector` (string, optional): CSS or role selector for the target element, when ref is not available + - Read-only: **false** + + + +- **browser_evaluate** + - Title: Evaluate JavaScript + - Description: Evaluate JavaScript expression on page or element + - Parameters: + - `function` (string): () => { /* code */ } or (element) => { /* code */ } when element is provided + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `ref` (string, optional): Exact target element reference from the page snapshot + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available. + - `filename` (string, optional): Filename to save the result to. If not provided, result is returned as text. + - Read-only: **false** + + + +- **browser_file_upload** + - Title: Upload files + - Description: Upload one or multiple files + - Parameters: + - `paths` (array, optional): The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled. + - Read-only: **false** + + + +- **browser_fill_form** + - Title: Fill form + - Description: Fill multiple form fields + - Parameters: + - `fields` (array): Fields to fill in + - Read-only: **false** + + + +- **browser_handle_dialog** + - Title: Handle a dialog + - Description: Handle a dialog + - Parameters: + - `accept` (boolean): Whether to accept the dialog. + - `promptText` (string, optional): The text of the prompt in case of a prompt dialog. + - Read-only: **false** + + + +- **browser_hover** + - Title: Hover mouse + - Description: Hover over element on page + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `ref` (string): Exact target element reference from the page snapshot + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available + - Read-only: **false** + + + +- **browser_navigate** + - Title: Navigate to a URL + - Description: Navigate to a URL + - Parameters: + - `url` (string): The URL to navigate to + - Read-only: **false** + + + +- **browser_navigate_back** + - Title: Go back + - Description: Go back to the previous page in the history + - Parameters: None + - Read-only: **false** + + + +- **browser_network_requests** + - Title: List network requests + - Description: Returns all network requests since loading the page + - Parameters: + - `static` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false. + - `requestBody` (boolean): Whether to include request body. Defaults to false. + - `requestHeaders` (boolean): Whether to include request headers. Defaults to false. + - `filter` (string, optional): Only return requests whose URL matches this regexp (e.g. "/api/.*user"). + - `filename` (string, optional): Filename to save the network requests to. If not provided, requests are returned as text. + - Read-only: **true** + + + +- **browser_press_key** + - Title: Press a key + - Description: Press a key on the keyboard + - Parameters: + - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a` + - Read-only: **false** + + + +- **browser_resize** + - Title: Resize browser window + - Description: Resize the browser window + - Parameters: + - `width` (number): Width of the browser window + - `height` (number): Height of the browser window + - Read-only: **false** + + + +- **browser_run_code** + - Title: Run Playwright code + - Description: Run Playwright code snippet + - Parameters: + - `code` (string, optional): A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }` + - `filename` (string, optional): Load code from the specified file. If both code and filename are provided, code will be ignored. + - Read-only: **false** + + + +- **browser_select_option** + - Title: Select option + - Description: Select an option in a dropdown + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `ref` (string): Exact target element reference from the page snapshot + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available + - `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values. + - Read-only: **false** + + + +- **browser_snapshot** + - Title: Page snapshot + - Description: Capture accessibility snapshot of the current page, this is better than screenshot + - Parameters: + - `filename` (string, optional): Save snapshot to markdown file instead of returning it in the response. + - `selector` (string, optional): Element selector of the root element to capture a partial snapshot instead of the whole page + - `depth` (number, optional): Limit the depth of the snapshot tree + - Read-only: **true** + + + +- **browser_take_screenshot** + - Title: Take a screenshot + - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions. + - Parameters: + - `type` (string): Image format for the screenshot. Default is png. + - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory. + - `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too. + - `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too. + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available. + - `fullPage` (boolean, optional): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots. + - Read-only: **true** + + + +- **browser_type** + - Title: Type text + - Description: Type text into editable element + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `ref` (string): Exact target element reference from the page snapshot + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available + - `text` (string): Text to type into the element + - `submit` (boolean, optional): Whether to submit entered text (press Enter after) + - `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once. + - Read-only: **false** + + + +- **browser_wait_for** + - Title: Wait for + - Description: Wait for text to appear or disappear or a specified time to pass + - Parameters: + - `time` (number, optional): The time to wait in seconds + - `text` (string, optional): The text to wait for + - `textGone` (string, optional): The text to wait for to disappear + - Read-only: **false** + +
+ +
+Tab management + + + +- **browser_tabs** + - Title: Manage tabs + - Description: List, create, close, or select a browser tab. + - Parameters: + - `action` (string): Operation to perform + - `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed. + - Read-only: **false** + +
+ +
+Browser installation + +
+ +
+Configuration (opt-in via --caps=config) + + + +- **browser_get_config** + - Title: Get config + - Description: Get the final resolved config after merging CLI options, environment variables and config file. + - Parameters: None + - Read-only: **true** + +
+ +
+Network (opt-in via --caps=network) + + + +- **browser_network_state_set** + - Title: Set network state + - Description: Sets the browser network state to online or offline. When offline, all network requests will fail. + - Parameters: + - `state` (string): Set to "offline" to simulate offline mode, "online" to restore network connectivity + - Read-only: **false** + + + +- **browser_route** + - Title: Mock network requests + - Description: Set up a route to mock network requests matching a URL pattern + - Parameters: + - `pattern` (string): URL pattern to match (e.g., "**/api/users", "**/*.{png,jpg}") + - `status` (number, optional): HTTP status code to return (default: 200) + - `body` (string, optional): Response body (text or JSON string) + - `contentType` (string, optional): Content-Type header (e.g., "application/json", "text/html") + - `headers` (array, optional): Headers to add in "Name: Value" format + - `removeHeaders` (string, optional): Comma-separated list of header names to remove from request + - Read-only: **false** + + + +- **browser_route_list** + - Title: List network routes + - Description: List all active network routes + - Parameters: None + - Read-only: **true** + + + +- **browser_unroute** + - Title: Remove network routes + - Description: Remove network routes matching a pattern (or all routes if no pattern specified) + - Parameters: + - `pattern` (string, optional): URL pattern to unroute (omit to remove all routes) + - Read-only: **false** + +
+ +
+Storage (opt-in via --caps=storage) + + + +- **browser_cookie_clear** + - Title: Clear cookies + - Description: Clear all cookies + - Parameters: None + - Read-only: **false** + + + +- **browser_cookie_delete** + - Title: Delete cookie + - Description: Delete a specific cookie + - Parameters: + - `name` (string): Cookie name to delete + - Read-only: **false** + + + +- **browser_cookie_get** + - Title: Get cookie + - Description: Get a specific cookie by name + - Parameters: + - `name` (string): Cookie name to get + - Read-only: **true** + + + +- **browser_cookie_list** + - Title: List cookies + - Description: List all cookies (optionally filtered by domain/path) + - Parameters: + - `domain` (string, optional): Filter cookies by domain + - `path` (string, optional): Filter cookies by path + - Read-only: **true** + + + +- **browser_cookie_set** + - Title: Set cookie + - Description: Set a cookie with optional flags (domain, path, expires, httpOnly, secure, sameSite) + - Parameters: + - `name` (string): Cookie name + - `value` (string): Cookie value + - `domain` (string, optional): Cookie domain + - `path` (string, optional): Cookie path + - `expires` (number, optional): Cookie expiration as Unix timestamp + - `httpOnly` (boolean, optional): Whether the cookie is HTTP only + - `secure` (boolean, optional): Whether the cookie is secure + - `sameSite` (string, optional): Cookie SameSite attribute + - Read-only: **false** + + + +- **browser_localstorage_clear** + - Title: Clear localStorage + - Description: Clear all localStorage + - Parameters: None + - Read-only: **false** + + + +- **browser_localstorage_delete** + - Title: Delete localStorage item + - Description: Delete a localStorage item + - Parameters: + - `key` (string): Key to delete + - Read-only: **false** + + + +- **browser_localstorage_get** + - Title: Get localStorage item + - Description: Get a localStorage item by key + - Parameters: + - `key` (string): Key to get + - Read-only: **true** + + + +- **browser_localstorage_list** + - Title: List localStorage + - Description: List all localStorage key-value pairs + - Parameters: None + - Read-only: **true** + + + +- **browser_localstorage_set** + - Title: Set localStorage item + - Description: Set a localStorage item + - Parameters: + - `key` (string): Key to set + - `value` (string): Value to set + - Read-only: **false** + + + +- **browser_sessionstorage_clear** + - Title: Clear sessionStorage + - Description: Clear all sessionStorage + - Parameters: None + - Read-only: **false** + + + +- **browser_sessionstorage_delete** + - Title: Delete sessionStorage item + - Description: Delete a sessionStorage item + - Parameters: + - `key` (string): Key to delete + - Read-only: **false** + + + +- **browser_sessionstorage_get** + - Title: Get sessionStorage item + - Description: Get a sessionStorage item by key + - Parameters: + - `key` (string): Key to get + - Read-only: **true** + + + +- **browser_sessionstorage_list** + - Title: List sessionStorage + - Description: List all sessionStorage key-value pairs + - Parameters: None + - Read-only: **true** + + + +- **browser_sessionstorage_set** + - Title: Set sessionStorage item + - Description: Set a sessionStorage item + - Parameters: + - `key` (string): Key to set + - `value` (string): Value to set + - Read-only: **false** + + + +- **browser_set_storage_state** + - Title: Restore storage state + - Description: Restore storage state (cookies, local storage) from a file. This clears existing cookies and local storage before restoring. + - Parameters: + - `filename` (string): Path to the storage state file to restore from + - Read-only: **false** + + + +- **browser_storage_state** + - Title: Save storage state + - Description: Save storage state (cookies, local storage) to a file for later reuse + - Parameters: + - `filename` (string, optional): File name to save the storage state to. Defaults to `storage-state-{timestamp}.json` if not specified. + - Read-only: **true** + +
+ +
+DevTools (opt-in via --caps=devtools) + + + +- **browser_resume** + - Title: Resume paused script execution + - Description: Resume script execution after it was paused. When called with step set to true, execution will pause again before the next action. + - Parameters: + - `step` (boolean, optional): When true, execution will pause again before the next action, allowing step-by-step debugging. + - `location` (string, optional): Pause execution at a specific :, e.g. "example.spec.ts:42". + - Read-only: **false** + + + +- **browser_start_tracing** + - Title: Start tracing + - Description: Start trace recording + - Parameters: None + - Read-only: **true** + + + +- **browser_start_video** + - Title: Start video + - Description: Start video recording + - Parameters: + - `filename` (string, optional): Filename to save the video. + - `size` (object, optional): Video size + - Read-only: **true** + + + +- **browser_stop_tracing** + - Title: Stop tracing + - Description: Stop trace recording + - Parameters: None + - Read-only: **true** + + + +- **browser_stop_video** + - Title: Stop video + - Description: Stop video recording + - Parameters: None + - Read-only: **true** + + + +- **browser_video_chapter** + - Title: Video chapter + - Description: Add a chapter marker to the video recording. Shows a full-screen chapter card with blurred backdrop. + - Parameters: + - `title` (string): Chapter title + - `description` (string, optional): Chapter description + - `duration` (number, optional): Duration in milliseconds to show the chapter card + - Read-only: **true** + +
+ +
+Coordinate-based (opt-in via --caps=vision) + + + +- **browser_mouse_click_xy** + - Title: Click + - Description: Click mouse button at a given position + - Parameters: + - `x` (number): X coordinate + - `y` (number): Y coordinate + - `button` (string, optional): Button to click, defaults to left + - `clickCount` (number, optional): Number of clicks, defaults to 1 + - `delay` (number, optional): Time to wait between mouse down and mouse up in milliseconds, defaults to 0 + - Read-only: **false** + + + +- **browser_mouse_down** + - Title: Press mouse down + - Description: Press mouse down + - Parameters: + - `button` (string, optional): Button to press, defaults to left + - Read-only: **false** + + + +- **browser_mouse_drag_xy** + - Title: Drag mouse + - Description: Drag left mouse button to a given position + - Parameters: + - `startX` (number): Start X coordinate + - `startY` (number): Start Y coordinate + - `endX` (number): End X coordinate + - `endY` (number): End Y coordinate + - Read-only: **false** + + + +- **browser_mouse_move_xy** + - Title: Move mouse + - Description: Move mouse to a given position + - Parameters: + - `x` (number): X coordinate + - `y` (number): Y coordinate + - Read-only: **false** + + + +- **browser_mouse_up** + - Title: Press mouse up + - Description: Press mouse up + - Parameters: + - `button` (string, optional): Button to press, defaults to left + - Read-only: **false** + + + +- **browser_mouse_wheel** + - Title: Scroll mouse wheel + - Description: Scroll mouse wheel + - Parameters: + - `deltaX` (number): X delta + - `deltaY` (number): Y delta + - Read-only: **false** + +
+ +
+PDF generation (opt-in via --caps=pdf) + + + +- **browser_pdf_save** + - Title: Save as PDF + - Description: Save page as PDF + - Parameters: + - `filename` (string, optional): File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory. + - Read-only: **true** + +
+ +
+Test assertions (opt-in via --caps=testing) + + + +- **browser_generate_locator** + - Title: Create locator for element + - Description: Generate locator for the given element to use in tests + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `ref` (string): Exact target element reference from the page snapshot + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available + - Read-only: **true** + + + +- **browser_verify_element_visible** + - Title: Verify element visible + - Description: Verify element is visible on the page + - Parameters: + - `role` (string): ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":` + - `accessibleName` (string): ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"` + - Read-only: **false** + + + +- **browser_verify_list_visible** + - Title: Verify list visible + - Description: Verify list is visible on the page + - Parameters: + - `element` (string): Human-readable list description + - `ref` (string): Exact target element reference that points to the list + - `selector` (string, optional): CSS or role selector for the target list, when "ref" is not available. + - `items` (array): Items to verify + - Read-only: **false** + + + +- **browser_verify_text_visible** + - Title: Verify text visible + - Description: Verify text is visible on the page. Prefer browser_verify_element_visible if possible. + - Parameters: + - `text` (string): TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}` + - Read-only: **false** + + + +- **browser_verify_value** + - Title: Verify value + - Description: Verify element value + - Parameters: + - `type` (string): Type of the element + - `element` (string): Human-readable element description + - `ref` (string): Exact target element reference from the page snapshot + - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available + - `value` (string): Value to verify. For checkbox, use "true" or "false". + - Read-only: **false** + +
+ + + diff --git a/mcps/playwright_mcp/node_modules/@playwright/mcp/cli.js b/mcps/playwright_mcp/node_modules/@playwright/mcp/cli.js new file mode 100755 index 0000000..9f58e91 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/mcp/cli.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { program } = require('playwright-core/lib/utilsBundle'); +const { decorateMCPCommand } = require('playwright-core/lib/tools/mcp/program'); + +if (process.argv.includes('install-browser')) { + const argv = process.argv.map(arg => arg === 'install-browser' ? 'install' : arg); + const { program: mainProgram } = require('playwright-core/lib/cli/program'); + mainProgram.parse(argv); + return; +} + +const packageJSON = require('./package.json'); +const p = program.version('Version ' + packageJSON.version).name('Playwright MCP'); +decorateMCPCommand(p, packageJSON.version) + +void program.parseAsync(process.argv); diff --git a/mcps/playwright_mcp/node_modules/@playwright/mcp/config.d.ts b/mcps/playwright_mcp/node_modules/@playwright/mcp/config.d.ts new file mode 100644 index 0000000..26cc075 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/mcp/config.d.ts @@ -0,0 +1,230 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type * as playwright from '../../..'; + +export type ToolCapability = + 'config' | + 'core' | + 'core-navigation' | + 'core-tabs' | + 'core-input' | + 'core-install' | + 'network' | + 'pdf' | + 'storage' | + 'testing' | + 'vision' | + 'devtools'; + +export type Config = { + /** + * The browser to use. + */ + browser?: { + /** + * The type of browser to use. + */ + browserName?: 'chromium' | 'firefox' | 'webkit'; + + /** + * Keep the browser profile in memory, do not save it to disk. + */ + isolated?: boolean; + + /** + * Path to a user data directory for browser profile persistence. + * Temporary directory is created by default. + */ + userDataDir?: string; + + /** + * Launch options passed to + * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context + * + * This is useful for settings options like `channel`, `headless`, `executablePath`, etc. + */ + launchOptions?: playwright.LaunchOptions; + + /** + * Context options for the browser context. + * + * This is useful for settings options like `viewport`. + */ + contextOptions?: playwright.BrowserContextOptions; + + /** + * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. + */ + cdpEndpoint?: string; + + /** + * CDP headers to send with the connect request. + */ + cdpHeaders?: Record; + + /** + * Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout. + */ + cdpTimeout?: number; + + /** + * Remote endpoint to connect to an existing Playwright server. + */ + remoteEndpoint?: string; + + /** + * Paths to TypeScript files to add as initialization scripts for Playwright page. + */ + initPage?: string[]; + + /** + * Paths to JavaScript files to add as initialization scripts. + * The scripts will be evaluated in every page before any of the page's scripts. + */ + initScript?: string[]; + }, + + /** + * Connect to a running browser instance (Edge/Chrome only). If specified, `browser` + * config is ignored. + * Requires the "Playwright MCP Bridge" browser extension to be installed. + */ + extension?: boolean; + + server?: { + /** + * The port to listen on for SSE or MCP transport. + */ + port?: number; + + /** + * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. + */ + host?: string; + + /** + * The hosts this server is allowed to serve from. Defaults to the host server is bound to. + * This is not for CORS, but rather for the DNS rebinding protection. + */ + allowedHosts?: string[]; + }, + + /** + * List of enabled tool capabilities. Possible values: + * - 'core': Core browser automation features. + * - 'pdf': PDF generation and manipulation. + * - 'vision': Coordinate-based interactions. + * - 'devtools': Developer tools features. + */ + capabilities?: ToolCapability[]; + + /** + * Whether to save the Playwright session into the output directory. + */ + saveSession?: boolean; + + /** + * Reuse the same browser context between all connected HTTP clients. + */ + sharedBrowserContext?: boolean; + + /** + * Secrets are used to replace matching plain text in the tool responses to prevent the LLM + * from accidentally getting sensitive data. It is a convenience and not a security feature, + * make sure to always examine information coming in and from the tool on the client. + */ + secrets?: Record; + + /** + * The directory to save output files. + */ + outputDir?: string; + + console?: { + /** + * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". + */ + level?: 'error' | 'warning' | 'info' | 'debug'; + }, + + network?: { + /** + * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + allowedOrigins?: string[]; + + /** + * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + blockedOrigins?: string[]; + }; + + /** + * Specify the attribute to use for test ids, defaults to "data-testid". + */ + testIdAttribute?: string; + + timeouts?: { + /* + * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms. + */ + action?: number; + + /* + * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms. + */ + navigation?: number; + + /** + * Configures default expect timeout: https://playwright.dev/docs/test-timeouts#expect-timeout. Defaults to 5000ms. + */ + expect?: number; + }; + + /** + * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them. + */ + imageResponses?: 'allow' | 'omit'; + + snapshot?: { + /** + * When taking snapshots for responses, specifies the mode to use. + */ + mode?: 'full' | 'none'; + }; + + /** + * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally + * wandering outside its intended workspace. It is a convenience defense to catch unintended + * file access, not a secure boundary; a deliberate attempt to reach other directories can be + * easily worked around, so always rely on client-level permissions for true security. + */ + allowUnrestrictedFileAccess?: boolean; + + /** + * Specify the language to use for code generation. + */ + codegen?: 'typescript' | 'none'; +}; diff --git a/mcps/playwright_mcp/node_modules/@playwright/mcp/index.d.ts b/mcps/playwright_mcp/node_modules/@playwright/mcp/index.d.ts new file mode 100644 index 0000000..4a2b679 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/mcp/index.d.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { Config } from './config'; +import type { BrowserContext } from 'playwright'; + +export declare function createConnection(config?: Config, contextGetter?: () => Promise): Promise; +export {}; diff --git a/mcps/playwright_mcp/node_modules/@playwright/mcp/index.js b/mcps/playwright_mcp/node_modules/@playwright/mcp/index.js new file mode 100755 index 0000000..eddee70 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/mcp/index.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { createConnection } = require('playwright-core/lib/tools/exports'); +module.exports = { createConnection }; diff --git a/mcps/playwright_mcp/node_modules/@playwright/mcp/package.json b/mcps/playwright_mcp/node_modules/@playwright/mcp/package.json new file mode 100644 index 0000000..af2af60 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/mcp/package.json @@ -0,0 +1,42 @@ +{ + "name": "@playwright/mcp", + "version": "0.0.70", + "description": "Playwright Tools for MCP", + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/playwright-mcp.git" + }, + "homepage": "https://playwright.dev", + "engines": { + "node": ">=18" + }, + "author": { + "name": "Microsoft Corporation" + }, + "license": "Apache-2.0", + "mcpName": "io.github.microsoft/playwright-mcp", + "scripts": { + "lint": "node update-readme.js", + "test": "playwright test", + "ctest": "playwright test --project=chrome", + "ftest": "playwright test --project=firefox", + "wtest": "playwright test --project=webkit", + "dtest": "MCP_IN_DOCKER=1 playwright test --project=chromium-docker", + "build": "echo OK", + "npm-publish": "npm run lint && npm run test && npm publish" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "dependencies": { + "playwright": "1.60.0-alpha-1774999321000", + "playwright-core": "1.60.0-alpha-1774999321000" + }, + "bin": { + "playwright-mcp": "cli.js" + } +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/LICENSE b/mcps/playwright_mcp/node_modules/@playwright/test/LICENSE new file mode 100644 index 0000000..df11237 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Portions Copyright (c) Microsoft Corporation. + Portions Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/NOTICE b/mcps/playwright_mcp/node_modules/@playwright/test/NOTICE new file mode 100644 index 0000000..814ec16 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/NOTICE @@ -0,0 +1,5 @@ +Playwright +Copyright (c) Microsoft Corporation + +This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer), +available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE). diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/README.md b/mcps/playwright_mcp/node_modules/@playwright/test/README.md new file mode 100644 index 0000000..4a44d1f --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/README.md @@ -0,0 +1,170 @@ +# 🎭 Playwright + +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-147.0.7727.15-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-148.0.2-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.4-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) + +## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) + +Playwright is a framework for Web Testing and Automation. It allows testing [Chromium](https://www.chromium.org/Home)1, [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable**, and **fast**. + +| | Linux | macOS | Windows | +| :--- | :---: | :---: | :---: | +| Chromium1 147.0.7727.15 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| WebKit 26.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 148.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | + +Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details. + +Looking for Playwright for [Python](https://playwright.dev/python/docs/intro), [.NET](https://playwright.dev/dotnet/docs/intro), or [Java](https://playwright.dev/java/docs/intro)? + +1 Playwright uses [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing) by default. + +## Installation + +Playwright has its own test runner for end-to-end tests, we call it Playwright Test. + +### Using init command + +The easiest way to get started with Playwright Test is to run the init command. + +```Shell +# Run from your project's root directory +npm init playwright@latest +# Or create a new project +npm init playwright@latest new-project +``` + +This will create a configuration file, optionally add examples, a GitHub Action workflow and a first test example.spec.ts. You can now jump directly to writing assertions section. + +### Manually + +Add dependency and install browsers. + +```Shell +npm i -D @playwright/test +# install supported browsers +npx playwright install +``` + +You can optionally install only selected browsers, see [install browsers](https://playwright.dev/docs/cli#install-browsers) for more details. Or you can install no browsers at all and use existing [browser channels](https://playwright.dev/docs/browsers). + +* [Getting started](https://playwright.dev/docs/intro) +* [API reference](https://playwright.dev/docs/api/class-playwright) + +## Capabilities + +### Resilient • No flaky tests + +**Auto-wait**. Playwright waits for elements to be actionable prior to performing actions. It also has a rich set of introspection events. The combination of the two eliminates the need for artificial timeouts - a primary cause of flaky tests. + +**Web-first assertions**. Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met. + +**Tracing**. Configure test retry strategy, capture execution trace, videos and screenshots to eliminate flakes. + +### No trade-offs • No limits + +Browsers run web content belonging to different origins in different processes. Playwright is aligned with the architecture of the modern browsers and runs tests out-of-process. This makes Playwright free of the typical in-process test runner limitations. + +**Multiple everything**. Test scenarios that span multiple tabs, multiple origins and multiple users. Create scenarios with different contexts for different users and run them against your server, all in one test. + +**Trusted events**. Hover elements, interact with dynamic controls and produce trusted events. Playwright uses real browser input pipeline indistinguishable from the real user. + +Test frames, pierce Shadow DOM. Playwright selectors pierce shadow DOM and allow entering frames seamlessly. + +### Full isolation • Fast execution + +**Browser contexts**. Playwright creates a browser context for each test. Browser context is equivalent to a brand new browser profile. This delivers full test isolation with zero overhead. Creating a new browser context only takes a handful of milliseconds. + +**Log in once**. Save the authentication state of the context and reuse it in all the tests. This bypasses repetitive log-in operations in each test, yet delivers full isolation of independent tests. + +### Powerful Tooling + +**[Codegen](https://playwright.dev/docs/codegen)**. Generate tests by recording your actions. Save them into any language. + +**[Playwright inspector](https://playwright.dev/docs/inspector)**. Inspect page, generate selectors, step through the test execution, see click points and explore execution logs. + +**[Trace Viewer](https://playwright.dev/docs/trace-viewer)**. Capture all the information to investigate the test failure. Playwright trace contains test execution screencast, live DOM snapshots, action explorer, test source and many more. + +Looking for Playwright for [TypeScript](https://playwright.dev/docs/intro), [JavaScript](https://playwright.dev/docs/intro), [Python](https://playwright.dev/python/docs/intro), [.NET](https://playwright.dev/dotnet/docs/intro), or [Java](https://playwright.dev/java/docs/intro)? + +## Examples + +To learn how to run these Playwright Test examples, check out our [getting started docs](https://playwright.dev/docs/intro). + +#### Page screenshot + +This code snippet navigates to Playwright homepage and saves a screenshot. + +```TypeScript +import { test } from '@playwright/test'; + +test('Page Screenshot', async ({ page }) => { + await page.goto('https://playwright.dev/'); + await page.screenshot({ path: `example.png` }); +}); +``` + +#### Mobile and geolocation + +This snippet emulates Mobile Safari on a device at given geolocation, navigates to maps.google.com, performs the action and takes a screenshot. + +```TypeScript +import { test, devices } from '@playwright/test'; + +test.use({ + ...devices['iPhone 13 Pro'], + locale: 'en-US', + geolocation: { longitude: 12.492507, latitude: 41.889938 }, + permissions: ['geolocation'], +}) + +test('Mobile and geolocation', async ({ page }) => { + await page.goto('https://maps.google.com'); + await page.getByText('Your location').click(); + await page.waitForRequest(/.*preview\/pwa/); + await page.screenshot({ path: 'colosseum-iphone.png' }); +}); +``` + +#### Evaluate in browser context + +This code snippet navigates to example.com, and executes a script in the page context. + +```TypeScript +import { test } from '@playwright/test'; + +test('Evaluate in browser context', async ({ page }) => { + await page.goto('https://www.example.com/'); + const dimensions = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio + } + }); + console.log(dimensions); +}); +``` + +#### Intercept network requests + +This code snippet sets up request routing for a page to log all network requests. + +```TypeScript +import { test } from '@playwright/test'; + +test('Intercept network requests', async ({ page }) => { + // Log and continue all network requests + await page.route('**', route => { + console.log(route.request().url()); + route.continue(); + }); + await page.goto('http://todomvc.com'); +}); +``` + +## Resources + +* [Documentation](https://playwright.dev) +* [API reference](https://playwright.dev/docs/api/class-playwright/) +* [Contribution guide](CONTRIBUTING.md) +* [Changelog](https://github.com/microsoft/playwright/releases) diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/cli.js b/mcps/playwright_mcp/node_modules/@playwright/test/cli.js new file mode 100755 index 0000000..e42facb --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/cli.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { program } = require('playwright/lib/program'); +program.parse(process.argv); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/index.d.ts b/mcps/playwright_mcp/node_modules/@playwright/test/index.d.ts new file mode 100644 index 0000000..8d99c91 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/index.d.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from 'playwright/test'; +export { default } from 'playwright/test'; diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/index.js b/mcps/playwright_mcp/node_modules/@playwright/test/index.js new file mode 100644 index 0000000..8536f06 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/index.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = require('playwright/test'); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/index.mjs b/mcps/playwright_mcp/node_modules/@playwright/test/index.mjs new file mode 100644 index 0000000..8d99c91 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/index.mjs @@ -0,0 +1,18 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from 'playwright/test'; +export { default } from 'playwright/test'; diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright new file mode 120000 index 0000000..50992a7 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright @@ -0,0 +1 @@ +../playwright/cli.js \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright-core b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright-core new file mode 120000 index 0000000..08d6c28 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/.bin/playwright-core @@ -0,0 +1 @@ +../playwright-core/cli.js \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/LICENSE b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/LICENSE new file mode 100644 index 0000000..df11237 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Portions Copyright (c) Microsoft Corporation. + Portions Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/NOTICE b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/NOTICE new file mode 100644 index 0000000..814ec16 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/NOTICE @@ -0,0 +1,5 @@ +Playwright +Copyright (c) Microsoft Corporation + +This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer), +available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE). diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/README.md b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/README.md new file mode 100644 index 0000000..422b373 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/README.md @@ -0,0 +1,3 @@ +# playwright-core + +This package contains the no-browser flavor of [Playwright](http://github.com/microsoft/playwright). diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/ThirdPartyNotices.txt b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/ThirdPartyNotices.txt new file mode 100644 index 0000000..8804482 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/ThirdPartyNotices.txt @@ -0,0 +1,3552 @@ +microsoft/playwright-core + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION + +This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. + +- @hono/node-server@1.19.11 (https://github.com/honojs/node-server) +- @modelcontextprotocol/sdk@1.28.0 (https://github.com/modelcontextprotocol/typescript-sdk) +- accepts@2.0.0 (https://github.com/jshttp/accepts) +- agent-base@7.1.4 (https://github.com/TooTallNate/proxy-agents) +- ajv-formats@3.0.1 (https://github.com/ajv-validator/ajv-formats) +- ajv@8.18.0 (https://github.com/ajv-validator/ajv) +- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match) +- body-parser@2.2.2 (https://github.com/expressjs/body-parser) +- brace-expansion@1.1.12 (https://github.com/juliangruber/brace-expansion) +- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32) +- bytes@3.1.2 (https://github.com/visionmedia/bytes.js) +- call-bind-apply-helpers@1.0.2 (https://github.com/ljharb/call-bind-apply-helpers) +- call-bound@1.0.4 (https://github.com/ljharb/call-bound) +- codemirror@5.65.18 (https://github.com/codemirror/CodeMirror) +- colors@1.4.0 (https://github.com/Marak/colors.js) +- commander@13.1.0 (https://github.com/tj/commander.js) +- concat-map@0.0.1 (https://github.com/substack/node-concat-map) +- content-disposition@1.0.1 (https://github.com/jshttp/content-disposition) +- content-type@1.0.5 (https://github.com/jshttp/content-type) +- cookie-signature@1.2.2 (https://github.com/visionmedia/node-cookie-signature) +- cookie@0.7.2 (https://github.com/jshttp/cookie) +- cors@2.8.5 (https://github.com/expressjs/cors) +- cross-spawn@7.0.6 (https://github.com/moxystudio/node-cross-spawn) +- debug@4.3.4 (https://github.com/debug-js/debug) +- debug@4.4.0 (https://github.com/debug-js/debug) +- debug@4.4.3 (https://github.com/debug-js/debug) +- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) +- depd@2.0.0 (https://github.com/dougwilson/nodejs-depd) +- diff@8.0.4 (https://github.com/kpdecker/jsdiff) +- dotenv@16.4.5 (https://github.com/motdotla/dotenv) +- dunder-proto@1.0.1 (https://github.com/es-shims/dunder-proto) +- ee-first@1.1.1 (https://github.com/jonathanong/ee-first) +- encodeurl@2.0.0 (https://github.com/pillarjs/encodeurl) +- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) +- es-define-property@1.0.1 (https://github.com/ljharb/es-define-property) +- es-errors@1.3.0 (https://github.com/ljharb/es-errors) +- es-object-atoms@1.1.1 (https://github.com/ljharb/es-object-atoms) +- escape-html@1.0.3 (https://github.com/component/escape-html) +- etag@1.8.1 (https://github.com/jshttp/etag) +- eventsource-parser@3.0.3 (https://github.com/rexxars/eventsource-parser) +- eventsource@3.0.7 (git://git@github.com/EventSource/eventsource) +- express-rate-limit@8.3.1 (https://github.com/express-rate-limit/express-rate-limit) +- express@5.2.1 (https://github.com/expressjs/express) +- fast-deep-equal@3.1.3 (https://github.com/epoberezkin/fast-deep-equal) +- fast-uri@3.1.0 (https://github.com/fastify/fast-uri) +- finalhandler@2.1.1 (https://github.com/pillarjs/finalhandler) +- forwarded@0.2.0 (https://github.com/jshttp/forwarded) +- fresh@2.0.0 (https://github.com/jshttp/fresh) +- function-bind@1.1.2 (https://github.com/Raynos/function-bind) +- get-intrinsic@1.3.0 (https://github.com/ljharb/get-intrinsic) +- get-proto@1.0.1 (https://github.com/ljharb/get-proto) +- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream) +- gopd@1.2.0 (https://github.com/ljharb/gopd) +- graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs) +- has-symbols@1.1.0 (https://github.com/inspect-js/has-symbols) +- hasown@2.0.2 (https://github.com/inspect-js/hasOwn) +- hono@4.12.7 (https://github.com/honojs/hono) +- http-errors@2.0.1 (https://github.com/jshttp/http-errors) +- https-proxy-agent@7.0.6 (https://github.com/TooTallNate/proxy-agents) +- iconv-lite@0.7.2 (https://github.com/pillarjs/iconv-lite) +- inherits@2.0.4 (https://github.com/isaacs/inherits) +- ini@6.0.0 (https://github.com/npm/ini) +- ip-address@10.1.0 (https://github.com/beaugunderson/ip-address) +- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address) +- ipaddr.js@1.9.1 (https://github.com/whitequark/ipaddr.js) +- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker) +- is-promise@4.0.0 (https://github.com/then/is-promise) +- is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl) +- isexe@2.0.0 (https://github.com/isaacs/isexe) +- jose@6.1.3 (https://github.com/panva/jose) +- jpeg-js@0.4.4 (https://github.com/eugeneware/jpeg-js) +- jsbn@1.1.0 (https://github.com/andyperlitch/jsbn) +- json-schema-traverse@1.0.0 (https://github.com/epoberezkin/json-schema-traverse) +- json-schema-typed@8.0.2 (https://github.com/RemyRylan/json-schema-typed) +- math-intrinsics@1.1.0 (https://github.com/es-shims/math-intrinsics) +- media-typer@1.1.0 (https://github.com/jshttp/media-typer) +- merge-descriptors@2.0.0 (https://github.com/sindresorhus/merge-descriptors) +- mime-db@1.54.0 (https://github.com/jshttp/mime-db) +- mime-types@3.0.2 (https://github.com/jshttp/mime-types) +- mime@3.0.0 (https://github.com/broofa/mime) +- minimatch@3.1.4 (https://github.com/isaacs/minimatch) +- ms@2.1.2 (https://github.com/zeit/ms) +- ms@2.1.3 (https://github.com/vercel/ms) +- negotiator@1.0.0 (https://github.com/jshttp/negotiator) +- object-assign@4.1.1 (https://github.com/sindresorhus/object-assign) +- object-inspect@1.13.4 (https://github.com/inspect-js/object-inspect) +- on-finished@2.4.1 (https://github.com/jshttp/on-finished) +- once@1.4.0 (https://github.com/isaacs/once) +- open@8.4.0 (https://github.com/sindresorhus/open) +- parseurl@1.3.3 (https://github.com/pillarjs/parseurl) +- path-key@3.1.1 (https://github.com/sindresorhus/path-key) +- path-to-regexp@8.3.0 (https://github.com/pillarjs/path-to-regexp) +- pend@1.2.0 (https://github.com/andrewrk/node-pend) +- pkce-challenge@5.0.0 (https://github.com/crouchcd/pkce-challenge) +- pngjs@6.0.0 (https://github.com/lukeapage/pngjs) +- progress@2.0.3 (https://github.com/visionmedia/node-progress) +- proxy-addr@2.0.7 (https://github.com/jshttp/proxy-addr) +- proxy-from-env@2.0.0 (https://github.com/Rob--W/proxy-from-env) +- pump@3.0.2 (https://github.com/mafintosh/pump) +- qs@6.15.0 (https://github.com/ljharb/qs) +- range-parser@1.2.1 (https://github.com/jshttp/range-parser) +- raw-body@3.0.2 (https://github.com/stream-utils/raw-body) +- require-from-string@2.0.2 (https://github.com/floatdrop/require-from-string) +- retry@0.12.0 (https://github.com/tim-kos/node-retry) +- router@2.2.0 (https://github.com/pillarjs/router) +- safer-buffer@2.1.2 (https://github.com/ChALkeR/safer-buffer) +- send@1.2.1 (https://github.com/pillarjs/send) +- serve-static@2.2.1 (https://github.com/expressjs/serve-static) +- setprototypeof@1.2.0 (https://github.com/wesleytodd/setprototypeof) +- shebang-command@2.0.0 (https://github.com/kevva/shebang-command) +- shebang-regex@3.0.0 (https://github.com/sindresorhus/shebang-regex) +- side-channel-list@1.0.0 (https://github.com/ljharb/side-channel-list) +- side-channel-map@1.0.1 (https://github.com/ljharb/side-channel-map) +- side-channel-weakmap@1.0.2 (https://github.com/ljharb/side-channel-weakmap) +- side-channel@1.1.0 (https://github.com/ljharb/side-channel) +- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit) +- smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer) +- socks-proxy-agent@8.0.5 (https://github.com/TooTallNate/proxy-agents) +- socks@2.8.3 (https://github.com/JoshGlazebrook/socks) +- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js) +- statuses@2.0.2 (https://github.com/jshttp/statuses) +- toidentifier@1.0.1 (https://github.com/component/toidentifier) +- type-is@2.0.1 (https://github.com/jshttp/type-is) +- unpipe@1.0.0 (https://github.com/stream-utils/unpipe) +- vary@1.1.2 (https://github.com/jshttp/vary) +- which@2.0.2 (https://github.com/isaacs/node-which) +- wrappy@1.0.2 (https://github.com/npm/wrappy) +- ws@8.17.1 (https://github.com/websockets/ws) +- yaml@2.8.3 (https://github.com/eemeli/yaml) +- yauzl@3.2.1 (https://github.com/thejoshwolfe/yauzl) +- yazl@2.5.1 (https://github.com/thejoshwolfe/yazl) +- zod-to-json-schema@3.25.1 (https://github.com/StefanTerdell/zod-to-json-schema) +- zod@4.3.6 (https://github.com/colinhacks/zod) + +%% @hono/node-server@1.19.11 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2022 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF @hono/node-server@1.19.11 AND INFORMATION + +%% @modelcontextprotocol/sdk@1.28.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Anthropic, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF @modelcontextprotocol/sdk@1.28.0 AND INFORMATION + +%% accepts@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF accepts@2.0.0 AND INFORMATION + +%% agent-base@7.1.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF agent-base@7.1.4 AND INFORMATION + +%% ajv-formats@3.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2020 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ajv-formats@3.0.1 AND INFORMATION + +%% ajv@8.18.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015-2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ajv@8.18.0 AND INFORMATION + +%% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF balanced-match@1.0.2 AND INFORMATION + +%% body-parser@2.2.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF body-parser@2.2.2 AND INFORMATION + +%% brace-expansion@1.1.12 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF brace-expansion@1.1.12 AND INFORMATION + +%% buffer-crc32@0.2.13 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License + +Copyright (c) 2013 Brian J. Brennan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF buffer-crc32@0.2.13 AND INFORMATION + +%% bytes@3.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF bytes@3.1.2 AND INFORMATION + +%% call-bind-apply-helpers@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF call-bind-apply-helpers@1.0.2 AND INFORMATION + +%% call-bound@1.0.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF call-bound@1.0.4 AND INFORMATION + +%% codemirror@5.65.18 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (C) 2017 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF codemirror@5.65.18 AND INFORMATION + +%% colors@1.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Original Library + - Copyright (c) Marak Squires + +Additional Functionality + - Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF colors@1.4.0 AND INFORMATION + +%% commander@13.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF commander@13.1.0 AND INFORMATION + +%% concat-map@0.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF concat-map@0.0.1 AND INFORMATION + +%% content-disposition@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF content-disposition@1.0.1 AND INFORMATION + +%% content-type@1.0.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF content-type@1.0.5 AND INFORMATION + +%% cookie-signature@1.2.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012–2024 LearnBoost and other contributors; + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF cookie-signature@1.2.2 AND INFORMATION + +%% cookie@0.7.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF cookie@0.7.2 AND INFORMATION + +%% cors@2.8.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF cors@2.8.5 AND INFORMATION + +%% cross-spawn@7.0.6 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2018 Made With MOXY Lda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF cross-spawn@7.0.6 AND INFORMATION + +%% debug@4.3.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF debug@4.3.4 AND INFORMATION + +%% debug@4.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF debug@4.4.0 AND INFORMATION + +%% debug@4.4.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF debug@4.4.3 AND INFORMATION + +%% define-lazy-prop@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF define-lazy-prop@2.0.0 AND INFORMATION + +%% depd@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2018 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF depd@2.0.0 AND INFORMATION + +%% diff@8.0.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +BSD 3-Clause License + +Copyright (c) 2009-2015, Kevin Decker +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF diff@8.0.4 AND INFORMATION + +%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Scott Motte +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF dotenv@16.4.5 AND INFORMATION + +%% dunder-proto@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF dunder-proto@1.0.1 AND INFORMATION + +%% ee-first@1.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ee-first@1.1.1 AND INFORMATION + +%% encodeurl@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF encodeurl@2.0.0 AND INFORMATION + +%% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF end-of-stream@1.4.4 AND INFORMATION + +%% es-define-property@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF es-define-property@1.0.1 AND INFORMATION + +%% es-errors@1.3.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF es-errors@1.3.0 AND INFORMATION + +%% es-object-atoms@1.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF es-object-atoms@1.1.1 AND INFORMATION + +%% escape-html@1.0.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF escape-html@1.0.3 AND INFORMATION + +%% etag@1.8.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF etag@1.8.1 AND INFORMATION + +%% eventsource-parser@3.0.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2025 Espen Hovlandsdal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF eventsource-parser@3.0.3 AND INFORMATION + +%% eventsource@3.0.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License + +Copyright (c) EventSource GitHub organisation + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF eventsource@3.0.7 AND INFORMATION + +%% express-rate-limit@8.3.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +# MIT License + +Copyright 2023 Nathan Friedly, Vedant K + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF express-rate-limit@8.3.1 AND INFORMATION + +%% express@5.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF express@5.2.1 AND INFORMATION + +%% fast-deep-equal@3.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF fast-deep-equal@3.1.3 AND INFORMATION + +%% fast-uri@3.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae +Copyright (c) 2021-present The Fastify team +All rights reserved. + +The Fastify team members are listed at https://github.com/fastify/fastify#team. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of any contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * * * + +The complete list of contributors can be found at: +- https://github.com/garycourt/uri-js/graphs/contributors +========================================= +END OF fast-uri@3.1.0 AND INFORMATION + +%% finalhandler@2.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF finalhandler@2.1.1 AND INFORMATION + +%% forwarded@0.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF forwarded@0.2.0 AND INFORMATION + +%% fresh@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF fresh@2.0.0 AND INFORMATION + +%% function-bind@1.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF function-bind@1.1.2 AND INFORMATION + +%% get-intrinsic@1.3.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2020 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF get-intrinsic@1.3.0 AND INFORMATION + +%% get-proto@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2025 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF get-proto@1.0.1 AND INFORMATION + +%% get-stream@5.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF get-stream@5.2.0 AND INFORMATION + +%% gopd@1.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2022 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF gopd@1.2.0 AND INFORMATION + +%% graceful-fs@4.2.10 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF graceful-fs@4.2.10 AND INFORMATION + +%% has-symbols@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2016 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF has-symbols@1.1.0 AND INFORMATION + +%% hasown@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Jordan Harband and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF hasown@2.0.2 AND INFORMATION + +%% hono@4.12.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2021 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF hono@4.12.7 AND INFORMATION + +%% http-errors@2.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF http-errors@2.0.1 AND INFORMATION + +%% https-proxy-agent@7.0.6 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF https-proxy-agent@7.0.6 AND INFORMATION + +%% iconv-lite@0.7.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF iconv-lite@0.7.2 AND INFORMATION + +%% inherits@2.0.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF inherits@2.0.4 AND INFORMATION + +%% ini@6.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF ini@6.0.0 AND INFORMATION + +%% ip-address@10.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (C) 2011 by Beau Gunderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ip-address@10.1.0 AND INFORMATION + +%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (C) 2011 by Beau Gunderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ip-address@9.0.5 AND INFORMATION + +%% ipaddr.js@1.9.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (C) 2011-2017 whitequark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ipaddr.js@1.9.1 AND INFORMATION + +%% is-docker@2.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF is-docker@2.2.1 AND INFORMATION + +%% is-promise@4.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF is-promise@4.0.0 AND INFORMATION + +%% is-wsl@2.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF is-wsl@2.2.0 AND INFORMATION + +%% isexe@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF isexe@2.0.0 AND INFORMATION + +%% jose@6.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF jose@6.1.3 AND INFORMATION + +%% jpeg-js@0.4.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2014, Eugene Ware +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Eugene Ware nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY EUGENE WARE ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL EUGENE WARE BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF jpeg-js@0.4.4 AND INFORMATION + +%% jsbn@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Licensing +--------- + +This software is covered under the following copyright: + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + +Address all questions regarding this license to: + + Tom Wu + tjw@cs.Stanford.EDU +========================================= +END OF jsbn@1.1.0 AND INFORMATION + +%% json-schema-traverse@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF json-schema-traverse@1.0.0 AND INFORMATION + +%% json-schema-typed@8.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +BSD 2-Clause License + +Original source code is copyright (c) 2019-2025 Remy Rylan + + +All JSON Schema documentation and descriptions are copyright (c): + +2009 [draft-0] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2009 [draft-1] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2010 [draft-2] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2010 [draft-3] IETF Trust , Kris Zyp , +Gary Court , and SitePen (USA) . + +2013 [draft-4] IETF Trust ), Francis Galiegue +, Kris Zyp , Gary Court +, and SitePen (USA) . + +2018 [draft-7] IETF Trust , Austin Wright , +Henry Andrews , Geraint Luff , and +Cloudflare, Inc. . + +2019 [draft-2019-09] IETF Trust , Austin Wright +, Henry Andrews , Ben Hutton +, and Greg Dennis . + +2020 [draft-2020-12] IETF Trust , Austin Wright +, Henry Andrews , Ben Hutton +, and Greg Dennis . + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF json-schema-typed@8.0.2 AND INFORMATION + +%% math-intrinsics@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF math-intrinsics@1.1.0 AND INFORMATION + +%% media-typer@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF media-typer@1.1.0 AND INFORMATION + +%% merge-descriptors@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Jonathan Ong +Copyright (c) Douglas Christopher Wilson +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF merge-descriptors@2.0.0 AND INFORMATION + +%% mime-db@1.54.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF mime-db@1.54.0 AND INFORMATION + +%% mime-types@3.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF mime-types@3.0.2 AND INFORMATION + +%% mime@3.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF mime@3.0.0 AND INFORMATION + +%% minimatch@3.1.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF minimatch@3.1.4 AND INFORMATION + +%% ms@2.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2016 Zeit, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ms@2.1.2 AND INFORMATION + +%% ms@2.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2020 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ms@2.1.3 AND INFORMATION + +%% negotiator@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF negotiator@1.0.0 AND INFORMATION + +%% object-assign@4.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF object-assign@4.1.1 AND INFORMATION + +%% object-inspect@1.13.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF object-inspect@1.13.4 AND INFORMATION + +%% on-finished@2.4.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF on-finished@2.4.1 AND INFORMATION + +%% once@1.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF once@1.4.0 AND INFORMATION + +%% open@8.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF open@8.4.0 AND INFORMATION + +%% parseurl@1.3.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF parseurl@1.3.3 AND INFORMATION + +%% path-key@3.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF path-key@3.1.1 AND INFORMATION + +%% path-to-regexp@8.3.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF path-to-regexp@8.3.0 AND INFORMATION + +%% pend@1.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (Expat) + +Copyright (c) 2014 Andrew Kelley + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF pend@1.2.0 AND INFORMATION + +%% pkce-challenge@5.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF pkce-challenge@5.0.0 AND INFORMATION + +%% pngjs@6.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +pngjs2 original work Copyright (c) 2015 Luke Page & Original Contributors +pngjs derived work Copyright (c) 2012 Kuba Niegowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF pngjs@6.0.0 AND INFORMATION + +%% progress@2.0.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2017 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF progress@2.0.3 AND INFORMATION + +%% proxy-addr@2.0.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF proxy-addr@2.0.7 AND INFORMATION + +%% proxy-from-env@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License + +Copyright (C) 2016-2018 Rob Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF proxy-from-env@2.0.0 AND INFORMATION + +%% pump@3.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF pump@3.0.2 AND INFORMATION + +%% qs@6.15.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF qs@6.15.0 AND INFORMATION + +%% range-parser@1.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF raw-body@3.0.2 AND INFORMATION + +%% require-from-string@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF require-from-string@2.0.2 AND INFORMATION + +%% retry@0.12.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011: +Tim Koschützki (tim@debuggable.com) +Felix Geisendörfer (felix@debuggable.com) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +========================================= +END OF retry@0.12.0 AND INFORMATION + +%% router@2.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Roman Shtylman +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF router@2.2.0 AND INFORMATION + +%% safer-buffer@2.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF safer-buffer@2.1.2 AND INFORMATION + +%% send@1.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF send@1.2.1 AND INFORMATION + +%% serve-static@2.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF serve-static@2.2.1 AND INFORMATION + +%% setprototypeof@1.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Wes Todd + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF setprototypeof@1.2.0 AND INFORMATION + +%% shebang-command@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Kevin Mårtensson (github.com/kevva) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF shebang-command@2.0.0 AND INFORMATION + +%% shebang-regex@3.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF shebang-regex@3.0.0 AND INFORMATION + +%% side-channel-list@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel-list@1.0.0 AND INFORMATION + +%% side-channel-map@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel-map@1.0.1 AND INFORMATION + +%% side-channel-weakmap@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel-weakmap@1.0.2 AND INFORMATION + +%% side-channel@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel@1.1.0 AND INFORMATION + +%% signal-exit@3.0.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF signal-exit@3.0.7 AND INFORMATION + +%% smart-buffer@4.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2013-2017 Josh Glazebrook + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF smart-buffer@4.2.0 AND INFORMATION + +%% socks-proxy-agent@8.0.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF socks-proxy-agent@8.0.5 AND INFORMATION + +%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2013 Josh Glazebrook + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF socks@2.8.3 AND INFORMATION + +%% sprintf-js@1.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2007-present, Alexandru Mărășteanu +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this software nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF sprintf-js@1.1.3 AND INFORMATION + +%% statuses@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF statuses@2.0.2 AND INFORMATION + +%% toidentifier@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF toidentifier@1.0.1 AND INFORMATION + +%% type-is@2.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF type-is@2.0.1 AND INFORMATION + +%% unpipe@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF unpipe@1.0.0 AND INFORMATION + +%% vary@1.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF vary@1.1.2 AND INFORMATION + +%% which@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF which@2.0.2 AND INFORMATION + +%% wrappy@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF wrappy@1.0.2 AND INFORMATION + +%% ws@8.17.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF ws@8.17.1 AND INFORMATION + +%% yaml@2.8.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright Eemeli Aro + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +========================================= +END OF yaml@2.8.3 AND INFORMATION + +%% yauzl@3.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Josh Wolfe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF yauzl@3.2.1 AND INFORMATION + +%% yazl@2.5.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Josh Wolfe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF yazl@2.5.1 AND INFORMATION + +%% zod-to-json-schema@3.25.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +ISC License + +Copyright (c) 2020, Stefan Terdell + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF zod-to-json-schema@3.25.1 AND INFORMATION + +%% zod@4.3.6 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF zod@4.3.6 AND INFORMATION + +SUMMARY BEGIN HERE +========================================= +Total Packages: 133 +========================================= +END OF SUMMARY \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_media_pack.ps1 b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_media_pack.ps1 new file mode 100644 index 0000000..6170754 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_media_pack.ps1 @@ -0,0 +1,5 @@ +$osInfo = Get-WmiObject -Class Win32_OperatingSystem +# check if running on Windows Server +if ($osInfo.ProductType -eq 3) { + Install-WindowsFeature Server-Media-Foundation +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_webkit_wsl.ps1 b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_webkit_wsl.ps1 new file mode 100644 index 0000000..ccaaf15 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/install_webkit_wsl.ps1 @@ -0,0 +1,33 @@ +$ErrorActionPreference = 'Stop' + +# This script sets up a WSL distribution that will be used to run WebKit. + +$Distribution = "playwright" +$Username = "pwuser" + +$distributions = (wsl --list --quiet) -split "\r?\n" +if ($distributions -contains $Distribution) { + Write-Host "WSL distribution '$Distribution' already exists. Skipping installation." +} else { + Write-Host "Installing new WSL distribution '$Distribution'..." + $VhdSize = "10GB" + wsl --install -d Ubuntu-24.04 --name $Distribution --no-launch --vhd-size $VhdSize + wsl -d $Distribution -u root adduser --gecos GECOS --disabled-password $Username +} + +$pwshDirname = (Resolve-Path -Path $PSScriptRoot).Path; +$playwrightCoreRoot = Resolve-Path (Join-Path $pwshDirname "..") + +$initScript = @" +if [ ! -f "/home/$Username/node/bin/node" ]; then + mkdir -p /home/$Username/node + curl -fsSL https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz -o /home/$Username/node/node-v22.17.0-linux-x64.tar.xz + tar -xJf /home/$Username/node/node-v22.17.0-linux-x64.tar.xz -C /home/$Username/node --strip-components=1 + sudo -u $Username echo 'export PATH=/home/$Username/node/bin:\`$PATH' >> /home/$Username/.profile +fi +/home/$Username/node/bin/node cli.js install-deps webkit +sudo -u $Username PLAYWRIGHT_SKIP_BROWSER_GC=1 /home/$Username/node/bin/node cli.js install webkit +"@ -replace "\r\n", "`n" + +wsl -d $Distribution --cd $playwrightCoreRoot -u root -- bash -c "$initScript" +Write-Host "Done!" \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh new file mode 100755 index 0000000..0451bda --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old beta if any. +if dpkg --get-selections | grep -q "^google-chrome-beta[[:space:]]*install$" >/dev/null; then + apt-get remove -y google-chrome-beta +fi + +# 2. Update apt lists (needed to install curl and chrome dependencies) +apt-get update + +# 3. Install curl to download chrome +if ! command -v curl >/dev/null; then + apt-get install -y curl +fi + +# 4. download chrome beta from dl.google.com and install it. +cd /tmp +curl -O https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb +apt-get install -y ./google-chrome-beta_current_amd64.deb +rm -rf ./google-chrome-beta_current_amd64.deb +cd - +google-chrome-beta --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh new file mode 100755 index 0000000..617e3b5 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e +set -x + +rm -rf "/Applications/Google Chrome Beta.app" +cd /tmp +curl --retry 3 -o ./googlechromebeta.dmg https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg +hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg +cp -pR "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications +hdiutil detach /Volumes/googlechromebeta.dmg +rm -rf /tmp/googlechromebeta.dmg + +/Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 new file mode 100644 index 0000000..3fbe551 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' + +$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromebetastandaloneenterprise64.msi' + +Write-Host "Downloading Google Chrome Beta" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\google-chrome-beta.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Google Chrome Beta" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Google\\Chrome Beta\\Application\\chrome.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Google Chrome Beta." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh new file mode 100755 index 0000000..78f1d41 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old stable if any. +if dpkg --get-selections | grep -q "^google-chrome[[:space:]]*install$" >/dev/null; then + apt-get remove -y google-chrome +fi + +# 2. Update apt lists (needed to install curl and chrome dependencies) +apt-get update + +# 3. Install curl to download chrome +if ! command -v curl >/dev/null; then + apt-get install -y curl +fi + +# 4. download chrome stable from dl.google.com and install it. +cd /tmp +curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +apt-get install -y ./google-chrome-stable_current_amd64.deb +rm -rf ./google-chrome-stable_current_amd64.deb +cd - +google-chrome --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh new file mode 100755 index 0000000..6aa650a --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -e +set -x + +rm -rf "/Applications/Google Chrome.app" +cd /tmp +curl --retry 3 -o ./googlechrome.dmg https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg +hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg +cp -pR "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications +hdiutil detach /Volumes/googlechrome.dmg +rm -rf /tmp/googlechrome.dmg +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 new file mode 100644 index 0000000..7ca2dba --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' +$url = 'https://dl.google.com/tag/s/dl/chrome/install/googlechromestandaloneenterprise64.msi' + +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\google-chrome.msi" +Write-Host "Downloading Google Chrome" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Google Chrome" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + + +$suffix = "\\Google\\Chrome\\Application\\chrome.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Google Chrome." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh new file mode 100755 index 0000000..a1531a9 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old beta if any. +if dpkg --get-selections | grep -q "^microsoft-edge-beta[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-beta +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-beta + +microsoft-edge-beta --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh new file mode 100755 index 0000000..72ec3e4 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl --retry 3 -o ./msedge_beta.pkg "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_beta.pkg -target / +rm -rf /tmp/msedge_beta.pkg +/Applications/Microsoft\ Edge\ Beta.app/Contents/MacOS/Microsoft\ Edge\ Beta --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 new file mode 100644 index 0000000..cce0d0b --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = 'Stop' +$url = $args[0] + +Write-Host "Downloading Microsoft Edge Beta" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-beta.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge Beta" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge Beta\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge Beta." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh new file mode 100755 index 0000000..7fde34e --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old dev if any. +if dpkg --get-selections | grep -q "^microsoft-edge-dev[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-dev +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-dev + +microsoft-edge-dev --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh new file mode 100755 index 0000000..3376e86 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl --retry 3 -o ./msedge_dev.pkg "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_dev.pkg -target / +rm -rf /tmp/msedge_dev.pkg +/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 new file mode 100644 index 0000000..22e6db8 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = 'Stop' +$url = $args[0] + +Write-Host "Downloading Microsoft Edge Dev" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-dev.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge Dev" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge Dev\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge Dev." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh new file mode 100755 index 0000000..4acb1db --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old stable if any. +if dpkg --get-selections | grep -q "^microsoft-edge-stable[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-stable +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-stable.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-stable + +microsoft-edge-stable --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh new file mode 100755 index 0000000..afcd2f5 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl --retry 3 -o ./msedge_stable.pkg "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_stable.pkg -target / +rm -rf /tmp/msedge_stable.pkg +/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --version diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 new file mode 100644 index 0000000..31fdf51 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' + +$url = $args[0] + +Write-Host "Downloading Microsoft Edge" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-stable.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} \ No newline at end of file diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/browsers.json b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/browsers.json new file mode 100644 index 0000000..aff3102 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/browsers.json @@ -0,0 +1,81 @@ +{ + "comment": "Do not edit this file, use utils/roll_browser.js", + "browsers": [ + { + "name": "chromium", + "revision": "1217", + "installByDefault": true, + "browserVersion": "147.0.7727.15", + "title": "Chrome for Testing" + }, + { + "name": "chromium-headless-shell", + "revision": "1217", + "installByDefault": true, + "browserVersion": "147.0.7727.15", + "title": "Chrome Headless Shell" + }, + { + "name": "chromium-tip-of-tree", + "revision": "1417", + "installByDefault": false, + "browserVersion": "148.0.7755.0", + "title": "Chrome Canary for Testing" + }, + { + "name": "chromium-tip-of-tree-headless-shell", + "revision": "1417", + "installByDefault": false, + "browserVersion": "148.0.7755.0", + "title": "Chrome Canary Headless Shell" + }, + { + "name": "firefox", + "revision": "1511", + "installByDefault": true, + "browserVersion": "148.0.2", + "title": "Firefox" + }, + { + "name": "firefox-beta", + "revision": "1505", + "installByDefault": false, + "browserVersion": "148.0b9", + "title": "Firefox Beta" + }, + { + "name": "webkit", + "revision": "2272", + "installByDefault": true, + "revisionOverrides": { + "mac14": "2251", + "mac14-arm64": "2251", + "debian11-x64": "2105", + "debian11-arm64": "2105", + "ubuntu20.04-x64": "2092", + "ubuntu20.04-arm64": "2092" + }, + "browserVersion": "26.4", + "title": "WebKit" + }, + { + "name": "ffmpeg", + "revision": "1011", + "installByDefault": true, + "revisionOverrides": { + "mac12": "1010", + "mac12-arm64": "1010" + } + }, + { + "name": "winldd", + "revision": "1007", + "installByDefault": false + }, + { + "name": "android", + "revision": "1001", + "installByDefault": false + } + ] +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/cli.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/cli.js new file mode 100755 index 0000000..fb309ea --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/cli.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const { program } = require('./lib/cli/programWithTestStub'); +program.parse(process.argv); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.d.ts b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.d.ts new file mode 100644 index 0000000..97c1493 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.d.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types/types'; diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.js new file mode 100644 index 0000000..d4991d0 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const minimumMajorNodeVersion = 18; +const currentNodeVersion = process.versions.node; +const semver = currentNodeVersion.split('.'); +const [major] = [+semver[0]]; + +if (major < minimumMajorNodeVersion) { + console.error( + 'You are running Node.js ' + + currentNodeVersion + + '.\n' + + `Playwright requires Node.js ${minimumMajorNodeVersion} or higher. \n` + + 'Please update your version of Node.js.' + ); + process.exit(1); +} + +module.exports = require('./lib/inprocess'); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.mjs b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.mjs new file mode 100644 index 0000000..3b3c75b --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/index.mjs @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import playwright from './index.js'; + +export const chromium = playwright.chromium; +export const firefox = playwright.firefox; +export const webkit = playwright.webkit; +export const selectors = playwright.selectors; +export const devices = playwright.devices; +export const errors = playwright.errors; +export const request = playwright.request; +export const _electron = playwright._electron; +export const _android = playwright._android; +export default playwright; diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/androidServerImpl.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/androidServerImpl.js new file mode 100644 index 0000000..568548b --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/androidServerImpl.js @@ -0,0 +1,65 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var androidServerImpl_exports = {}; +__export(androidServerImpl_exports, { + AndroidServerLauncherImpl: () => AndroidServerLauncherImpl +}); +module.exports = __toCommonJS(androidServerImpl_exports); +var import_playwrightServer = require("./remote/playwrightServer"); +var import_playwright = require("./server/playwright"); +var import_crypto = require("./server/utils/crypto"); +var import_utilsBundle = require("./utilsBundle"); +var import_progress = require("./server/progress"); +class AndroidServerLauncherImpl { + async launchServer(options = {}) { + const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true }); + const controller = new import_progress.ProgressController(); + let devices = await controller.run((progress) => playwright.android.devices(progress, { + host: options.adbHost, + port: options.adbPort, + omitDriverInstall: options.omitDriverInstall + })); + if (devices.length === 0) + throw new Error("No devices found"); + if (options.deviceSerialNumber) { + devices = devices.filter((d) => d.serial === options.deviceSerialNumber); + if (devices.length === 0) + throw new Error(`No device with serial number '${options.deviceSerialNumber}' was found`); + } + if (devices.length > 1) + throw new Error(`More than one device found. Please specify deviceSerialNumber`); + const device = devices[0]; + const path = options.wsPath ? options.wsPath.startsWith("/") ? options.wsPath : `/${options.wsPath}` : `/${(0, import_crypto.createGuid)()}`; + const server = new import_playwrightServer.PlaywrightServer({ mode: "launchServer", path, maxConnections: 1, preLaunchedAndroidDevice: device }); + const wsEndpoint = await server.listen(options.port, options.host); + const browserServer = new import_utilsBundle.ws.EventEmitter(); + browserServer.wsEndpoint = () => wsEndpoint; + browserServer.close = () => device.close(); + browserServer.kill = () => device.close(); + device.on("close", () => { + server.close(); + browserServer.emit("close"); + }); + return browserServer; + } +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + AndroidServerLauncherImpl +}); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/bootstrap.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/bootstrap.js new file mode 100644 index 0000000..f00db60 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/bootstrap.js @@ -0,0 +1,77 @@ +"use strict"; +if (process.env.PW_INSTRUMENT_MODULES) { + const Module = require("module"); + const originalLoad = Module._load; + const root = { name: "", selfMs: 0, totalMs: 0, childrenMs: 0, children: [] }; + let current = root; + const stack = []; + Module._load = function(request, _parent, _isMain) { + const node = { name: request, selfMs: 0, totalMs: 0, childrenMs: 0, children: [] }; + current.children.push(node); + stack.push(current); + current = node; + const start = performance.now(); + let result; + try { + result = originalLoad.apply(this, arguments); + } catch (e) { + current = stack.pop(); + current.children.pop(); + throw e; + } + const duration = performance.now() - start; + node.totalMs = duration; + node.selfMs = Math.max(0, duration - node.childrenMs); + current = stack.pop(); + current.childrenMs += duration; + return result; + }; + process.on("exit", () => { + function printTree(node, prefix, isLast, lines2, depth) { + if (node.totalMs < 1 && depth > 0) + return; + const connector = depth === 0 ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "; + const time = `${node.totalMs.toFixed(1).padStart(8)}ms`; + const self = node.children.length ? ` (self: ${node.selfMs.toFixed(1)}ms)` : ""; + lines2.push(`${time} ${prefix}${connector}${node.name}${self}`); + const childPrefix = prefix + (depth === 0 ? "" : isLast ? " " : "\u2502 "); + const sorted2 = node.children.slice().sort((a, b) => b.totalMs - a.totalMs); + for (let i = 0; i < sorted2.length; i++) + printTree(sorted2[i], childPrefix, i === sorted2.length - 1, lines2, depth + 1); + } + let totalModules = 0; + function count(n) { + totalModules++; + n.children.forEach(count); + } + root.children.forEach(count); + const lines = []; + const sorted = root.children.slice().sort((a, b) => b.totalMs - a.totalMs); + for (let i = 0; i < sorted.length; i++) + printTree(sorted[i], "", i === sorted.length - 1, lines, 0); + const totalMs = root.children.reduce((s, c) => s + c.totalMs, 0); + process.stderr.write(` +--- Module load tree: ${totalModules} modules, ${totalMs.toFixed(0)}ms total --- +` + lines.join("\n") + "\n"); + const flat = /* @__PURE__ */ new Map(); + function gather(n) { + const existing = flat.get(n.name); + if (existing) { + existing.selfMs += n.selfMs; + existing.totalMs += n.totalMs; + existing.count++; + } else { + flat.set(n.name, { selfMs: n.selfMs, totalMs: n.totalMs, count: 1 }); + } + n.children.forEach(gather); + } + root.children.forEach(gather); + const top50 = [...flat.entries()].sort((a, b) => b[1].selfMs - a[1].selfMs).slice(0, 50); + const flatLines = top50.map( + ([mod, { selfMs, totalMs: totalMs2, count: count2 }]) => `${selfMs.toFixed(1).padStart(8)}ms self ${totalMs2.toFixed(1).padStart(8)}ms total (x${String(count2).padStart(3)}) ${mod}` + ); + process.stderr.write(` +--- Top 50 modules by self time --- +` + flatLines.join("\n") + "\n"); + }); +} diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/browserServerImpl.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/browserServerImpl.js new file mode 100644 index 0000000..ac2b25d --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/browserServerImpl.js @@ -0,0 +1,120 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var browserServerImpl_exports = {}; +__export(browserServerImpl_exports, { + BrowserServerLauncherImpl: () => BrowserServerLauncherImpl +}); +module.exports = __toCommonJS(browserServerImpl_exports); +var import_playwrightServer = require("./remote/playwrightServer"); +var import_helper = require("./server/helper"); +var import_playwright = require("./server/playwright"); +var import_crypto = require("./server/utils/crypto"); +var import_debug = require("./server/utils/debug"); +var import_stackTrace = require("./utils/isomorphic/stackTrace"); +var import_time = require("./utils/isomorphic/time"); +var import_utilsBundle = require("./utilsBundle"); +var validatorPrimitives = __toESM(require("./protocol/validatorPrimitives")); +var import_progress = require("./server/progress"); +class BrowserServerLauncherImpl { + constructor(browserName) { + this._browserName = browserName; + } + async launchServer(options = {}) { + const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true }); + const metadata = { id: "", startTime: 0, endTime: 0, type: "Internal", method: "", params: {}, log: [], internal: true }; + const validatorContext = { + tChannelImpl: (names, arg, path2) => { + throw new validatorPrimitives.ValidationError(`${path2}: channels are not expected in launchServer`); + }, + binary: "buffer", + isUnderTest: import_debug.isUnderTest + }; + let launchOptions = { + ...options, + ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0, + ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), + env: options.env ? envObjectToArray(options.env) : void 0, + timeout: options.timeout ?? import_time.DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT + }; + let browser; + try { + const controller = new import_progress.ProgressController(metadata); + browser = await controller.run(async (progress) => { + if (options._userDataDir !== void 0) { + const validator = validatorPrimitives.scheme["BrowserTypeLaunchPersistentContextParams"]; + launchOptions = validator({ ...launchOptions, userDataDir: options._userDataDir }, "", validatorContext); + const context = await playwright[this._browserName].launchPersistentContext(progress, options._userDataDir, launchOptions); + return context._browser; + } else { + const validator = validatorPrimitives.scheme["BrowserTypeLaunchParams"]; + launchOptions = validator(launchOptions, "", validatorContext); + return await playwright[this._browserName].launch(progress, launchOptions, toProtocolLogger(options.logger)); + } + }); + } catch (e) { + const log = import_helper.helper.formatBrowserLogs(metadata.log); + (0, import_stackTrace.rewriteErrorMessage)(e, `${e.message} Failed to launch browser.${log}`); + throw e; + } + const path = options.wsPath ? options.wsPath.startsWith("/") ? options.wsPath : `/${options.wsPath}` : `/${(0, import_crypto.createGuid)()}`; + const server = new import_playwrightServer.PlaywrightServer({ mode: options._sharedBrowser ? "launchServerShared" : "launchServer", path, maxConnections: Infinity, preLaunchedBrowser: browser }); + const wsEndpoint = await server.listen(options.port, options.host); + const browserServer = new import_utilsBundle.ws.EventEmitter(); + browserServer.process = () => browser.options.browserProcess.process; + browserServer.wsEndpoint = () => wsEndpoint; + browserServer.close = () => browser.options.browserProcess.close(); + browserServer[Symbol.asyncDispose] = browserServer.close; + browserServer.kill = () => browser.options.browserProcess.kill(); + browserServer._disconnectForTest = () => server.close(); + browserServer._userDataDirForTest = browser._userDataDirForTest; + browser.options.browserProcess.onclose = (exitCode, signal) => { + server.close(); + browserServer.emit("close", exitCode, signal); + }; + return browserServer; + } +} +function toProtocolLogger(logger) { + return logger ? (direction, message) => { + if (logger.isEnabled("protocol", "verbose")) + logger.log("protocol", "verbose", (direction === "send" ? "SEND \u25BA " : "\u25C0 RECV ") + JSON.stringify(message), [], {}); + } : void 0; +} +function envObjectToArray(env) { + const result = []; + for (const name in env) { + if (!Object.is(env[name], void 0)) + result.push({ name, value: String(env[name]) }); + } + return result; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + BrowserServerLauncherImpl +}); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/browserActions.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/browserActions.js new file mode 100644 index 0000000..2a00914 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/browserActions.js @@ -0,0 +1,308 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var browserActions_exports = {}; +__export(browserActions_exports, { + codegen: () => codegen, + open: () => open, + pdf: () => pdf, + screenshot: () => screenshot +}); +module.exports = __toCommonJS(browserActions_exports); +var import_fs = __toESM(require("fs")); +var import_os = __toESM(require("os")); +var import_path = __toESM(require("path")); +var playwright = __toESM(require("../..")); +var import_utils = require("../utils"); +var import_utilsBundle = require("../utilsBundle"); +async function launchContext(options, extraOptions) { + validateOptions(options); + const browserType = lookupBrowserType(options); + const launchOptions = extraOptions; + if (options.channel) + launchOptions.channel = options.channel; + launchOptions.handleSIGINT = false; + const contextOptions = ( + // Copy the device descriptor since we have to compare and modify the options. + options.device ? { ...playwright.devices[options.device] } : {} + ); + if (!extraOptions.headless) + contextOptions.deviceScaleFactor = import_os.default.platform() === "darwin" ? 2 : 1; + if (browserType.name() === "webkit" && process.platform === "linux") { + delete contextOptions.hasTouch; + delete contextOptions.isMobile; + } + if (contextOptions.isMobile && browserType.name() === "firefox") + contextOptions.isMobile = void 0; + if (options.blockServiceWorkers) + contextOptions.serviceWorkers = "block"; + if (options.proxyServer) { + launchOptions.proxy = { + server: options.proxyServer + }; + if (options.proxyBypass) + launchOptions.proxy.bypass = options.proxyBypass; + } + if (options.viewportSize) { + try { + const [width, height] = options.viewportSize.split(",").map((n) => +n); + if (isNaN(width) || isNaN(height)) + throw new Error("bad values"); + contextOptions.viewport = { width, height }; + } catch (e) { + throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"'); + } + } + if (options.geolocation) { + try { + const [latitude, longitude] = options.geolocation.split(",").map((n) => parseFloat(n.trim())); + contextOptions.geolocation = { + latitude, + longitude + }; + } catch (e) { + throw new Error('Invalid geolocation format, should be "lat,long". For example --geolocation="37.819722,-122.478611"'); + } + contextOptions.permissions = ["geolocation"]; + } + if (options.userAgent) + contextOptions.userAgent = options.userAgent; + if (options.lang) + contextOptions.locale = options.lang; + if (options.colorScheme) + contextOptions.colorScheme = options.colorScheme; + if (options.timezone) + contextOptions.timezoneId = options.timezone; + if (options.loadStorage) + contextOptions.storageState = options.loadStorage; + if (options.ignoreHttpsErrors) + contextOptions.ignoreHTTPSErrors = true; + if (options.saveHar) { + contextOptions.recordHar = { path: import_path.default.resolve(process.cwd(), options.saveHar), mode: "minimal" }; + if (options.saveHarGlob) + contextOptions.recordHar.urlFilter = options.saveHarGlob; + contextOptions.serviceWorkers = "block"; + } + let browser; + let context; + if (options.userDataDir) { + context = await browserType.launchPersistentContext(options.userDataDir, { ...launchOptions, ...contextOptions }); + browser = context.browser(); + } else { + browser = await browserType.launch(launchOptions); + context = await browser.newContext(contextOptions); + } + let closingBrowser = false; + async function closeBrowser() { + if (closingBrowser) + return; + closingBrowser = true; + if (options.saveStorage) + await context.storageState({ path: options.saveStorage }).catch((e) => null); + if (options.saveHar) + await context.close(); + await browser.close(); + } + context.on("page", (page) => { + page.on("dialog", () => { + }); + page.on("close", () => { + const hasPage = browser.contexts().some((context2) => context2.pages().length > 0); + if (hasPage) + return; + closeBrowser().catch(() => { + }); + }); + }); + process.on("SIGINT", async () => { + await closeBrowser(); + (0, import_utils.gracefullyProcessExitDoNotHang)(130); + }); + const timeout = options.timeout ? parseInt(options.timeout, 10) : 0; + context.setDefaultTimeout(timeout); + context.setDefaultNavigationTimeout(timeout); + delete launchOptions.headless; + delete launchOptions.executablePath; + delete launchOptions.handleSIGINT; + delete contextOptions.deviceScaleFactor; + return { browser, browserName: browserType.name(), context, contextOptions, launchOptions, closeBrowser }; +} +async function openPage(context, url) { + let page = context.pages()[0]; + if (!page) + page = await context.newPage(); + if (url) { + if (import_fs.default.existsSync(url)) + url = "file://" + import_path.default.resolve(url); + else if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:") && !url.startsWith("data:")) + url = "http://" + url; + await page.goto(url); + } + return page; +} +async function open(options, url) { + const { context } = await launchContext(options, { headless: !!process.env.PWTEST_CLI_HEADLESS, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH }); + await context._exposeConsoleApi(); + await openPage(context, url); +} +async function codegen(options, url) { + const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options; + const tracesDir = import_path.default.join(import_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`); + const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, { + headless: !!process.env.PWTEST_CLI_HEADLESS, + executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH, + tracesDir + }); + const donePromise = new import_utils.ManualPromise(); + maybeSetupTestHooks(browser, closeBrowser, donePromise); + import_utilsBundle.dotenv.config({ path: "playwright.env" }); + await context._enableRecorder({ + language, + launchOptions, + contextOptions, + device: options.device, + saveStorage: options.saveStorage, + mode: "recording", + testIdAttributeName, + outputFile: outputFile ? import_path.default.resolve(outputFile) : void 0, + handleSIGINT: false + }); + await openPage(context, url); + donePromise.resolve(); +} +async function maybeSetupTestHooks(browser, closeBrowser, donePromise) { + if (!process.env.PWTEST_CLI_IS_UNDER_TEST) + return; + const logs = []; + require("playwright-core/lib/utilsBundle").debug.log = (...args) => { + const line = require("util").format(...args) + "\n"; + logs.push(line); + process.stderr.write(line); + }; + browser.on("disconnected", () => { + const hasCrashLine = logs.some((line) => line.includes("process did exit:") && !line.includes("process did exit: exitCode=0, signal=null")); + if (hasCrashLine) { + process.stderr.write("Detected browser crash.\n"); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); + } + }); + const close = async () => { + await donePromise; + await closeBrowser(); + }; + if (process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT) { + setTimeout(close, +process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT); + return; + } + let stdin = ""; + process.stdin.on("data", (data) => { + stdin += data.toString(); + if (stdin.startsWith("exit")) { + process.stdin.destroy(); + close(); + } + }); +} +async function waitForPage(page, captureOptions) { + if (captureOptions.waitForSelector) { + console.log(`Waiting for selector ${captureOptions.waitForSelector}...`); + await page.waitForSelector(captureOptions.waitForSelector); + } + if (captureOptions.waitForTimeout) { + console.log(`Waiting for timeout ${captureOptions.waitForTimeout}...`); + await page.waitForTimeout(parseInt(captureOptions.waitForTimeout, 10)); + } +} +async function screenshot(options, captureOptions, url, path2) { + const { context } = await launchContext(options, { headless: true }); + console.log("Navigating to " + url); + const page = await openPage(context, url); + await waitForPage(page, captureOptions); + console.log("Capturing screenshot into " + path2); + await page.screenshot({ path: path2, fullPage: !!captureOptions.fullPage }); + await page.close(); +} +async function pdf(options, captureOptions, url, path2) { + if (options.browser !== "chromium") + throw new Error("PDF creation is only working with Chromium"); + const { context } = await launchContext({ ...options, browser: "chromium" }, { headless: true }); + console.log("Navigating to " + url); + const page = await openPage(context, url); + await waitForPage(page, captureOptions); + console.log("Saving as pdf into " + path2); + await page.pdf({ path: path2, format: captureOptions.paperFormat }); + await page.close(); +} +function lookupBrowserType(options) { + let name = options.browser; + if (options.device) { + const device = playwright.devices[options.device]; + name = device.defaultBrowserType; + } + let browserType; + switch (name) { + case "chromium": + browserType = playwright.chromium; + break; + case "webkit": + browserType = playwright.webkit; + break; + case "firefox": + browserType = playwright.firefox; + break; + case "cr": + browserType = playwright.chromium; + break; + case "wk": + browserType = playwright.webkit; + break; + case "ff": + browserType = playwright.firefox; + break; + } + if (browserType) + return browserType; + import_utilsBundle.program.help(); +} +function validateOptions(options) { + if (options.device && !(options.device in playwright.devices)) { + const lines = [`Device descriptor not found: '${options.device}', available devices are:`]; + for (const name in playwright.devices) + lines.push(` "${name}"`); + throw new Error(lines.join("\n")); + } + if (options.colorScheme && !["light", "dark"].includes(options.colorScheme)) + throw new Error('Invalid color scheme, should be one of "light", "dark"'); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + codegen, + open, + pdf, + screenshot +}); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/driver.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/driver.js new file mode 100644 index 0000000..c96c3f6 --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/driver.js @@ -0,0 +1,98 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var driver_exports = {}; +__export(driver_exports, { + launchBrowserServer: () => launchBrowserServer, + printApiJson: () => printApiJson, + runDriver: () => runDriver, + runServer: () => runServer +}); +module.exports = __toCommonJS(driver_exports); +var import_fs = __toESM(require("fs")); +var playwright = __toESM(require("../..")); +var import_pipeTransport = require("../server/utils/pipeTransport"); +var import_playwrightServer = require("../remote/playwrightServer"); +var import_server = require("../server"); +var import_processLauncher = require("../server/utils/processLauncher"); +function printApiJson() { + console.log(JSON.stringify(require("../../api.json"))); +} +function runDriver() { + const dispatcherConnection = new import_server.DispatcherConnection(); + new import_server.RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => { + const playwright2 = (0, import_server.createPlaywright)({ sdkLanguage }); + return new import_server.PlaywrightDispatcher(rootScope, playwright2); + }); + const transport = new import_pipeTransport.PipeTransport(process.stdout, process.stdin); + transport.onmessage = (message) => dispatcherConnection.dispatch(JSON.parse(message)); + const isJavaScriptLanguageBinding = !process.env.PW_LANG_NAME || process.env.PW_LANG_NAME === "javascript"; + const replacer = !isJavaScriptLanguageBinding && String.prototype.toWellFormed ? (key, value) => { + if (typeof value === "string") + return value.toWellFormed(); + return value; + } : void 0; + dispatcherConnection.onmessage = (message) => transport.send(JSON.stringify(message, replacer)); + transport.onclose = () => { + dispatcherConnection.onmessage = () => { + }; + (0, import_processLauncher.gracefullyProcessExitDoNotHang)(0); + }; + process.on("SIGINT", () => { + }); +} +async function runServer(options) { + const { + port, + host, + path = "/", + maxConnections = Infinity, + extension, + artifactsDir + } = options; + const server = new import_playwrightServer.PlaywrightServer({ mode: extension ? "extension" : "default", path, maxConnections, artifactsDir }); + const wsEndpoint = await server.listen(port, host); + process.on("exit", () => server.close().catch(console.error)); + console.log("Listening on " + wsEndpoint); + process.stdin.on("close", () => (0, import_processLauncher.gracefullyProcessExitDoNotHang)(0)); +} +async function launchBrowserServer(browserName, configFile) { + let options = {}; + if (configFile) + options = JSON.parse(import_fs.default.readFileSync(configFile).toString()); + const browserType = playwright[browserName]; + const server = await browserType.launchServer(options); + console.log(server.wsEndpoint()); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + launchBrowserServer, + printApiJson, + runDriver, + runServer +}); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/installActions.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/installActions.js new file mode 100644 index 0000000..eddcf4a --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/installActions.js @@ -0,0 +1,171 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var installActions_exports = {}; +__export(installActions_exports, { + installBrowsers: () => installBrowsers, + installDeps: () => installDeps, + markDockerImage: () => markDockerImage, + registry: () => import_server.registry, + uninstallBrowsers: () => uninstallBrowsers +}); +module.exports = __toCommonJS(installActions_exports); +var import_path = __toESM(require("path")); +var import_server = require("../server"); +var import_utils = require("../utils"); +var import_utils2 = require("../utils"); +var import_ascii = require("../server/utils/ascii"); +function printInstalledBrowsers(browsers) { + const browserPaths = /* @__PURE__ */ new Set(); + for (const browser of browsers) + browserPaths.add(browser.browserPath); + console.log(` Browsers:`); + for (const browserPath of [...browserPaths].sort()) + console.log(` ${browserPath}`); + console.log(` References:`); + const references = /* @__PURE__ */ new Set(); + for (const browser of browsers) + references.add(browser.referenceDir); + for (const reference of [...references].sort()) + console.log(` ${reference}`); +} +function printGroupedByPlaywrightVersion(browsers) { + const dirToVersion = /* @__PURE__ */ new Map(); + for (const browser of browsers) { + if (dirToVersion.has(browser.referenceDir)) + continue; + const packageJSON = require(import_path.default.join(browser.referenceDir, "package.json")); + const version = packageJSON.version; + dirToVersion.set(browser.referenceDir, version); + } + const groupedByPlaywrightMinorVersion = /* @__PURE__ */ new Map(); + for (const browser of browsers) { + const version = dirToVersion.get(browser.referenceDir); + let entries = groupedByPlaywrightMinorVersion.get(version); + if (!entries) { + entries = []; + groupedByPlaywrightMinorVersion.set(version, entries); + } + entries.push(browser); + } + const sortedVersions = [...groupedByPlaywrightMinorVersion.keys()].sort((a, b) => { + const aComponents = a.split("."); + const bComponents = b.split("."); + const aMajor = parseInt(aComponents[0], 10); + const bMajor = parseInt(bComponents[0], 10); + if (aMajor !== bMajor) + return aMajor - bMajor; + const aMinor = parseInt(aComponents[1], 10); + const bMinor = parseInt(bComponents[1], 10); + if (aMinor !== bMinor) + return aMinor - bMinor; + return aComponents.slice(2).join(".").localeCompare(bComponents.slice(2).join(".")); + }); + for (const version of sortedVersions) { + console.log(` +Playwright version: ${version}`); + printInstalledBrowsers(groupedByPlaywrightMinorVersion.get(version)); + } +} +async function markDockerImage(dockerImageNameTemplate) { + (0, import_utils2.assert)(dockerImageNameTemplate, "dockerImageNameTemplate is required"); + await (0, import_server.writeDockerVersion)(dockerImageNameTemplate); +} +async function installBrowsers(args, options) { + if ((0, import_utils.isLikelyNpxGlobal)()) { + console.error((0, import_ascii.wrapInASCIIBox)([ + `WARNING: It looks like you are running 'npx playwright install' without first`, + `installing your project's dependencies.`, + ``, + `To avoid unexpected behavior, please install your dependencies first, and`, + `then run Playwright's install command:`, + ``, + ` npm install`, + ` npx playwright install`, + ``, + `If your project does not yet depend on Playwright, first install the`, + `applicable npm package (most commonly @playwright/test), and`, + `then run Playwright's install command to download the browsers:`, + ``, + ` npm install @playwright/test`, + ` npx playwright install`, + `` + ].join("\n"), 1)); + } + if (options.shell === false && options.onlyShell) + throw new Error(`Only one of --no-shell and --only-shell can be specified`); + const shell = options.shell === false ? "no" : options.onlyShell ? "only" : void 0; + const executables = import_server.registry.resolveBrowsers(args, { shell }); + if (options.withDeps) + await import_server.registry.installDeps(executables, !!options.dryRun); + if (options.dryRun && options.list) + throw new Error(`Only one of --dry-run and --list can be specified`); + if (options.dryRun) { + for (const executable of executables) { + console.log(import_server.registry.calculateDownloadTitle(executable)); + console.log(` Install location: ${executable.directory ?? ""}`); + if (executable.downloadURLs?.length) { + const [url, ...fallbacks] = executable.downloadURLs; + console.log(` Download url: ${url}`); + for (let i = 0; i < fallbacks.length; ++i) + console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`); + } + console.log(``); + } + } else if (options.list) { + const browsers = await import_server.registry.listInstalledBrowsers(); + printGroupedByPlaywrightVersion(browsers); + } else { + await import_server.registry.install(executables, { force: options.force }); + await import_server.registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || "javascript").catch((e) => { + e.name = "Playwright Host validation warning"; + console.error(e); + }); + } +} +async function uninstallBrowsers(options) { + delete process.env.PLAYWRIGHT_SKIP_BROWSER_GC; + await import_server.registry.uninstall(!!options.all).then(({ numberOfBrowsersLeft }) => { + if (!options.all && numberOfBrowsersLeft > 0) { + console.log("Successfully uninstalled Playwright browsers for the current Playwright installation."); + console.log(`There are still ${numberOfBrowsersLeft} browsers left, used by other Playwright installations. +To uninstall Playwright browsers for all installations, re-run with --all flag.`); + } + }); +} +async function installDeps(args, options) { + await import_server.registry.installDeps(import_server.registry.resolveBrowsers(args, {}), !!options.dryRun); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + installBrowsers, + installDeps, + markDockerImage, + registry, + uninstallBrowsers +}); diff --git a/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/program.js b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/program.js new file mode 100644 index 0000000..39b59dd --- /dev/null +++ b/mcps/playwright_mcp/node_modules/@playwright/test/node_modules/playwright-core/lib/cli/program.js @@ -0,0 +1,225 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var program_exports = {}; +__export(program_exports, { + program: () => import_utilsBundle2.program +}); +module.exports = __toCommonJS(program_exports); +var import_bootstrap = require("../bootstrap"); +var import_utils = require("../utils"); +var import_traceCli = require("../tools/trace/traceCli"); +var import_utilsBundle = require("../utilsBundle"); +var import_utilsBundle2 = require("../utilsBundle"); +const packageJSON = require("../../package.json"); +import_utilsBundle.program.version("Version " + (process.env.PW_CLI_DISPLAY_VERSION || packageJSON.version)).name(buildBasePlaywrightCLICommand(process.env.PW_LANG_NAME)); +import_utilsBundle.program.command("mark-docker-image [dockerImageNameTemplate]", { hidden: true }).description("mark docker image").allowUnknownOption(true).action(async function(dockerImageNameTemplate) { + const { markDockerImage } = require("./installActions"); + markDockerImage(dockerImageNameTemplate).catch(logErrorAndExit); +}); +commandWithOpenOptions("open [url]", "open page in browser specified via -b, --browser", []).action(async function(url, options) { + const { open } = require("./browserActions"); + open(options, url).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ open + $ open -b webkit https://example.com`); +commandWithOpenOptions( + "codegen [url]", + "open page and generate code for user actions", + [ + ["-o, --output ", "saves the generated script to a file"], + ["--target ", `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()], + ["--test-id-attribute ", "use the specified attribute to generate data test ID selectors"] + ] +).action(async function(url, options) { + const { codegen } = require("./browserActions"); + await codegen(options, url); +}).addHelpText("afterAll", ` +Examples: + + $ codegen + $ codegen --target=python + $ codegen -b webkit https://example.com`); +import_utilsBundle.program.command("install [browser...]").description("ensure browsers necessary for this version of Playwright are installed").option("--with-deps", "install system dependencies for browsers").option("--dry-run", "do not execute installation, only print information").option("--list", "prints list of browsers from all playwright installations").option("--force", "force reinstall of already installed browsers").option("--only-shell", "only install headless shell when installing chromium").option("--no-shell", "do not install chromium headless shell").action(async function(args, options) { + try { + const { installBrowsers } = require("./installActions"); + await installBrowsers(args, options); + } catch (e) { + console.log(`Failed to install browsers +${e}`); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); + } +}).addHelpText("afterAll", ` + +Examples: + - $ install + Install default browsers. + + - $ install chrome firefox + Install custom browsers, supports chromium, firefox, webkit, chromium-headless-shell.`); +import_utilsBundle.program.command("uninstall").description("Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.").option("--all", "Removes all browsers used by any Playwright installation from the system.").action(async (options) => { + const { uninstallBrowsers } = require("./installActions"); + uninstallBrowsers(options).catch(logErrorAndExit); +}); +import_utilsBundle.program.command("install-deps [browser...]").description("install dependencies necessary to run browsers (will ask for sudo permissions)").option("--dry-run", "Do not execute installation commands, only print them").action(async function(args, options) { + try { + const { installDeps } = require("./installActions"); + await installDeps(args, options); + } catch (e) { + console.log(`Failed to install browser dependencies +${e}`); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); + } +}).addHelpText("afterAll", ` +Examples: + - $ install-deps + Install dependencies for default browsers. + + - $ install-deps chrome firefox + Install dependencies for specific browsers, supports chromium, firefox, webkit, chromium-headless-shell.`); +const browsers = [ + { alias: "cr", name: "Chromium", type: "chromium" }, + { alias: "ff", name: "Firefox", type: "firefox" }, + { alias: "wk", name: "WebKit", type: "webkit" } +]; +for (const { alias, name, type } of browsers) { + commandWithOpenOptions(`${alias} [url]`, `open page in ${name}`, []).action(async function(url, options) { + const { open } = require("./browserActions"); + open({ ...options, browser: type }, url).catch(logErrorAndExit); + }).addHelpText("afterAll", ` +Examples: + + $ ${alias} https://example.com`); +} +commandWithOpenOptions( + "screenshot ", + "capture a page screenshot", + [ + ["--wait-for-selector ", "wait for selector before taking a screenshot"], + ["--wait-for-timeout ", "wait for timeout in milliseconds before taking a screenshot"], + ["--full-page", "whether to take a full page screenshot (entire scrollable area)"] + ] +).action(async function(url, filename, command) { + const { screenshot } = require("./browserActions"); + screenshot(command, command, url, filename).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ screenshot -b webkit https://example.com example.png`); +commandWithOpenOptions( + "pdf ", + "save page as pdf", + [ + ["--paper-format ", "paper format: Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6"], + ["--wait-for-selector ", "wait for given selector before saving as pdf"], + ["--wait-for-timeout ", "wait for given timeout in milliseconds before saving as pdf"] + ] +).action(async function(url, filename, options) { + const { pdf } = require("./browserActions"); + pdf(options, options, url, filename).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ pdf https://example.com example.pdf`); +import_utilsBundle.program.command("run-driver", { hidden: true }).action(async function(options) { + const { runDriver } = require("./driver"); + runDriver(); +}); +import_utilsBundle.program.command("run-server", { hidden: true }).option("--port ", "Server port").option("--host ", "Server host").option("--path ", "Endpoint Path", "/").option("--max-clients ", "Maximum clients").option("--mode ", 'Server mode, either "default" or "extension"').option("--artifacts-dir ", "Artifacts directory").action(async function(options) { + const { runServer } = require("./driver"); + runServer({ + port: options.port ? +options.port : void 0, + host: options.host, + path: options.path, + maxConnections: options.maxClients ? +options.maxClients : Infinity, + extension: options.mode === "extension" || !!process.env.PW_EXTENSION_MODE, + artifactsDir: options.artifactsDir + }).catch(logErrorAndExit); +}); +import_utilsBundle.program.command("print-api-json", { hidden: true }).action(async function(options) { + const { printApiJson } = require("./driver"); + printApiJson(); +}); +import_utilsBundle.program.command("launch-server", { hidden: true }).requiredOption("--browser ", 'Browser name, one of "chromium", "firefox" or "webkit"').option("--config ", "JSON file with launchServer options").action(async function(options) { + const { launchBrowserServer } = require("./driver"); + launchBrowserServer(options.browser, options.config); +}); +import_utilsBundle.program.command("show-trace [trace]").option("-b, --browser ", "browser to use, one of cr, chromium, ff, firefox, wk, webkit", "chromium").option("-h, --host ", "Host to serve trace on; specifying this option opens trace in a browser tab").option("-p, --port ", "Port to serve trace on, 0 for any free port; specifying this option opens trace in a browser tab").option("--stdin", "Accept trace URLs over stdin to update the viewer").description("show trace viewer").action(async function(trace, options) { + if (options.browser === "cr") + options.browser = "chromium"; + if (options.browser === "ff") + options.browser = "firefox"; + if (options.browser === "wk") + options.browser = "webkit"; + const openOptions = { + host: options.host, + port: +options.port, + isServer: !!options.stdin + }; + const { runTraceInBrowser, runTraceViewerApp } = require("../server/trace/viewer/traceViewer"); + if (options.port !== void 0 || options.host !== void 0) + runTraceInBrowser(trace, openOptions).catch(logErrorAndExit); + else + runTraceViewerApp(trace, options.browser, openOptions).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ show-trace + $ show-trace https://example.com/trace.zip`); +(0, import_traceCli.addTraceCommands)(import_utilsBundle.program, logErrorAndExit); +import_utilsBundle.program.command("cli", { hidden: true }).allowExcessArguments(true).allowUnknownOption(true).action(async (options) => { + const { program: cliProgram } = require("../tools/cli-client/program"); + process.argv.splice(process.argv.indexOf("cli"), 1); + cliProgram().catch(logErrorAndExit); +}); +function logErrorAndExit(e) { + if (process.env.PWDEBUGIMPL) + console.error(e); + else + console.error(e.name + ": " + e.message); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); +} +function codegenId() { + return process.env.PW_LANG_NAME || "playwright-test"; +} +function commandWithOpenOptions(command, description, options) { + let result = import_utilsBundle.program.command(command).description(description); + for (const option of options) + result = result.option(option[0], ...option.slice(1)); + return result.option("-b, --browser ", "browser to use, one of cr, chromium, ff, firefox, wk, webkit", "chromium").option("--block-service-workers", "block service workers").option("--channel ", 'Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc').option("--color-scheme ", 'emulate preferred color scheme, "light" or "dark"').option("--device ", 'emulate device, for example "iPhone 11"').option("--geolocation ", 'specify geolocation coordinates, for example "37.819722,-122.478611"').option("--ignore-https-errors", "ignore https errors").option("--load-storage ", "load context storage state from the file, previously saved with --save-storage").option("--lang ", 'specify language / locale, for example "en-GB"').option("--proxy-server ", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--proxy-bypass ", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--save-har ", "save HAR file with all network activity at the end").option("--save-har-glob ", "filter entries in the HAR by matching url against this glob pattern").option("--save-storage ", "save context storage state at the end, for later use with --load-storage").option("--timezone