
We Audited 193 Pages with Our Open-Source GEO MCP
GEOMCP found 11 small issues across our site. Here are the real findings, the fixes, and how we verified them through an MCP client.
tutorialWebDecoy AI Protection is in public beta. Follow this tutorial to add a check in front of your Next.js chat route so denied requests never reach the model.
Today we are opening AI Protection as a public beta.
AI Protection is for the AI features you build yourself: your chat route, your completion endpoint, the tools you expose over MCP. It is a set of server-side SDKs that run inside your route, after your own authentication and validation and immediately before the model or tool runs. If a request is denied, the model never starts, so you never pay for it.
This post is both the announcement and the setup guide. By the end you will have a Next.js chat route that:
We build WebDecoy. Everything below uses our Node SDK, which is open source under Apache-2.0.
In the WebDecoy dashboard, open AI Protection and select the property for your application, then open the Setup tab. Copy the property ID.
Then go to API keys and create a key scoped to that property with Write Detections permission. This key is a server secret. Never put it in browser code.
npm install @webdecoy/ai-protectionThe example also uses Vercel’s helper for the client IP:
npm install @vercel/functionsAdd these to your server environment (for local work, .env.local). None of them may start with NEXT_PUBLIC_.
WEBDECOY_URL=https://ai-protection.webdecoy.com
WEBDECOY_KEY=your-property-scoped-key
WEBDECOY_PROPERTY_ID=your-property-id
WEBDECOY_SUBJECT_SECRET=a-random-string-of-at-least-32-charactersWEBDECOY_SUBJECT_SECRET is used to hash identifiers before they leave your server. Generate one with openssl rand -hex 32 and keep it the same across all your instances.
Create lib/ai-protection.ts. It builds the protection instance once, on first use, so next build does not need your secrets.
import { after } from 'next/server';
import { ipAddress } from '@vercel/functions';
import { createAIProtection, type AIProtection } from '@webdecoy/ai-protection';
// Server-side context your route passes in. Never read these from the request body.
type ChatContext = { plan: 'free' | 'paid'; inputLength: number };
let protection: AIProtection<ChatContext> | undefined;
// Created on first use, so `next build` does not need the secrets.
export function getProtection() {
protection ??= createAIProtection<ChatContext>({
webdecoyUrl: process.env.WEBDECOY_URL!,
webdecoyKey: process.env.WEBDECOY_KEY!,
propertyId: process.env.WEBDECOY_PROPERTY_ID!,
subjectSecret: process.env.WEBDECOY_SUBJECT_SECRET!,
scopeId: 'support-chat',
route: '/api/chat',
protectionMode: 'observe',
// Keep reporting alive after the response is sent.
waitUntil: (task) => after(() => task),
// On Vercel, the platform sets the client IP. Elsewhere, read it from a
// proxy you control; never trust a client-supplied X-Forwarded-For.
resolveClientIP: (request) => ipAddress(request) ?? null,
// Your own policy, enforced locally even while cloud detection observes.
rules: [
{
id: 'free_plan_input_limit',
mode: 'enforce',
evaluate: (context) => ({
allowed: context.plan === 'paid' || context.inputLength <= 4000,
reason: 'free_plan_input_limit',
status: 403,
}),
},
],
});
return protection;
}A few things worth knowing about these options:
protectionMode: 'observe' records what WebDecoy’s detection would decide without blocking anyone. Start here.rules are yours. This one stops free-plan users from sending prompts over 4,000 characters, and because it is in enforce mode it applies right away, independent of cloud detection.resolveClientIP must return an address your hosting vouches for. ipAddress() reads the header Vercel sets. If you run behind your own proxy, read the header that proxy overwrites and block direct access to your origin. If no trusted IP is available, the SDK skips cloud scoring and reports degraded coverage rather than guessing.route is a fixed template. Never pass a raw URL that could contain user IDs.Here is app/api/chat/route.ts. Your authentication and validation stay first. The model call moves inside the callback you hand to the protection.
import { streamText, consumeStream } from 'ai';
import { openai } from '@ai-sdk/openai';
import { getProtection } from '@/lib/ai-protection';
import { getSession } from '@/lib/auth'; // your existing auth
export const runtime = 'nodejs';
export async function POST(request: Request) {
// 1. Your existing authentication and input validation stay first.
const session = await getSession(request);
if (!session) return Response.json({ error: 'unauthorized' }, { status: 401 });
const body = await request.json().catch(() => null);
const prompt = body?.prompt;
if (typeof prompt !== 'string' || !prompt || prompt.length > 8000)
return Response.json({ error: 'invalid_prompt' }, { status: 400 });
// 2. WebDecoy checks the request. The callback, and the model call inside it,
// only runs if the request is allowed.
return getProtection()(
request,
() =>
streamText({
model: openai('gpt-4o-mini'),
prompt,
abortSignal: request.signal,
}).toUIMessageStreamResponse({ consumeSseStream: consumeStream }),
{ plan: session.plan, inputLength: prompt.length }
);
}getSession stands in for whatever authentication you already have. The third argument is the trusted server context your rule reads: it comes from your session, not from anything the client sent. The streamed response passes through unchanged, and request.signal means a client that disconnects also cancels the model call.
Start your app and send a normal request with a valid session:
curl -N -X POST http://localhost:3000/api/chat \
-H "Authorization: Bearer <your session token>" \
-H "Content-Type: application/json" \
-d '{"prompt":"Hello"}'You get the model’s streamed response as before. Now send a free-plan prompt over 4,000 characters:
{"error":"free_plan_input_limit","request_id":"..."}That comes back as a 403, and the model was never called. Locally there is no trusted client IP, so cloud scoring is skipped and recorded as degraded coverage. That is expected: you will see full detection once you deploy behind your hosting provider.
Before you deploy, you can check how your configuration behaves when a request is allowed, denied, streamed, cancelled, or when WebDecoy is unreachable, without calling a real model. The doctor lives in the SDK repository:
git clone https://github.com/WebDecoy/ai-protection && cd ai-protection && npm ciSave this as check-chat.mjs in that folder, using the same options as your route:
import { checkModelProtection } from './scripts/doctor-model.mjs';
const report = await checkModelProtection({
// The same options as your route. The doctor swaps in a stand-in runtime and
// a stub model, so the key and secret can be placeholders.
protection: {
webdecoyUrl: 'https://ai-protection.webdecoy.com',
webdecoyKey: 'placeholder',
propertyId: process.env.WEBDECOY_PROPERTY_ID,
subjectSecret: 'placeholder-secret-at-least-32-characters',
scopeId: 'support-chat',
route: '/api/chat',
protectionMode: 'observe',
resolveClientIP: () => null,
rules: [
{
id: 'free_plan_input_limit',
mode: 'enforce',
evaluate: (context) => ({
allowed: context.plan === 'paid' || context.inputLength <= 4000,
reason: 'free_plan_input_limit',
status: 403,
}),
},
],
},
allowedContext: { plan: 'free', inputLength: 120 },
deniedContext: { plan: 'free', inputLength: 5000 },
});
for (const check of report.checks) console.log(check.status.padEnd(12), check.name);
console.log(report.passed ? 'All checks passed' : 'Some checks need attention');WEBDECOY_PROPERTY_ID=your-property-id node check-chat.mjsverified allowed
verified local_denial
verified detector_block
verified streaming
verified cancellation
verified outage
All checks passedEach check counts calls to a stub model independently, so “the denied request never reached the model” is measured, not assumed. You may also see a few “report failed” warnings while it runs: the outage check makes the stand-in refuse everything on purpose. The report lists what it did not exercise too, including your own route handler, so it complements a real test rather than replacing one.
Deploy, send a few real requests through your app, then open AI Protection in WebDecoy. The Overview tab shows whether model requests are reporting, how many were allowed and denied, and anything that needs attention. The Model requests tab lists each decision, the checks behind it and whether your code ran.
If the Overview still says “Not set up yet”, check that the environment variables reached your server and that the key belongs to the same property.
Leave observe mode on until the dashboard matches what you expect. Then switch protectionMode to 'enforce' in your code and set the detection mode to Enforce in the AI Protection Setup tab. Both are required. Enforcing cloud detection decisions requires a paid plan. Your own local rules, like the input limit above, already enforce on any plan.
If your AI feature exposes tools over MCP, the Node SDK includes an adapter that checks authentication, your per-tool authorization and optional work limits before each tool call. Tool listing is filtered by scope, every call checks again, and the tenant always comes from your verified caller, never from tool arguments.
The same SDK checkout has a doctor for MCP servers:
node scripts/doctor-mcp.mjs detect /path/to/your/appdetect reads your project without running it and prints the exact setup commands for your files. doctor-mcp.mjs check then runs your own allowed, forbidden, cross-tenant and outage calls against the running server. The MCP setup guide walks through it.
No. The SDK sends request metadata such as the method, your route template, client IP, User-Agent and header names, plus decisions and outcomes. It does not send prompts, model responses, tool arguments, tool results, raw user identities or credentials.
You choose per control. Cloud detection fails open by default, so requests keep working with degraded coverage. A limit you configure to fail closed refuses with a 503 instead. Your own local rules and authorization still apply either way.
You can install the SDK, run in observe mode and use the dashboard on any plan. Enforcing cloud detection decisions requires a paid plan. Local rules you write run in your own code either way.

GEOMCP found 11 small issues across our site. Here are the real findings, the fixes, and how we verified them through an MCP client.
tutorial
Use WebDecoy with React and Express to observe crawler requests, verify reporting, and inspect detections while keeping your site in monitor mode.
tutorial
Set up WebDecoy in Next.js monitor mode, verify your installation, and inspect bot and AI crawler detections without blocking visitors.
tutorialLike this post? Share it with your friends!
Get a personalized demo from our team.