Skip to content

MCP Gated Tools

RunTimeAdmin edited this page Nov 3, 2026 · 28 revisions

MCP Gated Tools

**Referenced Files in This Document** - [server.mjs](https://github.com/RunTimeAdmin/Sigvara/blob/main/examples/mcp-gated-tool/server.mjs) - [README.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/examples/mcp-gated-tool/README.md) - [gate.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/gate.ts) - [verifier.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/verifier.ts) - [challenge.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/challenge.ts) - [agent.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/agent.ts) - [types.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/types.ts) - [reputation-model.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/reputation-model.md) - [SECURITY_AND_MAINNET_READINESS_REVIEW.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/SECURITY_AND_MAINNET_READINESS_REVIEW.md)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. The other direction: sigvara-mcp
  10. Conclusion
  11. Appendices

Introduction

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.

Project Structure

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.ts and mcp-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
Loading

Diagram sources

Section sources

Core Components

  • 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

Architecture Overview

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"
Loading

Diagram sources

Detailed Component Analysis

MCP Server Pattern

The example server:

  • Exposes initialize and tools/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, calls gate.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
Loading

Diagram sources

Section sources

SigvaraGate

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 consume when 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 meetsThreshold semantics.
  • 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 threshold that is not a finite number of 0 or more, or a ttlSeconds or maxScoreAgeSeconds that is not a positive number. The example takes its threshold from Number(process.env.SIGVARA_THRESHOLD || 35), so a setting that is not a number now stops the server at start-up. On alpha.9 it becomes NaN, which admits every active agent that can prove its key. From 1.0.0-alpha.13 the gate also refuses instances: 'multiple' without a nonce store whose consume() is atomic, and from 1.0.0-alpha.14, with profile: 'production', it refuses to start until instances and maxScoreAgeSeconds are 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
Loading

Diagram sources

Section sources

SigvaraVerifier

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, and meetsThreshold.

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
Loading

Diagram sources

Section sources

Challenge Protocol

  • 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"]
Loading

Diagram sources

Section sources

Client-Side Integration Patterns

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 the audience field 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's signChallenge, which signed whatever it was handed, is removed in 1.0.0-alpha.12. signChallengeFor is in 1.0.0-alpha.10 and later; alpha.9 has only signChallenge, so on alpha.9 check the payload's audience yourself before you sign.
  • Integrate with MCP clients by calling the server’s sigvara_challenge tool 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"
Loading

Diagram sources

Section sources

Dependency Analysis

High-level dependencies:

  • The MCP server depends on SigvaraGate.
  • SigvaraGate depends on SigvaraVerifier and a NonceStore.
  • SigvaraVerifier depends 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
Loading

Diagram sources

Section sources

Performance Considerations

  • Prefer minimal chain reads: use getTotalScore instead 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 isActive and getTotalScore with bounded TTLs, but always re-check at admission boundaries.

Troubleshooting Guide

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 by sigvara_challenge and 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 sets maxScoreAgeSeconds. 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 other direction: sigvara-mcp

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

Conclusion

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.

Appendices

Step-by-Step: Creating a Custom MCP Server

  1. Initialize your MCP server with JSON-RPC over stdio.
  2. Import SigvaraGate from the SDK. It ships in @sigvara/protocol-sdk from 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 with profile: 'production'.
  3. Configure:
    • rpcUrl, chainId, and contract addresses for identity, reputation, and staking.
    • threshold from environment.
    • audience as a stable identifier for your deployment.
    • If more than one process serves the tool, instances: 'multiple' and a shared nonceStore with an atomic consume(), such as atomicNonceStore. For a deployment, profile: 'production' with instances and maxScoreAgeSeconds set.
  4. Implement sigvara_challenge:
    • Accept did.
    • Call gate.challenge(did) and store the challenge keyed by DID.
    • Return the payload, expiry, and audience.
  5. 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.
  6. Handle unknown tools and methods with proper JSON-RPC errors.
  7. Run and test via terminal or MCP host.

Section sources

Security Considerations

  • 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 signChallengeFor does.
  • 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; verifySignature does.
  • 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

Reputation Threshold Guidance

  • 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

Monitoring Strategies

  • Track admission decisions and refusal reasons.
  • Measure latency of challenge issuance, signature verification, and chain reads.
  • Alert on spikes in bad_proof or replayed to detect misuse.
  • Monitor RPC health and backoff behavior.
  • Correlate gated tool usage with on-chain reputation changes over epochs.

Overview

Clone this wiki locally