napplet.
← All NAPs

NAP-CVM / PR #31

ContextVM Bridge

Open PR

Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.

NAP-CVM

ContextVM Bridge

draft

NAP ID: NAP-CVM Domain: cvm Depends:

  • value — layering · optional — the shell MAY reuse its value payment backend for supported ContextVM payment methods; no value wire types cross the cvm seam. Web binding (NIP-5D): window.napplet.cvm; domain presence signals availability.

Description

NAP-CVM provides napplets with native access to ContextVM servers through the shell. ContextVM transports Model Context Protocol (MCP) messages over Nostr relays using public-key addressing and encrypted relay messages. A napplet could build this itself on top of NAP-RELAY, but doing so would force every napplet to implement ContextVM discovery, initialization, JSON-RPC correlation, relay routing, encryption, signing, and optional payment handling. NAP-CVM moves that legwork into the runtime.

The napplet supplies the server identity and the MCP operation it wants. The shell handles ContextVM transport details, user policy, signing, encryption, relay access, and value-transfer prompts.

API Surface

Operation Parameters Result Wire
discover optional query (CvmDiscoverQuery) list of CvmServer cvm.discover / cvm.discover.result
request server (CvmServerRef), message (McpMessage), optional options (CvmRequestOptions) McpMessage or null cvm.request / cvm.request.result
listTools server (CvmServerRef), optional options (CvmRequestOptions) list of McpTool wrapper over request
callTool server (CvmServerRef), name (text), optional args (JsonObject), optional options (CvmRequestOptions) McpToolResult wrapper over request
listResources server (CvmServerRef), optional options (CvmRequestOptions) list of McpResource wrapper over request
readResource server (CvmServerRef), uri (text), optional options (CvmRequestOptions) list of McpResourceContent wrapper over request
close server (CvmServerRef) none cvm.close / cvm.close.result
onEvent handler for CvmEvent Subscription handle cvm.event
registry.list optional query (CvmRegistryQuery) list of CvmRegistryEntry cvm.registry.list / result
registry.has selector (CvmRegistrySelector), optional options (CvmRegistryOptions) boolean cvm.registry.has / result
registry.describe selector (CvmRegistrySelector), optional options (CvmRegistryOptions) CvmRegistryEntry cvm.registry.describe / result
registry.call selector (CvmRegistrySelector), tool (text), optional args (JsonObject), optional options (CvmRegistryCallOptions) McpToolResult cvm.registry.call / result

Schemas

Primitive references:

Name Meaning
JsonObject JSON object.
JsonSchema JSON Schema object.
McpTool MCP tool object.
McpToolResult MCP tool result object.
McpResource MCP resource object.
McpResourceContent MCP resource content object.
Subscription Handle whose close() operation removes the local event handler; it does not close a server session.

Enumerations:

Name Values
CvmRequestOptions.payment deny, prompt, allow
CvmRegistryCallOptions.payment deny, prompt, allow
CvmRegistryCallOptions.cache default, reload, no-store
McpMessage.jsonrpc "2.0"

CvmServerRef:

Field Type Required Description
pubkey text yes ContextVM server pubkey as 64 lowercase hexadecimal characters.
relays list of text no Relay hints for the server.

CvmDiscoverQuery:

Field Type Required Description
search text no Search string.
kinds list of integer no Nonnegative announcement kinds to inspect; omitted means all supported announcement kinds.
relays list of text no Relay hints for discovery.
limit integer no Positive maximum number of servers to return.

CvmServer:

Field Type Required Description
pubkey text yes ContextVM server pubkey.
relays list of text no Relay hints for the server.
name text no Human-readable server name.
description text no Human-readable server description.
capabilities list of text no MCP server capability names, such as tools, resources, or prompts; never tool names or common-schema hashes. Omit when unknown.
paymentRequired boolean no Advisory shell-derived pricing hint; see Payment handling. Omit when unknown.

CvmRequestOptions:

Field Type Required Description
timeoutMs integer no Positive timeout in milliseconds, at most 300000; default 30000. See Timeouts.
initialize boolean no Whether the shell should initialize the MCP session if needed.
payment deny, prompt, or allow no Requested payment behavior; default prompt. Never grants spending authority. See Payment handling.

