You want your desktop AI client to be able to read web pages. The obvious move is to point it at a hosted scraping MCP server, and that works. But there is a version of this that gives you more control for about fifteen minutes of setup: build the MCP server yourself, in n8n, out of a trigger node and a scraping node you already have.
What you get for that is worth listing, because it is not obvious from the outside. Your scraping API key lives in an n8n credential and never enters the model's context. You choose exactly which capabilities the client sees — one tool, not thirty-nine. Every call lands in the Executions tab, so when the model does something expensive you can see what it did and why. And you can put anything else you like behind the same URL: a database lookup, a Slack post, a rate limiter of your own design.
What You Need
An n8n version that has the MCP Server Trigger node — search the nodes panel for "MCP" and you either have it or you need to upgrade. Self-hosted or Cloud both work. If you are self-hosting behind a reverse proxy, read the last section before you start rather than after it fails.
A scraping node. This guide uses Evomi's, because it ships as a verified community node and declares itself usable as a tool, which is the property that matters here. Any node with the same property works the same way.
An Evomi API key, from Settings → API in the dashboard.
A client that speaks MCP over SSE or streamable HTTP, or a stdio-only client plus
mcp-remote.
Step 1: Install the Node
Verified community nodes install from inside the editor, which is the part people expect to be harder than it is.
Open the nodes panel with + or the n key, search for "Evomi", and look for the More from the community section at the bottom of the results. Select the node, then Install. That installs it for the whole instance.
Two caveats. Only the instance owner or an admin can install; other members can use what is installed but cannot add to it. And the section only appears if verified community nodes are enabled — on Cloud that is a toggle in the admin panel, and on self-hosted it is governed by N8N_COMMUNITY_PACKAGES_ENABLED and N8N_VERIFIED_PACKAGES_ENABLED, both of which default to true.
Step 2: Create the Credential
Add an Evomi API credential:
Field | Value |
|---|---|
API Key | Your key from Settings → API |
Base URL |
|
n8n tests the credential by calling GET /api/v1/scraper/health, so a green tick here means the key and the host both work. At runtime the key travels as an x-api-key header — it is never in a URL, never in a query string, and never in anything the model sees.
Step 3: Add the MCP Server Trigger
New workflow, add an MCP Server Trigger. It will look wrong at first, because it does not connect to the next node the way every other trigger does. That is correct: this trigger only connects to tool nodes, and the client on the other end decides which of them to call. You are describing a menu, not a sequence.
Set three things.
Path. n8n generates a random one so two triggers cannot collide. Replace it with something you can type — scrape is fine. Your production URL becomes https://your-n8n-host/mcp/scrape.
Authentication. The options are None, Bearer, Header and n8n OAuth2. Choose Bearer and create the credential. Generate the token yourself and make it long:
openssl rand -hex 32
Do not skip this. The MCP URL is a public HTTP endpoint on your n8n host, and an unauthenticated one is an open scraping proxy that bills to your account. With Bearer set, a request carrying no token or the wrong one is refused with a 403 before the workflow runs at all.
Which URL you are looking at. The node shows a Test URL and a Production URL and you toggle between them. The test URL only exists while the workflow is listening — it is for watching data arrive on the canvas. The production URL exists once the workflow is published. Configure your client with the production one, or you will spend an evening debugging a client that worked until you closed the tab.
Step 4: Attach the Scraping Tool
Attach the Evomi node directly. Drag it onto the canvas and connect it to the trigger's tool connector — because it declares itself usable as a tool, n8n offers a Evomi Tool variant with a tool output instead of the usual main output, and that is what connects.
Then configure it:
Field | Value | Why |
|---|---|---|
Tool Description | Manual → "Fetch a web page and return its readable content as markdown" | The entire basis on which the model decides to call this |
Operation | Scrape | The only one it has |
URL |
| See below. This is the load-bearing line |
Mode | Auto | HTTP first, upgrades to a browser only if the page needs it |
Output | Markdown | Clean text, no HTML noise in the model's context |
Wait Seconds | 5 | The default; the node clamps this to 0–30 |
Proxy Country |
| Optional; omit the field entirely if you do not want callers choosing an exit |
The node uppercases the country and validates it against /^[A-Z]{2}$/, so a caller that sends de is fine and one that sends Germany gets a clear error rather than a silently wrong result.
The tool's name comes from the node's name on the canvas, with spaces turned into underscores. Rename the node to scrape page and the client sees scrape_page. Leave it as "Evomi Tool" and the client sees Evomi_Tool, which is a tool named after a company rather than a job, and models reach for it less.
$fromAI() Is Not Optional Here, and the Docs Suggest Otherwise
This is the part worth slowing down for, because getting it wrong produces two different failures and neither error message points at the cause.
n8n's documentation scopes $fromAI() to tools connected to the AI Agent node. Under the MCP Server Trigger it works identically, and it is the only thing that puts a parameter into the tool schema your client sees. Concretely, on a live 2.36 instance:
Leave the URL field empty and the workflow refuses to publish at all: Cannot publish workflow: Node "Evomi Tool": Missing or invalid required parameters: url. This is the good failure — it happens before anything is exposed.
Hardcode a URL and it publishes, and the tool works, and it fetches that one page forever regardless of what the model asks for.
Use
$fromAI()and the client gets what you actually want:
{
"name": "scrape_page",
"description": "Fetch a web page and return its readable content as markdown.",
"inputSchema": {
"type": "object",
"properties": {
"url": { "type": "string", "description": "Absolute URL of the web page to fetch" },
"country": { "type": "string", "description": "Two-letter ISO country code", "default": "" }
},
"required": ["url"]
}
}Note which fields ended up in required. Every $fromAI() call is required unless you give it a fourth argument, which is its default. $fromAI('country', '...', 'string') forces the model to supply a country on every call; adding , '' moves it out of required and gives it a default. One comma decides it, and the symptom of getting it wrong is a model that refuses to call the tool because it cannot fill a field it has no value for.
The Sub-Workflow Variant
Put the Evomi node in its own workflow and attach that instead when you want logic between the model and the fetch — a URL allowlist, a rate limiter, a cache lookup, several nodes in sequence. It is one more moving part, and the part is where that control lives.
New workflow. Execute Workflow Trigger with two input fields defined,
urlandcountry, both strings.Evomi, configured as in the table above, but reading
{{ $json.url }}and{{ $json.country }}from the trigger instead of$fromAI().Publish it. A sub-workflow that is still a draft fails at call time with Workflow is not active and cannot be executed — reported to the client as a tool error, with nothing in the MCP workflow to suggest what is wrong.
Then, on the MCP workflow, add a Call n8n Workflow Tool node — this is the node n8n's documentation used to call the Custom n8n Workflow Tool — connect it to the trigger's tool connector, point it at the sub-workflow, and give it a name and description.
The trap here is worse than in the direct route, because it fails quietly. Defining input fields on the Execute Workflow Trigger does not expose them to the MCP client. If you leave the Workflow Inputs mapping blank, the tool publishes happily and advertises this:
{ "type": "object", "properties": { "input": { "type": "string" } } }One untyped string called input. The model has no idea a URL is wanted, sends approximately anything, and the sub-workflow receives nothing usable. Put $fromAI() expressions into the Workflow Inputs mapping — same syntax as above — and the real schema appears. The rule is the same in both routes: the tool schema comes from $fromAI(), not from anywhere else.
Step 5: Connect a Client
Publish the workflow first — the production URL does not exist until you do.
Claude Desktop, or anything else that only speaks stdio
The MCP Server Trigger supports SSE and streamable HTTP. It does not support stdio, so a client that only launches subprocesses needs mcp-remote in between:
{
"mcpServers": {
"n8n-scrape": {
"command": "npx",
"args": [
"mcp-remote",
"https://your-n8n-host/mcp/scrape",
"--header",
"Authorization: Bearer ${AUTH_TOKEN}"
],
"env": {
"AUTH_TOKEN": "the-token-you-generated"
}
}
}
}The token goes in env rather than inline in args for a reason: args shows up in process listings.
A client that speaks HTTP directly
Cursor, VS Code and most recent clients will take the URL and a header. Point them at the production URL with Authorization: Bearer <token>. No wrapper process.
Another n8n instance
Use an MCP Client Tool sub-node on the far side, with the SSE endpoint set to your production URL and Bearer authentication. Two n8n instances talking MCP to each other is an odd-looking architecture that turns out to be a reasonable way to share a capability between teams without sharing credentials.
Step 6: Check What the Model Actually Sees
Ask your client to list tools before you ask it to do anything. You should see one tool, named after your node, with the description you wrote and a url property in its input schema. If the schema shows a single property called input, stop here — the $fromAI() wiring is not right, and no amount of prompting will fix it downstream.
Then try the real thing:
Read https://example.com and tell me what it says.
The first call will ask for approval in most clients. Watch the Executions tab while it runs: the direct attachment produces one execution, the sub-workflow variant produces two — the MCP workflow and the sub-workflow it called, with the URL the model chose visible in the second. Either way the URL the model supplied is in the execution data, which is the fastest way to confirm the schema is wired correctly.
What It Costs
The MCP layer is free. What runs through it bills as an ordinary Scraper API call, from $0.13 per 1,000 results, and the mode decides how many credits each call consumes:
Mode | Proxy type | Credits |
|---|---|---|
| Datacenter | 1 |
| Residential | 2 |
| Residential | 5 |
| Residential | 2 if the HTTP fetch succeeds, 6 if it upgrades to a browser |
Auto is the default for good reason: you pay browser prices only on the pages that actually need a browser.
One thing to be deliberate about. Browser and auto modes require residential proxies — datacenter is request only — so a workflow that needs JavaScript is a residential workflow whether you meant it to be or not.
Switch Output to JSON if you want the model to see its own spend. The node returns the response body rather than the response headers, so the JSON is where the useful telemetry is: credits_used, credits_remaining and mode_used, the last reading auto (request) or auto (browser) so you can tell which pages are costing you three times the others. The cost is a noisier response, and content omitted unless you turn on Include Content.
Hardening It
Rotate the bearer token like a password. It is one, and it authorises spending.
Constrain the URLs if the client is not you. This is the one job the sub-workflow variant does better: an IF node checking the hostname against an allowlist before the Evomi node runs takes five minutes and turns a general-purpose fetcher into a specific one. A general-purpose fetcher reachable by anyone holding the token is a service you are operating for the internet.
Turn off proxy buffering. Behind nginx, SSE connections that are buffered appear to connect and then quietly deliver nothing:
location /mcp/ {
proxy_http_version 1.1;
proxy_buffering off;
gzip off;
chunked_transfer_encoding off;
proxy_set_header Connection '';
# the rest of your proxy headers
}Set these explicitly rather than relying on defaults — the point is to stop them being inherited from elsewhere in the config.
Pin MCP traffic to one replica in queue mode. SSE and streamable HTTP need the same instance to hold the connection for its lifetime. One webhook replica works as-is. With more than one, create a dedicated single-container replica set and route all /mcp* traffic to it at the ingress. Without that, connections break intermittently — the failure mode that looks like a client bug and is not.
What People Get Wrong
Shipping the test URL. It only registers while the workflow is listening. It is not an endpoint; it is a debugging aid.
Leaving authentication off "for now". The URL is public the moment the workflow is published, and the thing behind it spends money.
Attaching the tool with no description. The description is the entire basis on which the model decides whether to call it. A tool named after a company is a tool the model will not reach for.
Hardcoding the URL instead of using
$fromAI(). It publishes, it runs, and it fetches the same page every time no matter what the model asked for.Forgetting the fourth argument on an optional
$fromAI()field. Without a default, the parameter is required, and the model has to invent a value or decline the call.Leaving a sub-workflow unpublished. The MCP workflow publishes fine; the call fails at run time with a message about the other workflow.
Choosing
browsermode by default. Auto already upgrades when a page needs it. Any page that would have worked over plain HTTP now costs browser price for nothing.Expecting datacenter proxies to render JavaScript. They are
requestmode only.Forgetting
mcp-remotefor desktop clients. No stdio on this trigger, and the failure message is not obvious about why.Assuming buffering is off. It is on by default in nginx, and the symptom is a connection that establishes and then does nothing.
Wrapping Up
The MCP Server Trigger is a small node that does something structurally interesting: it lets you assemble an MCP server out of things you already have, without writing one. A scraping node behind it gives an external model web access on your terms — your key, your allowlist, your execution log, and one tool instead of a catalogue.
The tradeoff worth naming is that this gives the model a fetch, not a connection. It can ask for a page and, if you wire it up, name a country; it cannot hold a session across requests, rotate an IP that has stopped working, or check what it has spent, because those are not things an n8n scraping node exposes. If a model needs to manage the connection rather than just use it, that is a different surface — a provider's own MCP server, like Evomi's — and it is worth knowing which of the two problems you have before you build either.
If you are not sure which of n8n's several MCP features you are looking at, start with the four things called "n8n MCP".

