Skip to content
All articles
Oct 5, 20267 min read

AI Website Builder vs Custom Website: 2026 Buyer's Guide

AI Website Builder vs Custom Website: 2026 Buyer's Guide

Use an AI website builder to launch fast on a small budget. Choose a custom website when speed, SEO, integrations or owning your code drive revenue.

In 2026 you can type a sentence and get a decent-looking website in minutes. Every major builder has an AI mode, and AI app builders like Lovable and Bolt will generate a full React project. So a business owner asking "why pay for a custom site?" is asking a fair question. I build custom websites for a living, and my honest answer is: often you shouldn't, at least not yet. Here is how to tell which side of the line you're on, and what the builder's pricing page doesn't show.

Key takeaways

  • AI builders are great for validating an idea, simple brochure sites and events. You can be live the same day.
  • The cost is ongoing rent and limits: monthly plans, paid add-ons, design and performance ceilings, and a rebuild if you ever leave.
  • Custom wins when the website is how you make money: lead generation that depends on search, fast pages, custom booking or quoting, integrations with your tools.
  • "AI-generated code" isn't "AI builder". A Lovable or Bolt project gives you real code, but someone still has to secure, host and maintain it.
  • A hybrid often works best: start on a builder, then move to custom when the site's results justify it.

What does an AI website builder actually give you?

There are two different products under the same name:

  • AI site builders (Wix, Squarespace, Framer, Webflow, Hostinger and others with AI modes). You describe your business, the AI writes the copy and picks a layout from the platform's components, and you edit visually. Hosting is included and locked to the platform.
  • AI app builders (Lovable, Bolt, v0, Replit). They generate source code, usually React, that you can export. More flexible, but you're now running a software project.

Both are genuinely good at the first 80%: a clean layout, reasonable copy, mobile-friendly pages. The differences show up in the last 20%, which is where most of the business value sits.

When an AI builder is the right choice

  • You're testing an idea. A landing page to see if anyone signs up doesn't need custom code.
  • Your site is a digital business card. Most visitors find you by name, a referral or social media, not by searching for what you do.
  • Your budget is small and your time is free. A builder plan plus a weekend of your time beats a cheap, badly built custom site.
  • The content changes often and the person changing it isn't technical.

If that's you, use a builder, connect your own domain from day one, and keep copies of your text and images outside the platform.

When a custom website pays for itself

  • Search drives your leads. You need full control of page speed, URL structure, metadata, structured data and internal links. Builders handle the basics, but competitive SEO, and now being quoted in AI answers, rewards control. My guide on getting cited by ChatGPT and AI Overviews explains what that control buys you.
  • Speed costs you money. Heavy builder scripts slow mobile pages, and slow pages lose visitors and conversions.
  • You need real features: quote calculators, client portals, bookings tied to your calendar and CRM, multi-language content, payments with custom logic.
  • Your brand needs to look like nobody else's. AI builders pull from the same component libraries, so sites in the same niche tend to look alike.
  • You want to own the asset. Custom code on your own hosting can be moved, sold or handed to another developer.

The costs builders don't put on the pricing page

Compare the total cost over three years, not the first month. Prices change often, so check current pricing pages, but the structure looks like this:

  • Plan upgrades. E-commerce, memberships, more pages, custom code embeds and removing branding often sit on higher tiers.
  • Add-ons and apps. Forms, bookings, reviews and SEO tools are often separate monthly subscriptions.
  • Per-seat and usage pricing for editors, AI credits and bandwidth.
  • Exit cost. Most site builders don't export a site you can host elsewhere. Leaving means rebuilding the design and moving content by hand.
  • Your time. "Easy" still means hours of tweaking, and that time has a price.

A custom site costs more up front, then mostly hosting and occasional updates. This website runs on a VPS alongside its API; the trade-offs are in Vercel vs self-hosting costs.

launchyear 1.5year 3 total cost AI builder: plans + add-ons custom: build once + hosting lines cross illustrative shape only, not real prices
The shape matters more than the numbers: builder costs grow with every plan upgrade and add-on, while a custom site costs more at launch and little after that. Where the lines cross depends on your plan and your scope.

