FDKEY verification middleware for MCP servers — gate AI-agent access behind LLM-only puzzles.
Project description
fdkey
FDKEY verification middleware for MCP servers (Python). Gate AI-agent access to your tools behind LLM-only puzzles. Drop-in for any Model Context Protocol server built on the official Python SDK's
FastMCP.
What it does
- Injects two MCP tools into your server:
fdkey_get_challengeandfdkey_submit_challenge. - Wraps the tools you want to protect — they return
fdkey_verification_requireduntil the connecting agent has solved a challenge. - Talks to
https://api.fdkey.comfor challenge issuance and scoring. - Verifies the Ed25519 JWT response offline using the public key
from
https://api.fdkey.com/.well-known/fdkey.json.
Install
pip install fdkey
You also need the official MCP Python SDK:
pip install mcp
Get an API key at app.fdkey.com.
Usage
import os
from mcp.server.fastmcp import FastMCP
from fdkey import with_fdkey
server = FastMCP("my-server")
with_fdkey(
server,
api_key=os.environ["FDKEY_API_KEY"],
protect={
"sensitive_action": {"policy": "each_call"},
"register": {"policy": "once_per_session"},
},
)
@server.tool()
def sensitive_action() -> str:
# Reaches here only after the agent has solved a challenge.
return "verified"
Policies
Per-tool gating policy — passed as {"policy": ...} in the protect map:
"each_call"— verification required for every invocation. Use for irreversible actions (payments, deletes)."once_per_session"— verification required once per connection. Use for account creation, signup-style flows.{"type": "every_minutes", "minutes": N}— verification good for N minutes after the puzzle was solved. Middle ground when "every call" is too aggressive but "once forever" is too loose. The timer does NOT extend on calls — it expiresminutesafter the solve, regardless of activity.
protect={
"delete_account": {"policy": "each_call"},
"register": {"policy": "once_per_session"},
"refresh_dashboard": {"policy": {"type": "every_minutes", "minutes": 15}},
}
Configuration reference
with_fdkey(
server,
api_key="fdk_...", # required
protect={...}, # tool name -> {"policy": ...}
vps_url="https://api.fdkey.com", # override for self-hosted
difficulty="medium", # easy | medium | hard
on_fail="block", # block | allow (puzzle failed)
on_vps_error="allow", # block | allow — see below
inline_challenge=False, # embed puzzle in blocked-tool error
tags={"env": "prod"}, # forwarded to FDKEY for analytics
)
Failure-mode defaults
on_vps_error="allow" is the default — if the FDKEY scoring service is
unreachable, the protected tool falls through to your handler instead of
blocking. We chose this so an FDKEY outage doesn't brick your workflow
(e.g. if we shut down or DNS can't resolve api.fdkey.com). FDKEY is
verification, not gating — your service should still serve traffic when
ours is down. Set on_vps_error="block" if you'd rather drop traffic
than admit unverified callers during an outage.
Reading verification context
from fdkey import get_fdkey_context
@server.tool()
def whoami(ctx) -> str:
fdkey = get_fdkey_context(server, ctx)
if fdkey and fdkey.verified:
# `score` and `tier` are first-class fields on the context.
# `score` is a 0..1 float — today effectively binary
# (1.0 passed / 0.0 failed) but reserved for future capability
# scoring without an API change. `tier` is the VPS-issued
# capability bucket label.
return f"verified (score={fdkey.score}, tier={fdkey.tier})"
return "not verified"
What FDKEY sees
- The MCP
clientInfoyour agent reports (when forwarded by your server). - Challenge IDs, scores, timestamps.
- Your integrator-supplied
tags.
Security notes
- JWT
audis not validated by the SDK. The audience claim binds the JWT to the integrator'svps_users.id, which the SDK doesn't know at verify time. The VPS already bindsaudto the API key that requested the challenge — defense in depth — but in principle, a JWT issued for one FDKEY-protected service could be replayed against a different one within the JWT lifetime (~5 min default). Keep the JWT lifetime short on the VPS side if your threat model includes cross-integrator replay.
What FDKEY does NOT see
- Your prompts.
- Tool inputs or outputs.
- Your end users' identities or PII.
Links
- Marketing + docs: https://fdkey.com
- Dashboard (sign up + manage keys): https://app.fdkey.com
- Source: https://github.com/fdkey/sdks
- Issues: https://github.com/fdkey/sdks/issues
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fdkey-0.1.1.tar.gz.
File metadata
- Download URL: fdkey-0.1.1.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4197e89d01f8906c57bb0a2c2d13e79f0eeb7e3577b8a619cfe2105b0dae8e77
|
|
| MD5 |
5df6b3b78bc0fa06165ab3aa7fb63781
|
|
| BLAKE2b-256 |
96b5de2c9fa3c361becf1ffb044c9a43ea712d699cbfb0b81a671234d30dae6a
|
Provenance
The following attestation bundles were made for fdkey-0.1.1.tar.gz:
Publisher:
publish-python.yml on fdkey/sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fdkey-0.1.1.tar.gz -
Subject digest:
4197e89d01f8906c57bb0a2c2d13e79f0eeb7e3577b8a619cfe2105b0dae8e77 - Sigstore transparency entry: 1499037406
- Sigstore integration time:
-
Permalink:
fdkey/sdks@cb0fde28bfa2ecf04a8a5f5c84a68263912c0f4a -
Branch / Tag:
refs/tags/py-v0.1.1 - Owner: https://github.com/fdkey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@cb0fde28bfa2ecf04a8a5f5c84a68263912c0f4a -
Trigger Event:
push
-
Statement type:
File details
Details for the file fdkey-0.1.1-py3-none-any.whl.
File metadata
- Download URL: fdkey-0.1.1-py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a62507ceb7bd58af1c42d62ea1c738620c42443521fcb24169caae36c2e89c68
|
|
| MD5 |
562312433f12ac7ebde4d5c90a68c4c1
|
|
| BLAKE2b-256 |
34350b1d0146387f53524e07e21ee9fa7632d21f0d9fa241e066a75b2d1fa05d
|
Provenance
The following attestation bundles were made for fdkey-0.1.1-py3-none-any.whl:
Publisher:
publish-python.yml on fdkey/sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fdkey-0.1.1-py3-none-any.whl -
Subject digest:
a62507ceb7bd58af1c42d62ea1c738620c42443521fcb24169caae36c2e89c68 - Sigstore transparency entry: 1499037770
- Sigstore integration time:
-
Permalink:
fdkey/sdks@cb0fde28bfa2ecf04a8a5f5c84a68263912c0f4a -
Branch / Tag:
refs/tags/py-v0.1.1 - Owner: https://github.com/fdkey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@cb0fde28bfa2ecf04a8a5f5c84a68263912c0f4a -
Trigger Event:
push
-
Statement type: