💬 Commit message: Update 2026-02-06 21:32:05, 5 files, 447 lines

📁 Files changed: 5
📝 Lines changed: 447

  • settings.local.json
  • cli.ts
  • logger.ts
  • mcp.ts
  • server.ts
This commit is contained in:
Adam Ladachowski
2026-02-06 21:32:05 +01:00
parent 603e8ca39a
commit 2736a4b467
5 changed files with 272 additions and 175 deletions
+105 -48
View File
@@ -3,6 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { ClaudeBrowser } from './browser.js';
import { type CommandLike, type ResultLike, stderrLogger as log } from './logger.js';
const browser = new ClaudeBrowser({ headless: true, width: 1280, height: 800 });
let launched = false;
@@ -14,52 +15,103 @@ async function ensureLaunched(): Promise<void> {
}
}
type ToolResult = { content: [{ type: 'text'; text: string }] };
function textResult(text: string): ToolResult {
return { content: [{ type: 'text' as const, text }] };
}
function withLogging<T extends Record<string, unknown>>(
cmd: string,
fn: (args: T) => Promise<ToolResult>
): (args: T) => Promise<ToolResult> {
return async (args: T) => {
const cmdLike: CommandLike = { cmd, ...args };
log.command(cmdLike);
try {
const result = await fn(args);
const parsed = JSON.parse(result.content[0]?.text || '{}');
const resultLike: ResultLike = { ok: true, ...parsed };
log.result(cmdLike, resultLike);
return result;
} catch (err) {
log.result(cmdLike, { ok: false, error: (err as Error).message });
throw err;
}
};
}
const server = new McpServer({
name: 'claude-browse',
version: '0.1.0',
});
// Navigation
server.tool('goto', 'Navigate to a URL', { url: z.string().url() }, async ({ url }) => {
await ensureLaunched();
const result = await browser.goto(url);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
server.tool(
'goto',
'Navigate to a URL',
{ url: z.string().url() },
withLogging('goto', async ({ url }) => {
await ensureLaunched();
const result = await browser.goto(url);
return textResult(JSON.stringify(result));
})
);
server.tool('back', 'Go back in browser history', {}, async () => {
await ensureLaunched();
const result = await browser.back();
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
server.tool(
'back',
'Go back in browser history',
{},
withLogging('back', async () => {
await ensureLaunched();
const result = await browser.back();
return textResult(JSON.stringify(result));
})
);
server.tool('forward', 'Go forward in browser history', {}, async () => {
await ensureLaunched();
const result = await browser.forward();
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
server.tool(
'forward',
'Go forward in browser history',
{},
withLogging('forward', async () => {
await ensureLaunched();
const result = await browser.forward();
return textResult(JSON.stringify(result));
})
);
server.tool('reload', 'Reload the current page', {}, async () => {
await ensureLaunched();
const result = await browser.reload();
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
server.tool(
'reload',
'Reload the current page',
{},
withLogging('reload', async () => {
await ensureLaunched();
const result = await browser.reload();
return textResult(JSON.stringify(result));
})
);
// Interaction
server.tool('click', 'Click on an element', { selector: z.string() }, async ({ selector }) => {
await ensureLaunched();
const result = await browser.click(selector);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
server.tool(
'click',
'Click on an element',
{ selector: z.string() },
withLogging('click', async ({ selector }) => {
await ensureLaunched();
const result = await browser.click(selector);
return textResult(JSON.stringify(result));
})
);
server.tool(
'type',
'Type text into an input field',
{ selector: z.string(), text: z.string() },
async ({ selector, text }) => {
withLogging('type', async ({ selector, text }) => {
await ensureLaunched();
await browser.type(selector, text);
return { content: [{ type: 'text', text: 'ok' }] };
}
return textResult(JSON.stringify({ ok: true }));
})
);
// Query
@@ -67,28 +119,33 @@ server.tool(
'query',
'Query elements by CSS selector, returns tag, text, and attributes',
{ selector: z.string() },
async ({ selector }) => {
withLogging('query', async ({ selector }) => {
await ensureLaunched();
const elements = await browser.query(selector);
return { content: [{ type: 'text', text: JSON.stringify(elements, null, 2) }] };
}
return textResult(JSON.stringify({ ok: true, count: elements.length, elements }));
})
);
server.tool('url', 'Get current URL and page title', {}, async () => {
await ensureLaunched();
const result = await browser.getUrl();
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
server.tool(
'url',
'Get current URL and page title',
{},
withLogging('url', async () => {
await ensureLaunched();
const result = await browser.getUrl();
return textResult(JSON.stringify(result));
})
);
server.tool(
'html',
'Get page HTML content',
{ full: z.boolean().optional().default(false) },
async ({ full }) => {
withLogging('html', async ({ full }) => {
await ensureLaunched();
const html = await browser.getHtml(full);
return { content: [{ type: 'text', text: html }] };
}
return textResult(JSON.stringify({ ok: true, html }));
})
);
// Screenshot
@@ -99,11 +156,11 @@ server.tool(
path: z.string().optional().default('screenshots/screenshot.png'),
fullPage: z.boolean().optional().default(false),
},
async ({ path, fullPage }) => {
withLogging('screenshot', async ({ path, fullPage }) => {
await ensureLaunched();
const result = await browser.screenshot(path, fullPage);
return { content: [{ type: 'text', text: `Screenshot saved to ${result.path}` }] };
}
return textResult(JSON.stringify({ ok: true, path: result.path }));
})
);
// Eval
@@ -111,11 +168,11 @@ server.tool(
'eval',
'Execute JavaScript in the browser context',
{ script: z.string() },
async ({ script }) => {
withLogging('eval', async ({ script }) => {
await ensureLaunched();
const result = await browser.eval(script);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
}
return textResult(JSON.stringify({ ok: true, result }));
})
);
// Utility
@@ -123,11 +180,11 @@ server.tool(
'wait',
'Wait for a specified time in milliseconds',
{ ms: z.number().optional().default(1000) },
async ({ ms }) => {
withLogging('wait', async ({ ms }) => {
await ensureLaunched();
await browser.wait(ms);
return { content: [{ type: 'text', text: 'ok' }] };
}
return textResult(JSON.stringify({ ok: true }));
})
);
// Start server