Browse Source

Merge branch 'main' into support_project_mcp

aheizi 11 months ago
parent
commit
5d8a456374

+ 2 - 2
.github/workflows/marketplace-publish.yml

@@ -27,8 +27,8 @@ jobs:
           node-version: 18
 
       - run: |
-          git config user.name github-actions
-          git config user.email [email protected]
+          git config user.name "github-actions[bot]"
+          git config user.email "github-actions[bot]@users.noreply.github.com"
 
       - name: Install Dependencies
         run: |

+ 59 - 0
.github/workflows/update-contributors.yml

@@ -0,0 +1,59 @@
+name: Update Contributors
+
+on:
+  push:
+    branches:
+      - main
+  workflow_dispatch:  # Allows manual triggering
+
+jobs:
+  update-contributors:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write  # Needed for pushing changes
+      pull-requests: write  # Needed for creating PRs
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+        
+      - name: Setup Node.js
+        uses: actions/setup-node@v3
+        with:
+          node-version: '18'
+          cache: 'npm'
+          
+      - name: Disable Husky
+        run: |
+          echo "HUSKY=0" >> $GITHUB_ENV
+          git config --global core.hooksPath /dev/null
+          
+      - name: Install dependencies
+        run: npm ci
+        
+      - name: Update contributors and format
+        run: |
+          npm run update-contributors
+          npx prettier --write README.md
+          if git diff --quiet README.md; then echo "changes=false" >> $GITHUB_OUTPUT; else echo "changes=true" >> $GITHUB_OUTPUT; fi
+        id: check-changes
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Create Pull Request
+        if: steps.check-changes.outputs.changes == 'true'
+        uses: peter-evans/create-pull-request@v5
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          add-paths: |
+            README.md
+          commit-message: "docs: update contributors list [skip ci]"
+          committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
+          branch: update-contributors
+          branch-suffix: timestamp
+          delete-branch: true
+          title: "Update contributors list"
+          body: |
+            Automated update of contributors list in README.md
+            
+            This PR was created automatically by a GitHub Action workflow.
+          base: main

+ 4 - 0
CHANGELOG.md

@@ -1,5 +1,9 @@
 # Roo Code Changelog
 
+## [3.8.6] - 2025-03-13
+
+- Revert SSE MCP support while we debug some config validation issues
+
 ## [3.8.5] - 2025-03-12
 
 - Refactor terminal architecture to address critical issues with the current design (thanks @KJ7LNW!)

+ 24 - 0
README.md

@@ -170,6 +170,30 @@ We love community contributions! Here’s how to get involved:
 
 ---
 
