Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
78 changes: 78 additions & 0 deletions src/tools/search.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
);
}