CvmRegistryQuery:

Field Type Required Description
search text no Search string.
family text no Shell-local family label.
schemaHash text no Required CEP-15 tool schema hash; portable matching key.
category text no Best-effort CEP-15 t category hint.
limit integer no Positive maximum number of entries to return.

CvmRegistrySelector:

Field Type Required Description
schemaHash text no CEP-15 tool identity; authoritative when present.
family text no Shell-local label used only when schemaHash is absent.

At least one selector field MUST be present. A supplied schemaHash MUST be a 64-character lowercase hexadecimal SHA-256 digest. A family label MUST be nonempty. Hash and label value constraints also apply when those fields are supplied in registry queries.

CvmRegistryOptions:

Field Type Required Description
server CvmServerRef no Required provider server.

CvmRegistryCallOptions:

Field Type Required Description
server CvmServerRef no Required provider server.
timeoutMs integer no Positive timeout in milliseconds, at most 300000; default 30000. See Timeouts.
initialize boolean no Whether the shell should initialize the MCP session if needed.
payment deny, prompt, or allow no Requested payment behavior; default prompt. Never grants spending authority. See Payment handling.
cache default, reload, or no-store no Registry cache policy.

CvmRegistryEntry:

Field Type Required Description
family text no Shell-local display label; not portable identity.
description text no Human-readable family description.
schemaHashes list of text yes Distinct CEP-15 hashes of all entries in tools; order has no meaning.
selected CvmServerRef no Selected provider implementing the complete tool set, if verified and reachable.
providers list of CvmServerRef no Candidate servers advertising the complete tool set; not proof of liveness.
tools list of CvmRegistryTool yes Nonempty tool set described by this entry.

CvmRegistryTool:

Field Type Required Description
name text yes Tool name.
description text no Human-readable tool description.
inputSchema JsonSchema yes Tool input schema.
outputSchema JsonSchema no Tool output schema.
schemaHash text yes Verified CEP-15 hash for this tool, including its name.

McpMessage:

Field Type Required Description
jsonrpc "2.0" yes JSON-RPC version.
id text or integer no JSON-RPC correlation ID; never null. See Message handling.
method text no JSON-RPC method name.
params JsonObject no MCP parameters.
result JsonObject no MCP result.
error JsonObject no MCP error with integer code, text message, and optional data.

CvmEvent:

Field Type Required Description
server CvmServerRef yes Server that emitted the event.
message McpMessage yes MCP message delivered by the server.

Discovery

discover(query?) returns public ContextVM server announcements known to the shell. Sources MAY include configured relays, cached announcements, and curated lists. Discovery is not proof of reachability or permission to call a server.

The shell MUST honor supplied kinds and limit filters, including for cached results. An empty kinds list returns no servers; an unsupported kind returns "unsupported kind", rather than silently broadening the search. search is a best-effort text filter. Relay hints remain subject to shell policy.

CvmServer.capabilities names MCP capabilities learned from initialization or CEP-6 announcements, including capability-list content. A tools announcement can establish tools even without CEP-15 tags. The shell MUST NOT substitute i-tag hashes or tool names for MCP capability names. It SHOULD resolve verified CEP-17 relay lists when available, respecting their direction markers and local relay policy. Discovery relay locations alone do not establish a server's operational relay set.

Message handling

request(server, message, options?) sends one MCP JSON-RPC message. The shell MUST validate it before publishing. Missing messages, batches, invalid IDs, and invalid field combinations return "invalid message". The forms follow MCP:

MCP form Fields Completion of the enclosing cvm.request
Request method and id; no result or error Matching MCP response in message.
Notification method, no id, result, or error After transport publication, result without message; API returns null. No MCP response is awaited.
Response id, exactly one of result or error; no method or params After transport publication, result without message; API returns null. No further MCP response is awaited.

The shell MUST preserve the supplied MCP message's form and payload. It MUST NOT add an ID to a notification or rewrite the ID of a napplet's response to a server request. For outgoing requests only, it MAY map IDs to unique transport IDs if it restores the caller's ID in the returned MCP response and consistently maps request references such as cancellation. Publication completion does not prove remote processing. The envelope ID correlates the napplet-to-shell exchange only.

