mcp-stateless-recon
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-stateless-reconCheck DNS and open ports for example.com"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-stateless-recon
A dependency-free, spec-accurate implementation of the stateless MCP 2026-07-28 protocol — with the Tasks extension, multi round-trip elicitation, and a protocol conformance suite. Exposed as a safe network-reconnaissance toolserver.
It does not wrap @modelcontextprotocol/sdk. It implements the wire protocol:
the _meta key grammar, resultType polymorphism, the error-code allocation
rules, the Mcp-Method/Mcp-Name routing headers and their -32020
mismatch rule, the Tasks state machine, and RFC 8707 audience-bound tokens. On
top of that sits a recon toolserver — DNS, TLS, security headers, port scanning
— that exists to exercise the hard parts of the specification with real I/O,
behind an SSRF guard that is tested harder than anything else in the repository.
708 tests, of which 140 are conformance tests named after the specification requirements they assert.
Why this exists
The 2026-07-28 revision of the Model Context Protocol made one change that touches everything else: it deleted the handshake.
There is no initialize, no notifications/initialized, and no
Mcp-Session-Id. Servers MUST NOT infer state from prior requests on the same
connection. Everything a server needs to serve a request now travels with that
request, in params._meta: the protocol version, the client's capabilities, and
optionally who the client claims to be.
That sounds like a simplification. In practice it inverts how you write a server.
Capabilities are per request, not per connection. A client that supports the Tasks extension on one call may not declare it on the next, and the server must answer each one on its own terms. "Does this client support tasks?" is no longer a property you can cache.
Anything that must outlive a request needs an explicit name. Long-running work becomes a
taskIdthe client carries. A half-finished call awaiting an answer becomes a signed continuation token the client carries. Nothing is parked in memory under a socket.Any request can land on any instance. That is the payoff: behind a round-robin load balancer, instances are interchangeable, and a restart drops nothing. It is also the constraint, because a single
Mapkeyed by connection quietly destroys it — and it will pass every test you write on a single machine with a single client.The failure mode is invisible. Stateful leakage does not throw. It works perfectly in development and produces rare, unreproducible bugs in production.
So this project treats statelessness as something to prove, not to assert.
tests/conformance/multi-round-trip.test.ts starts two independent server
instances, begins a multi-round-trip exchange on the first, answers it on the
second, and finishes it on the first. They share no memory — only the key used
to sign continuation tokens.
The revision also added things that are easy to implement carelessly and interesting to implement properly:
a formal
_metakey-name grammar with a reservation rule that hinges on the second label of a prefix (com.mcp.tools/is reserved for MCP,com.example.mcp/is not);resultTypeon every result, where the legal set is the core values plus whatever the extensions this client declared contribute;error-code bands with two codes forbidden outright;
routing headers that a gateway can act on without parsing the body, which only works because disagreement between header and body is now an error;
the Tasks extension, whose terminal statuses must never change and whose tasks must never be handed to a client that cannot poll them;
RFC 8707 resource indicators, the defence against a token minted for one MCP server being replayed at another.
A fuller write-up, with citations, is in docs/PROTOCOL.md.
Related MCP server: spectral
Quickstart
git clone https://github.com/abdulsalam-create/mcp-stateless-recon.git
cd mcp-stateless-recon
npm install
npm test
npm run buildServe over HTTP:
node dist/bin/cli.js --port 8848 --allow '*.example.com'Or over stdio, for a desktop client:
node dist/bin/cli.js --stdioWatch a full annotated transcript — discovery, a plain call, a task, two
elicitation round trips, and a deliberate -32020:
npm run example:clientCLI options
Flag | Default | Meaning |
|
| HTTP port; |
|
| bind address |
| off | serve on stdin/stdout instead of HTTP |
| none | comma-separated allowlist: hostname, |
| off | permit non-routable targets. Local testing only |
|
| hard cap on ports in one |
|
| redirect cap for |
|
| per-connection timeout |
| none | canonical resource URI; enables RFC 8707 bearer-token validation (requires |
| none | trusted authorization server issuer (comma-separated) |
Environment (STDIO takes credentials from here, as the spec directs):
MCP_RECON_ALLOW, MCP_RECON_CONTINUATION_SECRET, MCP_RECON_HS256_SECRET.
Deploying behind a load balancer
Nothing in the server depends on connection identity, so instances are interchangeable and need no sticky sessions. Two caveats, both honest:
Set
MCP_RECON_CONTINUATION_SECRETto the same value on every instance, or a multi-round-trip call started on instance A cannot be finished on B.The bundled
InMemoryTaskStoreis per-instance, so ataskIdcreated on A is not visible on B. TheTaskStoreinterface exists precisely so a shared implementation can be dropped in; see Not implemented.
Tools
Tool | Result shape | What it does | Safety |
|
| A, AAAA, CNAME, MX, TXT and NS records, with every returned address classified against the SSRF range table | allowlist, per-host rate limit |
|
| completes a handshake and reports subject, issuer, SANs, validity window, days to expiry, protocol and cipher, plus findings | connects to the validated IP with the hostname as SNI; reports bad chains rather than trusting them |
|
| grades HSTS, CSP, framing, nosniff, Referrer-Policy, Permissions-Policy and COOP/COEP/CORP; weighted 0-100 score with per-header findings | full guard on the request and every redirect hop; size, time and redirect caps |
|
| bounded TCP connect scan; progress via | requires the Tasks extension; hard port cap; asks for confirmation above 32 ports; bounded concurrency |
|
| composite DNS -> TLS -> headers | sends nothing until the operator confirms authorisation, then scope, over two round trips |
Every tool declares a JSON Schema 2020-12 inputSchema and returns structured
output alongside a text rendering.
The Tasks flow
sequenceDiagram
autonumber
participant C as Client
participant LB as Load balancer
participant S as Any server instance
participant T as Task store
C->>LB: tools/call port.scan<br/>_meta.clientCapabilities.extensions<br/>= { "io.modelcontextprotocol/tasks": {} }
LB->>S: (any instance)
Note over S: capability gate:<br/>no declaration means -32021,<br/>never a task it cannot poll
S->>T: create(task) - committed before responding
S-->>C: result { resultType: "task",<br/>task: { taskId, status: "working", ttlMs, pollIntervalMs } }
Note over S: work continues after the response
loop every pollIntervalMs
C->>LB: tasks/get { taskId }
LB->>S: (possibly a different instance)
S->>T: read
S-->>C: { status: "working", task.statusMessage: "scanned 12/22 ..." }
end
opt scan wider than 32 ports
S->>T: status = input_required + inputRequests
C->>S: tasks/get { taskId }
S-->>C: { status: "input_required", inputRequests: { "large-scan-confirmation": ... } }
C->>S: tasks/update { taskId, inputResponses: { "large-scan-confirmation": { confirmed: true } } }
S-->>C: { resultType: "complete" } (empty ack)
Note over S,T: unknown or already-satisfied keys<br/>are IGNORED, not rejected
end
opt operator changes their mind
C->>S: tasks/cancel { taskId }
S-->>C: { resultType: "complete" } (empty ack)
Note over S: cooperative - the task may still<br/>reach completed or failed
end
S->>T: status = completed (terminal, never changes again)
C->>S: tasks/get { taskId }
S-->>C: { status: "completed", result: { ... the synchronous answer ... } }The MRTR confirmation flow
recon.sweep will not emit a packet until a human says so — twice.
sequenceDiagram
autonumber
participant C as Client
participant A as Instance A
participant B as Instance B
C->>A: tools/call recon.sweep { host }
Note over A: no traffic sent yet
A-->>C: resultType: "input_required"<br/>inputRequests: { "scan-authorisation": elicitation/create }<br/>continuationToken: signed(round 1, args, answers)
Note over C: operator confirms authorisation
C->>B: tools/call recon.sweep<br/>continuationToken + inputResponses<br/>{ "scan-authorisation": { authorised: true, reference } }
Note over B: B has never seen this call before.<br/>The token carries everything.
B-->>C: resultType: "input_required"<br/>inputRequests: { "scan-scope": elicitation/create }<br/>continuationToken: signed(round 2, args, answers so far)
Note over C: operator picks dns / tls / headers
C->>A: tools/call recon.sweep<br/>continuationToken + inputResponses<br/>{ "scan-scope": { checks: ["dns","tls","headers"] } }
A->>A: resolve, then TLS, then headers,<br/>each through the SSRF guard
A-->>C: resultType: "complete"<br/>structuredContent: { authorisation, scope, dns, tls, headers, errors }The continuation token is base64url(payload).base64url(HMAC-SHA256). It is
signed, not encrypted, and never carries secrets. It exists because the
specification says cross-request state must be referenced by an explicit
identifier the client passes each time — and a self-contained token is the only
form of that which survives a round-robin load balancer with no shared store.
Worked examples
Real request/response pairs, taken from npm run example:client. _meta is
shown in full the first time and elided afterwards.
1. A plain tools/call
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: dns.resolve{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "dns.resolve",
"arguments": { "host": "example.com", "recordTypes": ["A", "AAAA"] },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "0.1.0" },
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {},
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
}
}
}{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "DNS records for example.com\nA: [\"104.20.23.154\",\"172.66.147.243\"]\nAAAA: [\"2606:4700:10::6814:179a\",\"2606:4700:10::ac42:93f3\"]"
}
],
"structuredContent": {
"host": "example.com",
"records": {
"A": ["104.20.23.154", "172.66.147.243"],
"AAAA": ["2606:4700:10::6814:179a", "2606:4700:10::ac42:93f3"]
},
"addressClassification": [
{
"address": "104.20.23.154",
"category": "public",
"globallyRoutable": true,
"reason": "globally routable unicast"
},
{ "...": "one entry per resolved address" }
],
"errors": {}
},
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "mcp-stateless-recon",
"title": "Stateless MCP Recon Toolserver",
"version": "0.1.0"
},
"io.modelcontextprotocol/protocolVersion": "2026-07-28"
}
}
}2. A task-returning call, then a poll
POST /mcp Mcp-Method: tools/call Mcp-Name: port.scan{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "port.scan",
"arguments": { "host": "127.0.0.1", "preset": "web", "timeoutMs": 400 },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
}
}
}The response arrives immediately. The task is already committed to the store, so
the taskId is pollable with no grace period.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "task",
"task": {
"taskId": "ba424d57-edee-4c72-b890-6d5979884e49",
"status": "working",
"createdAt": "2026-09-15T14:38:23.634Z",
"ttlMs": 600000,
"pollIntervalMs": 500,
"statusMessage": "resolving 127.0.0.1"
},
"_meta": { "io.modelcontextprotocol/serverInfo": { "...": "..." } }
}
}Poll it. Note Mcp-Name mirrors the taskId, so a gateway can meter per task.
POST /mcp Mcp-Method: tasks/get Mcp-Name: ba424d57-edee-4c72-b890-6d5979884e49{
"jsonrpc": "2.0",
"id": 5,
"method": "tasks/get",
"params": { "taskId": "ba424d57-edee-4c72-b890-6d5979884e49", "_meta": { "...": "..." } }
}{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "complete",
"task": {
"taskId": "ba424d57-edee-4c72-b890-6d5979884e49",
"status": "completed",
"createdAt": "2026-09-15T14:38:23.634Z",
"ttlMs": 600000,
"pollIntervalMs": 500,
"statusMessage": "completed"
},
"status": "completed",
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "TCP connect scan of 127.0.0.1 (127.0.0.1)\nports requested: 8, probed: 8\nopen: none"
}
],
"structuredContent": {
"host": "127.0.0.1",
"address": "127.0.0.1",
"scanned": 8,
"open": [],
"results": [{ "port": 80, "state": "closed", "latencyMs": 3, "reason": "ECONNREFUSED" }],
"cancelled": false
}
}
}
}Two details worth noticing. The status is completed, which is terminal:
a later tasks/cancel will be acknowledged but will not change it. And the
nested result is exactly what a synchronous call would have returned,
resultType and all.
3. An input_required round trip
POST /mcp Mcp-Method: tools/call Mcp-Name: recon.sweep{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "recon.sweep",
"arguments": { "host": "example.com" },
"_meta": { "...": "..." }
}
}No packet has been sent to example.com at this point:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"resultType": "input_required",
"inputRequests": {
"scan-authorisation": {
"method": "elicitation/create",
"params": {
"message": "About to run a reconnaissance sweep against example.com. Confirm you are authorised to test this host - an in-scope bug bounty target, or a system you own or have written permission to assess.",
"requestedSchema": {
"type": "object",
"properties": {
"authorised": {
"type": "boolean",
"description": "I am authorised to test this host."
},
"reference": { "type": "string", "maxLength": 200 }
},
"required": ["authorised"],
"additionalProperties": false
}
}
}
},
"continuationToken": "eyJ0b29sIjoicmVjb24uc3dlZXAiLCJyb3VuZCI6MSwiYXJncyI6eyJob3N0IjoiZXhhbXBsZS5jb20ifSwiYW5zd2VycyI6e30sImV4cGlyZXNBdCI6MTc4OTQ4MzQwNDE0Miwibm9uY2UiOiJGN0FydFgyY0RiWVAifQ.9EPnWj6WiZsmPxddNg98CDL6vrAIQkxrAZc5i8BeiOo"
}
}The client replays the token with the answer. Note that it does not resend
arguments — they travel inside the token.
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "recon.sweep",
"continuationToken": "eyJ0b29sIjoicmVjb24uc3dlZXAiLCJyb3VuZCI6MSwi...",
"inputResponses": {
"scan-authorisation": { "authorised": true, "reference": "example-programme" }
},
"_meta": { "...": "..." }
}
}{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "input_required",
"inputRequests": {
"scan-scope": {
"method": "elicitation/create",
"params": {
"message": "Which checks should the sweep of example.com run?",
"requestedSchema": {
"type": "object",
"properties": {
"checks": {
"type": "array",
"items": { "type": "string", "enum": ["dns", "tls", "headers"] }
}
},
"required": ["checks"]
}
}
}
},
"continuationToken": "eyJ0b29sIjoicmVjb24uc3dlZXAiLCJyb3VuZCI6Miwi..."
}
}One more round and the sweep runs, returning resultType: "complete" with the
authorisation reference preserved in the report for the audit trail.
Answer { "authorised": false } instead and nothing is scanned:
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": 1004,
"message": "the operator did not confirm authorisation to test example.com; no traffic was sent",
"data": { "host": "example.com" }
}
}1004 is a positive integer deliberately: new application codes belong
outside -32768..-32000.
4. A -32020 header mismatch
The body says tools/list; the header claims tools/call. A gateway routing on
the header would have billed and authorised something the server never ran.
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: port.scan{ "jsonrpc": "2.0", "id": 9, "method": "tools/list", "params": { "_meta": { "...": "..." } } }HTTP/1.1 400 Bad Request{
"jsonrpc": "2.0",
"id": 9,
"error": {
"code": -32020,
"message": "HTTP header mcp-method disagrees with the JSON-RPC body",
"data": { "header": "mcp-method", "headerValue": "tools/call", "bodyValue": "tools/list" }
}
}Spec conformance
Each row names a requirement and the test that proves it. Run npm test to
check the column on the right.
Requirement (2026-07-28) | Proven by |
No |
|
Servers MUST NOT issue or require a session id |
|
Servers MUST NOT infer state from prior requests on the same connection |
|
A request omitting |
|
A request omitting |
|
Results SHOULD carry |
|
|
|
A prefix whose second label is |
|
|
|
An unsupported protocol version MUST be |
|
|
|
|
|
|
|
Header protocol version disagreeing with |
|
Every result MUST carry |
|
|
|
Extensions MAY add |
|
|
|
Undefined codes in |
|
New codes MUST NOT be allocated in |
|
|
|
New application codes SHOULD sit outside |
|
Unknown tool / unknown task use |
|
|
|
|
|
A task MUST NOT be returned to a client that did not declare the extension |
|
A task MUST be durably created before its |
|
|
|
|
|
|
|
Responses for unknown keys MUST be ignored, not rejected |
|
Responses for already-satisfied keys MUST be ignored |
|
|
|
|
|
|
|
Subscription state is scoped to the request, not the connection |
|
A stateless follow-up may be served by a different instance |
|
RFC 8707: a token minted for another resource server MUST be rejected |
|
RFC 9207: an untrusted |
|
STDIO takes credentials from the environment, not the wire |
|
JSON Schema |
|
Security controls
This is a tool that opens sockets to hosts named by whoever is talking to the model. That makes it, by construction, an SSRF engine unless it is built not to be. The guard is the most-tested code in the repository.
The target guard (src/security/target-guard.ts)
Scheme allowlist. Only
http:andhttps:.file:,gopher:,ftp:,data:,ws:and URL-embedded credentials are refused before anything else happens.Resolve once, classify everything. The host is resolved and every returned address is classified. The target is refused if any address is non-routable — "any", not "all", because a host that answers with one public and one internal address is the multi-A-record rebinding trick.
Connect to the address that was validated. The socket is pinned through a custom
lookup, so a resolver that changes its answer between the check and the connect cannot move the connection to127.0.0.1. TLS still uses the hostname for SNI and certificate verification, so pinning costs nothing.tests/security/target-guard.test.tsincludes a resolver that answers127.0.0.1once and something else afterwards, and asserts the connection went where it was checked.Re-validate every redirect hop. A public URL that 302s to
http://169.254.169.254/latest/meta-data/is the single most common SSRF payload in the wild; the scheme, the allowlist and the range table are all re-applied on each hop.Allowlist mode.
--allowaccepts hostnames,*.suffixwildcards, IP literals and IPv4 CIDRs, and is enforced before resolution and again after every redirect.Caps. Redirects, response size, per-connection timeout, request body size.
The range table (src/security/ip-classify.ts)
Both address families, parsed from scratch, classified against an ordered rule
table. Blocked: loopback (127/8, ::1), RFC 1918 (10/8, 172.16/12,
192.168/16), CGNAT (100.64/10), link-local (169.254/16, including
169.254.169.254, and fe80::/10), unique-local (fc00::/7), multicast,
broadcast, unspecified, documentation and benchmark ranges, 240/4, deprecated
site-local, Teredo and 6to4 (both of which embed an arbitrary IPv4 endpoint),
and the discard prefix.
IPv4-mapped and NAT64-embedded IPv6 addresses are decomposed and the embedded
IPv4 address is classified, so ::ffff:127.0.0.1 and
64:ff9b::169.254.169.254 are refused. The IPv4 parser rejects leading zeros,
because some resolvers read 0177.0.0.1 as octal.
tests/security/ip-classify.test.ts is a 101-case table that includes the
address immediately either side of every boundary. It is the highest-value test
file here: a regression in it is a live SSRF.
Authorization (src/security/auth/resource-indicator.ts)
RFC 8707 audience binding, checked against this server's own canonical resource
URI after signature and expiry. A token minted for another MCP server is
rejected — that is the token-passthrough / confused-deputy defence, and it is
tested against exact mismatches, look-alike hosts and path prefixes. RFC 9207
issuer validation on top. alg: none and algorithm confusion are rejected
outright rather than "supported".
Everything else
Rate limiting per target host (token bucket) and bounded concurrency inside
port.scan, so a 128-port sweep does not open 128 sockets.A hard port cap that refuses an over-large request rather than silently truncating it — a report that quietly scanned fewer ports than asked is a lie.
Confirmation gates.
recon.sweepsends nothing without two explicit answers;port.scanasks again above 32 ports. Both record the authorisation reference in the report.Continuation tokens are HMAC-signed, expire, and are bound to the tool that issued them.
$refis never dereferenced. The MCP specification forbids automatic dereferencing of schema references, and a network-fetching$refresolver inside a tool server is a textbook SSRF sink. The validator reports$refas unsupported and does not evaluate behind it.clientInfoandserverInfoare never used for security decisions. They are self-reported and unauthenticated, and the code says so where it handles them.
Project layout
src/
protocol/ types, _meta grammar, error codes, JSON-RPC framing
server/ dispatcher, tool registry, server/discover, subscriptions
extensions/
tasks/ state machine, store, RPC methods, runner
transport/ streamable HTTP, routing headers, STDIO
security/ target guard, IP classifier, rate limiting,
continuation tokens, auth/resource-indicator
tools/ the five recon tools + the pure header grader
schema/ JSON Schema 2020-12 subset validator
bin/ CLI
tests/
conformance/ spec requirements, named after the requirement
protocol/ security/ extensions/ tools/ schema/ transport/
docs/PROTOCOL.md the 2026-07-28 changes, with citations
examples/client.ts a minimal spec-correct client that prints the transcriptEvery source file opens with a comment naming what it implements and citing the spec section or RFC.
Engineering
TypeScript with
strict,noUncheckedIndexedAccessandexactOptionalPropertyTypes; ESM; Node >= 20.ESLint flat config on
strictTypeChecked+stylisticTypeChecked, and Prettier. Both pass with no suppressions except one documentedeslint-disablefor Unicode code-point counting in the schema validator.Vitest.
npm run test:coveragefor coverage.CI on Node 20 and 22, which also checks formatting, asserts the runtime dependency count is zero, and runs the example client end to end.
Zero runtime dependencies
$ npm ls --omit=dev --depth=0
mcp-stateless-recon@0.1.0 /path/to/mcp-stateless-recon
`-- (empty)Development dependencies are TypeScript, Vitest, ESLint, typescript-eslint,
Prettier and @types/node. Nothing ships. The protocol, the JSON Schema subset,
the IP parsers, the JWT verification and the HTTP plumbing are all implemented
against node: built-ins. CI fails the build if a runtime dependency appears.
Known limitations / not implemented
Stated plainly, because a portfolio piece that overclaims is worse than one that does less.
No OAuth authorization server. This validates tokens; it does not mint them. There is no
/authorize, no/token, no PKCE flow. RFC 9207 issuer validation is enforced on the server side only — the client half, validatingissbefore redeeming a code, is documented indocs/PROTOCOL.mdbut is by definition a client's job.Only HS256 bearer tokens. No RS256, no JWKS fetching, no key rotation. A real deployment wants asymmetric verification against the authorization server's JWKS.
No Dynamic Client Registration and no CIMD. DCR is deprecated in this revision and CIMD replaces it; neither is implemented, because neither belongs in a resource server.
The JSON Schema validator is a documented subset, not a complete 2020-12 implementation. It covers
type,required,properties,additionalProperties,enum,const,minimum/maximum,exclusiveMinimum/exclusiveMaximum,minLength/maxLength,pattern,items,minItems/maxItemsanduniqueItems.allOf,anyOf,oneOf,not,if/then/else,patternProperties,dependentSchemas,formatassertions and$refresolution are not implemented.validate()returnsunsupportedKeywordsso a caller can fail closed, and a conformance test asserts the bundled tools stay inside the subset.The task store is in-memory and per-instance. A
taskIdcreated on one instance is not visible on another. TheTaskStoreinterface exists so a Redis or Postgres implementation can be dropped in, but none is included. Multi-round-trip calls do work across instances, because their state lives in the signed token rather than the store.Rate limits are per instance. Behind a load balancer each instance enforces its own share; there is no shared budget.
No MCP Apps / UI extension, no resources, no prompts, no completions. The server implements
server/discover,tools/*,tasks/*andsubscriptions/listen.Mcp-Namehandling forprompts/getandresources/readis implemented and tested, but those methods are not served.No JSON-RPC batching. The revision does not require it and it interacts badly with the routing headers, which describe a single method and name.
port.scanis a TCP connect scan only. No SYN scanning, no service fingerprinting, no banner grabbing, no UDP. It answers "is this port accepting connections" and nothing more, on purpose.tls.inspectreports on certificates rather than trusting them. It setsrejectUnauthorized: falseso it can describe expired and self-signed chains, and reportsauthorizedhonestly. Do not reuse that connection for anything.Deprecated capabilities are accepted, not implemented. Roots, Sampling and Logging appear in
ClientCapabilitiesfor the 12-month support window and are otherwise ignored.The 2026-07-28 revision is the only one spoken. There is no backwards-compatibility shim for
2025-06-18or earlier.
Legal and ethical use
This is reconnaissance tooling. Scanning hosts you neither own nor have
permission to test is a criminal offence in most jurisdictions. The design makes
authorisation explicit — recon.sweep will not send a packet until an operator
confirms, port.scan asks again before a wide scan, and --allow pins the
whole process to an agreed scope — but the tool cannot know what you are
authorised to do. That part is yours.
See SECURITY.md for responsible disclosure.
References
License
MIT (c) 2026 Abdulsalam A. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Scans remote MCP servers for protocol, security, and TLS issues; exposes scan tools via MCP.
Free, read-only security scanner for remote MCP servers, before you connect them.
Scan any website or MCP server for agent-trust-readiness; returns a signed, verifiable scorecard.
Remote MCP server: 19 domain-hygiene and email-auth tools (DNS, SPF, DMARC, DKIM, TLS).
Related MCP Servers
- FlicenseBqualityDmaintenanceEnables network scanning and reconnaissance tasks like port scanning, OS detection, DNS brute force, and more through MCP interfaces.158-
- AlicenseNot gradedqualityDmaintenanceMCP server for website reconnaissance. Gives Claude the ability to fingerprint tech stacks, audit security headers, map endpoints, analyze SSL certificates, enumerate DNS records, and scan ports.6,578 npmMIT
- FlicenseNot gradedqualityBmaintenanceRemote MCP server for company tech profiling. It exposes tools for DNS, domain registration, SSL, website fingerprinting, IP/cloud provider, subdomains, and company analysis using public data without API keys.-
- AlicenseNot gradedqualityCmaintenanceMCP server for passive domain reconnaissance and attack surface monitoring. It scans subdomains, security headers, TLS certs, and tech stack, detects subdomain takeovers, and reports changes since the last scan.2MIT