What about "vibe coding" the site yourself?

AI app builders and coding agents blur the line. You can get a custom React site without hiring anyone. That's a real option if you're technical or curious, but be clear about what you've taken on: hosting, security updates, backups, forms that don't get spammed, and fixing it when something breaks. Many of these projects end up needing a developer anyway, just later. If you go this way, run my vibe coding security checklist and read how to take a Lovable app to production before you send traffic to it.

A quick decision checklist

Count how many of these are true for your business:

  1. More than a third of new customers find you through Google or AI search.
  2. Your site needs a feature that isn't a standard form or booking widget.
  3. You're spending real money on ads that land on your site.
  4. Your competitors' sites are faster or rank above yours for your main services.
  5. You've already outgrown one builder plan or paid for three or more add-ons.
  6. You'd want to move hosts or developers without starting over.

0-1: stay on a builder. 2-3: plan a custom site in the next year and start collecting what works. 4+: your website is a sales channel, and it's worth building properly.

How to brief a custom website so you get what you pay for

  • Start from goals and numbers (leads per month, booking rate), not page lists.
  • Ask for performance targets in writing: Core Web Vitals in the green on mobile.
  • Make sure you own the domain, the code repository and the hosting account.
  • Ask how you'll edit content day to day: a CMS, an admin dashboard, or plain files.
  • Agree what maintenance looks like after launch: updates, backups, monitoring.

Frequently asked questions

Are AI website builders good for SEO?

Good enough for basic local and brand searches. For competitive keywords you'll hit limits on speed, markup and site structure sooner than with a custom build.

Can I move my builder website to a custom site later?

Yes, but expect to rebuild the design and move content by hand. Keep your own copies of text and images, and set up redirects from old URLs so you don't lose search rankings.

How much does a custom website cost in 2026?

It depends on scope: a fast marketing site with a CMS costs far less than one with portals, payments or integrations. Get a fixed-scope quote with performance targets included.

Is a Lovable or Bolt site a custom website?

It's custom code, generated by AI. You own and can host it, but it still needs a review for security, performance and SEO before it's production-ready.

Will AI replace web designers?

AI has replaced the cheap end of the market: basic templates and filler copy. Brand, strategy, conversion and engineering for speed and search still need people. The work has moved up, not away.

Thinking about a custom site?

I design and build fast, custom websites with SEO and AI search built in, on hosting you own. See my website design services or send me your current site for an honest "builder or custom" opinion.

  • AI website builder
  • custom website
  • website design
  • Framer
  • Webflow
  • small business website
  • vibe coding

Keep reading

Docker Bypasses UFW on Ubuntu: Why and How to Fix It
Linux System AdminOct 5, 2026

Docker Bypasses UFW on Ubuntu: Why and How to Fix It