Correlations MUST include the authenticated napplet endpoint, server, session, and direction. Identical IDs from different napplets MUST NOT collide. An ID already used for a client request in the same MCP session returns "invalid message"; a response must match an outstanding server request delivered to that napplet. An unmatched response returns "invalid message". notifications/cancelled may refer only to this napplet's requests. Shell-owned initialization, payment messages, and internal retries MUST use isolated correlation state; raw messages MUST NOT bypass shell policy.

listTools, callTool, listResources, and readResource wrap the corresponding MCP operations using the same request, timeout, and payment rules. readResource MUST return the complete ordered contents list from MCP resources/read, including every text or blob entry. It MUST NOT select only the first entry.

Sessions and events

Session state is scoped to an authenticated napplet endpoint and server. An accepted outbound MCP request or notification establishes that endpoint's participation; discover and onEvent alone do not open a session. The shell MAY initialize automatically and SHOULD initialize when requested or required by the server. Shared transport resources MUST preserve this isolation.

close(server) releases only the calling napplet endpoint's subscriptions, initialization state, pending correlations, and session authorizations. It MUST complete its pending operations with "session closed" before acknowledging the close. It MUST NOT interrupt another napplet's requests or subscriptions. Shared transport teardown is permitted only when no other endpoint needs it. Closing an already closed session succeeds. Endpoint destruction performs the same cleanup; local cleanup does not promise remote cancellation or a payment refund.

onEvent(handler) registers for MCP notifications and server-initiated requests from this endpoint's open sessions. The shell MUST deliver each cvm.event only to its owning endpoint; the binding invokes that endpoint's registered handlers. It MUST NOT broadcast session messages to unrelated napplets. Notifications correlated to a request, such as progress, use that request's owner. Payment control messages are handled by the shell under Payment handling.

A napplet SHOULD register its handler before opening a session. Registration does not create transport state. Without a handler, the binding MAY discard notifications; it MUST NOT redirect messages to another napplet. Server requests still require a response or expire under session policy. Removing a handler via Subscription.close() does not close the server session.

Client identity

The shell selects and controls the ContextVM signing key. Fresh ephemeral keys per napplet endpoint and shell session are RECOMMENDED. The ContextVM client pubkey is not the user's Nostr identity and MUST NOT be presented as such. CEP-16 clientPubkey identifies this transport signer, not the human user.

The shell MUST isolate session negotiation, inbound server requests, paid execution authorizations, and allowance accounting between napplets. If remote state is keyed only by client pubkey, the shell MUST use distinct client keys where sharing would let one napplet consume another's authorization or receive its messages. Sharing a relay connection does not authorize sharing that state. Closing a session MUST prevent its remaining authorization from being reused by another napplet, even if the server retains state after local closure.

Timeouts

timeoutMs defaults to 30000 and MUST be an integer from 1 through 300000. Out-of-range or malformed values return "invalid timeout" before publication. The shell MAY impose a lower policy maximum, but MUST reject a requested duration above that maximum with "timeout out of policy"; it MUST NOT silently clamp it.

The shell MUST enforce the accepted duration from receipt through queueing, initialization, consent, payment, and final response or publication. Retries and progress events MUST NOT reset the deadline. Expiry returns "relay timeout", releases the pending correlation, and prevents further automatic spending or retries for that operation. A late response MUST NOT produce a second result or be redelivered as cvm.event. Timeout does not prove remote cancellation or undo an already initiated transfer.

Projection bindings MUST honor the requested duration and permit delivery of the shell's final result after its deadline, accounting for projection delivery latency. In particular, a fixed 30-second binding timer MUST NOT cut off an accepted 120-second request. This applies equally to convenience wrappers and registry.call; other request/result operations use the default duration.

Payment handling

Payment processing is optional; payment policy enforcement is mandatory. CEP-8 owns pricing, payment methods, negotiation, correlation, and settlement messages. The shell derives paymentRequired from verified pricing information: true when a positive cap price is advertised, false only when known advertised capabilities are explicitly free, and omitted when pricing is unknown. It is an advisory hint, not a quote or guarantee about any invocation. pmi advertises methods, not a price; missing pricing or a pmi tag alone MUST NOT imply free service.

