Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdeba454b5 | ||
|
|
91ae93c167 | ||
|
|
35e6c49d7c | ||
|
|
e95b5b1dd6 | ||
|
|
23a2e5fee7 | ||
|
|
d01aa19ffa | ||
|
|
8cd7d5a753 | ||
|
|
42faa3ccf8 | ||
|
|
4694d60fc5 | ||
|
|
7dc689eee7 | ||
|
|
5df011ad4b | ||
|
|
200cf737bb |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -37,7 +37,8 @@ jobs:
|
|||||||
- name: Use Node.js 18
|
- name: Use Node.js 18
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
# https://github.com/microsoft/playwright-mcp/issues/344
|
||||||
|
node-version: '18.19'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
35
.github/workflows/publish.yml
vendored
35
.github/workflows/publish.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
id-token: write # Needed for npm provenance
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@@ -21,4 +21,35 @@ jobs:
|
|||||||
- run: npm run ctest
|
- run: npm run ctest
|
||||||
- run: npm publish --provenance
|
- run: npm publish --provenance
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
publish-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # Needed for OIDC login to Azure
|
||||||
|
environment: allow-publishing-docker-to-acr
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up QEMU # Needed for multi-platform builds (e.g., arm64 on amd64 runner)
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx # Needed for multi-platform builds
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Azure Login via OIDC
|
||||||
|
uses: azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}
|
||||||
|
subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }}
|
||||||
|
- name: Login to ACR
|
||||||
|
run: az acr login --name playwright
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile # Adjust path if your Dockerfile is elsewhere
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
playwright.azurecr.io/public/playwright/mcp:${{ github.ref_name }}
|
||||||
|
playwright.azurecr.io/public/playwright/mcp:latest
|
||||||
|
|||||||
65
Dockerfile
65
Dockerfile
@@ -1,22 +1,69 @@
|
|||||||
FROM node:22-bookworm-slim
|
ARG PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Base
|
||||||
|
# ------------------------------
|
||||||
|
# Base stage: Contains only the minimal dependencies required for runtime
|
||||||
|
# (node_modules and Playwright system dependencies)
|
||||||
|
FROM node:22-bookworm-slim AS base
|
||||||
|
|
||||||
|
ARG PLAYWRIGHT_BROWSERS_PATH
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH}
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package.json and package-lock.json at this stage to leverage the build cache
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
|
||||||
COPY package*.json ./
|
--mount=type=bind,source=package.json,target=package.json \
|
||||||
|
--mount=type=bind,source=package-lock.json,target=package-lock.json \
|
||||||
|
npm ci --omit=dev && \
|
||||||
|
# Install system dependencies for playwright
|
||||||
|
npx -y playwright-core install-deps chromium
|
||||||
|
|
||||||
# Install dependencies
|
# ------------------------------
|
||||||
RUN npm ci
|
# Builder
|
||||||
|
# ------------------------------
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
# Install chromium and its dependencies, but only for headless mode
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \
|
||||||
RUN npx -y playwright install --with-deps --only-shell chromium
|
--mount=type=bind,source=package.json,target=package.json \
|
||||||
|
--mount=type=bind,source=package-lock.json,target=package-lock.json \
|
||||||
|
npm ci
|
||||||
|
|
||||||
# Copy the rest of the app
|
# Copy the rest of the app
|
||||||
COPY . .
|
COPY *.json *.js *.ts .
|
||||||
|
COPY src src/
|
||||||
|
|
||||||
# Build the app
|
# Build the app
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Browser
|
||||||
|
# ------------------------------
|
||||||
|
# Cache optimization:
|
||||||
|
# - Browser is downloaded only when node_modules or Playwright system dependencies change
|
||||||
|
# - Cache is reused when only source code changes
|
||||||
|
FROM base AS browser
|
||||||
|
|
||||||
|
RUN npx -y playwright-core install --no-shell chromium
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Runtime
|
||||||
|
# ------------------------------
|
||||||
|
FROM base
|
||||||
|
|
||||||
|
ARG PLAYWRIGHT_BROWSERS_PATH
|
||||||
|
ARG USERNAME=node
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Set the correct ownership for the runtime user on production `node_modules`
|
||||||
|
RUN chown -R ${USERNAME}:${USERNAME} node_modules
|
||||||
|
|
||||||
|
USER ${USERNAME}
|
||||||
|
|
||||||
|
COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH}
|
||||||
|
COPY --chown=${USERNAME}:${USERNAME} cli.js package.json ./
|
||||||
|
COPY --from=builder --chown=${USERNAME}:${USERNAME} /app/lib /app/lib
|
||||||
|
|
||||||
# Run in headless and only with chromium (other browsers need more dependencies not included in this image)
|
# Run in headless and only with chromium (other browsers need more dependencies not included in this image)
|
||||||
ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium"]
|
ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium"]
|
||||||
|
|||||||
93
README.md
93
README.md
@@ -76,7 +76,10 @@ The Playwright MCP server supports the following command-line options:
|
|||||||
- `--user-data-dir <path>`: Path to the user data directory
|
- `--user-data-dir <path>`: Path to the user data directory
|
||||||
- `--port <port>`: Port to listen on for SSE transport
|
- `--port <port>`: Port to listen on for SSE transport
|
||||||
- `--host <host>`: Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
|
- `--host <host>`: Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
|
||||||
|
- `--allowed-origins <origins>`: Semicolon-separated list of origins to allow the browser to request. Default is to allow all. Origins matching both `--allowed-origins` and `--blocked-origins` will be blocked.
|
||||||
|
- `--blocked-origins <origins>`: Semicolon-separated list of origins to block the browser to request. Origins matching both `--allowed-origins` and `--blocked-origins` will be blocked.
|
||||||
- `--vision`: Run server that uses screenshots (Aria snapshots are used by default)
|
- `--vision`: Run server that uses screenshots (Aria snapshots are used by default)
|
||||||
|
- `--output-dir`: Directory for output files
|
||||||
- `--config <path>`: Path to the configuration file
|
- `--config <path>`: Path to the configuration file
|
||||||
|
|
||||||
### User profile
|
### User profile
|
||||||
@@ -152,13 +155,19 @@ The Playwright MCP server can be configured using a JSON configuration file. Her
|
|||||||
// Directory for output files
|
// Directory for output files
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
|
|
||||||
// Tool-specific configurations
|
// Network configuration
|
||||||
tools?: {
|
network?: {
|
||||||
browser_take_screenshot?: {
|
// List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
||||||
// Disable base64-encoded image responses
|
allowedOrigins?: string[];
|
||||||
omitBase64?: boolean;
|
|
||||||
}
|
// List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
||||||
}
|
blockedOrigins?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not send image responses to the client.
|
||||||
|
*/
|
||||||
|
noImageResponses?: boolean;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -222,9 +231,9 @@ http.createServer(async (req, res) => {
|
|||||||
// ...
|
// ...
|
||||||
|
|
||||||
// Creates a headless Playwright MCP server with SSE transport
|
// Creates a headless Playwright MCP server with SSE transport
|
||||||
const mcpServer = await createServer({ headless: true });
|
const connection = await createConnection({ headless: true });
|
||||||
const transport = new SSEServerTransport('/messages', res);
|
const transport = new SSEServerTransport('/messages', res);
|
||||||
await mcpServer.connect(transport);
|
await connection.connect(transport);
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
});
|
});
|
||||||
@@ -264,38 +273,47 @@ X Y coordinate space, based on the provided screenshot.
|
|||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_snapshot**
|
- **browser_snapshot**
|
||||||
|
- Title: Page snapshot
|
||||||
- Description: Capture accessibility snapshot of the current page, this is better than screenshot
|
- Description: Capture accessibility snapshot of the current page, this is better than screenshot
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_click**
|
- **browser_click**
|
||||||
|
- Title: Click
|
||||||
- Description: Perform click on a web page
|
- Description: Perform click on a web page
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
||||||
- `ref` (string): Exact target element reference from the page snapshot
|
- `ref` (string): Exact target element reference from the page snapshot
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_drag**
|
- **browser_drag**
|
||||||
|
- Title: Drag mouse
|
||||||
- Description: Perform drag and drop between two elements
|
- Description: Perform drag and drop between two elements
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element
|
- `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element
|
||||||
- `startRef` (string): Exact source element reference from the page snapshot
|
- `startRef` (string): Exact source element reference from the page snapshot
|
||||||
- `endElement` (string): Human-readable target element description used to obtain the permission to interact with the element
|
- `endElement` (string): Human-readable target element description used to obtain the permission to interact with the element
|
||||||
- `endRef` (string): Exact target element reference from the page snapshot
|
- `endRef` (string): Exact target element reference from the page snapshot
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_hover**
|
- **browser_hover**
|
||||||
|
- Title: Hover mouse
|
||||||
- Description: Hover over element on page
|
- Description: Hover over element on page
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
||||||
- `ref` (string): Exact target element reference from the page snapshot
|
- `ref` (string): Exact target element reference from the page snapshot
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_type**
|
- **browser_type**
|
||||||
|
- Title: Type text
|
||||||
- Description: Type text into editable element
|
- Description: Type text into editable element
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
||||||
@@ -303,54 +321,66 @@ X Y coordinate space, based on the provided screenshot.
|
|||||||
- `text` (string): Text to type into the element
|
- `text` (string): Text to type into the element
|
||||||
- `submit` (boolean, optional): Whether to submit entered text (press Enter after)
|
- `submit` (boolean, optional): Whether to submit entered text (press Enter after)
|
||||||
- `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.
|
- `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_select_option**
|
- **browser_select_option**
|
||||||
|
- Title: Select option
|
||||||
- Description: Select an option in a dropdown
|
- Description: Select an option in a dropdown
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
||||||
- `ref` (string): Exact target element reference from the page snapshot
|
- `ref` (string): Exact target element reference from the page snapshot
|
||||||
- `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.
|
- `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_take_screenshot**
|
- **browser_take_screenshot**
|
||||||
|
- Title: Take a screenshot
|
||||||
- Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
|
- Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
|
- `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
|
||||||
- `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
|
- `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
|
||||||
- `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
|
- `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
### Vision-based Interactions
|
### Vision-based Interactions
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_screen_capture**
|
- **browser_screen_capture**
|
||||||
|
- Title: Take a screenshot
|
||||||
- Description: Take a screenshot of the current page
|
- Description: Take a screenshot of the current page
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_screen_move_mouse**
|
- **browser_screen_move_mouse**
|
||||||
|
- Title: Move mouse
|
||||||
- Description: Move mouse to a given position
|
- Description: Move mouse to a given position
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
||||||
- `x` (number): X coordinate
|
- `x` (number): X coordinate
|
||||||
- `y` (number): Y coordinate
|
- `y` (number): Y coordinate
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_screen_click**
|
- **browser_screen_click**
|
||||||
|
- Title: Click
|
||||||
- Description: Click left mouse button
|
- Description: Click left mouse button
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
||||||
- `x` (number): X coordinate
|
- `x` (number): X coordinate
|
||||||
- `y` (number): Y coordinate
|
- `y` (number): Y coordinate
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_screen_drag**
|
- **browser_screen_drag**
|
||||||
|
- Title: Drag mouse
|
||||||
- Description: Drag left mouse button
|
- Description: Drag left mouse button
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
||||||
@@ -358,143 +388,188 @@ X Y coordinate space, based on the provided screenshot.
|
|||||||
- `startY` (number): Start Y coordinate
|
- `startY` (number): Start Y coordinate
|
||||||
- `endX` (number): End X coordinate
|
- `endX` (number): End X coordinate
|
||||||
- `endY` (number): End Y coordinate
|
- `endY` (number): End Y coordinate
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_screen_type**
|
- **browser_screen_type**
|
||||||
|
- Title: Type text
|
||||||
- Description: Type text
|
- Description: Type text
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `text` (string): Text to type into the element
|
- `text` (string): Text to type into the element
|
||||||
- `submit` (boolean, optional): Whether to submit entered text (press Enter after)
|
- `submit` (boolean, optional): Whether to submit entered text (press Enter after)
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
### Tab Management
|
### Tab Management
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_tab_list**
|
- **browser_tab_list**
|
||||||
|
- Title: List tabs
|
||||||
- Description: List browser tabs
|
- Description: List browser tabs
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_tab_new**
|
- **browser_tab_new**
|
||||||
|
- Title: Open a new tab
|
||||||
- Description: Open a new tab
|
- Description: Open a new tab
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank.
|
- `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank.
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_tab_select**
|
- **browser_tab_select**
|
||||||
|
- Title: Select a tab
|
||||||
- Description: Select a tab by index
|
- Description: Select a tab by index
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `index` (number): The index of the tab to select
|
- `index` (number): The index of the tab to select
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_tab_close**
|
- **browser_tab_close**
|
||||||
|
- Title: Close a tab
|
||||||
- Description: Close a tab
|
- Description: Close a tab
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `index` (number, optional): The index of the tab to close. Closes current tab if not provided.
|
- `index` (number, optional): The index of the tab to close. Closes current tab if not provided.
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
### Navigation
|
### Navigation
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_navigate**
|
- **browser_navigate**
|
||||||
|
- Title: Navigate to a URL
|
||||||
- Description: Navigate to a URL
|
- Description: Navigate to a URL
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `url` (string): The URL to navigate to
|
- `url` (string): The URL to navigate to
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_navigate_back**
|
- **browser_navigate_back**
|
||||||
|
- Title: Go back
|
||||||
- Description: Go back to the previous page
|
- Description: Go back to the previous page
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_navigate_forward**
|
- **browser_navigate_forward**
|
||||||
|
- Title: Go forward
|
||||||
- Description: Go forward to the next page
|
- Description: Go forward to the next page
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
### Keyboard
|
### Keyboard
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_press_key**
|
- **browser_press_key**
|
||||||
|
- Title: Press a key
|
||||||
- Description: Press a key on the keyboard
|
- Description: Press a key on the keyboard
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
|
- `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
### Console
|
### Console
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_console_messages**
|
- **browser_console_messages**
|
||||||
|
- Title: Get console messages
|
||||||
- Description: Returns all console messages
|
- Description: Returns all console messages
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
### Files and Media
|
### Files and Media
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_file_upload**
|
- **browser_file_upload**
|
||||||
|
- Title: Upload files
|
||||||
- Description: Upload one or multiple files
|
- Description: Upload one or multiple files
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_pdf_save**
|
- **browser_pdf_save**
|
||||||
|
- Title: Save as PDF
|
||||||
- Description: Save page as PDF
|
- Description: Save page as PDF
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
### Utilities
|
### Utilities
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_close**
|
- **browser_close**
|
||||||
|
- Title: Close browser
|
||||||
- Description: Close the page
|
- Description: Close the page
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_wait**
|
- **browser_wait**
|
||||||
|
- Title: Wait
|
||||||
- Description: Wait for a specified time in seconds
|
- Description: Wait for a specified time in seconds
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `time` (number): The time to wait in seconds
|
- `time` (number): The time to wait in seconds
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_resize**
|
- **browser_resize**
|
||||||
|
- Title: Resize browser window
|
||||||
- Description: Resize the browser window
|
- Description: Resize the browser window
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `width` (number): Width of the browser window
|
- `width` (number): Width of the browser window
|
||||||
- `height` (number): Height of the browser window
|
- `height` (number): Height of the browser window
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_install**
|
- **browser_install**
|
||||||
|
- Title: Install the browser specified in the config
|
||||||
- Description: Install the browser specified in the config. Call this if you get an error about the browser not being installed.
|
- Description: Install the browser specified in the config. Call this if you get an error about the browser not being installed.
|
||||||
- Parameters: None
|
- Parameters: None
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_handle_dialog**
|
- **browser_handle_dialog**
|
||||||
|
- Title: Handle a dialog
|
||||||
- Description: Handle a dialog
|
- Description: Handle a dialog
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `accept` (boolean): Whether to accept the dialog.
|
- `accept` (boolean): Whether to accept the dialog.
|
||||||
- `promptText` (string, optional): The text of the prompt in case of a prompt dialog.
|
- `promptText` (string, optional): The text of the prompt in case of a prompt dialog.
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
|
- **browser_network_requests**
|
||||||
|
- Title: List network requests
|
||||||
|
- Description: Returns all network requests since loading the page
|
||||||
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_generate_playwright_test**
|
- **browser_generate_playwright_test**
|
||||||
|
- Title: Generate a Playwright test
|
||||||
- Description: Generate a Playwright test for given scenario
|
- Description: Generate a Playwright test for given scenario
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `name` (string): The name of the test
|
- `name` (string): The name of the test
|
||||||
- `description` (string): The description of the test
|
- `description` (string): The description of the test
|
||||||
- `steps` (array): The steps of the test
|
- `steps` (array): The steps of the test
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
<!--- End of generated section -->
|
<!--- End of generated section -->
|
||||||
|
|||||||
26
config.d.ts
vendored
26
config.d.ts
vendored
@@ -94,20 +94,20 @@ export type Config = {
|
|||||||
*/
|
*/
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
|
|
||||||
/**
|
network?: {
|
||||||
* Configuration for specific tools.
|
|
||||||
*/
|
|
||||||
tools?: {
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the browser_take_screenshot tool.
|
* List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
||||||
*/
|
*/
|
||||||
browser_take_screenshot?: {
|
allowedOrigins?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to disable base64-encoded image responses to the clients that
|
* List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
||||||
* don't support binary data or prefer to save on tokens.
|
*/
|
||||||
*/
|
blockedOrigins?: string[];
|
||||||
omitBase64?: boolean;
|
};
|
||||||
}
|
|
||||||
}
|
/**
|
||||||
|
* Do not send image responses to the client.
|
||||||
|
*/
|
||||||
|
noImageResponses?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
10
index.d.ts
vendored
10
index.d.ts
vendored
@@ -16,8 +16,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
|
||||||
import type { Config } from './config';
|
import type { Config } from './config';
|
||||||
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
|
||||||
export declare function createServer(config?: Config): Promise<Server>;
|
export type Connection = {
|
||||||
|
server: Server;
|
||||||
|
connect(transport: Transport): Promise<void>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare function createConnection(config?: Config): Promise<Connection>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
4
index.js
4
index.js
@@ -15,5 +15,5 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createServer } from './lib/index';
|
import { createConnection } from './lib/index';
|
||||||
export default { createServer };
|
export default { createConnection };
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@playwright/mcp",
|
"name": "@playwright/mcp",
|
||||||
"version": "0.0.20",
|
"version": "0.0.21",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@playwright/mcp",
|
"name": "@playwright/mcp",
|
||||||
"version": "0.0.20",
|
"version": "0.0.21",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.10.1",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"playwright": "1.53.0-alpha-1746218818000",
|
"playwright": "1.53.0-alpha-1746218818000",
|
||||||
"yaml": "^2.7.1",
|
"yaml": "^2.7.1",
|
||||||
@@ -228,9 +228,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.10.1",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz",
|
||||||
"integrity": "sha512-xNYdFdkJqEfIaTVP1gPKoEvluACHZsHZegIoICX8DM1o6Qf3G5u2BQJHmgd0n4YgRPqqK/u1ujQvrgAxxSJT9w==",
|
"integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@playwright/mcp",
|
"name": "@playwright/mcp",
|
||||||
"version": "0.0.20",
|
"version": "0.0.21",
|
||||||
"description": "Playwright Tools for MCP",
|
"description": "Playwright Tools for MCP",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "npm run update-readme && eslint .",
|
"lint": "npm run update-readme && eslint . && tsc --noEmit",
|
||||||
"update-readme": "node utils/update-readme.js",
|
"update-readme": "node utils/update-readme.js",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.10.1",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"playwright": "1.53.0-alpha-1746218818000",
|
"playwright": "1.53.0-alpha-1746218818000",
|
||||||
"yaml": "^2.7.1",
|
"yaml": "^2.7.1",
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ export type CLIOptions = {
|
|||||||
host?: string;
|
host?: string;
|
||||||
vision?: boolean;
|
vision?: boolean;
|
||||||
config?: string;
|
config?: string;
|
||||||
|
allowedOrigins?: string[];
|
||||||
|
blockedOrigins?: string[];
|
||||||
|
outputDir?: string;
|
||||||
|
noImageResponses?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultConfig: Config = {
|
const defaultConfig: Config = {
|
||||||
browser: {
|
browser: {
|
||||||
browserName: 'chromium',
|
browserName: 'chromium',
|
||||||
userDataDir: os.tmpdir(),
|
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
channel: 'chrome',
|
channel: 'chrome',
|
||||||
headless: os.platform() === 'linux' && !process.env.DISPLAY,
|
headless: os.platform() === 'linux' && !process.env.DISPLAY,
|
||||||
@@ -50,6 +53,10 @@ const defaultConfig: Config = {
|
|||||||
viewport: null,
|
viewport: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
network: {
|
||||||
|
allowedOrigins: undefined,
|
||||||
|
blockedOrigins: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function resolveConfig(cliOptions: CLIOptions): Promise<Config> {
|
export async function resolveConfig(cliOptions: CLIOptions): Promise<Config> {
|
||||||
@@ -99,7 +106,7 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
|
|||||||
return {
|
return {
|
||||||
browser: {
|
browser: {
|
||||||
browserName,
|
browserName,
|
||||||
userDataDir: cliOptions.userDataDir ?? await createUserDataDir({ browserName, channel }),
|
userDataDir: cliOptions.userDataDir,
|
||||||
launchOptions,
|
launchOptions,
|
||||||
contextOptions,
|
contextOptions,
|
||||||
cdpEndpoint: cliOptions.cdpEndpoint,
|
cdpEndpoint: cliOptions.cdpEndpoint,
|
||||||
@@ -110,6 +117,11 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
|
|||||||
},
|
},
|
||||||
capabilities: cliOptions.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
|
capabilities: cliOptions.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
|
||||||
vision: !!cliOptions.vision,
|
vision: !!cliOptions.vision,
|
||||||
|
network: {
|
||||||
|
allowedOrigins: cliOptions.allowedOrigins,
|
||||||
|
blockedOrigins: cliOptions.blockedOrigins,
|
||||||
|
},
|
||||||
|
outputDir: cliOptions.outputDir,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,21 +147,6 @@ async function loadConfig(configFile: string | undefined): Promise<Config> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createUserDataDir(options: { browserName: 'chromium' | 'firefox' | 'webkit', channel: string | undefined }) {
|
|
||||||
let cacheDirectory: string;
|
|
||||||
if (process.platform === 'linux')
|
|
||||||
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
||||||
else if (process.platform === 'darwin')
|
|
||||||
cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
|
|
||||||
else if (process.platform === 'win32')
|
|
||||||
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
||||||
else
|
|
||||||
throw new Error('Unsupported platform: ' + process.platform);
|
|
||||||
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${options.channel ?? options.browserName}-profile`);
|
|
||||||
await fs.promises.mkdir(result, { recursive: true });
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function outputFile(config: Config, name: string): Promise<string> {
|
export async function outputFile(config: Config, name: string): Promise<string> {
|
||||||
const result = config.outputDir ?? os.tmpdir();
|
const result = config.outputDir ?? os.tmpdir();
|
||||||
await fs.promises.mkdir(result, { recursive: true });
|
await fs.promises.mkdir(result, { recursive: true });
|
||||||
@@ -185,5 +182,9 @@ function mergeConfig(base: Config, overrides: Config): Config {
|
|||||||
...pickDefined(base),
|
...pickDefined(base),
|
||||||
...pickDefined(overrides),
|
...pickDefined(overrides),
|
||||||
browser,
|
browser,
|
||||||
|
network: {
|
||||||
|
...pickDefined(base.network),
|
||||||
|
...pickDefined(overrides.network),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,24 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
import { CallToolRequestSchema, ListToolsRequestSchema, Tool as McpTool } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||||
|
|
||||||
import { Context } from './context.js';
|
import { Context, packageJSON } from './context.js';
|
||||||
|
import { snapshotTools, screenshotTools } from './tools.js';
|
||||||
|
|
||||||
import type { Tool } from './tools/tool.js';
|
|
||||||
import type { Config } from '../config.js';
|
import type { Config } from '../config.js';
|
||||||
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
|
||||||
type MCPServerOptions = {
|
export async function createConnection(config: Config): Promise<Connection> {
|
||||||
name: string;
|
const allTools = config.vision ? screenshotTools : snapshotTools;
|
||||||
version: string;
|
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
||||||
tools: Tool[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createServerWithTools(serverOptions: MCPServerOptions, config: Config): Server {
|
|
||||||
const { name, version, tools } = serverOptions;
|
|
||||||
const context = new Context(tools, config);
|
const context = new Context(tools, config);
|
||||||
const server = new Server({ name, version }, {
|
const server = new Server({ name: 'Playwright', version: packageJSON.version }, {
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
}
|
}
|
||||||
@@ -43,8 +40,14 @@ export function createServerWithTools(serverOptions: MCPServerOptions, config: C
|
|||||||
tools: tools.map(tool => ({
|
tools: tools.map(tool => ({
|
||||||
name: tool.schema.name,
|
name: tool.schema.name,
|
||||||
description: tool.schema.description,
|
description: tool.schema.description,
|
||||||
inputSchema: zodToJsonSchema(tool.schema.inputSchema)
|
inputSchema: zodToJsonSchema(tool.schema.inputSchema),
|
||||||
})),
|
annotations: {
|
||||||
|
title: tool.schema.title,
|
||||||
|
readOnlyHint: tool.schema.type === 'readOnly',
|
||||||
|
destructiveHint: tool.schema.type === 'destructive',
|
||||||
|
openWorldHint: true,
|
||||||
|
},
|
||||||
|
})) as McpTool[],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,38 +74,30 @@ export function createServerWithTools(serverOptions: MCPServerOptions, config: C
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const oldClose = server.close.bind(server);
|
const connection = new Connection(server, context);
|
||||||
|
return connection;
|
||||||
server.close = async () => {
|
|
||||||
await oldClose();
|
|
||||||
await context.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerList {
|
export class Connection {
|
||||||
private _servers: Server[] = [];
|
readonly server: Server;
|
||||||
private _serverFactory: () => Promise<Server>;
|
readonly context: Context;
|
||||||
|
|
||||||
constructor(serverFactory: () => Promise<Server>) {
|
constructor(server: Server, context: Context) {
|
||||||
this._serverFactory = serverFactory;
|
this.server = server;
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create() {
|
async connect(transport: Transport) {
|
||||||
const server = await this._serverFactory();
|
await this.server.connect(transport);
|
||||||
this._servers.push(server);
|
await new Promise<void>(resolve => {
|
||||||
return server;
|
this.server.oninitialized = () => resolve();
|
||||||
|
});
|
||||||
|
if (this.server.getClientVersion()?.name.includes('cursor'))
|
||||||
|
this.context.config.noImageResponses = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(server: Server) {
|
async close() {
|
||||||
const index = this._servers.indexOf(server);
|
await this.server.close();
|
||||||
if (index !== -1)
|
await this.context.close();
|
||||||
this._servers.splice(index, 1);
|
|
||||||
await server.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
async closeAll() {
|
|
||||||
await Promise.all(this._servers.map(server => server.close()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import url from 'node:url';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
|
|
||||||
import { waitForCompletion } from './tools/utils.js';
|
import { waitForCompletion } from './tools/utils.js';
|
||||||
@@ -286,11 +291,26 @@ ${code.join('\n')}
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _setupRequestInterception(context: playwright.BrowserContext) {
|
||||||
|
if (this.config.network?.allowedOrigins?.length) {
|
||||||
|
await context.route('**', route => route.abort('blockedbyclient'));
|
||||||
|
|
||||||
|
for (const origin of this.config.network.allowedOrigins)
|
||||||
|
await context.route(`*://${origin}/**`, route => route.continue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.network?.blockedOrigins?.length) {
|
||||||
|
for (const origin of this.config.network.blockedOrigins)
|
||||||
|
await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _ensureBrowserContext() {
|
private async _ensureBrowserContext() {
|
||||||
if (!this._browserContext) {
|
if (!this._browserContext) {
|
||||||
const context = await this._createBrowserContext();
|
const context = await this._createBrowserContext();
|
||||||
this._browser = context.browser;
|
this._browser = context.browser;
|
||||||
this._browserContext = context.browserContext;
|
this._browserContext = context.browserContext;
|
||||||
|
await this._setupRequestInterception(this._browserContext);
|
||||||
for (const page of this._browserContext.pages())
|
for (const page of this._browserContext.pages())
|
||||||
this._onPageCreated(page);
|
this._onPageCreated(page);
|
||||||
this._browserContext.on('page', page => this._onPageCreated(page));
|
this._browserContext.on('page', page => this._onPageCreated(page));
|
||||||
@@ -333,8 +353,10 @@ ${code.join('\n')}
|
|||||||
|
|
||||||
async function launchPersistentContext(browserConfig: Config['browser']): Promise<playwright.BrowserContext> {
|
async function launchPersistentContext(browserConfig: Config['browser']): Promise<playwright.BrowserContext> {
|
||||||
try {
|
try {
|
||||||
const browserType = browserConfig?.browserName ? playwright[browserConfig.browserName] : playwright.chromium;
|
const browserName = browserConfig?.browserName ?? 'chromium';
|
||||||
return await browserType.launchPersistentContext(browserConfig?.userDataDir || '', { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
|
const userDataDir = browserConfig?.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
|
||||||
|
const browserType = playwright[browserName];
|
||||||
|
return await browserType.launchPersistentContext(userDataDir, { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message.includes('Executable doesn\'t exist'))
|
if (error.message.includes('Executable doesn\'t exist'))
|
||||||
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
||||||
@@ -342,6 +364,24 @@ async function launchPersistentContext(browserConfig: Config['browser']): Promis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createUserDataDir(browserConfig: Config['browser']) {
|
||||||
|
let cacheDirectory: string;
|
||||||
|
if (process.platform === 'linux')
|
||||||
|
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
||||||
|
else if (process.platform === 'darwin')
|
||||||
|
cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
|
||||||
|
else if (process.platform === 'win32')
|
||||||
|
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||||
|
else
|
||||||
|
throw new Error('Unsupported platform: ' + process.platform);
|
||||||
|
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions.channel ?? browserConfig?.browserName}-profile`);
|
||||||
|
await fs.promises.mkdir(result, { recursive: true });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateLocator(locator: playwright.Locator): Promise<string> {
|
export async function generateLocator(locator: playwright.Locator): Promise<string> {
|
||||||
return (locator as any)._generateLocatorString();
|
return (locator as any)._generateLocatorString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
|
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
||||||
|
|||||||
60
src/index.ts
60
src/index.ts
@@ -14,62 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createServerWithTools } from './server.js';
|
import { Connection } from './connection.js';
|
||||||
import common from './tools/common.js';
|
|
||||||
import console from './tools/console.js';
|
|
||||||
import dialogs from './tools/dialogs.js';
|
|
||||||
import files from './tools/files.js';
|
|
||||||
import install from './tools/install.js';
|
|
||||||
import keyboard from './tools/keyboard.js';
|
|
||||||
import navigate from './tools/navigate.js';
|
|
||||||
import network from './tools/network.js';
|
|
||||||
import pdf from './tools/pdf.js';
|
|
||||||
import snapshot from './tools/snapshot.js';
|
|
||||||
import tabs from './tools/tabs.js';
|
|
||||||
import screen from './tools/screen.js';
|
|
||||||
import testing from './tools/testing.js';
|
|
||||||
import type { Tool } from './tools/tool.js';
|
|
||||||
import type { Config } from '../config.js';
|
import type { Config } from '../config.js';
|
||||||
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
||||||
|
|
||||||
const snapshotTools: Tool<any>[] = [
|
export async function createConnection(config: Config = {}): Promise<Connection> {
|
||||||
...common(true),
|
return createConnection(config);
|
||||||
...console,
|
|
||||||
...dialogs(true),
|
|
||||||
...files(true),
|
|
||||||
...install,
|
|
||||||
...keyboard(true),
|
|
||||||
...navigate(true),
|
|
||||||
...network,
|
|
||||||
...pdf,
|
|
||||||
...snapshot,
|
|
||||||
...tabs(true),
|
|
||||||
...testing,
|
|
||||||
];
|
|
||||||
|
|
||||||
const screenshotTools: Tool<any>[] = [
|
|
||||||
...common(false),
|
|
||||||
...console,
|
|
||||||
...dialogs(false),
|
|
||||||
...files(false),
|
|
||||||
...install,
|
|
||||||
...keyboard(false),
|
|
||||||
...navigate(false),
|
|
||||||
...network,
|
|
||||||
...pdf,
|
|
||||||
...screen,
|
|
||||||
...tabs(false),
|
|
||||||
...testing,
|
|
||||||
];
|
|
||||||
|
|
||||||
import packageJSON from '../package.json' with { type: 'json' };
|
|
||||||
|
|
||||||
export async function createServer(config: Config = {}): Promise<Server> {
|
|
||||||
const allTools = config.vision ? screenshotTools : snapshotTools;
|
|
||||||
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
|
||||||
return createServerWithTools({
|
|
||||||
name: 'Playwright',
|
|
||||||
version: packageJSON.version,
|
|
||||||
tools,
|
|
||||||
}, config);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,11 @@
|
|||||||
|
|
||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
|
|
||||||
import { createServer } from './index.js';
|
|
||||||
import { ServerList } from './server.js';
|
|
||||||
|
|
||||||
import { startHttpTransport, startStdioTransport } from './transport.js';
|
import { startHttpTransport, startStdioTransport } from './transport.js';
|
||||||
|
|
||||||
import { resolveConfig } from './config.js';
|
import { resolveConfig } from './config.js';
|
||||||
|
|
||||||
import packageJSON from '../package.json' with { type: 'json' };
|
import type { Connection } from './connection.js';
|
||||||
|
import { packageJSON } from './context.js';
|
||||||
|
|
||||||
program
|
program
|
||||||
.version('Version ' + packageJSON.version)
|
.version('Version ' + packageJSON.version)
|
||||||
@@ -37,23 +34,28 @@ program
|
|||||||
.option('--user-data-dir <path>', 'Path to the user data directory')
|
.option('--user-data-dir <path>', 'Path to the user data directory')
|
||||||
.option('--port <port>', 'Port to listen on for SSE transport.')
|
.option('--port <port>', 'Port to listen on for SSE transport.')
|
||||||
.option('--host <host>', 'Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
.option('--host <host>', 'Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
||||||
|
.option('--allowed-origins <origins>', 'Semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
|
||||||
|
.option('--blocked-origins <origins>', 'Semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
|
||||||
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
||||||
|
.option('--no-image-responses', 'Do not send image responses to the client.')
|
||||||
|
.option('--output-dir <path>', 'Path to the directory for output files.')
|
||||||
.option('--config <path>', 'Path to the configuration file.')
|
.option('--config <path>', 'Path to the configuration file.')
|
||||||
.action(async options => {
|
.action(async options => {
|
||||||
const config = await resolveConfig(options);
|
const config = await resolveConfig(options);
|
||||||
const serverList = new ServerList(() => createServer(config));
|
const connectionList: Connection[] = [];
|
||||||
setupExitWatchdog(serverList);
|
setupExitWatchdog(connectionList);
|
||||||
|
|
||||||
if (options.port)
|
if (options.port)
|
||||||
startHttpTransport(+options.port, options.host, serverList);
|
startHttpTransport(config, +options.port, options.host, connectionList);
|
||||||
else
|
else
|
||||||
await startStdioTransport(serverList);
|
await startStdioTransport(config, connectionList);
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupExitWatchdog(serverList: ServerList) {
|
function setupExitWatchdog(connectionList: Connection[]) {
|
||||||
const handleExit = async () => {
|
const handleExit = async () => {
|
||||||
setTimeout(() => process.exit(0), 15000);
|
setTimeout(() => process.exit(0), 15000);
|
||||||
await serverList.closeAll();
|
for (const connection of connectionList)
|
||||||
|
await connection.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,4 +64,8 @@ function setupExitWatchdog(serverList: ServerList) {
|
|||||||
process.on('SIGTERM', handleExit);
|
process.on('SIGTERM', handleExit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function semicolonSeparatedList(value: string): string[] {
|
||||||
|
return value.split(';').map(v => v.trim());
|
||||||
|
}
|
||||||
|
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
|
|||||||
61
src/tools.ts
Normal file
61
src/tools.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import common from './tools/common.js';
|
||||||
|
import console from './tools/console.js';
|
||||||
|
import dialogs from './tools/dialogs.js';
|
||||||
|
import files from './tools/files.js';
|
||||||
|
import install from './tools/install.js';
|
||||||
|
import keyboard from './tools/keyboard.js';
|
||||||
|
import navigate from './tools/navigate.js';
|
||||||
|
import network from './tools/network.js';
|
||||||
|
import pdf from './tools/pdf.js';
|
||||||
|
import snapshot from './tools/snapshot.js';
|
||||||
|
import tabs from './tools/tabs.js';
|
||||||
|
import screen from './tools/screen.js';
|
||||||
|
import testing from './tools/testing.js';
|
||||||
|
|
||||||
|
import type { Tool } from './tools/tool.js';
|
||||||
|
|
||||||
|
export const snapshotTools: Tool<any>[] = [
|
||||||
|
...common(true),
|
||||||
|
...console,
|
||||||
|
...dialogs(true),
|
||||||
|
...files(true),
|
||||||
|
...install,
|
||||||
|
...keyboard(true),
|
||||||
|
...navigate(true),
|
||||||
|
...network,
|
||||||
|
...pdf,
|
||||||
|
...snapshot,
|
||||||
|
...tabs(true),
|
||||||
|
...testing,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const screenshotTools: Tool<any>[] = [
|
||||||
|
...common(false),
|
||||||
|
...console,
|
||||||
|
...dialogs(false),
|
||||||
|
...files(false),
|
||||||
|
...install,
|
||||||
|
...keyboard(false),
|
||||||
|
...navigate(false),
|
||||||
|
...network,
|
||||||
|
...pdf,
|
||||||
|
...screen,
|
||||||
|
...tabs(false),
|
||||||
|
...testing,
|
||||||
|
];
|
||||||
@@ -22,10 +22,12 @@ const wait: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_wait',
|
name: 'browser_wait',
|
||||||
|
title: 'Wait',
|
||||||
description: 'Wait for a specified time in seconds',
|
description: 'Wait for a specified time in seconds',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
time: z.number().describe('The time to wait in seconds'),
|
time: z.number().describe('The time to wait in seconds'),
|
||||||
}),
|
}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -43,8 +45,10 @@ const close = defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_close',
|
name: 'browser_close',
|
||||||
|
title: 'Close browser',
|
||||||
description: 'Close the page',
|
description: 'Close the page',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
@@ -61,11 +65,13 @@ const resize: ToolFactory = captureSnapshot => defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_resize',
|
name: 'browser_resize',
|
||||||
|
title: 'Resize browser window',
|
||||||
description: 'Resize the browser window',
|
description: 'Resize the browser window',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
width: z.number().describe('Width of the browser window'),
|
width: z.number().describe('Width of the browser window'),
|
||||||
height: z.number().describe('Height of the browser window'),
|
height: z.number().describe('Height of the browser window'),
|
||||||
}),
|
}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ const console = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_console_messages',
|
name: 'browser_console_messages',
|
||||||
|
title: 'Get console messages',
|
||||||
description: 'Returns all console messages',
|
description: 'Returns all console messages',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
const messages = context.currentTabOrDie().console();
|
const messages = context.currentTabOrDie().console();
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ const handleDialog: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_handle_dialog',
|
name: 'browser_handle_dialog',
|
||||||
|
title: 'Handle a dialog',
|
||||||
description: 'Handle a dialog',
|
description: 'Handle a dialog',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
accept: z.boolean().describe('Whether to accept the dialog.'),
|
accept: z.boolean().describe('Whether to accept the dialog.'),
|
||||||
promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'),
|
promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ const uploadFile: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_file_upload',
|
name: 'browser_file_upload',
|
||||||
|
title: 'Upload files',
|
||||||
description: 'Upload one or multiple files',
|
description: 'Upload one or multiple files',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
|
paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
|||||||
@@ -20,21 +20,23 @@ import path from 'path';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineTool } from './tool.js';
|
import { defineTool } from './tool.js';
|
||||||
|
|
||||||
import { createRequire } from 'node:module';
|
import { fileURLToPath } from 'node:url';
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
const install = defineTool({
|
const install = defineTool({
|
||||||
capability: 'install',
|
capability: 'install',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_install',
|
name: 'browser_install',
|
||||||
|
title: 'Install the browser specified in the config',
|
||||||
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
|
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.launchOptions.browserName ?? 'chrome';
|
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.launchOptions.browserName ?? 'chrome';
|
||||||
const cli = path.join(require.resolve('playwright/package.json'), '..', 'cli.js');
|
const cliUrl = import.meta.resolve('playwright/package.json');
|
||||||
const child = fork(cli, ['install', channel], {
|
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
|
||||||
|
const child = fork(cliPath, ['install', channel], {
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
});
|
});
|
||||||
const output: string[] = [];
|
const output: string[] = [];
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ const pressKey: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_press_key',
|
name: 'browser_press_key',
|
||||||
|
title: 'Press a key',
|
||||||
description: 'Press a key on the keyboard',
|
description: 'Press a key on the keyboard',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
|
key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ const navigate: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
|
title: 'Navigate to a URL',
|
||||||
description: 'Navigate to a URL',
|
description: 'Navigate to a URL',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
url: z.string().describe('The URL to navigate to'),
|
url: z.string().describe('The URL to navigate to'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -49,8 +51,10 @@ const goBack: ToolFactory = captureSnapshot => defineTool({
|
|||||||
capability: 'history',
|
capability: 'history',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_navigate_back',
|
name: 'browser_navigate_back',
|
||||||
|
title: 'Go back',
|
||||||
description: 'Go back to the previous page',
|
description: 'Go back to the previous page',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
@@ -73,8 +77,10 @@ const goForward: ToolFactory = captureSnapshot => defineTool({
|
|||||||
capability: 'history',
|
capability: 'history',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_navigate_forward',
|
name: 'browser_navigate_forward',
|
||||||
|
title: 'Go forward',
|
||||||
description: 'Go forward to the next page',
|
description: 'Go forward to the next page',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
const tab = context.currentTabOrDie();
|
const tab = context.currentTabOrDie();
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ const requests = defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_network_requests',
|
name: 'browser_network_requests',
|
||||||
|
title: 'List network requests',
|
||||||
description: 'Returns all network requests since loading the page',
|
description: 'Returns all network requests since loading the page',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ const pdf = defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_pdf_save',
|
name: 'browser_pdf_save',
|
||||||
|
title: 'Save as PDF',
|
||||||
description: 'Save page as PDF',
|
description: 'Save page as PDF',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ const screenshot = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_capture',
|
name: 'browser_screen_capture',
|
||||||
|
title: 'Take a screenshot',
|
||||||
description: 'Take a screenshot of the current page',
|
description: 'Take a screenshot of the current page',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
@@ -59,11 +61,13 @@ const moveMouse = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_move_mouse',
|
name: 'browser_screen_move_mouse',
|
||||||
|
title: 'Move mouse',
|
||||||
description: 'Move mouse to a given position',
|
description: 'Move mouse to a given position',
|
||||||
inputSchema: elementSchema.extend({
|
inputSchema: elementSchema.extend({
|
||||||
x: z.number().describe('X coordinate'),
|
x: z.number().describe('X coordinate'),
|
||||||
y: z.number().describe('Y coordinate'),
|
y: z.number().describe('Y coordinate'),
|
||||||
}),
|
}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -86,11 +90,13 @@ const click = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_click',
|
name: 'browser_screen_click',
|
||||||
|
title: 'Click',
|
||||||
description: 'Click left mouse button',
|
description: 'Click left mouse button',
|
||||||
inputSchema: elementSchema.extend({
|
inputSchema: elementSchema.extend({
|
||||||
x: z.number().describe('X coordinate'),
|
x: z.number().describe('X coordinate'),
|
||||||
y: z.number().describe('Y coordinate'),
|
y: z.number().describe('Y coordinate'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -117,9 +123,9 @@ const click = defineTool({
|
|||||||
|
|
||||||
const drag = defineTool({
|
const drag = defineTool({
|
||||||
capability: 'core',
|
capability: 'core',
|
||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_drag',
|
name: 'browser_screen_drag',
|
||||||
|
title: 'Drag mouse',
|
||||||
description: 'Drag left mouse button',
|
description: 'Drag left mouse button',
|
||||||
inputSchema: elementSchema.extend({
|
inputSchema: elementSchema.extend({
|
||||||
startX: z.number().describe('Start X coordinate'),
|
startX: z.number().describe('Start X coordinate'),
|
||||||
@@ -127,6 +133,7 @@ const drag = defineTool({
|
|||||||
endX: z.number().describe('End X coordinate'),
|
endX: z.number().describe('End X coordinate'),
|
||||||
endY: z.number().describe('End Y coordinate'),
|
endY: z.number().describe('End Y coordinate'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -158,14 +165,15 @@ const drag = defineTool({
|
|||||||
|
|
||||||
const type = defineTool({
|
const type = defineTool({
|
||||||
capability: 'core',
|
capability: 'core',
|
||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_type',
|
name: 'browser_screen_type',
|
||||||
|
title: 'Type text',
|
||||||
description: 'Type text',
|
description: 'Type text',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
text: z.string().describe('Text to type into the element'),
|
text: z.string().describe('Text to type into the element'),
|
||||||
submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
|
submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ const snapshot = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_snapshot',
|
name: 'browser_snapshot',
|
||||||
|
title: 'Page snapshot',
|
||||||
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
|
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
@@ -50,8 +52,10 @@ const click = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_click',
|
name: 'browser_click',
|
||||||
|
title: 'Click',
|
||||||
description: 'Perform click on a web page',
|
description: 'Perform click on a web page',
|
||||||
inputSchema: elementSchema,
|
inputSchema: elementSchema,
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -76,6 +80,7 @@ const drag = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_drag',
|
name: 'browser_drag',
|
||||||
|
title: 'Drag mouse',
|
||||||
description: 'Perform drag and drop between two elements',
|
description: 'Perform drag and drop between two elements',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'),
|
startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'),
|
||||||
@@ -83,6 +88,7 @@ const drag = defineTool({
|
|||||||
endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
|
endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
|
||||||
endRef: z.string().describe('Exact target element reference from the page snapshot'),
|
endRef: z.string().describe('Exact target element reference from the page snapshot'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -108,8 +114,10 @@ const hover = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_hover',
|
name: 'browser_hover',
|
||||||
|
title: 'Hover mouse',
|
||||||
description: 'Hover over element on page',
|
description: 'Hover over element on page',
|
||||||
inputSchema: elementSchema,
|
inputSchema: elementSchema,
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -140,8 +148,10 @@ const type = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_type',
|
name: 'browser_type',
|
||||||
|
title: 'Type text',
|
||||||
description: 'Type text into editable element',
|
description: 'Type text into editable element',
|
||||||
inputSchema: typeSchema,
|
inputSchema: typeSchema,
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -184,8 +194,10 @@ const selectOption = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_select_option',
|
name: 'browser_select_option',
|
||||||
|
title: 'Select option',
|
||||||
description: 'Select an option in a dropdown',
|
description: 'Select an option in a dropdown',
|
||||||
inputSchema: selectOptionSchema,
|
inputSchema: selectOptionSchema,
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -221,8 +233,10 @@ const screenshot = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_take_screenshot',
|
name: 'browser_take_screenshot',
|
||||||
|
title: 'Take a screenshot',
|
||||||
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
||||||
inputSchema: screenshotSchema,
|
inputSchema: screenshotSchema,
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -244,7 +258,7 @@ const screenshot = defineTool({
|
|||||||
else
|
else
|
||||||
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
||||||
|
|
||||||
const includeBase64 = !context.config.tools?.browser_take_screenshot?.omitBase64;
|
const includeBase64 = !context.config.noImageResponses;
|
||||||
const action = async () => {
|
const action = async () => {
|
||||||
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ const listTabs = defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_list',
|
name: 'browser_tab_list',
|
||||||
|
title: 'List tabs',
|
||||||
description: 'List browser tabs',
|
description: 'List browser tabs',
|
||||||
inputSchema: z.object({}),
|
inputSchema: z.object({}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
@@ -47,10 +49,12 @@ const selectTab: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_select',
|
name: 'browser_tab_select',
|
||||||
|
title: 'Select a tab',
|
||||||
description: 'Select a tab by index',
|
description: 'Select a tab by index',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
index: z.number().describe('The index of the tab to select'),
|
index: z.number().describe('The index of the tab to select'),
|
||||||
}),
|
}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -72,10 +76,12 @@ const newTab: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_new',
|
name: 'browser_tab_new',
|
||||||
|
title: 'Open a new tab',
|
||||||
description: 'Open a new tab',
|
description: 'Open a new tab',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
url: z.string().optional().describe('The URL to navigate to in the new tab. If not provided, the new tab will be blank.'),
|
url: z.string().optional().describe('The URL to navigate to in the new tab. If not provided, the new tab will be blank.'),
|
||||||
}),
|
}),
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
@@ -99,10 +105,12 @@ const closeTab: ToolFactory = captureSnapshot => defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_close',
|
name: 'browser_tab_close',
|
||||||
|
title: 'Close a tab',
|
||||||
description: 'Close a tab',
|
description: 'Close a tab',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
index: z.number().optional().describe('The index of the tab to close. Closes current tab if not provided.'),
|
index: z.number().optional().describe('The index of the tab to close. Closes current tab if not provided.'),
|
||||||
}),
|
}),
|
||||||
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ const generateTest = defineTool({
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_generate_playwright_test',
|
name: 'browser_generate_playwright_test',
|
||||||
|
title: 'Generate a Playwright test',
|
||||||
description: 'Generate a Playwright test for given scenario',
|
description: 'Generate a Playwright test for given scenario',
|
||||||
inputSchema: generateTestSchema,
|
inputSchema: generateTestSchema,
|
||||||
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ import type { ToolCapability } from '../../config.js';
|
|||||||
|
|
||||||
export type ToolSchema<Input extends InputType> = {
|
export type ToolSchema<Input extends InputType> = {
|
||||||
name: string;
|
name: string;
|
||||||
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
inputSchema: Input;
|
inputSchema: Input;
|
||||||
|
type: 'readOnly' | 'destructive';
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputType = z.Schema;
|
type InputType = z.Schema;
|
||||||
|
|||||||
@@ -18,17 +18,22 @@ import http from 'node:http';
|
|||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
import { ServerList } from './server.js';
|
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
||||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
|
||||||
export async function startStdioTransport(serverList: ServerList) {
|
import { createConnection } from './connection.js';
|
||||||
const server = await serverList.create();
|
|
||||||
await server.connect(new StdioServerTransport());
|
import type { Config } from '../config.js';
|
||||||
|
import type { Connection } from './connection.js';
|
||||||
|
|
||||||
|
export async function startStdioTransport(config: Config, connectionList: Connection[]) {
|
||||||
|
const connection = await createConnection(config);
|
||||||
|
await connection.connect(new StdioServerTransport());
|
||||||
|
connectionList.push(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSSE(req: http.IncomingMessage, res: http.ServerResponse, url: URL, serverList: ServerList, sessions: Map<string, SSEServerTransport>) {
|
async function handleSSE(config: Config, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map<string, SSEServerTransport>, connectionList: Connection[]) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const sessionId = url.searchParams.get('sessionId');
|
const sessionId = url.searchParams.get('sessionId');
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
@@ -46,22 +51,24 @@ async function handleSSE(req: http.IncomingMessage, res: http.ServerResponse, ur
|
|||||||
} else if (req.method === 'GET') {
|
} else if (req.method === 'GET') {
|
||||||
const transport = new SSEServerTransport('/sse', res);
|
const transport = new SSEServerTransport('/sse', res);
|
||||||
sessions.set(transport.sessionId, transport);
|
sessions.set(transport.sessionId, transport);
|
||||||
const server = await serverList.create();
|
const connection = await createConnection(config);
|
||||||
|
await connection.connect(transport);
|
||||||
|
connectionList.push(connection);
|
||||||
res.on('close', () => {
|
res.on('close', () => {
|
||||||
sessions.delete(transport.sessionId);
|
sessions.delete(transport.sessionId);
|
||||||
serverList.close(server).catch(e => {
|
connection.close().catch(e => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return await server.connect(transport);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.statusCode = 405;
|
res.statusCode = 405;
|
||||||
res.end('Method not allowed');
|
res.end('Method not allowed');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStreamable(req: http.IncomingMessage, res: http.ServerResponse, serverList: ServerList, sessions: Map<string, StreamableHTTPServerTransport>) {
|
async function handleStreamable(config: Config, req: http.IncomingMessage, res: http.ServerResponse, sessions: Map<string, StreamableHTTPServerTransport>, connectionList: Connection[]) {
|
||||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
const transport = sessions.get(sessionId);
|
const transport = sessions.get(sessionId);
|
||||||
@@ -84,24 +91,28 @@ async function handleStreamable(req: http.IncomingMessage, res: http.ServerRespo
|
|||||||
if (transport.sessionId)
|
if (transport.sessionId)
|
||||||
sessions.delete(transport.sessionId);
|
sessions.delete(transport.sessionId);
|
||||||
};
|
};
|
||||||
const server = await serverList.create();
|
const connection = await createConnection(config);
|
||||||
await server.connect(transport);
|
connectionList.push(connection);
|
||||||
return await transport.handleRequest(req, res);
|
await Promise.all([
|
||||||
|
connection.connect(transport),
|
||||||
|
transport.handleRequest(req, res),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.statusCode = 400;
|
res.statusCode = 400;
|
||||||
res.end('Invalid request');
|
res.end('Invalid request');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startHttpTransport(port: number, hostname: string | undefined, serverList: ServerList) {
|
export function startHttpTransport(config: Config, port: number, hostname: string | undefined, connectionList: Connection[]) {
|
||||||
const sseSessions = new Map<string, SSEServerTransport>();
|
const sseSessions = new Map<string, SSEServerTransport>();
|
||||||
const streamableSessions = new Map<string, StreamableHTTPServerTransport>();
|
const streamableSessions = new Map<string, StreamableHTTPServerTransport>();
|
||||||
const httpServer = http.createServer(async (req, res) => {
|
const httpServer = http.createServer(async (req, res) => {
|
||||||
const url = new URL(`http://localhost${req.url}`);
|
const url = new URL(`http://localhost${req.url}`);
|
||||||
if (url.pathname.startsWith('/mcp'))
|
if (url.pathname.startsWith('/mcp'))
|
||||||
await handleStreamable(req, res, serverList, streamableSessions);
|
await handleStreamable(config, req, res, streamableSessions, connectionList);
|
||||||
else
|
else
|
||||||
await handleSSE(req, res, url, serverList, sseSessions);
|
await handleSSE(config, req, res, url, sseSessions, connectionList);
|
||||||
});
|
});
|
||||||
httpServer.listen(port, hostname, () => {
|
httpServer.listen(port, hostname, () => {
|
||||||
const address = httpServer.address();
|
const address = httpServer.address();
|
||||||
|
|||||||
41
tests/config.spec.ts
Normal file
41
tests/config.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
import { Config } from '../config.js';
|
||||||
|
import { test, expect } from './fixtures.js';
|
||||||
|
|
||||||
|
test('config user data dir', async ({ startClient, mcpBrowser }, testInfo) => {
|
||||||
|
const config: Config = {
|
||||||
|
browser: {
|
||||||
|
userDataDir: testInfo.outputPath('user-data-dir'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const configPath = testInfo.outputPath('config.json');
|
||||||
|
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
|
||||||
|
|
||||||
|
const client = await startClient({ args: ['--config', configPath] });
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
})).toContainTextContent(`Hello, world!`);
|
||||||
|
|
||||||
|
const files = await fs.promises.readdir(config.browser!.userDataDir!);
|
||||||
|
expect(files.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
@@ -34,7 +34,7 @@ export type TestOptions = {
|
|||||||
type TestFixtures = {
|
type TestFixtures = {
|
||||||
client: Client;
|
client: Client;
|
||||||
visionClient: Client;
|
visionClient: Client;
|
||||||
startClient: (options?: { args?: string[], config?: Config }) => Promise<Client>;
|
startClient: (options?: { clientName?: string, args?: string[], config?: Config }) => Promise<Client>;
|
||||||
wsEndpoint: string;
|
wsEndpoint: string;
|
||||||
cdpEndpoint: (port?: number) => Promise<string>;
|
cdpEndpoint: (port?: number) => Promise<string>;
|
||||||
server: TestServer;
|
server: TestServer;
|
||||||
@@ -79,7 +79,7 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
|
|||||||
command: 'node',
|
command: 'node',
|
||||||
args: [path.join(path.dirname(__filename), '../cli.js'), ...args],
|
args: [path.join(path.dirname(__filename), '../cli.js'), ...args],
|
||||||
});
|
});
|
||||||
client = new Client({ name: 'test', version: '1.0.0' });
|
client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' });
|
||||||
await client.connect(transport);
|
await client.connect(transport);
|
||||||
await client.ping();
|
await client.ping();
|
||||||
return client;
|
return client;
|
||||||
|
|||||||
91
tests/request-blocking.spec.ts
Normal file
91
tests/request-blocking.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { test, expect } from './fixtures.ts';
|
||||||
|
|
||||||
|
const BLOCK_MESSAGE = /Blocked by Web Inspector|NS_ERROR_FAILURE|net::ERR_BLOCKED_BY_CLIENT/g;
|
||||||
|
|
||||||
|
const fetchPage = async (client: Client, url: string) => {
|
||||||
|
const result = await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(result, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('default to allow all', async ({ server, client }) => {
|
||||||
|
server.route('/ppp', (_req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end('content:PPP');
|
||||||
|
});
|
||||||
|
const result = await fetchPage(client, server.PREFIX + '/ppp');
|
||||||
|
expect(result).toContain('content:PPP');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocked works', async ({ startClient }) => {
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--blocked-origins', 'microsoft.com;example.com;playwright.dev']
|
||||||
|
});
|
||||||
|
const result = await fetchPage(client, 'https://example.com/');
|
||||||
|
expect(result).toMatch(BLOCK_MESSAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allowed works', async ({ server, startClient }) => {
|
||||||
|
server.route('/ppp', (_req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end('content:PPP');
|
||||||
|
});
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--allowed-origins', `microsoft.com;${new URL(server.PREFIX).host};playwright.dev`]
|
||||||
|
});
|
||||||
|
const result = await fetchPage(client, server.PREFIX + '/ppp');
|
||||||
|
expect(result).toContain('content:PPP');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocked takes precedence', async ({ startClient }) => {
|
||||||
|
const client = await startClient({
|
||||||
|
args: [
|
||||||
|
'--blocked-origins', 'example.com',
|
||||||
|
'--allowed-origins', 'example.com',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const result = await fetchPage(client, 'https://example.com/');
|
||||||
|
expect(result).toMatch(BLOCK_MESSAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allowed without blocked blocks all non-explicitly specified origins', async ({ startClient }) => {
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--allowed-origins', 'playwright.dev'],
|
||||||
|
});
|
||||||
|
const result = await fetchPage(client, 'https://example.com/');
|
||||||
|
expect(result).toMatch(BLOCK_MESSAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocked without allowed allows non-explicitly specified origins', async ({ server, startClient }) => {
|
||||||
|
server.route('/ppp', (_req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end('content:PPP');
|
||||||
|
});
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--blocked-origins', 'example.com'],
|
||||||
|
});
|
||||||
|
const result = await fetchPage(client, server.PREFIX + '/ppp');
|
||||||
|
expect(result).toContain('content:PPP');
|
||||||
|
});
|
||||||
@@ -73,6 +73,28 @@ test('browser_take_screenshot (element)', async ({ client }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('--output-dir should work', async ({ startClient }, testInfo) => {
|
||||||
|
const outputDir = testInfo.outputPath('output');
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--output-dir', outputDir],
|
||||||
|
});
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
})).toContainTextContent(`Navigate to data:text/html`);
|
||||||
|
|
||||||
|
await client.callTool({
|
||||||
|
name: 'browser_take_screenshot',
|
||||||
|
arguments: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fs.existsSync(outputDir)).toBeTruthy();
|
||||||
|
expect([...fs.readdirSync(outputDir)]).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('browser_take_screenshot (outputDir)', async ({ startClient }, testInfo) => {
|
test('browser_take_screenshot (outputDir)', async ({ startClient }, testInfo) => {
|
||||||
const outputDir = testInfo.outputPath('output');
|
const outputDir = testInfo.outputPath('output');
|
||||||
const client = await startClient({
|
const client = await startClient({
|
||||||
@@ -94,14 +116,10 @@ test('browser_take_screenshot (outputDir)', async ({ startClient }, testInfo) =>
|
|||||||
expect([...fs.readdirSync(outputDir)]).toHaveLength(1);
|
expect([...fs.readdirSync(outputDir)]).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('browser_take_screenshot (omitBase64)', async ({ startClient }) => {
|
test('browser_take_screenshot (noImageResponses)', async ({ startClient }) => {
|
||||||
const client = await startClient({
|
const client = await startClient({
|
||||||
config: {
|
config: {
|
||||||
tools: {
|
noImageResponses: true,
|
||||||
browser_take_screenshot: {
|
|
||||||
omitBase64: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,3 +147,31 @@ test('browser_take_screenshot (omitBase64)', async ({ startClient }) => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('browser_take_screenshot (cursor)', async ({ startClient }) => {
|
||||||
|
const client = await startClient({ clientName: 'cursor:vscode' });
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
})).toContainTextContent(`Navigate to data:text/html`);
|
||||||
|
|
||||||
|
await client.callTool({
|
||||||
|
name: 'browser_take_screenshot',
|
||||||
|
arguments: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_take_screenshot',
|
||||||
|
arguments: {},
|
||||||
|
})).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: expect.stringContaining(`Screenshot viewport and save it as`),
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import filesTools from '../lib/tools/files.js';
|
|||||||
import installTools from '../lib/tools/install.js';
|
import installTools from '../lib/tools/install.js';
|
||||||
import keyboardTools from '../lib/tools/keyboard.js';
|
import keyboardTools from '../lib/tools/keyboard.js';
|
||||||
import navigateTools from '../lib/tools/navigate.js';
|
import navigateTools from '../lib/tools/navigate.js';
|
||||||
|
import networkTools from '../lib/tools/network.js';
|
||||||
import pdfTools from '../lib/tools/pdf.js';
|
import pdfTools from '../lib/tools/pdf.js';
|
||||||
import snapshotTools from '../lib/tools/snapshot.js';
|
import snapshotTools from '../lib/tools/snapshot.js';
|
||||||
import tabsTools from '../lib/tools/tabs.js';
|
import tabsTools from '../lib/tools/tabs.js';
|
||||||
@@ -62,6 +63,7 @@ const categories = {
|
|||||||
...commonTools(true),
|
...commonTools(true),
|
||||||
...installTools,
|
...installTools,
|
||||||
...dialogsTools(true),
|
...dialogsTools(true),
|
||||||
|
...networkTools,
|
||||||
],
|
],
|
||||||
'Testing': [
|
'Testing': [
|
||||||
...testTools,
|
...testTools,
|
||||||
@@ -75,74 +77,37 @@ const kStartMarker = `<!--- Generated by ${path.basename(__filename)} -->`;
|
|||||||
const kEndMarker = `<!--- End of generated section -->`;
|
const kEndMarker = `<!--- End of generated section -->`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ParsedToolSchema} tool
|
* @param {import('../src/tools/tool.js').ToolSchema<any>} tool
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatToolForReadme(tool) {
|
function formatToolForReadme(tool) {
|
||||||
const lines = /** @type {string[]} */ ([]);
|
const lines = /** @type {string[]} */ ([]);
|
||||||
lines.push(`<!-- NOTE: This has been generated via ${path.basename(__filename)} -->\n\n`);
|
lines.push(`<!-- NOTE: This has been generated via ${path.basename(__filename)} -->\n\n`);
|
||||||
lines.push(`- **${tool.name}**\n`);
|
lines.push(`- **${tool.name}**\n`);
|
||||||
|
lines.push(` - Title: ${tool.title}\n`);
|
||||||
lines.push(` - Description: ${tool.description}\n`);
|
lines.push(` - Description: ${tool.description}\n`);
|
||||||
|
|
||||||
if (tool.parameters && tool.parameters.length > 0) {
|
const inputSchema = /** @type {any} */ (zodToJsonSchema(tool.inputSchema || {}));
|
||||||
|
const requiredParams = inputSchema.required || [];
|
||||||
|
if (inputSchema.properties && Object.keys(inputSchema.properties).length) {
|
||||||
lines.push(` - Parameters:\n`);
|
lines.push(` - Parameters:\n`);
|
||||||
tool.parameters.forEach(param => {
|
Object.entries(inputSchema.properties).forEach(([name, param]) => {
|
||||||
|
const optional = !requiredParams.includes(name);
|
||||||
const meta = /** @type {string[]} */ ([]);
|
const meta = /** @type {string[]} */ ([]);
|
||||||
if (param.type)
|
if (param.type)
|
||||||
meta.push(param.type);
|
meta.push(param.type);
|
||||||
if (param.optional)
|
if (optional)
|
||||||
meta.push('optional');
|
meta.push('optional');
|
||||||
lines.push(` - \`${param.name}\` ${meta.length ? `(${meta.join(', ')})` : ''}: ${param.description}\n`);
|
lines.push(` - \`${name}\` ${meta.length ? `(${meta.join(', ')})` : ''}: ${param.description}\n`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
lines.push(` - Parameters: None\n`);
|
lines.push(` - Parameters: None\n`);
|
||||||
}
|
}
|
||||||
|
lines.push(` - Read-only: **${tool.type === 'readOnly'}**\n`);
|
||||||
lines.push('\n');
|
lines.push('\n');
|
||||||
return lines.join('');
|
return lines.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* name: any;
|
|
||||||
* description: any;
|
|
||||||
* parameters: {
|
|
||||||
* name: string;
|
|
||||||
* description: string;
|
|
||||||
* optional: boolean;
|
|
||||||
* type: string;
|
|
||||||
* }[];
|
|
||||||
*}} ParsedToolSchema
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('../src/tools/tool').ToolSchema<any>} schema
|
|
||||||
* @returns {ParsedToolSchema}
|
|
||||||
*/
|
|
||||||
function processToolSchema(schema) {
|
|
||||||
const inputSchema = /** @type {import('zod-to-json-schema').JsonSchema7ObjectType} */ (zodToJsonSchema(schema.inputSchema || {}));
|
|
||||||
if (inputSchema.type !== 'object')
|
|
||||||
throw new Error(`Tool ${schema.name} input schema is not an object`);
|
|
||||||
|
|
||||||
// In JSON Schema, properties are considered optional unless listed in the required array
|
|
||||||
const requiredParams = inputSchema?.required || [];
|
|
||||||
|
|
||||||
const parameters = Object.entries(inputSchema.properties).map(([name, prop]) => {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
description: prop.description || '',
|
|
||||||
optional: !requiredParams.includes(name),
|
|
||||||
type: /** @type {any} */ (prop).type,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: schema.name,
|
|
||||||
description: schema.description,
|
|
||||||
parameters
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateReadme() {
|
async function updateReadme() {
|
||||||
console.log('Loading tool information from compiled modules...');
|
console.log('Loading tool information from compiled modules...');
|
||||||
|
|
||||||
@@ -154,10 +119,8 @@ async function updateReadme() {
|
|||||||
|
|
||||||
for (const [category, categoryTools] of Object.entries(categories)) {
|
for (const [category, categoryTools] of Object.entries(categories)) {
|
||||||
generatedLines.push(`### ${category}\n\n`);
|
generatedLines.push(`### ${category}\n\n`);
|
||||||
for (const tool of categoryTools) {
|
for (const tool of categoryTools)
|
||||||
const scheme = processToolSchema(tool.schema);
|
generatedLines.push(formatToolForReadme(tool.schema));
|
||||||
generatedLines.push(formatToolForReadme(scheme));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const readmePath = path.join(path.dirname(__filename), '..', 'README.md');
|
const readmePath = path.join(path.dirname(__filename), '..', 'README.md');
|
||||||
|
|||||||
Reference in New Issue
Block a user