+<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
+
+## Contributors
+
+Thanks to all our contributors who have helped make Roo Code better!
+
+|              <a href="https://github.com/mrubens"><img src="https://avatars.githubusercontent.com/u/2600?v=4" width="100" height="100" alt="mrubens"/><br /><sub><b>mrubens</b></sub></a>              |         <a href="https://github.com/saoudrizwan"><img src="https://avatars.githubusercontent.com/u/7799382?v=4" width="100" height="100" alt="saoudrizwan"/><br /><sub><b>saoudrizwan</b></sub></a>         |                  <a href="https://github.com/cte"><img src="https://avatars.githubusercontent.com/u/16332?v=4" width="100" height="100" alt="cte"/><br /><sub><b>cte</b></sub></a>                  |          <a href="https://github.com/samhvw8"><img src="https://avatars.githubusercontent.com/u/12538214?v=4" width="100" height="100" alt="samhvw8"/><br /><sub><b>samhvw8</b></sub></a>          |        <a href="https://github.com/daniel-lxs"><img src="https://avatars.githubusercontent.com/u/57051444?v=4" width="100" height="100" alt="daniel-lxs"/><br /><sub><b>daniel-lxs</b></sub></a>         |                   <a href="https://github.com/a8trejo"><img src="https://avatars.githubusercontent.com/u/62401433?v=4" width="100" height="100" alt="a8trejo"/><br /><sub><b>a8trejo</b></sub></a>                    |
+| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+|       <a href="https://github.com/ColemanRoo"><img src="https://avatars.githubusercontent.com/u/117104599?v=4" width="100" height="100" alt="ColemanRoo"/><br /><sub><b>ColemanRoo</b></sub></a>       |             <a href="https://github.com/stea9499"><img src="https://avatars.githubusercontent.com/u/4163795?v=4" width="100" height="100" alt="stea9499"/><br /><sub><b>stea9499</b></sub></a>              |   <a href="https://github.com/joemanley201"><img src="https://avatars.githubusercontent.com/u/8299960?v=4" width="100" height="100" alt="joemanley201"/><br /><sub><b>joemanley201</b></sub></a>    |       <a href="https://github.com/System233"><img src="https://avatars.githubusercontent.com/u/20336040?v=4" width="100" height="100" alt="System233"/><br /><sub><b>System233</b></sub></a>       |           <a href="https://github.com/jquanton"><img src="https://avatars.githubusercontent.com/u/88576563?v=4" width="100" height="100" alt="jquanton"/><br /><sub><b>jquanton</b></sub></a>            |              <a href="https://github.com/nissa-seru"><img src="https://avatars.githubusercontent.com/u/119150866?v=4" width="100" height="100" alt="nissa-seru"/><br /><sub><b>nissa-seru</b></sub></a>               |
+|   <a href="https://github.com/hannesrudolph"><img src="https://avatars.githubusercontent.com/u/49103247?v=4" width="100" height="100" alt="hannesrudolph"/><br /><sub><b>hannesrudolph</b></sub></a>   |             <a href="https://github.com/MuriloFP"><img src="https://avatars.githubusercontent.com/u/50873657?v=4" width="100" height="100" alt="MuriloFP"/><br /><sub><b>MuriloFP</b></sub></a>             |            <a href="https://github.com/NyxJae"><img src="https://avatars.githubusercontent.com/u/52313587?v=4" width="100" height="100" alt="NyxJae"/><br /><sub><b>NyxJae</b></sub></a>            |        <a href="https://github.com/punkpeye"><img src="https://avatars.githubusercontent.com/u/108313943?v=4" width="100" height="100" alt="punkpeye"/><br /><sub><b>punkpeye</b></sub></a>        |                <a href="https://github.com/d-oit"><img src="https://avatars.githubusercontent.com/u/6849456?v=4" width="100" height="100" alt="d-oit"/><br /><sub><b>d-oit</b></sub></a>                 |            <a href="https://github.com/monotykamary"><img src="https://avatars.githubusercontent.com/u/1130103?v=4" width="100" height="100" alt="monotykamary"/><br /><sub><b>monotykamary</b></sub></a>             |
+|        <a href="https://github.com/lloydchang"><img src="https://avatars.githubusercontent.com/u/1329685?v=4" width="100" height="100" alt="lloydchang"/><br /><sub><b>lloydchang</b></sub></a>        | <a href="https://github.com/vigneshsubbiah16"><img src="https://avatars.githubusercontent.com/u/51325334?v=4" width="100" height="100" alt="vigneshsubbiah16"/><br /><sub><b>vigneshsubbiah16</b></sub></a> |           <a href="https://github.com/Szpadel"><img src="https://avatars.githubusercontent.com/u/1857251?v=4" width="100" height="100" alt="Szpadel"/><br /><sub><b>Szpadel</b></sub></a>           |      <a href="https://github.com/lupuletic"><img src="https://avatars.githubusercontent.com/u/105351510?v=4" width="100" height="100" alt="lupuletic"/><br /><sub><b>lupuletic</b></sub></a>       |             <a href="https://github.com/cannuri"><img src="https://avatars.githubusercontent.com/u/91494156?v=4" width="100" height="100" alt="cannuri"/><br /><sub><b>cannuri</b></sub></a>             | <a href="https://github.com/Smartsheet-JB-Brown"><img src="https://avatars.githubusercontent.com/u/171734120?v=4" width="100" height="100" alt="Smartsheet-JB-Brown"/><br /><sub><b>Smartsheet-JB-Brown</b></sub></a> |
+|          <a href="https://github.com/Premshay"><img src="https://avatars.githubusercontent.com/u/28099628?v=4" width="100" height="100" alt="Premshay"/><br /><sub><b>Premshay</b></sub></a>           |              <a href="https://github.com/psv2522"><img src="https://avatars.githubusercontent.com/u/87223770?v=4" width="100" height="100" alt="psv2522"/><br /><sub><b>psv2522</b></sub></a>               |    <a href="https://github.com/olweraltuve"><img src="https://avatars.githubusercontent.com/u/39308405?v=4" width="100" height="100" alt="olweraltuve"/><br /><sub><b>olweraltuve</b></sub></a>     |      <a href="https://github.com/RaySinner"><img src="https://avatars.githubusercontent.com/u/118297374?v=4" width="100" height="100" alt="RaySinner"/><br /><sub><b>RaySinner</b></sub></a>       |                <a href="https://github.com/qdaxb"><img src="https://avatars.githubusercontent.com/u/4157870?v=4" width="100" height="100" alt="qdaxb"/><br /><sub><b>qdaxb</b></sub></a>                 |            <a href="https://github.com/afshawnlotfi"><img src="https://avatars.githubusercontent.com/u/6283745?v=4" width="100" height="100" alt="afshawnlotfi"/><br /><sub><b>afshawnlotfi</b></sub></a>             |
+|           <a href="https://github.com/emshvac"><img src="https://avatars.githubusercontent.com/u/121588911?v=4" width="100" height="100" alt="emshvac"/><br /><sub><b>emshvac</b></sub></a>            |           <a href="https://github.com/Lunchb0ne"><img src="https://avatars.githubusercontent.com/u/22198661?v=4" width="100" height="100" alt="Lunchb0ne"/><br /><sub><b>Lunchb0ne</b></sub></a>            |             <a href="https://github.com/sammcj"><img src="https://avatars.githubusercontent.com/u/862951?v=4" width="100" height="100" alt="sammcj"/><br /><sub><b>sammcj</b></sub></a>             |         <a href="https://github.com/dtrugman"><img src="https://avatars.githubusercontent.com/u/2451669?v=4" width="100" height="100" alt="dtrugman"/><br /><sub><b>dtrugman</b></sub></a>         |          <a href="https://github.com/aitoroses"><img src="https://avatars.githubusercontent.com/u/1699368?v=4" width="100" height="100" alt="aitoroses"/><br /><sub><b>aitoroses</b></sub></a>           |                  <a href="https://github.com/yt3trees"><img src="https://avatars.githubusercontent.com/u/57471763?v=4" width="100" height="100" alt="yt3trees"/><br /><sub><b>yt3trees</b></sub></a>                  |
+|            <a href="https://github.com/yongjer"><img src="https://avatars.githubusercontent.com/u/54315206?v=4" width="100" height="100" alt="yongjer"/><br /><sub><b>yongjer</b></sub></a>            |         <a href="https://github.com/vincentsong"><img src="https://avatars.githubusercontent.com/u/2343574?v=4" width="100" height="100" alt="vincentsong"/><br /><sub><b>vincentsong</b></sub></a>         | <a href="https://github.com/pugazhendhi-m"><img src="https://avatars.githubusercontent.com/u/132246623?v=4" width="100" height="100" alt="pugazhendhi-m"/><br /><sub><b>pugazhendhi-m</b></sub></a> |            <a href="https://github.com/eonghk"><img src="https://avatars.githubusercontent.com/u/139964?v=4" width="100" height="100" alt="eonghk"/><br /><sub><b>eonghk</b></sub></a>             |            <a href="https://github.com/philfung"><img src="https://avatars.githubusercontent.com/u/1054593?v=4" width="100" height="100" alt="philfung"/><br /><sub><b>philfung</b></sub></a>            |                      <a href="https://github.com/pdecat"><img src="https://avatars.githubusercontent.com/u/318490?v=4" width="100" height="100" alt="pdecat"/><br /><sub><b>pdecat</b></sub></a>                      |
+|              <a href="https://github.com/napter"><img src="https://avatars.githubusercontent.com/u/6260841?v=4" width="100" height="100" alt="napter"/><br /><sub><b>napter</b></sub></a>              |                      <a href="https://github.com/mdp"><img src="https://avatars.githubusercontent.com/u/2868?v=4" width="100" height="100" alt="mdp"/><br /><sub><b>mdp</b></sub></a>                       |            <a href="https://github.com/jcbdev"><img src="https://avatars.githubusercontent.com/u/17152092?v=4" width="100" height="100" alt="jcbdev"/><br /><sub><b>jcbdev</b></sub></a>            |     <a href="https://github.com/anton-otee"><img src="https://avatars.githubusercontent.com/u/149477749?v=4" width="100" height="100" alt="anton-otee"/><br /><sub><b>anton-otee</b></sub></a>     |             <a href="https://github.com/AMHesch"><img src="https://avatars.githubusercontent.com/u/4777192?v=4" width="100" height="100" alt="AMHesch"/><br /><sub><b>AMHesch</b></sub></a>              |                   <a href="https://github.com/bannzai"><img src="https://avatars.githubusercontent.com/u/10897361?v=4" width="100" height="100" alt="bannzai"/><br /><sub><b>bannzai</b></sub></a>                    |
+|           <a href="https://github.com/dairui1"><img src="https://avatars.githubusercontent.com/u/183250644?v=4" width="100" height="100" alt="dairui1"/><br /><sub><b>dairui1</b></sub></a>            |               <a href="https://github.com/dqroid"><img src="https://avatars.githubusercontent.com/u/192424994?v=4" width="100" height="100" alt="dqroid"/><br /><sub><b>dqroid</b></sub></a>                |        <a href="https://github.com/kinandan"><img src="https://avatars.githubusercontent.com/u/186135699?v=4" width="100" height="100" alt="kinandan"/><br /><sub><b>kinandan</b></sub></a>         |             <a href="https://github.com/kohii"><img src="https://avatars.githubusercontent.com/u/6891780?v=4" width="100" height="100" alt="kohii"/><br /><sub><b>kohii</b></sub></a>              |       <a href="https://github.com/lightrabbit"><img src="https://avatars.githubusercontent.com/u/1521765?v=4" width="100" height="100" alt="lightrabbit"/><br /><sub><b>lightrabbit</b></sub></a>        |                        <a href="https://github.com/olup"><img src="https://avatars.githubusercontent.com/u/13785588?v=4" width="100" height="100" alt="olup"/><br /><sub><b>olup</b></sub></a>                        |
+|      <a href="https://github.com/moqimoqidea"><img src="https://avatars.githubusercontent.com/u/39821951?v=4" width="100" height="100" alt="moqimoqidea"/><br /><sub><b>moqimoqidea</b></sub></a>      |            <a href="https://github.com/mosleyit"><img src="https://avatars.githubusercontent.com/u/189396442?v=4" width="100" height="100" alt="mosleyit"/><br /><sub><b>mosleyit</b></sub></a>             |         <a href="https://github.com/oprstchn"><img src="https://avatars.githubusercontent.com/u/16177972?v=4" width="100" height="100" alt="oprstchn"/><br /><sub><b>oprstchn</b></sub></a>         |     <a href="https://github.com/philipnext"><img src="https://avatars.githubusercontent.com/u/81944499?v=4" width="100" height="100" alt="philipnext"/><br /><sub><b>philipnext</b></sub></a>      |      <a href="https://github.com/refactorthis"><img src="https://avatars.githubusercontent.com/u/3012240?v=4" width="100" height="100" alt="refactorthis"/><br /><sub><b>refactorthis</b></sub></a>      |           <a href="https://github.com/samir-nimbly"><img src="https://avatars.githubusercontent.com/u/112695483?v=4" width="100" height="100" alt="samir-nimbly"/><br /><sub><b>samir-nimbly</b></sub></a>            |
+|              <a href="https://github.com/shaybc"><img src="https://avatars.githubusercontent.com/u/8535905?v=4" width="100" height="100" alt="shaybc"/><br /><sub><b>shaybc</b></sub></a>              |       <a href="https://github.com/shohei-ihaya"><img src="https://avatars.githubusercontent.com/u/25131938?v=4" width="100" height="100" alt="shohei-ihaya"/><br /><sub><b>shohei-ihaya</b></sub></a>       |   <a href="https://github.com/student20880"><img src="https://avatars.githubusercontent.com/u/74263488?v=4" width="100" height="100" alt="student20880"/><br /><sub><b>student20880</b></sub></a>   | <a href="https://github.com/PretzelVector"><img src="https://avatars.githubusercontent.com/u/95664360?v=4" width="100" height="100" alt="PretzelVector"/><br /><sub><b>PretzelVector</b></sub></a> |       <a href="https://github.com/adamwlarson"><img src="https://avatars.githubusercontent.com/u/1392315?v=4" width="100" height="100" alt="adamwlarson"/><br /><sub><b>adamwlarson</b></sub></a>        |                     <a href="https://github.com/alarno"><img src="https://avatars.githubusercontent.com/u/4355547?v=4" width="100" height="100" alt="alarno"/><br /><sub><b>alarno</b></sub></a>                      |
+| <a href="https://github.com/andreastempsch"><img src="https://avatars.githubusercontent.com/u/117991125?v=4" width="100" height="100" alt="andreastempsch"/><br /><sub><b>andreastempsch</b></sub></a> |              <a href="https://github.com/Atlogit"><img src="https://avatars.githubusercontent.com/u/86947554?v=4" width="100" height="100" alt="Atlogit"/><br /><sub><b>Atlogit</b></sub></a>               |              <a href="https://github.com/dleen"><img src="https://avatars.githubusercontent.com/u/1297964?v=4" width="100" height="100" alt="dleen"/><br /><sub><b>dleen</b></sub></a>              |        <a href="https://github.com/dbasclpy"><img src="https://avatars.githubusercontent.com/u/139889137?v=4" width="100" height="100" alt="dbasclpy"/><br /><sub><b>dbasclpy</b></sub></a>        | <a href="https://github.com/celestial-vault"><img src="https://avatars.githubusercontent.com/u/58194240?v=4" width="100" height="100" alt="celestial-vault"/><br /><sub><b>celestial-vault</b></sub></a> |                <a href="https://github.com/DeXtroTip"><img src="https://avatars.githubusercontent.com/u/21011087?v=4" width="100" height="100" alt="DeXtroTip"/><br /><sub><b>DeXtroTip</b></sub></a>                 |
+|              <a href="https://github.com/hesara"><img src="https://avatars.githubusercontent.com/u/1335918?v=4" width="100" height="100" alt="hesara"/><br /><sub><b>hesara</b></sub></a>              |           <a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100" height="100" alt="eltociear"/><br /><sub><b>eltociear</b></sub></a>            |   <a href="https://github.com/libertyteeth"><img src="https://avatars.githubusercontent.com/u/32841567?v=4" width="100" height="100" alt="libertyteeth"/><br /><sub><b>libertyteeth</b></sub></a>   | <a href="https://github.com/mamertofabian"><img src="https://avatars.githubusercontent.com/u/7698436?v=4" width="100" height="100" alt="mamertofabian"/><br /><sub><b>mamertofabian</b></sub></a>  |     <a href="https://github.com/marvijo-code"><img src="https://avatars.githubusercontent.com/u/82562019?v=4" width="100" height="100" alt="marvijo-code"/><br /><sub><b>marvijo-code</b></sub></a>      |                       <a href="https://github.com/Sarke"><img src="https://avatars.githubusercontent.com/u/2719310?v=4" width="100" height="100" alt="Sarke"/><br /><sub><b>Sarke</b></sub></a>                       |
+|               <a href="https://github.com/tgfjt"><img src="https://avatars.githubusercontent.com/u/2628239?v=4" width="100" height="100" alt="tgfjt"/><br /><sub><b>tgfjt</b></sub></a>                |           <a href="https://github.com/vladstudio"><img src="https://avatars.githubusercontent.com/u/914320?v=4" width="100" height="100" alt="vladstudio"/><br /><sub><b>vladstudio</b></sub></a>           |            <a href="https://github.com/ashktn"><img src="https://avatars.githubusercontent.com/u/6723913?v=4" width="100" height="100" alt="ashktn"/><br /><sub><b>ashktn</b></sub></a>             |                                                                                                                                                                                                    |                                                                                                                                                                                                          |                                                                                                                                                                                                                       |
+
+<!-- END CONTRIBUTORS SECTION -->
+
 ## License
 
 [Apache 2.0 © 2025 Roo Veterinary, Inc.](./LICENSE)

