Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
- run: "./etc/scripts/workflows/ci.ts"

- name: Upload target directory
if: always()
uses: actions/upload-artifact@v6
with:
name: target-directory
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
- run: "./etc/scripts/workflows/ci.ts"

- name: Upload target directory
if: always()
uses: actions/upload-artifact@v6
with:
name: target-directory
Expand Down
5 changes: 4 additions & 1 deletion .moon/tasks/tag-playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ fileGroups:
- playwright.config.ts
playwright-browsers:
- $workspaceRoot/.tmp/playwright/browsers/**/*

playwright-workspace-sources:
- $workspaceRoot/**/src/**/*
tasks:
playwright-test:
toolchain: node
env:
PLAYWRIGHT_BROWSERS_PATH: $workspaceRoot/.tmp/playwright/browsers
NODE_OPTIONS: "--require @swc-node/register"
script: >-
playwright test
inputs:
- "@group(playwright-tests)"
- "@group(playwright-configs)"
- "@group(playwright-workspace-sources)"
playwright-show-report:
toolchain: node
preset: server
Expand Down
21 changes: 21 additions & 0 deletions apps/ext-e2e/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": true,
"dynamicImport": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2022",
"keepClassNames": true
},
"module": {
"type": "es6"
}
}


8 changes: 8 additions & 0 deletions apps/ext-e2e/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ tags:
dependsOn:
- core-utils
- core-server
tasks:
test:
options:
runDepsInParallel: false
deps:
- m2:build
- m3:build
- ~:playwright-test
10 changes: 8 additions & 2 deletions apps/ext-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { defineConfig, devices } from "@playwright/test";
import type { ExtContextOptions } from "./fixtures/ext-context";
import type { ExtContextOptions } from "./src/fixtures/ext-context";

process.env.NODE_OPTIONS = "--require @swc-node/register";