Docker publishes container ports with its own iptables rules, which run before UFW's. Fix it by binding ports to 127.0.0.1 or filtering in DOCKER-USER. This one surprises almost everyone who runs Docker on a VPS. You set up UFW, allow only SSH, HTTP and HTTPS, run ufw status and feel safe. Then you start Postgres or Redis in Docker with -p 5432:5432 , and it's reachable from the whole internet, even though UFW lists no rule for it. Docker's own documentation warns that publishing ports is insecure by default. Here is why it happens, how to check your servers in one minute, and four fixes, starting with the simplest. Key takeaways Published ports skip UFW. Docker rewrites the destination in the nat table and sends the packet through FORWARD , while UFW mostly filters INPUT . By default, -p 5432:5432 listens on all interfaces ( 0.0.0.0 and [::] ). Fix 1 (best): don't publish internal services at all, or publish them on 127.0.0.1 only. Fix 2: set "ip": "127.0.0.1" in daemon.json so localhost becomes the default. Fix 3: filter public traffic in the DOCKER-USER chain. Never set "iptables": false as a fix; it breaks container networking. Why does Docker bypass UFW? UFW is a front end for iptables (on Ubuntu 24.04, iptables itself runs on top of nftables). Its rules live mostly in the INPUT chain, which handles packets addressed to the host itself. When you publish a port, Docker adds a DNAT rule in the nat table's PREROUTING chain. A packet arriving for port 5432 gets its destination rewritten to the container's internal IP before filtering. From then on it isn't addressed to the host any more. It's being routed to another network (the Docker bridge), so it goes through the FORWARD chain, where Docker's own rules accept it. UFW's INPUT rules never see it. Traffic to the host itself passes UFW in the INPUT chain. Traffic to a published container port is rewritten first and routed through FORWARD, where Docker's rules let it in and UFW never gets a say. How to check your server in one minute List what Docker publishes on every interface: docker ps --format 'table {{.Names}}\t{{.Ports}}' | grep -E '0\.0\.0\.0|\[::\]|:::' sudo ss -tlnp | grep docker-proxy Anything showing 0.0.0.0:PORT or [::]:PORT is reachable from outside unless your cloud provider's firewall blocks it. Confirm from a different machine, not from the server itself: nc -zv -w3 your.server.ip 5432 nmap -Pn -p 1-65535 your.server.ip # slower, finds everything Databases, Redis, Elasticsearch, admin panels and internal APIs are the usual findings. Open Redis and Elasticsearch instances are a common way servers get compromised or wiped. Fix 1: Don't publish internal ports (or publish them on localhost) The best fix is not to expose the port at all. Containers on the same Docker network reach each other by service name, so your app container doesn't need Postgres published on the host: # compose.yml services: app: image: my-app ports: - "127.0.0.1:3000:3000" # only Nginx on the host talks to it environment: DATABASE_URL: postgres://app:secret@db:5432/app db: image: postgres:17 # no "ports:" at all. Reachable as db:5432 inside the network volumes: [pgdata:/var/lib/postgresql/data] volumes: pgdata: When the host itself needs the port (Nginx proxying to an app container, or you connecting through an SSH tunnel), publish it on 127.0.0.1 . Then expose only Nginx on 80 and 443, which is the same pattern as my Next.js on a VPS setup . To reach the database from your laptop, use an SSH tunnel instead of opening the port: ssh -N -L 5432:127.0.0.1:5432 you@your.server Docker versions before 28.0.0 had a gap where hosts on the same layer-2 network could still reach ports published on localhost. Another reason to stay up to date. Fix 2: Make localhost the default bind address To protect against someone writing -p 6379:6379 later, change Docker's default host IP for published ports: # /etc/docker/daemon.json { "ip": "127.0.0.1" } sudo systemctl restart docker docker compose up -d --force-recreate # existing containers keep old bindings Now an unqualified -p 6379:6379 binds to localhost, and you have to write 0.0.0.0: on purpose to expose something. This setting covers the default bridge network. For user-defined networks, set com.docker.network.bridge.host_binding_ipv4 on the network or use default-network-opts in daemon.json . Docker's port-publishing docs describe both. Fix 3: Filter in the DOCKER-USER chain Sometimes a container port must be public, but only to some addresses: a database your office connects to, or a metrics endpoint for one monitoring server. Docker leaves an empty DOCKER-USER chain for this, and it runs before Docker's own forwarding rules. Following Docker's documented pattern: # allow replies to established connections sudo iptables -I DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # drop new traffic arriving on the public interface unless it comes from the office IP sudo iptables -I DOCKER-USER 2 -i eth0 ! -s 198.51.100.7 -m conntrack --ctstate NEW -j DROP Replace eth0 with your public interface ( ip route get 1.1.1.1 shows it). Rules added with iptables don't survive a reboot, so persist them with iptables-persistent or add them to /etc/ufw/after.rules . If you'd rather keep everything in UFW syntax, the community ufw-docker helper script automates this pattern. Read it before running it as root. Fix 4: Use the cloud firewall as a second layer Hetzner, DigitalOcean, AWS and most providers offer a network firewall that filters traffic before it reaches your VM. Docker can't touch it. Allow only 22 (ideally from your IPs only), 80 and 443 there, and a mistake on the server stays private. It's not a replacement for Fix 1, but it's cheap insurance. What not to do Don't set "iptables": false in daemon.json . Docker docs warn it's not appropriate for most users. Containers lose outbound internet access and port publishing breaks in confusing ways. Don't rely on ufw deny 5432 . It adds an INPUT rule, and Docker traffic doesn't pass through INPUT. Don't switch firewall backends to fix this. Docker 29 added experimental nftables support, but there's no DOCKER-USER equivalent there yet. Stay on the default iptables backend unless you plan to manage nftables rules yourself. Add it to your hardening routine Put a port check into every server review: docker ps for 0.0.0.0 bindings and an external nmap scan. My Ubuntu 24.04 hardening checklist covers the rest of the server. If you run self-hosted tools like Ollama and Open WebUI or self-hosted S3 storage , check those containers first. Their admin ports are exactly what scanners look for. Frequently asked questions Does Docker bypass UFW for all ports? Only for ports you publish with -p or ports: . Container ports that aren't published aren't reachable from outside. Host services such as SSH and Nginx are still filtered by UFW normally. Is binding to 127.0.0.1 enough? For most setups, yes. Only processes on the host can connect. On Docker versions older than 28.0.0, machines on the same local network segment could still reach localhost-published ports, so keep Docker updated. Does this affect Docker on Ubuntu 24.04 specifically? It affects every Linux distribution where Docker manages iptables, including Ubuntu 22.04, 24.04 and Debian. UFW is just the place people notice it, because it gives a false sense of safety. What about Podman? Rootless Podman publishes ports through a user-space proxy rather than kernel NAT rules, so the behaviour is different. Check with an external scan rather than assuming either way. Should I use ufw-docker? It's a reasonable option if you want to manage container access with UFW-style commands. It's a third-party script that edits your firewall rules, so read it first. For most servers, Fix 1 and Fix 2 are enough. Want your servers checked? I audit and harden Linux servers running Docker: exposed ports, firewall rules, SSH, updates, backups and monitoring, with a written report of what I changed. See my Linux system admin services or book a server review .