payment defaults to prompt. It constrains the shell as follows:

Option Settlement authority Requested CEP-8 session mode
deny No new payment or consumption of paid authorization for this invocation. Free calls remain usable. explicit_gating preferred.
prompt Explicit user consent for this invocation, with server, operation, amount, and payment method shown by the shell. explicit_gating preferred.
allow Only a standing user allowance configured outside the napplet, covering this napplet, server, operation, method, amount, and remaining budget. No allowance means "payment denied" when payment is needed. transparent permitted; explicit_gating also permitted.

The shell MUST negotiate and track the effective payment_interaction at the session level, following CEP-8 and CEP-35. A request option is not a per-message transport mode. Mode changes MUST follow upstream negotiation and MUST NOT change the lifecycle of another in-flight operation. The shell MAY retain the current mode while enforcing each invocation's policy. If explicit gating is rejected, transparent fallback is permitted only when the shell can enforce the same consent and budget constraints. Otherwise the call fails with "payment required"; fallback MUST NOT grant spending authority.

The shell MUST intercept and correlate -32042 payment gates and notifications/payment_required before delivering application results. It MUST verify the server and the owning invocation, validate the offered amount and method, and apply the table above. These transport messages MUST NOT become a napplet-controlled payment path. A shell without a compatible payment handler returns "payment required" promptly; a policy prohibition or user refusal returns "payment denied". Neither outcome may hang awaiting a payment. The same policy applies to already-paid execution authorizations: the shell MUST check ownership and consent before sending an invocation that could consume one, not just before making a new transfer.

In transparent mode the shell settles an authorized offer and awaits the original response. In explicit gating it settles one authorized offer, then retries the same method and params under the same client identity as prescribed by CEP-8, using a fresh internal request ID and restoring the caller's ID in the final response. It MUST track payment-pending responses and paid authorizations without duplicate settlement. It MUST NOT blindly retry after an uncertain execution: a paid authorization does not guarantee replay of a lost result. Every retry remains within the original timeout and consent or allowance. Concurrent calls MUST reserve allowance budget atomically. A napplet MUST NOT borrow another napplet's paid authorization, including through an otherwise identical call.

The shell MAY reuse a compatible value backend internally. Exposing value to the napplet is not required, and its presence does not guarantee support for a server's payment method. Unsupported payment methods fail as above; this NAP does not define new value rails. Signing keys and settlement credentials remain outside the napplet.

Registry identity and matching

A registry family is a set of CEP-15 tool schema hashes. Its family name is a shell-local label, not portable identity. CvmRegistryEntry.schemaHashes MUST equal the distinct hashes of its tools. There is no aggregate family schemaHash. Hashes are computed and verified by the shell according to CEP-15; NAP-CVM defines no alternative hashing or schema-version algorithm.

registry.list(query?) returns known entries. category uses CEP-15 t tags as best-effort discovery hints. A schemaHash query finds the matching tool across local family names and returns singleton tool entries. Without a hash, family filters by local label. Search and category filters further narrow candidates; the positive limit, when present, bounds the returned count.

registry.has(selector, options?) returns whether a permitted, verified, recently reachable provider satisfies the selector and optional server pin. It returns false for no match or an unavailable registry implementation. registry.describe returns the matching entry and schemas; no match is "schema mismatch" for a hash selector or "family not found" for a label.

A selector's schemaHash is authoritative even if its optional family label is unknown or different on this shell. Hash selection returns a singleton tool entry and MUST NOT require registration under any local family name. Without a hash, the shell selects the complete tool set for the named family. selected and providers refer to providers of that complete returned set. Shells MUST verify tool hashes before exposing them as matching schemas; announcements are candidates, not proof of schema compatibility or liveness.

registry.call(selector, tool, args?, options?) invokes MCP tools/call under the same policy as callTool. The requested tool name MUST match the selected tool's verified schema hash; mismatch returns "schema mismatch" before publication. A supplied server pin MUST be honored; no other provider may substitute. The shell MAY select a different provider only before dispatch and only with the same verified tool hash and no server pin. An uncertain outcome after dispatch MUST NOT trigger automatic replay on another provider.

