Agentic AI Skills

Self-Hosting a ServiceNow MCP Server on a Home NUC, Wired Into Claude

Claude already ships a built-in ServiceNow connector. I didn't use it. The point of this track is learning the actual mechanics — running a real MCP server, exposing it safely to the internet, and registering it as a remote connector myself — on hardware I own rather than someone else's managed integration.

The pieces: a ServiceNow MCP server packaged as a Home Assistant add-on on my NUC, a Cloudflare Tunnel to get it off my home network without opening a port, and a remote connector registration at Claude so it can actually call the thing. Three separate systems, three separate sets of gotchas.

Part 1 — The Home Assistant Add-on

The add-on lives in a public sibling repo, https://github.com/devdog11/littledevlab-addons — it has to stay public because Home Assistant's Supervisor can't clone a private repo without credentials. It wraps https://github.com/jschuller/mcp-server-servicenow, run via uvx with --transport streamable-http, pointed at a ServiceNow dev/PDI instance over OAuth.

One detail worth calling out because it's easy to conflate: the add-on's Configuration page has two independent auth layers. The username / password / client_id / client_secret fields are the add-on's own login to my ServiceNow instance — Claude never touches them. A separate static_token field is the bearer token that callers (Claude, via the tunnel) have to present to reach the MCP endpoint. Two different problems, two different secrets, and it's worth keeping that distinction straight before you start debugging an auth failure in the wrong place.

Known friction: writing secrets into the add-on's Supervisor Options through the automation API gets blocked by a safety classifier almost every time — enter tokens manually through the Home Assistant UI instead. Also, any change to the add-on's run.sh or config.yaml needs a full uninstall → remove repository → add repository → install cycle, since Supervisor doesn't re-pull git changes on its own.

Part 2 — Cloudflare Tunnel

The add-on listens on the NUC's internal Docker network only — nothing routes to it from the internet by default, which is the point. A Cloudflare Tunnel gets it out without forwarding a port on my home router.

Create the tunnel

  1. Cloudflare Zero Trust dashboard → Networks → Tunnels & Mesh (renamed from just “Tunnels” — same place) → Create a tunnel → choose Cloudflared.
  2. Name it something you're willing to live with — deleting a tunnel later doesn't cleanly free its name, the DNS pointer sticks to it. If you do delete one, give the replacement a new name rather than trying to reuse the old one.
  3. Cloudflare shows an install command with a token embedded. Skip the install command — just copy the tunnel token out of it and keep it in a password manager, not in the repo.

Point the Home Assistant add-on at it

  1. Settings → Apps → Cloudflare Tunnel → Configuration, paste the tunnel token directly into the token field.
  2. Save and restart the add-on, then check its logs for “Registered tunnel connection” to confirm it's actually connected.
Rotating the tunnel token later: there's no obvious “regenerate” button on the tunnel's Overview page. Instead, open the tunnel → Connectors → Add a connector, then scroll past the install commands to Refresh token. That invalidates the old token for new connections but doesn't disconnect a cloudflared instance already running on it — you still have to copy the new token into the add-on's config and restart it, or it keeps running on the old, now-dead token. Confirm the rotation actually took by checking the Connectors tab shows a fresh “last seen” timestamp, not a stale “Healthy” left over from before.

Add the public hostname route

  1. On the tunnel's Hostname routes tab (previously “Public Hostname”) → Add a public hostname.
  2. Subdomain snow-mcp, your domain, service type HTTP, URL pointed at the add-on's internal hostname and port. Save — Cloudflare creates the DNS record automatically.

Test it from outside the house

From a device off the home network — phone on cellular works — hit the public hostname's /mcp path. An auth error at that point is actually good news: it means routing works and only the bearer token check is left. One trap: don't bother testing the add-on's internal hostname directly in a browser — that name only resolves inside Home Assistant Supervisor's own Docker network, so “can't be reached” there is expected and tells you nothing about whether the add-on itself is healthy. Check its Logs tab for a clean Uvicorn running on http://0.0.0.0:8080 instead.

Part 3 — Registering the Remote Connector at Claude

  1. Go to https://claude.ai/customize/connectors → Add custom connector, name it, and point it at the tunnel's public /mcp URL.
  2. Under Authentication, Claude will likely auto-detect “Sign in now” (OAuth) — that's a false positive. This server doesn't do a real OAuth handshake, it just checks a static bearer token. Select No sign-in instead.
  3. Under Request headers, add one: name Authorization, value the literal string Bearer followed by the token. The field does not add the “Bearer ” prefix for you — type it yourself, or every call fails auth for a reason that looks nothing like a missing prefix.
  4. The token itself comes from the add-on's own Configuration page — the static_token field, not the ServiceNow login fields next to it.
  5. Add the connector, then test the connection before moving on. “Couldn't connect to the server” means a routing/tunnel problem; an auth failure means the token pasted into Claude doesn't match the add-on's current static_token — check the add-on's logs for the exact mismatch.

Tool permissions

Once connected, the connector's detail page shows a permissions list for every tool the server exposes, split into read-only and write/delete groups, each individually settable to Always Allow, Needs Approval, or Deny. Here's where I landed:

ToolTypePermission
Aggregate RecordsRead-onlyAlways allow
Get CiRead-onlyAlways allow
Get Ci RelationshipsRead-onlyAlways allow
Get Current UserRead-onlyAlways allow
Get RecordRead-onlyAlways allow
Get System PropertiesRead-onlyAlways allow
Get Table SchemaRead-onlyAlways allow
Get Update SetRead-onlyAlways allow
List CiRead-onlyAlways allow
List RecordsRead-onlyAlways allow
List Update Set ChangesRead-onlyAlways allow
List Update SetsRead-onlyAlways allow
Create CiWrite/deleteNeeds approval
Create RecordWrite/deleteNeeds approval
Create Update SetWrite/deleteNeeds approval
Delete RecordWrite/deleteNeeds approval
Set Current Update SetWrite/deleteNeeds approval
Update CiWrite/deleteNeeds approval
Update RecordWrite/deleteAlways allow

Lookups against a dev instance are low-risk, so the whole read-only group is set to auto-allow — no reason to interrupt myself to approve a List Records call. Everything that creates, deletes, or reassigns state stays gated behind approval, with one exception: Update Record, opened up because the downstream automation I'm building only ever updates existing change requests, never creates or deletes anything.

Lessons Worth Keeping

What's Next

The connector is live and the permission set above is what I'm running with today. Still open: a sync-servicenow command that's meant to pull change requests tagged with a littledevlab category into this site's own change log automatically — blocked on confirming the category value in ServiceNow and a pass over what's safe to publish verbatim. That's the next post in this track.