mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
feat: added bookmarks tool and instructions in prompt (#276)
* feat: added bookmarks tool and instructions in prompt * feat: added bookmarks tool and instructions in prompt
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 BrowserOS
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { z } from 'zod'
|
||||
import { BookmarkAdapter } from '@/adapters/BookmarkAdapter'
|
||||
import { ActionHandler } from '../ActionHandler'
|
||||
|
||||
// Input schema
|
||||
const UpdateBookmarkInputSchema = z.object({
|
||||
id: z.string().describe('Bookmark ID to update'),
|
||||
title: z.string().optional().describe('New bookmark title'),
|
||||
url: z.string().url().optional().describe('New bookmark URL'),
|
||||
})
|
||||
|
||||
// Output schema
|
||||
const UpdateBookmarkOutputSchema = z.object({
|
||||
id: z.string().describe('Bookmark ID'),
|
||||
title: z.string().describe('Updated bookmark title'),
|
||||
url: z.string().optional().describe('Updated bookmark URL'),
|
||||
})
|
||||
|
||||
type UpdateBookmarkInput = z.infer<typeof UpdateBookmarkInputSchema>
|
||||
type UpdateBookmarkOutput = z.infer<typeof UpdateBookmarkOutputSchema>
|
||||
|
||||
/**
|
||||
* UpdateBookmarkAction - Update a bookmark's title or URL
|
||||
*
|
||||
* Updates an existing bookmark with new title and/or URL.
|
||||
*
|
||||
* Input:
|
||||
* - id: Bookmark ID to update
|
||||
* - title (optional): New title for the bookmark
|
||||
* - url (optional): New URL for the bookmark
|
||||
*
|
||||
* Output:
|
||||
* - id: Bookmark ID
|
||||
* - title: Updated title
|
||||
* - url: Updated URL
|
||||
*
|
||||
* Usage:
|
||||
* Update a bookmark's title or URL (at least one must be provided).
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* "id": "123",
|
||||
* "title": "New Title",
|
||||
* "url": "https://www.example.com"
|
||||
* }
|
||||
* // Returns: { id: "123", title: "New Title", url: "https://www.example.com" }
|
||||
*/
|
||||
export class UpdateBookmarkAction extends ActionHandler<
|
||||
UpdateBookmarkInput,
|
||||
UpdateBookmarkOutput
|
||||
> {
|
||||
readonly inputSchema = UpdateBookmarkInputSchema
|
||||
private bookmarkAdapter = new BookmarkAdapter()
|
||||
|
||||
async execute(input: UpdateBookmarkInput): Promise<UpdateBookmarkOutput> {
|
||||
const changes: { title?: string; url?: string } = {}
|
||||
|
||||
if (input.title !== undefined) {
|
||||
changes.title = input.title
|
||||
}
|
||||
if (input.url !== undefined) {
|
||||
changes.url = input.url
|
||||
}
|
||||
|
||||
if (Object.keys(changes).length === 0) {
|
||||
throw new Error('At least one of title or url must be provided')
|
||||
}
|
||||
|
||||
const updated = await this.bookmarkAdapter.updateBookmark(input.id, changes)
|
||||
|
||||
return {
|
||||
id: updated.id,
|
||||
title: updated.title,
|
||||
url: updated.url,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { GetBookmarksAction } from '@/actions/bookmark/GetBookmarksAction'
|
||||
import { MoveBookmarkAction } from '@/actions/bookmark/MoveBookmarkAction'
|
||||
import { RemoveBookmarkAction } from '@/actions/bookmark/RemoveBookmarkAction'
|
||||
import { RemoveBookmarkTreeAction } from '@/actions/bookmark/RemoveBookmarkTreeAction'
|
||||
import { UpdateBookmarkAction } from '@/actions/bookmark/UpdateBookmarkAction'
|
||||
import { CaptureScreenshotAction } from '@/actions/browser/CaptureScreenshotAction'
|
||||
import { CaptureScreenshotPointerAction } from '@/actions/browser/CaptureScreenshotPointerAction'
|
||||
import { ClearAction } from '@/actions/browser/ClearAction'
|
||||
@@ -204,6 +205,7 @@ export class BrowserOSController {
|
||||
this.actionRegistry.register('getBookmarks', new GetBookmarksAction())
|
||||
this.actionRegistry.register('createBookmark', new CreateBookmarkAction())
|
||||
this.actionRegistry.register('removeBookmark', new RemoveBookmarkAction())
|
||||
this.actionRegistry.register('updateBookmark', new UpdateBookmarkAction())
|
||||
this.actionRegistry.register(
|
||||
'createBookmarkFolder',
|
||||
new CreateBookmarkFolderAction(),
|
||||
|
||||
@@ -135,10 +135,25 @@ When user asks to "organize tabs", "group tabs", or "clean up tabs":
|
||||
|
||||
Use when built-in tools cannot accomplish the task.
|
||||
|
||||
## Bookmarks & History
|
||||
- \`browser_get_bookmarks(folderId?)\` - Get bookmarks
|
||||
- \`browser_create_bookmark(title, url, parentId?)\` - Create bookmark
|
||||
## Bookmarks
|
||||
- \`browser_get_bookmarks(folderId?)\` - Get all bookmarks or from specific folder
|
||||
- \`browser_create_bookmark(title, url, parentId?)\` - Create bookmark (use parentId to place in folder)
|
||||
- \`browser_update_bookmark(bookmarkId, title?, url?)\` - Edit bookmark title or URL
|
||||
- \`browser_remove_bookmark(bookmarkId)\` - Delete bookmark
|
||||
- \`browser_create_bookmark_folder(title, parentId?)\` - Create folder (returns folderId to use as parentId)
|
||||
- \`browser_get_bookmark_children(folderId)\` - Get contents of a folder
|
||||
- \`browser_move_bookmark(bookmarkId, parentId?, index?)\` - Move bookmark or folder to new location
|
||||
- \`browser_remove_bookmark_tree(folderId, confirm)\` - Delete folder and all contents
|
||||
|
||||
**Organizing bookmarks into folders:**
|
||||
\`\`\`
|
||||
1. browser_create_bookmark_folder("Work") → folderId: "123"
|
||||
2. browser_create_bookmark("Docs", "https://docs.google.com", parentId="123")
|
||||
3. browser_move_bookmark(existingBookmarkId, parentId="123")
|
||||
\`\`\`
|
||||
Use \`browser_get_bookmarks\` to find existing folder IDs, or create new folders with \`browser_create_bookmark_folder\`.
|
||||
|
||||
## History
|
||||
- \`browser_search_history(query, maxResults?)\` - Search history
|
||||
- \`browser_get_recent_history(count?)\` - Recent history
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
moveBookmark,
|
||||
removeBookmark,
|
||||
removeBookmarkTree,
|
||||
updateBookmark,
|
||||
} from './tools/bookmarks'
|
||||
// Content Extraction
|
||||
export { getPageContent } from './tools/content'
|
||||
@@ -77,6 +78,7 @@ import {
|
||||
moveBookmark,
|
||||
removeBookmark,
|
||||
removeBookmarkTree,
|
||||
updateBookmark,
|
||||
} from './tools/bookmarks'
|
||||
import { getPageContent } from './tools/content'
|
||||
import { clickCoordinates, typeAtCoordinates } from './tools/coordinates'
|
||||
@@ -106,7 +108,7 @@ import {
|
||||
} from './tools/tab-management'
|
||||
import { createWindow } from './tools/window-management'
|
||||
|
||||
// Array export for convenience (36 tools total)
|
||||
// Array export for convenience (37 tools total)
|
||||
export const allControllerTools = [
|
||||
getActiveTab,
|
||||
listTabs,
|
||||
@@ -142,6 +144,7 @@ export const allControllerTools = [
|
||||
getBookmarkChildren,
|
||||
moveBookmark,
|
||||
removeBookmarkTree,
|
||||
updateBookmark,
|
||||
searchHistory,
|
||||
getRecentHistory,
|
||||
createWindow,
|
||||
|
||||
@@ -60,7 +60,8 @@ export const getBookmarks = defineTool<z.ZodRawShape, Context, Response>({
|
||||
|
||||
export const createBookmark = defineTool<z.ZodRawShape, Context, Response>({
|
||||
name: 'browser_create_bookmark',
|
||||
description: 'Create a new bookmark',
|
||||
description:
|
||||
'Create a new bookmark. Use parentId to place it inside an existing folder or a newly created one.',
|
||||
annotations: {
|
||||
category: ToolCategories.BOOKMARKS,
|
||||
readOnlyHint: false,
|
||||
@@ -68,7 +69,12 @@ export const createBookmark = defineTool<z.ZodRawShape, Context, Response>({
|
||||
schema: {
|
||||
title: z.string().describe('Bookmark title'),
|
||||
url: z.string().describe('URL to bookmark'),
|
||||
parentId: z.string().optional().describe('Optional parent folder ID'),
|
||||
parentId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Folder ID to create bookmark in (from browser_get_bookmarks or browser_create_bookmark_folder)',
|
||||
),
|
||||
windowId: z.number().optional().describe('Window ID for routing'),
|
||||
},
|
||||
handler: async (request, response, context) => {
|
||||
@@ -116,13 +122,51 @@ export const removeBookmark = defineTool<z.ZodRawShape, Context, Response>({
|
||||
},
|
||||
})
|
||||
|
||||
export const updateBookmark = defineTool<z.ZodRawShape, Context, Response>({
|
||||
name: 'browser_update_bookmark',
|
||||
description: 'Update a bookmark title or URL',
|
||||
annotations: {
|
||||
category: ToolCategories.BOOKMARKS,
|
||||
readOnlyHint: false,
|
||||
},
|
||||
schema: {
|
||||
bookmarkId: z.string().describe('Bookmark ID to update'),
|
||||
title: z.string().optional().describe('New title for the bookmark'),
|
||||
url: z.string().url().optional().describe('New URL for the bookmark'),
|
||||
windowId: z.number().optional().describe('Window ID for routing'),
|
||||
},
|
||||
handler: async (request, response, context) => {
|
||||
const { bookmarkId, title, url, windowId } = request.params as {
|
||||
bookmarkId: string
|
||||
title?: string
|
||||
url?: string
|
||||
windowId?: number
|
||||
}
|
||||
|
||||
const result = await context.executeAction('updateBookmark', {
|
||||
id: bookmarkId,
|
||||
title,
|
||||
url,
|
||||
windowId,
|
||||
})
|
||||
const data = result as { id: string; title: string; url?: string }
|
||||
|
||||
response.appendResponseLine(`Updated bookmark: ${data.title}`)
|
||||
if (data.url) {
|
||||
response.appendResponseLine(`URL: ${data.url}`)
|
||||
}
|
||||
response.appendResponseLine(`ID: ${data.id}`)
|
||||
},
|
||||
})
|
||||
|
||||
export const createBookmarkFolder = defineTool<
|
||||
z.ZodRawShape,
|
||||
Context,
|
||||
Response
|
||||
>({
|
||||
name: 'browser_create_bookmark_folder',
|
||||
description: 'Create a new bookmark folder',
|
||||
description:
|
||||
'Create a new bookmark folder. Returns folderId to use as parentId when creating or moving bookmarks into this folder.',
|
||||
annotations: {
|
||||
category: ToolCategories.BOOKMARKS,
|
||||
readOnlyHint: false,
|
||||
@@ -214,14 +258,20 @@ export const getBookmarkChildren = defineTool<z.ZodRawShape, Context, Response>(
|
||||
|
||||
export const moveBookmark = defineTool<z.ZodRawShape, Context, Response>({
|
||||
name: 'browser_move_bookmark',
|
||||
description: 'Move a bookmark or folder to a new location',
|
||||
description:
|
||||
'Move a bookmark or folder into a different folder (existing or newly created).',
|
||||
annotations: {
|
||||
category: ToolCategories.BOOKMARKS,
|
||||
readOnlyHint: false,
|
||||
},
|
||||
schema: {
|
||||
bookmarkId: z.string().describe('Bookmark or folder ID to move'),
|
||||
parentId: z.string().optional().describe('New parent folder ID'),
|
||||
parentId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Destination folder ID (from browser_get_bookmarks or browser_create_bookmark_folder)',
|
||||
),
|
||||
index: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -366,6 +366,190 @@ describe('MCP Controller Bookmark Tools', () => {
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe('browser_update_bookmark - Success Cases', () => {
|
||||
it('tests that updating bookmark title succeeds', async () => {
|
||||
await withMcpServer(async (client) => {
|
||||
// First create a bookmark
|
||||
const createResult = await client.callTool({
|
||||
name: 'browser_create_bookmark',
|
||||
arguments: {
|
||||
title: 'Original Title',
|
||||
url: 'https://update.example.com',
|
||||
},
|
||||
})
|
||||
|
||||
const createText = createResult.content.find((c) => c.type === 'text')
|
||||
const idMatch = createText.text.match(/ID: (\d+)/)
|
||||
const bookmarkId = idMatch ? idMatch[1] : '1'
|
||||
|
||||
// Update the title
|
||||
const result = await client.callTool({
|
||||
name: 'browser_update_bookmark',
|
||||
arguments: {
|
||||
bookmarkId,
|
||||
title: 'Updated Title',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('\n=== Update Bookmark Title Response ===')
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
|
||||
assert.ok(!result.isError, 'Should succeed')
|
||||
|
||||
const textContent = result.content.find((c) => c.type === 'text')
|
||||
assert.ok(textContent, 'Should have text content')
|
||||
assert.ok(
|
||||
textContent.text.includes('Updated bookmark'),
|
||||
'Should confirm update',
|
||||
)
|
||||
assert.ok(
|
||||
textContent.text.includes('Updated Title'),
|
||||
'Should include new title',
|
||||
)
|
||||
|
||||
// Cleanup
|
||||
await client.callTool({
|
||||
name: 'browser_remove_bookmark',
|
||||
arguments: { bookmarkId },
|
||||
})
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
it('tests that updating bookmark URL succeeds', async () => {
|
||||
await withMcpServer(async (client) => {
|
||||
// First create a bookmark
|
||||
const createResult = await client.callTool({
|
||||
name: 'browser_create_bookmark',
|
||||
arguments: {
|
||||
title: 'URL Test',
|
||||
url: 'https://old-url.example.com',
|
||||
},
|
||||
})
|
||||
|
||||
const createText = createResult.content.find((c) => c.type === 'text')
|
||||
const idMatch = createText.text.match(/ID: (\d+)/)
|
||||
const bookmarkId = idMatch ? idMatch[1] : '1'
|
||||
|
||||
// Update the URL
|
||||
const result = await client.callTool({
|
||||
name: 'browser_update_bookmark',
|
||||
arguments: {
|
||||
bookmarkId,
|
||||
url: 'https://new-url.example.com',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('\n=== Update Bookmark URL Response ===')
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
|
||||
assert.ok(!result.isError, 'Should succeed')
|
||||
|
||||
const textContent = result.content.find((c) => c.type === 'text')
|
||||
assert.ok(textContent, 'Should have text content')
|
||||
assert.ok(
|
||||
textContent.text.includes('https://new-url.example.com'),
|
||||
'Should include new URL',
|
||||
)
|
||||
|
||||
// Cleanup
|
||||
await client.callTool({
|
||||
name: 'browser_remove_bookmark',
|
||||
arguments: { bookmarkId },
|
||||
})
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
it('tests that updating both title and URL succeeds', async () => {
|
||||
await withMcpServer(async (client) => {
|
||||
// First create a bookmark
|
||||
const createResult = await client.callTool({
|
||||
name: 'browser_create_bookmark',
|
||||
arguments: {
|
||||
title: 'Original',
|
||||
url: 'https://original.example.com',
|
||||
},
|
||||
})
|
||||
|
||||
const createText = createResult.content.find((c) => c.type === 'text')
|
||||
const idMatch = createText.text.match(/ID: (\d+)/)
|
||||
const bookmarkId = idMatch ? idMatch[1] : '1'
|
||||
|
||||
// Update both
|
||||
const result = await client.callTool({
|
||||
name: 'browser_update_bookmark',
|
||||
arguments: {
|
||||
bookmarkId,
|
||||
title: 'New Title',
|
||||
url: 'https://new.example.com',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('\n=== Update Bookmark Both Response ===')
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
|
||||
assert.ok(!result.isError, 'Should succeed')
|
||||
|
||||
const textContent = result.content.find((c) => c.type === 'text')
|
||||
assert.ok(textContent, 'Should have text content')
|
||||
assert.ok(
|
||||
textContent.text.includes('New Title'),
|
||||
'Should include new title',
|
||||
)
|
||||
assert.ok(
|
||||
textContent.text.includes('https://new.example.com'),
|
||||
'Should include new URL',
|
||||
)
|
||||
|
||||
// Cleanup
|
||||
await client.callTool({
|
||||
name: 'browser_remove_bookmark',
|
||||
arguments: { bookmarkId },
|
||||
})
|
||||
})
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe('browser_update_bookmark - Error Handling', () => {
|
||||
it('tests that missing bookmarkId is rejected', async () => {
|
||||
await withMcpServer(async (client) => {
|
||||
try {
|
||||
await client.callTool({
|
||||
name: 'browser_update_bookmark',
|
||||
arguments: { title: 'Test' },
|
||||
})
|
||||
assert.fail('Should have thrown validation error')
|
||||
} catch (error) {
|
||||
console.log('\n=== Update Bookmark Missing ID Error ===')
|
||||
console.log(error.message)
|
||||
|
||||
assert.ok(
|
||||
error.message.includes('Invalid arguments') ||
|
||||
error.message.includes('Required'),
|
||||
'Should reject with validation error',
|
||||
)
|
||||
}
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
it('tests that invalid bookmarkId is handled', async () => {
|
||||
await withMcpServer(async (client) => {
|
||||
const result = await client.callTool({
|
||||
name: 'browser_update_bookmark',
|
||||
arguments: {
|
||||
bookmarkId: '999999999',
|
||||
title: 'Test',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('\n=== Update Invalid Bookmark Response ===')
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
|
||||
// Should error with invalid ID
|
||||
assert.ok(result, 'Should return a result')
|
||||
})
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe('Bookmark Tools - Response Structure Validation', () => {
|
||||
it('tests that bookmark tools return valid MCP response structure', async () => {
|
||||
await withMcpServer(async (client) => {
|
||||
|
||||
Reference in New Issue
Block a user