The shell MAY cache discovery, schemas, and results under user policy. Result cache keys MUST include the tool schema hash, arguments, selected server, and napplet policy scope. Cache hits MUST pass the current policy checks. The cache option is default (local policy), reload (bypass stored results, may store the fresh result), or no-store (neither read nor write result cache). Discovery and schema caches are independent. A prohibited cache choice returns "cache policy denied".

Wire Protocol

cvm.* messages use the NIP-5D wire format ({ "type": "domain.action", ...payload }).

Type Direction Payload fields
cvm.discover napplet -> shell id, query?
cvm.discover.result shell -> napplet id, servers?, error?
cvm.request napplet -> shell id, server, message, options?
cvm.request.result shell -> napplet id, message?, error?
cvm.close napplet -> shell id, server
cvm.close.result shell -> napplet id, error?
cvm.event shell -> napplet server, message
cvm.registry.list napplet -> shell id, query?
cvm.registry.list.result shell -> napplet id, entries?, error?
cvm.registry.has napplet -> shell id, selector, options?
cvm.registry.has.result shell -> napplet id, has?, error?
cvm.registry.describe napplet -> shell id, selector, options?
cvm.registry.describe.result shell -> napplet id, entry?, error?
cvm.registry.call napplet -> shell id, selector, tool, args?, options?
cvm.registry.call.result shell -> napplet id, result?, error?

Key design notes:

  • cvm.request.result correlates to the NIP-5D request id; the embedded MCP message retains its own JSON-RPC id.
  • cvm.event carries no envelope id. Delivery is scoped to the owning napplet endpoint and open session as defined above.
  • The shell owns all ContextVM event IDs, relay subscriptions, p tags, e tags, encryption state, and signing.
  • The shell MAY initialize a ContextVM session automatically before the first capability request.
  • Registry messages are shell-level conveniences. They do not define new ContextVM server behavior.
  • Registry selectors pin a tool with selector.schemaHash; returned entries enumerate the tool set in schemaHashes. Family labels do not override hash matching.

Examples

Hashes and pubkeys in these examples are abbreviated for readability; actual requests use complete values.

Discover servers:

-> { "type": "cvm.discover", "id": "d1", "query": { "search": "relay", "limit": 5 } }
<- {
     "type": "cvm.discover.result",
     "id": "d1",
     "servers": [
       {
         "pubkey": "65a334...",
         "relays": ["wss://relay.example.com"],
         "name": "RelayVM",
         "description": "Relay intelligence exposed through MCP over Nostr"
       }
     ]
   }

Call an MCP tool through ContextVM:

-> {
     "type": "cvm.request",
     "id": "r1",
     "server": { "pubkey": "65a334...", "relays": ["wss://relay.example.com"] },
     "message": {
       "jsonrpc": "2.0",
       "id": 2,
       "method": "tools/call",
       "params": { "name": "get_relay", "arguments": { "url": "wss://relay.example.com" }, "_meta": { "progressToken": "tool-2" } }
     },
     "options": { "initialize": true, "payment": "prompt" }
   }
<- {
     "type": "cvm.request.result",
     "id": "r1",
     "message": {
       "jsonrpc": "2.0",
       "id": 2,
       "result": { "content": [{ "type": "text", "text": "{...}" }], "isError": false }
     }
   }

Server notification:

<- {
     "type": "cvm.event",
     "server": { "pubkey": "65a334..." },
     "message": { "jsonrpc": "2.0", "method": "notifications/progress", "params": { "progressToken": "tool-2", "progress": 50 } }
   }

Browse a local family, then call by portable tool hash:

-> { "type": "cvm.registry.has", "id": "h1", "selector": { "family": "relatr" } }
<- { "type": "cvm.registry.has.result", "id": "h1", "has": true }

