From 226ad251bdc046bbe945702d253a18a819f1c3aa Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 27 May 2025 18:33:05 -0700 Subject: [PATCH 01/47] Add a Playwright e2e test --- client/e2e/transport-type-dropdown.spec.ts | 22 ++++++++ package-lock.json | 64 ++++++++++++++++++++++ package.json | 1 + 3 files changed, 87 insertions(+) create mode 100644 client/e2e/transport-type-dropdown.spec.ts diff --git a/client/e2e/transport-type-dropdown.spec.ts b/client/e2e/transport-type-dropdown.spec.ts new file mode 100644 index 00000000..003420d2 --- /dev/null +++ b/client/e2e/transport-type-dropdown.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test'; + +// Adjust the URL if your dev server runs on a different port +const APP_URL = 'http://localhost:6274/'; + +test.describe('Transport Type Dropdown', () => { + test('should have options for STDIO, SSE, and Streamable HTTP', async ({ page }) => { + await page.goto(APP_URL); + + // Wait for the Transport Type dropdown to be visible + const selectTrigger = page.getByLabel('Transport Type'); + await expect(selectTrigger).toBeVisible(); + + // Open the dropdown + await selectTrigger.click(); + + // Check for the three options + await expect(page.getByRole('option', { name: 'STDIO' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'SSE' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Streamable HTTP' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 846d2eaa..25a242fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "mcp-inspector": "cli/build/cli.js" }, "devDependencies": { + "@playwright/test": "^1.52.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", "@types/shell-quote": "^1.7.5", @@ -2070,6 +2071,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -8519,6 +8536,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", diff --git a/package.json b/package.json index 8742cc8b..0e5cc4c2 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@playwright/test": "^1.52.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", "@types/shell-quote": "^1.7.5", From 69da70acb7316a04baa811f740c3b8d08063644f Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 27 May 2025 18:35:22 -0700 Subject: [PATCH 02/47] npm run prettier-fix --- client/e2e/transport-type-dropdown.spec.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/client/e2e/transport-type-dropdown.spec.ts b/client/e2e/transport-type-dropdown.spec.ts index 003420d2..7ca55655 100644 --- a/client/e2e/transport-type-dropdown.spec.ts +++ b/client/e2e/transport-type-dropdown.spec.ts @@ -1,22 +1,26 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; // Adjust the URL if your dev server runs on a different port -const APP_URL = 'http://localhost:6274/'; +const APP_URL = "http://localhost:6274/"; -test.describe('Transport Type Dropdown', () => { - test('should have options for STDIO, SSE, and Streamable HTTP', async ({ page }) => { +test.describe("Transport Type Dropdown", () => { + test("should have options for STDIO, SSE, and Streamable HTTP", async ({ + page, + }) => { await page.goto(APP_URL); // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel('Transport Type'); + const selectTrigger = page.getByLabel("Transport Type"); await expect(selectTrigger).toBeVisible(); // Open the dropdown await selectTrigger.click(); // Check for the three options - await expect(page.getByRole('option', { name: 'STDIO' })).toBeVisible(); - await expect(page.getByRole('option', { name: 'SSE' })).toBeVisible(); - await expect(page.getByRole('option', { name: 'Streamable HTTP' })).toBeVisible(); + await expect(page.getByRole("option", { name: "STDIO" })).toBeVisible(); + await expect(page.getByRole("option", { name: "SSE" })).toBeVisible(); + await expect( + page.getByRole("option", { name: "Streamable HTTP" }), + ).toBeVisible(); }); -}); \ No newline at end of file +}); From 8a13526c8b24c12f169152675b9754c7d8d9f6cd Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 27 May 2025 18:38:20 -0700 Subject: [PATCH 03/47] Exclude e2e from Jest --- client/jest.config.cjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/jest.config.cjs b/client/jest.config.cjs index b4d81cfc..8652eda5 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -21,6 +21,7 @@ module.exports = { "/node_modules/", "/dist/", "/bin/", + "/e2e/", "\\.config\\.(js|ts|cjs|mjs)$", ], // Exclude the same patterns from coverage reports @@ -28,6 +29,7 @@ module.exports = { "/node_modules/", "/dist/", "/bin/", + "/e2e/", "\\.config\\.(js|ts|cjs|mjs)$", ], }; From 8babc44f0b04a6037deee2de205710eda7c3b023 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 27 May 2025 18:43:29 -0700 Subject: [PATCH 04/47] Make `npm run test:e2e` work --- client/package.json | 3 ++- package.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/package.json b/client/package.json index daf549fc..9eb49574 100644 --- a/client/package.json +++ b/client/package.json @@ -20,7 +20,8 @@ "lint": "eslint .", "preview": "vite preview --port 6274", "test": "jest --config jest.config.cjs", - "test:watch": "jest --config jest.config.cjs --watch" + "test:watch": "jest --config jest.config.cjs --watch", + "test:e2e": "playwright test e2e" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/package.json b/package.json index 0e5cc4c2..1e7a931e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "prettier-check": "prettier --check .", "lint": "prettier --check . && cd client && npm run lint", "prepare": "npm run build", - "publish-all": "npm publish --workspaces --access public && npm publish --access public" + "publish-all": "npm publish --workspaces --access public && npm publish --access public", + "test:e2e": "npm run test:e2e --workspace=client" }, "dependencies": { "@modelcontextprotocol/inspector-cli": "^0.14.0", From 3a28efe5861f9416d41abb472e75059bf4f3c605 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 27 May 2025 19:21:06 -0700 Subject: [PATCH 05/47] Mention `npm run test:e2e` in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72502f96..1eeeb864 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Thanks for your interest in contributing! This guide explains how to get involve 1. Create a new branch for your changes 2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable. -3. Test changes locally by running `npm test` +3. Test changes locally by running `npm test` and `npm run test:e2e` 4. Update documentation as needed 5. Use clear commit messages explaining your changes 6. Verify all changes work as expected From 89e1efc443614aed0075958168c7db27db4d6f2a Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 3 Jun 2025 23:29:33 -0700 Subject: [PATCH 06/47] Add more Playwright tests --- client/e2e/transport-type-dropdown.spec.ts | 87 ++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/client/e2e/transport-type-dropdown.spec.ts b/client/e2e/transport-type-dropdown.spec.ts index 7ca55655..e4ac8fbb 100644 --- a/client/e2e/transport-type-dropdown.spec.ts +++ b/client/e2e/transport-type-dropdown.spec.ts @@ -23,4 +23,91 @@ test.describe("Transport Type Dropdown", () => { page.getByRole("option", { name: "Streamable HTTP" }), ).toBeVisible(); }); + + test("should show Command and Arguments fields and hide URL field when Transport Type is STDIO", async ({ + page, + }) => { + await page.goto(APP_URL); + + // Wait for the Transport Type dropdown to be visible + const selectTrigger = page.getByLabel("Transport Type"); + await expect(selectTrigger).toBeVisible(); + + // Open the dropdown and select STDIO + await selectTrigger.click(); + await page.getByRole("option", { name: "STDIO" }).click(); + + // Wait for the form to update + await page.waitForTimeout(100); + + // Check that Command and Arguments fields are visible + await expect(page.locator("#command-input")).toBeVisible(); + await expect(page.locator("#arguments-input")).toBeVisible(); + + // Check that URL field is not visible + await expect(page.locator("#sse-url-input")).not.toBeVisible(); + + // Also verify the labels are present + await expect(page.getByText("Command")).toBeVisible(); + await expect(page.getByText("Arguments")).toBeVisible(); + await expect(page.getByText("URL")).not.toBeVisible(); + }); + + test("should show URL field and hide Command and Arguments fields when Transport Type is SSE", async ({ + page, + }) => { + await page.goto(APP_URL); + + // Wait for the Transport Type dropdown to be visible + const selectTrigger = page.getByLabel("Transport Type"); + await expect(selectTrigger).toBeVisible(); + + // Open the dropdown and select SSE + await selectTrigger.click(); + await page.getByRole("option", { name: "SSE" }).click(); + + // Wait for the form to update + await page.waitForTimeout(100); + + // Check that URL field is visible + await expect(page.locator("#sse-url-input")).toBeVisible(); + + // Check that Command and Arguments fields are not visible + await expect(page.locator("#command-input")).not.toBeVisible(); + await expect(page.locator("#arguments-input")).not.toBeVisible(); + + // Also verify the labels are present/absent + await expect(page.getByText("URL")).toBeVisible(); + await expect(page.getByText("Command")).not.toBeVisible(); + await expect(page.getByText("Arguments")).not.toBeVisible(); + }); + + test("should show URL field and hide Command and Arguments fields when Transport Type is Streamable HTTP", async ({ + page, + }) => { + await page.goto(APP_URL); + + // Wait for the Transport Type dropdown to be visible + const selectTrigger = page.getByLabel("Transport Type"); + await expect(selectTrigger).toBeVisible(); + + // Open the dropdown and select Streamable HTTP + await selectTrigger.click(); + await page.getByRole("option", { name: "Streamable HTTP" }).click(); + + // Wait for the form to update + await page.waitForTimeout(100); + + // Check that URL field is visible + await expect(page.locator("#sse-url-input")).toBeVisible(); + + // Check that Command and Arguments fields are not visible + await expect(page.locator("#command-input")).not.toBeVisible(); + await expect(page.locator("#arguments-input")).not.toBeVisible(); + + // Also verify the labels are present/absent + await expect(page.getByText("URL")).toBeVisible(); + await expect(page.getByText("Command")).not.toBeVisible(); + await expect(page.getByText("Arguments")).not.toBeVisible(); + }); }); From c803d9327e1397ede53d1dc88031625655a78244 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 3 Jun 2025 23:51:14 -0700 Subject: [PATCH 07/47] Move test:e2e up with other two test scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1e7a931e..9ef35437 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,12 @@ "start-client": "cd client && npm run preview", "test": "npm run prettier-check && cd client && npm test", "test-cli": "cd cli && npm run test", + "test:e2e": "npm run test:e2e --workspace=client", "prettier-fix": "prettier --write .", "prettier-check": "prettier --check .", "lint": "prettier --check . && cd client && npm run lint", "prepare": "npm run build", - "publish-all": "npm publish --workspaces --access public && npm publish --access public", - "test:e2e": "npm run test:e2e --workspace=client" + "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, "dependencies": { "@modelcontextprotocol/inspector-cli": "^0.14.0", From 4f6a4cebde023d6d262fab05443cdfe0b4a99a86 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 3 Jun 2025 23:54:25 -0700 Subject: [PATCH 08/47] Add .github/workflows/e2e_tests.yml --- .github/workflows/e2e_tests.yml | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/e2e_tests.yml diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml new file mode 100644 index 00000000..538d8da2 --- /dev/null +++ b/.github/workflows/e2e_tests.yml @@ -0,0 +1,45 @@ +name: Playwright Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + timeout-minutes: 5 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + # Cache Playwright browsers + - name: Cache Playwright browsers + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright # The default Playwright cache path + key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install dependencies + run: npm ci + + - name: Install Playwright and browsers unless cached + run: npx playwright install --with-deps + if: steps.cache-playwright.outputs.cache-hit != 'true' + + - name: Start Server + run: npm run start + + - name: Wait for server to start + run: npx wait-on http://localhost:6274 + + - name: Run Playwright tests + run: npm run test:e2e From 66ad85aadc27694432b94020a26f468f160a8517 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 6 Jun 2025 18:19:56 -0700 Subject: [PATCH 09/47] npm run prettier-fix --- .github/workflows/e2e_tests.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 538d8da2..599d90df 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -2,26 +2,26 @@ name: Playwright Tests on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: test: timeout-minutes: 5 - runs-on: ubuntu-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 18 # Cache Playwright browsers - name: Cache Playwright browsers id: cache-playwright - uses: actions/cache@v4 + uses: actions/cache@v4 with: path: ~/.cache/ms-playwright # The default Playwright cache path key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json @@ -29,14 +29,14 @@ jobs: ${{ runner.os }}-playwright- - name: Install dependencies - run: npm ci + run: npm ci - name: Install Playwright and browsers unless cached run: npx playwright install --with-deps - if: steps.cache-playwright.outputs.cache-hit != 'true' + if: steps.cache-playwright.outputs.cache-hit != 'true' - name: Start Server - run: npm run start + run: npm run start - name: Wait for server to start run: npx wait-on http://localhost:6274 From 1df83d73f9604858ac4559c59b8eea6b2ee6fca4 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 6 Jun 2025 18:26:05 -0700 Subject: [PATCH 10/47] Do npm run start in background so that the CI job can proceed --- .github/workflows/e2e_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 599d90df..14553600 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -36,7 +36,7 @@ jobs: if: steps.cache-playwright.outputs.cache-hit != 'true' - name: Start Server - run: npm run start + run: npm run start & - name: Wait for server to start run: npx wait-on http://localhost:6274 From 826bc35cf2e5c6be740b8d32efc95b46082d9d9e Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 6 Jun 2025 18:31:07 -0700 Subject: [PATCH 11/47] Upload Playwright Report and Screenshots --- .github/workflows/e2e_tests.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 14553600..652863e4 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -43,3 +43,13 @@ jobs: - name: Run Playwright tests run: npm run test:e2e + + - name: Upload Playwright Report and Screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 30 From a421f329e98bff46a4d3e360ddd7dcf926065d96 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 6 Jun 2025 18:43:04 -0700 Subject: [PATCH 12/47] git add playwright.config.ts --- client/playwright.config.ts | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 client/playwright.config.ts diff --git a/client/playwright.config.ts b/client/playwright.config.ts new file mode 100644 index 00000000..5e848d26 --- /dev/null +++ b/client/playwright.config.ts @@ -0,0 +1,82 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html', { outputFolder: 'playwright-report' }], + ['line'] + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:6274', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Take screenshots on failure */ + screenshot: 'on', + // screenshot: 'only-on-failure', + + /* Record video on failure */ + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + // Skip WebKit on macOS due to compatibility issues + ...(process.platform !== 'darwin' ? [{ + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }] : []), + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:6274', + // reuseExistingServer: !process.env.CI, + // }, +}); \ No newline at end of file From f06a6e1fb7fcbfd08c5397751b130fe808cd8b4d Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 6 Jun 2025 18:44:27 -0700 Subject: [PATCH 13/47] npm run prettier-fix --- client/playwright.config.ts | 39 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 5e848d26..551f2d38 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -1,10 +1,10 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './e2e', + testDir: "./e2e", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -14,43 +14,44 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ - ['html', { outputFolder: 'playwright-report' }], - ['line'] - ], + reporter: [["html", { outputFolder: "playwright-report" }], ["line"]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:6274', + baseURL: "http://localhost:6274", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", /* Take screenshots on failure */ - screenshot: 'on', + screenshot: "on", // screenshot: 'only-on-failure', /* Record video on failure */ - video: 'retain-on-failure', + video: "retain-on-failure", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, // Skip WebKit on macOS due to compatibility issues - ...(process.platform !== 'darwin' ? [{ - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }] : []), + ...(process.platform !== "darwin" + ? [ + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ] + : []), /* Test against mobile viewports. */ // { @@ -79,4 +80,4 @@ export default defineConfig({ // url: 'http://127.0.0.1:6274', // reuseExistingServer: !process.env.CI, // }, -}); \ No newline at end of file +}); From a2a58c2bc21baa55d9eac6d1e48d0662d82181de Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 6 Jun 2025 18:57:02 -0700 Subject: [PATCH 14/47] Install Playwright dependencies --- .github/workflows/e2e_tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 652863e4..b80ffd81 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -12,6 +12,11 @@ jobs: runs-on: ubuntu-latest steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwoff1 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -31,6 +36,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Install Playwright dependencies + run: npx playwright install-deps + - name: Install Playwright and browsers unless cached run: npx playwright install --with-deps if: steps.cache-playwright.outputs.cache-hit != 'true' From 705d7b46c27016f16befa44ae5311b5f98f43a43 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 6 Jun 2025 19:05:07 -0700 Subject: [PATCH 15/47] e2e_tests.yml: playwright-report and test-result in client dir --- .github/workflows/e2e_tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index b80ffd81..d61227ba 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -58,6 +58,6 @@ jobs: with: name: playwright-report path: | - playwright-report/ - test-results/ + client/playwright-report/ + client/test-results/ retention-days: 30 From cdb9180629542dc87fe2b13e0e274277493b3768 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Sat, 7 Jun 2025 08:11:23 -0700 Subject: [PATCH 16/47] Generate report with https://github.com/daun/playwright-report-summary --- .github/workflows/e2e_tests.yml | 16 ++++++++++++++++ client/playwright.config.ts | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index d61227ba..df323c07 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -60,4 +60,20 @@ jobs: path: | client/playwright-report/ client/test-results/ + client/results.json retention-days: 30 + + - name: Publish Playwright Test Summary + uses: daun/playwright-report-summary@v3 + if: always() + with: + report-file: client/results.json + comment-title: "šŸŽ­ Playwright E2E Test Results" + job-summary: true + icon-style: "emojis" + custom-info: | + **Test Environment:** Ubuntu Latest, Node.js 18 + **Browsers:** Chromium, Firefox + + šŸ“Š [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts) + test-command: "npm run test:e2e" diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 551f2d38..935e4b3d 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -14,7 +14,11 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [["html", { outputFolder: "playwright-report" }], ["line"]], + reporter: [ + ["html", { outputFolder: "playwright-report" }], + ["json", { outputFile: "results.json" }], + ["line"], + ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ From 2c0af88f504e09461b7bf5c307d84e9bd70485ae Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Sat, 7 Jun 2025 08:35:13 -0700 Subject: [PATCH 17/47] Take screenshots on failure --- client/playwright.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 935e4b3d..fad4176b 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -28,8 +28,7 @@ export default defineConfig({ trace: "on-first-retry", /* Take screenshots on failure */ - screenshot: "on", - // screenshot: 'only-on-failure', + screenshot: "only-on-failure", /* Record video on failure */ video: "retain-on-failure", From 6e5ccf679ac6cd4e8c1ca4f728ee4e7b0dfb24be Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Mon, 16 Jun 2025 20:55:45 -0700 Subject: [PATCH 18/47] Remove commented out stuff in playwright.config.ts --- client/playwright.config.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index fad4176b..42612a26 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -55,32 +55,5 @@ export default defineConfig({ }, ] : []), - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:6274', - // reuseExistingServer: !process.env.CI, - // }, }); From 8b1faf110cbf5956df05dd0bd18589c3ba1a972f Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 17 Jun 2025 20:18:32 -0700 Subject: [PATCH 19/47] playwright.config.ts: Start dev server automatically --- client/playwright.config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 42612a26..e4d6a30d 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -4,6 +4,14 @@ import { defineConfig, devices } from "@playwright/test"; * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ + /* Run your local dev server before starting the tests */ + webServer: { + cwd: '..', + command: 'npm run dev', + url: 'http://localhost:6274', + reuseExistingServer: !process.env.CI, + }, + testDir: "./e2e", /* Run tests in files in parallel */ fullyParallel: true, From 4ef6bdb1b8144b23d698fe13f47f4e380059ad64 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 17 Jun 2025 20:19:41 -0700 Subject: [PATCH 20/47] e2e_tests.yml: Remove start dev server since now we are doing that in `playwright.config.ts` --- .github/workflows/e2e_tests.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index df323c07..b47daf2b 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -43,12 +43,6 @@ jobs: run: npx playwright install --with-deps if: steps.cache-playwright.outputs.cache-hit != 'true' - - name: Start Server - run: npm run start & - - - name: Wait for server to start - run: npx wait-on http://localhost:6274 - - name: Run Playwright tests run: npm run test:e2e From 0f1cc199268d5c8e45dcbdac898948d788c857db Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 17 Jun 2025 20:26:22 -0700 Subject: [PATCH 21/47] prettier fix playwright.config.ts --- client/playwright.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index e4d6a30d..f4650843 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -6,9 +6,9 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - cwd: '..', - command: 'npm run dev', - url: 'http://localhost:6274', + cwd: "..", + command: "npm run dev", + url: "http://localhost:6274", reuseExistingServer: !process.env.CI, }, From 0b31ce745423f50a070e593bdb7ec2daf3e54d55 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 17 Jun 2025 23:44:49 -0700 Subject: [PATCH 22/47] .gitignore: Add playwright artifacts --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 60faf653..8e184de8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ client/tsconfig.app.tsbuildinfo client/tsconfig.node.tsbuildinfo cli/build test-output +client/playwright-report/ +client/results.json +client/test-results/ + From b783b50f7f5c30e1e8dad9ffc8e6a95fe4871aae Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 17 Jun 2025 23:56:18 -0700 Subject: [PATCH 23/47] Make "npm run clean" remove Playwright files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ef35437..9108e3e3 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build-server": "cd server && npm run build", "build-client": "cd client && npm run build", "build-cli": "cd cli && npm run build", - "clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install", + "clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json client/playwright-report client/results.json client/test-results && npm install", "dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"", "dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows\"", "start": "node client/bin/start.js", From d06f3b5ebc226940d0d85e938ccf8eae9c944be7 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 16 Jun 2025 13:42:37 +0100 Subject: [PATCH 24/47] feat: Enable auto-open with authentication and unify start scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Unified start.js to handle both dev and production modes with --dev flag - Generate session token in parent process, pass via MCP_PROXY_TOKEN env var - Enable browser auto-open even when authentication is enabled by including token in URL - Auto-reloads now work seamlessly with persistent tokens across hot reloads - Simplified npm scripts - dev and dev:windows now use the same unified script - Better developer experience with consistent token handling The token is generated once per session and remains stable through server/client reloads, making development smoother while maintaining security. šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/bin/start.js | 190 ++++++++++++++++++++++++++++++++++---------- package.json | 4 +- server/src/index.ts | 6 +- 3 files changed, 154 insertions(+), 46 deletions(-) diff --git a/client/bin/start.js b/client/bin/start.js index ccce7ade..2cd1758e 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -2,8 +2,9 @@ import open from "open"; import { resolve, dirname } from "path"; -import { spawnPromise } from "spawn-rx"; +import { spawnPromise, spawn } from "spawn-rx"; import { fileURLToPath } from "url"; +import { randomBytes } from "crypto"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -18,6 +19,7 @@ async function main() { const mcpServerArgs = []; let command = null; let parsingFlags = true; + let isDev = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -27,6 +29,11 @@ async function main() { continue; } + if (parsingFlags && arg === "--dev") { + isDev = true; + continue; + } + if (parsingFlags && arg === "-e" && i + 1 < args.length) { const envVar = args[++i]; const equalsIndex = envVar.indexOf("="); @@ -38,34 +45,25 @@ async function main() { } else { envVars[envVar] = ""; } - } else if (!command) { + } else if (!command && !isDev) { command = arg; - } else { + } else if (!isDev) { mcpServerArgs.push(arg); } } - const inspectorServerPath = resolve( - __dirname, - "../..", - "server", - "build", - "index.js", - ); - - // Path to the client entry point - const inspectorClientPath = resolve( - __dirname, - "../..", - "client", - "bin", - "client.js", - ); - const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274"; const SERVER_PORT = process.env.SERVER_PORT ?? "6277"; - console.log("Starting MCP inspector..."); + console.log( + isDev + ? "Starting MCP inspector in development mode..." + : "Starting MCP inspector...", + ); + + // Generate session token for authentication + const sessionToken = randomBytes(32).toString("hex"); + const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; const abort = new AbortController(); @@ -74,42 +72,150 @@ async function main() { cancelled = true; abort.abort(); }); + let server, serverOk; + try { - server = spawnPromise( - "node", - [ - inspectorServerPath, - ...(command ? [`--env`, command] : []), - ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), - ], - { + if (isDev) { + // Development mode - use tsx watch + const serverCommand = "npx"; + const serverArgs = [ + "tsx", + "watch", + "--clear-screen=false", + "src/index.ts", + ]; + const isWindows = process.platform === "win32"; + + const serverOptions = { + cwd: resolve(__dirname, "../..", "server"), env: { ...process.env, PORT: SERVER_PORT, + CLIENT_PORT: CLIENT_PORT, + MCP_PROXY_TOKEN: sessionToken, MCP_ENV_VARS: JSON.stringify(envVars), }, signal: abort.signal, echoOutput: true, - }, - ); + }; - // Make sure server started before starting client - serverOk = await Promise.race([server, delay(2 * 1000)]); + // For Windows, we need to use stdin: 'ignore' to simulate < NUL + if (isWindows) { + serverOptions.stdin = "ignore"; + } + + server = spawn(serverCommand, serverArgs, serverOptions); + + // Give server time to start + serverOk = await Promise.race([ + new Promise((resolve) => { + server.subscribe({ + complete: () => resolve(false), + error: () => resolve(false), + next: () => {}, // We're using echoOutput + }); + }), + delay(3000).then(() => true), + ]); + } else { + // Production mode - use built files + const inspectorServerPath = resolve( + __dirname, + "../..", + "server", + "build", + "index.js", + ); + + server = spawnPromise( + "node", + [ + inspectorServerPath, + ...(command ? [`--env`, command] : []), + ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), + ], + { + env: { + ...process.env, + PORT: SERVER_PORT, + CLIENT_PORT: CLIENT_PORT, + MCP_PROXY_TOKEN: sessionToken, + MCP_ENV_VARS: JSON.stringify(envVars), + }, + signal: abort.signal, + echoOutput: true, + }, + ); + + // Make sure server started before starting client + serverOk = await Promise.race([server, delay(2 * 1000)]); + } } catch (error) {} if (serverOk) { try { - // Only auto-open when auth is disabled - const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && authDisabled) { - open(`http://127.0.0.1:${CLIENT_PORT}`); + if (isDev) { + // Development mode - use vite + const clientCommand = "npx"; + const clientArgs = ["vite", "--port", CLIENT_PORT]; + + const client = spawn(clientCommand, clientArgs, { + cwd: resolve(__dirname, ".."), + env: { ...process.env, PORT: CLIENT_PORT }, + signal: abort.signal, + echoOutput: true, + }); + + // Auto-open browser after vite starts + if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { + const url = authDisabled + ? `http://127.0.0.1:${CLIENT_PORT}` + : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; + + // Give vite time to start before opening browser + setTimeout(() => { + open(url); + console.log(`\nšŸ”— Opening browser at: ${url}\n`); + }, 3000); + } + + await new Promise((resolve) => { + client.subscribe({ + complete: resolve, + error: (err) => { + if (!cancelled || process.env.DEBUG) { + console.error("Client error:", err); + } + resolve(null); + }, + next: () => {}, // We're using echoOutput + }); + }); + } else { + // Production mode - use client.js + const inspectorClientPath = resolve( + __dirname, + "../..", + "client", + "bin", + "client.js", + ); + + // Auto-open browser with token + if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { + const url = authDisabled + ? `http://127.0.0.1:${CLIENT_PORT}` + : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; + open(url); + } + + await spawnPromise("node", [inspectorClientPath], { + env: { ...process.env, PORT: CLIENT_PORT }, + signal: abort.signal, + echoOutput: true, + }); } - await spawnPromise("node", [inspectorClientPath], { - env: { ...process.env, PORT: CLIENT_PORT }, - signal: abort.signal, - echoOutput: true, - }); } catch (e) { if (!cancelled || process.env.DEBUG) throw e; } diff --git a/package.json b/package.json index 7e7be67f..20c69c2a 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "build-client": "cd client && npm run build", "build-cli": "cd cli && npm run build", "clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install", - "dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"", - "dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows\"", + "dev": "node client/bin/start.js --dev", + "dev:windows": "node client/bin/start.js --dev", "start": "node client/bin/start.js", "start-server": "cd server && npm run start", "start-client": "cd client && npm run preview", diff --git a/server/src/index.ts b/server/src/index.ts index 38d62b71..7653597a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -89,7 +89,9 @@ app.use((req, res, next) => { const webAppTransports: Map = new Map(); // Web app transports by web app sessionId const serverTransports: Map = new Map(); // Server Transports by web app sessionId -const sessionToken = randomBytes(32).toString("hex"); +// Use provided token from environment or generate a new one +const sessionToken = + process.env.MCP_PROXY_TOKEN || randomBytes(32).toString("hex"); const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; // Origin validation middleware to prevent DNS rebinding attacks @@ -544,7 +546,7 @@ server.on("listening", () => { const clientPort = process.env.CLIENT_PORT || "6274"; const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; console.log( - `\nšŸ”— Open inspector with token pre-filled:\n ${clientUrl}\n (Auto-open is disabled when authentication is enabled)\n`, + `\nšŸ”— Open inspector with token pre-filled:\n ${clientUrl}\n`, ); } else { console.log( From 9fec0aaf9807a7d6e0e3fd0985d6142c2711761a Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 16 Jun 2025 13:52:57 +0100 Subject: [PATCH 25/47] refactor: Extract server and client launch functions for better code organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extracted 4 functions: startDevServer, startProdServer, startDevClient, startProdClient - Eliminated deep nesting in main() function - Each function has a single responsibility - Main function now clearly shows the flow: parse args → start server → start client - No functional changes, purely organizational refactoring šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/bin/start.js | 308 +++++++++++++++++++++++++------------------- 1 file changed, 175 insertions(+), 133 deletions(-) diff --git a/client/bin/start.js b/client/bin/start.js index 2cd1758e..ad75078a 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -12,6 +12,157 @@ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms, true)); } +async function startDevServer(serverOptions) { + const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } = + serverOptions; + const serverCommand = "npx"; + const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"]; + const isWindows = process.platform === "win32"; + + const spawnOptions = { + cwd: resolve(__dirname, "../..", "server"), + env: { + ...process.env, + PORT: SERVER_PORT, + CLIENT_PORT: CLIENT_PORT, + MCP_PROXY_TOKEN: sessionToken, + MCP_ENV_VARS: JSON.stringify(envVars), + }, + signal: abort.signal, + echoOutput: true, + }; + + // For Windows, we need to use stdin: 'ignore' to simulate < NUL + if (isWindows) { + spawnOptions.stdin = "ignore"; + } + + const server = spawn(serverCommand, serverArgs, spawnOptions); + + // Give server time to start + const serverOk = await Promise.race([ + new Promise((resolve) => { + server.subscribe({ + complete: () => resolve(false), + error: () => resolve(false), + next: () => {}, // We're using echoOutput + }); + }), + delay(3000).then(() => true), + ]); + + return { server, serverOk }; +} + +async function startProdServer(serverOptions) { + const { + SERVER_PORT, + CLIENT_PORT, + sessionToken, + envVars, + abort, + command, + mcpServerArgs, + } = serverOptions; + const inspectorServerPath = resolve( + __dirname, + "../..", + "server", + "build", + "index.js", + ); + + const server = spawnPromise( + "node", + [ + inspectorServerPath, + ...(command ? [`--env`, command] : []), + ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), + ], + { + env: { + ...process.env, + PORT: SERVER_PORT, + CLIENT_PORT: CLIENT_PORT, + MCP_PROXY_TOKEN: sessionToken, + MCP_ENV_VARS: JSON.stringify(envVars), + }, + signal: abort.signal, + echoOutput: true, + }, + ); + + // Make sure server started before starting client + const serverOk = await Promise.race([server, delay(2 * 1000)]); + + return { server, serverOk }; +} + +async function startDevClient(clientOptions) { + const { CLIENT_PORT, authDisabled, sessionToken, abort, cancelled } = + clientOptions; + const clientCommand = "npx"; + const clientArgs = ["vite", "--port", CLIENT_PORT]; + + const client = spawn(clientCommand, clientArgs, { + cwd: resolve(__dirname, ".."), + env: { ...process.env, PORT: CLIENT_PORT }, + signal: abort.signal, + echoOutput: true, + }); + + // Auto-open browser after vite starts + if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { + const url = authDisabled + ? `http://127.0.0.1:${CLIENT_PORT}` + : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; + + // Give vite time to start before opening browser + setTimeout(() => { + open(url); + console.log(`\nšŸ”— Opening browser at: ${url}\n`); + }, 3000); + } + + await new Promise((resolve) => { + client.subscribe({ + complete: resolve, + error: (err) => { + if (!cancelled || process.env.DEBUG) { + console.error("Client error:", err); + } + resolve(null); + }, + next: () => {}, // We're using echoOutput + }); + }); +} + +async function startProdClient(clientOptions) { + const { CLIENT_PORT, authDisabled, sessionToken, abort } = clientOptions; + const inspectorClientPath = resolve( + __dirname, + "../..", + "client", + "bin", + "client.js", + ); + + // Auto-open browser with token + if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { + const url = authDisabled + ? `http://127.0.0.1:${CLIENT_PORT}` + : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; + open(url); + } + + await spawnPromise("node", [inspectorClientPath], { + env: { ...process.env, PORT: CLIENT_PORT }, + signal: abort.signal, + echoOutput: true, + }); +} + async function main() { // Parse command line arguments const args = process.argv.slice(2); @@ -76,146 +227,37 @@ async function main() { let server, serverOk; try { - if (isDev) { - // Development mode - use tsx watch - const serverCommand = "npx"; - const serverArgs = [ - "tsx", - "watch", - "--clear-screen=false", - "src/index.ts", - ]; - const isWindows = process.platform === "win32"; + const serverOptions = { + SERVER_PORT, + CLIENT_PORT, + sessionToken, + envVars, + abort, + command, + mcpServerArgs, + }; - const serverOptions = { - cwd: resolve(__dirname, "../..", "server"), - env: { - ...process.env, - PORT: SERVER_PORT, - CLIENT_PORT: CLIENT_PORT, - MCP_PROXY_TOKEN: sessionToken, - MCP_ENV_VARS: JSON.stringify(envVars), - }, - signal: abort.signal, - echoOutput: true, - }; + const result = isDev + ? await startDevServer(serverOptions) + : await startProdServer(serverOptions); - // For Windows, we need to use stdin: 'ignore' to simulate < NUL - if (isWindows) { - serverOptions.stdin = "ignore"; - } - - server = spawn(serverCommand, serverArgs, serverOptions); - - // Give server time to start - serverOk = await Promise.race([ - new Promise((resolve) => { - server.subscribe({ - complete: () => resolve(false), - error: () => resolve(false), - next: () => {}, // We're using echoOutput - }); - }), - delay(3000).then(() => true), - ]); - } else { - // Production mode - use built files - const inspectorServerPath = resolve( - __dirname, - "../..", - "server", - "build", - "index.js", - ); - - server = spawnPromise( - "node", - [ - inspectorServerPath, - ...(command ? [`--env`, command] : []), - ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), - ], - { - env: { - ...process.env, - PORT: SERVER_PORT, - CLIENT_PORT: CLIENT_PORT, - MCP_PROXY_TOKEN: sessionToken, - MCP_ENV_VARS: JSON.stringify(envVars), - }, - signal: abort.signal, - echoOutput: true, - }, - ); - - // Make sure server started before starting client - serverOk = await Promise.race([server, delay(2 * 1000)]); - } + server = result.server; + serverOk = result.serverOk; } catch (error) {} if (serverOk) { try { - if (isDev) { - // Development mode - use vite - const clientCommand = "npx"; - const clientArgs = ["vite", "--port", CLIENT_PORT]; + const clientOptions = { + CLIENT_PORT, + authDisabled, + sessionToken, + abort, + cancelled, + }; - const client = spawn(clientCommand, clientArgs, { - cwd: resolve(__dirname, ".."), - env: { ...process.env, PORT: CLIENT_PORT }, - signal: abort.signal, - echoOutput: true, - }); - - // Auto-open browser after vite starts - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { - const url = authDisabled - ? `http://127.0.0.1:${CLIENT_PORT}` - : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; - - // Give vite time to start before opening browser - setTimeout(() => { - open(url); - console.log(`\nšŸ”— Opening browser at: ${url}\n`); - }, 3000); - } - - await new Promise((resolve) => { - client.subscribe({ - complete: resolve, - error: (err) => { - if (!cancelled || process.env.DEBUG) { - console.error("Client error:", err); - } - resolve(null); - }, - next: () => {}, // We're using echoOutput - }); - }); - } else { - // Production mode - use client.js - const inspectorClientPath = resolve( - __dirname, - "../..", - "client", - "bin", - "client.js", - ); - - // Auto-open browser with token - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { - const url = authDisabled - ? `http://127.0.0.1:${CLIENT_PORT}` - : `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; - open(url); - } - - await spawnPromise("node", [inspectorClientPath], { - env: { ...process.env, PORT: CLIENT_PORT }, - signal: abort.signal, - echoOutput: true, - }); - } + await (isDev + ? startDevClient(clientOptions) + : startProdClient(clientOptions)); } catch (e) { if (!cancelled || process.env.DEBUG) throw e; } From 698d2458c04a65eea5772bf5d8830648b195504c Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 16 Jun 2025 13:59:08 +0100 Subject: [PATCH 26/47] docs: Update README to reflect auto-open now works with authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed outdated statement about auto-open being disabled with authentication - Clarified that browser automatically opens with token pre-filled in URL - Updated MCP_AUTO_OPEN_ENABLED description to note it works with authentication The README now accurately reflects the improved developer experience where authentication no longer prevents the browser from auto-opening. šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ae82a695..94efe83c 100644 --- a/README.md +++ b/README.md @@ -148,9 +148,9 @@ The MCP Inspector proxy server requires authentication by default. When starting http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4 ``` -This token must be included as a Bearer token in the Authorization header for all requests to the server. When authentication is enabled, auto-open is disabled by default to ensure you use the secure URL. +This token must be included as a Bearer token in the Authorization header for all requests to the server. The inspector will automatically open your browser with the token pre-filled in the URL. -**Recommended: Use the pre-filled URL** - Click or copy the link shown in the console to open the inspector with the token already configured. +**Automatic browser opening** - The inspector now automatically opens your browser with the token pre-filled in the URL when authentication is enabled. **Alternative: Manual configuration** - If you already have the inspector open: @@ -188,13 +188,13 @@ ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://localhost:800 The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI: -| Setting | Description | Default | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------- | -| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 | -| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true | -| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 | -| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" | -| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts. Only as environment var, not configurable in browser. | true | +| Setting | Description | Default | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 | +| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true | +| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 | +| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" | +| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true | These settings can be adjusted in real-time through the UI and will persist across sessions. From db9ca3762a9c99501d45f72c4724ad2089e2fa82 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Jun 2025 00:00:32 -0700 Subject: [PATCH 27/47] No playwright-report or results.json for non-CI --- client/playwright.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index f4650843..3e4f1d2e 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -22,11 +22,11 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ + reporter: process.env.CI ? [ ["html", { outputFolder: "playwright-report" }], ["json", { outputFile: "results.json" }], ["line"], - ], + ] : [["line"]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ From 08e4f6b4a83c8e4ac753974269c7f8f0fda74e5c Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Jun 2025 00:06:36 -0700 Subject: [PATCH 28/47] outputDir: "./e2e/test-results" --- client/playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 3e4f1d2e..fd24eb17 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ }, testDir: "./e2e", + outputDir: "./e2e/test-results", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ From 424b44966bd711b794efbd375108116bbfc6fd12 Mon Sep 17 00:00:00 2001 From: Alex Andru Date: Thu, 19 Jun 2025 14:22:02 +0200 Subject: [PATCH 29/47] chore: bump @modelcontextprotocol/sdk to version 1.13.0 --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a54bf664..e557269a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@modelcontextprotocol/inspector-cli": "^0.14.3", "@modelcontextprotocol/inspector-client": "^0.14.3", "@modelcontextprotocol/inspector-server": "^0.14.3", - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2", @@ -2005,9 +2005,10 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", - "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.0.tgz", + "integrity": "sha512-P5FZsXU0kY881F6Hbk9GhsYx02/KgWK1DYf7/tyE/1lcFKhDYPQR9iYjhQXJn+Sg6hQleMo3DB7h7+p4wgp2Lw==", + "license": "MIT", "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", diff --git a/package.json b/package.json index 20c69c2a..a21363a7 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@modelcontextprotocol/inspector-cli": "^0.14.3", "@modelcontextprotocol/inspector-client": "^0.14.3", "@modelcontextprotocol/inspector-server": "^0.14.3", - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2", From 8b3613e11ae76d3f29ad84c6e8a4bc38a5e76c52 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Jun 2025 07:02:06 -0700 Subject: [PATCH 30/47] prettier fix --- client/playwright.config.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/playwright.config.ts b/client/playwright.config.ts index fd24eb17..570dd054 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -23,11 +23,13 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? [ - ["html", { outputFolder: "playwright-report" }], - ["json", { outputFile: "results.json" }], - ["line"], - ] : [["line"]], + reporter: process.env.CI + ? [ + ["html", { outputFolder: "playwright-report" }], + ["json", { outputFile: "results.json" }], + ["line"], + ] + : [["line"]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ From 1807542582482cb2f769ea69c5686cafd6ddb088 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Jun 2025 07:22:38 -0700 Subject: [PATCH 31/47] Cleanup client/e2e/test-results/ after test:e2e --- client/e2e/global-teardown.js | 18 ++++++++++++++++++ client/package.json | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 client/e2e/global-teardown.js diff --git a/client/e2e/global-teardown.js b/client/e2e/global-teardown.js new file mode 100644 index 00000000..9308d940 --- /dev/null +++ b/client/e2e/global-teardown.js @@ -0,0 +1,18 @@ +import { rimraf } from "rimraf"; + +async function globalTeardown() { + if (!process.env.CI) { + console.log("Cleaning up test-results directory..."); + // Add a small delay to ensure all Playwright files are written + await new Promise((resolve) => setTimeout(resolve, 100)); + await rimraf("./e2e/test-results"); + console.log("Test-results directory cleaned up."); + } +} + +export default globalTeardown; + +// Call the function when this script is run directly +if (import.meta.url === `file://${process.argv[1]}`) { + globalTeardown().catch(console.error); +} diff --git a/client/package.json b/client/package.json index a34ecbc4..6d2e7d1a 100644 --- a/client/package.json +++ b/client/package.json @@ -21,7 +21,8 @@ "preview": "vite preview --port 6274", "test": "jest --config jest.config.cjs", "test:watch": "jest --config jest.config.cjs --watch", - "test:e2e": "playwright test e2e" + "test:e2e": "playwright test e2e && npm run cleanup:e2e", + "cleanup:e2e": "node e2e/global-teardown.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", From b481ed0077f662562ddfb0ffd5b81eec8e9578be Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Jun 2025 09:39:52 -0700 Subject: [PATCH 32/47] Set `MCP_AUTO_OPEN_ENABLED=false` for `test:e2e` script Since merging the latest changes from `main` that put auto-open back in when running the start or dev scripts, the `test:e2e` run results in the Inspector being opened in the browser. This change suppresses that (tested locally). Thanks, @cliffhall! Co-authored-by: Cliff Hall --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f333ef9e..3461976b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "start-client": "cd client && npm run preview", "test": "npm run prettier-check && cd client && npm test", "test-cli": "cd cli && npm run test", - "test:e2e": "npm run test:e2e --workspace=client", + "test:e2e": "MCP_AUTO_OPEN_ENABLED=false npm run test:e2e --workspace=client", "prettier-fix": "prettier --write .", "prettier-check": "prettier --check .", "lint": "prettier --check . && cd client && npm run lint", From 0e6915a061f6cbef16fe29eb8d271a5869e46afb Mon Sep 17 00:00:00 2001 From: Alex Andru Date: Fri, 20 Jun 2025 18:54:13 +0200 Subject: [PATCH 33/47] chore: bump workspace deps --- cli/package.json | 2 +- client/package.json | 2 +- server/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index f1ff1c33..382eb1b4 100644 --- a/cli/package.json +++ b/cli/package.json @@ -21,7 +21,7 @@ }, "devDependencies": {}, "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "commander": "^13.1.0", "spawn-rx": "^5.1.2" } diff --git a/client/package.json b/client/package.json index 37a0675b..449e0637 100644 --- a/client/package.json +++ b/client/package.json @@ -23,7 +23,7 @@ "test:watch": "jest --config jest.config.cjs --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "@radix-ui/react-checkbox": "^1.1.4", "ajv": "^6.12.6", "@radix-ui/react-dialog": "^1.1.3", diff --git a/server/package.json b/server/package.json index 8f81a9df..9c248635 100644 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", From d5b4a6e6a77f52b510306c9a6ba3bb256559469a Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 11:12:03 -0700 Subject: [PATCH 34/47] Update .github/workflows/e2e_tests.yml Co-authored-by: Cliff Hall --- .github/workflows/e2e_tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index b47daf2b..2d36f128 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + pull-requests: write + jobs: test: timeout-minutes: 5 From eb89e0191f7a54ac421e593c0af520e8349a20e7 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 11:39:18 -0700 Subject: [PATCH 35/47] Change retention-days from 30 to 2 --- .github/workflows/e2e_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 2d36f128..f620c318 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -58,7 +58,7 @@ jobs: client/playwright-report/ client/test-results/ client/results.json - retention-days: 30 + retention-days: 2 - name: Publish Playwright Test Summary uses: daun/playwright-report-summary@v3 From 1ff39f13f5339a1bf87101f80848bc7b69c089b4 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 11:44:45 -0700 Subject: [PATCH 36/47] Replace pull_request with pull_request_target --- .github/workflows/e2e_tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index f620c318..c601e45b 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -3,7 +3,7 @@ name: Playwright Tests on: push: branches: [main] - pull_request: + pull_request_target: branches: [main] permissions: @@ -20,7 +20,10 @@ jobs: sudo apt-get update sudo apt-get install -y libwoff1 + # Security: Explicitly checkout the PR's code, not the target branch - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v4 with: From 4b14b96f417598bde7fbae5acfe99b3b8faff2e5 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 12:13:29 -0700 Subject: [PATCH 37/47] Both pull_request and pull_request_target --- .github/workflows/e2e_tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index c601e45b..f52e928f 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -3,6 +3,8 @@ name: Playwright Tests on: push: branches: [main] + pull_request: + branches: [main] pull_request_target: branches: [main] From 9ffe63ee08908718b0ade8bdd457cbe46567dae1 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 13:35:45 -0700 Subject: [PATCH 38/47] Revert "Both pull_request and pull_request_target" This reverts commit 4b14b96f417598bde7fbae5acfe99b3b8faff2e5. --- .github/workflows/e2e_tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index f52e928f..c601e45b 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -3,8 +3,6 @@ name: Playwright Tests on: push: branches: [main] - pull_request: - branches: [main] pull_request_target: branches: [main] From 3e28357e9cdb0434a4d519608f9bd49ad6cdf049 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 13:36:00 -0700 Subject: [PATCH 39/47] Revert "Replace pull_request with pull_request_target" This reverts commit 1ff39f13f5339a1bf87101f80848bc7b69c089b4. --- .github/workflows/e2e_tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index c601e45b..f620c318 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -3,7 +3,7 @@ name: Playwright Tests on: push: branches: [main] - pull_request_target: + pull_request: branches: [main] permissions: @@ -20,10 +20,7 @@ jobs: sudo apt-get update sudo apt-get install -y libwoff1 - # Security: Explicitly checkout the PR's code, not the target branch - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v4 with: From bda9b139718d288ad5224219599d9729bb30df7f Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 20 Jun 2025 16:46:25 -0400 Subject: [PATCH 40/47] Wasn't fully updated with dependency versions in #532 Update package-lock.json --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e557269a..09db8c0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "version": "0.14.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "commander": "^13.1.0", "spawn-rx": "^5.1.2" }, @@ -66,7 +66,7 @@ "version": "0.14.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -10924,7 +10924,7 @@ "version": "0.14.3", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.13.0", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", From 465ee9aaf4480e9703039bb72c18b2774329ec34 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 13:55:12 -0700 Subject: [PATCH 41/47] Revert "Update .github/workflows/e2e_tests.yml" This reverts commit d5b4a6e6a77f52b510306c9a6ba3bb256559469a. --- .github/workflows/e2e_tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index f620c318..83f7da96 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -6,9 +6,6 @@ on: pull_request: branches: [main] -permissions: - pull-requests: write - jobs: test: timeout-minutes: 5 From 4375433a8550117b5260f56f401a27750637deda Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Fri, 20 Jun 2025 15:27:39 -0700 Subject: [PATCH 42/47] docs: Update project organization in CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cli/ directory description - Remove non-existent bin/ directory reference šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index e62fd188..ec826b1f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,4 +30,4 @@ The project is organized as a monorepo with workspaces: - `client/`: React frontend with Vite, TypeScript and Tailwind - `server/`: Express backend with TypeScript -- `bin/`: CLI scripts +- `cli/`: Command-line interface for testing and invoking MCP server methods directly From c747fe85d8e1f0619ed761366dd66c8c30aeced4 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 19:17:12 -0700 Subject: [PATCH 43/47] Only try to comment on PR if the PR is from the canonical repo and not from a fork --- .github/workflows/e2e_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 83f7da96..e56a49d8 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -59,7 +59,7 @@ jobs: - name: Publish Playwright Test Summary uses: daun/playwright-report-summary@v3 - if: always() + if: always() && github.event.pull_request.head.repo.full_name == github.repository with: report-file: client/results.json comment-title: "šŸŽ­ Playwright E2E Test Results" From 94a35d07f3f3e7b98de1ae72c9b22c2604685c3e Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 19:24:32 -0700 Subject: [PATCH 44/47] Only try to comment on PR if the PR is from the canonical repo and not from a fork --- .github/workflows/e2e_tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index e56a49d8..35786cbe 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -59,8 +59,9 @@ jobs: - name: Publish Playwright Test Summary uses: daun/playwright-report-summary@v3 - if: always() && github.event.pull_request.head.repo.full_name == github.repository + if: always() with: + create-comment: github.event.pull_request.head.repo.full_name == github.repository report-file: client/results.json comment-title: "šŸŽ­ Playwright E2E Test Results" job-summary: true From 2cd7b7490c1e68a01fcb214f659d6b6eaad822ae Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 19:29:47 -0700 Subject: [PATCH 45/47] Revert --- .github/workflows/e2e_tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 35786cbe..e56a49d8 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -59,9 +59,8 @@ jobs: - name: Publish Playwright Test Summary uses: daun/playwright-report-summary@v3 - if: always() + if: always() && github.event.pull_request.head.repo.full_name == github.repository with: - create-comment: github.event.pull_request.head.repo.full_name == github.repository report-file: client/results.json comment-title: "šŸŽ­ Playwright E2E Test Results" job-summary: true From ebc249b3d5144cdc27c5763c2c1ea6cd7c991cb5 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 20 Jun 2025 19:43:19 -0700 Subject: [PATCH 46/47] Always gen test summary but only comment on same repo PRs --- .github/workflows/e2e_tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index e56a49d8..f40dd582 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -59,8 +59,9 @@ jobs: - name: Publish Playwright Test Summary uses: daun/playwright-report-summary@v3 - if: always() && github.event.pull_request.head.repo.full_name == github.repository + if: always() with: + create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }} report-file: client/results.json comment-title: "šŸŽ­ Playwright E2E Test Results" job-summary: true From 5db64265cec2942ace9f5aceebd7862aeef3625c Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 23 Jun 2025 16:39:07 +0100 Subject: [PATCH 47/47] [auth] Add resource compatibility to debugger (RFC 8707) (#526) * pass resource in auth debugger, reusing server url * throw error on resource url mismatch to match regular auth flow * lint & tests * add test * make authserver url make more sense * use latest typescript sdk * more lenient resource checking * pick resource, and store in state * fixing tests and types * [auth] use sdk function for selecting resource (#537) * use sdk function * bump typescript sdk * fix some tests --- client/src/components/OAuthFlowProgress.tsx | 2 +- .../__tests__/AuthDebugger.test.tsx | 22 ++++++++++--------- client/src/lib/auth-types.ts | 2 ++ client/src/lib/oauth-state-machine.ts | 19 +++++++++++----- package-lock.json | 9 ++++---- package.json | 2 +- 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index b9b67c6c..0e260556 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -156,7 +156,7 @@ export const OAuthFlowProgress = ({ {authState.resourceMetadataError && (

