Skip to main content

MCP (Model Context Protocol)

MCP is where agent identity gets concrete: a tool call either runs or it doesn't. @helixid/mcp puts a verification step in front of the tool.

npm install @helixid/mcp

The full picture

HelixID flow. Setup: the HelixID issuer signs a one-time Agent-Authority VC that sets the scope ceiling, and the service provider signs a Delegated Grant VC after the user logs in and consents; both are stored in the agent wallet. Then, in sequence: 1, the agent requests a VP from its wallet; 2, the wallet returns a signed VP; 3, the agent makes its tool call with _helixVP attached; 4, the MCP server verifies signature, expiry, revocation and scopes in-process with no network hop, in about 1 to 6 milliseconds cached; 5, allow executes the tool, deny returns an error; 6, the agent SDK asynchronously logs VP_VERIFIED or VP_REJECTED to the HelixID audit log.

Steps 3 to 5 are what the two exports below do. attachHelixVP produces the call in step 3; helixidMCPMiddleware is step 4, and decides step 5.

Open the full-size version.

Protecting a tool (server side)

import { helixidMCPMiddleware } from '@helixid/mcp';

const requireHelix = helixidMCPMiddleware({
requiredScopes: ['read:orders'],
});

The middleware requires _helixVP on the tool input, verifies it, and enforces the listed scopes. A call arriving without a valid presentation is refused before the tool runs — the action never happens, and the refusal is recorded.

OptionPurpose
requiredScopesScopes the caller must hold for the tool to run
allowSelfSignedAccept self-issued credentials. Defaults to false. Development only.

Presenting a credential (client side)

import { attachHelixVP } from '@helixid/mcp';

const outboundCall = await attachHelixVP(
{ name: 'orders.lookup', input: { orderId: 'ORD-1001' } },
{
walletPassphrase: process.env.WALLET_PASSPHRASE!,
walletFilePath: './agent-wallet.enc',
userDid: 'did:web:user.example.com',
targetService: 'orders',
},
);

The wallet is loaded, a fresh VP is signed locally, and _helixVP is attached to the tool input. Signing is local on every call — nothing is cached, and the private key never leaves the agent.

Seeing the denial path

The clearest way to confirm a tool is actually protected is to call it with no presentation at all:

curl -s http://localhost:7100/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"book_flight","arguments":{"flightId":"BA249","passengerName":"Mallory"}}}'

The protected tool refuses the booking because HelixID did not receive a valid presentation.

Why MCP is a good fit

An MCP server frequently sits at an organizational boundary — the tools belong to one party, the agent to another. That is exactly the case where a bearer token is weakest: it proves possession of a secret and nothing about who authorized this agent to do this thing, and revoking it breaks every other caller holding the same key.

A presentation carries its own proof of delegated authority, is bound to one target service, expires in minutes, and can be revoked individually. See Comparisons.

An MCP server's tools/list doubles as a scope catalog: @helixid/widget's resolveConsentScopes() unions the scopes it advertises with the SP's curated fallback to build the list a user sees on a consent page. Tools and consent scopes stay in sync without a second registry to maintain.

A runnable example

examples/framework-middleware exercises the real MCP adapter against the live API:

pnpm example:middleware:setup
pnpm example:middleware:mcp

Demo B — Travel Concierge is the full version: a real LLM agent calling a protected MCP booking tool, plus revocation and delegation.