-> { "type": "cvm.registry.describe", "id": "s1", "selector": { "family": "relatr" } }
<- {
     "type": "cvm.registry.describe.result",
     "id": "s1",
     "entry": {
       "family": "relatr",
       "schemaHashes": ["a7f3d9..."],
       "selected": { "pubkey": "65a334...", "relays": ["wss://relay.example.com"] },
       "tools": [
         {
           "name": "search_profiles",
           "schemaHash": "a7f3d9...",
           "inputSchema": {
             "type": "object",
             "properties": { "q": { "type": "string" } },
             "required": ["q"]
           }
         }
       ]
     }
   }

-> {
     "type": "cvm.registry.call",
     "id": "c1",
     "selector": { "schemaHash": "a7f3d9..." },
     "tool": "search_profiles",
     "args": { "q": "jack" }
   }
<- {
     "type": "cvm.registry.call.result",
     "id": "c1",
     "result": { "content": [{ "type": "text", "text": "{...}" }], "isError": false }
   }

Publish a notification without creating an MCP request:

Here MCP request 2 is still pending for this napplet.

-> { "type": "cvm.request", "id": "n1", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "method": "notifications/cancelled", "params": { "requestId": 2 } } }
<- { "type": "cvm.request.result", "id": "n1" }

Answer a server request, preserving its ID:

<- { "type": "cvm.event", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "id": "srv-7", "method": "ping" } }
-> { "type": "cvm.request", "id": "answer-1", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "id": "srv-7", "result": {} } }
<- { "type": "cvm.request.result", "id": "answer-1" }

Unsupported registry and denied request:

-> { "type": "cvm.registry.list", "id": "l1" }
<- { "type": "cvm.registry.list.result", "id": "l1", "error": "unsupported method" }
-> { "type": "cvm.registry.has", "id": "h2", "selector": { "family": "relatr" } }
<- { "type": "cvm.registry.has.result", "id": "h2", "has": false }
-> { "type": "cvm.request", "id": "denied-1", "server": { "pubkey": "65a334..." }, "message": { "jsonrpc": "2.0", "id": 3, "method": "tools/list" } }
<- { "type": "cvm.request.result", "id": "denied-1", "error": "policy denied" }

Error Handling

A shell exposing cvm MUST recognize every napplet-to-shell request type in the wire table and return exactly one corresponding .result with the same envelope id, including failures and notification/response publication acknowledgments. Recognized but unimplemented operations return "unsupported method", except registry.has, which returns has: false when registry support is absent. Truly unknown message types retain the silent-ignore behavior defined by NIP-5D.

Shell and transport failures MUST use the top-level text error in that result. There are no cvm.*.error messages; ACL denial returns the matching .result with error: "policy denied". On a shell error, success payload fields MUST be omitted. On success, the operation's payload is required, except for close and notification/response publication, whose result has only type and id.

Error Meaning
invalid message Missing or malformed MCP message, invalid field combination, duplicate client request ID, or unmatched server response.
invalid request Malformed server reference, query, selector, or other non-message parameter.
invalid timeout Timeout is not an integer in the permitted range.
timeout out of policy Valid timeout exceeds the shell's configured maximum.
unsupported method Recognized NAP operation is not implemented.
unsupported kind Requested announcement kind is not supported.
server not found Server cannot be resolved.
relay timeout Operation deadline expired.
initialization failed Required MCP initialization failed.
session closed The calling endpoint released the session while work was pending.
payment required A payment gate cannot be fulfilled with available payment support or negotiation.
payment denied Request policy, allowance, or user decision disallows payment.
policy denied Shell ACL disallows the operation.
family not found Local family label has no match.
schema mismatch Pinned tool hash or tool name cannot be satisfied.
provider unavailable Matching providers are not reachable.
cache policy denied Requested cache behavior is forbidden.

Remote MCP errors MUST remain in message.error for raw request, except for payment control handled above. Convenience wrappers MUST surface remote errors as failures, not successful empty results. A registry tool failure uses its McpToolResult when available; a remote JSON-RPC error becomes a top-level text error describing that failure. Validation errors MUST NOT be reported as "unsupported method".

