
Remote MCP server, MIT licensed
Everything a salesperson could learn about a company before they knock, read live off that company's own website and DNS, scored against your rules, and written into HubSpot.
Checked from your browser: not checked yet not checked yet
Streamable HTTP, stateless, and no authentication at all on the research tools.
claude mcp add --transport http doorknock https://doorknock-eight.vercel.app/mcp
The HubSpot tools need a private app token with crm.objects.companies.read and
.write. It goes on the connection as a header, never as a tool argument.
claude mcp add --transport http doorknock https://doorknock-eight.vercel.app/mcp \ --header "X-HubSpot-Token: pat-na1-..."
Anything that does not speak MCP gets the identical operations at /v1/<operation>,
described by an OpenAPI document generated from the same constants the router uses, so the two
cannot drift. That is what a custom GPT Action or an automation platform points at.
Each one states what it will not do beside what it does, because the refusals are the part that keeps a model honest.
Reads the company's own home page and its public DNS. Returns the marketing and CRM tools loaded on the page, which standard pages exist, who handles their email, and whether their outbound email is protected.
Will not tell you headcount, revenue, ownership or contact names. None of it is knowable from a domain, and it would rather say nothing than guess.Runs the research, then scores it against the profile you pass in, returning a tier with the reasoning attached to every rule: what was expected, what was observed, and the source behind it.
Will not treat a signal it could not determine as a miss. That comes back as indeterminate and blocks the tier, because "we could not tell" and "no" send a lead to different people.MX, SPF and DMARC read in plain words. It keeps two questions apart on purpose: whether your mail will reach them is about their MX records, and whether their own mail lands is about SPF and DMARC.
Will not check DKIM. The selector cannot be discovered from DNS, so checking it means guessing, and a guess that misses looks identical to a domain with no DKIM at all.The vocabulary that profile rules are written against: 54 named facts, what each one means, and what its absence does and does not prove. No network calls.
Will not let a rule quietly reference a signal that does not exist. That comes back labelled as an unknown signal.Calls HubSpot with the token on this connection and reports the portal and scopes. Worth running before the first write of a session: a token with read but not write scope fails at the moment of writing, which is the worst time to find out.
Finds a company by exact domain, so an enrichment does not create a second record for a company already there under a different name.
Creates or updates the company and optionally adds a timeline note. Properties the portal does not have are skipped and listed back rather than failing the whole write, and an update that would change nothing reports "unchanged" instead of touching the record.
Will not write a property you did not name, and will not decide on its own that a lifecycle stage should move.token
parameter on the HubSpot tools, and it is the wrong one: a tool parameter is produced by the
model, which puts the secret in the prompt, the context window, the client's transcript and
any log that records tool calls. Here the token travels on the HTTP request instead, and no
tool schema has a field for it. One adversarial check exists purely to assert that none
ever grows one.Six defects so far. Three came from the adversarial suite on its first run against the deployment, one from the independent verification script, and one was in a test rather than in the server.
Asking it about 169.254.169.254, the cloud metadata address, returned HTTP 200
with a result object. Nothing leaked, because the fetch guard refused to contact it. But
the research call runs the page fetch and the DNS lookups in parallel and the DNS half
carried on, so the caller got a 200 wrapped around a refusal. Defence in depth had
worked and the response contract had not. The address rules now run at the front door.
The first version validated only the first hop, so a public host answering 302 to an internal address would have gone straight through the guard. Hops are now followed by hand, five maximum, each one re-resolved and re-checked before it is taken.
No length check anywhere. DNS itself stops at 253 characters and 63 per label, so anything longer cannot be a real name and is only ever someone testing what the server does with a long string.
Equal-priority MX records came back in a different order on every call, because resolvers rotate them and sorting by priority alone is not a total order. It was caught by the script that re-fetches every fact from a different resolver than the server used. Every one of my own tests compared the server against itself.
The adversarial script truncated every response body to 600 characters before checking it. One assertion looked for a field four kilobytes in, never saw it, and reported the server broken when the server was correct. That cost more time than any of the real defects.
Inherited from the previous server built this way, where it reached production: the tests
built their own Request objects, everything passed, and the deployed platform
passed an IncomingMessage instead. It is why one suite here stands up a real
HTTP server and talks to it over a socket.
My own tests passing proves very little to someone who did not write them.
# Anthropic's own inspector, not my code
npx @modelcontextprotocol/inspector --cli \
https://doorknock-eight.vercel.app/mcp --transport http --method tools/list
# raw JSON-RPC, no client, no session, because it is stateless
curl -s -X POST https://doorknock-eight.vercel.app/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# in the repository: re-fetches every fact from a DIFFERENT resolver
# than the server used, and the page straight from the company
npm run prove -- https://doorknock-eight.vercel.app
Or the version that needs only a browser. Open both of these and compare the DMARC line.