+ 0 - 8
esbuild.js

@@ -72,14 +72,6 @@ const extensionConfig = {
 		copyWasmFiles,
 		/* add to the end of plugins array */
 		esbuildProblemMatcherPlugin,
-		{
-			name: "alias-plugin",
-			setup(build) {
-				build.onResolve({ filter: /^pkce-challenge$/ }, (args) => {
-					return { path: require.resolve("pkce-challenge/dist/index.browser.js") }
-				})
-			},
-		},
 	],
 	entryPoints: ["src/extension.ts"],
 	format: "cjs",

File diff suppressed because it is too large
+ 34 - 621
package-lock.json


+ 4 - 5
package.json

@@ -3,7 +3,7 @@
 	"displayName": "Roo Code (prev. Roo Cline)",
 	"description": "A whole dev team of AI agents in your editor.",
 	"publisher": "RooVeterinaryInc",
-	"version": "3.8.5",
+	"version": "3.8.6",
 	"icon": "assets/icons/rocket.png",
 	"galleryBanner": {
 		"color": "#617A91",
@@ -307,7 +307,8 @@
 		"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
 		"watch-tests": "tsc -p . -w --outDir out",
 		"changeset": "changeset",
-		"knip": "knip --include files"
+		"knip": "knip --include files",
+		"update-contributors": "node scripts/update-contributors.js"
 	},
 	"dependencies": {
 		"@anthropic-ai/bedrock-sdk": "^0.10.2",
@@ -317,7 +318,7 @@
 		"@google-cloud/vertexai": "^1.9.3",
 		"@google/generative-ai": "^0.18.0",
 		"@mistralai/mistralai": "^1.3.6",
-		"@modelcontextprotocol/sdk": "^1.7.0",
+		"@modelcontextprotocol/sdk": "^1.0.1",
 		"@types/clone-deep": "^4.0.4",
 		"@types/pdf-parse": "^1.1.4",
 		"@types/tmp": "^0.2.6",
@@ -344,12 +345,10 @@
 		"os-name": "^6.0.0",
 		"p-wait-for": "^5.0.2",
 		"pdf-parse": "^1.1.1",
-		"pkce-challenge": "^4.1.0",
 		"posthog-node": "^4.7.0",
 		"pretty-bytes": "^6.1.1",
 		"puppeteer-chromium-resolver": "^23.0.0",
 		"puppeteer-core": "^23.4.0",
-		"reconnecting-eventsource": "^1.6.4",
 		"serialize-error": "^11.0.3",
 		"simple-git": "^3.27.0",
 		"sound-play": "^1.1.0",

+ 208 - 0
scripts/update-contributors.js

@@ -0,0 +1,208 @@
+#!/usr/bin/env node
+
+/**
+ * This script fetches contributor data from GitHub and updates the README.md file
+ * with a contributors section showing avatars and usernames.
+ */
+
+const https = require("https")
+const fs = require("fs")
+const path = require("path")
+
+// GitHub API URL for fetching contributors
+const GITHUB_API_URL = "https://api.github.com/repos/RooVetGit/Roo-Code/contributors?per_page=100"
+const README_PATH = path.join(__dirname, "..", "README.md")
+
+// Sentinel markers for contributors section
+const START_MARKER = "<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->"
+const END_MARKER = "<!-- END CONTRIBUTORS SECTION -->"
+
+// HTTP options for GitHub API request
+const options = {
+	headers: {
+		"User-Agent": "Roo-Code-Contributors-Script",
+	},
+}
+
+// Add GitHub token for authentication if available
+if (process.env.GITHUB_TOKEN) {
+	options.headers.Authorization = `token ${process.env.GITHUB_TOKEN}`
+	console.log("Using GitHub token from environment variable")
+}
+
+/**
+ * Fetches contributors data from GitHub API
+ * @returns {Promise<Array>} Array of contributor objects
+ */
+function fetchContributors() {
+	return new Promise((resolve, reject) => {
+		https
+			.get(GITHUB_API_URL, options, (res) => {
+				if (res.statusCode !== 200) {
+					reject(new Error(`GitHub API request failed with status code: ${res.statusCode}`))
+					return
+				}
+
+				let data = ""
+				res.on("data", (chunk) => {
+					data += chunk
+				})
+
+				res.on("end", () => {
+					try {
+						const contributors = JSON.parse(data)
+						resolve(contributors)
+					} catch (error) {
+						reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
+					}
+				})
+			})
+			.on("error", (error) => {
+				reject(new Error(`GitHub API request failed: ${error.message}`))
+			})
+	})
+}
+
+/**
+ * Reads the README.md file
+ * @returns {Promise<string>} README content
+ */
+function readReadme() {
+	return new Promise((resolve, reject) => {
+		fs.readFile(README_PATH, "utf8", (err, data) => {
+			if (err) {
+				reject(new Error(`Failed to read README.md: ${err.message}`))
+				return
+			}
+			resolve(data)
+		})
+	})
+}
+
+/**
+ * Creates HTML for the contributors section
+ * @param {Array} contributors Array of contributor objects from GitHub API
+ * @returns {string} HTML for contributors section
+ */
+function formatContributorsSection(contributors) {
+	// Filter out GitHub Actions bot
+	const filteredContributors = contributors.filter((c) => !c.login.includes("[bot]") && !c.login.includes("R00-B0T"))
+
+	// Start building with Markdown table format
+	let markdown = `${START_MARKER}
+## Contributors
+
+Thanks to all our contributors who have helped make Roo Code better!
+
+`
+	// Number of columns in the table
+	const COLUMNS = 6
+
+	// Create contributor cell HTML
+	const createCell = (contributor) => {
+		return `<a href="${contributor.html_url}"><img src="${contributor.avatar_url}" width="100" height="100" alt="${contributor.login}"/><br /><sub><b>${contributor.login}</b></sub></a>`
+	}
+
+	if (filteredContributors.length > 0) {
+		// Table header is the first row of contributors
+		const headerCells = filteredContributors.slice(0, COLUMNS).map(createCell)
+
+		// Fill any empty cells in header row
+		while (headerCells.length < COLUMNS) {
+			headerCells.push(" ")
+		}
+
+		// Add header row
+		markdown += `|${headerCells.join("|")}|\n`
+
+		// Add alignment row
+		markdown += "|"
+		for (let i = 0; i < COLUMNS; i++) {
+			markdown += ":---:|"
+		}
+		markdown += "\n"
+
+		// Add remaining contributor rows starting with the second batch
+		for (let i = COLUMNS; i < filteredContributors.length; i += COLUMNS) {
+			const rowContributors = filteredContributors.slice(i, i + COLUMNS)
+
+			// Create cells for each contributor in this row
+			const cells = rowContributors.map(createCell)
+
+			// Fill any empty cells to maintain table structure
+			while (cells.length < COLUMNS) {
+				cells.push(" ")
+			}
+
+			// Add row to the table
+			markdown += `|${cells.join("|")}|\n`
+		}
+	}
+
+	markdown += `${END_MARKER}`
+	return markdown
+}
+
+/**
+ * Updates the README.md file with contributors section
+ * @param {string} readmeContent Original README content
+ * @param {string} contributorsSection HTML for contributors section
+ * @returns {Promise<void>}
+ */
+function updateReadme(readmeContent, contributorsSection) {
+	// Find existing contributors section markers
+	const startPos = readmeContent.indexOf(START_MARKER)
+	const endPos = readmeContent.indexOf(END_MARKER)
+
+	if (startPos === -1 || endPos === -1) {
+		console.warn("Warning: Could not find contributors section markers in README.md")
+		console.warn("Skipping update - please add markers to enable automatic updates.")
+		return
+	}
+
+	// Replace existing section, trimming whitespace at section boundaries
+	const beforeSection = readmeContent.substring(0, startPos).trimEnd()
+	const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
+	// Ensure single newline separators between sections
+	const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
+
+	return writeReadme(updatedContent)
+}
+
+/**
+ * Writes updated content to README.md
+ * @param {string} content Updated README content
+ * @returns {Promise<void>}
+ */
+function writeReadme(content) {
+	return new Promise((resolve, reject) => {
+		fs.writeFile(README_PATH, content, "utf8", (err) => {
+			if (err) {
+				reject(new Error(`Failed to write updated README.md: ${err.message}`))
+				return
+			}
+			resolve()
+		})
+	})
+}
+
+/**
+ * Main function that orchestrates the update process
+ */
+async function main() {
+	try {
+		const contributors = await fetchContributors()
+
+		const readmeContent = await readReadme()
+
+		const contributorsSection = formatContributorsSection(contributors)
+
+		await updateReadme(readmeContent, contributorsSection)
+	} catch (error) {
+		console.error(`Error: ${error.message}`)
+		process.exit(1)
+	}
+}
+
+// Run the script
+main()

+ 0 - 14
src/__mocks__/@modelcontextprotocol/sdk/client/sse.js

@@ -1,14 +0,0 @@
-class SSEClientTransport {
-	constructor(url, options = {}) {
-		this.url = url
-		this.options = options
-		this.onerror = null
-		this.connect = jest.fn().mockResolvedValue()
-		this.close = jest.fn().mockResolvedValue()
-		this.start = jest.fn().mockResolvedValue()
-	}
-}
-
-module.exports = {
-	SSEClientTransport,
-}

+ 313 - 0
src/api/providers/__tests__/bedrock-invokedModelId.test.ts

@@ -0,0 +1,313 @@
+// Mock AWS SDK credential providers
+jest.mock("@aws-sdk/credential-providers", () => ({
+	fromIni: jest.fn().mockReturnValue({
+		accessKeyId: "profile-access-key",
+		secretAccessKey: "profile-secret-key",
+	}),
+}))
+
+import { AwsBedrockHandler, StreamEvent } from "../bedrock"
+import { ApiHandlerOptions } from "../../../shared/api"
+import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
+
+describe("AwsBedrockHandler with invokedModelId", () => {
+	let mockSend: jest.SpyInstance
+
+	beforeEach(() => {
+		// Mock the BedrockRuntimeClient.prototype.send method
+		mockSend = jest.spyOn(BedrockRuntimeClient.prototype, "send").mockImplementation(async () => {
+			return {
+				stream: createMockStream([]),
+			}
+		})
+	})
+
+	afterEach(() => {
+		mockSend.mockRestore()
+	})
+
+	// Helper function to create a mock async iterable stream
+	function createMockStream(events: StreamEvent[]) {
+		return {
+			[Symbol.asyncIterator]: async function* () {
+				for (const event of events) {
+					yield event
+				}
+				// Always yield a metadata event at the end
+				yield {
+					metadata: {
+						usage: {
+							inputTokens: 100,
+							outputTokens: 200,
+						},
+					},
+				}
+			},
+		}
+	}
+
+	it("should update costModelConfig when invokedModelId is present in the stream", async () => {
+		// Create a handler with a custom ARN
+		const mockOptions: ApiHandlerOptions = {
+			//	apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+			awsAccessKey: "test-access-key",
+			awsSecretKey: "test-secret-key",
+			awsRegion: "us-east-1",
+			awsCustomArn: "arn:aws:bedrock:us-west-2:699475926481:default-prompt-router/anthropic.claude:1",
+		}
+
+		const handler = new AwsBedrockHandler(mockOptions)
+
+		// Create a spy on the getModel method before mocking it
+		const getModelSpy = jest.spyOn(handler, "getModelByName")
+
+		// Mock the stream to include an event with invokedModelId and usage metadata
+		mockSend.mockImplementationOnce(async () => {
+			return {
+				stream: createMockStream([
+					// First event with invokedModelId and usage metadata
+					{
+						trace: {
+							promptRouter: {
+								invokedModelId:
+									"arn:aws:bedrock:us-west-2:699475926481:inference-profile/us.anthropic.claude-3-5-sonnet-20240620-v1:0",
+								usage: {
+									inputTokens: 150,
+									outputTokens: 250,
+								},
+							},
+						},
+						// Some content events
+					},
+					{
+						contentBlockStart: {
+							start: {
+								text: "Hello",
+							},
+							contentBlockIndex: 0,
+						},
+					},
+					{
+						contentBlockDelta: {
+							delta: {
+								text: ", world!",
+							},
+							contentBlockIndex: 0,
+						},
+					},
+				]),
+			}
+		})
+
+		// Create a message generator
+		const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
+
+		// Collect all yielded events to verify usage events
+		const events = []
+		for await (const event of messageGenerator) {
+			events.push(event)
+		}
+
+		// Verify that getModel was called with the correct model name
+		expect(getModelSpy).toHaveBeenCalledWith("anthropic.claude-3-5-sonnet-20240620-v1:0")
+
+		// Verify that getModel returns the updated model info
+		const costModel = handler.getModel()
+		expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20240620-v1:0")
+		expect(costModel.info.inputPrice).toBe(3)
+
+		// Verify that a usage event was emitted after updating the costModelConfig
+		const usageEvents = events.filter((event) => event.type === "usage")
+		expect(usageEvents.length).toBeGreaterThanOrEqual(1)
+
+		// The last usage event should have the token counts from the metadata
+		const lastUsageEvent = usageEvents[usageEvents.length - 1]
+		expect(lastUsageEvent).toEqual({
+			type: "usage",
+			inputTokens: 100,
+			outputTokens: 200,
+		})
+	})
+
+	it("should not update costModelConfig when invokedModelId is not present", async () => {
+		// Create a handler with default settings
+		const mockOptions: ApiHandlerOptions = {
+			apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+			awsAccessKey: "test-access-key",
+			awsSecretKey: "test-secret-key",
+			awsRegion: "us-east-1",
+		}
+
+		const handler = new AwsBedrockHandler(mockOptions)
+
+		// Mock the stream without an invokedModelId event
+		mockSend.mockImplementationOnce(async () => {
+			return {
+				stream: createMockStream([
+					// Some content events but no invokedModelId
+					{
+						contentBlockStart: {
+							start: {
+								text: "Hello",
+							},
+							contentBlockIndex: 0,
+						},
+					},
+					{
+						contentBlockDelta: {
+							delta: {
+								text: ", world!",
+							},
+							contentBlockIndex: 0,
+						},
+					},
+				]),
+			}
+		})
+
+		// Mock getModel to return expected values
+		const getModelSpy = jest.spyOn(handler, "getModel").mockReturnValue({
+			id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+			info: {
+				maxTokens: 4096,
+				contextWindow: 128_000,
+				supportsPromptCache: false,
+				supportsImages: true,
+			},
+		})
+
+		// Create a message generator
+		const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
+
+		// Consume the generator
+		for await (const _ of messageGenerator) {
+			// Just consume the messages
+		}
+
+		// Verify that getModel returns the original model info
+		const costModel = handler.getModel()
+		expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0")
+
+		// Verify getModel was not called with a model name parameter
+		expect(getModelSpy).not.toHaveBeenCalledWith(expect.any(String))
+	})
+
+	it("should handle invalid invokedModelId format gracefully", async () => {
+		// Create a handler with default settings
+		const mockOptions: ApiHandlerOptions = {
+			apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+			awsAccessKey: "test-access-key",
+			awsSecretKey: "test-secret-key",
+			awsRegion: "us-east-1",
+		}
+
+		const handler = new AwsBedrockHandler(mockOptions)
+
+		// Mock the stream with an invalid invokedModelId
+		mockSend.mockImplementationOnce(async () => {
+			return {
+				stream: createMockStream([
+					// Event with invalid invokedModelId format
+					{
+						trace: {
+							promptRouter: {
+								invokedModelId: "invalid-format-not-an-arn",
+							},
+						},
+					},
+					// Some content events
+					{
+						contentBlockStart: {
+							start: {
+								text: "Hello",
+							},
+							contentBlockIndex: 0,
+						},
+					},
+				]),
+			}
+		})
+
+		// Mock getModel to return expected values
+		const getModelSpy = jest.spyOn(handler, "getModel").mockReturnValue({
+			id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+			info: {
+				maxTokens: 4096,
+				contextWindow: 128_000,
+				supportsPromptCache: false,
+				supportsImages: true,
+			},
+		})
+
+		// Create a message generator
+		const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
+
+		// Consume the generator
+		for await (const _ of messageGenerator) {
+			// Just consume the messages
+		}
+
+		// Verify that getModel returns the original model info
+		const costModel = handler.getModel()
+		expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0")
+	})
+
+	it("should handle errors during invokedModelId processing", async () => {
+		// Create a handler with default settings
+		const mockOptions: ApiHandlerOptions = {
+			apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+			awsAccessKey: "test-access-key",
+			awsSecretKey: "test-secret-key",
+			awsRegion: "us-east-1",
+		}
+
+		const handler = new AwsBedrockHandler(mockOptions)
+
+		// Mock the stream with a valid invokedModelId
+		mockSend.mockImplementationOnce(async () => {
+			return {
+				stream: createMockStream([
+					// Event with valid invokedModelId
+					{
+						trace: {
+							promptRouter: {
+								invokedModelId:
+									"arn:aws:bedrock:us-east-1:123456789:foundation-model/anthropic.claude-3-sonnet-20240229-v1:0",
+							},
+						},
+					},
+				]),
+			}
+		})
+
+		// Mock getModel to throw an error when called with the model name
+		jest.spyOn(handler, "getModel").mockImplementation((modelName?: string) => {
+			if (modelName === "anthropic.claude-3-sonnet-20240229-v1:0") {
+				throw new Error("Test error during model lookup")
+			}
+
+			// Default return value for initial call
+			return {
+				id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			}
+		})
+
+		// Create a message generator
+		const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
+
+		// Consume the generator
+		for await (const _ of messageGenerator) {
+			// Just consume the messages
+		}
+
+		// Verify that getModel returns the original model info
+		const costModel = handler.getModel()
+		expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0")
+	})
+})

+ 186 - 1
src/api/providers/__tests__/bedrock.test.ts

@@ -327,10 +327,36 @@ describe("AwsBedrockHandler", () => {
 			const modelInfo = customArnHandler.getModel()
 			expect(modelInfo.id).toBe("arn:aws:bedrock:us-east-1::foundation-model/custom-model")
 			expect(modelInfo.info.maxTokens).toBe(4096)
-			expect(modelInfo.info.contextWindow).toBe(128_000)
+			expect(modelInfo.info.contextWindow).toBe(200_000)
 			expect(modelInfo.info.supportsPromptCache).toBe(false)
 		})
 
+		it("should correctly identify model info from inference profile ARN", () => {
+			//this test intentionally uses a model that has different maxTokens, contextWindow and other values than the fall back option in the code
+			const customArnHandler = new AwsBedrockHandler({
+				apiModelId: "meta.llama3-8b-instruct-v1:0", // This will be ignored when awsCustomArn is provided
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-west-2",
+				awsCustomArn:
+					"arn:aws:bedrock:us-west-2:699475926481:inference-profile/us.meta.llama3-8b-instruct-v1:0",
+			})
+			const modelInfo = customArnHandler.getModel()
+
+			// Verify the ARN is used as the model ID
+			expect(modelInfo.id).toBe(
+				"arn:aws:bedrock:us-west-2:699475926481:inference-profile/us.meta.llama3-8b-instruct-v1:0",
+			)
+
+			//these should not be the default fall back. they should be Llama's config
+			expect(modelInfo.info.maxTokens).toBe(2048)
+			expect(modelInfo.info.contextWindow).toBe(4_000)
+			expect(modelInfo.info.supportsImages).toBe(false)
+			expect(modelInfo.info.supportsPromptCache).toBe(false)
+
+			// This test highlights that the regex in getModel needs to be updated to handle inference-profile ARNs
+		})
+
 		it("should use default model when custom-arn is selected but no ARN is provided", () => {
 			const customArnHandler = new AwsBedrockHandler({
 				apiModelId: "custom-arn",
@@ -345,4 +371,163 @@ describe("AwsBedrockHandler", () => {
 			expect(modelInfo.info).toBeDefined()
 		})
 	})
+
+	describe("invokedModelId handling", () => {
+		it("should update costModelConfig when invokedModelId is present in custom ARN scenario", async () => {
+			const customArnHandler = new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+				awsCustomArn: "arn:aws:bedrock:us-east-1:123456789:foundation-model/custom-model",
+			})
+
+			const mockStreamEvent = {
+				trace: {
+					promptRouter: {
+						invokedModelId: "arn:aws:bedrock:us-east-1:123456789:foundation-model/custom-model:0",
+					},
+				},
+			}
+
+			jest.spyOn(customArnHandler, "getModel").mockReturnValue({
+				id: "custom-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+
+			await customArnHandler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
+
+			expect(customArnHandler.getModel()).toEqual({
+				id: "custom-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+		})
+
+		it("should update costModelConfig when invokedModelId is present in default model scenario", async () => {
+			handler = new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+			})
+
+			const mockStreamEvent = {
+				trace: {
+					promptRouter: {
+						invokedModelId: "arn:aws:bedrock:us-east-1:123456789:foundation-model/default-model:0",
+					},
+				},
+			}
+
+			jest.spyOn(handler, "getModel").mockReturnValue({
+				id: "default-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+
+			await handler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
+
+			expect(handler.getModel()).toEqual({
+				id: "default-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+		})
+
+		it("should not update costModelConfig when invokedModelId is not present", async () => {
+			handler = new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+			})
+
+			const mockStreamEvent = {
+				trace: {
+					promptRouter: {
+						// No invokedModelId present
+					},
+				},
+			}
+
+			jest.spyOn(handler, "getModel").mockReturnValue({
+				id: "default-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+
+			await handler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
+
+			expect(handler.getModel()).toEqual({
+				id: "default-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+		})
+
+		it("should not update costModelConfig when invokedModelId cannot be parsed", async () => {
+			handler = new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+			})
+
+			const mockStreamEvent = {
+				trace: {
+					promptRouter: {
+						invokedModelId: "invalid-arn",
+					},
+				},
+			}
+
+			jest.spyOn(handler, "getModel").mockReturnValue({
+				id: "default-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+
+			await handler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
+
+			expect(handler.getModel()).toEqual({
+				id: "default-model",
+				info: {
+					maxTokens: 4096,
+					contextWindow: 128_000,
+					supportsPromptCache: false,
+					supportsImages: true,
+				},
+			})
+		})
+	})
 })

+ 112 - 116
src/api/providers/bedrock.ts

@@ -3,11 +3,19 @@ import {
 	ConverseStreamCommand,
 	ConverseCommand,
 	BedrockRuntimeClientConfig,
+	ConverseStreamCommandOutput,
 } from "@aws-sdk/client-bedrock-runtime"
 import { fromIni } from "@aws-sdk/credential-providers"
 import { Anthropic } from "@anthropic-ai/sdk"
 import { SingleCompletionHandler } from "../"
-import { ApiHandlerOptions, BedrockModelId, ModelInfo, bedrockDefaultModelId, bedrockModels } from "../../shared/api"
+import {
+	ApiHandlerOptions,
+	BedrockModelId,
+	ModelInfo,
+	bedrockDefaultModelId,
+	bedrockModels,
+	bedrockDefaultPromptRouterModelId,
+} from "../../shared/api"
 import { ApiStream } from "../transform/stream"
 import { convertToBedrockConverseMessages } from "../transform/bedrock-converse-format"
 import { BaseProvider } from "./base-provider"
@@ -21,7 +29,8 @@ import { logger } from "../../utils/logging"
  */
 function validateBedrockArn(arn: string, region?: string) {
 	// Validate ARN format
-	const arnRegex = /^arn:aws:bedrock:([^:]+):(\d+):(foundation-model|provisioned-model|default-prompt-router)\/(.+)$/
+	const arnRegex =
+		/^arn:aws:bedrock:([^:]+):(\d+):(foundation-model|provisioned-model|default-prompt-router|prompt-router)\/(.+)$/
 	const match = arn.match(arnRegex)
 
 	if (!match) {
@@ -86,12 +95,27 @@ export interface StreamEvent {
 			latencyMs: number
 		}
 	}
+	trace?: {
+		promptRouter?: {
+			invokedModelId?: string
+			usage?: {
+				inputTokens: number
+				outputTokens: number
+				totalTokens?: number // Made optional since we don't use it
+			}
+		}
+	}
 }
 
 export class AwsBedrockHandler extends BaseProvider implements SingleCompletionHandler {
 	protected options: ApiHandlerOptions
 	private client: BedrockRuntimeClient
 
+	private costModelConfig: { id: BedrockModelId | string; info: ModelInfo } = {
+		id: "",
+		info: { maxTokens: 0, contextWindow: 0, supportsPromptCache: false, supportsImages: false },
+	}
+
 	constructor(options: ApiHandlerOptions) {
 		super()
 		this.options = options
@@ -141,8 +165,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
 	}
 
 	override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
-		const modelConfig = this.getModel()
-
+		let modelConfig = this.getModel()
 		// Handle cross-region inference
 		let modelId: string
 
@@ -250,8 +273,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
 					continue
 				}
 
-				// Handle metadata events first
-				if (streamEvent.metadata?.usage) {
+				// Handle metadata events first.
+				if (streamEvent?.metadata?.usage) {
 					yield {
 						type: "usage",
 						inputTokens: streamEvent.metadata.usage.inputTokens || 0,
@@ -260,6 +283,37 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
 					continue
 				}
 
+				if (streamEvent?.trace?.promptRouter?.invokedModelId) {
+					try {
+						const invokedModelId = streamEvent.trace.promptRouter.invokedModelId
+						const modelMatch = invokedModelId.match(/\/([^\/]+)(?::|$)/)
+						if (modelMatch && modelMatch[1]) {
+							let modelName = modelMatch[1]
+
+							// Get a new modelConfig from getModel() using invokedModelId.. remove the region first
+							let region = modelName.slice(0, 3)
+
+							if (region === "us." || region === "eu.") modelName = modelName.slice(3)
+							this.costModelConfig = this.getModelByName(modelName)
+						}
+
+						// Handle metadata events for the promptRouter.
+						if (streamEvent?.trace?.promptRouter?.usage) {
+							yield {
+								type: "usage",
+								inputTokens: streamEvent?.trace?.promptRouter?.usage?.inputTokens || 0,
+								outputTokens: streamEvent?.trace?.promptRouter?.usage?.outputTokens || 0,
+							}
+							continue
+						}
+					} catch (error) {
+						logger.error("Error handling Bedrock invokedModelId", {
+							ctx: "bedrock",
+							error: error instanceof Error ? error : String(error),
+						})
+					}
+				}
+
 				// Handle message start
 				if (streamEvent.messageStart) {
 					continue
@@ -282,7 +336,6 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
 					}
 					continue
 				}
-
 				// Handle message stop
 				if (streamEvent.messageStop) {
 					continue
@@ -428,122 +481,75 @@ Please check:
 		}
 	}
 
+	//Prompt Router responses come back in a different sequence and the yield calls are not resulting in costs getting updated
+	getModelByName(modelName: string): { id: BedrockModelId | string; info: ModelInfo } {
+		// Try to find the model in bedrockModels
+		if (modelName in bedrockModels) {
+			const id = modelName as BedrockModelId
+
+			//Do a deep copy of the model info so that later in the code the model id and maxTokens can be set.
+			// The bedrockModels array is a constant and updating the model ID from the returned invokedModelID value
+			// in a prompt router response isn't possible on the constant.
+			let model = JSON.parse(JSON.stringify(bedrockModels[id]))
+
+			// If modelMaxTokens is explicitly set in options, override the default
+			if (this.options.modelMaxTokens && this.options.modelMaxTokens > 0) {
+				model.maxTokens = this.options.modelMaxTokens
+			}
+
+			return { id, info: model }
+		}
+
+		return { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
+	}
+
 	override getModel(): { id: BedrockModelId | string; info: ModelInfo } {
+		if (this.costModelConfig.id.trim().length > 0) {
+			return this.costModelConfig
+		}
+
 		// If custom ARN is provided, use it
 		if (this.options.awsCustomArn) {
-			// Custom ARNs should not be modified with region prefixes
-			// as they already contain the full resource path
-
-			// Check if the ARN contains information about the model type
-			// This helps set appropriate token limits for models behind prompt routers
-			const arnLower = this.options.awsCustomArn.toLowerCase()
-
-			// Determine model info based on ARN content
-			let modelInfo: ModelInfo
-
-			if (arnLower.includes("claude-3-7-sonnet") || arnLower.includes("claude-3.7-sonnet")) {
-				// Claude 3.7 Sonnet has 8192 tokens in Bedrock
-				modelInfo = {
-					maxTokens: 8192,
-					contextWindow: 200_000,
-					supportsPromptCache: false,
-					supportsImages: true,
-					supportsComputerUse: true,
-				}
-			} else if (arnLower.includes("claude-3-5-sonnet") || arnLower.includes("claude-3.5-sonnet")) {
-				// Claude 3.5 Sonnet has 8192 tokens in Bedrock
-				modelInfo = {
-					maxTokens: 8192,
-					contextWindow: 200_000,
-					supportsPromptCache: false,
-					supportsImages: true,
-					supportsComputerUse: true,
-				}
-			} else if (arnLower.includes("claude-3-opus") || arnLower.includes("claude-3.0-opus")) {
-				// Claude 3 Opus has 4096 tokens in Bedrock
-				modelInfo = {
-					maxTokens: 4096,
-					contextWindow: 200_000,
-					supportsPromptCache: false,
-					supportsImages: true,
-				}
-			} else if (arnLower.includes("claude-3-haiku") || arnLower.includes("claude-3.0-haiku")) {
-				// Claude 3 Haiku has 4096 tokens in Bedrock
-				modelInfo = {
-					maxTokens: 4096,
-					contextWindow: 200_000,
-					supportsPromptCache: false,
-					supportsImages: true,
-				}
-			} else if (arnLower.includes("claude-3-5-haiku") || arnLower.includes("claude-3.5-haiku")) {
-				// Claude 3.5 Haiku has 8192 tokens in Bedrock
-				modelInfo = {
-					maxTokens: 8192,
-					contextWindow: 200_000,
-					supportsPromptCache: false,
-					supportsImages: false,
-				}
-			} else if (arnLower.includes("claude")) {
-				// Generic Claude model with conservative token limit
-				modelInfo = {
-					maxTokens: 4096,
-					contextWindow: 128_000,
-					supportsPromptCache: false,
-					supportsImages: true,
-				}
-			} else if (arnLower.includes("llama3") || arnLower.includes("llama-3")) {
-				// Llama 3 models typically have 8192 tokens in Bedrock
-				modelInfo = {
-					maxTokens: 8192,
-					contextWindow: 128_000,
-					supportsPromptCache: false,
-					supportsImages: arnLower.includes("90b") || arnLower.includes("11b"),
-				}
-			} else if (arnLower.includes("nova-pro")) {
-				// Amazon Nova Pro
-				modelInfo = {
-					maxTokens: 5000,
-					contextWindow: 300_000,
-					supportsPromptCache: false,
-					supportsImages: true,
-				}
-			} else {
-				// Default for unknown models or prompt routers
-				modelInfo = {
-					maxTokens: 4096,
-					contextWindow: 128_000,
-					supportsPromptCache: false,
-					supportsImages: true,
+			// Extract the model name from the ARN
+			const arnMatch = this.options.awsCustomArn.match(
+				/^arn:aws:bedrock:([^:]+):(\d+):(inference-profile|foundation-model|provisioned-model)\/(.+)$/,
+			)
+
+			let modelName = arnMatch ? arnMatch[4] : ""
+			if (modelName) {
+				let region = modelName.slice(0, 3)
+				if (region === "us." || region === "eu.") modelName = modelName.slice(3)
+
+				let modelData = this.getModelByName(modelName)
+				modelData.id = this.options.awsCustomArn
+
+				if (modelData) {
+					return modelData
 				}
 			}
 
-			// If modelMaxTokens is explicitly set in options, override the default
-			if (this.options.modelMaxTokens && this.options.modelMaxTokens > 0) {
-				modelInfo.maxTokens = this.options.modelMaxTokens
-			}
+			// An ARN was used, but no model info match found, use default values based on common patterns
+			let model = this.getModelByName(bedrockDefaultPromptRouterModelId)
 
+			// For custom ARNs, always return the specific values expected by tests
 			return {
 				id: this.options.awsCustomArn,
-				info: modelInfo,
+				info: model.info,
 			}
 		}
 
-		const modelId = this.options.apiModelId
-		if (modelId) {
+		if (this.options.apiModelId) {
 			// Special case for custom ARN option
-			if (modelId === "custom-arn") {
+			if (this.options.apiModelId === "custom-arn") {
 				// This should not happen as we should have awsCustomArn set
 				// but just in case, return a default model
-				return {
-					id: bedrockDefaultModelId,
-					info: bedrockModels[bedrockDefaultModelId],
-				}
+				return this.getModelByName(bedrockDefaultModelId)
 			}
 
-			// For tests, allow any model ID
+			// For tests, allow any model ID (but not custom ARNs, which are handled above)
 			if (process.env.NODE_ENV === "test") {
 				return {
-					id: modelId,
+					id: this.options.apiModelId,
 					info: {
 						maxTokens: 5000,
 						contextWindow: 128_000,
@@ -552,15 +558,9 @@ Please check:
 				}
 			}
 			// For production, validate against known models
-			if (modelId in bedrockModels) {
-				const id = modelId as BedrockModelId
-				return { id, info: bedrockModels[id] }
-			}
-		}
-		return {
-			id: bedrockDefaultModelId,
-			info: bedrockModels[bedrockDefaultModelId],
+			return this.getModelByName(this.options.apiModelId)
 		}
+		return this.getModelByName(bedrockDefaultModelId)
 	}
 
 	async completePrompt(prompt: string): Promise<string> {
@@ -573,10 +573,6 @@ Please check:
 			// For custom ARNs, use the ARN directly without modification
 			if (this.options.awsCustomArn) {
 				modelId = modelConfig.id
-				logger.debug("Using custom ARN in completePrompt", {
-					ctx: "bedrock",
-					customArn: this.options.awsCustomArn,
-				})
 
 				// Validate ARN format and check region match
 				const clientRegion = this.client.config.region as string

File diff suppressed because it is too large
+ 667 - 374
src/core/prompts/__tests__/__snapshots__/system.test.ts.snap


+ 4 - 73
src/core/prompts/__tests__/system.test.ts

@@ -250,51 +250,6 @@ describe("SYSTEM_PROMPT", () => {
 			true, // enableMcpServerCreation
 		)
 
-		// Verify basic MCP server info is included
-		expect(prompt).toContain("MCP SERVERS")
-		expect(prompt).toContain("The Model Context Protocol (MCP) enables communication")
-
-		// Verify server types are described
-		expect(prompt).toContain("Local (Stdio-based) servers")
-		expect(prompt).toContain("Remote (SSE-based) servers")
-
-		// Verify connected servers section exists
-		expect(prompt).toContain("Connected MCP Servers")
-		expect(prompt).toContain("When a server is connected")
-
-		// Verify server creation info is included
-		expect(prompt).toContain("Creating an MCP Server")
-		expect(prompt).toContain("MCP Server Types and Configuration")
-
-		// Verify configuration examples are included
-		expect(prompt).toContain("Local (Stdio) Server Configuration")
-		expect(prompt).toContain("Remote (SSE) Server Configuration")
-		expect(prompt).toContain("Common configuration options for both types")
-
-		// Verify example server info is included
-		expect(prompt).toContain("Example Local MCP Server")
-		expect(prompt).toContain("create-typescript-server")
-	})
-
-	it("should include MCP server creation info when enabled", async () => {
-		const mockMcpHub = createMockMcpHub()
-
-		const prompt = await SYSTEM_PROMPT(
-			mockContext,
-			"/test/path",
-			false,
-			mockMcpHub,
-			undefined,
-			undefined,
-			defaultModeSlug,
-			undefined,
-			undefined,
-			undefined,
-			undefined,
-			experiments,
-			true,
-		)
-
 		expect(prompt).toMatchSnapshot()
 	})
 
@@ -315,10 +270,7 @@ describe("SYSTEM_PROMPT", () => {
 			true, // enableMcpServerCreation
 		)
 
-		// Should not contain any MCP server related content since mcpHub is undefined
-		expect(prompt).not.toContain("MCP SERVERS")
-		expect(prompt).not.toContain("Connected MCP Servers")
-		expect(prompt).not.toContain("Creating an MCP Server")
+		expect(prompt).toMatchSnapshot()
 	})
 
 	it("should handle different browser viewport sizes", async () => {
@@ -781,18 +733,8 @@ describe("addCustomInstructions", () => {
 			true, // enableMcpServerCreation
 		)
 
-		// Verify server creation info is included
 		expect(prompt).toContain("Creating an MCP Server")
-		expect(prompt).toContain("MCP Server Types and Configuration")
-
-		// Verify configuration examples are included
-		expect(prompt).toContain("Local (Stdio) Server Configuration")
-		expect(prompt).toContain("Remote (SSE) Server Configuration")
-		expect(prompt).toContain("Common configuration options for both types")
-
-		// Verify example server info is included
-		expect(prompt).toContain("Example Local MCP Server")
-		expect(prompt).toContain("create-typescript-server")
+		expect(prompt).toMatchSnapshot()
 	})
 
 	it("should exclude MCP server creation info when disabled", async () => {
@@ -815,23 +757,12 @@ describe("addCustomInstructions", () => {
 		)
 
 		expect(prompt).not.toContain("Creating an MCP Server")
-		expect(prompt).not.toContain("MCP Server Types and Configuration")
-		expect(prompt).toContain("MCP SERVERS")
-		expect(prompt).toContain("Connected MCP Servers")
+		expect(prompt).toMatchSnapshot()
 	})
 
 	it("should prioritize mode-specific rules for code mode", async () => {
 		const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
-		const rulesSection = instructions.split("\n\n").find((section) => section.startsWith("Rules:"))
-		expect(rulesSection).toBeDefined()
-
-		// Get the rules as separate lines and filter for header lines only
-		const ruleLines = rulesSection?.split("\n").filter((line) => line.startsWith("#")) ?? []
-
-		// Verify we have both rule headers
-		expect(ruleLines).toHaveLength(2)
-		expect(ruleLines[0]).toContain(`Rules from .clinerules-${defaultModeSlug}`)
-		expect(ruleLines[1]).toContain("Rules from .clinerules")
+		expect(instructions).toMatchSnapshot()
 	})
 
 	it("should prioritize mode-specific rules for ask mode", async () => {

+ 4 - 45
src/core/prompts/sections/mcp-servers.ts

@@ -49,10 +49,7 @@ export async function getMcpServersSection(
 
 	const baseSection = `MCP SERVERS
 
-The Model Context Protocol (MCP) enables communication between the system and MCP servers that provide additional tools and resources to extend your capabilities. MCP servers can be one of two types:
-
-1. Local (Stdio-based) servers: These run locally on the user's machine and communicate via standard input/output
-2. Remote (SSE-based) servers: These run on remote machines and communicate via Server-Sent Events (SSE) over HTTP/HTTPS
+The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
 
 # Connected MCP Servers
 
@@ -74,51 +71,13 @@ The user may ask you something along the lines of "add a tool" that does some fu
 
 When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
 
-Unless the user specifies otherwise, new local MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
-
-### MCP Server Types and Configuration
-
-MCP servers can be configured in two ways in the MCP settings file:
-
-1. Local (Stdio) Server Configuration:
-\`\`\`json
-{
-  "mcpServers": {
-    "local-weather": {
-      "command": "node",
-      "args": ["/path/to/weather-server/build/index.js"],
-      "env": {
-        "OPENWEATHER_API_KEY": "your-api-key"
-      }
-    }
-  }
-}
-\`\`\`
-
-2. Remote (SSE) Server Configuration:
-\`\`\`json
-{
-  "mcpServers": {
-    "remote-weather": {
-      "url": "https://api.example.com/mcp",
-      "headers": {
-        "Authorization": "Bearer your-api-key"
-      }
-    }
-  }
-}
-\`\`\`
-
-Common configuration options for both types:
-- \`disabled\`: (optional) Set to true to temporarily disable the server
-- \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60)
-- \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation
+Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
 
-### Example Local MCP Server
+### Example MCP Server
 
 For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
 
-The following example demonstrates how to build a local MCP server that provides weather data functionality using the Stdio transport. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
+The following example demonstrates how to build an MCP server that provides weather data functionality. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
 
 1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
 

+ 78 - 111
src/services/mcp/McpHub.ts

@@ -1,7 +1,5 @@
 import { Client } from "@modelcontextprotocol/sdk/client/index.js"
 import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
-import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
-import ReconnectingEventSource from "reconnecting-eventsource"
 import {
 	CallToolResultSchema,
 	ListResourcesResultSchema,
@@ -33,40 +31,23 @@ import { arePathsEqual } from "../../utils/path"
 export type McpConnection = {
 	server: McpServer
 	client: Client
-	transport: StdioClientTransport | SSEClientTransport
+	transport: StdioClientTransport
 }
 
-// Base configuration schema for common settings
-const BaseConfigSchema = z.object({
+// StdioServerParameters
+const AlwaysAllowSchema = z.array(z.string()).default([])
+
+export const StdioConfigSchema = z.object({
+	command: z.string(),
+	args: z.array(z.string()).optional(),
+	env: z.record(z.string()).optional(),
+	alwaysAllow: AlwaysAllowSchema.optional(),
 	disabled: z.boolean().optional(),
 	timeout: z.number().min(1).max(3600).optional().default(60),
-	alwaysAllow: z.array(z.string()).default([]),
 })
 
-// Server configuration schema with automatic type inference
-export const ServerConfigSchema = z.union([
-	// Stdio config (has command field)
-	BaseConfigSchema.extend({
-		command: z.string(),
-		args: z.array(z.string()).optional(),
-		env: z.record(z.string()).optional(),
-	}).transform((data) => ({
-		...data,
-		type: "stdio" as const,
-	})),
-	// SSE config (has url field)
-	BaseConfigSchema.extend({
-		url: z.string().url(),
-		headers: z.record(z.string()).optional(),
-	}).transform((data) => ({
-		...data,
-		type: "sse" as const,
-	})),
-])
-
-// Settings schema
 const McpSettingsSchema = z.object({
-	mcpServers: z.record(ServerConfigSchema),
+	mcpServers: z.record(StdioConfigSchema),
 })
 
 export class McpHub {
@@ -75,7 +56,6 @@ export class McpHub {
 	private settingsWatcher?: vscode.FileSystemWatcher
 	private projectMcpWatcher?: vscode.FileSystemWatcher
 	private fileWatchers: Map<string, FSWatcher> = new Map()
-	private isDisposed: boolean = false
 	connections: McpConnection[] = []
 	isConnecting: boolean = false
 
@@ -267,10 +247,11 @@ export class McpHub {
 		config: z.infer<typeof ServerConfigSchema>,
 		source: "global" | "project" = "global",
 	): Promise<void> {
-		// Remove existing connection if it exists
-		await this.deleteConnection(name)
+		// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
+		this.connections = this.connections.filter((conn) => conn.server.name !== name)
 
 		try {
+			// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
 			const client = new Client(
 				{
 					name: "Roo Code",
@@ -281,96 +262,60 @@ export class McpHub {
 				},
 			)
 
-			let transport: StdioClientTransport | SSEClientTransport
-
-			if (config.type === "stdio") {
-				transport = new StdioClientTransport({
-					command: config.command,
-					args: config.args,
-					env: {
-						...config.env,
-						...(process.env.PATH ? { PATH: process.env.PATH } : {}),
-					},
-					stderr: "pipe",
-				})
+			const transport = new StdioClientTransport({
+				command: config.command,
+				args: config.args,
+				env: {
+					...config.env,
+					...(process.env.PATH ? { PATH: process.env.PATH } : {}),
+					// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
+				},
+				stderr: "pipe", // necessary for stderr to be available
+			})
 
-				// Set up stdio specific error handling
-				transport.onerror = async (error) => {
-					console.error(`Transport error for "${name}":`, error)
-					const connection = this.connections.find((conn) => conn.server.name === name)
-					if (connection) {
-						connection.server.status = "disconnected"
-						this.appendErrorMessage(connection, error.message)
-					}
-					await this.notifyWebviewOfServerChanges()
+			transport.onerror = async (error) => {
+				console.error(`Transport error for "${name}":`, error)
+				const connection = this.connections.find((conn) => conn.server.name === name)
+				if (connection) {
+					connection.server.status = "disconnected"
+					this.appendErrorMessage(connection, error.message)
 				}
+				await this.notifyWebviewOfServerChanges()
+			}
 
-				transport.onclose = async () => {
-					const connection = this.connections.find((conn) => conn.server.name === name)
-					if (connection) {
-						connection.server.status = "disconnected"
-					}
-					await this.notifyWebviewOfServerChanges()
+			transport.onclose = async () => {
+				const connection = this.connections.find((conn) => conn.server.name === name)
+				if (connection) {
+					connection.server.status = "disconnected"
 				}
+				await this.notifyWebviewOfServerChanges()
+			}
 
-				// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
-				// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
-				await transport.start()
-				const stderrStream = transport.stderr
-				if (stderrStream) {
-					stderrStream.on("data", async (data: Buffer) => {
-						const errorOutput = data.toString()
-						console.error(`Server "${name}" stderr:`, errorOutput)
-						const connection = this.connections.find((conn) => conn.server.name === name)
-						if (connection) {
-							// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
-							this.appendErrorMessage(connection, errorOutput)
-							// Only need to update webview right away if it's already disconnected
-							if (connection.server.status === "disconnected") {
-								await this.notifyWebviewOfServerChanges()
-							}
-						}
-					})
-				} else {
-					console.error(`No stderr stream for ${name}`)
-				}
-				transport.start = async () => {} // No-op now, .connect() won't fail
-			} else {
-				// SSE connection
-				const sseOptions = {
-					requestInit: {
-						headers: config.headers,
+			// If the config is invalid, show an error
+			if (!StdioConfigSchema.safeParse(config).success) {
+				console.error(`Invalid config for "${name}": missing or invalid parameters`)
+				const connection: McpConnection = {
+					server: {
+						name,
+						config: JSON.stringify(config),
+						status: "disconnected",
+						error: "Invalid config: missing or invalid parameters",
 					},
+					client,
+					transport,
 				}
-				// Configure ReconnectingEventSource options
-				const reconnectingEventSourceOptions = {
-					max_retry_time: 5000, // Maximum retry time in milliseconds
-					withCredentials: config.headers?.["Authorization"] ? true : false, // Enable credentials if Authorization header exists
-				}
-				global.EventSource = ReconnectingEventSource
-				transport = new SSEClientTransport(new URL(config.url), {
-					...sseOptions,
-					eventSourceInit: reconnectingEventSourceOptions,
-				})
-
-				// Set up SSE specific error handling
-				transport.onerror = async (error) => {
-					console.error(`Transport error for "${name}":`, error)
-					const connection = this.connections.find((conn) => conn.server.name === name)
-					if (connection) {
-						connection.server.status = "disconnected"
-						this.appendErrorMessage(connection, error.message)
-					}
-					await this.notifyWebviewOfServerChanges()
-				}
+				this.connections.push(connection)
+				return
 			}
 
+			// valid schema
+			const parsedConfig = StdioConfigSchema.parse(config)
 			const connection: McpConnection = {
 				server: {
 					name,
 					config: JSON.stringify(config),
 					status: "connecting",
-					disabled: config.disabled,
+					disabled: parsedConfig.disabled,
 					source,
 					projectPath: source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined,
 				},
@@ -379,7 +324,30 @@ export class McpHub {
 			}
 			this.connections.push(connection)
 
-			// Connect (this will automatically start the transport)
+			// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
+			// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
+			await transport.start()
+			const stderrStream = transport.stderr
+			if (stderrStream) {
+				stderrStream.on("data", async (data: Buffer) => {
+					const errorOutput = data.toString()
+					console.error(`Server "${name}" stderr:`, errorOutput)
+					const connection = this.connections.find((conn) => conn.server.name === name)
+					if (connection) {
+						// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
+						this.appendErrorMessage(connection, errorOutput)
+						// Only need to update webview right away if it's already disconnected
+						if (connection.server.status === "disconnected") {
+							await this.notifyWebviewOfServerChanges()
+						}
+					}
+				})
+			} else {
+				console.error(`No stderr stream for ${name}`)
+			}
+			transport.start = async () => {} // No-op now, .connect() won't fail
+
+			// Connect
 			await client.connect(transport)
 			connection.server.status = "connected"
 			connection.server.error = ""
@@ -807,7 +775,7 @@ export class McpHub {
 
 		let timeout: number
 		try {
-			const parsedConfig = ServerConfigSchema.parse(JSON.parse(connection.server.config))
+			const parsedConfig = StdioConfigSchema.parse(JSON.parse(connection.server.config))
 			timeout = (parsedConfig.timeout ?? 60) * 1000
 		} catch (error) {
 			console.error("Failed to parse server config for timeout:", error)
@@ -869,7 +837,6 @@ export class McpHub {
 	}
 
 	async dispose(): Promise<void> {
-		this.isDisposed = true
 		this.removeAllFileWatchers()
 		for (const connection of this.connections) {
 			try {

+ 8 - 19
src/services/mcp/__tests__/McpHub.test.ts

@@ -2,7 +2,7 @@ import type { McpHub as McpHubType } from "../McpHub"
 import type { ClineProvider } from "../../../core/webview/ClineProvider"
 import type { ExtensionContext, Uri } from "vscode"
 import type { McpConnection } from "../McpHub"
-import { ServerConfigSchema } from "../McpHub"
+import { StdioConfigSchema } from "../McpHub"
 
 const fs = require("fs/promises")
 const { McpHub } = require("../McpHub")
@@ -71,7 +71,6 @@ describe("McpHub", () => {
 			JSON.stringify({
 				mcpServers: {
 					"test-server": {
-						type: "stdio",
 						command: "node",
 						args: ["test.js"],
 						alwaysAllow: ["allowed-tool"],
@@ -88,7 +87,6 @@ describe("McpHub", () => {
 			const mockConfig = {
 				mcpServers: {
 					"test-server": {
-						type: "stdio",
 						command: "node",
 						args: ["test.js"],
 						alwaysAllow: [],
@@ -111,7 +109,6 @@ describe("McpHub", () => {
 			const mockConfig = {
 				mcpServers: {
 					"test-server": {
-						type: "stdio",
 						command: "node",
 						args: ["test.js"],
 						alwaysAllow: ["existing-tool"],
@@ -134,7 +131,6 @@ describe("McpHub", () => {
 			const mockConfig = {
 				mcpServers: {
 					"test-server": {
-						type: "stdio",
 						command: "node",
 						args: ["test.js"],
 					},
@@ -159,7 +155,6 @@ describe("McpHub", () => {
 			const mockConfig = {
 				mcpServers: {
 					"test-server": {
-						type: "stdio",
 						command: "node",
 						args: ["test.js"],
 						disabled: false,
@@ -299,21 +294,20 @@ describe("McpHub", () => {
 			it("should validate timeout values", () => {
 				// Test valid timeout values
 				const validConfig = {
-					type: "stdio",
 					command: "test",
 					timeout: 60,
 				}
-				expect(() => ServerConfigSchema.parse(validConfig)).not.toThrow()
+				expect(() => StdioConfigSchema.parse(validConfig)).not.toThrow()
 
 				// Test invalid timeout values
 				const invalidConfigs = [
-					{ type: "stdio", command: "test", timeout: 0 }, // Too low
-					{ type: "stdio", command: "test", timeout: 3601 }, // Too high
-					{ type: "stdio", command: "test", timeout: -1 }, // Negative
+					{ command: "test", timeout: 0 }, // Too low
+					{ command: "test", timeout: 3601 }, // Too high
+					{ command: "test", timeout: -1 }, // Negative
 				]
 
 				invalidConfigs.forEach((config) => {
-					expect(() => ServerConfigSchema.parse(config)).toThrow()
+					expect(() => StdioConfigSchema.parse(config)).toThrow()
 				})
 			})
 
@@ -321,7 +315,7 @@ describe("McpHub", () => {
 				const mockConnection: McpConnection = {
 					server: {
 						name: "test-server",
-						config: JSON.stringify({ type: "stdio", command: "test" }), // No timeout specified
+						config: JSON.stringify({ command: "test" }), // No timeout specified
 						status: "connected",
 					},
 					client: {
@@ -344,7 +338,7 @@ describe("McpHub", () => {
 				const mockConnection: McpConnection = {
 					server: {
 						name: "test-server",
-						config: JSON.stringify({ type: "stdio", command: "test", timeout: 120 }), // 2 minutes
+						config: JSON.stringify({ command: "test", timeout: 120 }), // 2 minutes
 						status: "connected",
 					},
 					client: {
@@ -369,7 +363,6 @@ describe("McpHub", () => {
 				const mockConfig = {
 					mcpServers: {
 						"test-server": {
-							type: "stdio",
 							command: "node",
 							args: ["test.js"],
 							timeout: 60,
@@ -392,7 +385,6 @@ describe("McpHub", () => {
 				const mockConfig = {
 					mcpServers: {
 						"test-server": {
-							type: "stdio",
 							command: "node",
 							args: ["test.js"],
 							timeout: 60,
@@ -414,7 +406,6 @@ describe("McpHub", () => {
 					server: {
 						name: "test-server",
 						config: JSON.stringify({
-							type: "stdio",
 							command: "node",
 							args: ["test.js"],
 							timeout: 3601, // Invalid timeout
@@ -444,7 +435,6 @@ describe("McpHub", () => {
 				const mockConfig = {
 					mcpServers: {
 						"test-server": {
-							type: "stdio",
 							command: "node",
 							args: ["test.js"],
 							timeout: 60,
@@ -468,7 +458,6 @@ describe("McpHub", () => {
 				const mockConfig = {
 					mcpServers: {
 						"test-server": {
-							type: "stdio",
 							command: "node",
 							args: ["test.js"],
 							timeout: 60,

+ 2 - 0
src/shared/api.ts

@@ -246,6 +246,8 @@ export interface MessageContent {
 
 export type BedrockModelId = keyof typeof bedrockModels
 export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-7-sonnet-20250219-v1:0"
+export const bedrockDefaultPromptRouterModelId: BedrockModelId = "anthropic.claude-3-sonnet-20240229-v1:0"
+
 // March, 12 2025 - updated prices to match US-West-2 list price shown at https://aws.amazon.com/bedrock/pricing/
 // including older models that are part of the default prompt routers AWS enabled for GA of the promot router feature
 export const bedrockModels = {

+ 3 - 2
webview-ui/src/components/chat/TaskHeader.tsx

@@ -267,10 +267,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
 							<div
 								style={{
 									cursor: "pointer",
-									color: "var(--vscode-textLink-foreground)",
+									color: "var(--vscode-badge-foreground)",
+									fontSize: "11px",
 									marginLeft: "auto",
 									textAlign: "right",
-									paddingRight: 2,
+									paddingRight: 8,
 								}}
 								onClick={() => setIsTextExpanded(!isTextExpanded)}>
 								See less

Some files were not shown because too many files changed in this diff