Skip to content

Commit 1ae98d4

Browse files
committed
feat: add experimental tools provider
1 parent 04ad6ec commit 1ae98d4

File tree

6 files changed

+159
-3
lines changed

6 files changed

+159
-3
lines changed

cloudflare/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77

88
# Include playwright core and test entry points
99
!index.d.ts
10+
!experimental.d.ts

cloudflare/experimental.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Browser, BrowserContext, BrowserEndpoint } from '@cloudflare/playwright';
2+
import { ToolSet } from 'ai';
3+
import { ToolCapability } from './index.js';
4+
5+
/**
6+
* ToolsProvider interface for providing Playwright MCP tools.
7+
*/
8+
export interface ToolsProvider {
9+
/**
10+
* Returns a ToolSet containing all available tools mapped from the context
11+
* Each tool includes parameters, description, and execute function
12+
*/
13+
tools(): ToolSet;
14+
15+
/**
16+
* Closes the underlying browser context and cleans up resources
17+
*/
18+
close(): Promise<void>;
19+
20+
[Symbol.asyncDispose](): Promise<void>;
21+
}
22+
23+
export declare function createToolsProvider(endpoint: BrowserEndpoint | Browser | BrowserContext, options?: { capabilities?: ToolCapability[] }): Promise<ToolsProvider>;

cloudflare/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,25 @@
2020
".": {
2121
"types": "./index.d.ts",
2222
"default": "./lib/index.js"
23+
},
24+
"./experimental": {
25+
"types": "./experimental.d.ts",
26+
"default": "./lib/experimental.js"
2327
}
2428
},
2529
"dependencies": {
2630
"@cloudflare/playwright": "https://pkg.pr.new/cloudflare/playwright/@cloudflare/playwright@71",
2731
"@modelcontextprotocol/sdk": "^1.16.0",
28-
"agents": "^0.0.109",
29-
"i": "^0.3.7",
30-
"npm": "^11.6.0",
32+
"agents": "^0.0.113",
3133
"yaml": "^2.8.0",
3234
"zod-to-json-schema": "^3.24.6"
3335
},
3436
"devDependencies": {
3537
"@cloudflare/workers-types": "^4.20250725.0",
3638
"pkg-pr-new": "^0.0.59",
3739
"vite": "^7.0.6"
40+
},
41+
"peerDependencies": {
42+
"ai": "^4.3.19"
3843
}
3944
}

cloudflare/src/experimental.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Browser, BrowserContext, BrowserEndpoint, endpointURLString } from '@cloudflare/playwright';
2+
import type { ToolCapability } from '../../config';
3+
import { Context } from '../../src/context.js';
4+
import { FullConfig, resolveConfig } from '../../src/config.js';
5+
import type { BrowserContextFactory, ClientInfo } from '../../src/browserContextFactory.js';
6+
import type { Tool, ToolSet } from 'ai';
7+
import { ToolsProvider } from '../experimental.js';
8+
import { filteredTools } from '../../src/tools';
9+
import { Response } from '../../src/response';
10+
import { BrapiContextFactory } from '.';
11+
12+
class ToolsProviderImpl implements ToolsProvider {
13+
private _context: Context;
14+
private _tools: ToolSet;
15+
16+
constructor(context: Context) {
17+
this._context = context;
18+
}
19+
20+
tools(): ToolSet {
21+
if (!this._tools) {
22+
this._tools = Object.fromEntries(
23+
this._context.tools.map(tool => [
24+
tool.schema.name,
25+
{
26+
parameters: tool.schema.inputSchema,
27+
description: tool.schema.description,
28+
execute: async (rawArguments: any) => {
29+
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
30+
const context = this._context;
31+
const response = new Response(context, tool.schema.name, parsedArguments);
32+
context.setRunningTool(tool.schema.name);
33+
try {
34+
await tool.handle(context, parsedArguments, response);
35+
await response.finish();
36+
context.sessionLog?.logResponse(response);
37+
} catch (error: any) {
38+
response.addError(String(error));
39+
} finally {
40+
context.setRunningTool(undefined);
41+
}
42+
return response.serialize();
43+
},
44+
} satisfies Tool,
45+
])
46+
);
47+
}
48+
return this._tools;
49+
}
50+
51+
async close() {
52+
await this._context.dispose();
53+
}
54+
55+
async [Symbol.asyncDispose]() {
56+
await this.close();
57+
}
58+
}
59+
60+
function isBrowser(browser: any): browser is Browser {
61+
return (
62+
typeof browser.newPage === 'function' &&
63+
typeof browser.newContext === 'function' &&
64+
// Fetcher may match the previous ones
65+
typeof browser[Symbol.asyncDispose] === 'function'
66+
);
67+
}
68+
69+
function isBrowserContext(browserContext: any): browserContext is BrowserContext {
70+
return (
71+
typeof browserContext.newPage === 'function' &&
72+
typeof browserContext.newContext === 'undefined' &&
73+
// Fetcher may match the previous ones
74+
typeof browserContext[Symbol.asyncDispose] === 'function'
75+
);
76+
}
77+
78+
export async function createToolsProvider(endpoint: BrowserEndpoint | Browser | BrowserContext, options?: { capabilities?: ToolCapability[], clientInfo: ClientInfo }): Promise<ToolsProvider> {
79+
let config: FullConfig;
80+
let browserContextFactory: BrowserContextFactory;
81+
82+
if (isBrowser(endpoint)) {
83+
config = await resolveConfig({});
84+
browserContextFactory = {
85+
createContext: async () => {
86+
const browserContext = await endpoint.newContext();
87+
return { browserContext, close: () => browserContext.close() };
88+
},
89+
} as unknown as BrowserContextFactory;
90+
} else if (isBrowserContext(endpoint)) {
91+
config = await resolveConfig({});
92+
const browserContext = endpoint;
93+
browserContextFactory = {
94+
createContext: async () => {
95+
return { browserContext, close: () => {} };
96+
},
97+
} as unknown as BrowserContextFactory;
98+
} else {
99+
const cdpEndpoint = typeof endpoint === 'string'
100+
? endpoint
101+
: endpoint instanceof URL
102+
? endpoint.toString()
103+
: endpointURLString(endpoint);
104+
config = await resolveConfig({
105+
browser: {
106+
cdpEndpoint,
107+
},
108+
});
109+
browserContextFactory = new BrapiContextFactory(config);
110+
}
111+
112+
const tools = filteredTools(config);
113+
const clientInfo = options?.clientInfo ?? { name: 'unknown', version: 'unknown' };
114+
115+
const context = new Context({
116+
tools,
117+
config,
118+
browserContextFactory,
119+
clientInfo,
120+
sessionLog: undefined,
121+
});
122+
return new ToolsProviderImpl(context);
123+
}

cloudflare/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default defineConfig({
5050
name: '@cloudflare/playwright',
5151
entry: [
5252
path.resolve(__dirname, './src/index.ts'),
53+
path.resolve(__dirname, './src/experimental.ts'),
5354
],
5455
},
5556
// prevents __defProp, __defNormalProp, __publicField in compiled code

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,7 @@ export default [
238238
'notice/notice': 'off',
239239
},
240240
},
241+
{
242+
ignores: ["cloudflare/example/**/*"],
243+
},
241244
];

0 commit comments

Comments
 (0)