- ā„¹ļø No resource metadata available from{" "} + ā„¹ļø Problem with resource metadata from{" "} ({ startAuthorization: jest.fn(), exchangeAuthorization: jest.fn(), discoverOAuthProtectedResourceMetadata: jest.fn(), + selectResourceURL: jest.fn(), })); // Import the functions to get their types @@ -88,7 +89,7 @@ describe("AuthDebugger", () => { const defaultAuthState = EMPTY_DEBUGGER_STATE; const defaultProps = { - serverUrl: "https://example.com", + serverUrl: "https://example.com/mcp", onBack: jest.fn(), authState: defaultAuthState, updateAuthState: jest.fn(), @@ -203,7 +204,7 @@ describe("AuthDebugger", () => { // Should first discover and save OAuth metadata expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( - new URL("https://example.com"), + new URL("https://example.com/"), ); // Check that updateAuthState was called with the right info message @@ -320,6 +321,7 @@ describe("AuthDebugger", () => { isInitiatingAuth: false, resourceMetadata: null, resourceMetadataError: null, + resource: null, oauthTokens: null, oauthStep: "metadata_discovery", latestError: null, @@ -361,7 +363,7 @@ describe("AuthDebugger", () => { }); expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( - new URL("https://example.com"), + new URL("https://example.com/"), ); }); @@ -496,11 +498,11 @@ describe("AuthDebugger", () => { it("should successfully fetch and display protected resource metadata", async () => { const updateAuthState = jest.fn(); const mockResourceMetadata = { - resource: "https://example.com/api", + resource: "https://example.com/mcp", authorization_servers: ["https://custom-auth.example.com"], bearer_methods_supported: ["header", "body"], - resource_documentation: "https://example.com/api/docs", - resource_policy_uri: "https://example.com/api/policy", + resource_documentation: "https://example.com/mcp/docs", + resource_policy_uri: "https://example.com/mcp/policy", }; // Mock successful metadata discovery @@ -538,7 +540,7 @@ describe("AuthDebugger", () => { // Wait for the metadata to be fetched await waitFor(() => { expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( - "https://example.com", + "https://example.com/mcp", ); }); @@ -584,7 +586,7 @@ describe("AuthDebugger", () => { // Wait for the metadata fetch to fail await waitFor(() => { expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( - "https://example.com", + "https://example.com/mcp", ); }); @@ -594,7 +596,7 @@ describe("AuthDebugger", () => { expect.objectContaining({ resourceMetadataError: mockError, // Should use the original server URL as fallback - authServerUrl: new URL("https://example.com"), + authServerUrl: new URL("https://example.com/"), oauthStep: "client_registration", }), ); @@ -602,7 +604,7 @@ describe("AuthDebugger", () => { // Verify that regular OAuth metadata discovery was still called expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( - new URL("https://example.com"), + new URL("https://example.com/"), ); }); }); diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index 5e8113ef..f8f927c6 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -30,6 +30,7 @@ export interface AuthDebuggerState { oauthStep: OAuthStep; resourceMetadata: OAuthProtectedResourceMetadata | null; resourceMetadataError: Error | null; + resource: URL | null; authServerUrl: URL | null; oauthMetadata: OAuthMetadata | null; oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null; @@ -47,6 +48,7 @@ export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = { oauthMetadata: null, resourceMetadata: null, resourceMetadataError: null, + resource: null, authServerUrl: null, oauthClientInfo: null, authorizationUrl: null, diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 5f10a783..d87b3ecd 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -6,6 +6,7 @@ import { startAuthorization, exchangeAuthorization, discoverOAuthProtectedResourceMetadata, + selectResourceURL, } from "@modelcontextprotocol/sdk/client/auth.js"; import { OAuthMetadataSchema, @@ -29,17 +30,15 @@ export const oauthTransitions: Record = { metadata_discovery: { canTransition: async () => true, execute: async (context) => { - let authServerUrl = new URL(context.serverUrl); + // Default to discovering from the server's URL + let authServerUrl = new URL("/", context.serverUrl); let resourceMetadata: OAuthProtectedResourceMetadata | null = null; let resourceMetadataError: Error | null = null; try { resourceMetadata = await discoverOAuthProtectedResourceMetadata( context.serverUrl, ); - if ( - resourceMetadata && - resourceMetadata.authorization_servers?.length - ) { + if (resourceMetadata?.authorization_servers?.length) { authServerUrl = new URL(resourceMetadata.authorization_servers[0]); } } catch (e) { @@ -50,6 +49,13 @@ export const oauthTransitions: Record = { } } + const resource: URL | undefined = await selectResourceURL( + context.serverUrl, + context.provider, + // we default to null, so swap it for undefined if not set + resourceMetadata ?? undefined, + ); + const metadata = await discoverOAuthMetadata(authServerUrl); if (!metadata) { throw new Error("Failed to discover OAuth metadata"); @@ -58,6 +64,7 @@ export const oauthTransitions: Record = { context.provider.saveServerMetadata(parsedMetadata); context.updateState({ resourceMetadata, + resource, resourceMetadataError, authServerUrl, oauthMetadata: parsedMetadata, @@ -113,6 +120,7 @@ export const oauthTransitions: Record = { clientInformation, redirectUrl: context.provider.redirectUrl, scope, + resource: context.state.resource ?? undefined, }, ); @@ -163,6 +171,7 @@ export const oauthTransitions: Record = { authorizationCode: context.state.authorizationCode, codeVerifier, redirectUri: context.provider.redirectUrl, + resource: context.state.resource ?? undefined, }); context.provider.saveTokens(tokens); diff --git a/package-lock.json b/package-lock.json index 09db8c0d..695f6a02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@modelcontextprotocol/inspector-cli": "^0.14.3", "@modelcontextprotocol/inspector-client": "^0.14.3", "@modelcontextprotocol/inspector-server": "^0.14.3", - "@modelcontextprotocol/sdk": "^1.13.0", + "@modelcontextprotocol/sdk": "^1.13.1", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2", @@ -2005,10 +2005,9 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.0.tgz", - "integrity": "sha512-P5FZsXU0kY881F6Hbk9GhsYx02/KgWK1DYf7/tyE/1lcFKhDYPQR9iYjhQXJn+Sg6hQleMo3DB7h7+p4wgp2Lw==", - "license": "MIT", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.1.tgz", + "integrity": "sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==", "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", diff --git a/package.json b/package.json index a21363a7..7b89edb8 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@modelcontextprotocol/inspector-cli": "^0.14.3", "@modelcontextprotocol/inspector-client": "^0.14.3", "@modelcontextprotocol/inspector-server": "^0.14.3", - "@modelcontextprotocol/sdk": "^1.13.0", + "@modelcontextprotocol/sdk": "^1.13.1", "concurrently": "^9.0.1", "open": "^10.1.0", "shell-quote": "^1.8.2",