unlock-mcp
Read-only MCP server for inspecting Unlock Protocol on-chain state — no writes, signing, or private keys.
Check membership: Verify whether a wallet holds a valid (non-expired) key for a specific Unlock Protocol lock, returning expiration, tokenId, lock name, and network.
Get lock details: Read a lock's public shape directly from the chain — name, symbol, PublicLock version, key price/currency, expiration duration, max keys, and total keys sold.
List keys: Enumerate all keys a wallet holds across locks on a network via Unlock's subgraph, optionally including expired/cancelled keys, sorted by expiration and capped at 100.
Read-only & live: All values are fetched at call time; malformed or non-lock addresses return clear classification results rather than raw errors.
Network support: Currently configured for Base, with RPC fallback chains and environment-variable overrides for RPC and subgraph endpoints.
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., "@unlock-mcpDoes 0x1234...abcd have a valid key for lock 0xabcd...efgh on Base?"
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.
unlock-mcp
Read-only MCP server exposing Unlock Protocol on-chain state. No private keys, no signing, no write calls — every tool only reads from the chain.
Install
Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"unlock": {
"command": "npx",
"args": ["-y", "unlock-mcp"]
}
}
}Claude Code
claude mcp add unlock -- npx -y unlock-mcpLocal development
If you're working on the server itself, point either of the above at a local checkout instead:
{
"mcpServers": {
"unlock": {
"command": "node",
"args": ["/absolute/path/to/unlock-mcp/dist/index.js"]
}
}
}Related MCP server: Vaultfire MCP Server
Tools
unlock_check_membership
Checks whether a wallet holds a valid (non-expired) key for an Unlock Protocol lock.
Input | Type | Required | Description |
|
| yes | The PublicLock contract to check |
|
| yes | The wallet to check for a key |
| string | no | Defaults to |
| boolean | no | Defaults to |
Returns whether the wallet holds a valid key, and if so its expiration (ISO timestamp and a relative form like "in 2 years"), the tokenId, the lock name, and the network.
The valid/expired verdict comes from the lock's own getHasValidKey, not from
comparing keyExpirationTimestampFor to the host's system clock — that avoids two
agents on different machines (or with a skewed clock) disagreeing about the same key,
and matches what the lock itself enforces. keyExpirationTimestampFor is still read
and returned as expiresAt/expiresRelative to explain the verdict, and locks that
predate getHasValidKey fall back to the local clock comparison. When the contract and
the local comparison disagree, the response includes a verdictDisagreement field —
{ contractVerdict, localVerdict } — instead of silently picking one; it's absent, not
false, when the two agree.
When the verdict falls back to the local clock comparison (getHasValidKey reverts or
returns zero data — typically a lock that predates it), the response includes
verdictSource: "local_clock"; it's absent, not false or present-and-undefined, when
the contract itself produced the verdict. That fallback is a supported answer by
default. Set requireContractVerdict: true to instead fail the call with a clear error
whenever the verdict would have come from the local clock rather than the contract —
useful for callers that would rather get no answer than one the lock itself didn't
attest to. It has no effect when the contract answers normally.
keyExpirationTimestampFor changed signature across PublicLock versions — locks below
publicLockVersion 10 take a key owner address, version 10 and up take a tokenId. The
tool reads publicLockVersion() and calls whichever form the lock actually implements.
Distinct, plain-language results are returned for: a lock address that isn't a contract, a contract that isn't a PublicLock, a wallet with no key, and a wallet with an expired or valid key. These are all normal results, not errors — a definitive answer about the chain isn't a tool failure. The tool only reports an error for cases it genuinely can't answer: an unreachable/rate-limited RPC endpoint, or malformed input. No raw RPC error or revert reason is ever passed through.
Every value is read live from the chain at call time — including name() — so it
reflects current on-chain state, not a block explorer's indexed snapshot (a lock's
name is only set once at deploy time in most explorer UIs, but can change on-chain
afterward; this tool always reads the current value).
unlock_get_lock
Reads a lock's public shape directly from the chain via RPC (not the subgraph — this is point-in-time state, and the RPC path already exists).
Input | Type | Required | Description |
|
| yes | The PublicLock contract to read |
| string | no | Defaults to |
Returns name, symbol, address, network, PublicLock version, key price (amount, raw
value, and currency — the token's own symbol/decimals for an ERC-20, or the chain's
native currency if the lock's tokenAddress() is the zero address), expiration
duration in both seconds and human-readable form, max number of keys, and total keys
sold (totalSupply(), a running counter of every key ever created — not the current
valid supply).
expirationDuration and maxNumberOfKeys each use a max-uint256 sentinel for
"unlimited" (never-expiring keys, or no cap on keys respectively) — the tool reports
those explicitly as unlimited: true rather than surfacing the raw sentinel as a
number.
Lock managers are deliberately not included: PublicLock exposes no enumerable
getter for that role (it uses a plain OpenZeppelin AccessControl role, not
AccessControlEnumerable) — only isLockManager(address), a point check against an
address you'd already have to know. Getting the actual list would mean either
replaying RoleGranted/RoleRevoked logs from deployment (not a cheap RPC read) or
asking the subgraph, which this tool intentionally avoids so it stays pure on-chain
state. Same three classification results as unlock_check_membership apply here for
a bad address: not a contract, not a PublicLock, or a working lock.
unlock_list_keys
Lists every key a wallet holds across locks on a network, via Unlock's subgraph — enumerating a wallet's keys across all locks isn't something RPC can do without already knowing which locks to look at.
Input | Type | Required | Description |
|
| yes | The wallet to list keys for |
| string | no | Defaults to |
| boolean | no | Include expired/cancelled keys too (default |
Returns, per key: lock address, lock name, tokenId, expiration (ISO timestamp, or
"never" for a lifetime key), and whether it's currently valid. Results are sorted by
expiration descending and capped at 100, with a note in the response if the cap was
hit. A wallet holding no keys is a normal empty result, not an error.
Networks
Chains are configured as data in src/networks.ts — adding one is a new object, not a
code change. Only Base is configured today.
The Base Unlock factory address was cross-checked against
unlock-protocol/unlock
(packages/networks/src/networks/base.ts) directly, since @unlock-protocol/networks
on npm hasn't been republished since 0.0.25 (Dec 2024). As of this check, the two
still agree — no divergence found.
RPC endpoints
Each network has a primary RPC and one or more fallbacks, tried in order — a failure
on one (timeout, connection error, rate limit) automatically retries on the next. Base
defaults to Unlock's own public RPC (rpc.unlock-protocol.com), falling back to the
public Base RPC (mainnet.base.org).
To override the endpoints tried for a given network, set
UNLOCK_MCP_RPC_URL_<NETWORK> (uppercased network name), e.g.:
UNLOCK_MCP_RPC_URL_BASE=https://your-rpc.example.comThe override is tried first; the built-in defaults remain as fallbacks behind it.
Subgraph endpoint
unlock_list_keys reads from Unlock's public subgraph, one endpoint per network
(no fallback chain, since there's only one). To override it for a given network, set
UNLOCK_MCP_SUBGRAPH_URL_<NETWORK> (uppercased network name), e.g.:
UNLOCK_MCP_SUBGRAPH_URL_BASE=https://your-subgraph.example.comDevelopment
npm install
npm run build
npm testAvailable Tools
3 toolsunlock_check_membershipCheck Unlock Protocol membershipA
Check whether a wallet holds a valid (non-expired) key for a specific Unlock Protocol lock, read-only. Returns status (valid, expired, no_key, not_a_contract, or not_a_lock), lock name, tokenId, and expiration. Use this when you already know the lock address to check; use unlock_list_keys instead when you need every lock a wallet holds and don't know the addresses up front. The verdict normally comes from the lock's own getHasValidKey; on locks that predate it, the call falls back to comparing the read expiration to the local clock and reports verdictSource: "local_clock" (absent when the contract answered). Set requireContractVerdict to fail instead of accepting that fallback.
| Name | Required | Description | Default |
|---|---|---|---|
| network | No | Network name (defaults to "base") | base |
| lockAddress | Yes | Address of the PublicLock contract to check | |
| walletAddress | Yes | Wallet address to check for a valid key | |
| requireContractVerdict | No | When true, fail with an error instead of falling back to comparing keyExpirationTimestampFor to the local clock when the lock's own getHasValidKey can't be read (e.g. it predates getHasValidKey). That fallback is a supported valid/expired answer by default (false) — set this to require a verdict sourced from the contract itself. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden, and it delivers: it declares the operation as read-only, describes return statuses, and explains the getHasValidKey vs. local_clock fallback behavior plus the requireContractVerdict escape hatch. This is unusually transparent about how the verdict is produced.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but every sentence earns its place: what it does, what it returns, when to use it, and the fallback behavior. It front-loads the core purpose before diving into edge-case mechanics.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Even though there is no output schema, the description enumerates the returned status values and fields. It also explains the fallback mechanism and the parameter that disables it, making the tool callable and interpretable without additional context.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the parameters are already documented. The description adds value by explaining what requireContractVerdict actually controls and how verdictSource relates to the fallback, going beyond the schema's description.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a precise action: check whether a wallet holds a valid key for a specific Unlock Protocol lock. It also differentiates itself from the sibling unlock_list_keys by noting the addressing condition, so an agent can distinguish the tools without opening schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly says to use this tool when you already know the lock address, and to use unlock_list_keys when you need every lock a wallet holds and don't know the addresses up front. This gives clear selection criteria against the most likely alternative.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unlock_get_lockGet Unlock Protocol lock detailsA
Read an Unlock Protocol lock's public shape from the chain, read-only: name, symbol, key price/currency, expiration duration, max keys, and total keys sold.
| Name | Required | Description | Default |
|---|---|---|---|
| network | No | Network name (defaults to "base") | base |
| lockAddress | Yes | Address of the PublicLock contract to read |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure. It explicitly states 'read-only' and 'public shape', making clear that the call has no side effects and requires no sensitive access. It also indicates the data is fetched 'from the chain'. It does not describe error behavior or response formatting, but for a simple read operation this is adequate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One tight sentence that leads with the verb, states the read-only nature, and uses a coherent colon-separated list of the lock attributes. No ffiller or redundant wording.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple two-parameter read tool with no output schema, the description covers the purpose, the read-only behavior, and the specific fields returned. It doesn't mention the network default (though the schema does) or the exact response envelope, but the listed fields sufficiently set expectations. Minor gaps only.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and both parameters already have descriptions ('Network name (defaults to "base")' and 'Address of the PublicLock contract to read'). The description adds context about the lock fields that the address will resolve to, but it does not fundamentally enhance the meaning of the parameters themselves beyond the schema. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Read'), a clear resource ('Unlock Protocol lock's public shape from the chain'), and a precise scope ('read-only') with an explicit list of returned fields (name, symbol, key price/currency, expiration duration, max keys, total keys sold). This clearly differentiates the tool from its siblings, which handle membership checks and key listing.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies its use case: whenever an agent needs a lock's public on-chain details. However, it does not explicitly name when-to-use conditions or mention the sibling tools, so there is no direct guidance on choosing between unlock_get_lock, unlock_check_membership, and unlock_list_keys.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
unlock_list_keysList Unlock Protocol keys held by a walletA
List every Unlock Protocol key a wallet holds across locks on a network, via the Unlock subgraph, read-only. Returns each key's lock address and name, tokenId, expiration, and validity, sorted by expiration descending and capped at 100 results with a truncation flag. Use this when you need every membership a wallet holds and don't know the lock addresses up front; use unlock_check_membership instead when you already know which lock to check.
| Name | Required | Description | Default |
|---|---|---|---|
| network | No | Network name (defaults to "base") | base |
| walletAddress | Yes | Wallet address to list Unlock Protocol keys for | |
| includeExpired | No | Include expired and cancelled keys, not just currently valid ones (default false) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden, and it delivers: it says the operation is read-only, uses the Unlock subgraph, returns specific fields, sorts by expiration descending, caps at 100 results, and includes a truncation flag. These are meaningful behavioral details beyond the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences with no filler: the action and scope come first, then return behavior and limits, then usage guidance with an alternative. Every sentence contributes distinct, useful information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema and no annotations, but the description compensates by disclosing return fields, sorting, cap, truncation flag, read-only nature, and the intended use case. This is complete for an agent to select and invoke the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all three parameters already have descriptions. The tool description does not add much parameter-specific meaning beyond restating the general purpose, which is acceptable but not an enhancement. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Uses a specific verb ('List') with a clear resource ('Unlock Protocol keys a wallet holds across locks on a network') and explicitly distinguishes itself from unlock_check_membership. The description states both scope and what results include, so an agent can identify the tool without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to use this tool ('when you need every membership a wallet holds and don't know the lock addresses up front') and names the alternative ('use unlock_check_membership instead when you already know which lock to check'). This gives an agent direct routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v0.4.0- Changed
unlock_check_membership1 field changed- added
Input schema / properties / requireContractVerdictAdded value: +{ + "default": false, + "description": "When true, fail with an error instead of falling back to comparing keyExpirationTimestampFor to the local clock when the lock's own getHasValidKey can't be read (e.g. it predates getHasValidKey). That fallback is a supported valid/expired answer by default (false) — set this to require a verdict sourced from the contract itself.", + "type": "boolean" +}
3 tool updates
v0.2.0- Changed
unlock_check_membership2 fields changed- removed
Input schema / properties / walletAddress / $refRemoved value: -"#/properties/lockAddress" - added
Input schema / properties / walletAddress / typeAdded value: +"string"
- Added
unlock_get_lock - Added
unlock_list_keys
1 tool update
v0.1.0- First observed
unlock_check_membership
TDQS
Scored across 3 tools
Each tool has a clearly distinct role: check a known lock, read lock metadata, or enumerate all keys for a wallet. unlock_check_membership and unlock_list_keys overlap somewhat but their descriptions explicitly define when to use each.
All tools share the unlock_ prefix and follow a consistent verb_noun pattern: check_membership, get_lock, list_keys. The naming is predictable and easy to route.
Three tools is a well-scoped size for a read-only Unlock Protocol membership and lock information server. Each tool covers a distinct, necessary operation without bloat.
The core read-only workflows are covered: membership verification, wallet-wide key listing, and lock details. The main gap is that unlock_list_keys caps at 100 results with no pagination, so very large key sets cannot be fully enumerated.
Maintenance
Related MCP Connectors
Verify on-chain lifecycle state and provenance of NFC-tagged physical products.
Provide AI agents and automation tools with contextual access to blockchain data including balance…
Read-only on-chain intelligence for AI agents on Base: balances, tokens, gas, tx status.
Read-only on-chain intelligence for AI agents on Base: balances, tokens, gas, tx status.
Related MCP Servers
FlicenseAqualityBmaintenanceA server that exposes blockchain data (balances, tokens, NFTs, contract metadata) via the Model Context Protocol, enabling AI agents and tools to access and analyze blockchain information contextually.1847-- AlicenseAqualityCmaintenanceProvides on-chain trust verification for AI agents through the Vaultfire Protocol, enabling read-only trust checks and write operations like agent registration and partnership bonds across multiple blockchains.712 npm1MIT

Noves MCP Serverofficial
AlicenseBqualityDmaintenanceEnables AI assistants to access and explain blockchain data across 100+ networks using natural language, without requiring authentication.912 npmMIT- AlicenseAqualityBmaintenanceThe only MCP server that gives AI agents native access to Stacks, Celo, and Base — from a single npm package.551 npm1MIT