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.
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
- Cloudflare Zero Trust dashboard → Networks → Tunnels & Mesh (renamed from just “Tunnels” — same place) → Create a tunnel → choose Cloudflared.
- 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.
- 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
- Settings → Apps → Cloudflare Tunnel → Configuration, paste the tunnel token directly into the token field.
- Save and restart the add-on, then check its logs for “Registered tunnel connection” to confirm it's actually connected.
Add the public hostname route
- On the tunnel's Hostname routes tab (previously “Public Hostname”) → Add a public hostname.
- Subdomain
snow-mcp, your domain, service typeHTTP, 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
- Go to https://claude.ai/customize/connectors → Add custom connector, name it, and point it at the tunnel's public
/mcpURL. - 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.
- Under Request headers, add one: name
Authorization, value the literal stringBearerfollowed 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. - The token itself comes from the add-on's own Configuration page — the
static_tokenfield, not the ServiceNow login fields next to it. - 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:
| Tool | Type | Permission |
|---|---|---|
| Aggregate Records | Read-only | Always allow |
| Get Ci | Read-only | Always allow |
| Get Ci Relationships | Read-only | Always allow |
| Get Current User | Read-only | Always allow |
| Get Record | Read-only | Always allow |
| Get System Properties | Read-only | Always allow |
| Get Table Schema | Read-only | Always allow |
| Get Update Set | Read-only | Always allow |
| List Ci | Read-only | Always allow |
| List Records | Read-only | Always allow |
| List Update Set Changes | Read-only | Always allow |
| List Update Sets | Read-only | Always allow |
| Create Ci | Write/delete | Needs approval |
| Create Record | Write/delete | Needs approval |
| Create Update Set | Write/delete | Needs approval |
| Delete Record | Write/delete | Needs approval |
| Set Current Update Set | Write/delete | Needs approval |
| Update Ci | Write/delete | Needs approval |
| Update Record | Write/delete | Always 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
- Two independent secrets are easy to conflate: the Cloudflare tunnel token (routes traffic to the NUC) and the MCP static bearer token (authenticates Claude to the server itself). Rotating one has zero effect on the other — regenerating the tunnel token doesn't touch the connector's saved header, and vice versa.
- Any change to the add-on's own code requires the full uninstall/reinstall dance — there's no lighter “reload from git” option in Supervisor.
- Automating secret entry through an API is the wrong instinct here anyway — the manual UI path is what actually works, consistently, every time.
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.