Repository Public Research preview
1// Opt-in checks against real public sites (needs network). Run: node --test test/realweb.test.mjs2// Read-only: searches and reads, never signs in or submits anything but a search box.3import assert from "node:assert/strict";4import { mkdtempSync, rmSync } from "node:fs";5import { tmpdir } from "node:os";6import { dirname, join } from "node:path";7import { after, before, test } from "node:test";8import { fileURLToPath } from "node:url";9import { Client } from "@modelcontextprotocol/sdk/client/index.js";10import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";1112const root = dirname(dirname(fileURLToPath(import.meta.url)));13const work = mkdtempSync(join(tmpdir(), "safari-mcp-web-"));14let c;15const call = async (name, args = {}) => { const r = await c.callTool({ name, arguments: args }, undefined, { timeout: 120_000 }); return { data: r.structuredContent, isError: !!r.isError }; };1617before(async () => {18 c = new Client({ name: "realweb", version: "1" });19 await c.connect(new StdioClientTransport({ command: process.execPath, args: [join(root, "server.mjs")], env: { PATH: process.env.PATH, SAFARI_MCP_FILE_ROOT: work, SAFARI_MCP_ALLOW_DOMAINS: "wikipedia.org,example.com,iana.org,developer.mozilla.org" }, stderr: "pipe" }));20});21after(async () => { await call("close_tab", { window: true }).catch(() => {}); await c.close(); rmSync(work, { recursive: true, force: true }); });2223test("example.com: open, read, follow the link across sites", async () => {24 const o = await call("open_url", { url: "https://example.com/" });25 assert.equal(o.data.title, "Example Domain");26 assert.match((await call("read_text")).data.text, /documentation examples|illustrative examples/i);27 const link = (await call("snapshot")).data.elements.find((e) => e.role === "link");28 const nav = await call("click", { target: { ref: link.ref } });29 assert.equal(nav.data.page.navigated, true);30 assert.match(nav.data.page.url, /iana\.org/);31});3233test("Wikipedia: search form, keyboard submit, article text, table and links", async () => {34 await call("navigate", { url: "https://en.wikipedia.org/wiki/Special:Search" });35 const f = await call("fill", { target: { selector: 'input[name="search"]', index: 0 }, value: "Ada Lovelace" });36 assert.equal(f.isError, false, JSON.stringify(f.data));37 const k = await call("press_key", { key: "Enter", target: { selector: 'input[name="search"]', index: 0 } });38 assert.equal(k.data.page.navigated, true);39 // The search page lists results (or jumps straight to the article): open the article either way.40 if ((await call("read_text", { target: { selector: "h1" } })).data.text !== "Ada Lovelace") {41 const hit = await call("click", { target: { selector: '.mw-search-result-heading a[title="Ada Lovelace"]', index: 0 } });42 assert.equal(hit.data.page.navigated, true, JSON.stringify(hit.data));43 }44 assert.equal((await call("read_text", { target: { selector: "h1" } })).data.text, "Ada Lovelace");45 assert.match((await call("read_text", { max_chars: 4000 })).data.text, /mathematician/i);46 const tables = await call("extract_tables", { selector: "table.infobox", max_rows: 40 });47 assert.ok(tables.data.count >= 1);48 assert.ok(tables.data.tables[0].rows.some((r) => /Born/.test(r[0] ?? "")));49 const links = await call("list_links", { filter: "Charles_Babbage", same_origin: true, limit: 5 });50 assert.ok(links.data.count >= 1);51 const shot = await call("screenshot", { output_path: "ada.png", include_preview: false });52 assert.ok(shot.data.bytes > 20000);53});5455test("the domain allow list holds on the open web", async () => {56 assert.equal((await call("open_url", { url: "https://www.apple.com/" })).data.error.code, "domain_not_allowed");57});58