export default defineConfig<ExtContextOptions>({
testDir: "./tests",
testDir: "./src/tests",
outputDir: "target/playwright/test-results",
fullyParallel: true,
forbidOnly: !!process.env.CI,
Expand All @@ -14,6 +16,10 @@ export default defineConfig<ExtContextOptions>({
video: "on",
},

build: {
external: [],
},

projects: [
{
name: "m3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from 'path';
export type ExtTarget = 'm2' | 'm3';

function getExtensionPath(target: ExtTarget): string {
const basePath = path.join(__dirname, '..', '..', target, 'target', 'extension', 'tmp', 'extension');
const basePath = path.join(__dirname, '..', '..', '..', target, 'target', 'extension', 'tmp', 'extension');
return basePath;
}

Expand All @@ -22,7 +22,7 @@ export const extContextTest = base.extend<ExtContextFixtures, ExtContextOptions>

context: async ({ extTarget, launchOptions }, use) => {
const pathToExtension = getExtensionPath(extTarget);

const context = await chromium.launchPersistentContext('', {
...launchOptions,
channel: 'chromium',
Expand All @@ -32,11 +32,11 @@ export const extContextTest = base.extend<ExtContextFixtures, ExtContextOptions>
`--load-extension=${pathToExtension}`,
],
});

await use(context);
await context.close();
},

extensionId: async ({ context, extTarget }, use) => {
if (extTarget === 'm3') {
let [serviceWorker] = context.serviceWorkers();
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { BasePage } from "./base-page";
export { McpClientPageObject } from "./mcp-client-page-object";
export { PlaywrightPage } from "./playwright-page";

Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import {
ListToolsResultSchema,
CallToolResultSchema,
ListResourcesResultSchema,
type CallToolResult,
} from "@modelcontextprotocol/sdk/types.js";
import type {
ServerToolName,
ServerToolArgs,
ServerToolResult,
} from "@mcp-browser-kit/core-server";

export type TypedCallToolResult<T extends ServerToolName> = Omit<
CallToolResult,
"structuredContent"
> & {
structuredContent?: ServerToolResult<T>;
};
import {
createCoreServerContainer,
LoggerFactoryOutputPort,
Expand Down Expand Up @@ -97,7 +105,7 @@ export class McpClientPageObject {
: [
args: ServerToolArgs<T>,
]
): Promise<ServerToolResult<T>> {
): Promise<TypedCallToolResult<T>> {
const res = await this.client.request(
{
method: "tools/call",
Expand All @@ -108,7 +116,7 @@ export class McpClientPageObject {
},
CallToolResultSchema,
);
return res.content as ServerToolResult<T>;
return res as TypedCallToolResult<T>;
}

async listResources() {
Expand All @@ -121,4 +129,12 @@ export class McpClientPageObject {
);
return res.resources;
}

async waitForBrowsers(timeout = 20000) {
const { expect } = await import("@playwright/test");
await expect(async () => {
const contextOutput = await this.callTool("getContext", {});
expect(contextOutput.structuredContent?.browsers?.length).toBeGreaterThan(0);
}).toPass({ timeout, intervals: [2000] });
}
}
8 changes: 8 additions & 0 deletions apps/ext-e2e/src/tests/mcp-connection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test } from "../fixtures/ext-test";

test("establishes MCP server connection and initializes browsers", async ({ playwrightPage, mcpClientPage }) => {
await playwrightPage.navigate();
await mcpClientPage.startServer();
await mcpClientPage.connect();
await mcpClientPage.waitForBrowsers();
});
12 changes: 0 additions & 12 deletions apps/ext-e2e/tests/example.spec.ts

This file was deleted.

3 changes: 1 addition & 2 deletions apps/ext-e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@
"outDir": "../../.moon/cache/types/apps/ext-e2e"
},
"include": [
"src/**/*",
"pages/**/*"
"src/**/*"
],
"references": [
{
Expand Down
10 changes: 6 additions & 4 deletions etc/scripts/actions/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ if (fs.existsSync(".git/shallow")) {
await $`rm -rf .git/shallow`;
}

await pipeOutput($`moon run scripts:playwright-install`);
await pipeOutput($`moon ci`);

await collectWorkspaceTargets();
try {
await pipeOutput($`moon run scripts:playwright-install`);
await pipeOutput($`moon ci`);
} finally {
await collectWorkspaceTargets();
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"@mozilla/readability": "^0.6.0",
"@playwright/test": "^1.57.0",
"@readme/data-urls": "^3.0.0",
"@swc-node/register": "^1.11.1",
"@swc/core": "^1.15.8",
"@trpc/client": "^11.7.2",
"@trpc/server": "^11.7.2",
"@types/data-urls": "^3.0.4",
Expand Down
57 changes: 25 additions & 32 deletions packages/server-driving-mcp-server/src/services/browser-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import {
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { inject, injectable } from "inversify";
import { over } from "ok-value-error-reason";
import { registerTool } from "../utils/register-tool";
import {
createErrorResponse,
createImageResponse,
createStructuredResponse,
createTextResponse,
} from "../utils/tool-helpers";
import {
browserContextOutputSchema,
contextOutputSchema,
invokeJsFnOutputSchema,
invokeJsFnSchema,
openTabOutputSchema,
Expand All @@ -43,30 +44,31 @@ export class BrowserTools {
}

register(server: McpServer): void {
this.registerGetBasicBrowserContext(server);
this.registerGetContext(server);
this.registerCaptureTab(server);
this.registerInvokeJsFn(server);
this.registerOpenTab(server);
this.registerCloseTab(server);
this.registerGetSelection(server);
}

private registerGetBasicBrowserContext(server: McpServer): void {
this.logger.verbose("Registering tool: getBasicBrowserContext");
server.registerTool(
"getBasicBrowserContext",
private registerGetContext(server: McpServer): void {
this.logger.verbose("Registering tool: getContext");
registerTool(
server,
"getContext",
{
description:
this.toolDescriptionsInputPort.getBasicBrowserContextInstruction(),
inputSchema: {},
outputSchema: browserContextOutputSchema,
outputSchema: contextOutputSchema,
},
async () => {
this.logger.verbose("Executing getBasicBrowserContext");
this.logger.verbose("Executing getContext");
const overCtx = await over(this.toolsInputPort.getContext);

if (!overCtx.ok) {
this.logger.error("Failed to get basic browser context", {
this.logger.error("Failed to get context", {
reason: overCtx.reason,
});
return createErrorResponse(
Expand All @@ -76,29 +78,18 @@ export class BrowserTools {
}

const ctx = overCtx.value;
const tabs = ctx.browsers.flatMap((browser) =>
browser.browserWindows.flatMap((window) =>
window.tabs.map((tab) => ({
tabKey: tab.tabKey,
windowKey: window.windowKey,
url: tab.url,
title: tab.title,
})),
),
);
this.logger.verbose("Retrieved browser context", {
tabs,
});
return createStructuredResponse(browserContextOutputSchema, {
tabs,
browserCount: ctx.browsers.length,
});
return createStructuredResponse(contextOutputSchema, ctx);
},
);
}

private registerCaptureTab(server: McpServer): void {
this.logger.verbose("Registering tool: captureTab");
server.registerTool(
registerTool(
server,
"captureTab",
{
description: this.toolDescriptionsInputPort.captureTabInstruction(),
Expand Down Expand Up @@ -139,7 +130,8 @@ export class BrowserTools {

private registerInvokeJsFn(server: McpServer): void {
this.logger.verbose("Registering tool: invokeJsFn");
server.registerTool(
registerTool(
server,
"invokeJsFn",
{
description: this.toolDescriptionsInputPort.invokeJsFnInstruction(),
Expand Down Expand Up @@ -179,7 +171,8 @@ export class BrowserTools {

private registerOpenTab(server: McpServer): void {
this.logger.verbose("Registering tool: openTab");
server.registerTool(
registerTool(
server,
"openTab",
{
description: this.toolDescriptionsInputPort.openTabInstruction(),
Expand Down Expand Up @@ -226,7 +219,8 @@ export class BrowserTools {

private registerCloseTab(server: McpServer): void {
this.logger.verbose("Registering tool: closeTab");
server.registerTool(
registerTool(
server,
"closeTab",
{
description: this.toolDescriptionsInputPort.closeTabInstruction(),
Expand Down Expand Up @@ -261,7 +255,8 @@ export class BrowserTools {

private registerGetSelection(server: McpServer): void {
this.logger.verbose("Registering tool: getSelection");
server.registerTool(
registerTool(
server,
"getSelection",
{
description: this.toolDescriptionsInputPort.getSelectionInstruction(),
Expand Down Expand Up @@ -290,11 +285,9 @@ export class BrowserTools {
const selection = overResult.value;
this.logger.verbose("Selection retrieved successfully", {
tabKey,
hasSelection: !!selection?.selectedText,
});
return createStructuredResponse(selectionOutputSchema, {
selection: selection?.selectedText ?? null,
hasSelection: !!selection.selectedText,
});
return createStructuredResponse(selectionOutputSchema, selection);
},
);
}
Expand Down
Loading