diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index 9d29724d86..919080a2de 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -35,19 +35,29 @@ runs: bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }} bun-download-url: ${{ steps.bun-url.outputs.url }} + # Use the fixed Windows patch installer without changing the compiled runtime: + # Bun 1.4.2 regresses our Effect graph at packaged CLI startup. + - name: Setup Windows dependency installer + if: runner.os == 'Windows' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.4.2" + - name: Get cache directory id: cache shell: bash - run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT" + run: | + echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT" + echo "version=$(bun --version)" >> "$GITHUB_OUTPUT" - name: Restore Bun dependencies id: bun-cache uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ${{ steps.cache.outputs.dir }} - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.cache.outputs.version }}-${{ hashFiles('**/bun.lock', 'patches/**') }} restore-keys: | - ${{ runner.os }}-bun- + ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.cache.outputs.version }}- - name: Install setuptools for distutils compatibility run: python3 -m pip install setuptools || pip install setuptools || true @@ -70,4 +80,15 @@ runs: uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ${{ steps.cache.outputs.dir }} - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.cache.outputs.version }}-${{ hashFiles('**/bun.lock', 'patches/**') }} + + - name: Restore pinned Bun runtime after Windows install + if: runner.os == 'Windows' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }} + bun-download-url: ${{ steps.bun-url.outputs.url }} + + - name: Verify pinned Bun runtime + shell: bash + run: test "$(bun --version)" = "$(node -p "require('./package.json').packageManager.split('@')[1]")" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ef106a2bd1..69825dcce0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,7 +64,10 @@ jobs: { target: "linux-x64", runner: "ubuntu-24.04", arch: "x64" }, ] const target = process.env.CANDIDATE_TARGET || "all" - if (!["all", "windows-x64"].includes(target)) throw Error("Invalid candidate target") + if (!["all", "windows-x64"].includes(target)) { + console.error("Invalid candidate target") + process.exit(1) + } const include = target === "all" ? targets : targets.filter((item) => item.target === target) require("node:fs").appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include })}\n`) ' diff --git a/bun.lock b/bun.lock index e0ac63f8bb..583aa819a6 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.15.46", + "version": "1.15.47", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -225,7 +225,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.15.46", + "version": "1.15.47", "dependencies": { "@bharatcode/mobile-connector": "workspace:*", "@zip.js/zip.js": "2.7.62", @@ -253,7 +253,7 @@ "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "@valibot/to-json-schema": "1.6.0", - "electron": "42.3.3", + "electron": "42.11.16", "electron-builder": "26.16.1", "electron-vite": "^5", "solid-js": "catalog:", @@ -369,7 +369,7 @@ }, "packages/opencode": { "name": "bharatcode", - "version": "1.15.46", + "version": "1.15.47", "bin": { "bharatcode": "./bin/bharatcode.mjs", }, @@ -402,7 +402,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/sdk": "1.32.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -716,11 +716,11 @@ "tree-sitter-bash", ], "patchedDependencies": { + "@modelcontextprotocol/sdk@1.32.0": "patches/@modelcontextprotocol%2Fsdk@1.32.0.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@ai-sdk/openai@3.0.88": "patches/@ai-sdk%2Fopenai@3.0.88.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@ai-sdk/anthropic@3.0.111": "patches/@ai-sdk%2Fanthropic@3.0.111.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", @@ -1055,6 +1055,8 @@ "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="], + "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.6", "", {}, "sha512-/aQLgFDdCSnwChfUwqekSmE1q2dCTzhUy5saIHBb5vyYn5LzWaKPaIz1wM0lDMCZYBoIG4svJNvAJb6vkI8BuA=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], @@ -1235,7 +1237,7 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.32.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-8BviX/hK4Gd2eL1KdTwm0gfl4d3wdoErW0Jd00h6nomUAtk6IJk5vwQJc6kfWtJTzN5OEV/lgIiJKI8fiDGAlA=="], "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], @@ -1893,8 +1895,6 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="], "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="], @@ -2279,7 +2279,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron": ["electron@42.3.3", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-0MwYp9wTb7TrtTalOYqeW+suqd9T/Znstr/nDLKqFGIjHdBZX339guo3mQqTPURRZ/UQmYM4uMpzKpI5wLptfQ=="], + "electron": ["electron@42.11.16", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-CHCEsZbWm9zspEnYsro+g8FFllh0e82A1Jtofi/XqPs/XqlrpDPdLP0OhzfhG/IkxNXQ8iHEoYms+wVepZNzZA=="], "electron-builder": ["electron-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.16.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg=="], @@ -2377,8 +2377,6 @@ "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], @@ -2407,8 +2405,6 @@ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], @@ -2465,7 +2461,7 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], @@ -2935,8 +2931,6 @@ "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -3461,8 +3455,6 @@ "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], @@ -3859,6 +3851,8 @@ "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -3901,8 +3895,6 @@ "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], - "execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], - "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -4029,8 +4021,6 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - "@actions/artifact/@actions/core/@actions/exec": ["@actions/exec@2.0.0", "", { "dependencies": { "@actions/io": "^2.0.0" } }, "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw=="], "@actions/core/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], diff --git a/package.json b/package.json index 8f436df5fd..b99059e86a 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@modelcontextprotocol/sdk@1.32.0": "patches/@modelcontextprotocol%2Fsdk@1.32.0.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", diff --git a/packages/app/package.json b/packages/app/package.json index d3c45bcc26..b847390968 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.15.46", + "version": "1.15.47", "description": "", "type": "module", "exports": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 8d7ff04069..41f8f9e803 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.15.46", + "version": "1.15.47", "type": "module", "license": "MIT", "homepage": "https://bharatcode.ai", @@ -50,7 +50,7 @@ "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "@valibot/to-json-schema": "1.6.0", - "electron": "42.3.3", + "electron": "42.11.16", "electron-builder": "26.16.1", "electron-vite": "^5", "solid-js": "catalog:", diff --git a/packages/desktop/scripts/packaging.test.ts b/packages/desktop/scripts/packaging.test.ts index 4f932fb9cb..0c2c3667d9 100644 --- a/packages/desktop/scripts/packaging.test.ts +++ b/packages/desktop/scripts/packaging.test.ts @@ -2,6 +2,26 @@ import { expect, test } from "bun:test" import config, { assertPackagingPolicy } from "../electron-builder.config" import { bundledCliFilename } from "./utils" +test("Windows installer cannot silently replace the pinned build runtime", async () => { + const action = Bun.YAML.parse( + await Bun.file(new URL("../../../.github/actions/setup-bun/action.yml", import.meta.url)).text(), + ) as { + runs: { steps: { name: string; if?: string; run?: string; with?: Record }[] } + } + const steps = action.runs.steps + const installer = steps.findIndex((step) => step.name === "Setup Windows dependency installer") + const install = steps.findIndex((step) => step.name === "Install dependencies") + const restore = steps.findIndex((step) => step.name === "Restore pinned Bun runtime after Windows install") + expect(installer).toBeGreaterThan(-1) + expect(install).toBeGreaterThan(installer) + expect(restore).toBeGreaterThan(install) + expect(steps[installer].if).toBe("runner.os == 'Windows'") + expect(steps[installer].with?.["bun-version"]).toBe("1.4.2") + expect(steps[restore].with?.["bun-download-url"]).toBe("${{ steps.bun-url.outputs.url }}") + expect(steps.at(-1)?.run).toContain("packageManager.split") + expect(steps.at(-1)?.run).toContain("bun --version") +}) + test("packaging cannot substitute an upstream product, protocol, CLI or update destination", () => { expect(config.appId).toStartWith("ai.bharatcode.desktop") expect(config.productName).toStartWith("BharatCode") diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7576baedcf..32e450bab5 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.15.46", + "version": "1.15.47", "name": "bharatcode", "type": "module", "license": "MIT", @@ -91,7 +91,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/sdk": "1.32.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 808aa30296..5ace8df64d 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -8,6 +8,7 @@ import { EffectFlock } from "@opencode-ai/core/util/effect-flock" export const Tokens = Schema.Struct({ accessToken: Schema.mutableKey(Schema.String), + issuer: Schema.mutableKey(Schema.optional(Schema.String)), refreshToken: Schema.mutableKey(Schema.optional(Schema.String)), expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)), scope: Schema.mutableKey(Schema.optional(Schema.String)), @@ -16,6 +17,7 @@ export type Tokens = Schema.Schema.Type export const ClientInfo = Schema.Struct({ clientId: Schema.mutableKey(Schema.String), + issuer: Schema.mutableKey(Schema.optional(Schema.String)), clientSecret: Schema.mutableKey(Schema.optional(Schema.String)), clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)), clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)), diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 596bfe1d55..6f18d79474 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -53,16 +53,21 @@ export class McpOAuthProvider implements OAuthClientProvider { } async clientInformation(): Promise { + const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) if (this.config.clientId) { return { client_id: this.config.clientId, client_secret: this.config.clientSecret, + issuer: + entry?.clientInfo?.clientId === this.config.clientId && + entry.clientInfo.clientSecret === this.config.clientSecret + ? entry.clientInfo.issuer + : undefined, } } // Check stored client info (from dynamic registration) // Use getForUrl to validate credentials are for the current server URL - const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) if (entry?.clientInfo) { // Check if client secret has expired if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) { @@ -71,6 +76,7 @@ export class McpOAuthProvider implements OAuthClientProvider { return { client_id: entry.clientInfo.clientId, client_secret: entry.clientInfo.clientSecret, + issuer: entry.clientInfo.issuer, } } @@ -84,6 +90,7 @@ export class McpOAuthProvider implements OAuthClientProvider { this.mcpName, { clientId: info.client_id, + issuer: info.issuer, clientSecret: info.client_secret, clientIdIssuedAt: info.client_id_issued_at, clientSecretExpiresAt: info.client_secret_expires_at, @@ -100,6 +107,7 @@ export class McpOAuthProvider implements OAuthClientProvider { return { access_token: entry.tokens.accessToken, + issuer: entry.tokens.issuer, token_type: "Bearer", refresh_token: entry.tokens.refreshToken, expires_in: entry.tokens.expiresAt @@ -115,6 +123,7 @@ export class McpOAuthProvider implements OAuthClientProvider { this.mcpName, { accessToken: tokens.access_token, + issuer: tokens.issuer, refreshToken: tokens.refresh_token, expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined, scope: tokens.scope, @@ -186,10 +195,7 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { override async clientInformation(): Promise { if (!this.config.clientId) return this.pendingClientInfo - return { - client_id: this.config.clientId, - client_secret: this.config.clientSecret, - } + return this.pendingClientInfo ?? super.clientInformation() } override async saveClientInformation(info: OAuthClientInformationFull): Promise { @@ -217,19 +223,20 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { { tokens: { accessToken: this.pendingTokens.access_token, + issuer: this.pendingTokens.issuer, refreshToken: this.pendingTokens.refresh_token, expiresAt: this.pendingTokens.expires_in ? Date.now() / 1000 + this.pendingTokens.expires_in : undefined, scope: this.pendingTokens.scope, }, - clientInfo: - this.pendingClientInfo && !this.config.clientId - ? { - clientId: this.pendingClientInfo.client_id, - clientSecret: this.pendingClientInfo.client_secret, - clientIdIssuedAt: this.pendingClientInfo.client_id_issued_at, - clientSecretExpiresAt: this.pendingClientInfo.client_secret_expires_at, - } - : undefined, + clientInfo: this.pendingClientInfo + ? { + clientId: this.pendingClientInfo.client_id, + issuer: this.pendingClientInfo.issuer, + clientSecret: this.pendingClientInfo.client_secret, + clientIdIssuedAt: this.pendingClientInfo.client_id_issued_at, + clientSecretExpiresAt: this.pendingClientInfo.client_secret_expires_at, + } + : undefined, }, this.serverUrl, ), diff --git a/packages/opencode/test/mcp/auth.test.ts b/packages/opencode/test/mcp/auth.test.ts index 5aa099983a..794ae0578a 100644 --- a/packages/opencode/test/mcp/auth.test.ts +++ b/packages/opencode/test/mcp/auth.test.ts @@ -4,6 +4,8 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Effect, Layer } from "effect" import { FSUtil } from "@opencode-ai/core/fs-util" import { McpAuth } from "../../src/mcp/auth" +import { auth as authorize } from "@modelcontextprotocol/sdk/client/auth.js" +import { McpOAuthPendingProvider, McpOAuthProvider } from "../../src/mcp/oauth-provider" function authFile() { let raw = "" @@ -76,3 +78,55 @@ test("serializes concurrent auth file updates across service instances", async ( }), ) }) + +for (const configured of [false, true]) { + test(`preserves issuer binding for ${configured ? "configured" : "registered"} MCP credentials`, async () => { + const service = await Effect.runPromise(authService(authFile().fsLayer)) + const serverUrl = "https://mcp.example.com/mcp" + const issuer = "https://trusted.example.com" + const config = configured ? { clientId: "test-client", clientSecret: "test-secret" } : {} + const pending = new McpOAuthPendingProvider("test", serverUrl, config, { onRedirect: async () => {} }, service) + await pending.saveClientInformation({ + client_id: "test-client", + client_secret: "test-secret", + redirect_uris: [pending.redirectUrl], + issuer, + }) + await pending.saveTokens({ + access_token: "test-access", + refresh_token: "test-refresh", + token_type: "Bearer", + issuer, + }) + await pending.commit() + + const provider = new McpOAuthProvider("test", serverUrl, config, { onRedirect: async () => {} }, service) + expect((await provider.clientInformation())?.issuer).toBe(issuer) + expect((await provider.tokens())?.issuer).toBe(issuer) + let requests = 0 + const changedIssuer = Object.assign(provider, { + discoveryState: async () => ({ + authorizationServerUrl: "https://other.example.com", + resourceMetadata: { resource: serverUrl }, + authorizationServerMetadata: { + issuer: "https://other.example.com", + authorization_endpoint: "https://other.example.com/authorize", + token_endpoint: "https://other.example.com/token", + response_types_supported: ["code"], + }, + }), + }) + await expect( + authorize(changedIssuer, { + serverUrl, + authorizationCode: "synthetic-code", + fetchFn: async () => { + requests++ + throw new Error("Credentials must not reach the changed issuer") + }, + }), + ).rejects.toThrow("Existing OAuth client information is required") + expect(requests).toBe(0) + expect((await provider.tokens())?.refresh_token).toBe("test-refresh") + }) +} diff --git a/packages/opencode/test/mcp/transport.test.ts b/packages/opencode/test/mcp/transport.test.ts index f7a74d4d72..b7dbbfd4ec 100644 --- a/packages/opencode/test/mcp/transport.test.ts +++ b/packages/opencode/test/mcp/transport.test.ts @@ -36,3 +36,51 @@ test("does not reconnect an SSE stream after a JSON-RPC error response", async ( expect(requests).toBe(1) }) + +test("does not follow an MCP redirect to another origin", async () => { + let requests = 0 + const transport = new StreamableHTTPClientTransport(new URL("https://mcp.example.com/mcp"), { + requestInit: { headers: { Authorization: "Bearer synthetic-secret" } }, + fetch: async () => { + requests++ + return Response.redirect("https://other.example.com/collect", 307) + }, + }) + await transport.start() + try { + await expect(transport.send({ jsonrpc: "2.0", method: "ping", id: 1 })).rejects.toThrow() + expect(requests).toBe(1) + } finally { + await transport.close() + } +}) + +test("does not replay cancelled work after session recovery", async () => { + let requests = 0 + let active = true + const transport = new StreamableHTTPClientTransport(new URL("https://mcp.example.com/mcp"), { + sessionId: "expired", + fetch: async () => { + requests++ + return new Response("", { status: 404 }) + }, + }) + Object.assign(transport, { + onsessionexpired: async () => { + active = false + }, + }) + await transport.start() + try { + // The patched Protocol supplies this callback from its live response-handler map. + const options: NonNullable[1]> & { isRequestActive: () => boolean } = { + isRequestActive: () => active, + } + await expect(transport.send({ jsonrpc: "2.0", method: "ping", id: 1 }, options)).rejects.toThrow( + "Request is no longer active", + ) + expect(requests).toBe(1) + } finally { + await transport.close() + } +}) diff --git a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch b/patches/@modelcontextprotocol%2Fsdk@1.32.0.patch similarity index 85% rename from patches/@modelcontextprotocol%2Fsdk@1.29.0.patch rename to patches/@modelcontextprotocol%2Fsdk@1.32.0.patch index 3b70bea558..4317317ef6 100644 --- a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch +++ b/patches/@modelcontextprotocol%2Fsdk@1.32.0.patch @@ -1,5 +1,87 @@ +diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts +index 520d8b6..d04ba5f 100644 +--- a/dist/cjs/client/auth.d.ts ++++ b/dist/cjs/client/auth.d.ts +@@ -216,6 +216,15 @@ export declare function selectClientAuthMethod(clientInformation: OAuthClientInf + * @returns A Promise that resolves to an OAuthError instance + */ + export declare function parseErrorResponse(input: Response | string): Promise; ++/** ++ * Selects scopes per the MCP spec and augments them for refresh token support. ++ */ ++export declare function determineScope(options: { ++ requestedScope?: string; ++ resourceMetadata?: OAuthProtectedResourceMetadata; ++ authServerMetadata?: AuthorizationServerMetadata; ++ clientMetadata: OAuthClientMetadata; ++}): string | undefined; + /** + * Orchestrates the full auth flow with a server. + * +diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js +index 4c87a15..c52465b 100644 +--- a/dist/cjs/client/auth.js ++++ b/dist/cjs/client/auth.js +@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0; + exports.selectClientAuthMethod = selectClientAuthMethod; + exports.parseErrorResponse = parseErrorResponse; + exports.auth = auth; ++exports.determineScope = determineScope; + exports.isHttpsUrl = isHttpsUrl; + exports.selectResourceURL = selectResourceURL; + exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams; +@@ -226,6 +227,19 @@ async function auth(provider, options) { + throw error; + } + } ++/** ++ * Selects scopes per the MCP spec and augments them for refresh token support. ++ */ ++function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) { ++ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope; ++ if (effectiveScope && ++ authServerMetadata?.scopes_supported?.includes('offline_access') && ++ !effectiveScope.split(' ').includes('offline_access') && ++ clientMetadata.grant_types?.includes('refresh_token')) { ++ effectiveScope = `${effectiveScope} offline_access`; ++ } ++ return effectiveScope; ++} + async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { + // Check if the provider has cached discovery state to skip discovery + const cachedState = await provider.discoveryState?.(); +@@ -296,7 +310,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res + // 2. PRM scopes_supported + // 3. Client metadata scope (user-configured fallback) + // The resolved scope is used consistently for both DCR and the authorization request. +- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope; ++ const resolvedScope = determineScope({ ++ requestedScope: scope, ++ resourceMetadata, ++ authServerMetadata: metadata, ++ clientMetadata: provider.clientMetadata ++ }); + // Handle client registration if needed. Client information stamped for a different + // authorization server reads back as `undefined`, so the flow re-registers exactly as if + // nothing were stored. +@@ -833,7 +852,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo + if (scope) { + authorizationUrl.searchParams.set('scope', scope); + } +- if (scope?.includes('offline_access')) { ++ if (scope?.split(' ').includes('offline_access')) { + // if the request includes the OIDC-only "offline_access" scope, + // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access + // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess +@@ -1043,4 +1062,4 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata + } + return RegistrationResponseSchema.parse(await response.json()); + } +-//# sourceMappingURL=auth.js.map +\ No newline at end of file ++//# sourceMappingURL=auth.js.map diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts -index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 +index 6f567a1..5b7c841 100644 --- a/dist/cjs/client/index.d.ts +++ b/dist/cjs/client/index.d.ts @@ -428,6 +428,8 @@ export declare class Client>; -+ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; - callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ - [x: string]: unknown; - content: ({ -diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts -index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 ---- a/dist/esm/client/index.d.ts -+++ b/dist/esm/client/index.d.ts -@@ -428,6 +428,8 @@ export declare class Client>; + callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ [x: string]: unknown; content: ({ diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js -index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644 +index 6581be2..1518666 100644 --- a/dist/cjs/client/index.js +++ b/dist/cjs/client/index.js -@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol { +@@ -277,41 +277,16 @@ class Client extends protocol_js_1.Protocol { } async connect(transport, options) { await super.connect(transport); @@ -74,7 +143,7 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1 } catch (error) { // Disconnect if initialization fails. -@@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol { +@@ -319,6 +294,37 @@ class Client extends protocol_js_1.Protocol { throw error; } } @@ -112,7 +181,8 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1 /** * After initialization has completed, this will be populated with the server's reported capabilities. */ -@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol { +@@ -529,10 +535,12 @@ class Client extends protocol_js_1.Protocol { + * Cache validators for tool output schemas. * Called after listTools() to pre-compile validators for better performance. */ - cacheToolMetadata(tools) { @@ -128,7 +198,7 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1 for (const tool of tools) { // If the tool has an outputSchema, create and cache the validator if (tool.outputSchema) { -@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol { +@@ -558,7 +566,7 @@ class Client extends protocol_js_1.Protocol { async listTools(params, options) { const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options); // Cache the tools and their output schemas for future validation @@ -138,10 +208,10 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1 } /** diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js -index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644 +index a8f4e59..80d374f 100644 --- a/dist/cjs/client/streamableHttp.js +++ b/dist/cjs/client/streamableHttp.js -@@ -204,7 +204,7 @@ class StreamableHTTPClientTransport { +@@ -218,7 +218,7 @@ class StreamableHTTPClientTransport { if (!event.event || event.event === 'message') { try { const message = types_js_1.JSONRPCMessageSchema.parse(JSON.parse(event.data)); @@ -150,7 +220,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed // Mark that we received a response - no need to reconnect for this request receivedResponse = true; if (replayMessageId !== undefined) { -@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport { +@@ -304,7 +304,38 @@ class StreamableHTTPClientTransport { this.onclose?.(); } async send(message, options) { @@ -189,7 +259,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed const { resumptionToken, onresumptiontoken } = options || {}; if (resumptionToken) { // If we have at last event ID, we need to reconnect the SSE stream -@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport { +@@ -312,6 +343,7 @@ class StreamableHTTPClientTransport { return; } const headers = await this._commonHeaders(); @@ -197,8 +267,8 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed headers.set('content-type', 'application/json'); headers.set('accept', 'application/json, text/event-stream'); const init = { -@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport { - const response = await (this._fetch ?? fetch)(this._url, init); +@@ -324,11 +356,20 @@ class StreamableHTTPClientTransport { + const response = await (0, transport_js_1.fetchWithinOrigin)(this._fetch ?? fetch)(this._url, init); // Handle session ID received during initialization const sessionId = response.headers.get('mcp-session-id'); - if (sessionId) { @@ -219,7 +289,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed if (response.status === 401 && this._authProvider) { // Prevent infinite recursion when server returns 401 after successful auth if (this._hasCompletedAuthFlow) { -@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport { +@@ -349,7 +390,7 @@ class StreamableHTTPClientTransport { // Mark that we completed auth flow this._hasCompletedAuthFlow = true; // Purposely _not_ awaited, so we don't call onerror twice @@ -228,7 +298,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed } if (response.status === 403 && this._authProvider) { const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response); -@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport { +@@ -376,7 +417,7 @@ class StreamableHTTPClientTransport { if (result !== 'AUTHORIZED') { throw new auth_js_1.UnauthorizedError(); } @@ -236,12 +306,19 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed + return this._send(message, options, isSessionRetry); } } - throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); + throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${this._unfollowedRedirect(response, this._url) ?? text}`); +@@ -494,4 +535,4 @@ class StreamableHTTPClientTransport { + } + } + exports.StreamableHTTPClientTransport = StreamableHTTPClientTransport; +-//# sourceMappingURL=streamableHttp.js.map +\ No newline at end of file ++//# sourceMappingURL=streamableHttp.js.map diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js -index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644 +index 2eaacb7..b3eeb28 100644 --- a/dist/cjs/shared/protocol.js +++ b/dist/cjs/shared/protocol.js -@@ -744,7 +744,12 @@ class Protocol { +@@ -772,7 +772,12 @@ class Protocol { } else { // No related task - send through transport normally @@ -255,91 +332,11 @@ index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb7 this._cleanupTimeout(messageId); reject(error); }); -diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts -index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644 ---- a/dist/cjs/client/auth.d.ts -+++ b/dist/cjs/client/auth.d.ts -@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise; -+/** -+ * Selects scopes per the MCP spec and augments them for refresh token support. -+ */ -+export declare function determineScope(options: { -+ requestedScope?: string; -+ resourceMetadata?: OAuthProtectedResourceMetadata; -+ authServerMetadata?: AuthorizationServerMetadata; -+ clientMetadata: OAuthClientMetadata; -+}): string | undefined; - /** - * Orchestrates the full auth flow with a server. - * -diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js -index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644 ---- a/dist/cjs/client/auth.js -+++ b/dist/cjs/client/auth.js -@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0; - exports.selectClientAuthMethod = selectClientAuthMethod; - exports.parseErrorResponse = parseErrorResponse; - exports.auth = auth; -+exports.determineScope = determineScope; - exports.isHttpsUrl = isHttpsUrl; - exports.selectResourceURL = selectResourceURL; - exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams; -@@ -186,6 +187,19 @@ async function auth(provider, options) { - throw error; - } - } -+/** -+ * Selects scopes per the MCP spec and augments them for refresh token support. -+ */ -+function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) { -+ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope; -+ if (effectiveScope && -+ authServerMetadata?.scopes_supported?.includes('offline_access') && -+ !effectiveScope.split(' ').includes('offline_access') && -+ clientMetadata.grant_types?.includes('refresh_token')) { -+ effectiveScope = `${effectiveScope} offline_access`; -+ } -+ return effectiveScope; -+} - async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { - // Check if the provider has cached discovery state to skip discovery - const cachedState = await provider.discoveryState?.(); -@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res - }); - } - const resource = await selectResourceURL(serverUrl, provider, resourceMetadata); -- // Apply scope selection strategy (SEP-835): -- // 1. WWW-Authenticate scope (passed via `scope` param) -- // 2. PRM scopes_supported -- // 3. Client metadata scope (user-configured fallback) -- // The resolved scope is used consistently for both DCR and the authorization request. -- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope; -+ const resolvedScope = determineScope({ -+ requestedScope: scope, -+ resourceMetadata, -+ authServerMetadata: metadata, -+ clientMetadata: provider.clientMetadata -+ }); - // Handle client registration if needed - let clientInformation = await Promise.resolve(provider.clientInformation()); - if (!clientInformation) { -@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo - if (scope) { - authorizationUrl.searchParams.set('scope', scope); - } -- if (scope?.includes('offline_access')) { -+ if (scope?.split(' ').includes('offline_access')) { - // if the request includes the OIDC-only "offline_access" scope, - // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access - // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts -index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644 +index 520d8b6..d04ba5f 100644 --- a/dist/esm/client/auth.d.ts +++ b/dist/esm/client/auth.d.ts -@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise; @@ -356,10 +353,10 @@ index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433bee * Orchestrates the full auth flow with a server. * diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js -index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644 +index 9b184c0..8df20e8 100644 --- a/dist/esm/client/auth.js +++ b/dist/esm/client/auth.js -@@ -161,6 +161,19 @@ export async function auth(provider, options) { +@@ -201,6 +201,19 @@ export async function auth(provider, options) { throw error; } } @@ -379,15 +376,10 @@ index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af6 async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { // Check if the provider has cached discovery state to skip discovery const cachedState = await provider.discoveryState?.(); -@@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res - }); - } - const resource = await selectResourceURL(serverUrl, provider, resourceMetadata); -- // Apply scope selection strategy (SEP-835): -- // 1. WWW-Authenticate scope (passed via `scope` param) -- // 2. PRM scopes_supported -- // 3. Client metadata scope (user-configured fallback) -- // The resolved scope is used consistently for both DCR and the authorization request. +@@ -271,7 +284,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res + // 2. PRM scopes_supported + // 3. Client metadata scope (user-configured fallback) + // The resolved scope is used consistently for both DCR and the authorization request. - const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope; + const resolvedScope = determineScope({ + requestedScope: scope, @@ -395,10 +387,10 @@ index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af6 + authServerMetadata: metadata, + clientMetadata: provider.clientMetadata + }); - // Handle client registration if needed - let clientInformation = await Promise.resolve(provider.clientInformation()); - if (!clientInformation) { -@@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli + // Handle client registration if needed. Client information stamped for a different + // authorization server reads back as `undefined`, so the flow re-registers exactly as if + // nothing were stored. +@@ -808,7 +826,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli if (scope) { authorizationUrl.searchParams.set('scope', scope); } @@ -407,11 +399,31 @@ index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af6 // if the request includes the OIDC-only "offline_access" scope, // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess +@@ -1018,4 +1036,4 @@ export async function registerClient(authorizationServerUrl, { metadata, clientM + } + return RegistrationResponseSchema.parse(await response.json()); + } +-//# sourceMappingURL=auth.js.map +\ No newline at end of file ++//# sourceMappingURL=auth.js.map +diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts +index 6f567a1..5b7c841 100644 +--- a/dist/esm/client/index.d.ts ++++ b/dist/esm/client/index.d.ts +@@ -428,6 +428,8 @@ export declare class Client>; ++ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; + callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ + [x: string]: unknown; + content: ({ diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js -index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644 +index 30795e5..bb62534 100644 --- a/dist/esm/client/index.js +++ b/dist/esm/client/index.js -@@ -284,41 +284,16 @@ export class Client extends Protocol { +@@ -273,41 +273,16 @@ export class Client extends Protocol { } async connect(transport, options) { await super.connect(transport); @@ -457,7 +469,7 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e } catch (error) { // Disconnect if initialization fails. -@@ -326,6 +301,37 @@ export class Client extends Protocol { +@@ -315,6 +290,37 @@ export class Client extends Protocol { throw error; } } @@ -495,7 +507,8 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e /** * After initialization has completed, this will be populated with the server's reported capabilities. */ -@@ -537,9 +543,11 @@ export class Client extends Protocol { +@@ -525,10 +531,12 @@ export class Client extends Protocol { + * Cache validators for tool output schemas. * Called after listTools() to pre-compile validators for better performance. */ - cacheToolMetadata(tools) { @@ -511,7 +524,7 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e for (const tool of tools) { // If the tool has an outputSchema, create and cache the validator if (tool.outputSchema) { -@@ -565,7 +573,7 @@ export class Client extends Protocol { +@@ -554,7 +562,7 @@ export class Client extends Protocol { async listTools(params, options) { const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options); // Cache the tools and their output schemas for future validation @@ -521,17 +534,18 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e } /** diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js -index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644 +index 3409dfc..1e3b63d 100644 --- a/dist/esm/client/streamableHttp.js +++ b/dist/esm/client/streamableHttp.js -@@ -1,5 +1,5 @@ - import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js'; +@@ -1,6 +1,6 @@ + import { mediaTypeEssence } from '../shared/mediaType.js'; + import { createFetchWithInit, fetchLeavingRedirects, fetchWithinOrigin, followWithinOrigin, normalizeHeaders, unfollowedRedirect } from '../shared/transport.js'; -import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; +import { isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js'; import { EventSourceParserStream } from 'eventsource-parser/stream'; // Default reconnection options for StreamableHTTP connections -@@ -200,7 +200,7 @@ export class StreamableHTTPClientTransport { +@@ -214,7 +214,7 @@ export class StreamableHTTPClientTransport { if (!event.event || event.event === 'message') { try { const message = JSONRPCMessageSchema.parse(JSON.parse(event.data)); @@ -540,7 +554,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 // Mark that we received a response - no need to reconnect for this request receivedResponse = true; if (replayMessageId !== undefined) { -@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport { +@@ -300,7 +300,38 @@ export class StreamableHTTPClientTransport { this.onclose?.(); } async send(message, options) { @@ -579,7 +593,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 const { resumptionToken, onresumptiontoken } = options || {}; if (resumptionToken) { // If we have at last event ID, we need to reconnect the SSE stream -@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport { +@@ -308,6 +339,7 @@ export class StreamableHTTPClientTransport { return; } const headers = await this._commonHeaders(); @@ -587,8 +601,8 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 headers.set('content-type', 'application/json'); headers.set('accept', 'application/json, text/event-stream'); const init = { -@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport { - const response = await (this._fetch ?? fetch)(this._url, init); +@@ -320,11 +352,20 @@ export class StreamableHTTPClientTransport { + const response = await fetchWithinOrigin(this._fetch ?? fetch)(this._url, init); // Handle session ID received during initialization const sessionId = response.headers.get('mcp-session-id'); - if (sessionId) { @@ -609,7 +623,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 if (response.status === 401 && this._authProvider) { // Prevent infinite recursion when server returns 401 after successful auth if (this._hasCompletedAuthFlow) { -@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport { +@@ -345,7 +386,7 @@ export class StreamableHTTPClientTransport { // Mark that we completed auth flow this._hasCompletedAuthFlow = true; // Purposely _not_ awaited, so we don't call onerror twice @@ -618,7 +632,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 } if (response.status === 403 && this._authProvider) { const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response); -@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport { +@@ -372,7 +413,7 @@ export class StreamableHTTPClientTransport { if (result !== 'AUTHORIZED') { throw new UnauthorizedError(); } @@ -626,12 +640,19 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 + return this._send(message, options, isSessionRetry); } } - throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); + throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${this._unfollowedRedirect(response, this._url) ?? text}`); +@@ -489,4 +530,4 @@ export class StreamableHTTPClientTransport { + }); + } + } +-//# sourceMappingURL=streamableHttp.js.map +\ No newline at end of file ++//# sourceMappingURL=streamableHttp.js.map diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js -index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644 +index 58100ad..e36ff0f 100644 --- a/dist/esm/shared/protocol.js +++ b/dist/esm/shared/protocol.js -@@ -740,7 +740,12 @@ export class Protocol { +@@ -768,7 +768,12 @@ export class Protocol { } else { // No related task - send through transport normally