Generate random IDs, QR codes, and hashes, encode and decode values, and geolocate IPs, plus gated network and system diagnostics, via MCP. STDIO or Streamable HTTP.
7 Tools
Overview
A standalone developer-utilities server โ the five always-on tools need no upstream API: generate identifiers, QR codes, and cryptographic digests, encode and decode values, and geolocate a public IP or hostname. Two more tools report diagnostics about the server's own host, gated off by default. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|
toolkit_hash_value | Generate a cryptographic digest (sha256/sha384/sha512/sha1/md5) as hex, base64, or SRI, or constant-time-compare a value against an expected digest. |
toolkit_generate_id | Mint cryptographically-random identifiers โ UUIDv4, UUIDv7, or ULID โ singly or in batches up to 1000. |
toolkit_generate_qr | Encode text or a URL into a QR code as SVG markup, base64 PNG, or a terminal-renderable string. |
toolkit_encode_value | Encode or decode a value across base64, base64url, hex, or URL percent-encoding, in either direction. |
toolkit_geolocate_ip | Resolve a public IP or hostname to geographic and network metadata โ country, city, coordinates, ASN, timezone. |
toolkit_check_network | Gated, off by default. Read-only network diagnostics from the server host โ ping, traceroute, TCP connectivity, or egress-IP detection. |
toolkit_check_system | Gated, off by default. Report a facet of the server host's system state โ OS, CPU, memory, load average, or network interfaces. |
Capability reference
operation: generate (a digest) or compare (timing-safe check via timingSafeEqual); omitted, it compares when expected is sent and generates otherwise. generate sent together with expected is rejected with a typed expected_without_compare error rather than ignoring expected
- Algorithms:
sha256 (default), sha384, and sha512 for security; sha1 and md5 are exposed for checksum and file-integrity compatibility only โ never for passwords or signatures
digestEncoding sets the generated digest's form: hex (lowercase, default), base64, or sri (sha512-<base64>, the npm lockfile integrity and Subresource Integrity form โ sha256/sha384/sha512 only)
expected is accepted as hex, base64, or SRI, recognized by its shape at the algorithm's digest length, so a published checksum is pasted as-is. An SRI value may hold several space-separated entries, as an npm integrity field can: entries for other algorithms are skipped, and it matches when any entry for algorithm does
- Typed errors separate an unrecognizable digest (
expected_malformed), one of the wrong length (expected_length_mismatch, whose hint names the algorithm that length belongs to), and an SRI value with no entry for algorithm (expected_algorithm_mismatch)
inputEncoding reads value as utf8 (default), hex, or base64, so binary blobs skip a decode round-trip
- Canonical use: match a download against a vendor-published checksum or a lockfile integrity entry
type: uuid_v4 (random, default), uuid_v7 (time-ordered, sortable by creation), or ulid (26-char Crockford base32, lexicographically sortable)
count mints a batch up to 1000 in one call; the returned ids array always holds exactly count values
uuid_v7 and ulid batches are monotonic โ strictly increasing even within the same millisecond โ so ids stays in sorted creation order. Ids minted in the same millisecond are separated by random gaps (a 32-bit draw plus one), so no id in a batch is derivable from another; for ulid this departs from the spec's reference +1 increment on purpose
- Read-only โ minting changes nothing โ but never idempotent, so a client won't cache or deduplicate a batch
format: svg (inline markup), png_base64 (raster bytes with mimeType and byteLength), or terminal (plain Unicode half-blocks with no escape codes, fenced in content[])
terminal is drawn for a dark background: light modules, quiet zone included, are blocks and dark modules are spaces
errorCorrection (L/M/Q/H) trades data capacity for damage tolerance; margin sets the quiet-zone width in modules for every format; scale sets pixels per module for svg (its width/height) and png_base64, so both are (modules + 2 ร margin) ร scale px per side
- The returned
version (1โ40) reflects how dense the encoded data is
png_base64 also arrives as an MCP image content block, so a client reading content[] can render the code without decoding structuredContent
- A rendered PNG is bounded at 2048 px per side โ
(modules + 2 ร margin) ร scale โ so a dense symbol at a high scale is rejected with a typed raster_too_large error naming a scale that fits; svg and terminal are unbounded
data is encoded as UTF-8 and capped at 2953 bytes โ the absolute ceiling (version 40, level L, byte mode); a non-ASCII character takes 2โ4 bytes, and usable capacity is lower at higher errorCorrection levels, so over-capacity input is rejected with a typed data_too_large error that reports the payload's byte count
encoding: base64, base64url (URL-safe alphabet), hex, or url (percent-encoding)
operation: encode (raw UTF-8 โ encoding) or decode (encoded value โ bytes)
outputEncoding (decode only) returns the recovered bytes as utf8 text (when omitted), hex, or base64 โ lossless for binary data, and a direct transcode between encodings (a base64 digest to hex, for example). Sent with encode, it is rejected with a typed output_encoding_not_applicable error
- Decode never substitutes replacement characters: bytes that aren't valid UTF-8 return a typed
decode_not_utf8 error pointing at outputEncoding, and a leading byte-order mark is kept
- Whitespace in
hex, base64, and base64url input is ignored, so line-wrapped MIME and PEM bodies decode as-is (without PEM's -----BEGIN/END----- lines, which aren't base64); a url value is taken literally
- Malformed decode input returns a typed
decode_failed error with a recovery hint, not a silent best-effort
- Returns country, region, city, latitude/longitude, ASN, owning organization, and timezone
proxy, hosting, and mobile flag when the address is a proxy/VPN/Tor exit, a datacenter network, or a mobile carrier โ a true on any of them means the coordinates describe infrastructure, not a person. Absent when the provider doesn't report them
- A hostname is DNS-resolved first;
resolvedIp echoes the IP actually located, and source names the answering provider
- SSRF-free โ the server calls the provider, never the target; the resolved IP is re-checked against private ranges, and private/reserved addresses are rejected (they have no public geolocation)
- Best-effort and provider-bounded: VPNs, proxies, mobile NAT, and anycast all defeat IP-to-location, accuracy is city-level at best, and absent fields are reported as unknown rather than invented
- Provider-supplied strings are truncated and stripped of control characters before they reach the response, so registry-controlled text (
org, isp, as) cannot flood or format a model's context
- Keyless by default (ip-api free tier, which is plaintext HTTP โ see
TOOLKIT_GEO_BASE_URL); results are cached in memory by resolved IP under a fixed entry cap
- Gated โ registered only when
TOOLKIT_ENABLE_NET_DIAGNOSTICS=true; absent from tools/list otherwise
mode: ping (ICMP round-trip), traceroute (hop path to the target), connectivity (raw TCP connect to target on port), or public_ip (the host's own egress IP)
- A host that does not respond is reported as
reachable: false โ a valid result, not an error. A ping or traceroute binary that is missing, or that exits without a result, is an unreachable error naming the binary instead
ping reports sent, received, and packetLossPercent alongside the average rttMs; on macOS/BSD an IPv6 target runs ping6/traceroute6
connectivity reports an outcome โ open, refused (nothing listening), timeout (traffic dropped), or unreachable (no route) โ and the connect time as rttMs when open
- Diagnoses the server's own network, so it is useful on a local or self-hosted deployment; reaching a private/reserved/internal target additionally requires
TOOLKIT_ALLOW_PRIVATE_NETWORK=true, which keeps the cloud-metadata endpoint blocked by default
- Gated โ registered only when
TOOLKIT_ENABLE_SYSTEM_INFO=true; absent from tools/list otherwise
what: os, cpu, memory, load, or interfaces
- Exactly one facet object is populated per call, matching
what
memory reports availableBytes (headroom for new allocations) and, when the server runs under a container memory limit, limitBytes; totalBytes, freeBytes, and usedBytes are the raw OS figures, which count reclaimable cache as used and read the host's RAM inside a container
- Describes the host this server runs on, not the calling client โ meaningful on a local or self-hosted deployment; gated off by default because
os and interfaces disclose host topology and version details
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Toolkit-specific:
- Local, pure-compute core โ hashing, ID minting, QR encoding, and value encode/decode run entirely in-process via
node:crypto and the qrcode library; no upstream calls
toolkit_geolocate_ip is the one keyless-by-default network call (ip-api free tier, optional TOOLKIT_GEO_API_KEY); the server calls the provider directly and re-checks the DNS-resolved IP against private ranges, so a hostname can't smuggle a request to an internal address
- Fail-closed gating โ the two host-probing tools (
toolkit_check_network, toolkit_check_system) are absent from tools/list unless explicitly enabled, so a hosted instance exposes no SSRF or info-disclosure surface by default
- Two-tier network gate โ even with diagnostics enabled, private/reserved/loopback/link-local targets (including the cloud-metadata endpoint) stay blocked until a second flag permits them
- Bounded inputs โ QR
data capped at 2953 bytes, rendered PNGs capped at 2048 px per side, ID batches capped at 1000; CSPRNG-backed primitives with constant-time hash comparison via timingSafeEqual
Agent-friendly output:
- Provenance โ geolocation echoes
resolvedIp (the IP actually located) and source (the answering provider); absent upstream fields are reported as unknown, never invented
- Response shaping โ provider-supplied strings (
org, isp, as) are length-bounded and stripped of control characters before they reach the response, so untrusted registry text can't flood or format a model's context
- Discriminated output contracts โ
operation, format, mode, and what fields echo back exactly what ran, with only the branch-relevant fields populated per call; an unreachable host in toolkit_check_network reports reachable: false as valid data, not an error
- Typed failure reasons โ decode, hashing, QR, geolocation, and network failures each carry a structured
reason plus a next-step recovery hint (e.g. decode_not_utf8, expected_malformed, raster_too_large, private_target_blocked); expected sent with generate, and outputEncoding sent with encode, are rejected by name rather than silently ignored
Getting started
Public Hosted Instance
A public instance is available at https://toolkit.caseyjhand.com/mcp โ no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"toolkit-mcp-server": {
"type": "streamable-http",
"url": "https://toolkit.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file. No API key is required โ the five always-on tools and the default keyless geolocation tier work out of the box.
{
"mcpServers": {
"toolkit-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/toolkit-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"toolkit-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/toolkit-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"toolkit-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/toolkit-mcp-server:latest"]
}
}
}
To enable the gated host-probing tools, add their flags to env (or -e for Docker):
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"TOOLKIT_ENABLE_NET_DIAGNOSTICS": "true",
"TOOLKIT_ENABLE_SYSTEM_INFO": "true"
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No API key needed โ geolocation uses the keyless ip-api free tier by default.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/toolkit-mcp-server.git
- Navigate into the directory:
- Install dependencies:
Configuration
Every variable is optional. Server-specific options are validated at startup via the Zod schema in src/config/server-config.ts.
| Variable | Description | Default |
|---|
TOOLKIT_ENABLE_NET_DIAGNOSTICS | Register the gated toolkit_check_network tool. Leave off for hosted or shared deployments. | false |
TOOLKIT_ENABLE_SYSTEM_INFO | Register the gated toolkit_check_system tool. Meaningful only on a local or self-hosted deployment. | false |
TOOLKIT_ALLOW_PRIVATE_NETWORK | With network diagnostics on, permit private/reserved/loopback targets. The second explicit gate. | false |
TOOLKIT_GEO_API_KEY | API key for the geolocation endpoint, if it requires one. | none |
TOOLKIT_GEO_BASE_URL | Base URL for an ip-api-compatible geolocation endpoint. The default is plaintext HTTP โ ip-api's HTTPS endpoint is not part of the keyless free tier and answers 403 SSL unavailable for this endpoint without a paid key. Point this at an HTTPS endpoint (with TOOLKIT_GEO_API_KEY) to encrypt the provider request. | http://ip-api.com |
TOOLKIT_GEO_CACHE_TTL_SECONDS | In-memory geolocation cache TTL in seconds. | 3600 |
TOOLKIT_GEO_RATE_LIMIT_PER_MIN | Max geolocation requests per minute. | 45 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
MCP_SESSION_MODE | auto, stateful, or stateless. No tool requests multi-round input. auto is the framework schema default and resolves to stateful, but with MCP_SESSION_MODE unset the server resolves stateless from createApp({ sessionMode }); an explicit MCP_SESSION_MODE value still overrides it. | stateless |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
OTEL_ENABLED | Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
bun run rebuild
bun run start:stdio
bun run start:http
-
Run checks and tests:
bun run devcheck
bun run test
bun run lint:mcp
Docker
docker build -t toolkit-mcp-server .
docker run --rm -e MCP_TRANSPORT_TYPE=http -p 3010:3010 toolkit-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/toolkit-mcp-server. OpenTelemetry peer dependencies are installed by default โ build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|
src/index.ts | createApp() entry point โ registers tools and inits services, with fail-closed gating for the two host-probing tools. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Seven tools โ five always-on, two gated. |
src/services/geo | Geolocation service โ DNS resolution, provider call with retry/backoff, normalization, in-memory cache. |
src/services/network | Network-diagnostic service plus the shared target validator and private-range classifier. |
tests/ | Unit and integration tests mirroring the src/ structure. |
Development guide
See CLAUDE.md / AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches โ no
try/catch in tool logic
- Use
ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
- Register new tools in the
createApp() arrays in src/index.ts
- The two host-probing tools register behind their enable-flags; the network target gate validates after DNS resolution โ never fabricate a result for an unlocatable or unreachable target
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 โ see LICENSE for details.