Repository navigation
MCP Gated Tools
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- The other direction: sigvara-mcp
- Conclusion
- Appendices
This document explains how to implement Model Context Protocol (MCP) gated tools using Sigvara’s reputation system. It covers the MCP server pattern shown in the example, the authentication flow based on Ed25519 challenge-response, reputation threshold enforcement, and session management. It also provides guidance for building custom MCP servers that gate tool access by agent reputation, plus security considerations, client integration patterns, scalability advice, and monitoring strategies.
The example MCP server demonstrates a two-step handshake:
- The server issues a signed challenge bound to an audience.
- The client signs the challenge with its registered Ed25519 key and submits it along with a request for a restricted tool.
- The server verifies the signature against the on-chain identity, checks that the agent is active, reads the matured reputation score, and enforces a configurable threshold before executing the tool.
The relevant implementation lives in three places:
- An example MCP server under
examples/mcp-gated-tool. - The Sigvara SDK under
packages/sdk, which implements the gate, verifier, challenge protocol, and types used by the server. -
sigvara-mcp(packages/sdk/src/mcp.tsandmcp-cli.ts, from 1.0.0-alpha.15), the trust server for the other direction, described in The other direction.
graph TB
subgraph "Example MCP Server"
S["examples/mcp-gated-tool/server.mjs"]
R["examples/mcp-gated-tool/README.md"]
end
subgraph "Sigvara SDK"
G["packages/sdk/src/gate.ts"]
V["packages/sdk/src/verifier.ts"]
C["packages/sdk/src/challenge.ts"]
A["packages/sdk/src/agent.ts"]
T["packages/sdk/src/types.ts"]
end
subgraph "On-Chain Layer"
I["SigvaraIdentity"]
RP["SigvaraReputation"]
ST["SigvaraStaking"]
end
S --> G
G --> V
G --> C
A --> C
V --> I
V --> RP
V --> ST
R -. guides .-> S
Diagram sources
Section sources
- MCP server example: Implements JSON-RPC over stdio, exposes tools, performs challenge-response, and gates access by reputation.
- SigvaraGate: Issues challenges, tracks nonces, verifies signatures, checks agent status, reads matured scores, and enforces thresholds.
- SigvaraVerifier: Reads identity, reputation, and staking state from the blockchain; verifies Ed25519 signatures against the registered public key.
- Challenge module: Generates and parses v1/v2 challenge payloads, handles TTL, nonce, and audience binding.
- Agent helper: Provides client-side helpers for generating keys, DID derivation, and signing challenges.
- Types: Shared interfaces for contract addresses, agent status, reputation data, and challenge structures.
Key responsibilities:
- Authentication: Prove control of the on-chain DID via Ed25519 signature over a server-issued challenge.
- Authorization: Enforce that the agent is active and meets the configured reputation threshold.
- Replay protection: Use single-use nonces and audience binding to prevent replay and relay attacks.
- Score semantics: Compare against the matured total score, not raw factors.
Section sources
The gated tool architecture connects an MCP host, the example server, the Sigvara SDK, and the on-chain contracts.
sequenceDiagram
participant Host as "MCP Host"
participant Client as "Agent Client"
participant Server as "MCP Server<br/>server.mjs"
participant Gate as "SigvaraGate<br/>gate.ts"
participant Verifier as "SigvaraVerifier<br/>verifier.ts"
participant Identity as "SigvaraIdentity"
participant Reputation as "SigvaraReputation"
participant Staking as "SigvaraStaking"
Host->>Server : "initialize"
Server-->>Host : "capabilities + serverInfo"
Client->>Server : "tools/call sigvara_challenge{did}"
Server->>Gate : "challenge(did)"
Gate-->>Server : "payload, nonce, expiresAt"
Server-->>Client : "challenge payload"
Client->>Client : "signChallengeFor(payload, audience)"
Client->>Server : "tools/call restricted_search{did, challenge, signature, query}"
Server->>Gate : "admit(did, challenge, signature)"
Gate->>Verifier : "verifySignature(did, payload, signature, ttl, audience)"
Verifier->>Identity : "getIdentity(did)"
Identity-->>Verifier : "ed25519PubKey, status"
Verifier-->>Gate : "signature valid?"
Note over Gate,Verifier : isActive, hasMinimumStake and getTotalScore are sent together
Gate->>Verifier : "isActive(did) and hasMinimumStake(did)"
Verifier->>Identity : "isActive(didHash)"
Identity-->>Verifier : "active?"
Verifier-->>Gate : "active and bonded?"
Gate->>Verifier : "getTotalScore(did)"
Verifier->>Reputation : "getTotalScore(didHash)"
Reputation-->>Verifier : "matured score"
Verifier-->>Gate : "score"
Gate-->>Server : "ok or refusal reason"
Server-->>Client : "tool result or refusal"
Diagram sources
The example server:
- Exposes
initializeandtools/list. - Implements
sigvara_challenge: returns a challenge payload bound to the caller’s DID and the server’s audience. - Implements
restricted_search: validates that the submitted challenge matches one issued by this server, consumes the nonce, callsgate.admit, and only executes the tool if admitted. - Returns structured refusals with actionable reasons.
Configuration:
- Threshold from environment (
SIGVARA_THRESHOLD). - Audience from environment (
SIGVARA_AUDIENCE) to bind responses to this deployment. - Chain ID and RPC URL from environment.
Session management:
- In-memory map of outstanding challenges keyed by DID.
- Single-use per challenge; removed after admission attempt.
flowchart TD
Start(["Request received"]) --> Method{"Method?"}
Method --> |initialize| Init["Return capabilities"]
Method --> |tools/list| List["Return TOOLS"]
Method --> |tools/call| Call["Dispatch by tool name"]
Call --> Challenge{"Tool = sigvara_challenge?"}
Challenge --> |Yes| Issue["Issue challenge via gate.challenge()<br/>Store outstanding"]
Challenge --> |No| Restricted{"Tool = restricted_search?"}
Restricted --> |Yes| Validate["Validate challenge matches issued<br/>Consume nonce"]
Validate --> Admit["gate.admit(did, challenge, signature)"]
Admit --> Decision{"Admitted?"}
Decision --> |No| Refuse["Return structured refusal"]
Decision --> |Yes| Execute["Execute tool logic"]
Execute --> Result["Return result"]
Refuse --> End(["Response sent"])
Result --> End
Challenge --> |No| Unknown["Unknown tool error"]
Init --> End
List --> End
Unknown --> End
Diagram sources
Section sources
Responsibilities:
- Generate challenges with TTL and audience.
- Track nonces with optional distributed store support.
- Verify signatures through SigvaraVerifier.
- Check agent activity and read matured score.
- Enforce threshold and return detailed refusal reasons.
Important behaviors:
- Nonce consumption prefers atomic
consumewhen available; otherwise uses in-process lock to avoid double-spend within a process. - Signature verification order keeps unauthenticated callers from triggering the status and score reads. Checking the proof itself costs one identity read, because the key it is checked against lives on chain, so rate-limit callers where the server is exposed.
- Uses matured total score to match on-chain
meetsThresholdsemantics. - Reads the status, the bond held now and the score together once the proof and the nonce have passed, and abandons any chain read after 30 seconds by default (
timeoutMs). Both are new in 1.0.0-alpha.10: alpha.9 reads the status and the score one after the other, does not read the bond, and waits as long as ethers does, which is five minutes. - Refuses to be constructed with a
thresholdthat is not a finite number of 0 or more, or attlSecondsormaxScoreAgeSecondsthat is not a positive number. The example takes its threshold fromNumber(process.env.SIGVARA_THRESHOLD || 35), so a setting that is not a number now stops the server at start-up. On alpha.9 it becomesNaN, which admits every active agent that can prove its key. From 1.0.0-alpha.13 the gate also refusesinstances: 'multiple'without a nonce store whoseconsume()is atomic, and from 1.0.0-alpha.14, withprofile: 'production', it refuses to start untilinstancesandmaxScoreAgeSecondsare both set. The example sets neither, so it runs as one process with the in-memory store and no age limit.
classDiagram
class SigvaraGate {
-verifier : SigvaraVerifier
-nonces : NonceStore
-ttl : number
-inFlight : Set<string>
+threshold : number
+audience : string
+challenge(did) Challenge
+admit(did, challenge, signature) AdmitResult
-consumeNonce(nonce, expiresAt, nowSeconds) boolean
}
class NonceStore {
<<interface>>
+seen(nonce) boolean
+add(nonce, expiresAt) void
+prune(nowSeconds) void
+consume(nonce, expiresAt) boolean
}
class MemoryNonceStore {
-spent : Map<string, number>
+seen(nonce) boolean
+add(nonce, expiresAt) void
+prune(nowSeconds) void
+consume(nonce, expiresAt) boolean
}
SigvaraGate --> NonceStore : "uses"
MemoryNonceStore ..|> NonceStore
Diagram sources
Section sources
Responsibilities:
- Resolve agent identity and Ed25519 public key.
- Verify challenge signatures with strict bounds checking.
- Query reputation and staking contracts.
- Provide convenience methods like
isActive,getTotalScore, andmeetsThreshold.
Security notes:
- Validates signature length before decoding to avoid expensive operations on malformed input.
- Rejects legacy v1 payloads, which name no audience, and throws if the expected audience is missing.
- Treats malformed inputs as failures rather than exceptions.
flowchart TD
Entry(["verifySignature(did, payload, sig, ttl, audience)"]) --> AudGiven{"audience given?"}
AudGiven --> |No| Throw["throw: expectedAudience is required"]
AudGiven --> |Yes| Parse["parseChallengePayload()"]
Parse --> DidCheck{"parsed.did == did?"}
DidCheck --> |No| Fail["return false"]
DidCheck --> |Yes| VersionCheck{"version == 2 && aud == expected?"}
VersionCheck --> |No| Fail
VersionCheck --> |Yes| Expired["isChallengeExpired(payload, ttl)"]
Expired --> |Yes| Fail
Expired --> |No| LenCheck{"sig length in [64,88]?"}
LenCheck --> |No| Fail
LenCheck --> ReadID["getIdentity(did)"]
ReadID --> ValidID{"registeredAt > 0 && pubKey != 0x0...?"}
ValidID --> |No| Fail
ValidID --> Verify["verifyChallenge(payload, sig, pubKey)"]
Verify --> Exit(["boolean"])
Fail --> Exit
Diagram sources
Section sources
- v2 payloads include prover DID, audience, nonce, and timestamp, separated by newlines to avoid ambiguity with colon-bearing fields.
- TTL controls freshness; expiration is checked against the signed timestamp plus gate TTL.
- Audience binding prevents relay attacks across verifiers.
flowchart TD
Gen["generateChallenge(proverDid, audience, ttl)"] --> Fields["Build lines:<br/>V2 header<br/>did<br/>aud<br/>nonce<br/>ts"]
Fields --> Join["Join with newlines"]
Join --> Return["Return Challenge object"]
Parse["parseChallengePayload(payload)"] --> Detect{"Starts with V2?"}
Detect --> |Yes| Split["Split by newline"]
Split --> Validate["Validate fields and types"]
Validate --> Parsed["Return ParsedChallenge"]
Detect --> |No| Fallback["Parse v1 format"]
Diagram sources
Section sources
Use the SDK’s agent helper to:
- Generate an Ed25519 keypair and derive DID values.
- Issue challenges to peers or sign challenges received from servers.
- Sign a challenge received from a server with
signChallengeFor(payload, audience), passing an audience you already know for that server (pinned in your own configuration, or the origin you connected to), never theaudiencefield in the server's own reply. It signs only a v2 challenge that names this agent as the prover and exactly that audience, so a server cannot hand an agent a payload meant for a different verifier and collect a signature it can use there. The agent'ssignChallenge, which signed whatever it was handed, is removed in 1.0.0-alpha.12.signChallengeForis in 1.0.0-alpha.10 and later; alpha.9 has onlysignChallenge, so on alpha.9 check the payload's audience yourself before you sign. - Integrate with MCP clients by calling the server’s
sigvara_challengetool first, then submitting the signed response to the gated tool.
sequenceDiagram
participant App as "Your Application"
participant Agent as "SigvaraAgent"
participant Server as "MCP Server"
App->>Agent : "generate({agentAddress, chainId})"
App->>Server : "tools/call sigvara_challenge{did}"
Server-->>App : "challenge payload"
App->>Agent : "signChallengeFor(payload, audience)"
App->>Server : "tools/call restricted_search{did, challenge, signature, query}"
Server-->>App : "result or refusal"
Diagram sources
Section sources
High-level dependencies:
- The MCP server depends on
SigvaraGate. -
SigvaraGatedepends onSigvaraVerifierand aNonceStore. -
SigvaraVerifierdepends on Ethereum provider and ABI-wrapped contracts for identity, reputation, and staking. - Challenge utilities are shared between server and client flows.
graph LR
Server["server.mjs"] --> Gate["gate.ts"]
Gate --> Verifier["verifier.ts"]
Gate --> Challenge["challenge.ts"]
Verifier --> Contracts["Identity / Reputation / Staking"]
Client["agent.ts"] --> Challenge
Diagram sources
Section sources
- Prefer minimal chain reads: use
getTotalScoreinstead of full reputation breakdown when only thresholding. - Use a distributed nonce store (e.g., Redis) for multi-instance deployments to ensure single-use semantics across processes.
- Prune expired nonces opportunistically; the memory store prunes from the oldest entries efficiently.
- Cache provider and contract instances where appropriate; the verifier already lazily instantiates contracts.
- For high-throughput MCP environments:
- Rate-limit challenge issuance and admission attempts.
- Batch tool executions after successful admission within a short-lived session token.
- Monitor RPC latency and back off on errors.
- Consider local caching of
isActiveandgetTotalScorewith bounded TTLs, but always re-check at admission boundaries.
Common refusal reasons and remediation:
-
bad_proof: Signature does not verify against the registered Ed25519 key, or was issued for a different agent, audience, or challenge. Ensure the client signs the exact payload returned bysigvara_challengeand that the server’s audience matches the configuration. -
replayed: The same challenge has already been consumed. Request a fresh challenge. -
not_active: The agent is not Active (unbonded, bonded below the current minimum stake, suspended, or slashed). Deposit minimum stake or address slashing status. -
stale_score: Only if the server setsmaxScoreAgeSeconds. The score was written longer ago than the limit, or never; the oracle may have stopped writing. -
below_threshold: The agent is active and authenticated but its matured score is below the configured threshold. Increase reputation by earning payments and attestations.
Operational tips:
- Log structured refusal reasons and scores for diagnostics.
- Validate environment variables: threshold, audience, RPC URL, chain ID.
- Inspect on-chain state via the verifier’s methods to confirm identity status and score.
Section sources
The gated tool above refuses work to callers without standing. sigvara-mcp, in the SDK from 1.0.0-alpha.15, is the other side: an MCP server that lets an agent check another agent on chain before paying or relying on it. Every tool reads, none signs or sends a transaction, and it speaks MCP over stdio with no dependency beyond the SDK's own.
claude mcp add sigvara -- npx -y -p @sigvara/protocol-sdk sigvara-mcp| Tool | What it answers |
|---|---|
sigvara_check_agent |
Registered, Active and holding the minimum stake now; the agent's and operator's addresses on chain; matured and earned score and when it was written; what a slash could take; and anything pending. With threshold, the yes or no SigvaraGate gives, without the proof |
sigvara_score, sigvara_stake, sigvara_identity
|
The detail behind that check |
sigvara_verify_evidence |
Whether the payments the oracle lists are the ones the finalized root on chain committed to, with up to the last 100 covered payments confirmed by the contract's verifyEvidence (the root match covers the rest). Offered only with an evidence source: SIGVARA_EVIDENCE_URL, the primary operator by default on Arc testnet |
sigvara_challenge, sigvara_verify_proof
|
That the agent's key signed a challenge naming you. Offered only with SIGVARA_AUDIENCE. Here the checker calls sigvara_challenge and sends the challenge on to the agent being checked, the reverse of the example server's tool of the same name |
The yes or no applies the gate's order: Active with the minimum stake now, then a score written recently enough if maxScoreAgeSeconds is set, then the matured score against the threshold. A test runs both over 96 chain states. The proof tools need an audience the checked agent can verify about you, such as your own DID or origin, because a proof resists relay only when the agent checks the audience before it signs, which SigvaraAgent.signChallengeFor does. Use an audience no SigvaraGate of yours also uses: a gate admits any fresh challenge signed for its audience, so a proof given to sigvara-mcp would pass that gate within its lifetime. A check names the on-chain agentAddress and operator, the addresses to pay, never one the counterparty supplies. The server reads Arc testnet unless SIGVARA_RPC_URL, SIGVARA_CHAIN_ID and the registry addresses say otherwise (off Arc testnet, sigvara_verify_evidence also needs SIGVARA_EVIDENCE_URL), and refuses to read if the RPC serves another chain.
Section sources
The MCP gated tool pattern combines strong authentication with economic accountability. By requiring agents to prove control of an on-chain identity and meet a maturity-aware reputation threshold, servers can refuse low-standing callers without maintaining allowlists. The SDK provides robust primitives for challenge-response, nonce management, and on-chain verification. Production deployments should harden replay protection with distributed nonces, enforce audience binding, validate all inputs, and monitor usage patterns to detect anomalies and maintain service quality.
- Initialize your MCP server with JSON-RPC over stdio.
- Import
SigvaraGatefrom the SDK. It ships in@sigvara/protocol-sdkfrom 1.0.0-alpha.9; earlier versions do not contain it, so an older install fails on the import. The example's README asks for 1.0.0-alpha.12 or later; 1.0.0-alpha.15 is current, and 1.0.0-alpha.14 was the first withprofile: 'production'. - Configure:
-
rpcUrl,chainId, and contract addresses for identity, reputation, and staking. -
thresholdfrom environment. -
audienceas a stable identifier for your deployment. - If more than one process serves the tool,
instances: 'multiple'and a sharednonceStorewith an atomicconsume(), such asatomicNonceStore. For a deployment,profile: 'production'withinstancesandmaxScoreAgeSecondsset.
-
- Implement
sigvara_challenge:- Accept
did. - Call
gate.challenge(did)and store the challenge keyed by DID. - Return the payload, expiry, and audience.
- Accept
- Implement your gated tool (e.g.,
restricted_search):- Require
did,challenge,signature, and your tool parameters. - Verify the submitted challenge matches the one you issued.
- Consume the nonce and call
gate.admit. - If admitted, execute the tool; otherwise return a structured refusal.
- Require
- Handle unknown tools and methods with proper JSON-RPC errors.
- Run and test via terminal or MCP host.
Section sources
- Always bind challenges to an audience to prevent relay attacks. The binding protects an agent only if the agent checks the audience before it signs, which
signChallengeFordoes. - Never trust wrapper fields for nonce or expiry; parse and validate the signed payload.
- Enforce signature length bounds before decoding.
- Reject legacy v1 payloads, which name no audience;
verifySignaturedoes. - Use a distributed nonce store for fleet deployments, and set
instances: 'multiple'so the gate refuses to start without one. - Validate all inputs and treat malformed data as failures.
- Gate on matured score to align with on-chain semantics.
Section sources
- Understand what numbers mean today: current deployment caps reachable scores due to external trust and propagation being zero.
- Choose thresholds aligned with the intended risk profile; very high thresholds may refuse everyone.
- Remember that new agents start with no score and must build standing through verified payments and attestations.
Section sources
- Track admission decisions and refusal reasons.
- Measure latency of challenge issuance, signature verification, and chain reads.
- Alert on spikes in
bad_prooforreplayedto detect misuse. - Monitor RPC health and backoff behavior.
- Correlate gated tool usage with on-chain reputation changes over epochs.
- Agent Registration and Lifecycle
- Contract Overview and Architecture
- Economic Security Model and Incentives
- Evidence Verification and Merkle Proofs
- Key Management and Rotation
- Maturity Mechanism and Score Release
- Proposal Workflow and Challenge System
- Score Storage and Data Model
- SigvaraIdentity Contract
- SigvaraOracleBond Contract
- SigvaraReputation Contract
- SigvaraStaking Contract
- Slashing Lifecycle and Dispute Resolution
- Stake Management Functions
- Status Management and Control
- Token and Fee Contracts
- Unbonding Period and Withdrawal Security
- Anti-Manipulation and Security Measures
- Community Flag Processing
- Evidence Sources and Verification
- External Trust Integration
- Merkle Proof System
- Oracle Score Computation
- Payment-Backed Attestations
- Scoring Model and Formulas
- Agent Creation and Key Management
- Agent Operations
- Agent Registration Workflow
- Challenge-Response Authentication
- Gate Access Control
- Integration Patterns and Examples
- SDK Overview and Setup
- Verifier Functions