Shell Behavior

  • The shell MUST sign, encrypt, route, and verify ContextVM events outside the napplet sandbox. Relay delivery alone is not proof of the server's identity.
  • The shell MUST enforce the session, message, timeout, payment, registry, and result rules above, including when a feature is unsupported.
  • The shell MUST NOT expose direct relay sockets, transport tags, or signing authority to a napplet through this domain.
  • Initialization and cached state MUST preserve per-napplet isolation. Advertised MCP client capabilities MUST reflect features the shell and target napplet can actually service; forwarding a server request does not itself implement one.
  • Transport implementation is runtime-owned. The ContextVM SDK or another conforming transport MAY be used; either must preserve these NAP contracts and the linked upstream protocols.

Conformance cases

These cases exercise the contract; they are not implementation test results.

Case Required outcome
A and B call the same server using MCP ID 1; A closes. A's pending call fails with session closed; B's call and events remain usable.
A registers onEvent without an open session; B receives a server request. A receives nothing. Only B can answer the request.
A sends a notification, then answers a server request. Neither awaits an MCP response; notification stays ID-less, response ID is preserved, each envelope receives one acknowledgment.
Missing message, id: null, or both result and error. invalid message in cvm.request.result, with no publication.
Registry is not implemented; an ACL denies a direct request. Registry operations return unsupported method (has returns false); ACL denial uses the matching .result, never .error.
A 120000 ms call returns after 45000 ms. It succeeds if permitted by policy; no fixed 30000 ms binding timeout.
Timeout is zero, above 300000, or above a lower shell maximum. invalid timeout for the first two; timeout out of policy for the last, before publication.
A call expires or closes before its response arrives. Exactly one result; the late response is not an event or a second result.
deny, refused prompt, or allow without sufficient allowance meets a payment gate. payment denied; no new settlement or unauthorized consumption of prior payment.
A shell without payment support receives -32042 or notifications/payment_required. payment required promptly, without a stalled MCP call.
Two napplets make the same paid call concurrently. No shared authorization consumption or budget overrun; session modes remain isolated.
Two shells use different family labels for the same tool hash. The same hash selector resolves on both; returned schemaHashes equals the set of tool hashes.
A tool hash/name mismatches, or a pinned provider is unavailable. No substitute tool or provider is called; schema mismatch or provider unavailable.
resources/read returns several text/blob entries. readResource returns all entries in order.
A tools announcement has no CEP-15 i tags; query restricts announcement kinds. Capability names still derive from MCP data; only the requested supported kinds contribute results.

Security Considerations

  • ContextVM servers are remote programs. Shells SHOULD treat tool calls as untrusted remote execution and enforce per-napplet policy for which servers and methods may be called.
  • MCP tool arguments can contain sensitive user data. Shells SHOULD expose consent or ACL controls before allowing a napplet to send data to a new server.
  • Shells MUST verify server identity by public key. A relay delivering a response is not proof that the response came from the requested server.
  • Shells MUST enforce the timeouts above and SHOULD apply per-napplet concurrency limits to prevent resource exhaustion.
  • Payment requests from ContextVM servers MUST NOT be auto-approved unless covered by an explicit user allowance.
  • Napplets receive MCP results but not ContextVM private keys, encryption keys, NWC secrets, relay credentials, or direct socket access.
  • Registry discovery is not trust. Shells SHOULD apply the same per-napplet policy to registry.call that they apply to direct callTool.
  • Shells SHOULD NOT serve cached registry tool results across napplets when the arguments or selected provider are user-sensitive or policy-scoped.

References

Implementations

  • kehto/web — existing partial implementation of discovery, raw MCP requests, convenience wrappers, sessions, and events. The 2026-10-14 review identifies required runtime corrections and reports registry and payment handling as unimplemented. This is not a claim of conformance to this revision.

Changelog

  • e001eaf - Introduced NAP-CVM as a native ContextVM bridge.
  • 1873f7e - Added a runtime registry facade for discovering CVM surfaces.
  • 69d6bbd - Adopted injected-domain availability and linked the current upstream web binding.
  • 12ad986 - Scoped sessions, client identities, events, and payment authorization per napplet; defined MCP message completion, bounded timeouts, result errors, portable tool-hash registry selection, complete resource reads, discovery semantics, and conformance cases.