Read article →
Slopsquatting: Stop AI From Installing Fake npm Packages
DevOpsOct 5, 2026

Slopsquatting: Stop AI From Installing Fake npm Packages

Slopsquatting is when attackers publish packages under names AI tools hallucinate. Stop it with release-age cooldowns, blocked install scripts and name checks. When an AI coding agent writes npm install for a package that doesn't exist, the command just fails. Unless someone has already registered that name and filled it with malware. That's slopsquatting, and it matters more now that agents run install commands themselves. The fixes are mostly config: a few lines in .npmrc or pnpm-workspace.yaml , and a rule in your agent's instructions. Here is how the attack works and a setup that blocks it on any Node project. Key takeaways Models hallucinate package names, and repeat them. In a large 2025 study, 19.7% of AI-generated code samples referenced at least one package that didn't exist, and many fake names came back on every rerun. Repeatable names are what make the attack work. An attacker registers the name once and waits for agents to install it. Release-age cooldowns (npm min-release-age , pnpm minimumReleaseAge ) block brand-new versions, which is where most malicious packages live. Block dependency install scripts. pnpm 10+ does this by default; npm needs ignore-scripts . Never let an agent add a dependency without review. Put the rule in AGENTS.md and enforce it in CI. What is slopsquatting? The name, coined by Python security developer Seth Larson, mixes "AI slop" with typosquatting. Typosquatting relies on people mistyping lodash . Slopsquatting relies on models confidently inventing a plausible package, such as react-form-guardx (a made-up example), which an attacker then publishes. The research behind it is solid. A USENIX Security 2025 paper by Spracklen and colleagues generated 576,000 code samples across 16 models and found: 19.7% of samples included at least one hallucinated package, with 205,474 unique fake names. Open-source models hallucinated far more often (21.7% on average) than commercial ones (5.2%). When the same prompt was rerun ten times, 43% of hallucinated names appeared every time. Newer frontier models hallucinate less, but even a small rate becomes a real risk once agents run thousands of installs a day without a person reading the command. How the attack plays out An attacker prompts popular models with common tasks and collects the package names they invent. They publish those names on npm or PyPI, often with a convincing README and a working-looking API. A developer, or an agent running unattended, installs the package. A postinstall script runs on the developer's machine or in CI and reads environment variables, .env files, SSH keys and cloud tokens. Step 4 happens during install, before any of your code runs. That's why the most useful defences act at install time. Defence 1: Add a release-age cooldown Most malicious packages and hijacked versions are caught and removed within days. If your package manager refuses versions younger than a few days, you skip that window entirely. npm (11.10 or newer; older versions silently ignore the setting), in the project's .npmrc : # refuse versions published less than 7 days ago min-release-age=7 pnpm (10.16 or newer; the value is in minutes, and pnpm 11 defaults to 1440, one day), in pnpm-workspace.yaml : minimumReleaseAge: 10080 # 7 days When you really need a fresh release, such as an urgent security patch, allow that one version explicitly. Check npm --version on every machine and CI runner. A cooldown that's silently ignored gives you false confidence. With a 7-day cooldown, the package manager refuses any version younger than a week. Most malicious uploads are reported and pulled inside that window, so you never install them. Defence 2: Stop install scripts from running Since version 10, pnpm doesn't run dependencies' preinstall / postinstall scripts unless you allow them. Newer pnpm versions use an allowBuilds map for that; older ones use onlyBuiltDependencies . Bun also skips lifecycle scripts for packages that aren't in its trusted list. With npm, turn them off yourself: # .npmrc ignore-scripts=true A few packages with native code genuinely need their build step. Allow them by name after checking them, rather than allowing everything. This one setting turns most install-time malware into a harmless tarball sitting in node_modules . Defence 3: Check every new name before installing Before installing a package an AI suggested, spend thirty seconds on it: npm view react-form-guardx name time.created repository.url maintainers # 404 = doesn't exist (hallucinated). Created last week with no repo = walk away. Does it exist, and is it the package the docs of the library you're using actually mention? How old is it, how many weekly downloads does it have, and does it link to a real repository? Is there a well-known package that already does this? Prefer it. Watch for cross-ecosystem traps too. A name hallucinated for Python may exist on npm as a completely different, possibly malicious, package. Defence 4: Lock down the agent and CI Rules file: add "Never add or upgrade a dependency without asking. Propose the package name and why." to your AGENTS.md or CLAUDE.md . Hooks: in Claude Code, a PreToolUse hook can block npm install <name> -style commands outright, which is stronger than a rule the model might ignore. CI installs from the lockfile only: npm ci or pnpm install --frozen-lockfile . A dependency added without a reviewed lockfile change fails the build. Least-privilege CI secrets: the install step shouldn't see deploy keys. My GitHub Actions deploy guide uses a separate deploy job and a forced-command SSH key for this reason. Review dependency diffs in pull requests the way you review code. A new package in package.json deserves a comment explaining why. What if a bad package already ran? Remove it, delete node_modules and the lockfile entry, and reinstall from a known-good lockfile. Rotate every secret the machine or CI runner could read: .env values, cloud keys, npm and GitHub tokens, SSH keys. Check for persistence: new SSH keys in authorized_keys , cron jobs, shell profile changes, unexpected GitHub workflows. Report the package to the registry so it gets taken down for everyone. Slopsquatting is one item on a longer list. For the rest, see my vibe coding security checklist . Frequently asked questions What is the difference between typosquatting and slopsquatting? Typosquatting targets humans mistyping a real package name. Slopsquatting targets AI tools that invent names that never existed. The defences overlap, but slopsquatting scales with how much code agents install without supervision. Does npm audit catch slopsquatted packages? Not reliably. npm audit reports known vulnerabilities in the advisory database. A new malicious package has no advisory until someone reports it, which is why cooldowns and blocked scripts matter more. What minimum release age should I use? Three to seven days is a common choice. It skips the window when most malicious versions are caught, while keeping you reasonably current. Allow exceptions for urgent security patches. Are paid AI models safe from hallucinating packages? They hallucinate less, but not never. Commercial models averaged about 5% in the 2025 study, and newer models still produce fake names now and then. Verify any package you haven't heard of. Does this affect Python and pip too? Yes. PyPI has the same problem. Use pinned, hash-checked requirements, review new dependencies, and consider tools that delay or vet new releases. Want your pipeline locked down? I harden Node.js supply chains and CI/CD: lockfile-only installs, cooldowns, script allow-lists, least-privilege secrets and agent guardrails. See my DevOps services or ask for a pipeline review .

