-
Notifications
You must be signed in to change notification settings - Fork 8
Add browser pool parity fields to MCP #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
bd034e4
Add browser session parity options
IlyaasK a689dcc
Clean up browser tool action validation
IlyaasK bbadbaf
Derive browser field scopes from map
IlyaasK 6498c24
Guard empty browser update response
IlyaasK 9b8716e
Add browser utility MCP tool
IlyaasK 2c91a8a
Split browser utility MCP tools
IlyaasK a2e930b
Simplify browser curl params
IlyaasK 4e867c8
Add browser pool parity fields
IlyaasK 8a9eaad
Share MCP browser config and response helpers
IlyaasK 91c1f00
Tighten browser pool start URL schema
IlyaasK ea927af
Clean up MCP resource handlers
IlyaasK 680ef3b
Allow partial browser pool updates
IlyaasK 5d4a8e6
Align browser tools with MCP responses
IlyaasK 43487a0
Use SDK 0.60 browser pool update types
IlyaasK dcf3732
Address browser pool Cursor findings
IlyaasK 1331dab
Simplify browser MCP review fixes
IlyaasK db71a9b
Improve MCP agent ergonomics
IlyaasK ca5d455
Fix browser pool review findings
IlyaasK dd77f9f
Resolve main merge conflicts
IlyaasK 8dd29c8
Fix PR 112 review follow-ups
IlyaasK f438766
Keep paginated MCP responses structured
IlyaasK b3b3c74
Address PR 112 review nits
IlyaasK bb8ad61
Make pool fill_rate_per_minute integer-only
IlyaasK 33b5e58
Allow fractional pool fill_rate_per_minute
IlyaasK 87a580c
Use stable pool id in acquire release hint
IlyaasK 6f3e4a7
Omit empty pool proxy_id
IlyaasK 3aa6fc7
Merge branch 'main' into browser-pools-parity-mcp-tool
IlyaasK File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| import type { KernelClient } from "@/lib/mcp/kernel-client"; | ||
|
|
||
| type BrowserCreateParams = NonNullable< | ||
| Parameters<KernelClient["browsers"]["create"]>[0] | ||
| >; | ||
| type BrowserUpdateParams = Parameters<KernelClient["browsers"]["update"]>[1]; | ||
| type BrowserPoolCreateParams = Parameters< | ||
| KernelClient["browserPools"]["create"] | ||
| >[0]; | ||
| type BrowserPoolUpdateParams = Parameters< | ||
| KernelClient["browserPools"]["update"] | ||
| >[1]; | ||
|
|
||
| export type BrowserProfileParams = { | ||
| profile_name?: string; | ||
| profile_id?: string; | ||
| save_profile_changes?: boolean; | ||
| }; | ||
|
|
||
| export type BrowserExtensionParams = { | ||
| extension_id?: string; | ||
| extension_name?: string; | ||
| }; | ||
|
|
||
| export type BrowserViewportParams = { | ||
| viewport_width?: number; | ||
| viewport_height?: number; | ||
| viewport_refresh_rate?: number; | ||
| }; | ||
|
|
||
| export type BrowserViewportUpdateParams = BrowserViewportParams & { | ||
| viewport_force?: boolean; | ||
| }; | ||
|
|
||
| export type BrowserCreateConfigParams = BrowserProfileParams & | ||
| BrowserExtensionParams & | ||
| BrowserViewportParams & { | ||
| start_url?: string; | ||
| }; | ||
|
|
||
| export type BrowserUpdateConfigParams = BrowserProfileParams & | ||
| BrowserViewportUpdateParams; | ||
|
|
||
| type BrowserProfileConfig = NonNullable< | ||
| | BrowserCreateParams["profile"] | ||
| | BrowserUpdateParams["profile"] | ||
| | BrowserPoolCreateParams["profile"] | ||
| | BrowserPoolUpdateParams["profile"] | ||
| >; | ||
|
|
||
| type BrowserExtensionConfig = NonNullable< | ||
| | BrowserCreateParams["extensions"] | ||
| | BrowserPoolCreateParams["extensions"] | ||
| | BrowserPoolUpdateParams["extensions"] | ||
| >; | ||
|
|
||
| type BrowserViewportConfig = NonNullable< | ||
| | BrowserCreateParams["viewport"] | ||
| | BrowserPoolCreateParams["viewport"] | ||
| | BrowserPoolUpdateParams["viewport"] | ||
| >; | ||
|
|
||
| type BrowserViewportUpdateConfig = NonNullable<BrowserUpdateParams["viewport"]>; | ||
|
|
||
| export type BrowserCreateConfig = Pick< | ||
| BrowserCreateParams, | ||
| "profile" | "extensions" | "viewport" | "start_url" | ||
| >; | ||
|
|
||
| export type BrowserUpdateConfig = Pick< | ||
| BrowserUpdateParams, | ||
| "profile" | "viewport" | ||
| >; | ||
|
|
||
| export type BrowserConfigResult<T> = | ||
| | { ok: true; value: T } | ||
| | { ok: false; error: string }; | ||
|
|
||
| function configValue<T>(value: T): BrowserConfigResult<T> { | ||
| return { ok: true, value }; | ||
| } | ||
|
|
||
| function configError<T>(message: string): BrowserConfigResult<T> { | ||
| return { ok: false, error: `Error: ${message}` }; | ||
| } | ||
|
|
||
| function buildBrowserStartUrl( | ||
| startUrl: string | undefined, | ||
| ): BrowserConfigResult<string | undefined> { | ||
| if (startUrl === undefined) return configValue(undefined); | ||
|
|
||
| try { | ||
| new URL(startUrl); | ||
| } catch { | ||
| return configError("start_url must be a valid URL."); | ||
| } | ||
|
|
||
| return configValue(startUrl); | ||
| } | ||
|
|
||
| function buildBrowserProfile( | ||
| params: BrowserProfileParams, | ||
| ): BrowserConfigResult<BrowserProfileConfig | undefined> { | ||
| if (params.profile_name && params.profile_id) { | ||
| return configError("Cannot specify both profile_name and profile_id."); | ||
| } | ||
| if ( | ||
| params.save_profile_changes !== undefined && | ||
| !params.profile_name && | ||
| !params.profile_id | ||
| ) { | ||
| return configError( | ||
| "profile_name or profile_id is required when save_profile_changes is set.", | ||
| ); | ||
| } | ||
| if (!params.profile_name && !params.profile_id) return configValue(undefined); | ||
| return configValue({ | ||
| ...(params.profile_name && { name: params.profile_name }), | ||
| ...(params.profile_id && { id: params.profile_id }), | ||
| ...(params.save_profile_changes !== undefined && { | ||
| save_changes: params.save_profile_changes, | ||
| }), | ||
| }); | ||
| } | ||
|
|
||
| function buildBrowserExtensions( | ||
| params: BrowserExtensionParams, | ||
| ): BrowserConfigResult<BrowserExtensionConfig | undefined> { | ||
| if (params.extension_id && params.extension_name) { | ||
| return configError("Cannot specify both extension_id and extension_name."); | ||
| } | ||
| if (!params.extension_id && !params.extension_name) | ||
| return configValue(undefined); | ||
| return configValue([ | ||
| { | ||
| ...(params.extension_id && { id: params.extension_id }), | ||
| ...(params.extension_name && { name: params.extension_name }), | ||
| }, | ||
| ]); | ||
| } | ||
|
|
||
| function buildBrowserViewport( | ||
| params: BrowserViewportParams, | ||
| ): BrowserConfigResult<BrowserViewportConfig | undefined> { | ||
| const width = params.viewport_width; | ||
| const height = params.viewport_height; | ||
| const hasViewportOptions = | ||
| width !== undefined || | ||
| height !== undefined || | ||
| params.viewport_refresh_rate !== undefined; | ||
|
|
||
| if (!hasViewportOptions) return configValue(undefined); | ||
| if (width === undefined || height === undefined) { | ||
| return configError( | ||
| "viewport_width and viewport_height must be provided together.", | ||
| ); | ||
| } | ||
|
|
||
| return configValue({ | ||
| width, | ||
| height, | ||
| ...(params.viewport_refresh_rate !== undefined && { | ||
| refresh_rate: params.viewport_refresh_rate, | ||
| }), | ||
| }); | ||
| } | ||
|
|
||
| function buildBrowserViewportUpdate( | ||
| params: BrowserViewportUpdateParams, | ||
| ): BrowserConfigResult<BrowserViewportUpdateConfig | undefined> { | ||
| const viewport = buildBrowserViewport(params); | ||
| if (!viewport.ok) return viewport; | ||
|
|
||
| if (!viewport.value) { | ||
| if (params.viewport_force !== undefined) { | ||
| return configError( | ||
| "viewport_width and viewport_height must be provided when viewport_force is set.", | ||
| ); | ||
| } | ||
| return configValue(undefined); | ||
| } | ||
|
|
||
| return configValue({ | ||
| ...viewport.value, | ||
| ...(params.viewport_force !== undefined && { | ||
| force: params.viewport_force, | ||
| }), | ||
| }); | ||
| } | ||
|
|
||
| export function buildBrowserCreateConfig( | ||
| params: BrowserCreateConfigParams, | ||
| ): BrowserConfigResult<BrowserCreateConfig> { | ||
| const profile = buildBrowserProfile(params); | ||
| if (!profile.ok) return profile; | ||
|
|
||
| const extensions = buildBrowserExtensions(params); | ||
| if (!extensions.ok) return extensions; | ||
|
|
||
| const viewport = buildBrowserViewport(params); | ||
| if (!viewport.ok) return viewport; | ||
|
|
||
| const startUrl = buildBrowserStartUrl(params.start_url); | ||
| if (!startUrl.ok) return startUrl; | ||
|
|
||
| return configValue({ | ||
| ...(profile.value && { profile: profile.value }), | ||
| ...(extensions.value && { extensions: extensions.value }), | ||
| ...(viewport.value && { viewport: viewport.value }), | ||
| ...(startUrl.value !== undefined && { start_url: startUrl.value }), | ||
| }); | ||
| } | ||
|
|
||
| export function buildBrowserUpdateConfig( | ||
| params: BrowserUpdateConfigParams, | ||
| ): BrowserConfigResult<BrowserUpdateConfig> { | ||
| const profile = buildBrowserProfile(params); | ||
| if (!profile.ok) return profile; | ||
|
|
||
| const viewport = buildBrowserViewportUpdate(params); | ||
| if (!viewport.ok) return viewport; | ||
|
|
||
| return configValue({ | ||
| ...(profile.value && { profile: profile.value }), | ||
| ...(viewport.value && { viewport: viewport.value }), | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { | ||
| ResourceTemplate, | ||
| type McpServer, | ||
| } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { createKernelClient, type KernelClient } from "@/lib/mcp/kernel-client"; | ||
|
|
||
| type JsonResourceTemplateOptions = { | ||
| name: string; | ||
| uriTemplate: string; | ||
| variableName: string; | ||
| resourceLabel: string; | ||
| read: ( | ||
| client: KernelClient, | ||
| identifier: string, | ||
| ) => Promise<unknown | null | undefined>; | ||
| }; | ||
|
|
||
| function templateVariableValue( | ||
| variables: Record<string, string | string[]>, | ||
| name: string, | ||
| ) { | ||
| const value = variables[name]; | ||
| return Array.isArray(value) ? value[0] : value; | ||
| } | ||
|
|
||
| export function registerJsonResourceTemplate( | ||
| server: McpServer, | ||
| options: JsonResourceTemplateOptions, | ||
| ) { | ||
| server.resource( | ||
| options.name, | ||
| new ResourceTemplate(options.uriTemplate, { list: undefined }), | ||
| async (uri, variables, extra) => { | ||
| if (!extra.authInfo) { | ||
| throw new Error("Authentication required"); | ||
| } | ||
|
|
||
| const identifier = templateVariableValue(variables, options.variableName); | ||
| if (!identifier) { | ||
| throw new Error(`Invalid ${options.resourceLabel} URI: ${uri}`); | ||
| } | ||
|
|
||
| const client = createKernelClient(extra.authInfo.token); | ||
| const resource = await options.read(client, identifier); | ||
|
|
||
| if (!resource) { | ||
| throw new Error(`${options.resourceLabel} "${identifier}" not found`); | ||
| } | ||
|
|
||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uri.toString(), | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(resource, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| }, | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.