diff --git a/README.md b/README.md index 1f9270c..e55a9d1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Claude: *opens browser* *navigates* *fills form* *clicks submit* ## Features -- **30 MCP tools** for complete browser control +- **31 MCP tools** for complete browser control - **Inline screenshots** — Claude sees what the browser sees - **Accessibility snapshots** — full page structure as text - **Test scenario runner** — define multi-step tests in JSON @@ -116,6 +116,35 @@ Just ask Claude to interact with your web app: | `browser_tab_select` | Switch to a tab | | `browser_close` | Close the browser | +### Optional web search tool + +If `YDC_API_KEY` is set, Glance also exposes `browser_web_search` for lightweight You.com Search lookups before opening pages in the browser. + +Example: + +```json +{ + "mcpServers": { + "glance": { + "command": "npx", + "args": ["glance-mcp"], + "env": { + "YDC_API_KEY": "your-key-here" + } + } + } +} +``` + +Usage: + +```text +browser_web_search {"query":"best practices for Playwright locators","count":5} +``` + +- Returns web and news results with titles, URLs, descriptions, and snippets. +- Fails closed if the API key is missing, so existing browser tooling is unchanged. + ### Test Automation (7 tools) | Tool | Description | diff --git a/src/index.ts b/src/index.ts index f87b8bb..698c444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { registerTestingTools } from './tools/testing.js'; import { registerEventTools } from './tools/events.js'; import { registerSessionTools } from './tools/session.js'; import { registerVisualTools } from './tools/visual.js'; +import { registerSearchTools } from './tools/search.js'; async function main() { const config = getConfig(); @@ -46,8 +47,9 @@ async function main() { registerEventTools(server); registerSessionTools(server); registerVisualTools(server, config); + registerSearchTools(server); - console.error('[glance] 30 tools registered'); + console.error('[glance] 31 tools registered'); // Start MCP transport const transport = new StdioServerTransport(); diff --git a/src/tools/search.ts b/src/tools/search.ts new file mode 100644 index 0000000..da6b35f --- /dev/null +++ b/src/tools/search.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +const YDC_SEARCH_URL = 'https://ydc-index.io/v1/search'; + +function getApiKey() { + return process.env.YDC_API_KEY?.trim(); +} + +function formatResults(data: any) { + const web = Array.isArray(data?.results?.web) ? data.results.web : []; + const news = Array.isArray(data?.results?.news) ? data.results.news : []; + const items = [...web.map((item: any) => ({ ...item, kind: 'web' })), ...news.map((item: any) => ({ ...item, kind: 'news' }))]; + + if (!items.length) return 'No results returned.'; + + return items.map((item: any, index: number) => { + const lines = [ + `${index + 1}. [${item.kind}] ${item.title ?? item.url}`, + ` ${item.url}`, + ]; + if (item.description) lines.push(` ${item.description}`); + if (Array.isArray(item.snippets) && item.snippets.length) { + lines.push(` Snippets: ${item.snippets.slice(0, 3).join(' | ')}`); + } + return lines.join('\n'); + }).join('\n\n'); +} + +export function registerSearchTools(server: McpServer) { + const apiKey = getApiKey(); + if (!apiKey) return; + + server.tool( + 'browser_web_search', + { + query: z.string().min(1).describe('Search query'), + count: z.number().int().min(1).max(10).optional().default(5).describe('Number of results to return'), + freshness: z.enum(['day', 'week', 'month', 'year']).optional(), + country: z.string().length(2).optional().describe('Country code like US or GB'), + language: z.string().optional().describe('Language code like EN'), + safesearch: z.enum(['off', 'moderate', 'strict']).optional(), + }, + async ({ query, count, freshness, country, language, safesearch }) => { + try { + const url = new URL(YDC_SEARCH_URL); + url.searchParams.set('query', query); + url.searchParams.set('count', String(count ?? 5)); + if (freshness) url.searchParams.set('freshness', freshness); + if (country) url.searchParams.set('country', country); + if (language) url.searchParams.set('language', language); + if (safesearch) url.searchParams.set('safesearch', safesearch); + + const resp = await fetch(url, { + headers: { 'X-API-Key': apiKey }, + }); + + if (!resp.ok) { + const body = await resp.text(); + return { + content: [{ type: 'text' as const, text: `You.com Search API error ${resp.status}: ${body}` }], + isError: true, + }; + } + + const data = await resp.json(); + return { + content: [{ type: 'text' as const, text: formatResults(data) }], + }; + } catch (err: any) { + return { + content: [{ type: 'text' as const, text: `You.com search failed: ${err.message}` }], + isError: true, + }; + } + } + ); +}