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:
shivammittal274
2026-01-27 23:26:57 +05:30
committed by GitHub
parent 69e159f886
commit ec91d69b1d
6 changed files with 345 additions and 9 deletions

View File

@@ -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,
}
}
}

View File

@@ -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(),

View File

@@ -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

View File

@@ -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,

View File

@@ -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()

View File

@@ -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) => {