Read article →
Build an MCP Server in TypeScript (2026 SDK v2 Guide)
Web DevelopmentOct 5, 2026

Build an MCP Server in TypeScript (2026 SDK v2 Guide)

An MCP server exposes tools and data to AI agents over one standard protocol. In TypeScript, install @modelcontextprotocol/server and register typed tools. Model Context Protocol (MCP) is how Claude Code, Cursor, Codex and most other agents connect to things outside your code: databases, logs, ticket systems, your own APIs. Instead of pasting output into chat, the agent calls a tool and gets real data back. The 2026-07-28 version of the spec is the biggest change since launch. The protocol is now stateless, and the TypeScript SDK v2 was released alongside it. This guide builds a small, useful server on the new SDK and connects it to your agent. Key takeaways MCP servers expose tools, resources and prompts. Tools are functions the agent can call; they're what you'll use most. The 2026-07-28 spec is stateless: no initialize handshake or Mcp-Session-Id header, so HTTP servers scale behind a normal load balancer. TypeScript SDK v2 splits into packages: @modelcontextprotocol/server , @modelcontextprotocol/client , plus adapters for Express, Hono, Fastify and Node. Start with stdio for local tools. Use HTTP only when several people or machines need the same server. Treat tool input as untrusted. Validate it, keep tokens read-only where possible, and never pass raw input into a shell or SQL. What is an MCP server, in plain terms? It's a small program that tells an AI client: "here are the things I can do, with these inputs". The client lists the tools, the model decides when one would help, the client calls it with JSON arguments, and your code returns a result. MCP standardises that conversation, so one server works with every client that speaks MCP. Tools: actions with typed input, such as check_url , search_orders or create_ticket . Resources: read-only data the client can load, such as a config file or a schema. Prompts: reusable prompt templates that users can pick. What changed in the 2026-07-28 spec? Stateless core. Each request carries its own protocol version, client identity and capabilities. No sticky sessions or shared session store. Header routing. HTTP requests carry Mcp-Method and Mcp-Name headers, so gateways can route and authorise without parsing the JSON body. Multi Round-Trip Requests. When a tool needs more input from the user, the server returns an "input required" result and the client retries with the answers, instead of holding a stream open. Extensions. Tasks, MCP Apps and enterprise-managed authorisation are now formal extensions. Deprecations. Roots, sampling, logging and the old HTTP+SSE transport are deprecated, with a long migration window. If you've built an MCP server before, read the official migration guide. If you're starting now, the new SDK hides most of this from you. The agent asks what tools exist, then calls one with typed arguments. Your server does the real work and returns text the model can reason about. Under the 2026 spec each request stands alone, with no session to keep. Step 1: Create the project mkdir site-ops-mcp && cd site-ops-mcp npm init -y npm install @modelcontextprotocol/server zod npm install -D typescript @types/node npx tsc --init --module nodenext --target es2022 --outDir build Set "type": "module" in package.json . The SDK uses Zod for input schemas, imported as zod/v4 . Step 2: Write a server with real tools Here is a small "site ops" server with two tools: one checks a URL, one reads recent errors from your own API with a read-only token. Swap in whatever your team keeps pasting into chat. // src/index.ts import { McpServer } from '@modelcontextprotocol/server'; import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; import * as z from 'zod/v4'; const server = new McpServer({ name: 'site-ops', version: '1.0.0' }); server.registerTool( 'check_url', { description: 'Return the HTTP status and response time of a public URL', inputSchema: z.object({ url: z.url() }), }, async ({ url }) => { const started = Date.now(); const res = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(10_000) }); return { content: [{ type: 'text', text: `${res.status} in ${Date.now() - started} ms` }] }; } ); server.registerTool( 'recent_errors', { description: 'List the latest application errors (read-only)', inputSchema: z.object({ limit: z.number().int().min(1).max(50).default(10) }), }, async ({ limit }) => { const res = await fetch(`${process.env.OPS_API}/errors?limit=${limit}`, { headers: { authorization: `Bearer ${process.env.OPS_READONLY_TOKEN}` }, }); if (!res.ok) return { content: [{ type: 'text', text: `API error ${res.status}` }], isError: true }; return { content: [{ type: 'text', text: JSON.stringify(await res.json(), null, 2) }] }; } ); await server.connect(new StdioServerTransport()); Notes on the design: Descriptions matter. The model picks tools by reading them. Say what the tool returns and when to use it. Tight schemas. z.url() , ranges and defaults stop the model sending nonsense, and the SDK validates input before your handler runs. Return errors as results with isError: true , so the agent can see what went wrong and try something else. Read-only token. The agent can look at errors but can't change anything. Step 3: Test it with MCP Inspector npx tsc npx @modelcontextprotocol/inspector node build/index.js The Inspector opens a local web UI where you can list tools, call them with test input and see the raw messages. Fix descriptions and schemas here before involving a model. Step 4: Connect it to your agent For Claude Code, register the server with the CLI: claude mcp add site-ops \ -e OPS_API=https://api.example.com -e OPS_READONLY_TOKEN=xxxx \ -- node /path/to/site-ops-mcp/build/index.js Cursor and other clients use a JSON config (for Cursor, .cursor/mcp.json ) with the same command and environment variables. Then ask "is the homepage up, and what were the last five errors?" and watch the agent call your tools. To decide which agent to wire this into, see my Claude Code vs Cursor vs Codex comparison , and mention your MCP tools in your AGENTS.md or CLAUDE.md so the agent knows when to use them. Step 5: Go remote with HTTP (when you need it) stdio servers run on each developer's machine. When a whole team, a CI job or a hosted agent needs the same tools, run the server over HTTP. SDK v2 ships adapters for Express, Hono, Fastify and plain Node, and the docs include a full HTTP example. Because the protocol is stateless now, you can run several instances behind Nginx or any load balancer, the same way you'd run a normal API. Deploy it like any other Node service: PM2, Nginx and HTTPS, as in my Next.js VPS deployment guide . A remote server is a public API, so secure it like one: Require OAuth or at least a bearer token. The 2026 spec tightened authorisation, so follow the SDK's auth guide rather than rolling your own. Bind to localhost and expose it only through Nginx with HTTPS. Restrict allowed hosts and origins to prevent DNS rebinding attacks from browsers. Log every tool call with its arguments, and rate-limit expensive tools. Security rules for any MCP server Least privilege. Give the server the narrowest token that does the job. Prefer read-only tools; add write tools one at a time. No shell or SQL from model input. Never build a command or query string from tool arguments. Use parameterised queries and allow-lists. Watch for prompt injection. Data your tool returns (tickets, web pages, emails) can contain instructions aimed at the model. Mark destructive tools so the client asks the user first. Pin and review third-party servers. An MCP server runs with your credentials. Install them like any dependency, and check the package name is real before running it (see slopsquatting ). Frequently asked questions What is the difference between an MCP server and an API? An API is for programs written against it. An MCP server wraps capabilities in a standard format that any AI client can discover and call without custom integration code. Many MCP servers are thin wrappers around existing APIs. Do I need to update my old MCP server for the 2026 spec? Plan for it. The SDK v1 line still gets fixes for a while, but new clients target the stateless spec. Follow the official migration guide when you upgrade to SDK v2. Should I use stdio or HTTP? Use stdio for tools that run on your own machine for one user. Use HTTP for shared, remote or hosted servers that several users or agents need. Can I write an MCP server in Python or Go? Yes. Python, Go and C# have official Tier 1 SDKs that support the 2026 spec, and community SDKs cover other languages. Are MCP servers safe? They're as safe as the permissions you give them. Use narrow tokens, validate input, avoid shell and raw SQL, and require confirmation for anything destructive. Want custom MCP tools for your team? I build and host MCP servers that connect AI agents to your APIs, databases and ops tools, with auth, logging and least-privilege access. See my web development services or tell me what your agents should be able to do .

Read article →